mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
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.
This commit is contained in:
committed by
GitHub
parent
84c682aeb8
commit
2380543d59
@@ -0,0 +1,66 @@
|
||||
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 };
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { filterSongsToActiveLibrary } from '../api/subsonicLibrary';
|
||||
import { getPlaylist } from '../api/subsonicPlaylists';
|
||||
import type { SubsonicPlaylist } from '../api/subsonicTypes';
|
||||
import { isSmartPlaylistName } from '../utils/playlistsSmart';
|
||||
|
||||
/**
|
||||
* Build the 2×2 cover collage for each smart playlist. Pulls each smart
|
||||
* playlist's tracks (filtered to the active library scope) and collects up
|
||||
* to four unique cover-art IDs. Re-runs when the playlist list changes or
|
||||
* when the active library filter version bumps.
|
||||
*/
|
||||
export function useSmartCoverCollage(
|
||||
playlists: SubsonicPlaylist[],
|
||||
musicLibraryFilterVersion: number,
|
||||
): Record<string, string[]> {
|
||||
const [smartCoverIdsByPlaylist, setSmartCoverIdsByPlaylist] = useState<Record<string, string[]>>({});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
const smart = playlists.filter(pl => isSmartPlaylistName(pl.name));
|
||||
if (smart.length === 0) {
|
||||
if (!cancelled) setSmartCoverIdsByPlaylist({});
|
||||
return;
|
||||
}
|
||||
const rows = await Promise.all(
|
||||
smart.map(async (pl) => {
|
||||
try {
|
||||
const { songs } = await getPlaylist(pl.id);
|
||||
const filtered = await filterSongsToActiveLibrary(songs);
|
||||
const ids: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const s of filtered) {
|
||||
const cid = s.coverArt;
|
||||
if (!cid || seen.has(cid)) continue;
|
||||
seen.add(cid);
|
||||
ids.push(cid);
|
||||
if (ids.length >= 4) break;
|
||||
}
|
||||
return [pl.id, ids] as const;
|
||||
} catch {
|
||||
return [pl.id, [] as string[]] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
const next: Record<string, string[]> = {};
|
||||
for (const [id, ids] of rows) next[id] = ids;
|
||||
setSmartCoverIdsByPlaylist(next);
|
||||
};
|
||||
run();
|
||||
return () => { cancelled = true; };
|
||||
}, [playlists, musicLibraryFilterVersion]);
|
||||
|
||||
return smartCoverIdsByPlaylist;
|
||||
}
|
||||
Reference in New Issue
Block a user