Major Features: - Multi-source download system (Soulseek/Lidarr with fallback) - Configurable enrichment speed control (1-5x) - Mobile touch drag support for seek sliders - iOS PWA media controls (Control Center, Lock Screen) - Artist name alias resolution via Last.fm - Circuit breaker pattern for audio analysis Critical Fixes: - Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM) - Discovery system race conditions and import failures - Radio decade categorization using originalYear - LastFM API response normalization - Mood bucket infinite loop prevention Security: - Bull Board admin authentication - Lidarr webhook signature verification - JWT token expiration and refresh - Encryption key validation on startup Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { useParams, useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { queryKeys } from "@/hooks/useQueries";
|
|
import { api } from "@/lib/api";
|
|
import { useDownloadContext } from "@/lib/download-context";
|
|
import { Artist, Album, ArtistSource } from "../types";
|
|
import { useMemo, useEffect, useRef, useState } from "react";
|
|
|
|
export function useArtistData() {
|
|
const params = useParams();
|
|
const router = useRouter();
|
|
const id = params.id as string;
|
|
const { downloadStatus } = useDownloadContext();
|
|
const prevActiveCountRef = useRef(downloadStatus.activeDownloads.length);
|
|
|
|
// Use React Query - no polling needed, webhook events trigger refresh via download context
|
|
const {
|
|
data: artist,
|
|
isLoading,
|
|
error,
|
|
isError,
|
|
refetch,
|
|
} = useQuery({
|
|
queryKey: queryKeys.artist(id || ""),
|
|
queryFn: async () => {
|
|
if (!id) throw new Error("Artist ID is required");
|
|
try {
|
|
return await api.getArtist(id);
|
|
} catch (error) {
|
|
return await api.getArtistDiscovery(id);
|
|
}
|
|
},
|
|
enabled: !!id,
|
|
staleTime: 10 * 60 * 1000,
|
|
retry: 1,
|
|
});
|
|
|
|
// Refetch when downloads complete (active count decreases)
|
|
useEffect(() => {
|
|
const currentActiveCount = downloadStatus.activeDownloads.length;
|
|
if (
|
|
prevActiveCountRef.current > 0 &&
|
|
currentActiveCount < prevActiveCountRef.current
|
|
) {
|
|
// Downloads have completed, refresh data
|
|
refetch();
|
|
}
|
|
prevActiveCountRef.current = currentActiveCount;
|
|
}, [downloadStatus.activeDownloads.length, refetch]);
|
|
|
|
// Determine source from the artist data (if it came from library or discovery)
|
|
const source: ArtistSource | null = useMemo(() => {
|
|
if (!artist) return null;
|
|
return artist.id && !artist.id.includes("-") ? "library" : "discovery";
|
|
}, [artist]);
|
|
|
|
// Sort state: 'year' or 'dateAdded'
|
|
const [sortBy, setSortBy] = useState<"year" | "dateAdded">("year");
|
|
|
|
// Sort albums by year or dateAdded - memoized
|
|
const albums = useMemo(() => {
|
|
if (!artist?.albums) return [];
|
|
|
|
return [...artist.albums].sort((a, b) => {
|
|
if (sortBy === "dateAdded") {
|
|
// Sort by lastSynced (newest first, nulls last)
|
|
if (!a.lastSynced && !b.lastSynced) return 0;
|
|
if (!a.lastSynced) return 1;
|
|
if (!b.lastSynced) return -1;
|
|
return (
|
|
new Date(b.lastSynced).getTime() -
|
|
new Date(a.lastSynced).getTime()
|
|
);
|
|
} else {
|
|
// Sort by year (newest first, nulls last)
|
|
if (a.year == null && b.year == null) return 0;
|
|
if (a.year == null) return 1;
|
|
if (b.year == null) return -1;
|
|
return b.year - a.year;
|
|
}
|
|
});
|
|
}, [artist?.albums, sortBy]);
|
|
|
|
// Handle errors - only show toast once, don't auto-navigate
|
|
// The page component should handle displaying a "not found" state
|
|
// Don't call router.back() as it causes navigation loops
|
|
|
|
return {
|
|
artist,
|
|
albums,
|
|
loading: isLoading,
|
|
error: isError,
|
|
source,
|
|
sortBy,
|
|
setSortBy,
|
|
reloadArtist: refetch,
|
|
};
|
|
}
|