Skip to content

Commit

Permalink
feat(subsonic): implement getTopSongs.view
Browse files Browse the repository at this point in the history
closes #195
  • Loading branch information
xavier authored and sentriz committed Feb 10, 2022
1 parent 3d37737 commit 39b3ae5
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -13,4 +13,5 @@ dist
gonic
gonicscan
gonicembed
.vscode
.vscode
*.swp
69 changes: 61 additions & 8 deletions server/ctrlsubsonic/handlers_by_tags.go
Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion server/ctrlsubsonic/handlers_common.go
Expand Up @@ -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{}
Expand Down
5 changes: 5 additions & 0 deletions server/ctrlsubsonic/spec/spec.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
}
47 changes: 40 additions & 7 deletions server/scrobble/lastfm/lastfm.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -113,18 +134,30 @@ 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)
}
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")
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Expand Up @@ -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))
Expand Down

0 comments on commit 39b3ae5

Please sign in to comment.