fix(genres): hide empty genres after library resync (#1176)

This commit is contained in:
cucadmuh
2026-06-24 23:07:22 +03:00
committed by GitHub
parent 1031590742
commit ec98fcc4ff
6 changed files with 120 additions and 32 deletions
+26 -2
View File
@@ -4,11 +4,13 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Tags } from 'lucide-react';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { subscribeLibrarySyncIdle } from '../api/library';
import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { fetchGenreCatalog } from '../utils/library/genreBrowsePlayback';
import { fetchGenreCatalog, filterGenresWithContent } from '../utils/library/genreBrowsePlayback';
import { libraryScopeForServer } from '../api/subsonicClient';
import { peekGenreCatalogCache } from '../utils/library/genreCatalogCountsCache';
import { resolveIndexKey } from '../utils/server/serverIndexKey';
const CTP_COLORS = [
'var(--ctp-rosewater)', 'var(--ctp-flamingo)', 'var(--ctp-pink)', 'var(--ctp-mauve)',
@@ -63,10 +65,32 @@ export default function Genres() {
}, [serverId, indexEnabled, musicLibraryFilterVersion]);
const genres = useMemo(
() => [...rawGenres].sort((a, b) => b.albumCount - a.albumCount),
() => filterGenresWithContent([...rawGenres]).sort((a, b) => b.albumCount - a.albumCount),
[rawGenres],
);
// After library resync the in-memory catalog cache is cleared, but this page
// can still hold pre-sync genres until we refetch (issue #1162).
useEffect(() => {
if (!serverId || !indexEnabled) return;
let cancelled = false;
const indexKey = resolveIndexKey(serverId);
let unlisten: (() => void) | undefined;
void subscribeLibrarySyncIdle(payload => {
if (!payload.ok) return;
if (payload.serverId !== indexKey && payload.serverId !== serverId) return;
void fetchGenreCatalog(serverId, indexEnabled).then(data => {
if (!cancelled) setRawGenres(data);
});
}).then(fn => {
unlisten = fn;
});
return () => {
cancelled = true;
unlisten?.();
};
}, [serverId, indexEnabled]);
// Log-scale font sizing — flattens the long tail (a 1000-album genre and a
// 50-album genre look distinct, but a 1-album genre still has a readable size).
const maxLog = useMemo(() => {
@@ -4,6 +4,7 @@ import {
fetchGenreCatalog,
fetchGenreTracksForPlayback,
fetchLocalGenreTracksForPlayback,
filterGenresWithContent,
GENRE_PLAYBACK_QUEUE_CAP,
} from './genreBrowsePlayback';
@@ -135,6 +136,29 @@ describe('genreBrowsePlayback', () => {
expect(getGenres).not.toHaveBeenCalled();
});
it('drops empty genres from server fallback catalog', async () => {
vi.mocked(libraryIsReady).mockResolvedValue(false);
vi.mocked(getGenres).mockResolvedValue([
{ value: 'ruspop', songCount: 0, albumCount: 0 },
{ value: 'Rock', songCount: 10, albumCount: 3 },
]);
await expect(fetchGenreCatalog('srv-1', true)).resolves.toEqual([
{ value: 'Rock', albumCount: 3, songCount: 10 },
]);
});
it('filterGenresWithContent drops zero-count rows', () => {
expect(filterGenresWithContent([
{ value: 'Empty', albumCount: 0, songCount: 0 },
{ value: 'SongsOnly', albumCount: 0, songCount: 2 },
{ value: 'AlbumsOnly', albumCount: 1, songCount: 0 },
])).toEqual([
{ value: 'SongsOnly', albumCount: 0, songCount: 2 },
{ value: 'AlbumsOnly', albumCount: 1, songCount: 0 },
]);
});
it('reuses cached genre catalog without repeating SQL', async () => {
vi.mocked(libraryIsReady).mockResolvedValue(true);
vi.mocked(libraryGetGenreAlbumCounts).mockResolvedValue([
+8 -3
View File
@@ -21,6 +21,11 @@ import {
import { fetchGenreAlbumTotal } from './genreAlbumBrowse';
import { libraryIsReady } from './libraryReady';
/** Drop genres with no indexed albums/tracks (stale server list or orphan rows). */
export function filterGenresWithContent(genres: SubsonicGenre[]): SubsonicGenre[] {
return genres.filter(g => (g.albumCount ?? 0) > 0 || (g.songCount ?? 0) > 0);
}
async function loadLocalGenreCatalogRows(
serverId: string,
libraryScope: string | undefined,
@@ -29,11 +34,11 @@ async function loadLocalGenreCatalogRows(
serverId,
libraryScope,
});
return rows.map(row => ({
return filterGenresWithContent(rows.map(row => ({
value: row.value,
albumCount: row.albumCount,
songCount: row.songCount,
}));
})));
}
async function fetchLocalGenreCatalog(
@@ -153,7 +158,7 @@ export async function fetchGenreCatalog(
/* network fallback */
}
}
const genres = await getGenres();
const genres = filterGenresWithContent(await getGenres());
writeGenreCatalogCache(serverId, scope, genres);
return genres;
};