Files
psysonic/src/hooks/usePlaylistsLibraryScopeCounts.ts
T
Frank Stellmacher 2380543d59 refactor(playlists): G.79 — extract smart helpers + cover images + 2 lazy-fetch hooks (cluster) (#646)
Four-cut cluster opening the Playlists refactor. 1039 → 763 LOC
(−276).

playlistsSmart — full smart-playlist module: SMART_PREFIX /
LIMIT_MAX / YEAR_MIN / YEAR_MAX constants, GenreMode / YearMode /
SmartFilters / PendingSmartPlaylist / NdSmartRuleNode types,
defaultSmartFilters seed, clampYear / isSmartPlaylistName /
displayPlaylistName / asRecord helpers, parseSmartRulesToFilters
(the Navidrome JSON rule walker), and buildSmartRulesPayload (the
reverse — page-state → Navidrome JSON). The payload builder
becomes parameterized on the filters object so it lives outside
the component.

PlaylistCoverImages — two tiny CachedImage wrappers
(PlaylistSmartCoverCell for the 200 px collage cells,
PlaylistCardMainCover for the 256 px main card cover).

useSmartCoverCollage — replaces the inline useEffect that builds
the 2×2 cover collage for each smart playlist (pulls playlist
tracks, filters to active library scope, collects up to four
unique cover-art ids). Returns the per-playlist id map and
re-fetches on playlist list change or library filter version bump.

usePlaylistsLibraryScopeCounts — replaces the inline useEffect
that recomputes per-playlist song count + total duration under
the current library scope. Chunked into batches of four parallel
fetches.

Playlists drops the inline definitions; pure code move otherwise.
2026-05-13 15:52:09 +02:00

67 lines
2.5 KiB
TypeScript

import { useEffect, useState } from 'react';
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
import { getPlaylist } from '../api/subsonicPlaylists';
import type { SubsonicPlaylist } from '../api/subsonicTypes';
export interface PlaylistsLibraryScopeCountsResult {
filteredSongCountByPlaylist: Record<string, number>;
filteredDurationByPlaylist: Record<string, number>;
}
/**
* Recompute song count + total duration for each playlist under the current
* library scope. Chunked into batches of 4 parallel fetches to avoid hammering
* Navidrome on large playlists. Re-runs when the playlist list changes or
* when the active library filter version bumps.
*/
export function usePlaylistsLibraryScopeCounts(
playlists: SubsonicPlaylist[],
musicLibraryFilterVersion: number,
): PlaylistsLibraryScopeCountsResult {
const [filteredSongCountByPlaylist, setFilteredSongCountByPlaylist] = useState<Record<string, number>>({});
const [filteredDurationByPlaylist, setFilteredDurationByPlaylist] = useState<Record<string, number>>({});
useEffect(() => {
let cancelled = false;
const run = async () => {
if (playlists.length === 0) {
if (!cancelled) {
setFilteredSongCountByPlaylist({});
setFilteredDurationByPlaylist({});
}
return;
}
const ids = playlists.map((pl) => pl.id);
const next: Record<string, number> = {};
const nextDuration: Record<string, number> = {};
for (let i = 0; i < ids.length; i += 4) {
const chunk = ids.slice(i, i + 4);
const rows = await Promise.all(
chunk.map(async (id) => {
try {
const { songs } = await getPlaylist(id);
const filtered = await filterSongsToActiveLibrary(songs);
const duration = filtered.reduce((acc, s) => acc + (s.duration ?? 0), 0);
return [id, filtered.length, duration] as const;
} catch {
return [id, -1, -1] as const;
}
}),
);
for (const [id, count, duration] of rows) {
if (count >= 0) next[id] = count;
if (duration >= 0) nextDuration[id] = duration;
}
}
if (!cancelled) {
setFilteredSongCountByPlaylist(next);
setFilteredDurationByPlaylist(nextDuration);
}
};
run();
return () => { cancelled = true; };
}, [playlists, musicLibraryFilterVersion]);
return { filteredSongCountByPlaylist, filteredDurationByPlaylist };
}