mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
d0a270d90a
Three-cut cluster pulling tightly-scoped state + memo islands out of PlaylistDetail.tsx as custom hooks. 659 → 565 LOC (−94). Each hook returns the same names the page already used, so call sites stay identical. usePlaylistCovers(songs, customCoverId) — the 2×2 cover quad memo, the four cover-quad URLs (with their stable cache keys to avoid the buildCoverArtUrl-salt re-render loop documented in the comment), the customCover fetch URL + cache key, and the blurred background URL going through useCachedUrl. Returns coverQuadUrls / customCoverFetchUrl / customCoverCacheKey / resolvedBgUrl. The page no longer imports buildCoverArtUrl / coverArtCacheKey / useCachedUrl directly. usePlaylistSelection(songs, setSongs, savePlaylist) — selectedIds / lastSelectedIdx state + toggleSelect (with shift-range support) + allSelected + toggleAll + bulkRemove (savePlaylist + setSongs + clears selection). Returns the same shape the tracklist props already destructured. savePlaylist moves a few lines up in PlaylistDetail so the hook can be called with it. usePlaylistSuggestions(songs, playlist.id) — suggestions state + loadSuggestions (genre-weighted random pull, top 10) + the auto-load useEffect that fires on playlist change. Returns setSuggestions too so addSong on the page can still filter the just-added song out of the suggestions strip. The page drops the getRandomSongs import. Pure code move otherwise — no behaviour change.
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import type React from 'react';
|
|
import { getRandomSongs } from '../api/subsonicLibrary';
|
|
import type { SubsonicSong } from '../api/subsonicTypes';
|
|
|
|
export interface PlaylistSuggestionsResult {
|
|
suggestions: SubsonicSong[];
|
|
setSuggestions: React.Dispatch<React.SetStateAction<SubsonicSong[]>>;
|
|
loadingSuggestions: boolean;
|
|
loadSuggestions: (currentSongs: SubsonicSong[]) => Promise<void>;
|
|
}
|
|
|
|
export function usePlaylistSuggestions(songs: SubsonicSong[], playlistId: string | undefined): PlaylistSuggestionsResult {
|
|
const [suggestions, setSuggestions] = useState<SubsonicSong[]>([]);
|
|
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
|
|
|
|
const loadSuggestions = useCallback(async (currentSongs: SubsonicSong[]) => {
|
|
if (!currentSongs.length) return;
|
|
// Count genres across playlist songs, pick the most common one
|
|
const genreCounts: Record<string, number> = {};
|
|
for (const s of currentSongs) {
|
|
if (s.genre) genreCounts[s.genre] = (genreCounts[s.genre] ?? 0) + 1;
|
|
}
|
|
const genres = Object.entries(genreCounts).sort((a, b) => b[1] - a[1]);
|
|
// Fall back to no genre filter if none of the songs have genre tags
|
|
const genre = genres.length > 0 ? genres[Math.floor(Math.random() * Math.min(3, genres.length))][0] : undefined;
|
|
const existingIds = new Set(currentSongs.map(s => s.id));
|
|
setLoadingSuggestions(true);
|
|
setSuggestions([]);
|
|
try {
|
|
const random = await getRandomSongs(25, genre);
|
|
setSuggestions(random.filter(s => !existingIds.has(s.id)).slice(0, 10));
|
|
} catch {}
|
|
setLoadingSuggestions(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (songs.length > 0) loadSuggestions(songs);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [playlistId]);
|
|
|
|
return { suggestions, setSuggestions, loadingSuggestions, loadSuggestions };
|
|
}
|