v7.0.9
This commit is contained in:
+40
-9
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||
import { Search, X, ArrowUp } from "lucide-react";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont } from "@/lib/settings";
|
||||
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings";
|
||||
import { applyTheme } from "@/lib/themes";
|
||||
import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg } from "../wailsjs/go/main/App";
|
||||
import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
||||
@@ -119,6 +119,17 @@ function App() {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
const handleEnableSpotFetchApi = async () => {
|
||||
try {
|
||||
await updateSettings({ useSpotFetchAPI: true });
|
||||
metadata.setShowApiModal(false);
|
||||
toast.success("SpotFetch API enabled! You can now try fetching again.");
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Failed to enable SpotFetch API:", err);
|
||||
toast.error("Failed to update settings");
|
||||
}
|
||||
};
|
||||
const scrollToTop = useCallback(() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, []);
|
||||
@@ -290,19 +301,19 @@ function App() {
|
||||
setSearchQuery(value);
|
||||
setCurrentListPage(1);
|
||||
};
|
||||
const toggleTrackSelection = (isrc: string) => {
|
||||
setSelectedTracks((prev) => prev.includes(isrc) ? prev.filter((id) => id !== isrc) : [...prev, isrc]);
|
||||
const toggleTrackSelection = (id: string) => {
|
||||
setSelectedTracks((prev) => prev.includes(id) ? prev.filter((prevId) => prevId !== id) : [...prev, id]);
|
||||
};
|
||||
const toggleSelectAll = (tracks: any[]) => {
|
||||
const tracksWithIsrc = tracks.filter((track) => track.isrc).map((track) => track.isrc);
|
||||
if (tracksWithIsrc.length === 0)
|
||||
const tracksWithId = tracks.filter((track) => track.spotify_id).map((track) => track.spotify_id || "");
|
||||
if (tracksWithId.length === 0)
|
||||
return;
|
||||
const allSelected = tracksWithIsrc.every(isrc => selectedTracks.includes(isrc));
|
||||
const allSelected = tracksWithId.every(id => selectedTracks.includes(id));
|
||||
if (allSelected) {
|
||||
setSelectedTracks(prev => prev.filter(isrc => !tracksWithIsrc.includes(isrc)));
|
||||
setSelectedTracks(prev => prev.filter(id => !tracksWithId.includes(id)));
|
||||
}
|
||||
else {
|
||||
setSelectedTracks(prev => Array.from(new Set([...prev, ...tracksWithIsrc])));
|
||||
setSelectedTracks(prev => Array.from(new Set([...prev, ...tracksWithId])));
|
||||
}
|
||||
};
|
||||
const handleOpenFolder = async () => {
|
||||
@@ -324,7 +335,8 @@ function App() {
|
||||
return null;
|
||||
if ("track" in metadata.metadata) {
|
||||
const { track } = metadata.metadata;
|
||||
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(track.isrc)} isFailed={download.failedTracks.has(track.isrc)} isSkipped={download.skippedTracks.has(track.isrc)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>);
|
||||
const trackId = track.spotify_id || "";
|
||||
return (<TrackInfo track={track} isDownloading={download.isDownloading} downloadingTrack={download.downloadingTrack} isDownloaded={download.downloadedTracks.has(trackId)} isFailed={download.failedTracks.has(trackId)} isSkipped={download.skippedTracks.has(trackId)} downloadingLyricsTrack={lyrics.downloadingLyricsTrack} downloadedLyrics={lyrics.downloadedLyrics.has(track.spotify_id || "")} failedLyrics={lyrics.failedLyrics.has(track.spotify_id || "")} skippedLyrics={lyrics.skippedLyrics.has(track.spotify_id || "")} checkingAvailability={availability.checkingTrackId === track.spotify_id} availability={availability.availabilityMap.get(track.spotify_id || "")} downloadingCover={cover.downloadingCoverTrack === (track.spotify_id || `${track.name}-${track.artists}`)} downloadedCover={cover.downloadedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} failedCover={cover.failedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} skippedCover={cover.skippedCovers.has(track.spotify_id || `${track.name}-${track.artists}`)} onDownload={download.handleDownloadTrack} onDownloadLyrics={(spotifyId, name, artists, albumName, albumArtist, releaseDate, discNumber) => lyrics.handleDownloadLyrics(spotifyId, name, artists, albumName, track.album_name, undefined, albumArtist, releaseDate, discNumber)} onDownloadCover={(coverUrl, trackName, artistName, albumName, _playlistName, _position, trackId, albumArtist, releaseDate, discNumber) => cover.handleDownloadCover(coverUrl, trackName, artistName, albumName, track.album_name, undefined, trackId, albumArtist, releaseDate, discNumber)} onCheckAvailability={availability.checkAvailability} onOpenFolder={handleOpenFolder} onBack={metadata.resetMetadata}/>);
|
||||
}
|
||||
if ("album_info" in metadata.metadata) {
|
||||
const { album_info, track_list } = metadata.metadata;
|
||||
@@ -555,6 +567,25 @@ function App() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={metadata.showApiModal} onOpenChange={metadata.setShowApiModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>SpotFetch API Recommended</DialogTitle>
|
||||
<DialogDescription>
|
||||
Direct fetch failed. This usually happens when your <span className="text-foreground font-bold">country is blocked</span> by Spotify or your IP is restricted. Would you like to enable the <span className="text-foreground font-bold">SpotFetch API</span> to bypass this?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => metadata.setShowApiModal(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEnableSpotFetchApi}>
|
||||
Enable SpotFetch API
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</TooltipProvider>);
|
||||
}
|
||||
|
||||
@@ -48,9 +48,9 @@ interface AlbumInfoProps {
|
||||
isBulkDownloadingLyrics?: boolean;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSortChange: (value: string) => void;
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleTrack: (id: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
|
||||
@@ -67,9 +67,9 @@ interface ArtistInfoProps {
|
||||
isBulkDownloadingLyrics?: boolean;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSortChange: (value: string) => void;
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleTrack: (id: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
@@ -491,8 +491,8 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
|
||||
<ScrollArea className="flex-1 pr-4">
|
||||
<div className="space-y-4">
|
||||
{filteredAlbumGroups.map(([albumName, data]) => {
|
||||
const tracksWithIsrc = data.tracks.filter(t => t.isrc);
|
||||
const isSelected = tracksWithIsrc.length > 0 && tracksWithIsrc.every(t => selectedTracks.includes(t.isrc!));
|
||||
const tracksWithId = data.tracks.filter(t => t.spotify_id);
|
||||
const isSelected = tracksWithId.length > 0 && tracksWithId.every(t => selectedTracks.includes(t.spotify_id!));
|
||||
return (<div key={albumName} className="flex items-start space-x-3 p-2 hover:bg-muted/50 rounded-md transition-colors">
|
||||
<Checkbox id={`album-select-${albumName}`} checked={isSelected} onCheckedChange={() => onToggleSelectAll(data.tracks)} className="mt-1"/>
|
||||
<div className="grid gap-1.5 leading-none flex-1">
|
||||
|
||||
@@ -54,9 +54,9 @@ interface PlaylistInfoProps {
|
||||
isBulkDownloadingLyrics?: boolean;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSortChange: (value: string) => void;
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleTrack: (id: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InputWithContext } from "@/components/ui/input-with-context";
|
||||
import { CloudDownload, XCircle, Link, Search, X, ChevronDown } from "lucide-react";
|
||||
import { CloudDownload, XCircle, Link, Search, X, ChevronDown, } from "lucide-react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
|
||||
import { FetchHistory } from "@/components/FetchHistory";
|
||||
@@ -10,12 +10,13 @@ import { SearchSpotify, SearchSpotifyByType } from "../../wailsjs/go/main/App";
|
||||
import { backend } from "../../wailsjs/go/models";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTypingEffect } from "@/hooks/useTypingEffect";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||
const FETCH_PLACEHOLDERS = [
|
||||
"https://open.spotify.com/track/...",
|
||||
"https://open.spotify.com/album/...",
|
||||
"https://open.spotify.com/playlist/...",
|
||||
"https://open.spotify.com/artist/..."
|
||||
"https://open.spotify.com/artist/...",
|
||||
];
|
||||
const SEARCH_PLACEHOLDERS = [
|
||||
"Golden",
|
||||
@@ -23,10 +24,194 @@ const SEARCH_PLACEHOLDERS = [
|
||||
"The Weeknd",
|
||||
"Starboy",
|
||||
"Joji",
|
||||
"Die For You"
|
||||
"Die For You",
|
||||
];
|
||||
const REGIONS = ["AD", "AE", "AG", "AL", "AM", "AO", "AR", "AT", "AU", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS", "BT", "BW", "BZ", "CA", "CD", "CG", "CH", "CI", "CL", "CM", "CO", "CR", "CV", "CW", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ES", "ET", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE", "GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HK", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK", "ML", "MN", "MO", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE", "NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK", "PL", "PS", "PT", "PW", "PY", "QA", "RO", "RS", "RW", "SA", "SB", "SC", "SE", "SG", "SI", "SK", "SL", "SM", "SN", "SR", "ST", "SV", "SZ", "TD", "TG", "TH", "TJ", "TL", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WS", "XK", "ZA", "ZM", "ZW"];
|
||||
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' });
|
||||
const REGIONS = [
|
||||
"AD",
|
||||
"AE",
|
||||
"AG",
|
||||
"AL",
|
||||
"AM",
|
||||
"AO",
|
||||
"AR",
|
||||
"AT",
|
||||
"AU",
|
||||
"AZ",
|
||||
"BA",
|
||||
"BB",
|
||||
"BD",
|
||||
"BE",
|
||||
"BF",
|
||||
"BG",
|
||||
"BH",
|
||||
"BI",
|
||||
"BJ",
|
||||
"BN",
|
||||
"BO",
|
||||
"BR",
|
||||
"BS",
|
||||
"BT",
|
||||
"BW",
|
||||
"BZ",
|
||||
"CA",
|
||||
"CD",
|
||||
"CG",
|
||||
"CH",
|
||||
"CI",
|
||||
"CL",
|
||||
"CM",
|
||||
"CO",
|
||||
"CR",
|
||||
"CV",
|
||||
"CW",
|
||||
"CY",
|
||||
"CZ",
|
||||
"DE",
|
||||
"DJ",
|
||||
"DK",
|
||||
"DM",
|
||||
"DO",
|
||||
"DZ",
|
||||
"EC",
|
||||
"EE",
|
||||
"EG",
|
||||
"ES",
|
||||
"ET",
|
||||
"FI",
|
||||
"FJ",
|
||||
"FM",
|
||||
"FR",
|
||||
"GA",
|
||||
"GB",
|
||||
"GD",
|
||||
"GE",
|
||||
"GH",
|
||||
"GM",
|
||||
"GN",
|
||||
"GQ",
|
||||
"GR",
|
||||
"GT",
|
||||
"GW",
|
||||
"GY",
|
||||
"HK",
|
||||
"HN",
|
||||
"HR",
|
||||
"HT",
|
||||
"HU",
|
||||
"ID",
|
||||
"IE",
|
||||
"IL",
|
||||
"IN",
|
||||
"IQ",
|
||||
"IS",
|
||||
"IT",
|
||||
"JM",
|
||||
"JO",
|
||||
"JP",
|
||||
"KE",
|
||||
"KG",
|
||||
"KH",
|
||||
"KI",
|
||||
"KM",
|
||||
"KN",
|
||||
"KR",
|
||||
"KW",
|
||||
"KZ",
|
||||
"LA",
|
||||
"LB",
|
||||
"LC",
|
||||
"LI",
|
||||
"LK",
|
||||
"LR",
|
||||
"LS",
|
||||
"LT",
|
||||
"LU",
|
||||
"LV",
|
||||
"LY",
|
||||
"MA",
|
||||
"MC",
|
||||
"MD",
|
||||
"ME",
|
||||
"MG",
|
||||
"MH",
|
||||
"MK",
|
||||
"ML",
|
||||
"MN",
|
||||
"MO",
|
||||
"MR",
|
||||
"MT",
|
||||
"MU",
|
||||
"MV",
|
||||
"MW",
|
||||
"MX",
|
||||
"MY",
|
||||
"MZ",
|
||||
"NA",
|
||||
"NE",
|
||||
"NG",
|
||||
"NI",
|
||||
"NL",
|
||||
"NO",
|
||||
"NP",
|
||||
"NR",
|
||||
"NZ",
|
||||
"OM",
|
||||
"PA",
|
||||
"PE",
|
||||
"PG",
|
||||
"PH",
|
||||
"PK",
|
||||
"PL",
|
||||
"PS",
|
||||
"PT",
|
||||
"PW",
|
||||
"PY",
|
||||
"QA",
|
||||
"RO",
|
||||
"RS",
|
||||
"RW",
|
||||
"SA",
|
||||
"SB",
|
||||
"SC",
|
||||
"SE",
|
||||
"SG",
|
||||
"SI",
|
||||
"SK",
|
||||
"SL",
|
||||
"SM",
|
||||
"SN",
|
||||
"SR",
|
||||
"ST",
|
||||
"SV",
|
||||
"SZ",
|
||||
"TD",
|
||||
"TG",
|
||||
"TH",
|
||||
"TJ",
|
||||
"TL",
|
||||
"TN",
|
||||
"TO",
|
||||
"TR",
|
||||
"TT",
|
||||
"TV",
|
||||
"TW",
|
||||
"TZ",
|
||||
"UA",
|
||||
"UG",
|
||||
"US",
|
||||
"UY",
|
||||
"UZ",
|
||||
"VC",
|
||||
"VE",
|
||||
"VN",
|
||||
"VU",
|
||||
"WS",
|
||||
"XK",
|
||||
"ZA",
|
||||
"ZM",
|
||||
"ZW",
|
||||
];
|
||||
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
const getRegionName = (code: string) => {
|
||||
try {
|
||||
if (code === "XK")
|
||||
@@ -56,7 +241,7 @@ interface SearchBarProps {
|
||||
region: string;
|
||||
onRegionChange: (region: string) => void;
|
||||
}
|
||||
export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, history, onHistorySelect, onHistoryRemove, hasResult, searchMode, onSearchModeChange, region, onRegionChange }: SearchBarProps) {
|
||||
export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, history, onHistorySelect, onHistoryRemove, hasResult, searchMode, onSearchModeChange, region, onRegionChange, }: SearchBarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<backend.SearchResponse | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
@@ -70,6 +255,8 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
artists: false,
|
||||
playlists: false,
|
||||
});
|
||||
const [showInvalidUrlDialog, setShowInvalidUrlDialog] = useState(false);
|
||||
const [invalidUrl, setInvalidUrl] = useState("");
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const placeholders = searchMode ? SEARCH_PLACEHOLDERS : FETCH_PLACEHOLDERS;
|
||||
const placeholderText = useTypingEffect(placeholders);
|
||||
@@ -125,7 +312,10 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
searchTimeoutRef.current = setTimeout(async () => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const results = await SearchSpotify({ query: searchQuery, limit: SEARCH_LIMIT });
|
||||
const results = await SearchSpotify({
|
||||
query: searchQuery,
|
||||
limit: SEARCH_LIMIT,
|
||||
});
|
||||
setSearchResults(results);
|
||||
setLastSearchedQuery(searchQuery.trim());
|
||||
saveRecentSearch(searchQuery.trim());
|
||||
@@ -181,10 +371,18 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
if (!prev)
|
||||
return prev;
|
||||
const updated = new backend.SearchResponse({
|
||||
tracks: activeTab === "tracks" ? [...prev.tracks, ...moreResults] : prev.tracks,
|
||||
albums: activeTab === "albums" ? [...prev.albums, ...moreResults] : prev.albums,
|
||||
artists: activeTab === "artists" ? [...prev.artists, ...moreResults] : prev.artists,
|
||||
playlists: activeTab === "playlists" ? [...prev.playlists, ...moreResults] : prev.playlists,
|
||||
tracks: activeTab === "tracks"
|
||||
? [...prev.tracks, ...moreResults]
|
||||
: prev.tracks,
|
||||
albums: activeTab === "albums"
|
||||
? [...prev.albums, ...moreResults]
|
||||
: prev.albums,
|
||||
artists: activeTab === "artists"
|
||||
? [...prev.artists, ...moreResults]
|
||||
: prev.artists,
|
||||
playlists: activeTab === "playlists"
|
||||
? [...prev.playlists, ...moreResults]
|
||||
: prev.playlists,
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
@@ -201,6 +399,35 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
setIsLoadingMore(false);
|
||||
}
|
||||
};
|
||||
const isSpotifyUrl = (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed)
|
||||
return true;
|
||||
const isUrl = /^(https?:\/\/|www\.)/i.test(trimmed) || /^spotify:/i.test(trimmed);
|
||||
if (!isUrl)
|
||||
return true;
|
||||
return (trimmed.includes("spotify.com") ||
|
||||
trimmed.includes("spotify.link") ||
|
||||
trimmed.startsWith("spotify:"));
|
||||
};
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
if (searchMode)
|
||||
return;
|
||||
const pastedText = e.clipboardData.getData("text");
|
||||
if (pastedText && !isSpotifyUrl(pastedText)) {
|
||||
e.preventDefault();
|
||||
setInvalidUrl(pastedText);
|
||||
setShowInvalidUrlDialog(true);
|
||||
}
|
||||
};
|
||||
const handleFetchWithValidation = () => {
|
||||
if (!isSpotifyUrl(url)) {
|
||||
setInvalidUrl(url);
|
||||
setShowInvalidUrlDialog(true);
|
||||
return;
|
||||
}
|
||||
onFetch();
|
||||
};
|
||||
const handleResultClick = (externalUrl: string) => {
|
||||
onSearchModeChange(false);
|
||||
onFetchUrl(externalUrl);
|
||||
@@ -210,18 +437,23 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
const hasAnyResults = searchResults && (searchResults.tracks.length > 0 ||
|
||||
searchResults.albums.length > 0 ||
|
||||
searchResults.artists.length > 0 ||
|
||||
searchResults.playlists.length > 0);
|
||||
const hasAnyResults = searchResults &&
|
||||
(searchResults.tracks.length > 0 ||
|
||||
searchResults.albums.length > 0 ||
|
||||
searchResults.artists.length > 0 ||
|
||||
searchResults.playlists.length > 0);
|
||||
const getTabCount = (tab: ResultTab): number => {
|
||||
if (!searchResults)
|
||||
return 0;
|
||||
switch (tab) {
|
||||
case "tracks": return searchResults.tracks.length;
|
||||
case "albums": return searchResults.albums.length;
|
||||
case "artists": return searchResults.artists.length;
|
||||
case "playlists": return searchResults.playlists.length;
|
||||
case "tracks":
|
||||
return searchResults.tracks.length;
|
||||
case "albums":
|
||||
return searchResults.albums.length;
|
||||
case "artists":
|
||||
return searchResults.artists.length;
|
||||
case "playlists":
|
||||
return searchResults.playlists.length;
|
||||
}
|
||||
};
|
||||
const tabs: {
|
||||
@@ -234,167 +466,201 @@ export function SearchBar({ url, loading, onUrlChange, onFetch, onFetchUrl, hist
|
||||
{ key: "playlists", label: "Playlists" },
|
||||
];
|
||||
return (<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0" onClick={() => onSearchModeChange(!searchMode)}>
|
||||
{searchMode ? <Link className="h-4 w-4"/> : <Search className="h-4 w-4"/>}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{searchMode ? "Fetch Mode" : "Search Mode"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="shrink-0" onClick={() => onSearchModeChange(!searchMode)}>
|
||||
{searchMode ? (<Link className="h-4 w-4"/>) : (<Search className="h-4 w-4"/>)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{searchMode ? "Fetch Mode" : "Search Mode"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="relative flex-1">
|
||||
{!searchMode ? (<>
|
||||
<InputWithContext id="spotify-url" placeholder={placeholderText} value={url} onChange={(e) => onUrlChange(e.target.value)} onKeyDown={(e) => e.key === "Enter" && onFetch()} className="pr-8"/>
|
||||
{url && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => onUrlChange("")}>
|
||||
<XCircle className="h-4 w-4"/>
|
||||
</button>)}
|
||||
</>) : (<>
|
||||
<InputWithContext id="spotify-search" placeholder={placeholderText} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pr-8"/>
|
||||
{searchQuery && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => {
|
||||
<div className="relative flex-1">
|
||||
{!searchMode ? (<>
|
||||
<InputWithContext id="spotify-url" placeholder={placeholderText} value={url} onChange={(e) => onUrlChange(e.target.value)} onPaste={handlePaste} onKeyDown={(e) => e.key === "Enter" && handleFetchWithValidation()} className="pr-8"/>
|
||||
{url && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => onUrlChange("")}>
|
||||
<XCircle className="h-4 w-4"/>
|
||||
</button>)}
|
||||
</>) : (<>
|
||||
<InputWithContext id="spotify-search" placeholder={placeholderText} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pr-8"/>
|
||||
{searchQuery && (<button type="button" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" onClick={() => {
|
||||
setSearchQuery("");
|
||||
setSearchResults(null);
|
||||
setLastSearchedQuery("");
|
||||
}}>
|
||||
<XCircle className="h-4 w-4"/>
|
||||
</button>)}
|
||||
</>)}
|
||||
</div>
|
||||
<XCircle className="h-4 w-4"/>
|
||||
</button>)}
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{!searchMode && (<>
|
||||
<Select value={region} onValueChange={onRegionChange}>
|
||||
<SelectTrigger className="w-[70px] shrink-0">
|
||||
<SelectValue placeholder="Region"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{REGIONS.map((r) => (<SelectItem key={r} value={r} textValue={r}>
|
||||
{r} <span className="text-muted-foreground">({getRegionName(r)})</span>
|
||||
</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={onFetch} disabled={loading}>
|
||||
{loading ? (<>
|
||||
<Spinner />
|
||||
Fetching...
|
||||
</>) : (<>
|
||||
<CloudDownload className="h-4 w-4"/>
|
||||
Fetch
|
||||
</>)}
|
||||
</Button>
|
||||
</>)}
|
||||
</div>
|
||||
{!searchMode && (<>
|
||||
<Select value={region} onValueChange={onRegionChange}>
|
||||
<SelectTrigger className="w-[70px] shrink-0">
|
||||
<SelectValue placeholder="Region"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{REGIONS.map((r) => (<SelectItem key={r} value={r} textValue={r}>
|
||||
{r}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
({getRegionName(r)})
|
||||
</span>
|
||||
</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleFetchWithValidation} disabled={loading}>
|
||||
{loading ? (<>
|
||||
<Spinner />
|
||||
Fetching...
|
||||
</>) : (<>
|
||||
<CloudDownload className="h-4 w-4"/>
|
||||
Fetch
|
||||
</>)}
|
||||
</Button>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{!searchMode && !hasResult && (<FetchHistory history={history} onSelect={onHistorySelect} onRemove={onHistoryRemove}/>)}
|
||||
{!searchMode && !hasResult && (<FetchHistory history={history} onSelect={onHistorySelect} onRemove={onHistoryRemove}/>)}
|
||||
|
||||
{searchMode && (<div className="space-y-4">
|
||||
{!searchQuery && !searchResults && recentSearches.length > 0 && (<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">Recent Searches</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recentSearches.map((query) => (<div key={query} className="group relative flex items-center px-3 py-1.5 bg-muted hover:bg-accent rounded-full text-sm cursor-pointer transition-colors" onClick={() => setSearchQuery(query)}>
|
||||
<span>{query}</span>
|
||||
<button type="button" className="absolute -top-1.5 -right-1.5 z-10 w-5 h-5 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all cursor-pointer shadow-sm" onClick={(e) => {
|
||||
{searchMode && (<div className="space-y-4">
|
||||
{!searchQuery && !searchResults && recentSearches.length > 0 && (<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">Recent Searches</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recentSearches.map((query) => (<div key={query} className="group relative flex items-center px-3 py-1.5 bg-muted hover:bg-accent rounded-full text-sm cursor-pointer transition-colors" onClick={() => setSearchQuery(query)}>
|
||||
<span>{query}</span>
|
||||
<button type="button" className="absolute -top-1.5 -right-1.5 z-10 w-5 h-5 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all cursor-pointer shadow-sm" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeRecentSearch(query);
|
||||
}}>
|
||||
<X className="h-3 w-3 text-red-900" strokeWidth={3}/>
|
||||
</button>
|
||||
</div>))}
|
||||
</div>
|
||||
</div>)}
|
||||
<X className="h-3 w-3 text-red-900" strokeWidth={3}/>
|
||||
</button>
|
||||
</div>))}
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{isSearching && (<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
<span className="ml-2 text-muted-foreground">Searching...</span>
|
||||
</div>)}
|
||||
{isSearching && (<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
<span className="ml-2 text-muted-foreground">Searching...</span>
|
||||
</div>)}
|
||||
|
||||
{!isSearching && searchQuery && !hasAnyResults && (<div className="text-center py-8 text-muted-foreground">
|
||||
No results found for "{searchQuery}"
|
||||
</div>)}
|
||||
{!isSearching && searchQuery && !hasAnyResults && (<div className="text-center py-8 text-muted-foreground">
|
||||
No results found for "{searchQuery}"
|
||||
</div>)}
|
||||
|
||||
{!isSearching && hasAnyResults && (<>
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((tab) => {
|
||||
{!isSearching && hasAnyResults && (<>
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((tab) => {
|
||||
const count = getTabCount(tab.key);
|
||||
if (count === 0)
|
||||
return null;
|
||||
return (<button key={tab.key} type="button" onClick={() => setActiveTab(tab.key)} className={cn("px-4 py-2 text-sm font-medium transition-colors cursor-pointer border-b-2 -mb-px", activeTab === tab.key
|
||||
? "border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground")}>
|
||||
{tab.label} ({count})
|
||||
</button>);
|
||||
{tab.label} ({count})
|
||||
</button>);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
{activeTab === "tracks" &&
|
||||
<div className="grid gap-2">
|
||||
{activeTab === "tracks" &&
|
||||
searchResults?.tracks.map((track) => (<button key={track.id} type="button" className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors" onClick={() => handleResultClick(track.external_urls)}>
|
||||
{track.images ? (<img src={track.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<p className="font-medium truncate">{track.name}</p>
|
||||
{track.is_explicit && (<span className="flex items-center justify-center min-w-[16px] h-[16px] rounded bg-red-600 text-[10px] font-bold text-white leading-none shrink-0" title="Explicit">
|
||||
E
|
||||
</span>)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{track.artists}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{formatDuration(track.duration_ms || 0)}
|
||||
</span>
|
||||
</button>))}
|
||||
{track.images ? (<img src={track.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<p className="font-medium truncate">{track.name}</p>
|
||||
{track.is_explicit && (<span className="flex items-center justify-center min-w-[16px] h-[16px] rounded bg-red-600 text-[10px] font-bold text-white leading-none shrink-0" title="Explicit">
|
||||
E
|
||||
</span>)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{track.artists}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{formatDuration(track.duration_ms || 0)}
|
||||
</span>
|
||||
</button>))}
|
||||
|
||||
{activeTab === "albums" &&
|
||||
{activeTab === "albums" &&
|
||||
searchResults?.albums.map((album) => (<button key={album.id} type="button" className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors" onClick={() => handleResultClick(album.external_urls)}>
|
||||
{album.images ? (<img src={album.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{album.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{album.artists}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{album.release_date || ""}
|
||||
</span>
|
||||
</button>))}
|
||||
{album.images ? (<img src={album.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{album.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{album.artists}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground shrink-0">
|
||||
{album.release_date || ""}
|
||||
</span>
|
||||
</button>))}
|
||||
|
||||
{activeTab === "artists" &&
|
||||
{activeTab === "artists" &&
|
||||
searchResults?.artists.map((artist) => (<button key={artist.id} type="button" className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors" onClick={() => handleResultClick(artist.external_urls)}>
|
||||
{artist.images ? (<img src={artist.images} alt="" className="w-12 h-12 rounded-full object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded-full bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{artist.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Artist</p>
|
||||
</div>
|
||||
</button>))}
|
||||
{artist.images ? (<img src={artist.images} alt="" className="w-12 h-12 rounded-full object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded-full bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{artist.name}</p>
|
||||
<p className="text-sm text-muted-foreground">Artist</p>
|
||||
</div>
|
||||
</button>))}
|
||||
|
||||
{activeTab === "playlists" &&
|
||||
{activeTab === "playlists" &&
|
||||
searchResults?.playlists.map((playlist) => (<button key={playlist.id} type="button" className="flex items-center gap-3 p-3 rounded-lg bg-card hover:bg-accent border cursor-pointer text-left transition-colors" onClick={() => handleResultClick(playlist.external_urls)}>
|
||||
{playlist.images ? (<img src={playlist.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{playlist.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{playlist.owner || ""}
|
||||
</p>
|
||||
</div>
|
||||
</button>))}
|
||||
</div>
|
||||
{playlist.images ? (<img src={playlist.images} alt="" className="w-12 h-12 rounded object-cover shrink-0"/>) : (<div className="w-12 h-12 rounded bg-muted shrink-0"/>)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{playlist.name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{playlist.owner || ""}
|
||||
</p>
|
||||
</div>
|
||||
</button>))}
|
||||
</div>
|
||||
|
||||
{hasMore[activeTab] && (<div className="flex justify-center pt-2">
|
||||
<Button variant="outline" onClick={handleLoadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? (<>
|
||||
<Spinner />
|
||||
Loading...
|
||||
</>) : (<>
|
||||
<ChevronDown className="h-4 w-4"/>
|
||||
Load More
|
||||
</>)}
|
||||
</Button>
|
||||
</div>)}
|
||||
</>)}
|
||||
{hasMore[activeTab] && (<div className="flex justify-center pt-2">
|
||||
<Button variant="outline" onClick={handleLoadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? (<>
|
||||
<Spinner />
|
||||
Loading...
|
||||
</>) : (<>
|
||||
<ChevronDown className="h-4 w-4"/>
|
||||
Load More
|
||||
</>)}
|
||||
</Button>
|
||||
</div>)}
|
||||
</div>);
|
||||
</>)}
|
||||
</div>)}
|
||||
|
||||
<Dialog open={showInvalidUrlDialog} onOpenChange={setShowInvalidUrlDialog}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Invalid URL</DialogTitle>
|
||||
<DialogDescription>
|
||||
Only Spotify links are allowed in Fetch mode.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{invalidUrl && (<div className="p-3 bg-muted rounded-md border text-xs font-mono break-all opacity-70">
|
||||
{invalidUrl}
|
||||
</div>)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => {
|
||||
setShowInvalidUrlDialog(false);
|
||||
setInvalidUrl("");
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
onSearchModeChange(true);
|
||||
setShowInvalidUrlDialog(false);
|
||||
setInvalidUrl("");
|
||||
}}>
|
||||
Switch to Search
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ export function TitleBar() {
|
||||
if (settings) {
|
||||
setUseSpotFetchAPI(settings.useSpotFetchAPI || false);
|
||||
}
|
||||
const handleSettingsUpdate = (event: any) => {
|
||||
const updatedSettings = event.detail;
|
||||
if (updatedSettings && typeof updatedSettings.useSpotFetchAPI !== 'undefined') {
|
||||
setUseSpotFetchAPI(updatedSettings.useSpotFetchAPI);
|
||||
}
|
||||
};
|
||||
window.addEventListener('settingsUpdated', handleSettingsUpdate);
|
||||
return () => window.removeEventListener('settingsUpdated', handleSettingsUpdate);
|
||||
}, []);
|
||||
const handleSpotFetchAPIToggle = () => {
|
||||
const newValue = !useSpotFetchAPI;
|
||||
|
||||
@@ -26,9 +26,9 @@ interface TrackInfoProps {
|
||||
downloadedCover?: boolean;
|
||||
failedCover?: boolean;
|
||||
skippedCover?: boolean;
|
||||
onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownload: (id: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string, playlistName?: string, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onOpenFolder: () => void;
|
||||
onBack?: () => void;
|
||||
@@ -95,9 +95,9 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
{track.isrc && (<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={() => onDownload(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, track.track_number, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} disabled={isDownloading || downloadingTrack === track.isrc}>
|
||||
{downloadingTrack === track.isrc ? (<Spinner />) : (<>
|
||||
{track.spotify_id && (<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={() => onDownload(track.spotify_id || "", track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, track.track_number, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} disabled={isDownloading || downloadingTrack === track.spotify_id}>
|
||||
{downloadingTrack === track.spotify_id ? (<Spinner />) : (<>
|
||||
<Download className="h-4 w-4"/>
|
||||
Download
|
||||
</>)}
|
||||
@@ -134,7 +134,7 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
|
||||
</Tooltip>)}
|
||||
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={() => onCheckAvailability(track.spotify_id!, track.isrc)} variant="outline" size="icon" disabled={checkingAvailability}>
|
||||
<Button onClick={() => onCheckAvailability(track.spotify_id!)} variant="outline" size="icon" disabled={checkingAvailability}>
|
||||
{checkingAvailability ? (<Spinner />) : availability ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -33,11 +33,11 @@ interface TrackListProps {
|
||||
failedCovers?: Set<string>;
|
||||
skippedCovers?: Set<string>;
|
||||
downloadingCoverTrack?: string | null;
|
||||
onToggleTrack: (isrc: string) => void;
|
||||
onToggleTrack: (id: string) => void;
|
||||
onToggleSelectAll: (tracks: TrackMetadata[]) => void;
|
||||
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadTrack: (id: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => void;
|
||||
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
|
||||
onCheckAvailability?: (spotifyId: string) => void;
|
||||
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string, albumArtist?: string, releaseDate?: string, discNumber?: number) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
onAlbumClick?: (album: {
|
||||
@@ -104,15 +104,15 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
}
|
||||
else if (sortBy === "downloaded") {
|
||||
filteredTracks = [...filteredTracks].sort((a, b) => {
|
||||
const aDownloaded = downloadedTracks.has(a.isrc);
|
||||
const bDownloaded = downloadedTracks.has(b.isrc);
|
||||
const aDownloaded = a.spotify_id ? downloadedTracks.has(a.spotify_id) : false;
|
||||
const bDownloaded = b.spotify_id ? downloadedTracks.has(b.spotify_id) : false;
|
||||
return (bDownloaded ? 1 : 0) - (aDownloaded ? 1 : 0);
|
||||
});
|
||||
}
|
||||
else if (sortBy === "not-downloaded") {
|
||||
filteredTracks = [...filteredTracks].sort((a, b) => {
|
||||
const aDownloaded = downloadedTracks.has(a.isrc);
|
||||
const bDownloaded = downloadedTracks.has(b.isrc);
|
||||
const aDownloaded = a.spotify_id ? downloadedTracks.has(a.spotify_id) : false;
|
||||
const bDownloaded = b.spotify_id ? downloadedTracks.has(b.spotify_id) : false;
|
||||
return (aDownloaded ? 1 : 0) - (bDownloaded ? 1 : 0);
|
||||
});
|
||||
}
|
||||
@@ -149,9 +149,9 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
const tracksWithIsrc = filteredTracks.filter((track) => track.isrc);
|
||||
const allSelected = tracksWithIsrc.length > 0 &&
|
||||
tracksWithIsrc.every((track) => selectedTracks.includes(track.isrc));
|
||||
const tracksWithId = filteredTracks.filter((track) => track.spotify_id);
|
||||
const allSelected = tracksWithId.length > 0 &&
|
||||
tracksWithId.every((track) => selectedTracks.includes(track.spotify_id!));
|
||||
const formatDuration = (ms: number) => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.floor((ms % 60000) / 1000);
|
||||
@@ -197,7 +197,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
<tbody>
|
||||
{paginatedTracks.map((track, index) => (<tr key={index} className="border-b transition-colors hover:bg-muted/50">
|
||||
{showCheckboxes && (<td className="p-4 align-middle">
|
||||
{track.isrc && (<Checkbox checked={selectedTracks.includes(track.isrc)} onCheckedChange={() => onToggleTrack(track.isrc)}/>)}
|
||||
{track.spotify_id && (<Checkbox checked={selectedTracks.includes(track.spotify_id)} onCheckedChange={() => onToggleTrack(track.spotify_id!)}/>)}
|
||||
</td>)}
|
||||
<td className="p-4 align-middle text-sm text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
@@ -223,7 +223,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
</span>) : (<span className="font-medium">{track.name}</span>)}
|
||||
{track.is_explicit && (<span className="inline-flex items-center justify-center bg-red-600 text-white text-[10px] h-4 w-4 rounded shrink-0" title="Explicit">E</span>)}
|
||||
|
||||
{skippedTracks.has(track.isrc) ? (<FileCheck className="h-4 w-4 text-yellow-500 shrink-0"/>) : downloadedTracks.has(track.isrc) ? (<CheckCircle className="h-4 w-4 text-green-500 shrink-0"/>) : failedTracks.has(track.isrc) ? (<XCircle className="h-4 w-4 text-red-500 shrink-0"/>) : null}
|
||||
{track.spotify_id && skippedTracks.has(track.spotify_id) ? (<FileCheck className="h-4 w-4 text-yellow-500 shrink-0"/>) : track.spotify_id && downloadedTracks.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500 shrink-0"/>) : track.spotify_id && failedTracks.has(track.spotify_id) ? (<XCircle className="h-4 w-4 text-red-500 shrink-0"/>) : null}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{track.artists_data && track.artists_data.length > 0 ? ((() => {
|
||||
@@ -270,14 +270,14 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
</td>
|
||||
<td className="p-4 align-middle text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{track.isrc && (<Tooltip>
|
||||
{track.spotify_id && (<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={() => onDownloadTrack(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} size="icon" disabled={isDownloading || downloadingTrack === track.isrc}>
|
||||
{downloadingTrack === track.isrc ? (<Spinner />) : skippedTracks.has(track.isrc) ? (<FileCheck className="h-4 w-4"/>) : downloadedTracks.has(track.isrc) ? (<CheckCircle className="h-4 w-4"/>) : failedTracks.has(track.isrc) ? (<XCircle className="h-4 w-4"/>) : (<Download className="h-4 w-4"/>)}
|
||||
<Button onClick={() => onDownloadTrack(track.spotify_id!, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher)} size="icon" disabled={isDownloading || downloadingTrack === track.spotify_id}>
|
||||
{downloadingTrack === track.spotify_id ? (<Spinner />) : skippedTracks.has(track.spotify_id) ? (<FileCheck className="h-4 w-4"/>) : downloadedTracks.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4"/>) : failedTracks.has(track.spotify_id) ? (<XCircle className="h-4 w-4"/>) : (<Download className="h-4 w-4"/>)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{downloadingTrack === track.isrc ? (<p>Downloading...</p>) : skippedTracks.has(track.isrc) ? (<p>Already exists</p>) : downloadedTracks.has(track.isrc) ? (<p>Downloaded</p>) : failedTracks.has(track.isrc) ? (<p>Failed</p>) : (<p>Download Track</p>)}
|
||||
{downloadingTrack === track.spotify_id ? (<p>Downloading...</p>) : skippedTracks.has(track.spotify_id) ? (<p>Already exists</p>) : downloadedTracks.has(track.spotify_id) ? (<p>Downloaded</p>) : failedTracks.has(track.spotify_id) ? (<p>Failed</p>) : (<p>Download Track</p>)}
|
||||
</TooltipContent>
|
||||
</Tooltip>)}
|
||||
{track.spotify_id && (<Tooltip>
|
||||
@@ -315,7 +315,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
|
||||
</Tooltip>)}
|
||||
{track.spotify_id && onCheckAvailability && (<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button onClick={() => onCheckAvailability(track.spotify_id!, track.isrc)} size="icon" variant="outline" disabled={checkingAvailabilityTrack === track.spotify_id}>
|
||||
<Button onClick={() => onCheckAvailability(track.spotify_id!)} size="icon" variant="outline" disabled={checkingAvailabilityTrack === track.spotify_id}>
|
||||
{checkingAvailabilityTrack === track.spotify_id ? (<Spinner />) : availabilityMap?.has(track.spotify_id) ? (<CheckCircle className="h-4 w-4 text-green-500"/>) : (<Globe className="h-4 w-4"/>)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function useAvailability() {
|
||||
const [checkingTrackId, setCheckingTrackId] = useState<string | null>(null);
|
||||
const [availabilityMap, setAvailabilityMap] = useState<Map<string, TrackAvailability>>(new Map());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const checkAvailability = useCallback(async (spotifyId: string, isrc?: string) => {
|
||||
const checkAvailability = useCallback(async (spotifyId: string) => {
|
||||
if (!spotifyId) {
|
||||
setError("No Spotify ID provided");
|
||||
return null;
|
||||
@@ -20,7 +20,7 @@ export function useAvailability() {
|
||||
setError(null);
|
||||
try {
|
||||
logger.info(`Checking availability for track: ${spotifyId}`);
|
||||
const response = await CheckTrackAvailability(spotifyId, isrc || "");
|
||||
const response = await CheckTrackAvailability(spotifyId);
|
||||
const availability: TrackAvailability = JSON.parse(response);
|
||||
setAvailabilityMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
|
||||
@@ -51,7 +51,7 @@ export function useDownload(region: string) {
|
||||
artists: string;
|
||||
} | null>(null);
|
||||
const shouldStopDownloadRef = useRef(false);
|
||||
const downloadWithAutoFallback = async (isrc: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
const downloadWithAutoFallback = async (id: string, settings: any, trackName?: string, artistName?: string, albumName?: string, playlistName?: string, position?: number, spotifyId?: string, durationMs?: number, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
const service = settings.downloader;
|
||||
const query = trackName && artistName ? `${trackName} ${artistName} ` : undefined;
|
||||
const os = settings.operatingSystem;
|
||||
@@ -117,7 +117,7 @@ export function useDownload(region: string) {
|
||||
if (trackName && artistName) {
|
||||
try {
|
||||
const checkRequest: CheckFileExistenceRequest = {
|
||||
spotify_id: spotifyId || isrc,
|
||||
spotify_id: spotifyId || id,
|
||||
track_name: trackName,
|
||||
artist_name: displayArtist || "",
|
||||
album_name: albumName,
|
||||
@@ -149,7 +149,7 @@ export function useDownload(region: string) {
|
||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||
let itemID: string | undefined;
|
||||
if (!fileExists) {
|
||||
itemID = await AddToDownloadQueue(isrc, trackName || "", displayArtist || "", albumName || "");
|
||||
itemID = await AddToDownloadQueue(id, trackName || "", displayArtist || "", albumName || "");
|
||||
}
|
||||
if (service === "auto") {
|
||||
let streamingURLs: any = null;
|
||||
@@ -174,13 +174,12 @@ export function useDownload(region: string) {
|
||||
try {
|
||||
logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "tidal",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -201,6 +200,7 @@ export function useDownload(region: string) {
|
||||
spotify_total_discs: spotifyTotalDiscs,
|
||||
copyright: copyright,
|
||||
publisher: publisher,
|
||||
use_first_artist_only: settings.useFirstArtistOnly,
|
||||
});
|
||||
if (response.success) {
|
||||
logger.success(`tidal: ${trackName} - ${artistName}`);
|
||||
@@ -218,13 +218,12 @@ export function useDownload(region: string) {
|
||||
try {
|
||||
logger.debug(`trying amazon for: ${trackName} - ${artistName}`);
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "amazon",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -260,13 +259,12 @@ export function useDownload(region: string) {
|
||||
try {
|
||||
logger.debug(`trying qobuz for: ${trackName} - ${artistName}`);
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "qobuz",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -314,13 +312,12 @@ export function useDownload(region: string) {
|
||||
audioFormat = settings.qobuzQuality || "6";
|
||||
}
|
||||
const singleServiceResponse = await downloadTrack({
|
||||
isrc,
|
||||
service: service as "tidal" | "qobuz" | "amazon",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -347,7 +344,7 @@ export function useDownload(region: string) {
|
||||
}
|
||||
return singleServiceResponse;
|
||||
};
|
||||
const downloadWithItemID = async (isrc: string, settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
const downloadWithItemID = async (settings: any, itemID: string, trackName?: string, artistName?: string, albumName?: string, folderName?: string, position?: number, spotifyId?: string, durationMs?: number, isAlbum?: boolean, releaseYear?: string, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
const service = settings.downloader;
|
||||
const query = trackName && artistName ? `${trackName} ${artistName}` : undefined;
|
||||
const os = settings.operatingSystem;
|
||||
@@ -375,13 +372,16 @@ export function useDownload(region: string) {
|
||||
const yearValue = releaseYear || finalReleaseDate?.substring(0, 4);
|
||||
const hasSubfolder = settings.folderTemplate && settings.folderTemplate.trim() !== "";
|
||||
const trackNumberForTemplate = (hasSubfolder && finalTrackNumber > 0) ? finalTrackNumber : (position || 0);
|
||||
if (hasSubfolder) {
|
||||
useAlbumTrackNumber = true;
|
||||
}
|
||||
const displayArtist = settings.useFirstArtistOnly && artistName
|
||||
? getFirstArtist(artistName)
|
||||
: artistName;
|
||||
const displayAlbumArtist = settings.useFirstArtistOnly && albumArtist
|
||||
? getFirstArtist(albumArtist)
|
||||
: albumArtist;
|
||||
const templateData: TemplateData = {
|
||||
artist: artistName?.replace(/\//g, placeholder),
|
||||
artist: displayArtist?.replace(/\//g, placeholder),
|
||||
album: albumName?.replace(/\//g, placeholder),
|
||||
album_artist: albumArtist?.replace(/\//g, placeholder) || artistName?.replace(/\//g, placeholder),
|
||||
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
|
||||
title: trackName?.replace(/\//g, placeholder),
|
||||
track: trackNumberForTemplate,
|
||||
year: yearValue,
|
||||
@@ -424,13 +424,12 @@ export function useDownload(region: string) {
|
||||
if (s === "tidal" && streamingURLs?.tidal_url) {
|
||||
try {
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "tidal",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -451,6 +450,7 @@ export function useDownload(region: string) {
|
||||
spotify_total_discs: spotifyTotalDiscs,
|
||||
copyright: copyright,
|
||||
publisher: publisher,
|
||||
use_first_artist_only: settings.useFirstArtistOnly,
|
||||
});
|
||||
if (response.success) {
|
||||
return response;
|
||||
@@ -465,13 +465,12 @@ export function useDownload(region: string) {
|
||||
else if (s === "amazon" && streamingURLs?.amazon_url) {
|
||||
try {
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "amazon",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -490,6 +489,7 @@ export function useDownload(region: string) {
|
||||
spotify_total_discs: spotifyTotalDiscs,
|
||||
copyright: copyright,
|
||||
publisher: publisher,
|
||||
use_first_artist_only: settings.useFirstArtistOnly,
|
||||
});
|
||||
if (response.success) {
|
||||
return response;
|
||||
@@ -504,13 +504,12 @@ export function useDownload(region: string) {
|
||||
else if (s === "qobuz") {
|
||||
try {
|
||||
const response = await downloadTrack({
|
||||
isrc,
|
||||
service: "qobuz",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -530,6 +529,7 @@ export function useDownload(region: string) {
|
||||
spotify_total_discs: spotifyTotalDiscs,
|
||||
copyright: copyright,
|
||||
publisher: publisher,
|
||||
use_first_artist_only: settings.useFirstArtistOnly,
|
||||
});
|
||||
if (response.success) {
|
||||
return response;
|
||||
@@ -557,13 +557,12 @@ export function useDownload(region: string) {
|
||||
audioFormat = settings.qobuzQuality || "6";
|
||||
}
|
||||
const singleServiceResponse = await downloadTrack({
|
||||
isrc,
|
||||
service: service as "tidal" | "qobuz" | "amazon",
|
||||
query,
|
||||
track_name: trackName,
|
||||
artist_name: artistName,
|
||||
artist_name: displayArtist,
|
||||
album_name: albumName,
|
||||
album_artist: albumArtist,
|
||||
album_artist: displayAlbumArtist,
|
||||
release_date: finalReleaseDate || releaseDate,
|
||||
cover_url: coverUrl,
|
||||
output_dir: outputDir,
|
||||
@@ -590,40 +589,41 @@ export function useDownload(region: string) {
|
||||
}
|
||||
return singleServiceResponse;
|
||||
};
|
||||
const handleDownloadTrack = async (isrc: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
if (!isrc) {
|
||||
toast.error("No ISRC found for this track");
|
||||
const handleDownloadTrack = async (id: string, trackName?: string, artistName?: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number, spotifyTotalDiscs?: number, copyright?: string, publisher?: string) => {
|
||||
if (!id) {
|
||||
toast.error("No ID found for this track");
|
||||
return;
|
||||
}
|
||||
logger.info(`starting download: ${trackName} - ${artistName}`);
|
||||
const settings = getSettings();
|
||||
setDownloadingTrack(isrc);
|
||||
const displayArtist = settings.useFirstArtistOnly && artistName ? getFirstArtist(artistName) : artistName;
|
||||
logger.info(`starting download: ${trackName} - ${displayArtist}`);
|
||||
setDownloadingTrack(id);
|
||||
try {
|
||||
const releaseYear = releaseDate?.substring(0, 4);
|
||||
const response = await downloadWithAutoFallback(isrc, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher);
|
||||
const response = await downloadWithAutoFallback(id, settings, trackName, artistName, albumName, playlistName, position, spotifyId, durationMs, releaseYear, albumArtist || "", releaseDate, coverUrl, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, copyright, publisher);
|
||||
if (response.success) {
|
||||
if (response.already_exists) {
|
||||
toast.info(response.message);
|
||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
||||
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
else {
|
||||
toast.success(response.message);
|
||||
}
|
||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||
setFailedTracks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(isrc);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
else {
|
||||
toast.error(response.error || "Download failed");
|
||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
||||
setFailedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Download failed");
|
||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
||||
setFailedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
finally {
|
||||
setDownloadingTrack(null);
|
||||
@@ -646,18 +646,20 @@ export function useDownload(region: string) {
|
||||
outputDir = joinPath(os, outputDir, sanitizePath(folderName.replace(/\//g, " "), os));
|
||||
}
|
||||
const selectedTrackObjects = selectedTracks
|
||||
.map((isrc) => allTracks.find((t) => t.isrc === isrc))
|
||||
.map((id) => allTracks.find((t) => t.spotify_id === id))
|
||||
.filter((t): t is TrackMetadata => t !== undefined);
|
||||
logger.info(`checking existing files in parallel...`);
|
||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||
const audioFormat = "flac";
|
||||
const existenceChecks = selectedTrackObjects.map((track, index) => {
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
const displayAlbumArtist = settings.useFirstArtistOnly && track.album_artist ? getFirstArtist(track.album_artist) : track.album_artist;
|
||||
return {
|
||||
spotify_id: track.spotify_id || track.isrc,
|
||||
spotify_id: track.spotify_id || "",
|
||||
track_name: track.name || "",
|
||||
artist_name: track.artists || "",
|
||||
artist_name: displayArtist || "",
|
||||
album_name: track.album_name || "",
|
||||
album_artist: track.album_artist || "",
|
||||
album_artist: displayAlbumArtist || "",
|
||||
release_date: track.release_date || "",
|
||||
track_number: track.track_number || 0,
|
||||
disc_number: track.disc_number || 0,
|
||||
@@ -682,20 +684,23 @@ export function useDownload(region: string) {
|
||||
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||
const itemIDs: string[] = [];
|
||||
for (const isrc of selectedTracks) {
|
||||
const track = allTracks.find((t) => t.isrc === isrc);
|
||||
const trackID = track?.spotify_id || isrc;
|
||||
const itemID = await AddToDownloadQueue(trackID, track?.name || "", track?.artists || "", track?.album_name || "");
|
||||
for (const id of selectedTracks) {
|
||||
const track = allTracks.find((t) => t.spotify_id === id);
|
||||
if (!track)
|
||||
continue;
|
||||
const trackID = track.spotify_id || id;
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
const itemID = await AddToDownloadQueue(trackID, track.name || "", displayArtist || "", track.album_name || "");
|
||||
itemIDs.push(itemID);
|
||||
if (existingSpotifyIDs.has(trackID)) {
|
||||
const filePath = existingFilePaths.get(trackID) || "";
|
||||
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
||||
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
}
|
||||
const tracksToDownload = selectedTrackObjects.filter((track) => {
|
||||
const trackID = track.spotify_id || track.isrc;
|
||||
const trackID = track.spotify_id || "";
|
||||
return !existingSpotifyIDs.has(trackID);
|
||||
});
|
||||
let successCount = 0;
|
||||
@@ -709,45 +714,46 @@ export function useDownload(region: string) {
|
||||
break;
|
||||
}
|
||||
const track = tracksToDownload[i];
|
||||
const isrc = track.isrc;
|
||||
const originalIndex = selectedTracks.indexOf(isrc);
|
||||
const id = track.spotify_id || "";
|
||||
const originalIndex = selectedTracks.indexOf(id);
|
||||
const itemID = itemIDs[originalIndex];
|
||||
setDownloadingTrack(isrc);
|
||||
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
|
||||
setDownloadingTrack(id);
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
setCurrentDownloadInfo({ name: track.name, artists: displayArtist || "" });
|
||||
try {
|
||||
const releaseYear = track.release_date?.substring(0, 4);
|
||||
const response = await downloadWithItemID(isrc, settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||
const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||
if (response.success) {
|
||||
if (response.already_exists) {
|
||||
skippedCount++;
|
||||
logger.info(`skipped: ${track.name} - ${track.artists} (already exists)`);
|
||||
setSkippedTracks((prev) => new Set(prev).add(isrc));
|
||||
logger.info(`skipped: ${track.name} - ${displayArtist} (already exists)`);
|
||||
setSkippedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
else {
|
||||
successCount++;
|
||||
logger.success(`downloaded: ${track.name} - ${track.artists}`);
|
||||
logger.success(`downloaded: ${track.name} - ${displayArtist}`);
|
||||
}
|
||||
if (response.file) {
|
||||
finalFilePaths.set(isrc, response.file);
|
||||
finalFilePaths.set(track.spotify_id || isrc, response.file);
|
||||
finalFilePaths.set(id, response.file);
|
||||
finalFilePaths.set(track.spotify_id || id, response.file);
|
||||
}
|
||||
setDownloadedTracks((prev) => new Set(prev).add(isrc));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(id));
|
||||
setFailedTracks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(isrc);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
logger.error(`failed: ${track.name} - ${track.artists}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
||||
logger.error(`failed: ${track.name} - ${displayArtist}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(id));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
errorCount++;
|
||||
logger.error(`error: ${track.name} - ${err}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(isrc));
|
||||
setFailedTracks((prev) => new Set(prev).add(id));
|
||||
if (itemID) {
|
||||
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
||||
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
||||
@@ -764,7 +770,7 @@ export function useDownload(region: string) {
|
||||
const { CancelAllQueuedItems } = await import("../../wailsjs/go/main/App");
|
||||
await CancelAllQueuedItems();
|
||||
if (settings.createM3u8File && folderName) {
|
||||
const paths = selectedTrackObjects.map((t) => finalFilePaths.get(t.spotify_id || t.isrc) || "").filter((p) => p !== "");
|
||||
const paths = selectedTrackObjects.map((t) => finalFilePaths.get(t.spotify_id || "") || "").filter((p) => p !== "");
|
||||
if (paths.length > 0) {
|
||||
try {
|
||||
logger.info(`creating m3u8 playlist: ${folderName}`);
|
||||
@@ -798,12 +804,12 @@ export function useDownload(region: string) {
|
||||
}
|
||||
};
|
||||
const handleDownloadAll = async (tracks: TrackMetadata[], folderName?: string, isAlbum?: boolean) => {
|
||||
const tracksWithIsrc = tracks.filter((track) => track.isrc);
|
||||
if (tracksWithIsrc.length === 0) {
|
||||
const tracksWithId = tracks.filter((track) => track.spotify_id);
|
||||
if (tracksWithId.length === 0) {
|
||||
toast.error("No tracks available for download");
|
||||
return;
|
||||
}
|
||||
logger.info(`starting batch download: ${tracksWithIsrc.length} tracks`);
|
||||
logger.info(`starting batch download: ${tracksWithId.length} tracks`);
|
||||
const settings = getSettings();
|
||||
setIsDownloading(true);
|
||||
setBulkDownloadType("all");
|
||||
@@ -817,13 +823,15 @@ export function useDownload(region: string) {
|
||||
logger.info(`checking existing files in parallel...`);
|
||||
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
|
||||
const audioFormat = "flac";
|
||||
const existenceChecks = tracksWithIsrc.map((track, index) => {
|
||||
const existenceChecks = tracksWithId.map((track, index) => {
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
const displayAlbumArtist = settings.useFirstArtistOnly && track.album_artist ? getFirstArtist(track.album_artist) : track.album_artist;
|
||||
return {
|
||||
spotify_id: track.spotify_id || track.isrc,
|
||||
spotify_id: track.spotify_id || "",
|
||||
track_name: track.name || "",
|
||||
artist_name: track.artists || "",
|
||||
artist_name: displayArtist || "",
|
||||
album_name: track.album_name || "",
|
||||
album_artist: track.album_artist || "",
|
||||
album_artist: displayAlbumArtist || "",
|
||||
release_date: track.release_date || "",
|
||||
track_number: track.track_number || 0,
|
||||
disc_number: track.disc_number || 0,
|
||||
@@ -835,7 +843,7 @@ export function useDownload(region: string) {
|
||||
};
|
||||
});
|
||||
const existenceResults = await CheckFilesExistence(outputDir, settings.downloadPath, existenceChecks);
|
||||
const finalFilePaths: string[] = new Array(tracksWithIsrc.length).fill("");
|
||||
const finalFilePaths: string[] = new Array(tracksWithId.length).fill("");
|
||||
const existingSpotifyIDs = new Set<string>();
|
||||
const existingFilePaths = new Map<string, string>();
|
||||
for (let i = 0; i < existenceResults.length; i++) {
|
||||
@@ -849,25 +857,26 @@ export function useDownload(region: string) {
|
||||
logger.info(`found ${existingSpotifyIDs.size} existing files`);
|
||||
const { AddToDownloadQueue } = await import("../../wailsjs/go/main/App");
|
||||
const itemIDs: string[] = [];
|
||||
for (const track of tracksWithIsrc) {
|
||||
const itemID = await AddToDownloadQueue(track.isrc, track.name, track.artists, track.album_name || "");
|
||||
for (const track of tracksWithId) {
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
const itemID = await AddToDownloadQueue(track.spotify_id || "", track.name || "", displayArtist || "", track.album_name || "");
|
||||
itemIDs.push(itemID);
|
||||
const trackID = track.spotify_id || track.isrc;
|
||||
const trackID = track.spotify_id || "";
|
||||
if (existingSpotifyIDs.has(trackID)) {
|
||||
const filePath = existingFilePaths.get(trackID) || "";
|
||||
setTimeout(() => SkipDownloadItem(itemID, filePath), 10);
|
||||
setSkippedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
setSkippedTracks((prev: Set<string>) => new Set(prev).add(trackID));
|
||||
setDownloadedTracks((prev: Set<string>) => new Set(prev).add(trackID));
|
||||
}
|
||||
}
|
||||
const tracksToDownload = tracksWithIsrc.filter((track) => {
|
||||
const trackID = track.spotify_id || track.isrc;
|
||||
const tracksToDownload = tracksWithId.filter((track) => {
|
||||
const trackID = track.spotify_id || "";
|
||||
return !existingSpotifyIDs.has(trackID);
|
||||
});
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
let skippedCount = existingSpotifyIDs.size;
|
||||
const total = tracksWithIsrc.length;
|
||||
const total = tracksWithId.length;
|
||||
setDownloadProgress(Math.round((skippedCount / total) * 100));
|
||||
for (let i = 0; i < tracksToDownload.length; i++) {
|
||||
if (shouldStopDownloadRef.current) {
|
||||
@@ -875,27 +884,29 @@ export function useDownload(region: string) {
|
||||
break;
|
||||
}
|
||||
const track = tracksToDownload[i];
|
||||
const originalIndex = tracksWithIsrc.findIndex((t) => t.isrc === track.isrc);
|
||||
const originalIndex = tracksWithId.findIndex((t) => t.spotify_id === track.spotify_id);
|
||||
const itemID = itemIDs[originalIndex];
|
||||
setDownloadingTrack(track.isrc);
|
||||
setCurrentDownloadInfo({ name: track.name, artists: track.artists });
|
||||
const trackId = track.spotify_id || "";
|
||||
setDownloadingTrack(trackId);
|
||||
const displayArtist = settings.useFirstArtistOnly && track.artists ? getFirstArtist(track.artists) : track.artists;
|
||||
setCurrentDownloadInfo({ name: track.name || "", artists: displayArtist || "" });
|
||||
try {
|
||||
const releaseYear = track.release_date?.substring(0, 4);
|
||||
const response = await downloadWithItemID(track.isrc, settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||
const response = await downloadWithItemID(settings, itemID, track.name, track.artists, track.album_name, folderName, originalIndex + 1, track.spotify_id, track.duration_ms, isAlbum, releaseYear, track.album_artist || "", track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks, track.total_discs, track.copyright, track.publisher);
|
||||
if (response.success) {
|
||||
if (response.already_exists) {
|
||||
skippedCount++;
|
||||
logger.info(`skipped: ${track.name} - ${track.artists} (already exists)`);
|
||||
setSkippedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
logger.info(`skipped: ${track.name} - ${displayArtist} (already exists)`);
|
||||
setSkippedTracks((prev) => new Set(prev).add(trackId));
|
||||
}
|
||||
else {
|
||||
successCount++;
|
||||
logger.success(`downloaded: ${track.name} - ${track.artists}`);
|
||||
logger.success(`downloaded: ${track.name} - ${displayArtist}`);
|
||||
}
|
||||
setDownloadedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
setDownloadedTracks((prev) => new Set(prev).add(trackId));
|
||||
setFailedTracks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(track.isrc);
|
||||
newSet.delete(trackId);
|
||||
return newSet;
|
||||
});
|
||||
if (response.file) {
|
||||
@@ -904,14 +915,14 @@ export function useDownload(region: string) {
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
logger.error(`failed: ${track.name} - ${track.artists}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
logger.error(`failed: ${track.name} - ${displayArtist}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(trackId));
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
errorCount++;
|
||||
logger.error(`error: ${track.name} - ${err}`);
|
||||
setFailedTracks((prev) => new Set(prev).add(track.isrc));
|
||||
setFailedTracks((prev) => new Set(prev).add(trackId));
|
||||
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
|
||||
await MarkDownloadItemFailed(itemID, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { 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";
|
||||
@@ -7,6 +8,7 @@ import type { SpotifyMetadataResponse } from "@/types/api";
|
||||
export function useMetadata() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
|
||||
const [showApiModal, setShowApiModal] = useState(false);
|
||||
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
|
||||
const [selectedAlbum, setSelectedAlbum] = useState<{
|
||||
id: string;
|
||||
@@ -109,7 +111,7 @@ export function useMetadata() {
|
||||
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`);
|
||||
logger.debug(`duration: ${data.track.duration_ms}ms`);
|
||||
}
|
||||
else if ("album_info" in data) {
|
||||
logger.success(`fetched album: ${data.album_info.name}`);
|
||||
@@ -129,7 +131,13 @@ export function useMetadata() {
|
||||
catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
|
||||
logger.error(`fetch failed: ${errorMsg}`);
|
||||
toast.error(errorMsg);
|
||||
const settings = getSettings();
|
||||
if (!settings.useSpotFetchAPI) {
|
||||
setShowApiModal(true);
|
||||
}
|
||||
else {
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
@@ -224,7 +232,13 @@ export function useMetadata() {
|
||||
catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
|
||||
logger.error(`fetch failed: ${errorMsg}`);
|
||||
toast.error(errorMsg);
|
||||
const settings = getSettings();
|
||||
if (!settings.useSpotFetchAPI) {
|
||||
setShowApiModal(true);
|
||||
}
|
||||
else {
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
@@ -243,6 +257,8 @@ export function useMetadata() {
|
||||
handleConfirmAlbumFetch,
|
||||
handleArtistClick,
|
||||
loadFromCache,
|
||||
showApiModal,
|
||||
setShowApiModal,
|
||||
resetMetadata: () => setMetadata(null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ export async function saveSettings(settings: Settings): Promise<void> {
|
||||
cachedSettings = settings;
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
await SaveToBackend(settings as any);
|
||||
window.dispatchEvent(new CustomEvent('settingsUpdated', { detail: settings }));
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to save settings:", error);
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface TrackMetadata {
|
||||
total_discs?: number;
|
||||
disc_number?: number;
|
||||
external_urls: string;
|
||||
isrc: string;
|
||||
album_type?: string;
|
||||
spotify_id?: string;
|
||||
album_id?: string;
|
||||
@@ -109,7 +108,6 @@ export interface ArtistResponse {
|
||||
}
|
||||
export type SpotifyMetadataResponse = TrackResponse | AlbumResponse | PlaylistResponse | ArtistDiscographyResponse | ArtistResponse;
|
||||
export interface DownloadRequest {
|
||||
isrc: string;
|
||||
service: "tidal" | "qobuz" | "amazon";
|
||||
query?: string;
|
||||
track_name?: string;
|
||||
@@ -139,6 +137,7 @@ export interface DownloadRequest {
|
||||
copyright?: string;
|
||||
publisher?: string;
|
||||
spotify_url?: string;
|
||||
use_first_artist_only?: boolean;
|
||||
}
|
||||
export interface DownloadResponse {
|
||||
success: boolean;
|
||||
|
||||
Reference in New Issue
Block a user