mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
ba0bf8aa9d
Five-cut cluster opening the ArtistDetail refactor. 944 → 631 LOC
(−313).
artistDetailHelpers — formatDuration (M:SS) + sanitizeHtml (strip
script/style/iframe/etc tags + onXxx + javascript: / data: hrefs).
ArtistSuggestionTrackCover — tiny CachedImage wrapper for the
32×32 cover thumbnail in the suggestions tracklist.
useArtistDetailData — owns the page's three primary fetch effects:
getArtist + getTopSongs on artist id change, getArtistInfo on id +
audiomuseNavidromeEnabled change, and the background "Also Featured
On" search that derives albums from search results not in the
artist's own album set. Exposes artist / setArtist / albums /
topSongs / info / featuredAlbums / loading flags + isStarred for
the star button.
useArtistSimilarArtists — owns the two parallel similar-artist
effects (Last.fm primary path when AudioMuse is off; Last.fm
fallback when AudioMuse is on but returned nothing) plus the
audiomuse-positive-result reset. Returns { similarArtists,
similarLoading }.
runArtistDetailPlay — three play orchestrators that share the
fetchAllTracks helper: runArtistDetailPlayAll, runArtistDetailShuffle,
runArtistDetailStartRadio (with the no-radio fallback alert). All
take deps objects so the page just delegates state setters.
ArtistDetail drops the now-unused direct search / getArtistInfo /
getTopSongs / getSimilarSongs2 / getArtist / lastfmGetSimilarArtists
imports. Pure code move otherwise.
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
import type { TFunction } from 'i18next';
|
|
import { getAlbum } from '../api/subsonicLibrary';
|
|
import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
|
|
import type { SubsonicAlbum, SubsonicArtist } from '../api/subsonicTypes';
|
|
import type { Track } from '../store/playerStoreTypes';
|
|
import { songToTrack } from './songToTrack';
|
|
|
|
async function fetchAllTracks(albums: SubsonicAlbum[]): Promise<Track[]> {
|
|
const results = await Promise.all(albums.map(a => getAlbum(a.id)));
|
|
const sorted = [...results].sort((a, b) => (a.album.year ?? 0) - (b.album.year ?? 0));
|
|
return sorted.flatMap(r => [...r.songs].sort((a, b) => (a.track ?? 0) - (b.track ?? 0))).map(songToTrack);
|
|
}
|
|
|
|
export interface RunArtistDetailPlayDeps {
|
|
albums: SubsonicAlbum[];
|
|
setPlayAllLoading: (v: boolean) => void;
|
|
playTrack: (track: Track, queue: Track[]) => void;
|
|
}
|
|
|
|
export async function runArtistDetailPlayAll(deps: RunArtistDetailPlayDeps): Promise<void> {
|
|
const { albums, setPlayAllLoading, playTrack } = deps;
|
|
if (albums.length === 0) return;
|
|
setPlayAllLoading(true);
|
|
try {
|
|
const tracks = await fetchAllTracks(albums);
|
|
if (tracks.length > 0) playTrack(tracks[0], tracks);
|
|
} finally {
|
|
setPlayAllLoading(false);
|
|
}
|
|
}
|
|
|
|
export async function runArtistDetailShuffle(deps: RunArtistDetailPlayDeps): Promise<void> {
|
|
const { albums, setPlayAllLoading, playTrack } = deps;
|
|
if (albums.length === 0) return;
|
|
setPlayAllLoading(true);
|
|
try {
|
|
const tracks = await fetchAllTracks(albums);
|
|
if (tracks.length > 0) {
|
|
const shuffled = [...tracks].sort(() => Math.random() - 0.5);
|
|
playTrack(shuffled[0], shuffled);
|
|
}
|
|
} finally {
|
|
setPlayAllLoading(false);
|
|
}
|
|
}
|
|
|
|
export interface RunArtistDetailStartRadioDeps {
|
|
artist: SubsonicArtist;
|
|
t: TFunction;
|
|
setRadioLoading: (v: boolean) => void;
|
|
playTrack: (track: Track, queue: Track[]) => void;
|
|
enqueue: (tracks: Track[]) => void;
|
|
}
|
|
|
|
export async function runArtistDetailStartRadio(deps: RunArtistDetailStartRadioDeps): Promise<void> {
|
|
const { artist, t, setRadioLoading, playTrack, enqueue } = deps;
|
|
setRadioLoading(true);
|
|
try {
|
|
// Fire both fetches in parallel
|
|
const topPromise = getTopSongs(artist.name);
|
|
const similarPromise = getSimilarSongs2(artist.id, 50);
|
|
|
|
// Start playing as soon as top songs arrive
|
|
const top = await topPromise;
|
|
if (top.length > 0) {
|
|
const firstTrack = songToTrack(top[0]);
|
|
playTrack(firstTrack, [firstTrack]);
|
|
setRadioLoading(false);
|
|
// Enqueue remaining tracks when similar songs arrive
|
|
const similar = await similarPromise;
|
|
const remaining = [...top.slice(1), ...similar].map(songToTrack);
|
|
if (remaining.length > 0) enqueue(remaining);
|
|
} else {
|
|
// No top songs — fall back to similar
|
|
const similar = await similarPromise;
|
|
if (similar.length > 0) {
|
|
const tracks = similar.map(songToTrack);
|
|
playTrack(tracks[0], tracks);
|
|
} else {
|
|
alert(t('artistDetail.noRadio'));
|
|
}
|
|
setRadioLoading(false);
|
|
}
|
|
} catch (e) {
|
|
console.error('Radio start failed', e);
|
|
setRadioLoading(false);
|
|
}
|
|
}
|