v5.7-beta2

This commit is contained in:
afkarxyz
2025-11-22 12:06:00 +07:00
parent 1c9bba0140
commit ee2976143a
13 changed files with 1759 additions and 1183 deletions
+290
View File
@@ -0,0 +1,290 @@
import { useState, useRef } from "react";
import { downloadTrack } from "@/lib/api";
import { getSettings } from "@/lib/settings";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath } from "@/lib/utils";
import type { TrackMetadata } from "@/types/api";
export function useDownload() {
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadingTrack, setDownloadingTrack] = useState<string | null>(null);
const [bulkDownloadType, setBulkDownloadType] = useState<"all" | "selected" | null>(null);
const [downloadedTracks, setDownloadedTracks] = useState<Set<string>>(new Set());
const [currentDownloadInfo, setCurrentDownloadInfo] = useState<{
name: string;
artists: string;
} | null>(null);
const shouldStopDownloadRef = useRef(false);
const downloadWithAutoFallback = async (
isrc: string,
settings: any,
trackName?: string,
artistName?: string,
albumName?: string,
playlistName?: string,
isArtistDiscography?: boolean
) => {
let service = settings.downloader;
const query = trackName && artistName ? `${trackName} ${artistName}` : undefined;
const os = settings.operatingSystem;
let outputDir = settings.downloadPath;
if (playlistName) {
outputDir = joinPath(os, outputDir, sanitizePath(playlistName, os));
if (isArtistDiscography) {
if (settings.albumSubfolder && albumName) {
outputDir = joinPath(os, outputDir, sanitizePath(albumName, os));
}
} else {
if (settings.artistSubfolder && artistName) {
outputDir = joinPath(os, outputDir, sanitizePath(artistName, os));
}
if (settings.albumSubfolder && albumName) {
outputDir = joinPath(os, outputDir, sanitizePath(albumName, os));
}
}
}
if (service === "auto") {
try {
const tidalResponse = await downloadTrack({
isrc,
service: "tidal",
query,
output_dir: outputDir,
filename_format: settings.filenameFormat,
track_number: settings.trackNumber,
});
if (tidalResponse.success) {
return tidalResponse;
}
service = "deezer";
} catch (tidalErr) {
service = "deezer";
}
}
return await downloadTrack({
isrc,
service: service as "deezer" | "tidal",
query,
output_dir: outputDir,
filename_format: settings.filenameFormat,
track_number: settings.trackNumber,
});
};
const handleDownloadTrack = async (
isrc: string,
trackName?: string,
artistName?: string,
albumName?: string
) => {
if (!isrc) {
toast.error("No ISRC found for this track");
return;
}
const settings = getSettings();
setDownloadingTrack(isrc);
try {
const response = await downloadWithAutoFallback(
isrc,
settings,
trackName,
artistName,
albumName,
undefined,
false
);
if (response.success) {
toast.success(response.message);
setDownloadedTracks((prev) => new Set(prev).add(isrc));
} else {
toast.error(response.error || "Download failed");
}
} catch (err) {
toast.error(err instanceof Error ? err.message : "Download failed");
} finally {
setDownloadingTrack(null);
}
};
const handleDownloadSelected = async (
selectedTracks: string[],
allTracks: TrackMetadata[],
playlistName?: string,
isArtistDiscography?: boolean
) => {
if (selectedTracks.length === 0) {
toast.error("No tracks selected");
return;
}
const settings = getSettings();
setIsDownloading(true);
setBulkDownloadType("selected");
setDownloadProgress(0);
let successCount = 0;
let errorCount = 0;
const total = selectedTracks.length;
for (let i = 0; i < selectedTracks.length; i++) {
if (shouldStopDownloadRef.current) {
toast.info(
`Download stopped. ${successCount} tracks downloaded, ${selectedTracks.length - i} skipped.`
);
break;
}
const isrc = selectedTracks[i];
const track = allTracks.find((t) => t.isrc === isrc);
setDownloadingTrack(isrc);
if (track) {
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
}
try {
const response = await downloadWithAutoFallback(
isrc,
settings,
track?.name,
track?.artists,
track?.album_name,
playlistName,
isArtistDiscography
);
if (response.success) {
successCount++;
setDownloadedTracks((prev) => new Set(prev).add(isrc));
} else {
errorCount++;
}
} catch (err) {
errorCount++;
}
setDownloadProgress(Math.round(((i + 1) / total) * 100));
}
setDownloadingTrack(null);
setCurrentDownloadInfo(null);
setIsDownloading(false);
setBulkDownloadType(null);
shouldStopDownloadRef.current = false;
if (errorCount === 0) {
toast.success(`Downloaded ${successCount} tracks successfully`);
} else {
toast.warning(`Downloaded ${successCount} tracks, ${errorCount} failed`);
}
};
const handleDownloadAll = async (
tracks: TrackMetadata[],
playlistName?: string,
isArtistDiscography?: boolean
) => {
const tracksWithIsrc = tracks.filter((track) => track.isrc);
if (tracksWithIsrc.length === 0) {
toast.error("No tracks available for download");
return;
}
const settings = getSettings();
setIsDownloading(true);
setBulkDownloadType("all");
setDownloadProgress(0);
let successCount = 0;
let errorCount = 0;
const total = tracksWithIsrc.length;
for (let i = 0; i < tracksWithIsrc.length; i++) {
if (shouldStopDownloadRef.current) {
toast.info(
`Download stopped. ${successCount} tracks downloaded, ${tracksWithIsrc.length - i} skipped.`
);
break;
}
const track = tracksWithIsrc[i];
setDownloadingTrack(track.isrc);
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
try {
const response = await downloadWithAutoFallback(
track.isrc,
settings,
track.name,
track.artists,
track.album_name,
playlistName,
isArtistDiscography
);
if (response.success) {
successCount++;
setDownloadedTracks((prev) => new Set(prev).add(track.isrc));
} else {
errorCount++;
}
} catch (err) {
errorCount++;
}
setDownloadProgress(Math.round(((i + 1) / total) * 100));
}
setDownloadingTrack(null);
setCurrentDownloadInfo(null);
setIsDownloading(false);
setBulkDownloadType(null);
shouldStopDownloadRef.current = false;
if (errorCount === 0) {
toast.success(`Downloaded ${successCount} tracks successfully`);
} else {
toast.warning(`Downloaded ${successCount} tracks, ${errorCount} failed`);
}
};
const handleStopDownload = () => {
shouldStopDownloadRef.current = true;
toast.info("Stopping download...");
};
const resetDownloadedTracks = () => {
setDownloadedTracks(new Set());
};
return {
downloadProgress,
isDownloading,
downloadingTrack,
bulkDownloadType,
downloadedTracks,
currentDownloadInfo,
handleDownloadTrack,
handleDownloadSelected,
handleDownloadAll,
handleStopDownload,
resetDownloadedTracks,
};
}
+116
View File
@@ -0,0 +1,116 @@
import { useState } from "react";
import { fetchSpotifyMetadata } from "@/lib/api";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import type { SpotifyMetadataResponse } from "@/types/api";
export function useMetadata() {
const [loading, setLoading] = useState(false);
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
const [showTimeoutDialog, setShowTimeoutDialog] = useState(false);
const [timeoutValue, setTimeoutValue] = useState(60);
const [pendingUrl, setPendingUrl] = useState("");
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
const [selectedAlbum, setSelectedAlbum] = useState<{
id: string;
name: string;
external_urls: string;
} | null>(null);
const fetchMetadataDirectly = async (url: string) => {
setLoading(true);
setMetadata(null);
try {
const data = await fetchSpotifyMetadata(url);
setMetadata(data);
toast.success("Metadata fetched successfully");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to fetch metadata");
} finally {
setLoading(false);
}
};
const handleFetchMetadata = async (url: string) => {
if (!url.trim()) {
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";
}
if (isArtistUrl) {
setPendingUrl(urlToFetch);
setShowTimeoutDialog(true);
} else {
await fetchMetadataDirectly(urlToFetch);
}
return urlToFetch;
};
const handleConfirmFetch = async () => {
setShowTimeoutDialog(false);
setLoading(true);
setMetadata(null);
try {
const data = await fetchSpotifyMetadata(pendingUrl, true, 1.0, timeoutValue);
setMetadata(data);
toast.success("Metadata fetched successfully");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to fetch metadata");
} finally {
setLoading(false);
}
};
const handleAlbumClick = (album: {
id: string;
name: string;
external_urls: string;
}) => {
setSelectedAlbum(album);
setShowAlbumDialog(true);
};
const handleConfirmAlbumFetch = async () => {
if (!selectedAlbum) return;
setShowAlbumDialog(false);
setLoading(true);
setMetadata(null);
try {
const data = await fetchSpotifyMetadata(selectedAlbum.external_urls);
setMetadata(data);
toast.success("Album metadata fetched successfully");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to fetch album metadata");
} finally {
setLoading(false);
setSelectedAlbum(null);
}
};
return {
loading,
metadata,
showTimeoutDialog,
setShowTimeoutDialog,
timeoutValue,
setTimeoutValue,
showAlbumDialog,
setShowAlbumDialog,
selectedAlbum,
handleFetchMetadata,
handleConfirmFetch,
handleAlbumClick,
handleConfirmAlbumFetch,
};
}