diff --git a/server/ctrlsubsonic/handlers_by_tags.go b/server/ctrlsubsonic/handlers_by_tags.go index ae702871..9fec4dba 100644 --- a/server/ctrlsubsonic/handlers_by_tags.go +++ b/server/ctrlsubsonic/handlers_by_tags.go @@ -512,3 +512,64 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response { } return sub } + +func (c *Controller) ServeGetSimilarSongsTwo(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.Artist { + return spec.NewError(10, "please provide an artist `id` parameter") + } + + apiKey, _ := c.DB.GetSetting("lastfm_api_key") + if apiKey == "" { + return spec.NewResponse() + } + + var artist db.Artist + err = c.DB. + Where("id=?", id.Value). + First(&artist). + Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return spec.NewError(0, "artist with id `%s` not found", id) + } + + similarArtists, err := lastfm.ArtistGetSimilar(apiKey, artist.Name) + if err != nil { + return spec.NewError(0, "fetching artist similar artists: %v", err) + } + if len(similarArtists.Artists) == 0 { + return spec.NewError(0, "no similar artist found for: %v", artist.Name) + } + + artistNames := make([]string, len(similarArtists.Artists)) + for i, similarArtist := range similarArtists.Artists { + artistNames[i] = similarArtist.Name + } + + var tracks []*db.Track + err = c.DB. + Preload("Album"). + Joins("JOIN artists on tracks.artist_id=artists.id"). + Where("artists.name IN (?)", artistNames). + 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", artist.Name) + } + + sub := spec.NewResponse() + sub.SimilarSongsTwo = &spec.SimilarSongsTwo{ + Tracks: make([]*spec.TrackChild, len(tracks)), + } + for i, track := range tracks { + sub.SimilarSongsTwo.Tracks[i] = spec.NewTrackByTags(track, track.Album) + } + return sub +} diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index f83f859a..3392a29e 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -49,6 +49,7 @@ type Response struct { StarredTwo *StarredTwo `xml:"starred2" json:"starred2,omitempty"` TopSongs *TopSongs `xml:"topSongs" json:"topSongs,omitempty"` SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"` + SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"` } func NewResponse() *Response { @@ -362,3 +363,7 @@ type TopSongs struct { type SimilarSongs struct { Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` } + +type SimilarSongsTwo struct { + Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"` +} diff --git a/server/scrobble/lastfm/lastfm.go b/server/scrobble/lastfm/lastfm.go index 0ad1b7b2..e5cd0ade 100644 --- a/server/scrobble/lastfm/lastfm.go +++ b/server/scrobble/lastfm/lastfm.go @@ -32,6 +32,7 @@ type LastFM struct { Artist Artist `xml:"artist"` TopTracks TopTracks `xml:"toptracks"` SimilarTracks SimilarTracks `xml:"similartracks"` + SimilarArtists SimilarArtists `xml:"similarartists"` } type Session struct { @@ -45,6 +46,18 @@ type Error struct { Value string `xml:",chardata"` } +type SimilarArtist struct { + XMLName xml.Name `xml:"artist"` + Name string `xml:"name"` + MBID string `xml:"mbid"` + URL string `xml:"url"` + Image []struct { + Text string `xml:",chardata"` + Size string `xml:"size,attr"` + } `xml:"image"` + Streamable string `xml:"streamable"` +} + type Artist struct { XMLName xml.Name `xml:"artist"` Name string `xml:"name"` @@ -92,6 +105,12 @@ type SimilarTracks struct { Tracks []Track `xml:"track"` } +type SimilarArtists struct { + XMLName xml.Name `xml:"similarartists"` + Artist string `xml:"artist,attr"` + Artists []Artist `xml:"artist"` +} + type Track struct { Rank int `xml:"rank,attr"` Tracks []Track `xml:"track"` @@ -178,6 +197,19 @@ func TrackGetSimilarTracks(apiKey string, artistName, trackName string) (Similar } return resp.SimilarTracks, nil } + +func ArtistGetSimilar(apiKey string, artistName string) (SimilarArtists, error) { + params := url.Values{} + params.Add("method", "artist.getSimilar") + params.Add("api_key", apiKey) + params.Add("artist", artistName) + resp, err := makeRequest("GET", params) + if err != nil { + return SimilarArtists{}, fmt.Errorf("making similar artists GET: %w", err) + } + return resp.SimilarArtists, 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 12b0fad5..5945d76a 100644 --- a/server/server.go +++ b/server/server.go @@ -219,6 +219,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) { r.Handle("/deleteBookmark{_:(?:\\.view)?}", ctrl.H(ctrl.ServeDeleteBookmark)) r.Handle("/getTopSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetTopSongs)) r.Handle("/getSimilarSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSimilarSongs)) + r.Handle("/getSimilarSongs2{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSimilarSongsTwo)) // raw r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload))