mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +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.
105 lines
4.0 KiB
TypeScript
105 lines
4.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { lastfmGetSimilarArtists, lastfmIsConfigured } from '../api/lastfm';
|
|
import { search } from '../api/subsonicSearch';
|
|
import type { SubsonicArtist, SubsonicArtistInfo } from '../api/subsonicTypes';
|
|
import { useAuthStore } from '../store/authStore';
|
|
|
|
export interface ArtistSimilarArtistsResult {
|
|
similarArtists: SubsonicArtist[];
|
|
similarLoading: boolean;
|
|
}
|
|
|
|
/**
|
|
* Resolves the "Similar Artists" list for the current artist:
|
|
* - Default: Last.fm getSimilar → server search for each name → keep first exact match.
|
|
* - With audiomuseNavidromeEnabled on: prefer info.similarArtist; fall back to Last.fm
|
|
* when the server returns nothing and Last.fm is configured.
|
|
*/
|
|
export function useArtistSimilarArtists(
|
|
artist: SubsonicArtist | null,
|
|
info: SubsonicArtistInfo | null,
|
|
artistInfoLoading: boolean,
|
|
): ArtistSimilarArtistsResult {
|
|
const audiomuseNavidromeEnabled = useAuthStore(
|
|
s => !!(s.activeServerId && s.audiomuseNavidromeByServer[s.activeServerId]),
|
|
);
|
|
const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion);
|
|
|
|
const [similarArtists, setSimilarArtists] = useState<SubsonicArtist[]>([]);
|
|
const [similarLoading, setSimilarLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!artist || audiomuseNavidromeEnabled || !lastfmIsConfigured()) return;
|
|
setSimilarArtists([]);
|
|
setSimilarLoading(true);
|
|
lastfmGetSimilarArtists(artist.name).then(async names => {
|
|
if (names.length === 0) { setSimilarLoading(false); return; }
|
|
const results = await Promise.all(
|
|
names.slice(0, 30).map(name =>
|
|
search(name, { artistCount: 3, albumCount: 0, songCount: 0 }).catch(() => ({ artists: [], albums: [], songs: [] }))
|
|
)
|
|
);
|
|
const seen = new Set<string>([artist.id]);
|
|
const found: SubsonicArtist[] = [];
|
|
for (let i = 0; i < results.length; i++) {
|
|
const targetName = names[i].toLowerCase();
|
|
const match = results[i].artists.find(a => a.name.toLowerCase() === targetName);
|
|
if (match && !seen.has(match.id)) {
|
|
seen.add(match.id);
|
|
found.push(match);
|
|
}
|
|
}
|
|
setSimilarArtists(found);
|
|
setSimilarLoading(false);
|
|
}).catch(() => setSimilarLoading(false));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [artist?.id, musicLibraryFilterVersion, audiomuseNavidromeEnabled]);
|
|
|
|
/** When AudioMuse is on but the server returns no similar artists, fall back to Last.fm (if configured). */
|
|
useEffect(() => {
|
|
if (!artist || !audiomuseNavidromeEnabled || !lastfmIsConfigured()) return;
|
|
if (artistInfoLoading) return;
|
|
if ((info?.similarArtist?.length ?? 0) > 0) return;
|
|
|
|
setSimilarArtists([]);
|
|
setSimilarLoading(true);
|
|
lastfmGetSimilarArtists(artist.name).then(async names => {
|
|
if (names.length === 0) { setSimilarLoading(false); return; }
|
|
const results = await Promise.all(
|
|
names.slice(0, 30).map(name =>
|
|
search(name, { artistCount: 3, albumCount: 0, songCount: 0 }).catch(() => ({ artists: [], albums: [], songs: [] }))
|
|
)
|
|
);
|
|
const seen = new Set<string>([artist.id]);
|
|
const found: SubsonicArtist[] = [];
|
|
for (let i = 0; i < results.length; i++) {
|
|
const targetName = names[i].toLowerCase();
|
|
const match = results[i].artists.find(a => a.name.toLowerCase() === targetName);
|
|
if (match && !seen.has(match.id)) {
|
|
seen.add(match.id);
|
|
found.push(match);
|
|
}
|
|
}
|
|
setSimilarArtists(found);
|
|
setSimilarLoading(false);
|
|
}).catch(() => setSimilarLoading(false));
|
|
}, [
|
|
artist?.id,
|
|
artist?.name,
|
|
musicLibraryFilterVersion,
|
|
audiomuseNavidromeEnabled,
|
|
artistInfoLoading,
|
|
info?.similarArtist?.length,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!audiomuseNavidromeEnabled) return;
|
|
if ((info?.similarArtist?.length ?? 0) > 0) {
|
|
setSimilarArtists([]);
|
|
setSimilarLoading(false);
|
|
}
|
|
}, [artist?.id, audiomuseNavidromeEnabled, info?.similarArtist?.length]);
|
|
|
|
return { similarArtists, similarLoading };
|
|
}
|