fix(jellyfin): honor AlbumIds filter on /Items

Feishin fetches an album's tracks with AlbumIds=<id>&IncludeItemTypes=Audio&Recursive=true, but the /Items handler never read the AlbumIds parameter, so the request degenerated into the entire library sorted by album: every track (with MediaSources, when requested) was counted and streamed for what should be one album's worth of songs — very slow on large libraries, and wrong results. Parse AlbumIds like GenreIds (comma-separated and repeated spellings, hex-decoded) and filter songs through a new filter.ByAlbumID helper, keeping the album_id column knowledge in the filter package.
This commit is contained in:
Deluan 2026-07-15 23:25:55 -04:00
parent 1d5efdd5a0
commit caa0043030
3 changed files with 32 additions and 0 deletions

View File

@ -189,6 +189,11 @@ func ByGenreID(genreIds []string) Sqlizer {
return genreTagFilter(Eq{"value": genreIds})
}
// ByAlbumID matches media files belonging to any of the given albums.
func ByAlbumID(albumIds []string) Sqlizer {
return Eq{"album_id": albumIds}
}
// ArtistsByGenreID matches artists credited as album artist on an album with any of the given
// genre tag ids. Non-correlated semi-join: the correlated EXISTS form rescans albums per artist row.
func ArtistsByGenreID(genreIds []string) Sqlizer {

View File

@ -179,6 +179,27 @@ var _ = Describe("Browsing", func() {
})
})
// Feishin fetches an album's tracks with AlbumIds=<albumId>&IncludeItemTypes=Audio&Recursive=true.
Describe("album filtering (AlbumIds)", func() {
It("filters songs by AlbumIds", func() {
q := queryResult(get("/Items?IncludeItemTypes=Audio&Recursive=true&AlbumIds=" + enc(albumID("Abbey Road"))))
Expect(names(q.Items)).To(ConsistOf("Come Together", "Something"))
Expect(q.TotalRecordCount).To(Equal(2))
})
It("matches any of multiple comma-separated AlbumIds", func() {
q := queryResult(get("/Items?IncludeItemTypes=Audio&Recursive=true&AlbumIds=" + enc(albumID("Abbey Road")) + "," + enc(albumID("IV"))))
Expect(names(q.Items)).To(ConsistOf("Come Together", "Something", "Stairway To Heaven"))
Expect(q.TotalRecordCount).To(Equal(3))
})
It("returns nothing for an unknown album id", func() {
q := queryResult(get("/Items?IncludeItemTypes=Audio&Recursive=true&AlbumIds=" + enc("no-such-album")))
Expect(q.Items).To(BeEmpty())
Expect(q.TotalRecordCount).To(Equal(0))
})
})
// Finamp's genre screen sends ParentId=<libraryId> (scoping) plus GenreIds=<genreId>.
Describe("genre filtering (GenreIds)", func() {
lib1 := enc("1")

View File

@ -221,6 +221,7 @@ type itemsQuery struct {
artistId string
contributingOnly bool
genreIds []string
albumIds []string
}
// parseItemsQuery also resolves the entity types (inferring them from the parent when
@ -243,6 +244,8 @@ func (api *Router) parseItemsQuery(ctx context.Context, r *http.Request) itemsQu
parentId: dto.DecodeID(p.StringOr("parentid", "")),
// Finamp's genre screen sends ParentId=<libraryId> for scoping plus GenreIds for the genre.
genreIds: decodedQueryIDs(r, "genreids"),
// Feishin fetches an album's tracks with AlbumIds instead of ParentId.
albumIds: decodedQueryIDs(r, "albumids"),
}
// An artist's page filters by artist, not ParentId: Finamp sends ParentId=<libraryId> for scoping
// plus AlbumArtistIds/ArtistIds/contributingArtistIds for the artist.
@ -528,6 +531,9 @@ func (api *Router) listSongs(ctx context.Context, opts model.QueryOptions, q ite
default:
filters = append(filters, notMissing)
}
if len(q.albumIds) > 0 {
filters = append(filters, filter.ByAlbumID(q.albumIds))
}
if len(q.genreIds) > 0 {
filters = append(filters, filter.ByGenreID(q.genreIds))
}