v6.8
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-2
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
0cfdd24cb906bd58f1194d05e38654ae
|
||||
39c59c100ededac1c1e21fae937a2755
|
||||
Generated
+39
-39
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</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 */}
|
||||
|
||||
@@ -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,7 +612,7 @@ export function AudioConverterPage() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
onClick={handleConvert}
|
||||
disabled={converting || convertableCount === 0}
|
||||
@@ -607,7 +620,7 @@ export function AudioConverterPage() {
|
||||
>
|
||||
{converting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Spinner className="h-4 w-4" />
|
||||
Converting...
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -221,11 +221,12 @@ export function SettingsPage() {
|
||||
{/* Source Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="downloader" className="text-sm">Source</Label>
|
||||
<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">
|
||||
<SelectTrigger id="downloader" className="h-9 w-fit">
|
||||
<SelectValue placeholder="Select a source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -244,6 +245,38 @@ export function SettingsPage() {
|
||||
</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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user