v7.0.5
This commit is contained in:
@@ -7,11 +7,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"spotiflac/backend"
|
"spotiflac/backend"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var isrcRegex = regexp.MustCompile(`^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$`)
|
||||||
|
|
||||||
|
func isValidISRC(isrc string) bool {
|
||||||
|
return isrcRegex.MatchString(isrc)
|
||||||
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -336,6 +345,11 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deezerISRC := req.ISRC
|
deezerISRC := req.ISRC
|
||||||
|
|
||||||
|
if len(deezerISRC) != 12 || !isValidISRC(deezerISRC) {
|
||||||
|
deezerISRC = ""
|
||||||
|
}
|
||||||
|
|
||||||
if deezerISRC == "" && req.SpotifyID != "" {
|
if deezerISRC == "" && req.SpotifyID != "" {
|
||||||
|
|
||||||
songlinkClient := backend.NewSongLinkClient()
|
songlinkClient := backend.NewSongLinkClient()
|
||||||
@@ -370,6 +384,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
backend.FailDownloadItem(itemID, fmt.Sprintf("Download failed: %v", err))
|
||||||
|
|
||||||
if filename != "" && !strings.HasPrefix(filename, "EXISTS:") {
|
if filename != "" && !strings.HasPrefix(filename, "EXISTS:") {
|
||||||
|
|
||||||
@@ -828,16 +843,19 @@ type DownloadFFmpegResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) DownloadFFmpeg() DownloadFFmpegResponse {
|
func (a *App) DownloadFFmpeg() DownloadFFmpegResponse {
|
||||||
|
runtime.EventsEmit(a.ctx, "ffmpeg:status", "starting")
|
||||||
err := backend.DownloadFFmpeg(func(progress int) {
|
err := backend.DownloadFFmpeg(func(progress int) {
|
||||||
fmt.Printf("[FFmpeg] Download progress: %d%%\n", progress)
|
runtime.EventsEmit(a.ctx, "ffmpeg:progress", progress)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "ffmpeg:status", "failed")
|
||||||
return DownloadFFmpegResponse{
|
return DownloadFFmpegResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runtime.EventsEmit(a.ctx, "ffmpeg:status", "completed")
|
||||||
return DownloadFFmpegResponse{
|
return DownloadFFmpegResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "FFmpeg installed successfully",
|
Message: "FFmpeg installed successfully",
|
||||||
@@ -1105,3 +1123,7 @@ func (a *App) LoadSettings() (map[string]interface{}, error) {
|
|||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CheckFFmpegInstalled() (bool, error) {
|
||||||
|
return backend.IsFFmpegInstalled()
|
||||||
|
}
|
||||||
|
|||||||
+33
-10
@@ -127,12 +127,12 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Getting download URL for track ID: %d with requested quality: %s\n", trackID, qualityCode)
|
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")
|
fmt.Printf("Quality codes: 6=FLAC 16-bit, 7=FLAC 24-bit\n")
|
||||||
|
|
||||||
primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9")
|
primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9")
|
||||||
|
|
||||||
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)
|
fmt.Printf("Trying Primary API: %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 {
|
||||||
@@ -143,7 +143,7 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
|
|
||||||
var streamResp QobuzStreamResponse
|
var streamResp QobuzStreamResponse
|
||||||
if err := json.Unmarshal(body, &streamResp); err == nil && streamResp.URL != "" {
|
if err := json.Unmarshal(body, &streamResp); err == nil && streamResp.URL != "" {
|
||||||
fmt.Printf("Got download URL from primary API\n")
|
fmt.Printf("✓ Got download URL from Primary API\n")
|
||||||
return streamResp.URL, nil
|
return streamResp.URL, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,20 +151,43 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Primary API failed, trying fallback...")
|
fmt.Println("Primary API failed, trying Fallback API #1...")
|
||||||
fallbackBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWJtdXNpYy54eXovYXBpL3N0cmVhbT90cmFja0lkPQ==")
|
fallbackBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWJtdXNpYy54eXovYXBpL3N0cmVhbT90cmFja0lkPQ==")
|
||||||
fallbackURL := fmt.Sprintf("%s%d&quality=%s", string(fallbackBase), trackID, qualityCode)
|
fallbackURL := fmt.Sprintf("%s%d&quality=%s", string(fallbackBase), trackID, qualityCode)
|
||||||
|
|
||||||
resp, err = q.client.Get(fallbackURL)
|
resp, err = q.client.Get(fallbackURL)
|
||||||
|
if err == nil && resp.StatusCode == 200 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err == nil && len(body) > 0 {
|
||||||
|
fmt.Printf("Fallback API #1 response: %s\n", string(body))
|
||||||
|
|
||||||
|
var streamResp QobuzStreamResponse
|
||||||
|
if err := json.Unmarshal(body, &streamResp); err == nil && streamResp.URL != "" {
|
||||||
|
fmt.Printf("✓ Got download URL from Fallback API #1\n")
|
||||||
|
return streamResp.URL, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Fallback API #1 failed, trying Fallback API #2...")
|
||||||
|
fallback2Base, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9xb2J1ei5zcXVpZC53dGYvYXBpL2Rvd25sb2FkLW11c2ljP3RyYWNrX2lkPQ==")
|
||||||
|
fallback2URL := fmt.Sprintf("%s%d&quality=%s", string(fallback2Base), trackID, qualityCode)
|
||||||
|
|
||||||
|
resp, err = q.client.Get(fallback2URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get download URL: %w", err)
|
return "", fmt.Errorf("all APIs failed to get download URL: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
fmt.Printf("Fallback API error response: %s\n", string(body))
|
fmt.Printf("Fallback API #2 error response (status %d): %s\n", resp.StatusCode, string(body))
|
||||||
return "", fmt.Errorf("API returned status %d", resp.StatusCode)
|
return "", fmt.Errorf("all APIs returned non-200 status")
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
@@ -176,7 +199,7 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
return "", fmt.Errorf("API returned empty response")
|
return "", fmt.Errorf("API returned empty response")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Fallback API response: %s\n", string(body))
|
fmt.Printf("Fallback API #2 response: %s\n", string(body))
|
||||||
|
|
||||||
var streamResp QobuzStreamResponse
|
var streamResp QobuzStreamResponse
|
||||||
if err := json.Unmarshal(body, &streamResp); err != nil {
|
if err := json.Unmarshal(body, &streamResp); err != nil {
|
||||||
@@ -189,10 +212,10 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if streamResp.URL == "" {
|
if streamResp.URL == "" {
|
||||||
return "", fmt.Errorf("no download URL available")
|
return "", fmt.Errorf("no download URL available from any API")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Got download URL from fallback API\n")
|
fmt.Printf("✓ Got download URL from Fallback API #2\n")
|
||||||
return streamResp.URL, nil
|
return streamResp.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import js from '@eslint/js'
|
import js from '@eslint/js';
|
||||||
import globals from 'globals'
|
import globals from 'globals';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint';
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
{
|
{
|
||||||
@@ -20,4 +19,4 @@ export default defineConfig([
|
|||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
|
|||||||
@@ -2,32 +2,23 @@ import sharp from 'sharp';
|
|||||||
import { readFileSync, mkdirSync } from 'fs';
|
import { readFileSync, mkdirSync } from 'fs';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const rootDir = join(__dirname, '..', '..');
|
const rootDir = join(__dirname, '..', '..');
|
||||||
|
|
||||||
const svgPath = join(rootDir, 'frontend', 'public', 'icon.svg');
|
const svgPath = join(rootDir, 'frontend', 'public', 'icon.svg');
|
||||||
const outputPath = join(rootDir, 'build', 'appicon.png');
|
const outputPath = join(rootDir, 'build', 'appicon.png');
|
||||||
|
|
||||||
async function generateIcon() {
|
async function generateIcon() {
|
||||||
try {
|
try {
|
||||||
// Ensure build directory exists
|
|
||||||
mkdirSync(join(rootDir, 'build'), { recursive: true });
|
mkdirSync(join(rootDir, 'build'), { recursive: true });
|
||||||
|
|
||||||
// Read SVG
|
|
||||||
const svgBuffer = readFileSync(svgPath);
|
const svgBuffer = readFileSync(svgPath);
|
||||||
|
|
||||||
// Convert SVG to PNG (1024x1024 for Wails)
|
|
||||||
await sharp(svgBuffer)
|
await sharp(svgBuffer)
|
||||||
.resize(1024, 1024)
|
.resize(1024, 1024)
|
||||||
.png()
|
.png()
|
||||||
.toFile(outputPath);
|
.toFile(outputPath);
|
||||||
|
|
||||||
console.log('✓ Icon generated:', outputPath);
|
console.log('✓ Icon generated:', outputPath);
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error('✗ Failed to generate icon:', error.message);
|
console.error('✗ Failed to generate icon:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateIcon();
|
generateIcon();
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { X, Download, CheckCircle2, XCircle, Clock, FileCheck, Trash2, HardDrive
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { GetDownloadQueue, ClearCompletedDownloads } from "../../wailsjs/go/main/App";
|
import { GetDownloadQueue, ClearCompletedDownloads, ClearAllDownloads } from "../../wailsjs/go/main/App";
|
||||||
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
import { backend } from "../../wailsjs/go/models";
|
import { backend } from "../../wailsjs/go/models";
|
||||||
interface DownloadQueueProps {
|
interface DownloadQueueProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -47,6 +48,17 @@ export function DownloadQueue({ isOpen, onClose }: DownloadQueueProps) {
|
|||||||
console.error("Failed to clear history:", error);
|
console.error("Failed to clear history:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleReset = async () => {
|
||||||
|
try {
|
||||||
|
await ClearAllDownloads();
|
||||||
|
const info = await GetDownloadQueue();
|
||||||
|
setQueueInfo(info);
|
||||||
|
toast.success("Download queue reset");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to reset queue:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
const getStatusIcon = (status: string) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "downloading":
|
case "downloading":
|
||||||
@@ -97,7 +109,7 @@ export function DownloadQueue({ isOpen, onClose }: DownloadQueueProps) {
|
|||||||
<DialogContent className="max-w-[1200px] w-[95vw] max-h-[80vh] flex flex-col p-0 gap-0 [&>button]:hidden">
|
<DialogContent className="max-w-[1200px] w-[95vw] max-h-[80vh] flex flex-col p-0 gap-0 [&>button]:hidden">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b space-y-0">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b space-y-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<DialogTitle className="text-lg font-semibold">Download Queue</DialogTitle>
|
<DialogTitle className="text-lg font-semibold hover:text-primary transition-colors cursor-pointer" onClick={handleReset}>Download Queue</DialogTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{(queueInfo.completed_count > 0 || queueInfo.failed_count > 0 || queueInfo.skipped_count > 0) && (<Button variant="ghost" size="sm" className="h-7 text-xs gap-1.5" onClick={handleClearHistory}>
|
{(queueInfo.completed_count > 0 || queueInfo.failed_count > 0 || queueInfo.skipped_count > 0) && (<Button variant="ghost" size="sm" className="h-7 text-xs gap-1.5" onClick={handleClearHistory}>
|
||||||
<Trash2 className="h-3 w-3"/>
|
<Trash2 className="h-3 w-3"/>
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|||||||
import { FolderOpen, Save, RotateCcw, Info } from "lucide-react";
|
import { FolderOpen, Save, RotateCcw, Info } from "lucide-react";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, applyFont, FONT_OPTIONS, FOLDER_PRESETS, FILENAME_PRESETS, TEMPLATE_VARIABLES, type Settings as SettingsType, type FontFamily, type FolderPreset, type FilenamePreset } from "@/lib/settings";
|
import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSettings, applyThemeMode, applyFont, FONT_OPTIONS, FOLDER_PRESETS, FILENAME_PRESETS, TEMPLATE_VARIABLES, type Settings as SettingsType, type FontFamily, type FolderPreset, type FilenamePreset } from "@/lib/settings";
|
||||||
import { themes, applyTheme } from "@/lib/themes";
|
import { themes, applyTheme } from "@/lib/themes";
|
||||||
import { SelectFolder } from "../../wailsjs/go/main/App";
|
import { SelectFolder } from "../../wailsjs/go/main/App";
|
||||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
|
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
|
||||||
const TidalIcon = () => (<svg viewBox="0 0 24 24" className="inline-block w-[1.1em] h-[1.1em] mr-2 fill-muted-foreground">
|
const TidalIcon = () => (<svg viewBox="0 0 24 24" className="inline-block w-[1.1em] h-[1.1em] mr-2 fill-muted-foreground">
|
||||||
<path d="M4.022 4.5 0 8.516l3.997 3.99 3.997-3.984L4.022 4.5Zm7.956 0L7.994 8.522l4.003 3.984L16 8.484 11.978 4.5Zm8.007 0L24 8.528l-4.003 3.978L16 8.484 19.985 4.5Z"></path>
|
<path d="M4.022 4.5 0 8.516l3.997 3.99 3.997-3.984L4.022 4.5Zm7.956 0L7.994 8.522l4.003 3.984L16 8.484 11.978 4.5Zm8.007 0L24 8.528l-4.003 3.978L16 8.484 19.985 4.5Z"></path>
|
||||||
<path d="m8.012 16.534 3.991 3.966L16 16.49l-4.003-3.984-3.985 4.028Z"></path>
|
<path d="m8.012 16.534 3.991 3.966L16 16.49l-4.003-3.984-3.985 4.028Z"></path>
|
||||||
@@ -33,6 +35,10 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
const [tempSettings, setTempSettings] = useState<SettingsType>(savedSettings);
|
const [tempSettings, setTempSettings] = useState<SettingsType>(savedSettings);
|
||||||
const [isDark, setIsDark] = useState(document.documentElement.classList.contains('dark'));
|
const [isDark, setIsDark] = useState(document.documentElement.classList.contains('dark'));
|
||||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||||
|
const [showFFmpegWarning, setShowFFmpegWarning] = useState(false);
|
||||||
|
const [isInstallingFFmpeg, setIsInstallingFFmpeg] = useState(false);
|
||||||
|
const [installProgress, setInstallProgress] = useState(0);
|
||||||
|
const downloadProgress = useDownloadProgress();
|
||||||
const hasUnsavedChanges = JSON.stringify(savedSettings) !== JSON.stringify(tempSettings);
|
const hasUnsavedChanges = JSON.stringify(savedSettings) !== JSON.stringify(tempSettings);
|
||||||
const resetToSaved = useCallback(() => {
|
const resetToSaved = useCallback(() => {
|
||||||
const freshSavedSettings = getSettings();
|
const freshSavedSettings = getSettings();
|
||||||
@@ -109,6 +115,51 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
toast.error(`Error selecting folder: ${error}`);
|
toast.error(`Error selecting folder: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => {
|
||||||
|
if (value === "HI_RES_LOSSLESS") {
|
||||||
|
try {
|
||||||
|
const { CheckFFmpegInstalled } = await import("../../wailsjs/go/main/App");
|
||||||
|
const isInstalled = await CheckFFmpegInstalled();
|
||||||
|
if (!isInstalled) {
|
||||||
|
setShowFFmpegWarning(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error checking FFmpeg:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTempSettings((prev) => ({ ...prev, tidalQuality: value }));
|
||||||
|
};
|
||||||
|
const handleInstallFFmpeg = async () => {
|
||||||
|
setIsInstallingFFmpeg(true);
|
||||||
|
setInstallProgress(0);
|
||||||
|
try {
|
||||||
|
const { DownloadFFmpeg } = await import("../../wailsjs/go/main/App");
|
||||||
|
const { EventsOn, EventsOff } = await import("../../wailsjs/runtime/runtime");
|
||||||
|
EventsOn("ffmpeg:progress", (progress: number) => {
|
||||||
|
setInstallProgress(progress);
|
||||||
|
});
|
||||||
|
const response = await DownloadFFmpeg();
|
||||||
|
EventsOff("ffmpeg:progress");
|
||||||
|
if (response.success) {
|
||||||
|
toast.success("FFmpeg installed successfully!");
|
||||||
|
setShowFFmpegWarning(false);
|
||||||
|
setTempSettings((prev) => ({ ...prev, tidalQuality: "HI_RES_LOSSLESS" }));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error(`Failed to install FFmpeg: ${response.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error installing FFmpeg:", error);
|
||||||
|
toast.error(`Error during FFmpeg installation: ${error}`);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsInstallingFFmpeg(false);
|
||||||
|
setInstallProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (<div className="space-y-6">
|
return (<div className="space-y-6">
|
||||||
<h1 className="text-2xl font-bold">Settings</h1>
|
<h1 className="text-2xl font-bold">Settings</h1>
|
||||||
|
|
||||||
@@ -208,7 +259,7 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{tempSettings.downloader === "tidal" && (<Select value={tempSettings.tidalQuality} onValueChange={(value: "LOSSLESS" | "HI_RES_LOSSLESS") => setTempSettings((prev) => ({ ...prev, tidalQuality: value }))}>
|
{tempSettings.downloader === "tidal" && (<Select value={tempSettings.tidalQuality} onValueChange={handleTidalQualityChange}>
|
||||||
<SelectTrigger className="h-9 w-fit">
|
<SelectTrigger className="h-9 w-fit">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -218,14 +269,13 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>)}
|
</Select>)}
|
||||||
|
|
||||||
{tempSettings.downloader === "qobuz" && (<Select value={tempSettings.qobuzQuality} onValueChange={(value: "6" | "7" | "27") => setTempSettings((prev) => ({ ...prev, qobuzQuality: value }))}>
|
{tempSettings.downloader === "qobuz" && (<Select value={tempSettings.qobuzQuality} onValueChange={(value: "6" | "7") => setTempSettings((prev) => ({ ...prev, qobuzQuality: value }))}>
|
||||||
<SelectTrigger className="h-9 w-fit">
|
<SelectTrigger className="h-9 w-fit">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="6">FLAC 16-bit (CD Quality)</SelectItem>
|
<SelectItem value="6">FLAC 16-bit (CD Quality)</SelectItem>
|
||||||
<SelectItem value="7">FLAC 24-bit</SelectItem>
|
<SelectItem value="7">FLAC 24-bit (Studio Quality)</SelectItem>
|
||||||
<SelectItem value="27">Hi-Res (24-bit/96kHz+)</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>)}
|
</Select>)}
|
||||||
|
|
||||||
@@ -234,7 +284,7 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="HI_RES">Hi-Res (24-bit/96kHz+)</SelectItem>
|
<SelectItem value="HI_RES">Hi-Res (24-bit/48kHz+)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>)}
|
</Select>)}
|
||||||
</div>
|
</div>
|
||||||
@@ -357,5 +407,37 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest }: Setting
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showFFmpegWarning} onOpenChange={(open) => !isInstallingFFmpeg && setShowFFmpegWarning(open)}>
|
||||||
|
<DialogContent className="max-w-md [&>button]:hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>FFmpeg Required</DialogTitle>
|
||||||
|
<DialogDescription className="space-y-4 pt-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Tidal 24-bit (Hi-Res Lossless) downloads audio in segmented files that need to be merged into a single FLAC file.</p>
|
||||||
|
<p>FFmpeg is required to merge these segments. {isInstallingFFmpeg ? "Installing FFmpeg..." : "Would you like to install FFmpeg now?"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isInstallingFFmpeg && (<div className="space-y-2 py-2">
|
||||||
|
<div className="flex justify-between text-xs font-medium">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>Downloading & Extracting...</span>
|
||||||
|
{downloadProgress.is_downloading && downloadProgress.mb_downloaded > 0 && (<span className="text-muted-foreground font-normal">
|
||||||
|
{downloadProgress.mb_downloaded.toFixed(2)} MB
|
||||||
|
{downloadProgress.speed_mbps > 0 && ` @ ${downloadProgress.speed_mbps.toFixed(2)} MB/s`}
|
||||||
|
</span>)}
|
||||||
|
</div>
|
||||||
|
<span>{installProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={installProgress} className="h-2"/>
|
||||||
|
</div>)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{!isInstallingFFmpeg && (<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setShowFFmpegWarning(false)}>Cancel</Button>
|
||||||
|
<Button onClick={handleInstallFFmpeg}>Install FFmpeg</Button>
|
||||||
|
</DialogFooter>)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface Settings {
|
|||||||
embedMaxQualityCover: boolean;
|
embedMaxQualityCover: boolean;
|
||||||
operatingSystem: "Windows" | "linux/MacOS";
|
operatingSystem: "Windows" | "linux/MacOS";
|
||||||
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
|
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
|
||||||
qobuzQuality: "6" | "7" | "27";
|
qobuzQuality: "6" | "7";
|
||||||
amazonQuality: "HI_RES";
|
amazonQuality: "HI_RES";
|
||||||
}
|
}
|
||||||
export const FOLDER_PRESETS: Record<FolderPreset, {
|
export const FOLDER_PRESETS: Record<FolderPreset, {
|
||||||
@@ -190,6 +190,9 @@ function getSettingsFromLocalStorage(): Settings {
|
|||||||
if (!('qobuzQuality' in parsed)) {
|
if (!('qobuzQuality' in parsed)) {
|
||||||
parsed.qobuzQuality = "6";
|
parsed.qobuzQuality = "6";
|
||||||
}
|
}
|
||||||
|
if (parsed.qobuzQuality === "27") {
|
||||||
|
parsed.qobuzQuality = "6";
|
||||||
|
}
|
||||||
if (!('amazonQuality' in parsed)) {
|
if (!('amazonQuality' in parsed)) {
|
||||||
parsed.amazonQuality = "HI_RES";
|
parsed.amazonQuality = "HI_RES";
|
||||||
}
|
}
|
||||||
@@ -257,6 +260,9 @@ export async function loadSettings(): Promise<Settings> {
|
|||||||
if (!('qobuzQuality' in parsed)) {
|
if (!('qobuzQuality' in parsed)) {
|
||||||
parsed.qobuzQuality = "6";
|
parsed.qobuzQuality = "6";
|
||||||
}
|
}
|
||||||
|
if (parsed.qobuzQuality === "27") {
|
||||||
|
parsed.qobuzQuality = "6";
|
||||||
|
}
|
||||||
if (!('amazonQuality' in parsed)) {
|
if (!('amazonQuality' in parsed)) {
|
||||||
parsed.amazonQuality = "HI_RES";
|
parsed.amazonQuality = "HI_RES";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import path from "path"
|
import path from "path";
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -11,4 +9,4 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user