v5.7
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user