This commit is contained in:
afkarxyz
2025-11-22 17:59:48 +07:00
parent 8a2dbe4e32
commit d90221b835
11 changed files with 211 additions and 15 deletions
+2 -1
View File
@@ -4,7 +4,8 @@
<div align="center"> <div align="center">
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.
<br><br> <br><br>
![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?style=for-the-badge&logo=windows&logoColor=white) ![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?style=for-the-badge&logo=windows&logoColor=white)
+9
View File
@@ -113,6 +113,10 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
req.FilenameFormat = "title-artist" req.FilenameFormat = "title-artist"
} }
// Set downloading state
backend.SetDownloading(true)
defer backend.SetDownloading(false)
if req.Service == "tidal" { if req.Service == "tidal" {
searchQuery := req.Query searchQuery := req.Query
if searchQuery == "" { if searchQuery == "" {
@@ -179,3 +183,8 @@ func (a *App) GetDefaults() map[string]string {
"downloadPath": backend.GetDefaultMusicPath(), "downloadPath": backend.GetDefaultMusicPath(),
} }
} }
// GetDownloadProgress returns current download progress
func (a *App) GetDownloadProgress() backend.ProgressInfo {
return backend.GetDownloadProgress()
}
+6 -1
View File
@@ -124,11 +124,16 @@ func (d *DeezerDownloader) DownloadFile(url, filepath string) error {
} }
defer out.Close() 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 { if err != nil {
return fmt.Errorf("failed to write file: %w", err) 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 return nil
} }
+93
View File
@@ -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
}
+6 -3
View File
@@ -184,13 +184,16 @@ func (q *QobuzDownloader) DownloadFile(url, filepath string) error {
} }
defer out.Close() defer out.Close()
fmt.Println("Writing file content...") fmt.Println("Downloading...")
written, err := io.Copy(out, resp.Body) // Use progress writer to track download
pw := NewProgressWriter(out)
_, err = io.Copy(pw, resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to write file: %w", err) 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 return nil
} }
+22 -2
View File
@@ -83,7 +83,22 @@ func NewTidalDownloader(apiURL string) *TidalDownloader {
func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) {
// Decode base64 API URL // Decode base64 API URL
apiURL, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2Fma2FyeHl6L1Nwb3RpRkxBQy9yZWZzL2hlYWRzL21haW4vdGlkYWwuanNvbg==") 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 { if err != nil {
return nil, fmt.Errorf("failed to fetch API list: %w", err) 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() 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 { if err != nil {
return fmt.Errorf("failed to write file: %w", err) 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") fmt.Println("Download complete")
return nil return nil
} }
+4
View File
@@ -24,6 +24,7 @@ import { TrackInfo } from "@/components/TrackInfo";
import { AlbumInfo } from "@/components/AlbumInfo"; import { AlbumInfo } from "@/components/AlbumInfo";
import { PlaylistInfo } from "@/components/PlaylistInfo"; import { PlaylistInfo } from "@/components/PlaylistInfo";
import { ArtistInfo } from "@/components/ArtistInfo"; import { ArtistInfo } from "@/components/ArtistInfo";
import { DownloadProgressToast } from "@/components/DownloadProgressToast";
// Hooks // Hooks
import { useDownload } from "@/hooks/useDownload"; import { useDownload } from "@/hooks/useDownload";
@@ -262,6 +263,9 @@ function App() {
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
<Header version={CURRENT_VERSION} hasUpdate={hasUpdate} /> <Header version={CURRENT_VERSION} hasUpdate={hasUpdate} />
{/* Download Progress Toast */}
<DownloadProgressToast />
{/* Timeout Dialog */} {/* Timeout Dialog */}
<Dialog <Dialog
open={metadata.showTimeoutDialog} open={metadata.showTimeoutDialog}
@@ -0,0 +1,23 @@
import { useDownloadProgress } from "@/hooks/useDownloadProgress";
import { Download } from "lucide-react";
export function DownloadProgressToast() {
const progress = useDownloadProgress();
if (!progress.is_downloading) {
return null;
}
return (
<div className="fixed top-4 left-4 z-50 animate-in slide-in-from-left-5 data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left-5">
<div className="bg-background border rounded-lg shadow-lg p-3">
<div className="flex items-center gap-2">
<Download className="h-4 w-4 text-primary animate-bounce" />
<p className="text-sm font-medium">
{progress.mb_downloaded.toFixed(2)} MB
</p>
</div>
</div>
</div>
);
}
+1 -1
View File
@@ -49,7 +49,7 @@ export function Header({ version, hasUpdate }: HeaderProps) {
</div> </div>
</div> </div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
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.
</p> </p>
</div> </div>
<div className="absolute right-0 top-0 flex gap-2"> <div className="absolute right-0 top-0 flex gap-2">
+42
View File
@@ -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<DownloadProgressInfo>({
is_downloading: false,
mb_downloaded: 0,
});
const intervalRef = useRef<number | null>(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;
}
+2 -6
View File
@@ -7,15 +7,11 @@
"frontend:dev:watcher": "pnpm run dev", "frontend:dev:watcher": "pnpm run dev",
"frontend:dev:serverUrl": "auto", "frontend:dev:serverUrl": "auto",
"author": { "author": {
"name": "afkarxyz", "name": "afkarxyz"
"email": "hi@afkarxyz.fun"
}, },
"info": { "info": {
"companyName": "afkarxyz",
"productName": "SpotiFLAC", "productName": "SpotiFLAC",
"productVersion": "5.7", "productVersion": "5.7"
"copyright": "Copyright © 2025",
"comments": "Get Spotify tracks in true FLAC from Tidal/Deezer — no account required."
}, },
"wailsjsdir": "./frontend", "wailsjsdir": "./frontend",
"assetdir": "./frontend/dist", "assetdir": "./frontend/dist",