fix(jellyfin): sort album tracks by track number when client sends IndexNumber SortBy

Finamp's album view requests SortBy=ParentIndexNumber,IndexNumber,SortName
(disc, track, name), but applySort didn't recognize ParentIndexNumber or
IndexNumber and fell through to SortName, sorting tracks alphabetically by
title. Map both keys to the album (disc+track) sort. The reversed-title
fixture lets the e2e tell track order from title order.
This commit is contained in:
Deluan 2026-07-06 08:52:14 -04:00
commit adf943fc87
4 changed files with 37 additions and 13 deletions

View file

@ -79,10 +79,17 @@ var _ = Describe("Browsing", func() {
It("browses an album's tracks in track order by default", func() {
q := queryResult(get("/Items?IncludeItemTypes=Audio&ParentId=" + enc(albumID("Abbey Road"))))
Expect(q.TotalRecordCount).To(Equal(2))
Expect(names(q.Items)).To(Equal([]string{"Come Together", "Something"}))
// Track order (Something=1, Come Together=2) differs from alphabetical title order,
// proving the sort is by track number, not name.
Expect(names(q.Items)).To(Equal([]string{"Something", "Come Together"}))
Expect(*q.Items[0].IndexNumber).To(Equal(1))
Expect(*q.Items[1].IndexNumber).To(Equal(2))
})
It("respects Finamp's explicit ParentIndexNumber/IndexNumber SortBy on an album", func() {
q := queryResult(get("/Items?IncludeItemTypes=Audio&ParentId=" + enc(albumID("Abbey Road")) + "&SortBy=ParentIndexNumber,IndexNumber,SortName"))
Expect(names(q.Items)).To(Equal([]string{"Something", "Come Together"}))
})
})
// Finamp's artist screen sends ParentId=<libraryId> (scoping) plus AlbumArtistIds/ArtistIds

View file

@ -11,7 +11,7 @@
//
// # Seeded library (see buildTestFS)
//
// Rock/The Beatles/Abbey Road/ 01 Come Together (1969), 02 Something (1969)
// Rock/The Beatles/Abbey Road/ 01 Something (1969), 02 Come Together (1969)
// Rock/The Beatles/Help!/ 01 Help! (1965)
// Rock/Led Zeppelin/IV/ 01 Stairway To Heaven (1971)
// Jazz/Miles Davis/Kind of Blue/01 So What (1959)
@ -120,8 +120,10 @@ func buildTestFS() storagetest.FakeFS {
singles := template(_t{"albumartist": "Solo Artist", "artist": "Solo Artist", "album": "Singles", "year": 2020, "genre": "Pop"})
return createFS(fstest.MapFS{
"Rock/The Beatles/Abbey Road/01 - Come Together.mp3": abbeyRoad(track(1, "Come Together")),
"Rock/The Beatles/Abbey Road/02 - Something.mp3": abbeyRoad(track(2, "Something")),
// Track numbers are deliberately reversed vs. alphabetical title order (Something=1,
// Come Together=2) so tests can tell track-order sorting apart from title sorting.
"Rock/The Beatles/Abbey Road/01 - Something.mp3": abbeyRoad(track(1, "Something")),
"Rock/The Beatles/Abbey Road/02 - Come Together.mp3": abbeyRoad(track(2, "Come Together")),
"Rock/The Beatles/Help!/01 - Help.mp3": help(track(1, "Help!")),
"Rock/Led Zeppelin/IV/01 - Stairway To Heaven.mp3": ledZepIV(track(1, "Stairway To Heaven")),
"Jazz/Miles Davis/Kind of Blue/01 - So What.mp3": kindOfBlue(track(1, "So What")),

View file

@ -410,14 +410,19 @@ func applySort(opts *model.QueryOptions, itemType, sortBy, order string) {
var sortColumnsByType = map[string]map[string]string{
"Audio": {
"sortname": "title", "name": "title",
"album": "album",
"artist": "artist",
"albumartist": "album_artist",
"datecreated": "recently_added",
"playcount": "play_count",
"dateplayed": "play_date",
"communityrating": "rating",
"random": "random",
"album": "album",
// Finamp's album view sorts by ParentIndexNumber,IndexNumber (disc, track). Navidrome's
// "album" sort key is order_album_name, album_id, disc_number, track_number, ..., which is
// exactly disc+track order within an album, so map both here.
"indexnumber": "album",
"parentindexnumber": "album",
"artist": "artist",
"albumartist": "album_artist",
"datecreated": "recently_added",
"playcount": "play_count",
"dateplayed": "play_date",
"communityrating": "rating",
"random": "random",
},
"MusicArtist": {
"sortname": "name", "name": "name",

View file

@ -288,12 +288,22 @@ var _ = Describe("Items", func() {
mfRepo := ds.MediaFile(context.Background()).(*tests.MockMediaFileRepo)
mfRepo.SetData(model.MediaFiles{{ID: "s1", Title: "Song"}})
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/Items?IncludeItemTypes=Audio&SortBy=ParentIndexNumber,IndexNumber,SortName", nil).WithContext(ctxUser())
r := httptest.NewRequest("GET", "/Items?IncludeItemTypes=Audio&SortBy=Unknown1,Unknown2,SortName", nil).WithContext(ctxUser())
api.getItems(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(mfRepo.Options.Sort).To(Equal("title"))
})
It("maps Finamp's album view SortBy (ParentIndexNumber,IndexNumber) to disc+track order", func() {
mfRepo := ds.MediaFile(context.Background()).(*tests.MockMediaFileRepo)
mfRepo.SetData(model.MediaFiles{{ID: "s1", Title: "Song"}})
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/Items?IncludeItemTypes=Audio&SortBy=ParentIndexNumber,IndexNumber,SortName", nil).WithContext(ctxUser())
api.getItems(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
Expect(mfRepo.Options.Sort).To(Equal("album"))
})
It("leaves Sort at the repo default when no SortBy key is recognized", func() {
albumRepo := ds.Album(context.Background()).(*tests.MockAlbumRepo)
albumRepo.SetData(model.Albums{{ID: "a1", Name: "One"}})