From 39b3ae5ecb2ddb8c733beb99c80b68356d203be2 Mon Sep 17 00:00:00 2001 From: xavier Date: Thu, 10 Feb 2022 11:49:09 +0000 Subject: [PATCH] feat(subsonic): implement getTopSongs.view closes #195 --- .gitignore | 3 +- server/ctrlsubsonic/handlers_by_tags.go | 69 ++++++++++++++++++++++--- server/ctrlsubsonic/handlers_common.go | 2 +- server/ctrlsubsonic/spec/spec.go | 5 ++ server/scrobble/lastfm/lastfm.go | 47 ++++++++++++++--- server/server.go | 1 + 6 files changed, 110 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index f9424e8c..1c2d1939 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ dist gonic gonicscan gonicembed -.vscode \ No newline at end of file +.vscode +*.swp diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index 4ed44867..d71e0d96 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -251,10 +251,10 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { return spec.NewError(10, "please provide an `id` parameter") } - artist := &db.Artist{} + var artist db.Artist err = c.DB. Where("id=?", id.Value). - Find(artist). + Find(&artist). Error if errors.Is(err, gorm.ErrRecordNotFound) { return spec.NewError(70, "artist with id `%s` not found", id) @@ -263,16 +263,16 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { sub := spec.NewResponse() sub.ArtistInfoTwo = &spec.ArtistInfo{} if artist.Cover != "" { - sub.ArtistInfoTwo.SmallImageURL = c.genArtistCoverURL(r, artist, 64) - sub.ArtistInfoTwo.MediumImageURL = c.genArtistCoverURL(r, artist, 126) - sub.ArtistInfoTwo.LargeImageURL = c.genArtistCoverURL(r, artist, 256) + sub.ArtistInfoTwo.SmallImageURL = c.genArtistCoverURL(r, &artist, 64) + sub.ArtistInfoTwo.MediumImageURL = c.genArtistCoverURL(r, &artist, 126) + sub.ArtistInfoTwo.LargeImageURL = c.genArtistCoverURL(r, &artist, 256) } apiKey, _ := c.DB.GetSetting("lastfm_api_key") if apiKey == "" { return sub } - info, err := lastfm.ArtistGetInfo(apiKey, artist) + info, err := lastfm.ArtistGetInfo(apiKey, artist.Name) if err != nil { return spec.NewError(0, "fetching artist info: %v", err) } @@ -300,13 +300,13 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response { if i == count { break } - artist = &db.Artist{} + var artist db.Artist err = c.DB. Select("artists.*, count(albums.id) album_count"). Where("name=?", similarInfo.Name). Joins("LEFT JOIN albums ON artists.id=albums.tag_artist_id"). Group("artists.id"). - Find(artist). + Find(&artist). Error if errors.Is(err, gorm.ErrRecordNotFound) && !inclNotPresent { continue @@ -396,3 +396,56 @@ func (c *Controller) genArtistCoverURL(r *http.Request, artist *db.Artist, size return coverURL.String() } + +func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + count := params.GetOrInt("count", 10) + artistName, err := params.Get("artist") + if err != nil { + return spec.NewError(10, "please provide an `artist` parameter") + } + var artist db.Artist + if err := c.DB.Where("name=?", artistName).Find(&artist).Error; err != nil { + return spec.NewError(0, "finding artist by name: %v", err) + } + + apiKey, _ := c.DB.GetSetting("lastfm_api_key") + if apiKey == "" { + return spec.NewResponse() + } + topTracks, err := lastfm.ArtistGetTopTracks(apiKey, artist.Name) + if err != nil { + return spec.NewError(0, "fetching artist top tracks: %v", err) + } + if len(topTracks.Tracks) == 0 { + return spec.NewError(70, "no top tracks found for artist: %v", artist) + } + + topTrackNames := make([]string, len(topTracks.Tracks)) + for i, t := range topTracks.Tracks { + topTrackNames[i] = t.Name + } + + var tracks []*db.Track + err = c.DB. + Preload("Album"). + Where("artist_id=? AND tracks.tag_title IN (?)", artist.ID, topTrackNames). + Limit(count). + Find(&tracks). + Error + if err != nil { + return spec.NewError(0, "error finding tracks: %v", err) + } + if len(tracks) == 0 { + return spec.NewError(70, "no tracks found matchind last fm top songs for artist: %v", artist) + } + + sub := spec.NewResponse() + sub.TopSongs = &spec.TopSongs{ + Tracks: make([]*spec.TrackChild, len(tracks)), + } + for i, track := range tracks { + sub.TopSongs.Tracks[i] = spec.NewTrackByTags(track, track.Album) + } + return sub +} diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 5b6a1ad0..0f5f7a16 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -44,7 +44,7 @@ func (c *Controller) ServeScrobble(r *http.Request) *spec.Response { id, err := params.GetID("id") if err != nil || id.Type != specid.Track { - return spec.NewError(10, "please provide an valid `id` track parameter") + return spec.NewError(10, "please provide a track `id` track parameter") } track := &db.Track{} diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index ed13f153..c0d6db1f 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -47,6 +47,7 @@ type Response struct { Bookmarks *Bookmarks `xml:"bookmarks" json:"bookmarks,omitempty"` Starred *Starred `xml:"starred" json:"starred,omitempty"` StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"` + TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"` } func NewResponse() *Response { @@ -352,3 +353,7 @@ type StarredTwo struct { Albums []*Album `xml:"album,omitempty" json:"album,omitempty"` Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` } + +type TopSongs struct { + Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` +} diff --git a/server/scrobble/lastfm/lastfm.go b/server/scrobble/lastfm/lastfm.go index 2b666083..756eb3b8 100644 --- a/server/scrobble/lastfm/lastfm.go +++ b/server/scrobble/lastfm/lastfm.go @@ -25,11 +25,12 @@ var ( ) type LastFM struct { - XMLName xml.Name `xml:"lfm"` - Status string `xml:"status,attr"` - Session Session `xml:"session"` - Error Error `xml:"error"` - Artist Artist `xml:"artist"` + XMLName xml.Name `xml:"lfm"` + Status string `xml:"status,attr"` + Session Session `xml:"session"` + Error Error `xml:"error"` + Artist Artist `xml:"artist"` + TopTracks TopTracks `xml:"toptracks"` } type Session struct { @@ -77,6 +78,26 @@ type ArtistBio struct { Content string `xml:"content"` } +type TopTracks struct { + XMLName xml.Name `xml:"toptracks"` + Artist string `xml:"artist,attr"` + Tracks []Track `xml:"track"` +} + +type Track struct { + Rank int `xml:"rank,attr"` + Tracks []Track `xml:"track"` + Name string `xml:"name"` + MBID string `xml:"mbid"` + PlayCount int `xml:"playcount"` + Listeners int `xml:"listeners"` + URL string `xml:"url"` + Image []struct { + Text string `xml:",chardata"` + Size string `xml:"size,attr"` + } `xml:"image"` +} + func getParamSignature(params url.Values, secret string) string { // the parameters must be in order before hashing paramKeys := make([]string, 0, len(params)) @@ -113,11 +134,11 @@ func makeRequest(method string, params url.Values) (LastFM, error) { return lastfm, nil } -func ArtistGetInfo(apiKey string, artist *db.Artist) (Artist, error) { +func ArtistGetInfo(apiKey string, artistName string) (Artist, error) { params := url.Values{} params.Add("method", "artist.getInfo") params.Add("api_key", apiKey) - params.Add("artist", artist.Name) + params.Add("artist", artistName) resp, err := makeRequest("GET", params) if err != nil { return Artist{}, fmt.Errorf("making artist GET: %w", err) @@ -125,6 +146,18 @@ func ArtistGetInfo(apiKey string, artist *db.Artist) (Artist, error) { return resp.Artist, nil } +func ArtistGetTopTracks(apiKey, artistName string) (TopTracks, error) { + params := url.Values{} + params.Add("method", "artist.getTopTracks") + params.Add("api_key", apiKey) + params.Add("artist", artistName) + resp, err := makeRequest("GET", params) + if err != nil { + return TopTracks{}, fmt.Errorf("making track GET: %w", err) + } + return resp.TopTracks, nil +} + func GetSession(apiKey, secret, token string) (string, error) { params := url.Values{} params.Add("method", "auth.getSession") diff --git a/server/server.go b/server/server.go index a9075c4a..21ada263 100644 --- a/server/server.go +++ b/server/server.go @@ -217,6 +217,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) { r.Handle("/getBookmarks{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetBookmarks)) r.Handle("/createBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreateBookmark)) r.Handle("/deleteBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteBookmark)) + r.Handle("/getTopSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetTopSongs)) // raw r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))