From d90221b835481f09d9816cf61acb8c103cc3b3b4 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Sat, 22 Nov 2025 17:59:48 +0700 Subject: [PATCH] v5.7 --- README.md | 3 +- app.go | 9 ++ backend/deezer.go | 7 +- backend/progress.go | 93 +++++++++++++++++++ backend/qobuz.go | 9 +- backend/tidal.go | 24 ++++- frontend/src/App.tsx | 4 + .../src/components/DownloadProgressToast.tsx | 23 +++++ frontend/src/components/Header.tsx | 2 +- frontend/src/hooks/useDownloadProgress.ts | 42 +++++++++ wails.json | 10 +- 11 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 backend/progress.go create mode 100644 frontend/src/components/DownloadProgressToast.tsx create mode 100644 frontend/src/hooks/useDownloadProgress.ts diff --git a/README.md b/README.md index 55df81e..326bfef 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@
-Get Spotify tracks in true FLAC from Tidal/Deezer — no account required. +Get Spotify tracks in true FLAC from Tidal, Deezer & Qobuz — no account required. +

![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?style=for-the-badge&logo=windows&logoColor=white) diff --git a/app.go b/app.go index 65fb952..b13aca9 100644 --- a/app.go +++ b/app.go @@ -113,6 +113,10 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { req.FilenameFormat = "title-artist" } + // Set downloading state + backend.SetDownloading(true) + defer backend.SetDownloading(false) + if req.Service == "tidal" { searchQuery := req.Query if searchQuery == "" { @@ -179,3 +183,8 @@ func (a *App) GetDefaults() map[string]string { "downloadPath": backend.GetDefaultMusicPath(), } } + +// GetDownloadProgress returns current download progress +func (a *App) GetDownloadProgress() backend.ProgressInfo { + return backend.GetDownloadProgress() +} diff --git a/backend/deezer.go b/backend/deezer.go index 3daafcb..2195c10 100644 --- a/backend/deezer.go +++ b/backend/deezer.go @@ -124,11 +124,16 @@ func (d *DeezerDownloader) DownloadFile(url, filepath string) error { } defer out.Close() - _, err = io.Copy(out, resp.Body) + fmt.Println("Downloading...") + // Use progress writer to track download + pw := NewProgressWriter(out) + _, err = io.Copy(pw, resp.Body) if err != nil { return fmt.Errorf("failed to write file: %w", err) } + // Print final size + fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) return nil } diff --git a/backend/progress.go b/backend/progress.go new file mode 100644 index 0000000..7565f29 --- /dev/null +++ b/backend/progress.go @@ -0,0 +1,93 @@ +package backend + +import ( + "fmt" + "io" + "sync" +) + +// Global progress tracker +var ( + currentProgress float64 + currentProgressLock sync.RWMutex + isDownloading bool + downloadingLock sync.RWMutex +) + +// ProgressInfo represents download progress information +type ProgressInfo struct { + IsDownloading bool `json:"is_downloading"` + MBDownloaded float64 `json:"mb_downloaded"` +} + +// GetDownloadProgress returns current download progress +func GetDownloadProgress() ProgressInfo { + downloadingLock.RLock() + downloading := isDownloading + downloadingLock.RUnlock() + + currentProgressLock.RLock() + progress := currentProgress + currentProgressLock.RUnlock() + + return ProgressInfo{ + IsDownloading: downloading, + MBDownloaded: progress, + } +} + +// SetDownloadProgress updates the current download progress +func SetDownloadProgress(mbDownloaded float64) { + currentProgressLock.Lock() + currentProgress = mbDownloaded + currentProgressLock.Unlock() +} + +// SetDownloading sets the downloading state +func SetDownloading(downloading bool) { + downloadingLock.Lock() + isDownloading = downloading + downloadingLock.Unlock() + + if !downloading { + // Reset progress when download completes + SetDownloadProgress(0) + } +} + +// ProgressWriter wraps an io.Writer and reports download progress +type ProgressWriter struct { + writer io.Writer + total int64 + lastPrinted int64 +} + +func NewProgressWriter(writer io.Writer) *ProgressWriter { + return &ProgressWriter{ + writer: writer, + total: 0, + lastPrinted: 0, + } +} + +func (pw *ProgressWriter) Write(p []byte) (int, error) { + n, err := pw.writer.Write(p) + pw.total += int64(n) + + // Report progress every 256KB for smoother updates + if pw.total-pw.lastPrinted >= 256*1024 { + mbDownloaded := float64(pw.total) / (1024 * 1024) + fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded) + + // Update global progress + SetDownloadProgress(mbDownloaded) + + pw.lastPrinted = pw.total + } + + return n, err +} + +func (pw *ProgressWriter) GetTotal() int64 { + return pw.total +} diff --git a/backend/qobuz.go b/backend/qobuz.go index 1b39275..b2defda 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -184,13 +184,16 @@ func (q *QobuzDownloader) DownloadFile(url, filepath string) error { } defer out.Close() - fmt.Println("Writing file content...") - written, err := io.Copy(out, resp.Body) + fmt.Println("Downloading...") + // Use progress writer to track download + pw := NewProgressWriter(out) + _, err = io.Copy(pw, resp.Body) if err != nil { return fmt.Errorf("failed to write file: %w", err) } - fmt.Printf("✓ Downloaded %d bytes\n", written) + // Print final size + fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) return nil } diff --git a/backend/tidal.go b/backend/tidal.go index 2c85bf4..7e6f66b 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -83,7 +83,22 @@ func NewTidalDownloader(apiURL string) *TidalDownloader { func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { // Decode base64 API URL apiURL, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2Fma2FyeHl6L1Nwb3RpRkxBQy9yZWZzL2hlYWRzL21haW4vdGlkYWwuanNvbg==") - resp, err := http.Get(string(apiURL)) + + // Add cache-busting parameter with current timestamp + urlWithCacheBust := fmt.Sprintf("%s?t=%d", string(apiURL), time.Now().Unix()) + + // Create request with cache bypass headers + req, err := http.NewRequest("GET", urlWithCacheBust, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Add headers to bypass cache + req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + req.Header.Set("Pragma", "no-cache") + req.Header.Set("Expires", "0") + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch API list: %w", err) } @@ -304,11 +319,16 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error { } defer out.Close() - _, err = io.Copy(out, resp.Body) + // Use progress writer to track download + pw := NewProgressWriter(out) + _, err = io.Copy(pw, resp.Body) if err != nil { return fmt.Errorf("failed to write file: %w", err) } + // Print final size + fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024)) + fmt.Println("Download complete") return nil } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c854bb9..fca5c2e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import { TrackInfo } from "@/components/TrackInfo"; import { AlbumInfo } from "@/components/AlbumInfo"; import { PlaylistInfo } from "@/components/PlaylistInfo"; import { ArtistInfo } from "@/components/ArtistInfo"; +import { DownloadProgressToast } from "@/components/DownloadProgressToast"; // Hooks import { useDownload } from "@/hooks/useDownload"; @@ -261,6 +262,9 @@ function App() {
+ + {/* Download Progress Toast */} + {/* Timeout Dialog */} +
+
+ +

