From 1094f2da2145fe908c2673dfe4960e2ae663558e Mon Sep 17 00:00:00 2001 From: sentriz Date: Thu, 18 Apr 2019 20:05:24 +0100 Subject: [PATCH] add scrobblingt --- cmd/scanner/main.go | 17 +++++----- cmd/server/main.go | 2 ++ db/base.go | 8 +---- db/model.go | 48 ++++++++++++++++++--------- handler/handler.go | 16 ++++++--- handler/media.go | 47 +++++++++++++++++++++++++++ lastfm/lastfm.go | 66 ++++++++++++++++++++++++++++++-------- static/images/favicon.ico | Bin 0 -> 1406 bytes subsonic/media.go | 62 +++++++++++++++++------------------ subsonic/response.go | 4 +-- templates/layout.tmpl | 2 ++ templates/pages/home.tmpl | 2 +- 12 files changed, 191 insertions(+), 83 deletions(-) create mode 100644 static/images/favicon.ico 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 0000000000000000000000000000000000000000..f86b050b926c1a69afd012a360f3190d96d3d1f9 GIT binary patch literal 1406 zcmd^0s|v61B`4AoN;1rRvCp1402`p`LusJ{pJ7NcfRj??|tXpd*8d~J#Z7l#|JU@ zC)yMIfG`2!B3R&t|E9S8L;uyMRkjlMo1H!7^?0T^m|AfK?`LRA1g~Zcm zSX1s`e^otwm%H$8uPZ5F%K%s}w&~ z3m)MScx$h*Fufd;vlTvb3paUvI1F zP^d+XW_%63$kJ>ywslZsZ6hhWkVyqC$nrWkVzT0~PDw;Y4Kwns4A5Lep5p?gg$=bA zk?F25K;O+KRWVt0Erez}kr#9@O!)naI*BQ@Fe=Z*;#vu>!)F+H$U^7|k;ih5^15a= z7uOS5-p7Oj7q<2;W@Vb0oo+vV;=WDl{)5y$-{QdIP~2taL@mF{Q8#`JvR5X z_t(ApEk{3ierI$*-oi(AOj2wJdogFZ*L>M3#XavmdtcC??;1LA?dq>itobDU ohj){vE?Ih`j~m}O{L82ZzL(!hPdAD`@mBEfSQkXu;wjw3b{{ 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