Files
SpotiFLAC/frontend/src/hooks/useMetadata.ts
T
2026-03-25 13:43:14 +07:00

336 lines
13 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { getSettings } from "@/lib/settings";
import { fetchSpotifyMetadata } from "@/lib/api";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { logger } from "@/lib/logger";
import { AddFetchHistory } from "../../wailsjs/go/main/App";
import { EventsOff, EventsOn } from "../../wailsjs/runtime/runtime";
import type { SpotifyMetadataResponse } from "@/types/api";
export function useMetadata() {
const [loading, setLoading] = useState(false);
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
const loadingToastId = useRef<string | number | null>(null);
const fetchedCount = useRef(0);
const currentName = useRef("");
const [showApiModal, setShowApiModal] = useState(false);
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
const [selectedAlbum, setSelectedAlbum] = useState<{
id: string;
name: string;
external_urls: string;
} | null>(null);
const [pendingArtistName, setPendingArtistName] = useState<string | null>(null);
useEffect(() => {
if (loading) {
fetchedCount.current = 0;
currentName.current = "";
loadingToastId.current = toast.silentInfo("fetching metadata...", {
duration: Infinity,
description: "please wait while we retrieve the information"
});
return;
}
if (loadingToastId.current) {
toast.dismiss(loadingToastId.current);
loadingToastId.current = null;
}
}, [loading]);
useEffect(() => {
const handler = (data: any) => {
if (!data) {
return;
}
if (Array.isArray(data)) {
fetchedCount.current += data.length;
if (loadingToastId.current && currentName.current) {
toast.silentInfo(`fetching tracks for ${currentName.current.toLowerCase()}...`, {
id: loadingToastId.current,
description: `${fetchedCount.current.toLocaleString()} tracks fetched`
});
}
}
else {
const baseInfo = data;
const name = "artist_info" in baseInfo ? baseInfo.artist_info.name :
"album_info" in baseInfo ? baseInfo.album_info.name :
"playlist_info" in baseInfo ? (baseInfo.playlist_info.name || baseInfo.playlist_info.owner.name) : "";
if (name) {
currentName.current = name;
if (loadingToastId.current) {
toast.silentInfo(`fetching tracks for ${name.toLowerCase()}...`, {
id: loadingToastId.current,
description: `${fetchedCount.current.toLocaleString()} tracks fetched`
});
}
}
}
setMetadata(prev => {
if (Array.isArray(data)) {
if (!prev || !("track_list" in prev)) {
return prev;
}
return {
...prev,
track_list: [...prev.track_list, ...data]
};
}
if (prev && "track_list" in prev && prev.track_list.length > 0) {
return prev;
}
const baseInfo = data;
if (!("track_list" in baseInfo)) {
baseInfo.track_list = [];
}
return baseInfo;
});
};
EventsOn("metadata-stream", handler);
return () => EventsOff("metadata-stream");
}, []);
const getUrlType = (url: string): string => {
if (url.includes("/track/"))
return "track";
if (url.includes("/album/"))
return "album";
if (url.includes("/playlist/"))
return "playlist";
if (url.includes("/artist/"))
return "artist";
return "unknown";
};
const saveToHistory = async (url: string, data: SpotifyMetadataResponse) => {
try {
let name = "";
let info = "";
let image = "";
let type = "unknown";
if ("track" in data) {
type = "track";
name = data.track.name;
info = data.track.artists;
image = (data.track.images && data.track.images.length > 0) ? data.track.images : "";
}
else if ("album_info" in data) {
type = "album";
name = data.album_info.name;
info = `${data.track_list.length} tracks`;
image = data.album_info.images;
}
else if ("playlist_info" in data) {
type = "playlist";
if (data.playlist_info.name) {
name = data.playlist_info.name;
}
else if (data.playlist_info.owner.name) {
name = data.playlist_info.owner.name;
}
info = `${data.playlist_info.tracks.total} tracks`;
image = data.playlist_info.cover || "";
}
else if ("artist_info" in data) {
type = "artist";
name = data.artist_info.name;
info = `${data.artist_info.total_albums || data.album_list.length} albums`;
image = data.artist_info.images;
}
const jsonStr = JSON.stringify(data);
await AddFetchHistory({
id: crypto.randomUUID(),
url: url,
type: type,
name: name,
info: info,
image: image,
data: jsonStr,
timestamp: Math.floor(Date.now() / 1000)
});
}
catch (err) {
console.error("Failed to save fetch history:", err);
}
};
const fetchMetadataDirectly = async (url: string) => {
const urlType = getUrlType(url);
logger.info(`fetching ${urlType} metadata...`);
logger.debug(`url: ${url}`);
setLoading(true);
setMetadata(null);
try {
const startTime = Date.now();
const timeout = urlType === "artist" ? 60 : 300;
const data = await fetchSpotifyMetadata(url, true, 1.0, timeout);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
if ("playlist_info" in data) {
const playlistInfo = data.playlist_info;
if (!playlistInfo.owner.name && playlistInfo.tracks.total === 0 && data.track_list.length === 0) {
logger.warning("playlist appears to be empty or private");
toast.error("Playlist not found or may be private");
setMetadata(null);
return;
}
}
else if ("album_info" in data) {
const albumInfo = data.album_info;
if (!albumInfo.name && albumInfo.total_tracks === 0 && data.track_list.length === 0) {
logger.warning("album appears to be empty or not found");
toast.error("Album not found or may be private");
setMetadata(null);
return;
}
}
setMetadata(data);
saveToHistory(url, data);
if ("track" in data) {
logger.success(`fetched track: ${data.track.name} - ${data.track.artists}`);
logger.debug(`duration: ${data.track.duration_ms}ms`);
}
else if ("album_info" in data) {
logger.success(`fetched album: ${data.album_info.name}`);
logger.debug(`${data.track_list.length} tracks, released: ${data.album_info.release_date}`);
}
else if ("playlist_info" in data) {
logger.success(`fetched playlist: ${data.track_list.length} tracks`);
logger.debug(`by ${data.playlist_info.owner.display_name || data.playlist_info.owner.name}`);
}
else if ("artist_info" in data) {
logger.success(`fetched artist: ${data.artist_info.name}`);
logger.debug(`${data.album_list.length} albums, ${data.track_list.length} tracks`);
}
logger.info(`fetch completed in ${elapsed}s`);
toast.success("Metadata fetched successfully");
}
catch (err) {
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
logger.error(`fetch failed: ${errorMsg}`);
const settings = getSettings();
if (!settings.useSpotFetchAPI) {
setShowApiModal(true);
}
else {
toast.error(errorMsg);
}
}
finally {
setLoading(false);
}
};
const loadFromCache = (cachedData: string) => {
try {
const data = JSON.parse(cachedData);
setMetadata(data);
toast.success("Loaded from cache");
}
catch (err) {
console.error("Failed to load from cache:", err);
toast.error("Failed to load from cache");
}
};
const handleFetchMetadata = async (url: string) => {
if (!url.trim()) {
logger.warning("empty url provided");
toast.error("Please enter a Spotify URL");
return;
}
let urlToFetch = url.trim();
const isArtistUrl = urlToFetch.includes("/artist/");
if (isArtistUrl && !urlToFetch.includes("/discography")) {
urlToFetch = urlToFetch.replace(/\/$/, "") + "/discography/all";
logger.debug("converted to discography url");
}
if (isArtistUrl) {
logger.info("artist url detected");
setPendingArtistName(null);
await fetchMetadataDirectly(urlToFetch);
}
else {
await fetchMetadataDirectly(urlToFetch);
}
return urlToFetch;
};
const handleAlbumClick = (album: {
id: string;
name: string;
external_urls: string;
}) => {
logger.debug(`album clicked: ${album.name}`);
setSelectedAlbum(album);
setShowAlbumDialog(true);
};
const handleArtistClick = async (artist: {
id: string;
name: string;
external_urls: string;
}) => {
logger.debug(`artist clicked: ${artist.name}`);
const artistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
setPendingArtistName(artist.name);
await fetchMetadataDirectly(artistUrl);
return artistUrl;
};
const handleConfirmAlbumFetch = async () => {
if (!selectedAlbum)
return;
const albumUrl = selectedAlbum.external_urls;
logger.info(`fetching album: ${selectedAlbum.name}...`);
logger.debug(`url: ${albumUrl}`);
setShowAlbumDialog(false);
setLoading(true);
setMetadata(null);
try {
const startTime = Date.now();
const data = await fetchSpotifyMetadata(albumUrl);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
if ("album_info" in data) {
const albumInfo = data.album_info;
if (!albumInfo.name && albumInfo.total_tracks === 0 && data.track_list.length === 0) {
logger.warning("album appears to be empty or not found");
toast.error("Album not found or may be private");
setMetadata(null);
setSelectedAlbum(null);
return albumUrl;
}
}
setMetadata(data);
saveToHistory(albumUrl, data);
if ("album_info" in data) {
logger.success(`fetched album: ${data.album_info.name}`);
logger.debug(`${data.track_list.length} tracks, released: ${data.album_info.release_date}`);
}
logger.info(`fetch completed in ${elapsed}s`);
toast.success("Album metadata fetched successfully");
return albumUrl;
}
catch (err) {
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
logger.error(`fetch failed: ${errorMsg}`);
const settings = getSettings();
if (!settings.useSpotFetchAPI) {
setShowApiModal(true);
}
else {
toast.error(errorMsg);
}
}
finally {
setLoading(false);
setSelectedAlbum(null);
}
};
return {
loading,
metadata,
showAlbumDialog,
setShowAlbumDialog,
selectedAlbum,
pendingArtistName,
handleFetchMetadata,
handleAlbumClick,
handleConfirmAlbumFetch,
handleArtistClick,
loadFromCache,
showApiModal,
setShowApiModal,
resetMetadata: () => setMetadata(null),
};
}