mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-23 23:18:05 -05:00
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:
@@ -35,7 +35,31 @@ var _ = Describe("MediaRepository", func() {
|
||||
})
|
||||
|
||||
It("counts the number of mediafiles in the DB", func() {
|
||||
Expect(mr.CountAll()).To(Equal(int64(4)))
|
||||
Expect(mr.CountAll()).To(Equal(int64(6)))
|
||||
})
|
||||
|
||||
It("returns songs ordered by lyrics with a specific title/artist", func() {
|
||||
// attempt to mimic filters.SongsByArtistTitleWithLyricsFirst, except we want all items
|
||||
results, err := mr.GetAll(model.QueryOptions{
|
||||
Sort: "lyrics, updated_at",
|
||||
Order: "desc",
|
||||
Filters: squirrel.And{
|
||||
squirrel.Eq{"title": "Antenna"},
|
||||
squirrel.Or{
|
||||
Exists("json_tree(participants, '$.albumartist')", squirrel.Eq{"value": "Kraftwerk"}),
|
||||
Exists("json_tree(participants, '$.artist')", squirrel.Eq{"value": "Kraftwerk"}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Expect(err).To(BeNil())
|
||||
Expect(results).To(HaveLen(3))
|
||||
Expect(results[0].Lyrics).To(Equal(`[{"lang":"xxx","line":[{"value":"This is a set of lyrics"}],"synced":false}]`))
|
||||
for _, item := range results[1:] {
|
||||
Expect(item.Lyrics).To(Equal("[]"))
|
||||
Expect(item.Title).To(Equal("Antenna"))
|
||||
Expect(item.Participants[model.RoleArtist][0].Name).To(Equal("Kraftwerk"))
|
||||
}
|
||||
})
|
||||
|
||||
It("checks existence of mediafiles in the DB", func() {
|
||||
|
||||
@@ -38,6 +38,9 @@ func mf(mf model.MediaFile) model.MediaFile {
|
||||
model.Participant{Artist: model.Artist{ID: mf.ArtistID, Name: mf.Artist}},
|
||||
},
|
||||
}
|
||||
if mf.Lyrics == "" {
|
||||
mf.Lyrics = "[]"
|
||||
}
|
||||
return mf
|
||||
}
|
||||
|
||||
@@ -78,11 +81,22 @@ var (
|
||||
Path: p("/kraft/radio/antenna.mp3"),
|
||||
RGAlbumGain: 1.0, RGAlbumPeak: 2.0, RGTrackGain: 3.0, RGTrackPeak: 4.0,
|
||||
})
|
||||
testSongs = model.MediaFiles{
|
||||
songAntennaWithLyrics = mf(model.MediaFile{
|
||||
ID: "1005",
|
||||
Title: "Antenna",
|
||||
ArtistID: "2",
|
||||
Artist: "Kraftwerk",
|
||||
AlbumID: "103",
|
||||
Lyrics: `[{"lang":"xxx","line":[{"value":"This is a set of lyrics"}],"synced":false}]`,
|
||||
})
|
||||
songAntenna2 = mf(model.MediaFile{ID: "1006", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103"})
|
||||
testSongs = model.MediaFiles{
|
||||
songDayInALife,
|
||||
songComeTogether,
|
||||
songRadioactivity,
|
||||
songAntenna,
|
||||
songAntennaWithLyrics,
|
||||
songAntenna2,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ func (r *sqlRepository) registerModel(instance any, filters map[string]filterFun
|
||||
// which gives precedence to sort tags.
|
||||
// Ex: order_title => (coalesce(nullif(sort_title,”),order_title) collate nocase)
|
||||
// To avoid performance issues, indexes should be created for these sort expressions
|
||||
//
|
||||
// NOTE: if an individual item has spaces, it should be wrapped in parentheses. For example,
|
||||
// you should write "(lyrics != '[]')". This prevents the item being split unexpectedly.
|
||||
// Without parentheses, "lyrics != '[]'" would be mapped as simply "lyrics"
|
||||
func (r *sqlRepository) setSortMappings(mappings map[string]string, tableName ...string) {
|
||||
tn := r.tableName
|
||||
if len(tableName) > 0 {
|
||||
|
||||
@@ -136,6 +136,10 @@ var _ = Describe("sqlRepository", func() {
|
||||
})
|
||||
|
||||
Describe("buildSortOrder", func() {
|
||||
BeforeEach(func() {
|
||||
r.sortMappings = map[string]string{}
|
||||
})
|
||||
|
||||
Context("single field", func() {
|
||||
It("sorts by specified field", func() {
|
||||
sql := r.buildSortOrder("name", "desc")
|
||||
@@ -163,6 +167,14 @@ var _ = Describe("sqlRepository", func() {
|
||||
sql := r.buildSortOrder("name desc, age, status asc", "desc")
|
||||
Expect(sql).To(Equal("name asc, age desc, status desc"))
|
||||
})
|
||||
It("handles spaces in mapped field", func() {
|
||||
r.sortMappings = map[string]string{
|
||||
"has_lyrics": "(lyrics != '[]'), updated_at",
|
||||
}
|
||||
sql := r.buildSortOrder("has_lyrics", "desc")
|
||||
Expect(sql).To(Equal("(lyrics != '[]') desc, updated_at desc"))
|
||||
})
|
||||
|
||||
})
|
||||
Context("function fields", func() {
|
||||
It("handles functions with multiple params", func() {
|
||||
|
||||
Reference in New Issue
Block a user