Files
Psychotoxical-psysonic/src/utils/runArtistDetailPlay.ts
T
Frank Stellmacher ba0bf8aa9d refactor(artist-detail): G.84 — extract helpers + suggestion cover + 2 data hooks + play utilities (cluster) (#651)
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.
2026-05-13 17:07:53 +02:00

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);
}
}