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">
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>
![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"
}
// 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()
}
+6 -1
View File
@@ -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
}
+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()
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
}
+22 -2
View File
@@ -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
}
+4
View File
@@ -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";
@@ -262,6 +263,9 @@ function App() {
<div className="max-w-4xl mx-auto space-y-6">
<Header version={CURRENT_VERSION} hasUpdate={hasUpdate} />
{/* Download Progress Toast */}
<DownloadProgressToast />
{/* Timeout Dialog */}
<Dialog
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>
<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>
</div>
<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: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",