fix(subsonic): Sort songs by presence of lyrics for getLyrics (#4237)
* fix(subsonic): Sort songs by presence of lyrics for `getLyrics` The current implementation of `getLyrics` fetches any songs matching the artist and title. However, this misses a case where there may be multiple matches for the same artist/song, and one has lyrics while the other doesn't. Resolve this by adding a custom SQL dynamic column that checks for the presence of lyrics. * add options to selectMediaFile, update test * more robust testing of GetAllByLyrics * fix(subsonic): refactor GetAllByLyrics to GetAll with lyrics sorting Signed-off-by: Deluan <deluan@navidrome.org> * use has_lyrics, and properly support multiple sort parts * better handle complicated internal sorts * just use a simpler filter * add note to setSortMappings * remove custom sort mapping, improve test with different updatedat * refactor tests and mock Signed-off-by: Deluan <deluan@navidrome.org> * default order when not specified is `asc` Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -108,9 +108,9 @@ func SongsByRandom(genre string, fromYear, toYear int) Options {
|
||||
return addDefaultFilters(options)
|
||||
}
|
||||
|
||||
func SongWithArtistTitle(artist, title string) Options {
|
||||
func SongsByArtistTitleWithLyricsFirst(artist, title string) Options {
|
||||
return addDefaultFilters(Options{
|
||||
Sort: "updated_at",
|
||||
Sort: "lyrics, updated_at",
|
||||
Order: "desc",
|
||||
Max: 1,
|
||||
Filters: And{
|
||||
|
||||
@@ -98,7 +98,7 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
||||
response := newResponse()
|
||||
lyricsResponse := responses.Lyrics{}
|
||||
response.Lyrics = &lyricsResponse
|
||||
mediaFiles, err := api.ds.MediaFile(r.Context()).GetAll(filter.SongWithArtistTitle(artist, title))
|
||||
mediaFiles, err := api.ds.MediaFile(r.Context()).GetAll(filter.SongsByArtistTitleWithLyricsFirst(artist, title))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,11 +2,13 @@ package subsonic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
@@ -84,12 +86,28 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
mockRepo.SetData(model.MediaFiles{
|
||||
{
|
||||
ID: "1",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: string(lyricsJson),
|
||||
ID: "2",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: "[]",
|
||||
UpdatedAt: baseTime.Add(2 * time.Hour), // No lyrics, newer
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: string(lyricsJson),
|
||||
UpdatedAt: baseTime.Add(1 * time.Hour), // Has lyrics, older
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: "[]",
|
||||
UpdatedAt: baseTime.Add(3 * time.Hour), // No lyrics, newest
|
||||
},
|
||||
})
|
||||
response, err := router.GetLyrics(r)
|
||||
@@ -122,6 +140,12 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
},
|
||||
{
|
||||
Path: "tests/fixtures/test.mp3",
|
||||
ID: "2",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
},
|
||||
})
|
||||
response, err := router.GetLyrics(r)
|
||||
Expect(err).To(BeNil())
|
||||
@@ -295,8 +319,25 @@ func (m *mockedMediaFile) SetData(mfs model.MediaFiles) {
|
||||
m.data = mfs
|
||||
}
|
||||
|
||||
func (m *mockedMediaFile) GetAll(...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.data, nil
|
||||
func (m *mockedMediaFile) GetAll(opts ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
if len(opts) == 0 || opts[0].Sort != "lyrics, updated_at" {
|
||||
return m.data, nil
|
||||
}
|
||||
|
||||
// Hardcoded support for lyrics sorting
|
||||
result := slices.Clone(m.data)
|
||||
// Sort by presence of lyrics, then by updated_at. Respect the order specified in opts.
|
||||
slices.SortFunc(result, func(a, b model.MediaFile) int {
|
||||
diff := cmp.Or(
|
||||
cmp.Compare(a.Lyrics, b.Lyrics),
|
||||
cmp.Compare(a.UpdatedAt.Unix(), b.UpdatedAt.Unix()),
|
||||
)
|
||||
if opts[0].Order == "desc" {
|
||||
return -diff
|
||||
}
|
||||
return diff
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *mockedMediaFile) Get(id string) (*model.MediaFile, error) {
|
||||
|
||||
Reference in New Issue
Block a user