mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
fix(albums): scope All Albums genre filter to selected music library (#959)
* fix(albums): scope All Albums genre filter to selected music library When the sidebar narrows to one Subsonic library, the genre popover fell back to server-wide getGenres() instead of the scoped local index catalog. Load genre options from libraryGetGenreAlbumCounts for library-only scope and enable the catalog path whenever the index is on and a library is selected. * docs: credit All Albums genre filter fix (PR #959, report zunoz) * chore: drop settingsCredits entry for small genre-filter fix
This commit is contained in:
@@ -414,6 +414,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* Startup no longer overwrites saved prefs before Zustand rehydration finishes; persisted volume is pushed to the Rust engine on boot.
|
* Startup no longer overwrites saved prefs before Zustand rehydration finishes; persisted volume is pushed to the Rust engine on boot.
|
||||||
|
|
||||||
|
|
||||||
|
### All Albums — genre filter respects sidebar library scope
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), reported by zunoz on the Psysonic Discord, PR [#959](https://github.com/Psychotoxical/psysonic/pull/959)**
|
||||||
|
|
||||||
|
* With multiple music libraries, narrowing the sidebar to one library no longer leaves the Genre filter showing server-wide genres — options now come from the scoped local index catalog (same scope as the album grid).
|
||||||
|
|
||||||
|
|
||||||
### In-page browse — virtual scroll and cover-art priority
|
### In-page browse — virtual scroll and cover-art priority
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
type AlbumBrowseQuery,
|
type AlbumBrowseQuery,
|
||||||
type GenreFilterOption,
|
type GenreFilterOption,
|
||||||
} from '../utils/library/albumBrowseLoad';
|
} from '../utils/library/albumBrowseLoad';
|
||||||
|
import { libraryScopeForServer } from '../api/subsonicClient';
|
||||||
import {
|
import {
|
||||||
ALBUM_YEAR_FILTER_DEBOUNCE_MS,
|
ALBUM_YEAR_FILTER_DEBOUNCE_MS,
|
||||||
resolveAlbumYearBounds,
|
resolveAlbumYearBounds,
|
||||||
@@ -167,7 +168,10 @@ export function useAlbumBrowseData({
|
|||||||
|
|
||||||
const genreFiltered = albumBrowseHasGenreFilter(browseQuery);
|
const genreFiltered = albumBrowseHasGenreFilter(browseQuery);
|
||||||
const serverFilterActive = albumBrowseHasServerFilters(browseQuery);
|
const serverFilterActive = albumBrowseHasServerFilters(browseQuery);
|
||||||
|
const libraryScopeActive = libraryScopeForServer(serverId) != null;
|
||||||
const narrowGenreList = yearFilterActive || losslessOnly || starredOnly || compFilterActive;
|
const narrowGenreList = yearFilterActive || losslessOnly || starredOnly || compFilterActive;
|
||||||
|
/** When true, GenreFilterBar uses `genreCatalogOptions` instead of server `getGenres()`. */
|
||||||
|
const genreCatalogActive = narrowGenreList || (indexEnabled && libraryScopeActive);
|
||||||
|
|
||||||
const compScanExhausted = useMemo(
|
const compScanExhausted = useMemo(
|
||||||
() => compFilterClientOnly && !genreFiltered
|
() => compFilterClientOnly && !genreFiltered
|
||||||
@@ -352,7 +356,7 @@ export function useAlbumBrowseData({
|
|||||||
}, [browseQuery, indexEnabled, serverId, loadBrowse, musicLibraryFilterVersion]);
|
}, [browseQuery, indexEnabled, serverId, loadBrowse, musicLibraryFilterVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!narrowGenreList) {
|
if (!genreCatalogActive) {
|
||||||
setGenreCatalogOptions(null);
|
setGenreCatalogOptions(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -364,7 +368,7 @@ export function useAlbumBrowseData({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
narrowGenreList,
|
genreCatalogActive,
|
||||||
serverId,
|
serverId,
|
||||||
indexEnabled,
|
indexEnabled,
|
||||||
browseQueryWithoutGenre,
|
browseQueryWithoutGenre,
|
||||||
@@ -466,6 +470,7 @@ export function useAlbumBrowseData({
|
|||||||
genreFiltered,
|
genreFiltered,
|
||||||
serverFilterActive,
|
serverFilterActive,
|
||||||
narrowGenreList,
|
narrowGenreList,
|
||||||
|
genreCatalogActive,
|
||||||
genreCatalogOptions,
|
genreCatalogOptions,
|
||||||
yearFilterActive,
|
yearFilterActive,
|
||||||
debouncedYearFields,
|
debouncedYearFields,
|
||||||
|
|||||||
@@ -163,8 +163,6 @@ export default function Albums() {
|
|||||||
const serverFilterActive = textSearchActive
|
const serverFilterActive = textSearchActive
|
||||||
? selectedGenres.length > 0 || textSearchYearBounds.active || losslessOnly || starredOnly
|
? selectedGenres.length > 0 || textSearchYearBounds.active || losslessOnly || starredOnly
|
||||||
: browseData.serverFilterActive;
|
: browseData.serverFilterActive;
|
||||||
const narrowGenreList = browseData.narrowGenreList;
|
|
||||||
const genreCatalogOptions = browseData.genreCatalogOptions;
|
|
||||||
const yearFilterActive = browseData.yearFilterActive;
|
const yearFilterActive = browseData.yearFilterActive;
|
||||||
const debouncedYearFields = browseData.debouncedYearFields;
|
const debouncedYearFields = browseData.debouncedYearFields;
|
||||||
const compFilterActive = browseData.compFilterActive;
|
const compFilterActive = browseData.compFilterActive;
|
||||||
@@ -398,7 +396,7 @@ export default function Albums() {
|
|||||||
|
|
||||||
<GenreFilterBar
|
<GenreFilterBar
|
||||||
selected={selectedGenres}
|
selected={selectedGenres}
|
||||||
catalogGenres={narrowGenreList ? genreCatalogOptions : null}
|
catalogGenres={browseData.genreCatalogActive ? browseData.genreCatalogOptions : null}
|
||||||
onSelectionChange={setSelectedGenres}
|
onSelectionChange={setSelectedGenres}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import type { AlbumBrowseQuery } from './albumBrowseTypes';
|
||||||
|
|
||||||
|
const libraryGetGenreAlbumCounts = vi.fn();
|
||||||
|
const libraryIsReady = vi.fn();
|
||||||
|
const libraryScopeForServer = vi.fn();
|
||||||
|
const runLocalAlbumBrowse = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../../api/library', () => ({
|
||||||
|
libraryGetGenreAlbumCounts: (...args: unknown[]) => libraryGetGenreAlbumCounts(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./libraryReady', () => ({
|
||||||
|
libraryIsReady: (...args: unknown[]) => libraryIsReady(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../api/subsonicClient', () => ({
|
||||||
|
libraryScopeForServer: (...args: unknown[]) => libraryScopeForServer(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./albumBrowseLocal', () => ({
|
||||||
|
runLocalAlbumBrowse: (...args: unknown[]) => runLocalAlbumBrowse(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { fetchAlbumBrowseGenreOptions } from './albumBrowseLoad';
|
||||||
|
|
||||||
|
const baseQuery: AlbumBrowseQuery = {
|
||||||
|
sort: 'alphabeticalByName',
|
||||||
|
genres: [],
|
||||||
|
losslessOnly: false,
|
||||||
|
starredOnly: false,
|
||||||
|
compFilter: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
libraryIsReady.mockResolvedValue(true);
|
||||||
|
libraryScopeForServer.mockReturnValue('lib-a');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAlbumBrowseGenreOptions', () => {
|
||||||
|
it('uses scoped local genre counts when only the sidebar library is narrowed', async () => {
|
||||||
|
libraryGetGenreAlbumCounts.mockResolvedValue([
|
||||||
|
{ value: 'Rock', albumCount: 12, songCount: 40 },
|
||||||
|
{ value: 'Jazz', albumCount: 3, songCount: 9 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(fetchAlbumBrowseGenreOptions('srv-1', true, baseQuery)).resolves.toEqual([
|
||||||
|
{ genre: 'Rock', count: 12 },
|
||||||
|
{ genre: 'Jazz', count: 3 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(libraryGetGenreAlbumCounts).toHaveBeenCalledWith({
|
||||||
|
serverId: 'srv-1',
|
||||||
|
libraryScope: 'lib-a',
|
||||||
|
});
|
||||||
|
expect(runLocalAlbumBrowse).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives genres from filtered albums when combined filters are active', async () => {
|
||||||
|
runLocalAlbumBrowse.mockResolvedValue({
|
||||||
|
albums: [
|
||||||
|
{ id: '1', name: 'A', artist: 'X', artistId: 'x', songCount: 1, duration: 1, genre: 'Rock' },
|
||||||
|
{ id: '2', name: 'B', artist: 'Y', artistId: 'y', songCount: 1, duration: 1, genre: 'Jazz' },
|
||||||
|
],
|
||||||
|
hasMore: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fetchAlbumBrowseGenreOptions('srv-1', true, { ...baseQuery, year: { from: 1990 } }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
{ genre: 'Jazz', count: 1 },
|
||||||
|
{ genre: 'Rock', count: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(libraryGetGenreAlbumCounts).not.toHaveBeenCalled();
|
||||||
|
expect(runLocalAlbumBrowse).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,10 +17,13 @@ export {
|
|||||||
} from './albumBrowseFilters';
|
} from './albumBrowseFilters';
|
||||||
export { runLocalAlbumBrowse } from './albumBrowseLocal';
|
export { runLocalAlbumBrowse } from './albumBrowseLocal';
|
||||||
|
|
||||||
import { countGenresFromAlbums, filterAlbumsByCompilation } from './albumBrowseFilters';
|
import { albumBrowseHasServerFilters, countGenresFromAlbums, filterAlbumsByCompilation } from './albumBrowseFilters';
|
||||||
import { runLocalAlbumBrowse } from './albumBrowseLocal';
|
import { runLocalAlbumBrowse } from './albumBrowseLocal';
|
||||||
import { fetchAlbumBrowseNetwork } from './albumBrowseNetwork';
|
import { fetchAlbumBrowseNetwork } from './albumBrowseNetwork';
|
||||||
import { fetchStarredAlbumBrowse } from './albumBrowseStarredFetch';
|
import { fetchStarredAlbumBrowse } from './albumBrowseStarredFetch';
|
||||||
|
import { libraryGetGenreAlbumCounts } from '../../api/library';
|
||||||
|
import { libraryScopeForServer } from '../../api/subsonicClient';
|
||||||
|
import { libraryIsReady } from './libraryReady';
|
||||||
import type {
|
import type {
|
||||||
AlbumBrowseFetchCallbacks,
|
AlbumBrowseFetchCallbacks,
|
||||||
AlbumBrowsePageResult,
|
AlbumBrowsePageResult,
|
||||||
@@ -55,6 +58,24 @@ export async function fetchAlbumBrowseGenreOptions(
|
|||||||
query: AlbumBrowseQuery,
|
query: AlbumBrowseQuery,
|
||||||
): Promise<GenreFilterOption[]> {
|
): Promise<GenreFilterOption[]> {
|
||||||
const withoutGenre: AlbumBrowseQuery = { ...query, genres: [] };
|
const withoutGenre: AlbumBrowseQuery = { ...query, genres: [] };
|
||||||
|
const scope = libraryScopeForServer(serverId);
|
||||||
|
const hasCombinedFilters =
|
||||||
|
albumBrowseHasServerFilters(withoutGenre) || query.compFilter !== 'all';
|
||||||
|
|
||||||
|
// Sidebar library scope only: use the full scoped genre catalog from the local
|
||||||
|
// index instead of getGenres() (server-wide) or a 500-album sample.
|
||||||
|
if (indexEnabled && serverId && scope && !hasCombinedFilters && (await libraryIsReady(serverId))) {
|
||||||
|
try {
|
||||||
|
const rows = await libraryGetGenreAlbumCounts({
|
||||||
|
serverId,
|
||||||
|
libraryScope: scope,
|
||||||
|
});
|
||||||
|
return rows.map(row => ({ genre: row.value, count: row.albumCount }));
|
||||||
|
} catch {
|
||||||
|
/* fall through to album-derived options */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const page = await fetchAlbumBrowsePage(
|
const page = await fetchAlbumBrowsePage(
|
||||||
serverId,
|
serverId,
|
||||||
indexEnabled,
|
indexEnabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user