diff --git a/app.go b/app.go index 31c98f9..b540e6a 100644 --- a/app.go +++ b/app.go @@ -254,7 +254,12 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { case "qobuz": downloader := backend.NewQobuzDownloader() - filename, err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber) + // Default to "6" (FLAC 16-bit) for Qobuz if not specified + quality := req.AudioFormat + if quality == "" { + quality = "6" + } + filename, err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber) default: // deezer downloader := backend.NewDeezerDownloader() diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index 39401ff..fa2f38e 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -13,6 +13,7 @@ import ( "runtime" "strings" "sync" + "syscall" "time" "github.com/ulikunitz/xz" @@ -74,6 +75,12 @@ func IsFFmpegInstalled() (bool, error) { // Verify it's executable cmd := exec.Command(ffmpegPath, "-version") + // Hide console window on Windows + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + } + } err = cmd.Run() return err == nil, nil } @@ -384,6 +391,12 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) { fmt.Printf("[FFmpeg] Converting: %s -> %s\n", inputFile, outputFile) cmd := exec.Command(ffmpegPath, args...) + // Hide console window on Windows + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + } + } output, err := cmd.CombinedOutput() if err != nil { result.Error = fmt.Sprintf("conversion failed: %s - %s", err.Error(), string(output)) @@ -528,6 +541,12 @@ func InstallFFmpegFromFile(filePath string) error { } cmd := exec.Command(ffmpegPath, "-version") + // Hide console window on Windows + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + } + } verifyErr = cmd.Run() if verifyErr == nil { break diff --git a/backend/qobuz.go b/backend/qobuz.go index 2b688d6..2771c41 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -122,15 +122,20 @@ func (q *QobuzDownloader) SearchByISRC(isrc string) (*QobuzTrack, error) { func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, error) { // Map quality to Qobuz quality code // Qobuz uses: 5 (MP3 320), 6 (FLAC 16-bit), 7 (FLAC 24-bit), 27 (Hi-Res) - qualityCode := "27" // Default to Hi-Res + qualityCode := quality // Use the provided quality parameter + if qualityCode == "" { + qualityCode = "6" // Default to FLAC 16-bit if not specified + } - fmt.Printf("Getting download URL for track ID: %d\n", trackID) + fmt.Printf("Getting download URL for track ID: %d with requested quality: %s\n", trackID, qualityCode) + fmt.Printf("Quality codes: 6=FLAC 16-bit, 7=FLAC 24-bit, 27=Hi-Res\n") // Decode base64 API URLs primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9") // Try primary API first primaryURL := fmt.Sprintf("%s%d&quality=%s", string(primaryBase), trackID, qualityCode) + fmt.Printf("Qobuz API URL: %s\n", primaryURL) resp, err := q.client.Get(primaryURL) if err == nil && resp.StatusCode == 200 { diff --git a/frontend/package.json b/frontend/package.json index 211fd45..3582c46 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,12 +38,12 @@ "tailwindcss": "^4.1.18" }, "devDependencies": { - "@eslint/js": "^9.39.1", + "@eslint/js": "^9.39.2", "@types/node": "^25.0.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", - "eslint": "^9.39.1", + "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 672605c..106c1e4 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -0cfdd24cb906bd58f1194d05e38654ae \ No newline at end of file +39c59c100ededac1c1e21fae937a2755 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0e1284b..7c8b143 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -82,8 +82,8 @@ importers: version: 4.1.18 devDependencies: '@eslint/js': - specifier: ^9.39.1 - version: 9.39.1 + specifier: ^9.39.2 + version: 9.39.2 '@types/node': specifier: ^25.0.1 version: 25.0.1 @@ -97,14 +97,14 @@ importers: specifier: ^5.1.2 version: 5.1.2(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)) eslint: - specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.1(jiti@2.6.1)) + version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.24 - version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) + version: 0.4.24(eslint@9.39.2(jiti@2.6.1)) globals: specifier: ^16.5.0 version: 16.5.0 @@ -119,7 +119,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.49.0 - version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.7 version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2) @@ -394,8 +394,8 @@ packages: resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.1': - resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1507,8 +1507,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.1: - resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2260,9 +2260,9 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2297,7 +2297,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.1': {} + '@eslint/js@9.39.2': {} '@eslint/object-schema@2.1.7': {} @@ -3098,15 +3098,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.49.0 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -3114,14 +3114,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3144,13 +3144,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3173,13 +3173,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3328,20 +3328,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.0.1(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): dependencies: '@babel/core': 7.28.5 '@babel/parser': 7.28.5 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.1.13 zod-validation-error: 4.0.2(zod@4.1.13) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.24(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-react-refresh@0.4.24(eslint@9.39.2(jiti@2.6.1)): dependencies: - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) eslint-scope@8.4.0: dependencies: @@ -3352,15 +3352,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.1(jiti@2.6.1): + eslint@9.39.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.1 + '@eslint/js': 9.39.2 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 @@ -3780,13 +3780,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color diff --git a/frontend/src/components/AudioAnalysis.tsx b/frontend/src/components/AudioAnalysis.tsx index 38c8198..6810228 100644 --- a/frontend/src/components/AudioAnalysis.tsx +++ b/frontend/src/components/AudioAnalysis.tsx @@ -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 ( -
- - - Audio Quality Analysis - - - Technical analysis of audio file properties - -
+ + + Audio Quality Analysis +
@@ -149,14 +131,6 @@ export function AudioAnalysis({

{(nyquistFreq / 1000).toFixed(1)} kHz

- -
-
- - Data Size -
-

{formatDataSize(dataSizeMB)}

-
{/* Dynamic Range Analysis */} diff --git a/frontend/src/components/AudioConverterPage.tsx b/frontend/src/components/AudioConverterPage.tsx index 63f99e1..b28d5c6 100644 --- a/frontend/src/components/AudioConverterPage.tsx +++ b/frontend/src/components/AudioConverterPage.tsx @@ -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(false); const [installingFfmpeg, setInstallingFfmpeg] = useState(false); const [files, setFiles] = useState(() => { - // 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 ; + return ; case "success": return ; case "error": @@ -390,13 +399,15 @@ export function AudioConverterPage() { // Show FFmpeg installation prompt if not installed if (ffmpegInstalled === false) { return ( -
+

Audio Converter

{installingFfmpeg ? ( <> - + Installing FFmpeg... ) : ( @@ -449,7 +460,7 @@ export function AudioConverterPage() { } return ( -
+
{/* Header */}

Audio Converter

@@ -457,7 +468,9 @@ export function AudioConverterPage() { {/* Drop Zone / File List */}
{/* Convert Button */} -
-
-
+
{/* Folder Structure */}
@@ -306,7 +339,7 @@ export function SettingsPage() { )}
-
+
{/* Filename Format */}
diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts index 4a3714a..74541bd 100644 --- a/frontend/src/hooks/useDownload.ts +++ b/frontend/src/hooks/useDownload.ts @@ -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 diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index cb48cef..b861383 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -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) {