diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index d71e0d96..ae702871 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -449,3 +449,66 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response { } return sub } + +func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + count := params.GetOrInt("count", 10) + id, err := params.GetID("id") + if err != nil || id.Type != specid.Track { + return spec.NewError(10, "please provide an track `id` parameter") + } + apiKey, _ := c.DB.GetSetting("lastfm_api_key") + if apiKey == "" { + return spec.NewResponse() + } + + var track db.Track + err = c.DB. + Preload("Artist"). + Preload("Album"). + Where("id=?", id.Value). + First(&track). + Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return spec.NewError(10, "couldn't find a track with that id") + } + + similarTracks, err := lastfm.TrackGetSimilarTracks(apiKey, track.Artist.Name, track.TagTitle) + if err != nil { + return spec.NewError(0, "fetching track similar tracks: %v", err) + } + if len(similarTracks.Tracks) == 0 { + return spec.NewError(70, "no similar songs found for track: %v", track.TagTitle) + } + + similarTrackNames := make([]string, len(similarTracks.Tracks)) + for i, t := range similarTracks.Tracks { + similarTrackNames[i] = t.Name + } + + var tracks []*db.Track + err = c.DB. + Preload("Artist"). + Preload("Album"). + Select("tracks.*"). + Where("tracks.tag_title IN (?)", similarTrackNames). + Order(gorm.Expr("random()")). + 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 similar song could be match with collection in database: %v", track.TagTitle) + } + + sub := spec.NewResponse() + sub.SimilarSongs = &spec.SimilarSongs{ + Tracks: make([]*spec.TrackChild, len(tracks)), + } + for i, track := range tracks { + sub.SimilarSongs.Tracks[i] = spec.NewTrackByTags(track, track.Album) + } + return sub +} diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index c0d6db1f..f83f859a 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -48,6 +48,7 @@ type Response struct { Starred *Starred `xml:"starred" json:"starred,omitempty"` StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"` TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"` + SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"` } func NewResponse() *Response { @@ -357,3 +358,7 @@ type StarredTwo struct { type TopSongs struct { Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` } + +type SimilarSongs struct { + Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` +} diff --git a/server/scrobble/lastfm/lastfm.go b/server/scrobble/lastfm/lastfm.go index 756eb3b8..0ad1b7b2 100644 --- a/server/scrobble/lastfm/lastfm.go +++ b/server/scrobble/lastfm/lastfm.go @@ -31,6 +31,7 @@ type LastFM struct { Error Error `xml:"error"` Artist Artist `xml:"artist"` TopTracks TopTracks `xml:"toptracks"` + SimilarTracks SimilarTracks `xml:"similartracks"` } type Session struct { @@ -84,6 +85,13 @@ type TopTracks struct { Tracks []Track `xml:"track"` } +type SimilarTracks struct { + XMLName xml.Name `xml:"similartracks"` + Artist string `xml:"artist,attr"` + Track string `xml:"track,attr"` + Tracks []Track `xml:"track"` +} + type Track struct { Rank int `xml:"rank,attr"` Tracks []Track `xml:"track"` @@ -158,6 +166,18 @@ func ArtistGetTopTracks(apiKey, artistName string) (TopTracks, error) { return resp.TopTracks, nil } +func TrackGetSimilarTracks(apiKey string, artistName, trackName string) (SimilarTracks, error) { + params := url.Values{} + params.Add("method", "track.getSimilar") + params.Add("api_key", apiKey) + params.Add("track", trackName) + params.Add("artist", artistName) + resp, err := makeRequest("GET", params) + if err != nil { + return SimilarTracks{}, fmt.Errorf("making track GET: %w", err) + } + return resp.SimilarTracks, 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 21ada263..12b0fad5 100644 --- a/server/server.go +++ b/server/server.go @@ -218,6 +218,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) { r.Handle("/createBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeCreateBookmark)) r.Handle("/deleteBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteBookmark)) r.Handle("/getTopSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetTopSongs)) + r.Handle("/getSimilarSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSimilarSongs)) // raw r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))