This commit is contained in:
afkarxyz
2025-12-13 13:32:09 +07:00
parent 5c1d6619b5
commit 22742f1ddd
11 changed files with 220 additions and 137 deletions
+6 -32
View File
@@ -1,4 +1,4 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import { Button } from "@/components/ui/button";
import {
@@ -8,8 +8,7 @@ import {
TrendingUp,
FileAudio,
Clock,
Gauge,
HardDrive
Gauge
} from "lucide-react";
import type { AnalysisResult } from "@/types/api";
@@ -80,30 +79,13 @@ export function AudioAnalysis({
// Calculate Nyquist frequency (half of sample rate)
const nyquistFreq = result.sample_rate / 2;
// Calculate approximate data size (uncompressed PCM)
// Formula: sample_rate * channels * (bits_per_sample / 8) * duration
const dataSizeBytes = result.sample_rate * result.channels * (result.bits_per_sample / 8) * result.duration;
const dataSizeMB = dataSizeBytes / (1024 * 1024);
const formatDataSize = (mb: number) => {
if (mb >= 1024) {
return `${(mb / 1024).toFixed(2)} GB`;
}
return `${mb.toFixed(2)} MB`;
};
return (
<Card>
<CardHeader>
<div className="space-y-1">
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Audio Quality Analysis
</CardTitle>
<CardDescription>
Technical analysis of audio file properties
</CardDescription>
</div>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Audio Quality Analysis
</CardTitle>
</CardHeader>
<CardContent className="px-6 space-y-6">
@@ -149,14 +131,6 @@ export function AudioAnalysis({
</div>
<p className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<HardDrive className="h-3 w-3" />
Data Size
</div>
<p className="font-semibold">{formatDataSize(dataSizeMB)}</p>
</div>
</div>
{/* Dynamic Range Analysis */}
+46 -33
View File
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef } from "react";
import { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import {
@@ -11,10 +11,10 @@ import {
X,
CheckCircle2,
AlertCircle,
Loader2,
Trash2,
FileMusic,
} from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import {
IsFFmpegInstalled,
DownloadFFmpeg,
@@ -47,9 +47,9 @@ export function AudioConverterPage() {
const [ffmpegInstalled, setFfmpegInstalled] = useState<boolean>(false);
const [installingFfmpeg, setInstallingFfmpeg] = useState(false);
const [files, setFiles] = useState<AudioFile[]>(() => {
// Initialize from localStorage synchronously
// Initialize from sessionStorage synchronously
try {
const saved = localStorage.getItem(STORAGE_KEY);
const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.files && Array.isArray(parsed.files) && parsed.files.length > 0) {
@@ -63,7 +63,7 @@ export function AudioConverterPage() {
});
const [outputFormat, setOutputFormat] = useState<"mp3" | "m4a">(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.outputFormat === "mp3" || parsed.outputFormat === "m4a") {
@@ -77,7 +77,7 @@ export function AudioConverterPage() {
});
const [bitrate, setBitrate] = useState(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY);
const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.bitrate) {
@@ -92,38 +92,47 @@ export function AudioConverterPage() {
const [converting, setConverting] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isDraggingFFmpeg, setIsDraggingFFmpeg] = useState(false);
const isInitialMount = useRef(true);
const [isFullscreen, setIsFullscreen] = useState(false);
// Helper function to save state
// Helper function to save state to sessionStorage
const saveState = useCallback((stateToSave: { files: AudioFile[]; outputFormat: "mp3" | "m4a"; bitrate: string }) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave));
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave));
} catch (err) {
console.error("Failed to save state:", err);
}
}, []);
// Load saved state from localStorage on mount (only for ffmpeg check)
// Load saved state from sessionStorage on mount (only for ffmpeg check)
useEffect(() => {
checkFfmpegInstallation();
}, []);
// Save state to localStorage whenever files, outputFormat, or bitrate changes
// Skip on initial mount to avoid overwriting with empty state
// Save state to sessionStorage whenever files, outputFormat, or bitrate changes
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
saveState({ files, outputFormat, bitrate });
}, [files, outputFormat, bitrate, saveState]);
// Save state on unmount as well
// Detect fullscreen/maximized window
useEffect(() => {
return () => {
saveState({ files, outputFormat, bitrate });
const checkFullscreen = () => {
// Check if window is maximized or fullscreen
// For Wails, we can check if window height is close to screen height
const isMaximized = window.innerHeight >= window.screen.height * 0.9;
setIsFullscreen(isMaximized);
};
}, [files, outputFormat, bitrate, saveState]);
checkFullscreen();
window.addEventListener("resize", checkFullscreen);
// Also check on window focus in case user maximizes externally
window.addEventListener("focus", checkFullscreen);
return () => {
window.removeEventListener("resize", checkFullscreen);
window.removeEventListener("focus", checkFullscreen);
};
}, []);
const checkFfmpegInstallation = async () => {
try {
@@ -373,7 +382,7 @@ export function AudioConverterPage() {
const getStatusIcon = (status: AudioFile["status"]) => {
switch (status) {
case "converting":
return <Loader2 className="h-4 w-4 animate-spin text-primary" />;
return <Spinner className="h-4 w-4 text-primary" />;
case "success":
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "error":
@@ -390,13 +399,15 @@ export function AudioConverterPage() {
// Show FFmpeg installation prompt if not installed
if (ffmpegInstalled === false) {
return (
<div className="space-y-6">
<div className={`space-y-6 ${isFullscreen ? "h-full flex flex-col" : ""}`}>
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold">Audio Converter</h1>
</div>
<div
className={`flex flex-col items-center justify-center h-[400px] border-2 border-dashed rounded-lg transition-colors ${
className={`flex flex-col items-center justify-center border-2 border-dashed rounded-lg transition-all ${
isFullscreen ? "flex-1 min-h-[400px]" : "h-[400px]"
} ${
isDraggingFFmpeg
? "border-primary bg-primary/10"
: "border-muted-foreground/30"
@@ -433,7 +444,7 @@ export function AudioConverterPage() {
>
{installingFfmpeg ? (
<>
<Loader2 className="h-5 w-5" />
<Spinner className="h-5 w-5" />
Installing FFmpeg...
</>
) : (
@@ -449,7 +460,7 @@ export function AudioConverterPage() {
}
return (
<div className="space-y-6">
<div className={`space-y-6 ${isFullscreen ? "h-full flex flex-col" : ""}`}>
{/* Header */}
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold">Audio Converter</h1>
@@ -457,7 +468,9 @@ export function AudioConverterPage() {
{/* Drop Zone / File List */}
<div
className={`flex flex-col items-center justify-center h-[400px] border-2 border-dashed rounded-lg transition-colors ${
className={`flex flex-col items-center justify-center border-2 border-dashed rounded-lg transition-all ${
isFullscreen ? "flex-1 min-h-[400px]" : "h-[400px]"
} ${
isDragging
? "border-primary bg-primary/10"
: "border-muted-foreground/30"
@@ -599,15 +612,15 @@ export function AudioConverterPage() {
</div>
{/* Convert Button */}
<div className="flex justify-end pt-4 border-t shrink-0">
<Button
onClick={handleConvert}
disabled={converting || convertableCount === 0}
size="lg"
>
<div className="flex justify-center pt-4 border-t shrink-0">
<Button
onClick={handleConvert}
disabled={converting || convertableCount === 0}
size="lg"
>
{converting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<Spinner className="h-4 w-4" />
Converting...
</>
) : (
+58 -25
View File
@@ -221,29 +221,62 @@ export function SettingsPage() {
{/* Source Selection */}
<div className="space-y-2">
<Label htmlFor="downloader" className="text-sm">Source</Label>
<Select
value={tempSettings.downloader}
onValueChange={(value: "auto" | "deezer" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))}
>
<SelectTrigger id="downloader" className="h-9">
<SelectValue placeholder="Select a source" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="tidal">
<span className="flex items-center"><TidalIcon />Tidal</span>
</SelectItem>
<SelectItem value="deezer">
<span className="flex items-center"><DeezerIcon />Deezer</span>
</SelectItem>
<SelectItem value="qobuz">
<span className="flex items-center"><QobuzIcon />Qobuz</span>
</SelectItem>
<SelectItem value="amazon">
<span className="flex items-center"><AmazonIcon />Amazon Music</span>
</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2">
<Select
value={tempSettings.downloader}
onValueChange={(value: "auto" | "deezer" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))}
>
<SelectTrigger id="downloader" className="h-9 w-fit">
<SelectValue placeholder="Select a source" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="tidal">
<span className="flex items-center"><TidalIcon />Tidal</span>
</SelectItem>
<SelectItem value="deezer">
<span className="flex items-center"><DeezerIcon />Deezer</span>
</SelectItem>
<SelectItem value="qobuz">
<span className="flex items-center"><QobuzIcon />Qobuz</span>
</SelectItem>
<SelectItem value="amazon">
<span className="flex items-center"><AmazonIcon />Amazon Music</span>
</SelectItem>
</SelectContent>
</Select>
{/* Quality dropdown for Tidal */}
{tempSettings.downloader === "tidal" && (
<Select
value={tempSettings.tidalQuality}
onValueChange={(value: "LOSSLESS" | "HI_RES_LOSSLESS") => setTempSettings((prev) => ({ ...prev, tidalQuality: value }))}
>
<SelectTrigger className="h-9 w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="LOSSLESS">Lossless (16-bit/CD Quality)</SelectItem>
<SelectItem value="HI_RES_LOSSLESS">Hi-Res Lossless (24-bit/48kHz+)</SelectItem>
</SelectContent>
</Select>
)}
{/* Quality dropdown for Qobuz */}
{tempSettings.downloader === "qobuz" && (
<Select
value={tempSettings.qobuzQuality}
onValueChange={(value: "6" | "7" | "27") => setTempSettings((prev) => ({ ...prev, qobuzQuality: value }))}
>
<SelectTrigger className="h-9 w-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="6">FLAC 16-bit (CD Quality)</SelectItem>
<SelectItem value="7">FLAC 24-bit</SelectItem>
<SelectItem value="27">Hi-Res (24-bit/96kHz+)</SelectItem>
</SelectContent>
</Select>
)}
</div>
</div>
{/* Embed Lyrics */}
@@ -256,7 +289,7 @@ export function SettingsPage() {
/>
</div>
<div className="border-t pt-4" />
<div className="border-t pt-2" />
{/* Folder Structure */}
<div className="space-y-2">
@@ -306,7 +339,7 @@ export function SettingsPage() {
)}
</div>
<div className="border-t pt-4" />
<div className="border-t pt-2" />
{/* Filename Format */}
<div className="space-y-2">
+22
View File
@@ -115,6 +115,7 @@ export function useDownload() {
service_url: streamingURLs.tidal_url,
duration: durationSeconds,
item_id: itemID, // Pass the same itemID through all attempts
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
});
if (tidalResponse.success) {
@@ -209,6 +210,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
});
// If Qobuz also failed, mark the item as failed
@@ -224,6 +226,14 @@ export function useDownload() {
// Convert duration from ms to seconds for backend
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
// Determine audio format based on service
let audioFormat: string | undefined;
if (service === "tidal") {
audioFormat = settings.tidalQuality || "LOSSLESS";
} else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
}
const singleServiceResponse = await downloadTrack({
isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon",
@@ -240,6 +250,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics,
duration: durationSecondsForFallback,
item_id: itemID, // Pass itemID for tracking
audio_format: audioFormat,
});
// Mark as failed if download failed for single-service attempt
@@ -344,6 +355,7 @@ export function useDownload() {
service_url: streamingURLs.tidal_url,
duration: durationSeconds,
item_id: itemID,
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
});
if (tidalResponse.success) {
@@ -429,6 +441,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
});
// If Qobuz also failed, mark the item as failed
@@ -443,6 +456,14 @@ export function useDownload() {
// Single service download
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
// Determine audio format based on service
let audioFormat: string | undefined;
if (service === "tidal") {
audioFormat = settings.tidalQuality || "LOSSLESS";
} else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6";
}
const singleServiceResponse = await downloadTrack({
isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon",
@@ -459,6 +480,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics,
duration: durationSecondsForFallback,
item_id: itemID,
audio_format: audioFormat,
});
// Mark as failed if download failed for single-service attempt
+14 -2
View File
@@ -26,7 +26,10 @@ export interface Settings {
trackNumber: boolean;
sfxEnabled: boolean;
embedLyrics: boolean;
operatingSystem: "Windows" | "linux/MacOS"
operatingSystem: "Windows" | "linux/MacOS";
// Quality settings for specific sources
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
qobuzQuality: "6" | "7" | "27";
}
// Folder preset templates
@@ -83,7 +86,9 @@ export const DEFAULT_SETTINGS: Settings = {
trackNumber: false,
sfxEnabled: true,
embedLyrics: false,
operatingSystem: detectOS()
operatingSystem: detectOS(),
tidalQuality: "LOSSLESS", // Default: 16-bit lossless
qobuzQuality: "6" // Default: FLAC 16-bit
};
export const FONT_OPTIONS: { value: FontFamily; label: string; fontFamily: string }[] = [
@@ -160,6 +165,13 @@ export function getSettings(): Settings {
}
// Always use detected OS (don't persist it)
parsed.operatingSystem = detectOS();
// Set default quality if not present
if (!('tidalQuality' in parsed)) {
parsed.tidalQuality = "LOSSLESS";
}
if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6";
}
return { ...DEFAULT_SETTINGS, ...parsed };
}
} catch (error) {