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;