diff --git a/cmd/scanner/main.go b/cmd/scanner/main.go index 8f04355..cbab2f4 100644 --- a/cmd/scanner/main.go +++ b/cmd/scanner/main.go @@ -48,7 +48,7 @@ var ( type lastAlbum struct { coverModTime time.Time // 1st needed for cover insertion coverPath string // 2rd needed for cover insertion - id uint // 3nd needed for cover insertion + id int // 3nd needed for cover insertion } func (l *lastAlbum) isEmpty() bool { @@ -133,18 +133,18 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error { return fmt.Errorf("when reading tags: %v", err) } trackNumber, totalTracks := tags.Track() - discNumber, TotalDiscs := tags.Disc() + discNumber, totalDiscs := tags.Disc() track.Path = fullPath track.Title = tags.Title() track.Artist = tags.Artist() - track.DiscNumber = uint(discNumber) - track.TotalDiscs = uint(TotalDiscs) - track.TotalTracks = uint(totalTracks) - track.TrackNumber = uint(trackNumber) - track.Year = uint(tags.Year()) + track.DiscNumber = discNumber + track.TotalDiscs = totalDiscs + track.TotalTracks = totalTracks + track.TrackNumber = trackNumber + track.Year = tags.Year() track.Suffix = extension track.ContentType = mime - track.Size = uint(stat.Size()) + track.Size = int(stat.Size()) // set album artist { albumArtist := db.AlbumArtist{ Name: tags.AlbumArtist(), @@ -188,6 +188,7 @@ func main() { &db.Cover{}, &db.User{}, &db.Setting{}, + &db.Play{}, ) // 🤫🤫🤫 orm.Exec(` diff --git a/cmd/server/main.go b/cmd/server/main.go index 135d6df..f687334 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -46,6 +46,8 @@ func setSubsonicRoutes(mux *http.ServeMux) { mux.HandleFunc("/rest/stream.view", withWare(cont.Stream)) mux.HandleFunc("/rest/download", withWare(cont.Stream)) mux.HandleFunc("/rest/download.view", withWare(cont.Stream)) + mux.HandleFunc("/rest/scrobble", withWare(cont.Scrobble)) + mux.HandleFunc("/rest/scrobble.view", withWare(cont.Scrobble)) mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt)) mux.HandleFunc("/rest/getArtists", withWare(cont.GetArtists)) diff --git a/db/base.go b/db/base.go index 8c31991..759ad21 100644 --- a/db/base.go +++ b/db/base.go @@ -11,11 +11,5 @@ type CrudBase struct { } type IDBase struct { - ID uint `gorm:"primary_key"` -} - -// Base is the base model with an auto incrementing primary key -type Base struct { - IDBase - CrudBase + ID int `gorm:"primary_key"` } diff --git a/db/model.go b/db/model.go index 84e39f5..975d430 100644 --- a/db/model.go +++ b/db/model.go @@ -1,48 +1,53 @@ package db +import "time" + // Album represents the albums table type Album struct { - Base + IDBase + CrudBase AlbumArtist AlbumArtist - AlbumArtistID uint + AlbumArtistID int Title string `gorm:"not null;index"` Tracks []Track } // AlbumArtist represents the AlbumArtists table type AlbumArtist struct { - Base + IDBase + CrudBase Albums []Album Name string `gorm:"not null;unique_index"` } // Track represents the tracks table type Track struct { - Base + IDBase + CrudBase Album Album - AlbumID uint + AlbumID int AlbumArtist AlbumArtist - AlbumArtistID uint + AlbumArtistID int Artist string - Bitrate uint + Bitrate int Codec string - DiscNumber uint - Duration uint + DiscNumber int + Duration int Title string - TotalDiscs uint - TotalTracks uint - TrackNumber uint - Year uint + TotalDiscs int + TotalTracks int + TrackNumber int + Year int Suffix string ContentType string - Size uint + Size int Path string `gorm:"not null;unique_index"` } // Cover represents the covers table type Cover struct { CrudBase - AlbumID uint `gorm:"primary_key;auto_increment:false"` + AlbumID int `gorm:"primary_key;auto_increment:false"` Album Album Image []byte Path string `gorm:"not null;unique_index"` @@ -50,7 +55,8 @@ type Cover struct { // User represents the users table type User struct { - Base + IDBase + CrudBase Name string `gorm:"not null;unique_index"` Password string LastFMSession string @@ -63,3 +69,13 @@ type Setting struct { Key string `gorm:"primary_key;auto_increment:false"` Value string } + +// Play represents the settings table +type Play struct { + IDBase + User User + UserID int + Track Track + TrackID int + Time time.Time +} diff --git a/handler/handler.go b/handler/handler.go index d87c43e..4547aed 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -84,9 +84,9 @@ type templateData struct { User *db.User SelectedUser *db.User AllUsers []*db.User - ArtistCount uint - AlbumCount uint - TrackCount uint + ArtistCount int + AlbumCount int + TrackCount int CurrentLastFMAPIKey string CurrentLastFMAPISecret string RequestRoot string @@ -96,6 +96,14 @@ func getStrParam(r *http.Request, key string) string { return r.URL.Query().Get(key) } +func getStrParamOr(r *http.Request, key, or string) string { + val := getStrParam(r, key) + if val == "" { + return or + } + return val +} + func getIntParam(r *http.Request, key string) (int, error) { strVal := r.URL.Query().Get(key) if strVal == "" { @@ -156,7 +164,7 @@ func respond(w http.ResponseWriter, r *http.Request, } func respondError(w http.ResponseWriter, r *http.Request, - code uint64, message string) { + code int, message string) { respondRaw(w, r, http.StatusBadRequest, subsonic.NewError( code, message, )) diff --git a/handler/media.go b/handler/media.go index 37d678b..90132e7 100644 --- a/handler/media.go +++ b/handler/media.go @@ -4,10 +4,12 @@ import ( "fmt" "net/http" "os" + "time" "unicode" "github.com/jinzhu/gorm" "github.com/sentriz/gonic/db" + "github.com/sentriz/gonic/lastfm" "github.com/sentriz/gonic/subsonic" "github.com/mozillazg/go-unidecode" @@ -210,6 +212,51 @@ func (c *Controller) GetLicence(w http.ResponseWriter, r *http.Request) { respond(w, r, sub) } +func (c *Controller) Scrobble(w http.ResponseWriter, r *http.Request) { + id, err := getIntParam(r, "id") + if err != nil { + respondError(w, r, 10, "please provide an `id` parameter") + return + } + // fetch user to get lastfm session + username := getStrParam(r, "u") + user := c.GetUserFromName(username) + if user == nil { + respondError(w, r, 10, "could not find a user with that name") + return + } + if user.LastFMSession == "" { + respondError(w, r, 0, fmt.Sprintf("no last.fm session for this user: %v", err)) + return + } + // fetch track for getting info to send to last.fm function + var track db.Track + c.DB. + Preload("Album"). + Preload("AlbumArtist"). + First(&track, id) + // get time from args or use now + time := getIntParamOr(r, "time", int(time.Now().Unix())) + // get submission, where the default is true. we will + // check if it's false later + submission := getStrParamOr(r, "submission", "true") + // scrobble with above info + err = lastfm.Scrobble( + c.GetSetting("lastfm_api_key"), + c.GetSetting("lastfm_secret"), + user.LastFMSession, + &track, + time, + submission != "false", + ) + if err != nil { + respondError(w, r, 0, fmt.Sprintf("error when submitting: %v", err)) + return + } + sub := subsonic.NewResponse() + respond(w, r, sub) +} + func (c *Controller) NotFound(w http.ResponseWriter, r *http.Request) { respondError(w, r, 0, "unknown route") } diff --git a/lastfm/lastfm.go b/lastfm/lastfm.go index 9a15eaf..64c5e43 100644 --- a/lastfm/lastfm.go +++ b/lastfm/lastfm.go @@ -7,7 +7,11 @@ import ( "fmt" "net/http" "net/url" + "sort" + "strconv" "time" + + "github.com/sentriz/gonic/db" ) var ( @@ -18,38 +22,72 @@ var ( ) func getParamSignature(params url.Values, secret string) string { + // the parameters must be in order before hashing + paramKeys := make([]string, 0) + for k, _ := range params { + paramKeys = append(paramKeys, k) + } + sort.Strings(paramKeys) toHash := "" - for k, v := range params { + for _, k := range paramKeys { toHash += k - toHash += v[0] + toHash += params[k][0] } toHash += secret hash := md5.Sum([]byte(toHash)) return hex.EncodeToString(hash[:]) } -func GetSession(apiKey, secret, token string) (string, error) { - params := url.Values{} - // the first 3 parameters here must be in alphabetical order - params.Add("api_key", apiKey) - params.Add("method", "auth.getSession") - params.Add("token", token) - params.Add("api_sig", getParamSignature(params, secret)) - req, _ := http.NewRequest("GET", baseURL, nil) +func makeRequest(method string, params url.Values) (*LastFM, error) { + req, _ := http.NewRequest(method, baseURL, nil) req.URL.RawQuery = params.Encode() resp, err := client.Do(req) if err != nil { - return "", fmt.Errorf("error when making request to last.fm: %v", err) + return nil, fmt.Errorf("get: %v", err) } defer resp.Body.Close() decoder := xml.NewDecoder(resp.Body) var lastfm LastFM err = decoder.Decode(&lastfm) if err != nil { - return "", fmt.Errorf("error when decoding last.fm response: %v", err) + return nil, fmt.Errorf("decoding: %v", err) } if lastfm.Error != nil { - return "", fmt.Errorf("error when parsing last.fm response: %v", lastfm.Error.Value) + return nil, fmt.Errorf("parsing: %v", lastfm.Error.Value) } - return lastfm.Session.Key, nil + return &lastfm, nil +} + +func GetSession(apiKey, secret, token string) (string, error) { + params := url.Values{} + params.Add("method", "auth.getSession") + params.Add("api_key", apiKey) + params.Add("token", token) + params.Add("api_sig", getParamSignature(params, secret)) + resp, err := makeRequest("GET", params) + if err != nil { + return "", fmt.Errorf("error when getting session step '%v'", err) + } + return resp.Session.Key, nil +} + +func Scrobble(apiKey, secret, session string, + track *db.Track, stamp int, submission bool) error { + params := url.Values{} + if submission { + params.Add("method", "track.Scrobble") + params.Add("timestamp", strconv.Itoa(stamp)) + } else { + params.Add("method", "track.updateNowPlaying") + } + params.Add("api_key", apiKey) + params.Add("sk", session) + params.Add("artist", track.Artist) + params.Add("track", track.Title) + params.Add("album", track.Album.Title) + params.Add("albumArtist", track.AlbumArtist.Name) + params.Add("trackNumber", strconv.Itoa(track.TrackNumber)) + params.Add("api_sig", getParamSignature(params, secret)) + _, err := makeRequest("POST", params) + return err } diff --git a/static/images/favicon.ico b/static/images/favicon.ico new file mode 100644 index 0000000..f86b050 Binary files /dev/null and b/static/images/favicon.ico differ diff --git a/subsonic/media.go b/subsonic/media.go index 15bf592..d30ad8a 100644 --- a/subsonic/media.go +++ b/subsonic/media.go @@ -3,13 +3,13 @@ package subsonic import "time" type Album struct { - ID uint `xml:"id,attr" json:"id"` + ID int `xml:"id,attr" json:"id"` Name string `xml:"name,attr" json:"name"` - ArtistID uint `xml:"artistId,attr" json:"artistId"` + ArtistID int `xml:"artistId,attr" json:"artistId"` Artist string `xml:"artist,attr" json:"artist"` - TrackCount uint `xml:"songCount,attr" json:"songCount"` - Duration uint `xml:"duration,attr" json:"duration"` - CoverID uint `xml:"coverArt,attr" json:"coverArt"` + TrackCount int `xml:"songCount,attr" json:"songCount"` + Duration int `xml:"duration,attr" json:"duration"` + CoverID int `xml:"coverArt,attr" json:"coverArt"` Created time.Time `xml:"created,attr" json:"created"` Tracks []*Track `xml:"song" json:"song,omitempty"` } @@ -19,38 +19,38 @@ type RandomTracks struct { } type Track struct { - ID uint `xml:"id,attr" json:"id"` - Parent uint `xml:"parent,attr" json:"parent"` + ID int `xml:"id,attr" json:"id"` + Parent int `xml:"parent,attr" json:"parent"` Title string `xml:"title,attr" json:"title"` Album string `xml:"album,attr" json:"album"` Artist string `xml:"artist,attr" json:"artist"` IsDir bool `xml:"isDir,attr" json:"isDir"` - CoverID uint `xml:"coverArt,attr" json:"coverArt"` + CoverID int `xml:"coverArt,attr" json:"coverArt"` Created time.Time `xml:"created,attr" json:"created"` - Duration uint `xml:"duration,attr" json:"duration"` + Duration int `xml:"duration,attr" json:"duration"` Genre string `xml:"genre,attr" json:"genre"` - BitRate uint `xml:"bitRate,attr" json:"bitRate"` - Size uint `xml:"size,attr" json:"size"` + BitRate int `xml:"bitRate,attr" json:"bitRate"` + Size int `xml:"size,attr" json:"size"` Suffix string `xml:"suffix,attr" json:"suffix"` ContentType string `xml:"contentType,attr" json:"contentType"` IsVideo bool `xml:"isVideo,attr" json:"isVideo"` Path string `xml:"path,attr" json:"path"` - AlbumID uint `xml:"albumId,attr" json:"albumId"` - ArtistID uint `xml:"artistId,attr" json:"artistId"` - TrackNo uint `xml:"track,attr" json:"track"` + AlbumID int `xml:"albumId,attr" json:"albumId"` + ArtistID int `xml:"artistId,attr" json:"artistId"` + TrackNo int `xml:"track,attr" json:"track"` Type string `xml:"type,attr" json:"type"` } type Artist struct { - ID uint `xml:"id,attr" json:"id"` + ID int `xml:"id,attr" json:"id"` Name string `xml:"name,attr" json:"name"` - CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"` - AlbumCount uint `xml:"albumCount,attr" json:"albumCount,omitempty"` + CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"` + AlbumCount int `xml:"albumCount,attr" json:"albumCount,omitempty"` Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` } type Indexes struct { - LastModified uint `xml:"lastModified,attr" json:"lastModified"` + LastModified int `xml:"lastModified,attr" json:"lastModified"` Index []*Index `xml:"index" json:"index"` } @@ -60,36 +60,36 @@ type Index struct { } type Directory struct { - ID uint `xml:"id,attr" json:"id"` - Parent uint `xml:"parent,attr" json:"parent"` + ID int `xml:"id,attr" json:"id"` + Parent int `xml:"parent,attr" json:"parent"` Name string `xml:"name,attr" json:"name"` Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` Children []Child `xml:"child" json:"child"` } type Child struct { - ID uint `xml:"id,attr" json:"id,omitempty"` - Parent uint `xml:"parent,attr" json:"parent,omitempty"` + ID int `xml:"id,attr" json:"id,omitempty"` + Parent int `xml:"parent,attr" json:"parent,omitempty"` Title string `xml:"title,attr" json:"title,omitempty"` IsDir bool `xml:"isDir,attr" json:"isDir,omitempty"` Album string `xml:"album,attr,omitempty" json:"album,omitempty"` - AlbumID uint `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` + AlbumID int `xml:"albumId,attr,omitempty" json:"albumId,omitempty"` Artist string `xml:"artist,attr,omitempty" json:"artist,omitempty"` - ArtistID uint `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` - Track uint `xml:"track,attr,omitempty" json:"track,omitempty"` - Year uint `xml:"year,attr,omitempty" json:"year,omitempty"` + ArtistID int `xml:"artistId,attr,omitempty" json:"artistId,omitempty"` + Track int `xml:"track,attr,omitempty" json:"track,omitempty"` + Year int `xml:"year,attr,omitempty" json:"year,omitempty"` Genre string `xml:"genre,attr,omitempty" json:"genre,omitempty"` - CoverID uint `xml:"coverArt,attr" json:"coverArt,omitempty"` - Size uint `xml:"size,attr,omitempty" json:"size,omitempty"` + CoverID int `xml:"coverArt,attr" json:"coverArt,omitempty"` + Size int `xml:"size,attr,omitempty" json:"size,omitempty"` ContentType string `xml:"contentType,attr,omitempty" json:"contentType,omitempty"` Suffix string `xml:"suffix,attr,omitempty" json:"suffix,omitempty"` - Duration uint `xml:"duration,attr,omitempty" json:"duration"` - BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"` + Duration int `xml:"duration,attr,omitempty" json:"duration"` + BitRate int `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"` } type MusicFolder struct { - ID uint `xml:"id,attr" json:"id,omitempty"` + ID int `xml:"id,attr" json:"id,omitempty"` Name string `xml:"name,attr" json:"name,omitempty"` } diff --git a/subsonic/response.go b/subsonic/response.go index b8a4f7b..a549a27 100644 --- a/subsonic/response.go +++ b/subsonic/response.go @@ -32,7 +32,7 @@ type Response struct { } type Error struct { - Code uint64 `xml:"code,attr" json:"code"` + Code int `xml:"code,attr" json:"code"` Message string `xml:"message,attr" json:"message"` } @@ -44,7 +44,7 @@ func NewResponse() *Response { } } -func NewError(code uint64, message string) *Response { +func NewError(code int, message string) *Response { return &Response{ Status: "failed", XMLNS: xmlns, diff --git a/templates/layout.tmpl b/templates/layout.tmpl index 6a38995..8ab6c37 100644 --- a/templates/layout.tmpl +++ b/templates/layout.tmpl @@ -6,6 +6,8 @@ {{ template "title" }} + + diff --git a/templates/pages/home.tmpl b/templates/pages/home.tmpl index 67f27c3..3adccdd 100644 --- a/templates/pages/home.tmpl +++ b/templates/pages/home.tmpl @@ -38,7 +38,7 @@
{{ range $user := .AllUsers }} - {{ $user.Name }} created {{ $user.CreatedAt.Format "Jan 02, 2006" }} change password
+ {{ $user.Name }} created {{ $user.CreatedAt.Format "Jan 02, 2006" }} change password
{{ end }} create new