mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(genres): hide empty genres after library resync (#1176)
This commit is contained in:
+26
-2
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user