+ {progress.mb_downloaded.toFixed(2)} MB +

+
+
+
+ ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 15f1f03..e9f1771 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -49,7 +49,7 @@ export function Header({ version, hasUpdate }: HeaderProps) {

- Get Spotify tracks in true FLAC from Tidal/Deezer — no account required. + Get Spotify tracks in true FLAC from Tidal, Deezer & Qobuz — no account required.

diff --git a/frontend/src/hooks/useDownloadProgress.ts b/frontend/src/hooks/useDownloadProgress.ts new file mode 100644 index 0000000..4137b52 --- /dev/null +++ b/frontend/src/hooks/useDownloadProgress.ts @@ -0,0 +1,42 @@ +import { useState, useEffect, useRef } from "react"; +import { GetDownloadProgress } from "../../wailsjs/go/main/App"; + +export interface DownloadProgressInfo { + is_downloading: boolean; + mb_downloaded: number; +} + +export function useDownloadProgress() { + const [progress, setProgress] = useState({ + is_downloading: false, + mb_downloaded: 0, + }); + const intervalRef = useRef(null); + + useEffect(() => { + // Poll progress every 200ms for smooth updates + const pollProgress = async () => { + try { + const progressInfo = await GetDownloadProgress(); + setProgress(progressInfo); + } catch (error) { + console.error("Failed to get download progress:", error); + } + }; + + // Start polling + intervalRef.current = window.setInterval(pollProgress, 200); + + // Initial fetch + pollProgress(); + + // Cleanup + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + return progress; +} diff --git a/wails.json b/wails.json index 4b2ad3e..5866c9f 100644 --- a/wails.json +++ b/wails.json @@ -7,17 +7,13 @@ "frontend:dev:watcher": "pnpm run dev", "frontend:dev:serverUrl": "auto", "author": { - "name": "afkarxyz", - "email": "hi@afkarxyz.fun" + "name": "afkarxyz" }, "info": { - "companyName": "afkarxyz", "productName": "SpotiFLAC", - "productVersion": "5.7", - "copyright": "Copyright © 2025", - "comments": "Get Spotify tracks in true FLAC from Tidal/Deezer — no account required." + "productVersion": "5.7" }, "wailsjsdir": "./frontend", "assetdir": "./frontend/dist", "reloaddirs": "./frontend/src" -} +} \ No newline at end of file