mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25: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.
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { search } from '../api/subsonicSearch';
|
|
import { getArtist, getArtistInfo, getTopSongs } from '../api/subsonicArtists';
|
|
import type {
|
|
SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo, SubsonicSong,
|
|
} from '../api/subsonicTypes';
|
|
import { useAuthStore } from '../store/authStore';
|
|
|
|
export interface ArtistDetailDataResult {
|
|
artist: SubsonicArtist | null;
|
|
setArtist: React.Dispatch<React.SetStateAction<SubsonicArtist | null>>;
|
|
albums: SubsonicAlbum[];
|
|
topSongs: SubsonicSong[];
|
|
info: SubsonicArtistInfo | null;
|
|
featuredAlbums: SubsonicAlbum[];
|
|
loading: boolean;
|
|
artistInfoLoading: boolean;
|
|
featuredLoading: boolean;
|
|
isStarred: boolean;
|
|
setIsStarred: React.Dispatch<React.SetStateAction<boolean>>;
|
|
}
|
|
|
|
export function useArtistDetailData(id: string | undefined): ArtistDetailDataResult {
|
|
const audiomuseNavidromeEnabled = useAuthStore(
|
|
s => !!(s.activeServerId && s.audiomuseNavidromeByServer[s.activeServerId]),
|
|
);
|
|
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
|
|
|
|
const [artist, setArtist] = useState<SubsonicArtist | null>(null);
|
|
const [albums, setAlbums] = useState<SubsonicAlbum[]>([]);
|
|
const [featuredAlbums, setFeaturedAlbums] = useState<SubsonicAlbum[]>([]);
|
|
const [topSongs, setTopSongs] = useState<SubsonicSong[]>([]);
|
|
const [info, setInfo] = useState<SubsonicArtistInfo | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isStarred, setIsStarred] = useState(false);
|
|
const [artistInfoLoading, setArtistInfoLoading] = useState(false);
|
|
const [featuredLoading, setFeaturedLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setInfo(null);
|
|
setTopSongs([]);
|
|
setFeaturedAlbums([]);
|
|
getArtist(id).then(artistData => {
|
|
if (cancelled) return;
|
|
setArtist(artistData.artist);
|
|
setAlbums(artistData.albums);
|
|
setIsStarred(!!artistData.artist.starred);
|
|
// Render the page immediately from local data
|
|
setLoading(false);
|
|
|
|
getTopSongs(artistData.artist.name).then(songsData => {
|
|
if (!cancelled) setTopSongs(songsData ?? []);
|
|
}).catch(() => {});
|
|
}).catch(err => {
|
|
if (!cancelled) { console.error(err); setLoading(false); }
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
let cancelled = false;
|
|
setArtistInfoLoading(true);
|
|
getArtistInfo(id, { similarArtistCount: audiomuseNavidromeEnabled ? 24 : undefined })
|
|
.then(artistInfo => {
|
|
if (!cancelled) setInfo(artistInfo ?? null);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setInfo(null);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setArtistInfoLoading(false);
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [id, audiomuseNavidromeEnabled]);
|
|
|
|
// "Also Featured On" — loaded in background after main content renders
|
|
useEffect(() => {
|
|
if (!id || !artist) return;
|
|
const ownAlbumIds = new Set(albums.map(a => a.id));
|
|
setFeaturedLoading(true);
|
|
search(artist.name, { songCount: 500, artistCount: 0, albumCount: 0 })
|
|
.catch(() => ({ songs: [], albums: [], artists: [] }))
|
|
.then(searchResults => {
|
|
const featuredSongs = (searchResults.songs ?? []).filter(
|
|
song => song.artistId === id && !ownAlbumIds.has(song.albumId)
|
|
);
|
|
const albumMap = new Map<string, SubsonicAlbum>();
|
|
featuredSongs.forEach(song => {
|
|
if (!albumMap.has(song.albumId)) {
|
|
albumMap.set(song.albumId, {
|
|
id: song.albumId,
|
|
name: song.album,
|
|
artist: song.albumArtist ?? '',
|
|
artistId: '',
|
|
coverArt: song.coverArt,
|
|
songCount: 1,
|
|
duration: song.duration,
|
|
year: song.year,
|
|
});
|
|
} else {
|
|
const a = albumMap.get(song.albumId)!;
|
|
a.songCount++;
|
|
a.duration += song.duration;
|
|
}
|
|
});
|
|
setFeaturedAlbums([...albumMap.values()]);
|
|
setFeaturedLoading(false);
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [artist?.id, musicLibraryFilterVersion]);
|
|
|
|
return {
|
|
artist, setArtist, albums, topSongs, info, featuredAlbums,
|
|
loading, artistInfoLoading, featuredLoading,
|
|
isStarred, setIsStarred,
|
|
};
|
|
}
|