Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/get similar songs 2, Feat/get similar songs, Feat/get top songs #195

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -13,4 +13,5 @@ dist
gonic
gonicscan
gonicembed
.vscode
.vscode
*.swp
181 changes: 181 additions & 0 deletions server/ctrlsubsonic/handlers_by_tags.go
Expand Up @@ -403,3 +403,184 @@ func (c *Controller) genAlbumCoverURL(r *http.Request, folder *db.Album, size in

return coverURL.String()
}

func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params)
count := params.GetOrInt("count", 10)
artist, err := params.Get("artist")
if err != nil {
return spec.NewError(10, "please provide an `artist` parameter")
}

sub := spec.NewResponse()
apiKey, _ := c.DB.GetSetting("lastfm_api_key")
if apiKey == "" {
return sub
}

topTracks, err := lastfm.ArtistGetTopTracks(apiKey, artist)
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
q := c.DB.
Preload("Artist").
Preload("Album").
Select("tracks.*").
Where("tracks.tag_title IN ( ? )", topTrackNames)

if err := q.Limit(count).Find(&tracks).Error; 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.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
}

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 {
return spec.NewError(10, "please provide an `id` parameter")
}

sub := spec.NewResponse()
apiKey, _ := c.DB.GetSetting("lastfm_api_key")
if apiKey == "" {
return sub
}

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
q := c.DB.
Preload("Artist").
Preload("Album").
Select("tracks.*").
Where("tracks.tag_title IN ( ? )", similarTrackNames).
Order("RANDOM()").Limit(count)

if err := q.Find(&tracks).Error; 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.SimilarSongs = &spec.SimilarSongs{
Tracks: make([]*spec.TrackChild, count),
}

for i, track := range tracks {
sub.SimilarSongs.Tracks[i] = spec.NewTrackByTags(track, track.Album)
}

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 {
return spec.NewError(10, "please provide an `id` parameter")
}

sub := spec.NewResponse()
apiKey, _ := c.DB.GetSetting("lastfm_api_key")
if apiKey == "" {
return sub
}

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
}

tracks := []*db.Track{}
q := c.DB.
Preload("Album").
Joins("JOIN artists on tracks.artist_id = artists.id").
Where("artists.name IN (?)", artistNames).
Order("RANDOM()").Limit(count)

if err := q.Find(&tracks).Error; 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.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
}
15 changes: 15 additions & 0 deletions server/ctrlsubsonic/spec/spec.go
Expand Up @@ -47,6 +47,9 @@ 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"`
SimilarSongs *SimilarSongs `xml:"similarSongs" json:"similarSongs,omitempty"`
SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"`
}

func NewResponse() *Response {
Expand Down Expand Up @@ -352,3 +355,15 @@ 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"`
}

type SimilarSongs struct {
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
}

type SimilarSongsTwo struct {
Tracks []*TrackChild `xml:"song,omitempty" json:"song,omitempty"`
}
99 changes: 94 additions & 5 deletions server/scrobble/lastfm/lastfm.go
Expand Up @@ -25,11 +25,14 @@ 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"`
SimilarTracks SimilarTracks `xml:"similartracks"`
SimilarArtists SimilarArtists `xml:"similarartists"`
}

type Session struct {
Expand All @@ -43,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"`
Expand Down Expand Up @@ -77,6 +92,39 @@ 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 SimilarTracks struct {
XMLName xml.Name `xml:"similartracks"`
Artist string `xml:"artist,attr"`
Track string `xml:"track,attr"`
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"`
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 @@ -125,6 +173,47 @@ 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 TrackGetSimilarTracks(apiKey, artistName, trackTitle string) (SimilarTracks, error) {
params := url.Values{}
params.Add("method", "track.getSimilar")
params.Add("api_key", apiKey)
params.Add("track", trackTitle)
params.Add("artist", artistName)

resp, err := makeRequest("GET", params)
if err != nil {
return SimilarTracks{}, fmt.Errorf("making track GET: %s %s %w", trackTitle, artistName, err)
}

return resp.SimilarTracks, nil
}

func ArtistGetSimilar(apiKey, 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")
Expand Down
3 changes: 3 additions & 0 deletions server/server.go
Expand Up @@ -217,6 +217,9 @@ 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))
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))
Expand Down