fix(library): All Albums compilation filter matches VA album artist (#1026)

* fix(library): All Albums compilation filter matches VA album artist

Local index SQL and track-grouped browse missed compilations tagged via
Various Artists on album_artist; genre-only browse also ignored combined
filters. Extend predicates on both sides and route genre+filter to advanced search.

* docs(changelog): All Albums compilation filter fix (PR #1026)
This commit is contained in:
cucadmuh
2026-06-08 01:19:24 +03:00
committed by GitHub
parent e527cfe67f
commit 32832246c0
6 changed files with 155 additions and 21 deletions
+26 -3
View File
@@ -30,19 +30,42 @@ export async function runLocalAlbumBrowse(
if (query.genres.length > 0) {
if (query.genres.length === 1) {
// Genre-only fast path; combined filters (year / lossless / compilation) need advanced search.
if (shared.length === 0) {
try {
const resp = await libraryListAlbumsByGenre({
serverId,
genre: query.genres[0],
libraryScope: scope,
sort: albumSortClauses(query.sort),
limit: pageSize,
offset,
});
if (resp.source !== 'local') return null;
let albums = resp.albums.map(albumToAlbum);
if (useServerStarredIds) albums = markServerStarredAlbums(albums);
return { albums, hasMore: resp.hasMore };
} catch {
return null;
}
}
try {
const resp = await libraryListAlbumsByGenre({
const resp = await libraryAdvancedSearch({
serverId,
genre: query.genres[0],
libraryScope: scope,
entityTypes: ['album'],
filters: [{ field: 'genre', op: 'eq', value: query.genres[0] }, ...shared],
starredOnly,
restrictAlbumIds: useServerStarredIds ? restrictAlbumIds : undefined,
sort: albumSortClauses(query.sort),
limit: pageSize,
offset,
skipTotals: true,
});
if (resp.source !== 'local') return null;
let albums = resp.albums.map(albumToAlbum);
if (useServerStarredIds) albums = markServerStarredAlbums(albums);
return { albums, hasMore: resp.hasMore };
return { albums, hasMore: albums.length === pageSize };
} catch {
return null;
}
+2 -1
View File
@@ -8,7 +8,7 @@ import {
import { filterAlbumsByCompilation } from './albumBrowseFilters';
const album = (
overrides: Partial<SubsonicAlbum> & { compilation?: boolean } = {},
overrides: Partial<SubsonicAlbum> & { compilation?: boolean; albumArtist?: string } = {},
): SubsonicAlbum => ({
id: '1',
name: 'A',
@@ -25,6 +25,7 @@ describe('albumIsCompilation', () => {
expect(albumIsCompilation(album({ compilation: true }))).toBe(true);
expect(albumIsCompilation(album({ releaseTypes: ['Live', 'Compilation'] }))).toBe(true);
expect(albumIsCompilation(album({ artist: 'Various Artists' }))).toBe(true);
expect(albumIsCompilation(album({ albumArtist: 'Various Artists' }))).toBe(true);
expect(albumIsCompilation(album())).toBe(false);
});
});
+5 -2
View File
@@ -10,12 +10,15 @@ const VARIOUS_ARTISTS = /\bvarious artists\b/i;
/** OpenSubsonic / Navidrome: `compilation`, `isCompilation`, `releaseTypes`, or VA artist. */
export function albumIsCompilation(a: SubsonicAlbum): boolean {
if (a.isCompilation === true) return true;
const loose = a as SubsonicAlbum & { compilation?: boolean };
const loose = a as SubsonicAlbum & { compilation?: boolean; albumArtist?: string };
if (loose.compilation === true) return true;
if (a.releaseTypes?.some(t => /^compilation$/i.test(t.trim()))) return true;
const artist = (a.artist ?? '').trim();
const displayArtist = (a.displayArtist ?? '').trim();
return VARIOUS_ARTISTS.test(artist) || VARIOUS_ARTISTS.test(displayArtist);
const albumArtist = (loose.albumArtist ?? '').trim();
return VARIOUS_ARTISTS.test(artist)
|| VARIOUS_ARTISTS.test(displayArtist)
|| VARIOUS_ARTISTS.test(albumArtist);
}
/** Stop paginating when the catalog tail is reached or the scan budget is spent. */