This commit is contained in:
afkarxyz
2026-02-25 14:20:48 +07:00
parent 9ef24f5a91
commit 3d8ff2cedd
28 changed files with 916 additions and 182 deletions
+2 -2
View File
@@ -317,7 +317,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
<h2 className="text-4xl font-bold text-white">{artistInfo.name}</h2>
{artistInfo.verified && (<BadgeCheck className="h-6 w-6 text-white fill-blue-400 shrink-0"/>)}
</div>
{artistInfo.biography && (<p className="text-sm text-white/90">{artistInfo.biography}</p>)}
{artistInfo.biography && (<p className="text-sm text-white/90 line-clamp-4">{artistInfo.biography}</p>)}
<div className="flex items-center gap-2 text-sm flex-wrap text-white/90">
{artistInfo.rank && (<>
<span>#{artistInfo.rank} rank</span>
@@ -370,7 +370,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
<h2 className="text-4xl font-bold">{artistInfo.name}</h2>
{artistInfo.verified && (<BadgeCheck className="h-6 w-6 text-white fill-blue-500 shrink-0"/>)}
</div>
{artistInfo.biography && (<p className="text-sm text-muted-foreground">{artistInfo.biography}</p>)}
{artistInfo.biography && (<p className="text-sm text-muted-foreground line-clamp-4">{artistInfo.biography}</p>)}
<div className="flex items-center gap-2 text-sm flex-wrap">
{artistInfo.rank && (<>
<span>#{artistInfo.rank} rank</span>
+37 -6
View File
@@ -4,7 +4,7 @@ import { Label } from "@/components/ui/label";
import { ToggleGroup, ToggleGroupItem, } from "@/components/ui/toggle-group";
import { Upload, X, CheckCircle2, AlertCircle, Trash2, FileMusic, WandSparkles, } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { ConvertAudio, SelectAudioFiles, } from "../../wailsjs/go/main/App";
import { ConvertAudio, SelectAudioFiles, SelectFolder, ListAudioFilesInDir, } from "../../wailsjs/go/main/App";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { OnFileDrop, OnFileDropOff } from "../../wailsjs/runtime/runtime";
interface AudioFile {
@@ -152,6 +152,27 @@ export function AudioConverterPage() {
});
}
};
const handleSelectFolder = async () => {
try {
const selectedFolder = await SelectFolder("");
if (selectedFolder) {
const folderFiles = await ListAudioFilesInDir(selectedFolder);
if (folderFiles && folderFiles.length > 0) {
addFiles(folderFiles.map((f) => f.path));
}
else {
toast.info("No audio files found", {
description: "No FLAC or MP3 files found in the selected folder.",
});
}
}
}
catch (err) {
toast.error("Folder Selection Failed", {
description: err instanceof Error ? err.message : "Failed to select folder",
});
}
};
const addFiles = useCallback(async (paths: string[]) => {
const validExtensions = [".mp3", ".flac"];
const m4aFiles = paths.filter((path) => {
@@ -298,7 +319,11 @@ export function AudioConverterPage() {
{files.length > 0 && (<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleSelectFiles}>
<Upload className="h-4 w-4"/>
Add More
Add Files
</Button>
<Button variant="outline" size="sm" onClick={handleSelectFolder}>
<Upload className="h-4 w-4"/>
Add Folder
</Button>
<Button variant="outline" size="sm" onClick={clearFiles} disabled={converting}>
<Trash2 className="h-4 w-4"/>
@@ -329,10 +354,16 @@ export function AudioConverterPage() {
? "Drop your audio files here"
: "Drag and drop audio files here, or click the button below to select"}
</p>
<Button onClick={handleSelectFiles} size="lg">
<Upload className="h-5 w-5"/>
Select Files
</Button>
<div className="flex gap-3">
<Button onClick={handleSelectFiles} size="lg">
<Upload className="h-5 w-5"/>
Select Files
</Button>
<Button onClick={handleSelectFolder} size="lg" variant="outline">
<Upload className="h-5 w-5"/>
Select Folder
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4 text-center">
Supported formats: FLAC, MP3
</p>
+1 -1
View File
@@ -35,7 +35,7 @@ export function Header({ version, hasUpdate, releaseDate }: HeaderProps) {
</div>
</div>
<p className="text-muted-foreground">
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music no account required.
Get Spotify tracks in true FLAC from Tidal, Qobuz, Amazon Music & Deezer no account required.
</p>
</div>
</div>);
+1 -1
View File
@@ -303,7 +303,7 @@ export function HistoryPage({ onHistorySelect }: HistoryPageProps) {
<td className="p-3 align-middle text-left hidden lg:table-cell">
<div className="flex flex-col items-start gap-1">
<span className="text-xs font-bold text-foreground">
{['HI_RES_LOSSLESS', 'LOSSLESS'].includes(item.format) ? 'FLAC' : item.format}
{['HI_RES_LOSSLESS', 'LOSSLESS', 'flac', '6', '7', '27'].includes(item.format?.toLowerCase() || '') ? 'FLAC' : item.format?.toUpperCase()}
</span>
{item.quality && <span className="text-[11px] text-muted-foreground leading-none whitespace-nowrap">{item.quality}</span>}
</div>
@@ -16,3 +16,8 @@ export const AmazonIcon = ({ className = "w-4 h-4" }: {
<path fillRule="evenodd" d="M15.62 11.13c-.15.1-.37.18-.64.18-.42 0-.82-.05-1.21-.18l-.22-.04c-.08 0-.1.04-.1.14v.25c0 .08.02.12.05.17.02.03.07.08.15.1.4.18.84.25 1.33.25.52 0 .91-.12 1.24-.37.32-.25.47-.57.47-.99 0-.3-.08-.52-.23-.72-.15-.17-.4-.34-.74-.47l-.7-.27c-.26-.1-.46-.2-.53-.3a.47.47 0 0 1-.15-.36c0-.38.27-.57.84-.57.32 0 .64.05.94.15l.2.04c.07 0 .12-.04.12-.14v-.25c0-.08-.03-.12-.05-.17-.03-.05-.08-.08-.15-.1-.37-.13-.74-.2-1.11-.2-.47 0-.87.12-1.16.35-.3.22-.45.54-.45.91 0 .57.32.99.97 1.24l.74.27c.24.1.4.17.5.27.09.1.12.2.12.35 0 .2-.08.37-.23.46Zm-3.88-3.55v3.28c-.42.28-.84.42-1.26.42-.27 0-.47-.07-.6-.22-.11-.15-.16-.37-.16-.7V7.59c0-.13-.05-.18-.18-.18h-.52c-.12 0-.17.05-.17.18v3.06c0 .42.1.77.32.99.22.22.55.35.97.35.56 0 1.13-.2 1.68-.6l.05.3c0 .07.02.1.07.12.02.03.07.03.15.03h.37c.12 0 .17-.05.17-.18V7.58c0-.13-.05-.18-.17-.18h-.52c-.15 0-.2.08-.2.18Zm-4.69 4.27h.52c.12 0 .17-.05.17-.17v-3.1c0-.41-.1-.73-.32-.95a1.25 1.25 0 0 0-.94-.35c-.57 0-1.16.2-1.73.62-.2-.42-.57-.62-1.11-.62-.55 0-1.1.2-1.64.57l-.04-.27c0-.08-.03-.1-.08-.13-.02-.02-.07-.02-.12-.02h-.4c-.12 0-.17.05-.17.17v4.1c0 .13.05.18.17.18h.52c.12 0 .17-.05.17-.18V8.37c.42-.25.84-.4 1.29-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.17.17h.52c.13 0 .18-.05.18-.17V8.37c.44-.27.86-.4 1.28-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.18.17Zm13.47 3.29a21.8 21.8 0 0 1-8.3 1.7c-3.96 0-7.8-1.08-10.88-2.89a.35.35 0 0 0-.15-.05c-.17 0-.27.2-.1.37a16.11 16.11 0 0 0 10.87 4.16c3.02 0 6.5-.94 8.9-2.72.42-.3.08-.74-.34-.57Zm-.08-6.74c.22-.26.57-.38 1.06-.38.25 0 .5.03.72.1l.15.02c.07 0 .12-.04.12-.17v-.25c0-.07-.02-.14-.05-.17a.54.54 0 0 0-.12-.1c-.32-.07-.64-.15-.94-.15-.7 0-1.21.2-1.6.62-.38.4-.57 1-.57 1.73 0 .74.17 1.31.54 1.7.37.4.89.6 1.58.6.37 0 .72-.05.99-.17.07-.03.12-.05.15-.1.02-.03.02-.1.02-.17v-.25c0-.13-.05-.17-.12-.17-.03 0-.07 0-.12.02-.28.07-.55.12-.8.12-.46 0-.81-.12-1.03-.37-.23-.24-.32-.64-.32-1.16v-.12c.02-.55.12-.94.34-1.19Z" clipRule="evenodd"></path>
<path fillRule="evenodd" d="M21.55 17.46c1.29-1.09 1.64-3.33 1.36-3.68-.12-.15-.71-.3-1.45-.3-.8 0-1.73.18-2.45.67-.22.15-.17.35.05.32.76-.1 2.5-.3 2.82.1.3.4-.35 2.03-.65 2.74-.07.23.1.3.32.15ZM18.12 7.4h-.52c-.12 0-.17.05-.17.18v4.1c0 .12.05.17.17.17h.52c.12 0 .17-.05.17-.17v-4.1c0-.1-.05-.18-.17-.18Zm.15-1.68a.58.58 0 0 0-.42-.15c-.18 0-.3.05-.4.15a.5.5 0 0 0-.15.37c0 .15.05.3.15.37.1.1.22.15.4.15.17 0 .3-.05.4-.15a.5.5 0 0 0 .14-.37c0-.15-.02-.3-.12-.37Z" clipRule="evenodd"></path>
</svg>);
export const DeezerIcon = ({ className = "w-4 h-4" }: {
className?: string;
}) => (<svg viewBox="0 0 512 512" className={`${className} fill-current`}>
<path d="M14.8 101.1C6.6 101.1 0 127.6 0 160.3s6.6 59.2 14.8 59.2s14.8-26.5 14.8-59.2s-6.6-59.2-14.8-59.2m433.9-60.2c-7.7 0-14.5 17.1-19.4 44.1c-7.7-46.7-20.2-77-34.2-77c-16.8 0-31.1 42.9-38 105.4c-6.6-45.4-16.8-74.2-28.3-74.2c-16.1 0-29.6 56.9-34.7 136.2c-9.4-40.8-23.2-66.3-38.3-66.3s-28.8 25.5-38.3 66.3c-5.1-79.3-18.6-136.2-34.7-136.2c-11.5 0-21.7 28.8-28.3 74.2C147.9 50.9 133.3 8 116.7 8c-14 0-26.5 30.4-34.2 77c-4.8-27-11.7-44.1-19.4-44.1c-14.3 0-26 59.2-26 132.1S49 305.2 63.3 305.2c5.9 0 11.5-9.9 15.8-26.8c6.9 61.7 21.2 104.1 38 104.1c13 0 24.5-25.5 32.1-65.6c5.4 76.3 18.6 130.4 34.2 130.4c9.7 0 18.6-21.4 25.3-56.4c7.9 72.2 26.3 122.7 47.7 122.7s39.5-50.5 47.7-122.7c6.6 35 15.6 56.4 25.3 56.4c15.6 0 28.8-54.1 34.2-130.4c7.7 40.1 19.4 65.6 32.1 65.6c16.6 0 30.9-42.3 38-104.1c4.3 16.8 9.7 26.8 15.8 26.8c14.3 0 26-59.2 26-132.1S463 40.9 448.7 40.9m48.5 60.2c-8.2 0-14.8 26.5-14.8 59.2s6.6 59.2 14.8 59.2S512 193 512 160.3s-6.6-59.2-14.8-59.2"/>
</svg>);
@@ -33,6 +33,7 @@ export function SearchAndSort({ searchQuery, sortBy, onSearchChange, onSortChang
<SelectItem value="plays-desc">Plays (High)</SelectItem>
<SelectItem value="downloaded">Downloaded</SelectItem>
<SelectItem value="not-downloaded">Not Downloaded</SelectItem>
<SelectItem value="failed">Failed Downloads</SelectItem>
</SelectContent>
</Select>
</div>);
+222 -66
View File
@@ -30,6 +30,11 @@ const AmazonIcon = ({ className }: {
<path fillRule="evenodd" d="M15.62 11.13c-.15.1-.37.18-.64.18-.42 0-.82-.05-1.21-.18l-.22-.04c-.08 0-.1.04-.1.14v.25c0 .08.02.12.05.17.02.03.07.08.15.1.4.18.84.25 1.33.25.52 0 .91-.12 1.24-.37.32-.25.47-.57.47-.99 0-.3-.08-.52-.23-.72-.15-.17-.4-.34-.74-.47l-.7-.27c-.26-.1-.46-.2-.53-.3a.47.47 0 0 1-.15-.36c0-.38.27-.57.84-.57.32 0 .64.05.94.15l.2.04c.07 0 .12-.04.12-.14v-.25c0-.08-.03-.12-.05-.17-.03-.05-.08-.08-.15-.1-.37-.13-.74-.2-1.11-.2-.47 0-.87.12-1.16.35-.3.22-.45.54-.45.91 0 .57.32.99.97 1.24l.74.27c.24.1.4.17.5.27.09.1.12.2.12.35 0 .2-.08.37-.23.46Zm-3.88-3.55v3.28c-.42.28-.84.42-1.26.42-.27 0-.47-.07-.6-.22-.11-.15-.16-.37-.16-.7V7.59c0-.13-.05-.18-.18-.18h-.52c-.12 0-.17.05-.17.18v3.06c0 .42.1.77.32.99.22.22.55.35.97.35.56 0 1.13-.2 1.68-.6l.05.3c0 .07.02.1.07.12.02.03.07.03.15.03h.37c.12 0 .17-.05.17-.18V7.58c0-.13-.05-.18-.17-.18h-.52c-.15 0-.2.08-.2.18Zm-4.69 4.27h.52c.12 0 .17-.05.17-.17v-3.1c0-.41-.1-.73-.32-.95a1.25 1.25 0 0 0-.94-.35c-.57 0-1.16.2-1.73.62-.2-.42-.57-.62-1.11-.62-.55 0-1.1.2-1.64.57l-.04-.27c0-.08-.03-.1-.08-.13-.02-.02-.07-.02-.12-.02h-.4c-.12 0-.17.05-.17.17v4.1c0 .13.05.18.17.18h.52c.12 0 .17-.05.17-.18V8.37c.42-.25.84-.4 1.29-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.17.17h.52c.13 0 .18-.05.18-.17V8.37c.44-.27.86-.4 1.28-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.18.17Zm13.47 3.29a21.8 21.8 0 0 1-8.3 1.7c-3.96 0-7.8-1.08-10.88-2.89a.35.35 0 0 0-.15-.05c-.17 0-.27.2-.1.37a16.11 16.11 0 0 0 10.87 4.16c3.02 0 6.5-.94 8.9-2.72.42-.3.08-.74-.34-.57Zm-.08-6.74c.22-.26.57-.38 1.06-.38.25 0 .5.03.72.1l.15.02c.07 0 .12-.04.12-.17v-.25c0-.07-.02-.14-.05-.17a.54.54 0 0 0-.12-.1c-.32-.07-.64-.15-.94-.15-.7 0-1.21.2-1.6.62-.38.4-.57 1-.57 1.73 0 .74.17 1.31.54 1.7.37.4.89.6 1.58.6.37 0 .72-.05.99-.17.07-.03.12-.05.15-.1.02-.03.02-.1.02-.17v-.25c0-.13-.05-.17-.12-.17-.03 0-.07 0-.12.02-.28.07-.55.12-.8.12-.46 0-.81-.12-1.03-.37-.23-.24-.32-.64-.32-1.16v-.12c.02-.55.12-.94.34-1.19Z" clipRule="evenodd"></path>
<path fillRule="evenodd" d="M21.55 17.46c1.29-1.09 1.64-3.33 1.36-3.68-.12-.15-.71-.3-1.45-.3-.8 0-1.73.18-2.45.67-.22.15-.17.35.05.32.76-.1 2.5-.3 2.82.1.3.4-.35 2.03-.65 2.74-.07.23.1.3.32.15ZM18.12 7.4h-.52c-.12 0-.17.05-.17.18v4.1c0 .12.05.17.17.17h.52c.12 0 .17-.05.17-.17v-4.1c0-.1-.05-.18-.17-.18Zm.15-1.68a.58.58 0 0 0-.42-.15c-.18 0-.3.05-.4.15a.5.5 0 0 0-.15.37c0 .15.05.3.15.37.1.1.22.15.4.15.17 0 .3-.05.4-.15a.5.5 0 0 0 .14-.37c0-.15-.02-.3-.12-.37Z" clipRule="evenodd"></path>
</svg>);
const DeezerIcon = ({ className }: {
className?: string;
}) => (<svg viewBox="0 0 512 512" className={`inline-block w-[1.1em] h-[1.1em] mr-2 ${className || "fill-muted-foreground"}`}>
<path fill="currentColor" d="M14.8 101.1C6.6 101.1 0 127.6 0 160.3s6.6 59.2 14.8 59.2s14.8-26.5 14.8-59.2s-6.6-59.2-14.8-59.2m433.9-60.2c-7.7 0-14.5 17.1-19.4 44.1c-7.7-46.7-20.2-77-34.2-77c-16.8 0-31.1 42.9-38 105.4c-6.6-45.4-16.8-74.2-28.3-74.2c-16.1 0-29.6 56.9-34.7 136.2c-9.4-40.8-23.2-66.3-38.3-66.3s-28.8 25.5-38.3 66.3c-5.1-79.3-18.6-136.2-34.7-136.2c-11.5 0-21.7 28.8-28.3 74.2C147.9 50.9 133.3 8 116.7 8c-14 0-26.5 30.4-34.2 77c-4.8-27-11.7-44.1-19.4-44.1c-14.3 0-26 59.2-26 132.1S49 305.2 63.3 305.2c5.9 0 11.5-9.9 15.8-26.8c6.9 61.7 21.2 104.1 38 104.1c13 0 24.5-25.5 32.1-65.6c5.4 76.3 18.6 130.4 34.2 130.4c9.7 0 18.6-21.4 25.3-56.4c7.9 72.2 26.3 122.7 47.7 122.7s39.5-50.5 47.7-122.7c6.6 35 15.6 56.4 25.3 56.4c15.6 0 28.8-54.1 34.2-130.4c7.7 40.1 19.4 65.6 32.1 65.6c16.6 0 30.9-42.3 38-104.1c4.3 16.8 9.7 26.8 15.8 26.8c14.3 0 26-59.2 26-132.1S463 40.9 448.7 40.9m48.5 60.2c-8.2 0-14.8 26.5-14.8 59.2s6.6 59.2 14.8 59.2S512 193 512 160.3s-6.6-59.2-14.8-59.2"/>
</svg>);
interface SettingsPageProps {
onUnsavedChangesChange?: (hasUnsavedChanges: boolean) => void;
onResetRequest?: (resetFn: () => void) => void;
@@ -118,7 +123,7 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => {
setTempSettings((prev) => ({ ...prev, tidalQuality: value }));
};
const handleQobuzQualityChange = (value: "6" | "7") => {
const handleQobuzQualityChange = (value: "6" | "7" | "27") => {
setTempSettings((prev) => ({ ...prev, qobuzQuality: value }));
};
const handleAutoQualityChange = async (value: "16" | "24") => {
@@ -234,7 +239,7 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
<div className="space-y-2">
<Label htmlFor="downloader">Source</Label>
<div className="flex gap-2 flex-wrap">
<Select value={tempSettings.downloader} onValueChange={(value: "auto" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({
<Select value={tempSettings.downloader} onValueChange={(value: any) => setTempSettings((prev) => ({
...prev,
downloader: value,
}))}>
@@ -261,6 +266,12 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
Amazon Music
</span>
</SelectItem>
<SelectItem value="deezer">
<span className="flex items-center">
<DeezerIcon />
Deezer
</span>
</SelectItem>
</SelectContent>
</Select>
@@ -273,6 +284,193 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tidal-qobuz-amazon-deezer">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-qobuz-deezer-amazon">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="qobuz-tidal-amazon-deezer">
<span className="flex items-center gap-1.5">
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-tidal-qobuz-deezer">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-tidal-qobuz-amazon">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-qobuz-amazon-tidal">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-amazon-tidal-qobuz">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-qobuz-deezer">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-amazon-deezer">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="qobuz-amazon-deezer">
<span className="flex items-center gap-1.5">
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-qobuz-deezer">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-tidal-deezer">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-qobuz-amazon">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-qobuz-amazon">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-deezer">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="qobuz-deezer">
<span className="flex items-center gap-1.5">
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-deezer">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<DeezerIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-tidal">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-qobuz">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="deezer-amazon">
<span className="flex items-center gap-1.5">
<DeezerIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-qobuz">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
@@ -313,62 +511,6 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-qobuz-amazon">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="tidal-amazon-qobuz">
<span className="flex items-center gap-1.5">
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="qobuz-tidal-amazon">
<span className="flex items-center gap-1.5">
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="qobuz-amazon-tidal">
<span className="flex items-center gap-1.5">
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-tidal-qobuz">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
</span>
</SelectItem>
<SelectItem value="amazon-qobuz-tidal">
<span className="flex items-center gap-1.5">
<AmazonIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<QobuzIcon className="fill-current"/>
<ArrowRight className="h-3 w-3 text-muted-foreground"/>
<TidalIcon className="fill-current"/>
</span>
</SelectItem>
</SelectContent>
@@ -403,19 +545,22 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</SelectTrigger>
<SelectContent>
<SelectItem value="6">16-bit/44.1kHz</SelectItem>
<SelectItem value="7">24-bit/48kHz</SelectItem>
<SelectItem value="27">24-bit/48kHz - 192kHz</SelectItem>
</SelectContent>
</Select>)}
{tempSettings.downloader === "amazon" && (<div className="h-9 px-3 flex items-center text-sm font-medium border border-input rounded-md bg-muted/30 text-muted-foreground whitespace-nowrap cursor-default">
16-bit - 24-bit/44.1kHz - 192kHz
</div>)}
{tempSettings.downloader === "deezer" && (<div className="h-9 px-3 flex items-center text-sm font-medium border border-input rounded-md bg-muted/30 text-muted-foreground whitespace-nowrap cursor-default">
16-bit/44.1kHz
</div>)}
</div>
{((tempSettings.downloader === "tidal" &&
tempSettings.tidalQuality === "HI_RES_LOSSLESS") ||
(tempSettings.downloader === "qobuz" &&
tempSettings.qobuzQuality === "7") ||
tempSettings.qobuzQuality === "27") ||
(tempSettings.downloader === "auto" &&
tempSettings.autoQuality === "24")) && (<div className="flex items-center gap-3 pt-2">
<div className="flex items-center gap-3">
@@ -452,14 +597,23 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</Label>
</div>
<div className="flex items-center gap-3">
<Switch id="use-single-genre" checked={tempSettings.useSingleGenre} onCheckedChange={(checked) => setTempSettings((prev) => ({
<Switch id="embed-genre" checked={tempSettings.embedGenre} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev,
useSingleGenre: checked,
embedGenre: checked,
}))}/>
<Label htmlFor="use-single-genre" className="text-sm cursor-pointer font-normal">
Use Single Genre
<Label htmlFor="embed-genre" className="cursor-pointer text-sm font-normal">
Embed Genre
</Label>
</div>
{tempSettings.embedGenre && (<div className="flex items-center gap-3">
<Switch id="use-single-genre" checked={tempSettings.useSingleGenre} onCheckedChange={(checked) => setTempSettings((prev) => ({
...prev,
useSingleGenre: checked,
}))}/>
<Label htmlFor="use-single-genre" className="text-sm cursor-pointer font-normal">
Use Single Genre
</Label>
</div>)}
</div>
</div>
</div>)}
@@ -513,7 +667,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{artist\}/g, "Kendrick Lamar, SZA")
.replace(/\{album\}/g, "Black Panther")
.replace(/\{album_artist\}/g, "Kendrick Lamar")
.replace(/\{year\}/g, "2018")}
.replace(/\{year\}/g, "2018")
.replace(/\{date\}/g, "2018-02-09")}
/
</span>
</p>)}
@@ -601,7 +756,8 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
.replace(/\{title\}/g, "All The Stars")
.replace(/\{track\}/g, "01")
.replace(/\{disc\}/g, "1")
.replace(/\{year\}/g, "2018")}
.replace(/\{year\}/g, "2018")
.replace(/\{date\}/g, "2018-02-09")}
.flac
</span>
</p>)}
+2 -1
View File
@@ -4,7 +4,7 @@ import { Download, FolderOpen, CheckCircle, XCircle, FileText, FileCheck, Globe,
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
import { TidalIcon, QobuzIcon, AmazonIcon, DeezerIcon } from "./PlatformIcons";
import { usePreview } from "@/hooks/usePreview";
interface TrackInfoProps {
track: TrackMetadata & {
@@ -143,6 +143,7 @@ export function TrackInfo({ track, isDownloading, downloadingTrack, isDownloaded
<TidalIcon className={`w-4 h-4 ${availability.tidal ? "text-green-500" : "text-red-500"}`}/>
<QobuzIcon className={`w-4 h-4 ${availability.qobuz ? "text-green-500" : "text-red-500"}`}/>
<AmazonIcon className={`w-4 h-4 ${availability.amazon ? "text-green-500" : "text-red-500"}`}/>
<DeezerIcon className={`w-4 h-4 ${availability.deezer ? "text-green-500" : "text-red-500"}`}/>
</div>) : (<p>Check Availability</p>)}
</TooltipContent>
</Tooltip>)}
+9 -1
View File
@@ -5,7 +5,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination";
import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
import { TidalIcon, QobuzIcon, AmazonIcon, DeezerIcon } from "./PlatformIcons";
import { usePreview } from "@/hooks/usePreview";
interface TrackListProps {
tracks: TrackMetadata[];
@@ -116,6 +116,13 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
return (aDownloaded ? 1 : 0) - (bDownloaded ? 1 : 0);
});
}
else if (sortBy === "failed") {
filteredTracks = [...filteredTracks].sort((a, b) => {
const aFailed = a.spotify_id ? failedTracks.has(a.spotify_id) : false;
const bFailed = b.spotify_id ? failedTracks.has(b.spotify_id) : false;
return (bFailed ? 1 : 0) - (aFailed ? 1 : 0);
});
}
const totalPages = Math.ceil(filteredTracks.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
@@ -324,6 +331,7 @@ export function TrackList({ tracks, searchQuery, sortBy, selectedTracks, downloa
<TidalIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.tidal ? "text-green-500" : "text-red-500"}`}/>
<QobuzIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.qobuz ? "text-green-500" : "text-red-500"}`}/>
<AmazonIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.amazon ? "text-green-500" : "text-red-500"}`}/>
<DeezerIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.deezer ? "text-green-500" : "text-red-500"}`}/>
</div>) : (<p>Check Availability</p>)}
</TooltipContent>
</Tooltip>)}
+15 -7
View File
@@ -2,7 +2,7 @@ import { useState, useRef } from "react";
import { downloadCover } from "@/lib/api";
import { getSettings, parseTemplate, type TemplateData } from "@/lib/settings";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath } from "@/lib/utils";
import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api";
export function useCover() {
@@ -29,12 +29,16 @@ export function useCover() {
let outputDir = settings.downloadPath;
const placeholder = "__SLASH_PLACEHOLDER__";
const yearValue = releaseDate?.substring(0, 4);
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: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder),
track: position,
year: yearValue,
date: releaseDate,
playlist: playlistName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -55,9 +59,9 @@ export function useCover() {
const response = await downloadCover({
cover_url: coverUrl,
track_name: trackName,
artist_name: artistName,
artist_name: displayArtist,
album_name: albumName || "",
album_artist: albumArtist || "",
album_artist: displayAlbumArtist || "",
release_date: releaseDate || "",
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
@@ -127,12 +131,16 @@ export function useCover() {
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
const trackPosition = useAlbumTrackNumber ? (track.track_number || i + 1) : (i + 1);
const yearValue = track.release_date?.substring(0, 4);
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;
const templateData: TemplateData = {
artist: track.artists?.replace(/\//g, placeholder),
artist: displayArtist?.replace(/\//g, placeholder),
album: track.album_name?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: track.name?.replace(/\//g, placeholder),
track: trackPosition,
year: yearValue,
date: track.release_date,
playlist: playlistName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -153,9 +161,9 @@ export function useCover() {
const response = await downloadCover({
cover_url: track.images,
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,
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
+156 -17
View File
@@ -2,16 +2,9 @@ import { useState, useRef } from "react";
import { downloadTrack, fetchSpotifyMetadata } from "@/lib/api";
import { getSettings, parseTemplate, type TemplateData } from "@/lib/settings";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath } from "@/lib/utils";
import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api";
function getFirstArtist(artistString: string): string {
if (!artistString)
return artistString;
const delimiters = /[,&]|(?:\s+(?:feat\.?|ft\.?|featuring)\s+)/i;
const parts = artistString.split(delimiters);
return parts[0].trim();
}
interface CheckFileExistenceRequest {
spotify_id: string;
track_name: string;
@@ -95,6 +88,7 @@ export function useDownload(region: string) {
title: trackName?.replace(/\//g, placeholder),
track: trackNumberForTemplate,
year: yearValue,
date: releaseDate,
playlist: playlistName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -166,9 +160,10 @@ export function useDownload(region: string) {
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = [];
const is24Bit = (settings.autoQuality || "24") === "24";
const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "7" : "6";
const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) {
if (s === "tidal" && streamingURLs?.tidal_url) {
try {
@@ -202,16 +197,20 @@ export function useDownload(region: string) {
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`tidal: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Tidal] ${errMsg}`);
lastResponse = response;
logger.warning(`tidal failed, trying next...`);
}
catch (err) {
logger.error(`tidal error: ${err}`);
fallbackErrors.push(`[Tidal] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
@@ -244,16 +243,20 @@ export function useDownload(region: string) {
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`amazon: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Amazon] ${errMsg}`);
lastResponse = response;
logger.warning(`amazon failed, trying next...`);
}
catch (err) {
logger.error(`amazon error: ${err}`);
fallbackErrors.push(`[Amazon] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
@@ -286,23 +289,76 @@ export function useDownload(region: string) {
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`qobuz: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Qobuz] ${errMsg}`);
lastResponse = response;
logger.warning(`qobuz failed, trying next...`);
}
catch (err) {
logger.error(`qobuz error: ${err}`);
fallbackErrors.push(`[Qobuz] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
else if (s === "deezer") {
try {
logger.debug(`trying deezer for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "deezer",
query,
track_name: trackName,
artist_name: displayArtist,
album_name: albumName,
album_artist: displayAlbumArtist,
release_date: finalReleaseDate || releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
position,
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSeconds,
item_id: itemID,
audio_format: "flac",
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`deezer: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Deezer] ${errMsg}`);
lastResponse = response;
logger.warning(`deezer failed, trying next...`);
}
catch (err) {
logger.error(`deezer error: ${err}`);
fallbackErrors.push(`[Deezer] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
}
if (itemID) {
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
await MarkDownloadItemFailed(itemID, lastResponse.error || "All services failed");
const finalError = fallbackErrors.length > 0 ? fallbackErrors.join(" | ") : (lastResponse.error || "All services failed");
await MarkDownloadItemFailed(itemID, finalError);
}
return lastResponse;
}
@@ -314,8 +370,12 @@ export function useDownload(region: string) {
else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
}
else if (service === "deezer") {
audioFormat = "flac";
}
logger.debug(`trying ${service} for: ${trackName} - ${artistName}`);
const singleServiceResponse = await downloadTrack({
service: service as "tidal" | "qobuz" | "amazon",
service: service as "tidal" | "qobuz" | "amazon" | "deezer",
query,
track_name: trackName,
artist_name: displayArtist,
@@ -341,6 +401,7 @@ export function useDownload(region: string) {
copyright: copyright,
publisher: publisher,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (!singleServiceResponse.success && itemID) {
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
@@ -389,6 +450,7 @@ export function useDownload(region: string) {
title: trackName?.replace(/\//g, placeholder),
track: trackNumberForTemplate,
year: yearValue,
date: releaseDate,
playlist: folderName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -421,12 +483,14 @@ export function useDownload(region: string) {
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = [];
const is24Bit = (settings.autoQuality || "24") === "24";
const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "7" : "6";
const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) {
if (s === "tidal" && streamingURLs?.tidal_url) {
try {
logger.debug(`trying tidal for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "tidal",
query,
@@ -456,19 +520,26 @@ export function useDownload(region: string) {
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`tidal: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Tidal] ${errMsg}`);
lastResponse = response;
logger.warning(`tidal failed, trying next...`);
}
catch (err) {
console.error("Tidal error:", err);
logger.error(`tidal error: ${err}`);
fallbackErrors.push(`[Tidal] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
else if (s === "amazon" && streamingURLs?.amazon_url) {
try {
logger.debug(`trying amazon for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "amazon",
query,
@@ -496,19 +567,26 @@ export function useDownload(region: string) {
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`amazon: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Amazon] ${errMsg}`);
lastResponse = response;
logger.warning(`amazon failed, trying next...`);
}
catch (err) {
console.error("Amazon error:", err);
logger.error(`amazon error: ${err}`);
fallbackErrors.push(`[Amazon] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
else if (s === "qobuz") {
try {
logger.debug(`trying qobuz for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "qobuz",
query,
@@ -537,21 +615,76 @@ export function useDownload(region: string) {
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`qobuz: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Qobuz] ${errMsg}`);
lastResponse = response;
logger.warning(`qobuz failed, trying next...`);
}
catch (err) {
console.error("Qobuz error:", err);
logger.error(`qobuz error: ${err}`);
fallbackErrors.push(`[Qobuz] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
else if (s === "deezer") {
try {
logger.debug(`trying deezer for: ${trackName} - ${artistName}`);
const response = await downloadTrack({
service: "deezer",
query,
track_name: trackName,
artist_name: displayArtist,
album_name: albumName,
album_artist: displayAlbumArtist,
release_date: finalReleaseDate || releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
position: trackNumberForTemplate,
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSeconds,
item_id: itemID,
audio_format: "flac",
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
spotify_total_discs: spotifyTotalDiscs,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (response.success) {
logger.success(`deezer: ${trackName} - ${artistName}`);
return response;
}
const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Deezer] ${errMsg}`);
lastResponse = response;
logger.warning(`deezer failed, trying next...`);
}
catch (err) {
logger.error(`deezer error: ${err}`);
fallbackErrors.push(`[Deezer] ${String(err)}`);
lastResponse = { success: false, error: String(err) };
}
}
}
if (!lastResponse.success && itemID) {
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
await MarkDownloadItemFailed(itemID, lastResponse.error || "All services failed");
const finalError = fallbackErrors.length > 0 ? fallbackErrors.join(" | ") : (lastResponse.error || "All services failed");
await MarkDownloadItemFailed(itemID, finalError);
}
return lastResponse;
}
@@ -563,8 +696,11 @@ export function useDownload(region: string) {
else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
}
else if (service === "deezer") {
audioFormat = "flac";
}
const singleServiceResponse = await downloadTrack({
service: service as "tidal" | "qobuz" | "amazon",
service: service as "tidal" | "qobuz" | "amazon" | "deezer",
query,
track_name: trackName,
artist_name: displayArtist,
@@ -589,6 +725,9 @@ export function useDownload(region: string) {
spotify_total_discs: spotifyTotalDiscs,
copyright: copyright,
publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre,
});
if (!singleServiceResponse.success && itemID) {
const { MarkDownloadItemFailed } = await import("../../wailsjs/go/main/App");
+15 -7
View File
@@ -2,7 +2,7 @@ import { useState, useRef } from "react";
import { downloadLyrics } from "@/lib/api";
import { getSettings, parseTemplate, type TemplateData } from "@/lib/settings";
import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { joinPath, sanitizePath } from "@/lib/utils";
import { joinPath, sanitizePath, getFirstArtist } from "@/lib/utils";
import { logger } from "@/lib/logger";
import type { TrackMetadata } from "@/types/api";
export function useLyrics() {
@@ -26,12 +26,16 @@ export function useLyrics() {
let outputDir = settings.downloadPath;
const placeholder = "__SLASH_PLACEHOLDER__";
const yearValue = releaseDate?.substring(0, 4);
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: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: trackName?.replace(/\//g, placeholder),
track: position,
year: yearValue,
date: releaseDate,
playlist: playlistName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -53,9 +57,9 @@ export function useLyrics() {
const response = await downloadLyrics({
spotify_id: spotifyId,
track_name: trackName,
artist_name: artistName,
artist_name: displayArtist,
album_name: albumName,
album_artist: albumArtist,
album_artist: displayAlbumArtist,
release_date: releaseDate,
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
@@ -123,12 +127,16 @@ export function useLyrics() {
const useAlbumTrackNumber = settings.folderTemplate?.includes("{album}") || false;
const trackPosition = useAlbumTrackNumber ? (track.track_number || i + 1) : (i + 1);
const yearValue = track.release_date?.substring(0, 4);
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;
const templateData: TemplateData = {
artist: track.artists?.replace(/\//g, placeholder),
artist: displayArtist?.replace(/\//g, placeholder),
album: track.album_name?.replace(/\//g, placeholder),
album_artist: displayAlbumArtist?.replace(/\//g, placeholder) || displayArtist?.replace(/\//g, placeholder),
title: track.name?.replace(/\//g, placeholder),
track: trackPosition,
year: yearValue,
date: track.release_date,
playlist: playlistName?.replace(/\//g, placeholder),
};
const folderTemplate = settings.folderTemplate || "";
@@ -149,9 +157,9 @@ export function useLyrics() {
const response = await downloadLyrics({
spotify_id: 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,
output_dir: outputDir,
filename_format: settings.filenameTemplate || "{title}",
+13 -11
View File
@@ -4,7 +4,7 @@ export type FolderPreset = "none" | "artist" | "album" | "year-album" | "year-ar
export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-title" | "track-title-artist" | "track-artist-title" | "title-album-artist" | "track-title-album-artist" | "artist-album-title" | "track-dash-title" | "disc-track-title" | "disc-track-title-artist" | "custom";
export interface Settings {
downloadPath: string;
downloader: "auto" | "tidal" | "qobuz" | "amazon";
downloader: "auto" | "tidal" | "qobuz" | "amazon" | "deezer";
theme: string;
themeMode: "auto" | "light" | "dark";
fontFamily: FontFamily;
@@ -21,9 +21,9 @@ export interface Settings {
embedMaxQualityCover: boolean;
operatingSystem: "Windows" | "linux/MacOS";
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
qobuzQuality: "6" | "7";
qobuzQuality: "6" | "7" | "27";
amazonQuality: "original";
autoOrder: "tidal-qobuz-amazon" | "tidal-amazon-qobuz" | "qobuz-tidal-amazon" | "qobuz-amazon-tidal" | "amazon-tidal-qobuz" | "amazon-qobuz-tidal" | "tidal-qobuz" | "tidal-amazon" | "qobuz-tidal" | "qobuz-amazon" | "amazon-tidal" | "amazon-qobuz";
autoOrder: "tidal-qobuz-amazon-deezer" | "tidal-qobuz-deezer-amazon" | "tidal-amazon-qobuz-deezer" | "tidal-amazon-deezer-qobuz" | "tidal-deezer-qobuz-amazon" | "tidal-deezer-amazon-qobuz" | "qobuz-tidal-amazon-deezer" | "qobuz-tidal-deezer-amazon" | "qobuz-amazon-tidal-deezer" | "qobuz-amazon-deezer-tidal" | "qobuz-deezer-tidal-amazon" | "qobuz-deezer-amazon-tidal" | "amazon-tidal-qobuz-deezer" | "amazon-tidal-deezer-qobuz" | "amazon-qobuz-tidal-deezer" | "amazon-qobuz-deezer-tidal" | "amazon-deezer-tidal-qobuz" | "amazon-deezer-qobuz-tidal" | "deezer-tidal-qobuz-amazon" | "deezer-tidal-amazon-qobuz" | "deezer-qobuz-tidal-amazon" | "deezer-qobuz-amazon-tidal" | "deezer-amazon-tidal-qobuz" | "deezer-amazon-qobuz-tidal" | string;
autoQuality: "16" | "24";
allowFallback: boolean;
useSpotFetchAPI: boolean;
@@ -32,6 +32,7 @@ export interface Settings {
createM3u8File: boolean;
useFirstArtistOnly: boolean;
useSingleGenre: boolean;
embedGenre: boolean;
}
export const FOLDER_PRESETS: Record<FolderPreset, {
label: string;
@@ -79,6 +80,7 @@ export const TEMPLATE_VARIABLES = [
{ key: "{track}", description: "Track number", example: "01" },
{ key: "{disc}", description: "Disc number", example: "1" },
{ key: "{year}", description: "Release year", example: "2014" },
{ key: "{date}", description: "Release date (YYYY-MM-DD)", example: "2014-10-27" },
];
function detectOS(): "Windows" | "linux/MacOS" {
const platform = window.navigator.platform.toLowerCase();
@@ -105,7 +107,7 @@ export const DEFAULT_SETTINGS: Settings = {
tidalQuality: "LOSSLESS",
qobuzQuality: "6",
amazonQuality: "original",
autoOrder: "tidal-qobuz-amazon",
autoOrder: "tidal-qobuz-amazon-deezer",
autoQuality: "16",
allowFallback: true,
useSpotFetchAPI: false,
@@ -113,7 +115,8 @@ export const DEFAULT_SETTINGS: Settings = {
createPlaylistFolder: true,
createM3u8File: false,
useFirstArtistOnly: false,
useSingleGenre: false
useSingleGenre: false,
embedGenre: true
};
export const FONT_OPTIONS: {
value: FontFamily;
@@ -208,9 +211,6 @@ function getSettingsFromLocalStorage(): Settings {
if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6";
}
if (parsed.qobuzQuality === "27") {
parsed.qobuzQuality = "6";
}
if (!('amazonQuality' in parsed)) {
parsed.amazonQuality = "original";
}
@@ -287,9 +287,6 @@ export async function loadSettings(): Promise<Settings> {
if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6";
}
if (parsed.qobuzQuality === "27") {
parsed.qobuzQuality = "6";
}
if (!('amazonQuality' in parsed)) {
parsed.amazonQuality = "original";
}
@@ -314,6 +311,9 @@ export async function loadSettings(): Promise<Settings> {
if (!('useSingleGenre' in parsed)) {
parsed.useSingleGenre = false;
}
if (!('embedGenre' in parsed)) {
parsed.embedGenre = true;
}
cachedSettings = { ...DEFAULT_SETTINGS, ...parsed };
return cachedSettings!;
}
@@ -339,6 +339,7 @@ export interface TemplateData {
track?: number;
disc?: number;
year?: string;
date?: string;
playlist?: string;
}
export function parseTemplate(template: string, data: TemplateData): string {
@@ -352,6 +353,7 @@ export function parseTemplate(template: string, data: TemplateData): string {
result = result.replace(/\{track\}/g, data.track ? String(data.track).padStart(2, "0") : "00");
result = result.replace(/\{disc\}/g, data.disc ? String(data.disc) : "1");
result = result.replace(/\{year\}/g, data.year || "0000");
result = result.replace(/\{date\}/g, data.date || "0000-00-00");
result = result.replace(/\{playlist\}/g, data.playlist || "");
return result;
}
+7
View File
@@ -46,3 +46,10 @@ export function openExternal(url: string) {
}
}
}
export function getFirstArtist(artistString: string): string {
if (!artistString)
return artistString;
const delimiters = /[,&]|(?:\s+(?:feat\.?|ft\.?|featuring)\s+)/i;
const parts = artistString.split(delimiters);
return parts[0].trim();
}
+4 -1
View File
@@ -108,7 +108,7 @@ export interface ArtistResponse {
}
export type SpotifyMetadataResponse = TrackResponse | AlbumResponse | PlaylistResponse | ArtistDiscographyResponse | ArtistResponse;
export interface DownloadRequest {
service: "tidal" | "qobuz" | "amazon";
service: "tidal" | "qobuz" | "amazon" | "deezer";
query?: string;
track_name?: string;
artist_name?: string;
@@ -139,6 +139,7 @@ export interface DownloadRequest {
spotify_url?: string;
use_first_artist_only?: boolean;
use_single_genre?: boolean;
embed_genre?: boolean;
}
export interface DownloadResponse {
success: boolean;
@@ -203,9 +204,11 @@ export interface TrackAvailability {
tidal: boolean;
amazon: boolean;
qobuz: boolean;
deezer: boolean;
tidal_url?: string;
amazon_url?: string;
qobuz_url?: string;
deezer_url?: string;
}
export interface CoverDownloadRequest {
cover_url: string;