This commit is contained in:
afkarxyz
2026-01-27 06:16:05 +07:00
parent e04f6e4fdd
commit 25233349b9
38 changed files with 2492 additions and 1682 deletions
+7 -5
View File
@@ -19,6 +19,7 @@ interface CheckFileExistenceRequest {
filename_format?: string;
include_track_number?: boolean;
audio_format?: string;
relative_path?: string;
}
interface FileExistenceResult {
spotify_id: string;
@@ -29,7 +30,7 @@ interface FileExistenceResult {
}
const CheckFilesExistence = (outputDir: string, tracks: CheckFileExistenceRequest[]): Promise<FileExistenceResult[]> => (window as any)["go"]["main"]["App"]["CheckFilesExistence"](outputDir, tracks);
const SkipDownloadItem = (itemID: string, filePath: string): Promise<void> => (window as any)["go"]["main"]["App"]["SkipDownloadItem"](itemID, filePath);
export function useDownload() {
export function useDownload(region: string) {
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadingTrack, setDownloadingTrack] = useState<string | null>(null);
@@ -141,7 +142,7 @@ export function useDownload() {
if (spotifyId) {
try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId);
const urlsJson = await GetStreamingURLs(spotifyId, region);
streamingURLs = JSON.parse(urlsJson);
}
catch (err) {
@@ -372,8 +373,9 @@ export function useDownload() {
year: yearValue,
playlist: folderName?.replace(/\//g, placeholder),
};
const useAlbumTag = settings.folderTemplate?.includes("{album}");
if (folderName && (!isAlbum || !useAlbumTag)) {
const folderTemplate = settings.folderTemplate || "";
const useAlbumSubfolder = folderTemplate.includes("{album}") || folderTemplate.includes("{album_artist}") || folderTemplate.includes("{playlist}");
if (folderName && (!isAlbum || !useAlbumSubfolder)) {
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
}
if (settings.folderTemplate) {
@@ -391,7 +393,7 @@ export function useDownload() {
if (spotifyId) {
try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId);
const urlsJson = await GetStreamingURLs(spotifyId, region);
streamingURLs = JSON.parse(urlsJson);
}
catch (err) {
+72 -41
View File
@@ -2,13 +2,11 @@ import { useState } from "react";
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 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;
@@ -27,6 +25,57 @@ export function useMetadata() {
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...`);
@@ -35,7 +84,8 @@ export function useMetadata() {
setMetadata(null);
try {
const startTime = Date.now();
const data = await fetchSpotifyMetadata(url);
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;
@@ -56,6 +106,7 @@ export function useMetadata() {
}
}
setMetadata(data);
saveToHistory(url, data);
if ("track" in data) {
logger.success(`fetched track: ${data.track.name} - ${data.track.artists}`);
logger.debug(`isrc: ${data.track.isrc}, duration: ${data.track.duration_ms}ms`);
@@ -84,6 +135,17 @@ export function useMetadata() {
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");
@@ -97,43 +159,15 @@ export function useMetadata() {
logger.debug("converted to discography url");
}
if (isArtistUrl) {
logger.info("artist url detected, showing timeout dialog");
setPendingUrl(urlToFetch);
logger.info("artist url detected");
setPendingArtistName(null);
setShowTimeoutDialog(true);
await fetchMetadataDirectly(urlToFetch);
}
else {
await fetchMetadataDirectly(urlToFetch);
}
return urlToFetch;
};
const handleConfirmFetch = async () => {
setShowTimeoutDialog(false);
logger.info(`fetching artist discography (timeout: ${timeoutValue}s)...`);
logger.debug(`url: ${pendingUrl}`);
setLoading(true);
setMetadata(null);
try {
const startTime = Date.now();
const data = await fetchSpotifyMetadata(pendingUrl, true, 1.0, timeoutValue);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
setMetadata(data);
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}`);
toast.error(errorMsg);
}
finally {
setLoading(false);
}
};
const handleAlbumClick = (album: {
id: string;
name: string;
@@ -150,9 +184,8 @@ export function useMetadata() {
}) => {
logger.debug(`artist clicked: ${artist.name}`);
const artistUrl = artist.external_urls.replace(/\/$/, "") + "/discography/all";
setPendingUrl(artistUrl);
setPendingArtistName(artist.name);
setShowTimeoutDialog(true);
await fetchMetadataDirectly(artistUrl);
return artistUrl;
};
const handleConfirmAlbumFetch = async () => {
@@ -179,6 +212,7 @@ export function useMetadata() {
}
}
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}`);
@@ -200,18 +234,15 @@ export function useMetadata() {
return {
loading,
metadata,
showTimeoutDialog,
setShowTimeoutDialog,
timeoutValue,
setTimeoutValue,
showAlbumDialog,
setShowAlbumDialog,
selectedAlbum,
pendingArtistName,
handleFetchMetadata,
handleConfirmFetch,
handleAlbumClick,
handleConfirmAlbumFetch,
handleArtistClick,
loadFromCache,
resetMetadata: () => setMetadata(null),
};
}
+9 -1
View File
@@ -1,10 +1,18 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { GetPreviewURL } from "@/../wailsjs/go/main/App";
import { toast } from "sonner";
export function usePreview() {
const [loadingPreview, setLoadingPreview] = useState<string | null>(null);
const [currentAudio, setCurrentAudio] = useState<HTMLAudioElement | null>(null);
const [playingTrack, setPlayingTrack] = useState<string | null>(null);
useEffect(() => {
return () => {
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
}
};
}, [currentAudio]);
const playPreview = async (trackId: string, trackName: string) => {
try {
if (playingTrack === trackId && currentAudio) {
+35
View File
@@ -0,0 +1,35 @@
import { useState, useEffect } from 'react';
export function useTypingEffect(texts: string[], typingSpeed: number = 50, deletingSpeed: number = 50, pauseDuration: number = 1500) {
const [displayedText, setDisplayedText] = useState('');
const [isDeleting, setIsDeleting] = useState(false);
const [textIndex, setTextIndex] = useState(0);
useEffect(() => {
setDisplayedText("");
setIsDeleting(false);
setTextIndex(0);
}, [texts]);
useEffect(() => {
const currentText = texts[textIndex % texts.length];
let timer: ReturnType<typeof setTimeout>;
if (isDeleting) {
timer = setTimeout(() => {
setDisplayedText((prev) => prev.substring(0, prev.length - 1));
}, deletingSpeed);
}
else {
timer = setTimeout(() => {
setDisplayedText((prev) => currentText.substring(0, prev.length + 1));
}, typingSpeed);
}
if (!isDeleting && displayedText === currentText) {
clearTimeout(timer);
timer = setTimeout(() => setIsDeleting(true), pauseDuration);
}
else if (isDeleting && displayedText === '') {
setIsDeleting(false);
setTextIndex((prev) => (prev + 1) % texts.length);
}
return () => clearTimeout(timer);
}, [displayedText, isDeleting, textIndex, texts, typingSpeed, deletingSpeed, pauseDuration]);
return displayedText;
}