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 -1
View File
@@ -254,7 +254,12 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
case "qobuz": case "qobuz":
downloader := backend.NewQobuzDownloader() 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 default: // deezer
downloader := backend.NewDeezerDownloader() downloader := backend.NewDeezerDownloader()
+19
View File
@@ -13,6 +13,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/ulikunitz/xz" "github.com/ulikunitz/xz"
@@ -74,6 +75,12 @@ func IsFFmpegInstalled() (bool, error) {
// Verify it's executable // Verify it's executable
cmd := exec.Command(ffmpegPath, "-version") cmd := exec.Command(ffmpegPath, "-version")
// Hide console window on Windows
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
}
err = cmd.Run() err = cmd.Run()
return err == nil, nil return err == nil, nil
} }
@@ -384,6 +391,12 @@ func ConvertAudio(req ConvertAudioRequest) ([]ConvertAudioResult, error) {
fmt.Printf("[FFmpeg] Converting: %s -> %s\n", inputFile, outputFile) fmt.Printf("[FFmpeg] Converting: %s -> %s\n", inputFile, outputFile)
cmd := exec.Command(ffmpegPath, args...) cmd := exec.Command(ffmpegPath, args...)
// Hide console window on Windows
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
}
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
result.Error = fmt.Sprintf("conversion failed: %s - %s", err.Error(), string(output)) 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") cmd := exec.Command(ffmpegPath, "-version")
// Hide console window on Windows
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
}
verifyErr = cmd.Run() verifyErr = cmd.Run()
if verifyErr == nil { if verifyErr == nil {
break break
+7 -2
View File
@@ -122,15 +122,20 @@ func (q *QobuzDownloader) SearchByISRC(isrc string) (*QobuzTrack, error) {
func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, error) { func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, error) {
// Map quality to Qobuz quality code // Map quality to Qobuz quality code
// Qobuz uses: 5 (MP3 320), 6 (FLAC 16-bit), 7 (FLAC 24-bit), 27 (Hi-Res) // 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 // Decode base64 API URLs
primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9") primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9")
// Try primary API first // Try primary API first
primaryURL := fmt.Sprintf("%s%d&quality=%s", string(primaryBase), trackID, qualityCode) 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) resp, err := q.client.Get(primaryURL)
if err == nil && resp.StatusCode == 200 { if err == nil && resp.StatusCode == 200 {
+2 -2
View File
@@ -38,12 +38,12 @@
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.2",
"@types/node": "^25.0.1", "@types/node": "^25.0.1",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.1", "eslint": "^9.39.2",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
+1 -1
View File
@@ -1 +1 @@
0cfdd24cb906bd58f1194d05e38654ae 39c59c100ededac1c1e21fae937a2755
+39 -39
View File
@@ -82,8 +82,8 @@ importers:
version: 4.1.18 version: 4.1.18
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^9.39.1 specifier: ^9.39.2
version: 9.39.1 version: 9.39.2
'@types/node': '@types/node':
specifier: ^25.0.1 specifier: ^25.0.1
version: 25.0.1 version: 25.0.1
@@ -97,14 +97,14 @@ importers:
specifier: ^5.1.2 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)) version: 5.1.2(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2))
eslint: eslint:
specifier: ^9.39.1 specifier: ^9.39.2
version: 9.39.1(jiti@2.6.1) version: 9.39.2(jiti@2.6.1)
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^7.0.1 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: eslint-plugin-react-refresh:
specifier: ^0.4.24 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: globals:
specifier: ^16.5.0 specifier: ^16.5.0
version: 16.5.0 version: 16.5.0
@@ -119,7 +119,7 @@ importers:
version: 5.9.3 version: 5.9.3
typescript-eslint: typescript-eslint:
specifier: ^8.49.0 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: vite:
specifier: ^7.2.7 specifier: ^7.2.7
version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2) 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==} resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.39.1': '@eslint/js@9.39.2':
resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7': '@eslint/object-schema@2.1.7':
@@ -1507,8 +1507,8 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.39.1: eslint@9.39.2:
resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -2260,9 +2260,9 @@ snapshots:
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
optional: true 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: dependencies:
eslint: 9.39.1(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
@@ -2297,7 +2297,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@eslint/js@9.39.1': {} '@eslint/js@9.39.2': {}
'@eslint/object-schema@2.1.7': {} '@eslint/object-schema@2.1.7': {}
@@ -3098,15 +3098,15 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 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: dependencies:
'@eslint-community/regexpp': 4.12.2 '@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/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/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.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)
'@typescript-eslint/visitor-keys': 8.49.0 '@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 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -3114,14 +3114,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/scope-manager': 8.49.0
'@typescript-eslint/types': 8.49.0 '@typescript-eslint/types': 8.49.0
'@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.49.0 '@typescript-eslint/visitor-keys': 8.49.0
debug: 4.4.3 debug: 4.4.3
eslint: 9.39.1(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3144,13 +3144,13 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 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: dependencies:
'@typescript-eslint/types': 8.49.0 '@typescript-eslint/types': 8.49.0
'@typescript-eslint/typescript-estree': 8.49.0(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) '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.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) ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -3173,13 +3173,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: 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/scope-manager': 8.49.0
'@typescript-eslint/types': 8.49.0 '@typescript-eslint/types': 8.49.0
'@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) '@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 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3328,20 +3328,20 @@ snapshots:
escape-string-regexp@4.0.0: {} 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: dependencies:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
'@babel/parser': 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 hermes-parser: 0.25.1
zod: 4.1.13 zod: 4.1.13
zod-validation-error: 4.0.2(zod@4.1.13) zod-validation-error: 4.0.2(zod@4.1.13)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
eslint: 9.39.1(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
eslint-scope@8.4.0: eslint-scope@8.4.0:
dependencies: dependencies:
@@ -3352,15 +3352,15 @@ snapshots:
eslint-visitor-keys@4.2.1: {} eslint-visitor-keys@4.2.1: {}
eslint@9.39.1(jiti@2.6.1): eslint@9.39.2(jiti@2.6.1):
dependencies: 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-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1 '@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2 '@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0 '@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.3 '@eslint/eslintrc': 3.3.3
'@eslint/js': 9.39.1 '@eslint/js': 9.39.2
'@eslint/plugin-kit': 0.4.1 '@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7 '@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
@@ -3780,13 +3780,13 @@ snapshots:
dependencies: dependencies:
prelude-ls: 1.2.1 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: 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/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.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/typescript-estree': 8.49.0(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) '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1) eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
+2 -28
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 { Spinner } from "@/components/ui/spinner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -8,8 +8,7 @@ import {
TrendingUp, TrendingUp,
FileAudio, FileAudio,
Clock, Clock,
Gauge, Gauge
HardDrive
} from "lucide-react"; } from "lucide-react";
import type { AnalysisResult } from "@/types/api"; import type { AnalysisResult } from "@/types/api";
@@ -80,30 +79,13 @@ export function AudioAnalysis({
// Calculate Nyquist frequency (half of sample rate) // Calculate Nyquist frequency (half of sample rate)
const nyquistFreq = result.sample_rate / 2; 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 ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<div className="space-y-1">
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" /> <Activity className="h-5 w-5" />
Audio Quality Analysis Audio Quality Analysis
</CardTitle> </CardTitle>
<CardDescription>
Technical analysis of audio file properties
</CardDescription>
</div>
</CardHeader> </CardHeader>
<CardContent className="px-6 space-y-6"> <CardContent className="px-6 space-y-6">
@@ -149,14 +131,6 @@ export function AudioAnalysis({
</div> </div>
<p className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</p> <p className="font-semibold">{(nyquistFreq / 1000).toFixed(1)} kHz</p>
</div> </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> </div>
{/* Dynamic Range Analysis */} {/* Dynamic Range Analysis */}
+41 -28
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 { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -11,10 +11,10 @@ import {
X, X,
CheckCircle2, CheckCircle2,
AlertCircle, AlertCircle,
Loader2,
Trash2, Trash2,
FileMusic, FileMusic,
} from "lucide-react"; } from "lucide-react";
import { Spinner } from "@/components/ui/spinner";
import { import {
IsFFmpegInstalled, IsFFmpegInstalled,
DownloadFFmpeg, DownloadFFmpeg,
@@ -47,9 +47,9 @@ export function AudioConverterPage() {
const [ffmpegInstalled, setFfmpegInstalled] = useState<boolean>(false); const [ffmpegInstalled, setFfmpegInstalled] = useState<boolean>(false);
const [installingFfmpeg, setInstallingFfmpeg] = useState(false); const [installingFfmpeg, setInstallingFfmpeg] = useState(false);
const [files, setFiles] = useState<AudioFile[]>(() => { const [files, setFiles] = useState<AudioFile[]>(() => {
// Initialize from localStorage synchronously // Initialize from sessionStorage synchronously
try { try {
const saved = localStorage.getItem(STORAGE_KEY); const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) { if (saved) {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
if (parsed.files && Array.isArray(parsed.files) && parsed.files.length > 0) { if (parsed.files && Array.isArray(parsed.files) && parsed.files.length > 0) {
@@ -63,7 +63,7 @@ export function AudioConverterPage() {
}); });
const [outputFormat, setOutputFormat] = useState<"mp3" | "m4a">(() => { const [outputFormat, setOutputFormat] = useState<"mp3" | "m4a">(() => {
try { try {
const saved = localStorage.getItem(STORAGE_KEY); const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) { if (saved) {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
if (parsed.outputFormat === "mp3" || parsed.outputFormat === "m4a") { if (parsed.outputFormat === "mp3" || parsed.outputFormat === "m4a") {
@@ -77,7 +77,7 @@ export function AudioConverterPage() {
}); });
const [bitrate, setBitrate] = useState(() => { const [bitrate, setBitrate] = useState(() => {
try { try {
const saved = localStorage.getItem(STORAGE_KEY); const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved) { if (saved) {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
if (parsed.bitrate) { if (parsed.bitrate) {
@@ -92,38 +92,47 @@ export function AudioConverterPage() {
const [converting, setConverting] = useState(false); const [converting, setConverting] = useState(false);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isDraggingFFmpeg, setIsDraggingFFmpeg] = 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 }) => { const saveState = useCallback((stateToSave: { files: AudioFile[]; outputFormat: "mp3" | "m4a"; bitrate: string }) => {
try { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave));
} catch (err) { } catch (err) {
console.error("Failed to save state:", 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(() => { useEffect(() => {
checkFfmpegInstallation(); checkFfmpegInstallation();
}, []); }, []);
// Save state to localStorage whenever files, outputFormat, or bitrate changes // Save state to sessionStorage whenever files, outputFormat, or bitrate changes
// Skip on initial mount to avoid overwriting with empty state
useEffect(() => { useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
saveState({ files, outputFormat, bitrate }); saveState({ files, outputFormat, bitrate });
}, [files, outputFormat, bitrate, saveState]); }, [files, outputFormat, bitrate, saveState]);
// Save state on unmount as well // Detect fullscreen/maximized window
useEffect(() => { useEffect(() => {
return () => { const checkFullscreen = () => {
saveState({ files, outputFormat, bitrate }); // 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 () => { const checkFfmpegInstallation = async () => {
try { try {
@@ -373,7 +382,7 @@ export function AudioConverterPage() {
const getStatusIcon = (status: AudioFile["status"]) => { const getStatusIcon = (status: AudioFile["status"]) => {
switch (status) { switch (status) {
case "converting": case "converting":
return <Loader2 className="h-4 w-4 animate-spin text-primary" />; return <Spinner className="h-4 w-4 text-primary" />;
case "success": case "success":
return <CheckCircle2 className="h-4 w-4 text-green-500" />; return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "error": case "error":
@@ -390,13 +399,15 @@ export function AudioConverterPage() {
// Show FFmpeg installation prompt if not installed // Show FFmpeg installation prompt if not installed
if (ffmpegInstalled === false) { if (ffmpegInstalled === false) {
return ( return (
<div className="space-y-6"> <div className={`space-y-6 ${isFullscreen ? "h-full flex flex-col" : ""}`}>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h1 className="text-2xl font-bold">Audio Converter</h1> <h1 className="text-2xl font-bold">Audio Converter</h1>
</div> </div>
<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 isDraggingFFmpeg
? "border-primary bg-primary/10" ? "border-primary bg-primary/10"
: "border-muted-foreground/30" : "border-muted-foreground/30"
@@ -433,7 +444,7 @@ export function AudioConverterPage() {
> >
{installingFfmpeg ? ( {installingFfmpeg ? (
<> <>
<Loader2 className="h-5 w-5" /> <Spinner className="h-5 w-5" />
Installing FFmpeg... Installing FFmpeg...
</> </>
) : ( ) : (
@@ -449,7 +460,7 @@ export function AudioConverterPage() {
} }
return ( return (
<div className="space-y-6"> <div className={`space-y-6 ${isFullscreen ? "h-full flex flex-col" : ""}`}>
{/* Header */} {/* Header */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<h1 className="text-2xl font-bold">Audio Converter</h1> <h1 className="text-2xl font-bold">Audio Converter</h1>
@@ -457,7 +468,9 @@ export function AudioConverterPage() {
{/* Drop Zone / File List */} {/* Drop Zone / File List */}
<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]"
} ${
isDragging isDragging
? "border-primary bg-primary/10" ? "border-primary bg-primary/10"
: "border-muted-foreground/30" : "border-muted-foreground/30"
@@ -599,7 +612,7 @@ export function AudioConverterPage() {
</div> </div>
{/* Convert Button */} {/* Convert Button */}
<div className="flex justify-end pt-4 border-t shrink-0"> <div className="flex justify-center pt-4 border-t shrink-0">
<Button <Button
onClick={handleConvert} onClick={handleConvert}
disabled={converting || convertableCount === 0} disabled={converting || convertableCount === 0}
@@ -607,7 +620,7 @@ export function AudioConverterPage() {
> >
{converting ? ( {converting ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Spinner className="h-4 w-4" />
Converting... Converting...
</> </>
) : ( ) : (
+36 -3
View File
@@ -221,11 +221,12 @@ export function SettingsPage() {
{/* Source Selection */} {/* Source Selection */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="downloader" className="text-sm">Source</Label> <Label htmlFor="downloader" className="text-sm">Source</Label>
<div className="flex gap-2">
<Select <Select
value={tempSettings.downloader} value={tempSettings.downloader}
onValueChange={(value: "auto" | "deezer" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))} onValueChange={(value: "auto" | "deezer" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))}
> >
<SelectTrigger id="downloader" className="h-9"> <SelectTrigger id="downloader" className="h-9 w-fit">
<SelectValue placeholder="Select a source" /> <SelectValue placeholder="Select a source" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -244,6 +245,38 @@ export function SettingsPage() {
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </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> </div>
{/* Embed Lyrics */} {/* Embed Lyrics */}
@@ -256,7 +289,7 @@ export function SettingsPage() {
/> />
</div> </div>
<div className="border-t pt-4" /> <div className="border-t pt-2" />
{/* Folder Structure */} {/* Folder Structure */}
<div className="space-y-2"> <div className="space-y-2">
@@ -306,7 +339,7 @@ export function SettingsPage() {
)} )}
</div> </div>
<div className="border-t pt-4" /> <div className="border-t pt-2" />
{/* Filename Format */} {/* Filename Format */}
<div className="space-y-2"> <div className="space-y-2">
+22
View File
@@ -115,6 +115,7 @@ export function useDownload() {
service_url: streamingURLs.tidal_url, service_url: streamingURLs.tidal_url,
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, // Pass the same itemID through all attempts item_id: itemID, // Pass the same itemID through all attempts
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
}); });
if (tidalResponse.success) { if (tidalResponse.success) {
@@ -209,6 +210,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
duration: durationMs ? Math.round(durationMs / 1000) : undefined, duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID, 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 // If Qobuz also failed, mark the item as failed
@@ -224,6 +226,14 @@ export function useDownload() {
// Convert duration from ms to seconds for backend // Convert duration from ms to seconds for backend
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined; 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({ const singleServiceResponse = await downloadTrack({
isrc, isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon", service: service as "deezer" | "tidal" | "qobuz" | "amazon",
@@ -240,6 +250,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, // Pass itemID for tracking item_id: itemID, // Pass itemID for tracking
audio_format: audioFormat,
}); });
// Mark as failed if download failed for single-service attempt // Mark as failed if download failed for single-service attempt
@@ -344,6 +355,7 @@ export function useDownload() {
service_url: streamingURLs.tidal_url, service_url: streamingURLs.tidal_url,
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, item_id: itemID,
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
}); });
if (tidalResponse.success) { if (tidalResponse.success) {
@@ -429,6 +441,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
duration: durationMs ? Math.round(durationMs / 1000) : undefined, duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID, 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 // If Qobuz also failed, mark the item as failed
@@ -443,6 +456,14 @@ export function useDownload() {
// Single service download // Single service download
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined; 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({ const singleServiceResponse = await downloadTrack({
isrc, isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon", service: service as "deezer" | "tidal" | "qobuz" | "amazon",
@@ -459,6 +480,7 @@ export function useDownload() {
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, item_id: itemID,
audio_format: audioFormat,
}); });
// Mark as failed if download failed for single-service attempt // Mark as failed if download failed for single-service attempt
+14 -2
View File
@@ -26,7 +26,10 @@ export interface Settings {
trackNumber: boolean; trackNumber: boolean;
sfxEnabled: boolean; sfxEnabled: boolean;
embedLyrics: 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 // Folder preset templates
@@ -83,7 +86,9 @@ export const DEFAULT_SETTINGS: Settings = {
trackNumber: false, trackNumber: false,
sfxEnabled: true, sfxEnabled: true,
embedLyrics: false, 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 }[] = [ 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) // Always use detected OS (don't persist it)
parsed.operatingSystem = detectOS(); 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 }; return { ...DEFAULT_SETTINGS, ...parsed };
} }
} catch (error) { } catch (error) {