diff --git a/app.go b/app.go index d0fd541..65fb952 100644 --- a/app.go +++ b/app.go @@ -37,6 +37,9 @@ type DownloadRequest struct { ISRC string `json:"isrc"` Service string `json:"service"` Query string `json:"query,omitempty"` + TrackName string `json:"track_name,omitempty"` + ArtistName string `json:"artist_name,omitempty"` + AlbumName string `json:"album_name,omitempty"` ApiURL string `json:"api_url,omitempty"` OutputDir string `json:"output_dir,omitempty"` AudioFormat string `json:"audio_format,omitempty"` @@ -118,14 +121,20 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { if req.ApiURL == "" || req.ApiURL == "auto" { downloader := backend.NewTidalDownloader("") - filename, err = downloader.DownloadWithFallback(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber) + filename, err = downloader.DownloadWithFallback(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) } else { downloader := backend.NewTidalDownloader(req.ApiURL) - filename, err = downloader.Download(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber) + filename, err = downloader.Download(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + } + } else if req.Service == "qobuz" { + downloader := backend.NewQobuzDownloader() + err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + if err == nil { + filename = "Downloaded via Qobuz" } } else { downloader := backend.NewDeezerDownloader() - err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.FilenameFormat, req.TrackNumber) + err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) if err == nil { filename = "Downloaded via Deezer" } diff --git a/backend/deezer.go b/backend/deezer.go index 2b9eb55..3daafcb 100644 --- a/backend/deezer.go +++ b/backend/deezer.go @@ -1,6 +1,7 @@ package backend import ( + "encoding/base64" "encoding/json" "fmt" "io" @@ -57,7 +58,9 @@ func NewDeezerDownloader() *DeezerDownloader { } func (d *DeezerDownloader) GetTrackByISRC(isrc string) (*DeezerTrack, error) { - url := fmt.Sprintf("https://api.deezer.com/2.0/track/isrc:%s", isrc) + // Decode base64 API URL + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlemVyLmNvbS8yLjAvdHJhY2svaXNyYzo=") + url := fmt.Sprintf("%s%s", string(apiBase), isrc) resp, err := d.client.Get(url) if err != nil { @@ -82,7 +85,9 @@ func (d *DeezerDownloader) GetTrackByISRC(isrc string) (*DeezerTrack, error) { } func (d *DeezerDownloader) GetDownloadURL(trackID int64) (string, error) { - url := fmt.Sprintf("https://api.deezmate.com/dl/%d", trackID) + // Decode base64 API URL + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlem1hdGUuY29tL2RsLw==") + url := fmt.Sprintf("%s%d", string(apiBase), trackID) resp, err := d.client.Get(url) if err != nil { @@ -183,7 +188,7 @@ func buildFilename(title, artist string, trackNumber int, format string, include return filename + ".flac" } -func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string, includeTrackNumber bool) error { +func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { fmt.Printf("Fetching track info for ISRC: %s\n", isrc) track, err := d.GetTrackByISRC(isrc) @@ -191,21 +196,36 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string return err } - artists := track.Artist.Name - if len(track.Contributors) > 0 { - var mainArtists []string - for _, contrib := range track.Contributors { - if contrib.Role == "Main" { - mainArtists = append(mainArtists, contrib.Name) + // Use Spotify metadata if provided, otherwise fallback to Deezer metadata + artists := spotifyArtistName + trackTitle := spotifyTrackName + albumTitle := spotifyAlbumName + + if artists == "" { + artists = track.Artist.Name + if len(track.Contributors) > 0 { + var mainArtists []string + for _, contrib := range track.Contributors { + if contrib.Role == "Main" { + mainArtists = append(mainArtists, contrib.Name) + } + } + if len(mainArtists) > 0 { + artists = strings.Join(mainArtists, ", ") } - } - if len(mainArtists) > 0 { - artists = strings.Join(mainArtists, ", ") } } - fmt.Printf("Found track: %s - %s\n", artists, track.Title) - fmt.Printf("Album: %s\n", track.Album.Title) + if trackTitle == "" { + trackTitle = track.Title + } + + if albumTitle == "" { + albumTitle = track.Album.Title + } + + fmt.Printf("Found track: %s - %s\n", artists, trackTitle) + fmt.Printf("Album: %s\n", albumTitle) downloadURL, err := d.GetDownloadURL(track.ID) if err != nil { @@ -213,7 +233,7 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string } safeArtist := sanitizeFilename(artists) - safeTitle := sanitizeFilename(track.Title) + safeTitle := sanitizeFilename(trackTitle) // Build filename based on format settings filename := buildFilename(safeTitle, safeArtist, track.TrackPos, filenameFormat, includeTrackNumber) @@ -239,9 +259,9 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string fmt.Println("Embedding metadata and cover art...") metadata := Metadata{ - Title: track.Title, + Title: trackTitle, Artist: artists, - Album: track.Album.Title, + Album: albumTitle, Date: track.ReleaseDate, TrackNumber: track.TrackPos, DiscNumber: track.DiskNumber, diff --git a/backend/qobuz.go b/backend/qobuz.go new file mode 100644 index 0000000..1b39275 --- /dev/null +++ b/backend/qobuz.go @@ -0,0 +1,355 @@ +package backend + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +type QobuzDownloader struct { + client *http.Client + appID string +} + +type QobuzSearchResponse struct { + Query string `json:"query"` + Tracks struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + Total int `json:"total"` + Items []QobuzTrack `json:"items"` + } `json:"tracks"` +} + +type QobuzTrack struct { + ID int64 `json:"id"` + Title string `json:"title"` + Version string `json:"version"` + Duration int `json:"duration"` + TrackNumber int `json:"track_number"` + MediaNumber int `json:"media_number"` + ISRC string `json:"isrc"` + Copyright string `json:"copyright"` + MaximumBitDepth int `json:"maximum_bit_depth"` + MaximumSamplingRate float64 `json:"maximum_sampling_rate"` + Hires bool `json:"hires"` + HiresStreamable bool `json:"hires_streamable"` + ReleaseDateOriginal string `json:"release_date_original"` + Performer struct { + Name string `json:"name"` + ID int64 `json:"id"` + } `json:"performer"` + Album struct { + Title string `json:"title"` + ID string `json:"id"` + Image struct { + Small string `json:"small"` + Thumbnail string `json:"thumbnail"` + Large string `json:"large"` + } `json:"image"` + Artist struct { + Name string `json:"name"` + ID int64 `json:"id"` + } `json:"artist"` + Label struct { + Name string `json:"name"` + } `json:"label"` + } `json:"album"` +} + +type QobuzStreamResponse struct { + URL string `json:"url"` +} + +func NewQobuzDownloader() *QobuzDownloader { + return &QobuzDownloader{ + client: &http.Client{ + Timeout: 60 * time.Second, + }, + appID: "798273057", + } +} + +func (q *QobuzDownloader) SearchByISRC(isrc string) (*QobuzTrack, error) { + // Decode base64 API URL + apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly93d3cucW9idXouY29tL2FwaS5qc29uLzAuMi90cmFjay9zZWFyY2g/cXVlcnk9") + url := fmt.Sprintf("%s%s&limit=1&app_id=%s", string(apiBase), isrc, q.appID) + + resp, err := q.client.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to search track: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("API returned status %d", resp.StatusCode) + } + + var searchResp QobuzSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if len(searchResp.Tracks.Items) == 0 { + return nil, fmt.Errorf("track not found for ISRC: %s", isrc) + } + + return &searchResp.Tracks.Items[0], nil +} + +func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string, error) { + // Map quality to Qobuz quality code + // Qobuz uses: 5 (MP3 320), 6 (FLAC 16-bit), 7 (FLAC 24-bit), 27 (Hi-Res) + qualityCode := "27" // Default to Hi-Res + + fmt.Printf("Getting download URL for track ID: %d\n", trackID) + + // Decode base64 API URLs + primaryBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWIueWVldC5zdS9hcGkvc3RyZWFtP3RyYWNrSWQ9") + + // Try primary API first + primaryURL := fmt.Sprintf("%s%d&quality=%s", string(primaryBase), trackID, qualityCode) + + resp, err := q.client.Get(primaryURL) + if err == nil && resp.StatusCode == 200 { + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Primary API response: %s\n", string(body)) + + var streamResp QobuzStreamResponse + if err := json.Unmarshal(body, &streamResp); err == nil && streamResp.URL != "" { + fmt.Printf("Got download URL from primary API\n") + return streamResp.URL, nil + } + } + if resp != nil { + resp.Body.Close() + } + + // Fallback to secondary API + fmt.Println("Primary API failed, trying fallback...") + fallbackBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9kYWJtdXNpYy54eXovYXBpL3N0cmVhbT90cmFja0lkPQ==") + fallbackURL := fmt.Sprintf("%s%d&quality=%s", string(fallbackBase), trackID, qualityCode) + + resp, err = q.client.Get(fallbackURL) + if err != nil { + return "", fmt.Errorf("failed to get download URL: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Fallback API error response: %s\n", string(body)) + return "", fmt.Errorf("API returned status %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Fallback API response: %s\n", string(body)) + + var streamResp QobuzStreamResponse + if err := json.Unmarshal(body, &streamResp); err != nil { + return "", fmt.Errorf("failed to decode response: %w", err) + } + + if streamResp.URL == "" { + return "", fmt.Errorf("no download URL available") + } + + fmt.Printf("Got download URL from fallback API\n") + return streamResp.URL, nil +} + +func (q *QobuzDownloader) DownloadFile(url, filepath string) error { + fmt.Println("Starting file download...") + resp, err := q.client.Get(url) + if err != nil { + return fmt.Errorf("failed to download file: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("download failed with status %d", resp.StatusCode) + } + + fmt.Printf("Creating file: %s\n", filepath) + out, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer out.Close() + + fmt.Println("Writing file content...") + written, err := io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + fmt.Printf("✓ Downloaded %d bytes\n", written) + return nil +} + +func (q *QobuzDownloader) DownloadCoverArt(coverURL, filepath string) error { + if coverURL == "" { + return fmt.Errorf("no cover URL provided") + } + + resp, err := q.client.Get(coverURL) + if err != nil { + return fmt.Errorf("failed to download cover: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("cover download failed with status %d", resp.StatusCode) + } + + out, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("failed to create cover file: %w", err) + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +func buildQobuzFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool) string { + var filename string + + // Build base filename based on format + switch format { + case "artist-title": + filename = fmt.Sprintf("%s - %s", artist, title) + case "title": + filename = title + default: // "title-artist" + filename = fmt.Sprintf("%s - %s", title, artist) + } + + // Add track number prefix if enabled + if includeTrackNumber && trackNumber > 0 { + filename = fmt.Sprintf("%02d. %s", trackNumber, filename) + } + + return filename + ".flac" +} + +func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { + fmt.Printf("Fetching track info for ISRC: %s\n", isrc) + + // Create output directory if it doesn't exist + if outputDir != "." { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + } + + track, err := q.SearchByISRC(isrc) + if err != nil { + return err + } + + // Use Spotify metadata if provided, otherwise fallback to Qobuz metadata + artists := spotifyArtistName + trackTitle := spotifyTrackName + albumTitle := spotifyAlbumName + + if artists == "" { + artists = track.Performer.Name + if track.Album.Artist.Name != "" { + artists = track.Album.Artist.Name + } + } + + if trackTitle == "" { + trackTitle = track.Title + if track.Version != "" && track.Version != "null" { + trackTitle = fmt.Sprintf("%s (%s)", track.Title, track.Version) + } + } + + if albumTitle == "" { + albumTitle = track.Album.Title + } + + fmt.Printf("Found track: %s - %s\n", artists, trackTitle) + fmt.Printf("Album: %s\n", albumTitle) + + qualityInfo := "Standard" + if track.Hires { + qualityInfo = fmt.Sprintf("Hi-Res (%d-bit / %.1f kHz)", track.MaximumBitDepth, track.MaximumSamplingRate) + } + fmt.Printf("Quality: %s\n", qualityInfo) + + fmt.Println("Getting download URL...") + downloadURL, err := q.GetDownloadURL(track.ID, quality) + if err != nil { + return fmt.Errorf("failed to get download URL: %w", err) + } + + if downloadURL == "" { + return fmt.Errorf("received empty download URL") + } + + // Show partial URL for security + urlPreview := downloadURL + if len(downloadURL) > 60 { + urlPreview = downloadURL[:60] + "..." + } + fmt.Printf("Download URL obtained: %s\n", urlPreview) + + safeArtist := sanitizeFilename(artists) + safeTitle := sanitizeFilename(trackTitle) + + // Build filename based on format settings + filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber) + filepath := filepath.Join(outputDir, filename) + + fmt.Printf("Downloading FLAC file to: %s\n", filepath) + if err := q.DownloadFile(downloadURL, filepath); err != nil { + return fmt.Errorf("failed to download file: %w", err) + } + + fmt.Printf("Downloaded: %s\n", filepath) + + coverPath := "" + if track.Album.Image.Large != "" { + coverPath = filepath + ".cover.jpg" + fmt.Println("Downloading cover art...") + if err := q.DownloadCoverArt(track.Album.Image.Large, coverPath); err != nil { + fmt.Printf("Warning: Failed to download cover art: %v\n", err) + } else { + defer os.Remove(coverPath) + } + } + + fmt.Println("Embedding metadata and cover art...") + + releaseYear := "" + if len(track.ReleaseDateOriginal) >= 4 { + releaseYear = track.ReleaseDateOriginal[:4] + } + + metadata := Metadata{ + Title: trackTitle, + Artist: artists, + Album: albumTitle, + Date: releaseYear, + TrackNumber: track.TrackNumber, + DiscNumber: track.MediaNumber, + ISRC: track.ISRC, + } + + if err := EmbedMetadata(filepath, metadata, coverPath); err != nil { + return fmt.Errorf("failed to embed metadata: %w", err) + } + + fmt.Println("Metadata embedded successfully!") + return nil +} diff --git a/backend/tidal.go b/backend/tidal.go index 39ebe05..2c85bf4 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -81,7 +81,9 @@ func NewTidalDownloader(apiURL string) *TidalDownloader { } func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { - resp, err := http.Get("https://raw.githubusercontent.com/afkarxyz/SpotiFLAC/refs/heads/main/tidal.json") + // Decode base64 API URL + apiURL, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2Fma2FyeHl6L1Nwb3RpRkxBQy9yZWZzL2hlYWRzL21haW4vdGlkYWwuanNvbg==") + resp, err := http.Get(string(apiURL)) if err != nil { return nil, fmt.Errorf("failed to fetch API list: %w", err) } @@ -107,7 +109,9 @@ func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { func (t *TidalDownloader) GetAccessToken() (string, error) { data := fmt.Sprintf("client_id=%s&grant_type=client_credentials", t.clientID) - req, err := http.NewRequest("POST", "https://auth.tidal.com/v1/oauth2/token", strings.NewReader(data)) + // Decode base64 API URL + authURL, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hdXRoLnRpZGFsLmNvbS92MS9vYXV0aDIvdG9rZW4=") + req, err := http.NewRequest("POST", string(authURL), strings.NewReader(data)) if err != nil { return "", err } @@ -142,8 +146,9 @@ func (t *TidalDownloader) SearchTracks(query string) (*TidalSearchResponse, erro return nil, fmt.Errorf("failed to get access token: %w", err) } - // URL encode the query parameter - searchURL := fmt.Sprintf("https://api.tidal.com/v1/search/tracks?query=%s&limit=25&offset=0&countryCode=US", url.QueryEscape(query)) + // Decode base64 API URL and encode the query parameter + searchBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkudGlkYWwuY29tL3YxL3NlYXJjaC90cmFja3M/cXVlcnk9") + searchURL := fmt.Sprintf("%s%s&limit=25&offset=0&countryCode=US", string(searchBase), url.QueryEscape(query)) req, err := http.NewRequest("GET", searchURL, nil) if err != nil { @@ -265,7 +270,9 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string, func (t *TidalDownloader) DownloadAlbumArt(albumID string) ([]byte, error) { albumID = strings.ReplaceAll(albumID, "-", "/") - artURL := fmt.Sprintf("https://resources.tidal.com/images/%s/1280x1280.jpg", albumID) + // Decode base64 API URL + imageBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9yZXNvdXJjZXMudGlkYWwuY29tL2ltYWdlcy8=") + artURL := fmt.Sprintf("%s%s/1280x1280.jpg", string(imageBase), albumID) resp, err := t.client.Get(artURL) if err != nil { @@ -306,7 +313,7 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error { return nil } -func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool) (string, error) { +func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { return "", fmt.Errorf("directory error: %w", err) @@ -322,26 +329,40 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm return "", fmt.Errorf("no track ID found") } - var artists []string - if len(trackInfo.Artists) > 0 { - for _, artist := range trackInfo.Artists { - if artist.Name != "" { - artists = append(artists, artist.Name) - } - } - } else if trackInfo.Artist.Name != "" { - artists = append(artists, trackInfo.Artist.Name) - } + // Use Spotify metadata if provided, otherwise fallback to Tidal metadata + artistName := spotifyArtistName + trackTitle := spotifyTrackName + albumTitle := spotifyAlbumName - artistName := "Unknown Artist" - if len(artists) > 0 { - artistName = strings.Join(artists, ", ") + if artistName == "" { + var artists []string + if len(trackInfo.Artists) > 0 { + for _, artist := range trackInfo.Artists { + if artist.Name != "" { + artists = append(artists, artist.Name) + } + } + } else if trackInfo.Artist.Name != "" { + artists = append(artists, trackInfo.Artist.Name) + } + + artistName = "Unknown Artist" + if len(artists) > 0 { + artistName = strings.Join(artists, ", ") + } } artistName = sanitizeFilename(artistName) - trackTitle := sanitizeFilename(trackInfo.Title) if trackTitle == "" { - trackTitle = fmt.Sprintf("track_%d", trackInfo.ID) + trackTitle = trackInfo.Title + if trackTitle == "" { + trackTitle = fmt.Sprintf("track_%d", trackInfo.ID) + } + } + trackTitle = sanitizeFilename(trackTitle) + + if albumTitle == "" { + albumTitle = trackInfo.Album.Title } // Build filename based on format settings @@ -387,9 +408,9 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm } metadata := Metadata{ - Title: trackInfo.Title, + Title: trackTitle, Artist: artistName, - Album: trackInfo.Album.Title, + Album: albumTitle, Date: releaseYear, TrackNumber: trackInfo.TrackNumber, DiscNumber: trackInfo.VolumeNumber, @@ -406,7 +427,7 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm return outputFilename, nil } -func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool) (string, error) { +func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { apis, err := t.GetAvailableAPIs() if err != nil { return "", fmt.Errorf("no APIs available for fallback: %w", err) @@ -418,7 +439,7 @@ func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, fallbackDownloader := NewTidalDownloader(apiURL) - result, err := fallbackDownloader.Download(query, isrc, outputDir, quality, filenameFormat, includeTrackNumber) + result, err := fallbackDownloader.Download(query, isrc, outputDir, quality, filenameFormat, includeTrackNumber, spotifyTrackName, spotifyArtistName, spotifyAlbumName) if err == nil { fmt.Printf("✓ Success with: %s\n", apiURL) return result, nil diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index ab4edd5..f658ffa 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -24,6 +24,27 @@ import { getSettings, getSettingsWithDefaults, saveSettings, resetToDefaultSetti import { themes, applyTheme } from "@/lib/themes"; import { SelectFolder } from "../../wailsjs/go/main/App"; +// Service Icons +const TidalIcon = () => ( + + + + +); + +const DeezerIcon = () => ( + + + +); + +const QobuzIcon = () => ( + + + + +); + export function Settings() { const [open, setOpen] = useState(false); const [savedSettings, setSavedSettings] = useState(getSettings()); @@ -134,7 +155,7 @@ export function Settings() { setTempSettings((prev) => ({ ...prev, downloadPath: value })); }; - const handleDownloaderChange = (value: "auto" | "deezer" | "tidal") => { + const handleDownloaderChange = (value: "auto" | "deezer" | "tidal" | "qobuz") => { setTempSettings((prev) => ({ ...prev, downloader: value })); }; @@ -203,9 +224,32 @@ export function Settings() { - Auto (Tidal → Deezer) - Tidal - Deezer + + + + + + Auto (Tidal → Deezer → Qobuz) + + + + + + Tidal + + + + + + Deezer + + + + + + Qobuz + + diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts index 1f27b50..dfade9f 100644 --- a/frontend/src/hooks/useDownload.ts +++ b/frontend/src/hooks/useDownload.ts @@ -52,11 +52,15 @@ export function useDownload() { } if (service === "auto") { + // Try Tidal first try { const tidalResponse = await downloadTrack({ isrc, service: "tidal", query, + track_name: trackName, + artist_name: artistName, + album_name: albumName, output_dir: outputDir, filename_format: settings.filenameFormat, track_number: settings.trackNumber, @@ -65,17 +69,42 @@ export function useDownload() { if (tidalResponse.success) { return tidalResponse; } - - service = "deezer"; } catch (tidalErr) { - service = "deezer"; + // Tidal failed, continue to Deezer } + + // Try Deezer second + try { + const deezerResponse = await downloadTrack({ + isrc, + service: "deezer", + query, + track_name: trackName, + artist_name: artistName, + album_name: albumName, + output_dir: outputDir, + filename_format: settings.filenameFormat, + track_number: settings.trackNumber, + }); + + if (deezerResponse.success) { + return deezerResponse; + } + } catch (deezerErr) { + // Deezer failed, continue to Qobuz + } + + // Try Qobuz as last fallback + service = "qobuz"; } return await downloadTrack({ isrc, - service: service as "deezer" | "tidal", + service: service as "deezer" | "tidal" | "qobuz", query, + track_name: trackName, + artist_name: artistName, + album_name: albumName, output_dir: outputDir, filename_format: settings.filenameFormat, track_number: settings.trackNumber, diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index c0d2db7..5b89e99 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -2,7 +2,7 @@ import { GetDefaults } from "../../wailsjs/go/main/App"; export interface Settings { downloadPath: string; - downloader: "auto" | "deezer" | "tidal"; + downloader: "auto" | "deezer" | "tidal" | "qobuz"; theme: string; themeMode: "auto" | "light" | "dark"; filenameFormat: "title-artist" | "artist-title" | "title"; diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index d1ab947..a6c6717 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -97,8 +97,11 @@ export type SpotifyMetadataResponse = export interface DownloadRequest { isrc: string; - service: "deezer" | "tidal"; + service: "deezer" | "tidal" | "qobuz"; query?: string; + track_name?: string; + artist_name?: string; + album_name?: string; api_url?: string; output_dir?: string; audio_format?: string;