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.
+

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 */}
+ );
+}
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