-Get Spotify tracks in true FLAC from Tidal, Deezer, Qobuz & Amazon Music — no account required.
+Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.


diff --git a/app.go b/app.go
index 67a6674..aaec347 100644
--- a/app.go
+++ b/app.go
@@ -37,25 +37,27 @@ type SpotifyMetadataRequest struct {
// DownloadRequest represents the request structure for downloading tracks
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"`
- FilenameFormat string `json:"filename_format,omitempty"`
- TrackNumber bool `json:"track_number,omitempty"`
- Position int `json:"position,omitempty"` // Position in playlist/album (1-based)
- UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"` // Use album track number instead of playlist position
- SpotifyID string `json:"spotify_id,omitempty"` // Spotify track ID
- EmbedLyrics bool `json:"embed_lyrics,omitempty"` // Whether to embed lyrics into the audio file
+ 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"`
+ AlbumArtist string `json:"album_artist,omitempty"`
+ ReleaseDate string `json:"release_date,omitempty"`
+ ApiURL string `json:"api_url,omitempty"`
+ OutputDir string `json:"output_dir,omitempty"`
+ AudioFormat string `json:"audio_format,omitempty"`
+ FilenameFormat string `json:"filename_format,omitempty"`
+ TrackNumber bool `json:"track_number,omitempty"`
+ Position int `json:"position,omitempty"` // Position in playlist/album (1-based)
+ UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"` // Use album track number instead of playlist position
+ SpotifyID string `json:"spotify_id,omitempty"` // Spotify track ID
+ EmbedLyrics bool `json:"embed_lyrics,omitempty"` // Whether to embed lyrics into the audio file
EmbedMaxQualityCover bool `json:"embed_max_quality_cover,omitempty"` // Whether to embed max quality cover art
- ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call
- Duration int `json:"duration,omitempty"` // Track duration in seconds for better matching
- ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
+ ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call
+ Duration int `json:"duration,omitempty"` // Track duration in seconds for better matching
+ ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
}
// DownloadResponse represents the response structure for download operations
@@ -128,7 +130,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}
if req.Service == "" {
- req.Service = "deezer"
+ req.Service = "tidal"
}
if req.OutputDir == "" {
@@ -225,7 +227,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
downloader := backend.NewTidalDownloader("")
if req.ServiceURL != "" {
// Use provided URL directly with fallback to multiple APIs
- filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
+ filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber)
} else {
if req.SpotifyID == "" {
return DownloadResponse{
@@ -234,13 +236,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal")
}
// Use ISRC matching for search fallback
- filename, err = downloader.DownloadWithFallbackAndISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber, req.Duration)
+ filename, err = downloader.DownloadWithFallbackAndISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.Duration)
}
} else {
downloader := backend.NewTidalDownloader(req.ApiURL)
if req.ServiceURL != "" {
// Use provided URL directly with specific API
- filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
+ filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber)
} else {
if req.SpotifyID == "" {
return DownloadResponse{
@@ -249,7 +251,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal")
}
// Use ISRC matching for search fallback
- filename, err = downloader.DownloadWithISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber, req.Duration)
+ filename, err = downloader.DownloadWithISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.Duration)
}
}
@@ -260,22 +262,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
if quality == "" {
quality = "6"
}
- filename, err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
+ filename, err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber)
- default: // deezer
- downloader := backend.NewDeezerDownloader()
- if req.ServiceURL != "" {
- // Use provided URL directly
- filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
- } else {
- if req.SpotifyID == "" {
- return DownloadResponse{
- Success: false,
- Error: "Spotify ID is required for Deezer",
- }, fmt.Errorf("spotify ID is required for Deezer")
- }
- filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
- }
+ default:
+ return DownloadResponse{
+ Success: false,
+ Error: fmt.Sprintf("Unknown service: %s", req.Service),
+ }, fmt.Errorf("unknown service: %s", req.Service)
}
if err != nil {
@@ -314,9 +307,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
fmt.Printf("Track: %s\n", trackName)
fmt.Printf("Artist: %s\n", artistName)
fmt.Println("Searching all sources...")
-
+
lyricsClient := backend.NewLyricsClient()
-
+
// Try all sources with fallbacks
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName)
if err != nil {
@@ -324,29 +317,29 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
return
}
-
+
if lyricsResp == nil || len(lyricsResp.Lines) == 0 {
fmt.Println("No lyrics content found")
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
return
}
-
+
fmt.Printf("Lyrics found from: %s\n", source)
fmt.Printf("Sync type: %s\n", lyricsResp.SyncType)
fmt.Printf("Total lines: %d\n", len(lyricsResp.Lines))
-
+
lyrics := lyricsClient.ConvertToLRC(lyricsResp, trackName, artistName)
if lyrics == "" {
fmt.Println("No lyrics content to embed")
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
return
}
-
+
// Show full lyrics in console for debugging
fmt.Printf("\n--- Full LRC Content ---\n")
fmt.Println(lyrics)
fmt.Printf("--- End LRC Content ---\n\n")
-
+
fmt.Printf("Embedding into: %s\n", filePath)
if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil {
fmt.Printf("Failed to embed lyrics: %v\n", err)
diff --git a/backend/deezer.go b/backend/deezer.go
deleted file mode 100644
index 2211ba0..0000000
--- a/backend/deezer.go
+++ /dev/null
@@ -1,451 +0,0 @@
-package backend
-
-import (
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "time"
-)
-
-type DeezerDownloader struct {
- client *http.Client
-}
-
-type DeezerTrack struct {
- ID int64 `json:"id"`
- Title string `json:"title"`
- TitleShort string `json:"title_short"`
- Duration int `json:"duration"`
- TrackPos int `json:"track_position"`
- DiskNumber int `json:"disk_number"`
- ISRC string `json:"isrc"`
- ReleaseDate string `json:"release_date"`
- Artist struct {
- Name string `json:"name"`
- ID int64 `json:"id"`
- } `json:"artist"`
- Album struct {
- Title string `json:"title"`
- ID int64 `json:"id"`
- CoverXL string `json:"cover_xl"`
- CoverBig string `json:"cover_big"`
- } `json:"album"`
- Contributors []struct {
- Name string `json:"name"`
- Role string `json:"role"`
- } `json:"contributors"`
-}
-
-type DeezMateResponse struct {
- Success bool `json:"success"`
- Links struct {
- FLAC string `json:"flac"`
- } `json:"links"`
-}
-
-func NewDeezerDownloader() *DeezerDownloader {
- return &DeezerDownloader{
- client: &http.Client{
- Timeout: 60 * time.Second,
- },
- }
-}
-
-func (d *DeezerDownloader) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
- // Decode base64 API URL
- spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==")
- spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID)
-
- apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==")
- apiURL := fmt.Sprintf("%s%s", string(apiBase), spotifyURL)
-
- req, err := http.NewRequest("GET", apiURL, nil)
- if err != nil {
- return "", fmt.Errorf("failed to create request: %w", err)
- }
-
- fmt.Println("Getting Deezer URL...")
-
- resp, err := d.client.Do(req)
- if err != nil {
- return "", fmt.Errorf("failed to get Deezer URL: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return "", fmt.Errorf("API returned status %d", resp.StatusCode)
- }
-
- // Read body first to handle encoding issues
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", fmt.Errorf("failed to read response body: %w", err)
- }
-
- if len(body) == 0 {
- return "", fmt.Errorf("API returned empty response")
- }
-
- var songLinkResp struct {
- LinksByPlatform map[string]struct {
- URL string `json:"url"`
- } `json:"linksByPlatform"`
- }
- if err := json.Unmarshal(body, &songLinkResp); err != nil {
- // Truncate body for error message (max 200 chars)
- bodyStr := string(body)
- if len(bodyStr) > 200 {
- bodyStr = bodyStr[:200] + "..."
- }
- return "", fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
- }
-
- deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]
- if !ok || deezerLink.URL == "" {
- return "", fmt.Errorf("deezer link not found")
- }
-
- deezerURL := deezerLink.URL
- fmt.Printf("Found Deezer URL: %s\n", deezerURL)
- return deezerURL, nil
-}
-
-func (d *DeezerDownloader) GetTrackIDFromURL(deezerURL string) (int64, error) {
- // Extract track ID from Deezer URL
- // Format: https://www.deezer.com/track/3412534581
- parts := strings.Split(deezerURL, "/track/")
- if len(parts) < 2 {
- return 0, fmt.Errorf("invalid Deezer URL format")
- }
-
- // Get the track ID part and remove any query parameters
- trackIDStr := strings.Split(parts[1], "?")[0]
- trackIDStr = strings.TrimSpace(trackIDStr)
-
- var trackID int64
- _, err := fmt.Sscanf(trackIDStr, "%d", &trackID)
- if err != nil {
- return 0, fmt.Errorf("failed to parse track ID: %w", err)
- }
-
- return trackID, nil
-}
-
-func (d *DeezerDownloader) GetTrackByID(trackID int64) (*DeezerTrack, error) {
- // Decode base64 API URL
- apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlemVyLmNvbS8yLjAvdHJhY2sv")
- url := fmt.Sprintf("%s%d", string(apiBase), trackID)
-
- resp, err := d.client.Get(url)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch track: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
- }
-
- // Read body first to handle encoding issues and provide better error messages
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response body: %w", err)
- }
-
- if len(body) == 0 {
- return nil, fmt.Errorf("API returned empty response")
- }
-
- var track DeezerTrack
- if err := json.Unmarshal(body, &track); err != nil {
- // Truncate body for error message (max 200 chars)
- bodyStr := string(body)
- if len(bodyStr) > 200 {
- bodyStr = bodyStr[:200] + "..."
- }
- return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
- }
-
- if track.ID == 0 {
- return nil, fmt.Errorf("track not found")
- }
-
- return &track, nil
-}
-
-func (d *DeezerDownloader) GetDownloadURL(trackID int64) (string, error) {
- // 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 {
- return "", fmt.Errorf("failed to get download URL: %w", err)
- }
- defer resp.Body.Close()
-
- // Read body first to handle encoding issues and provide better error messages
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", fmt.Errorf("failed to read response body: %w", err)
- }
-
- if len(body) == 0 {
- return "", fmt.Errorf("API returned empty response")
- }
-
- var apiResp DeezMateResponse
- if err := json.Unmarshal(body, &apiResp); err != nil {
- // Truncate body for error message (max 200 chars)
- bodyStr := string(body)
- if len(bodyStr) > 200 {
- bodyStr = bodyStr[:200] + "..."
- }
- return "", fmt.Errorf("failed to decode API response: %w (response: %s)", err, bodyStr)
- }
-
- if !apiResp.Success || apiResp.Links.FLAC == "" {
- return "", fmt.Errorf("no FLAC download link available")
- }
-
- return apiResp.Links.FLAC, nil
-}
-
-func (d *DeezerDownloader) DownloadFile(url, filepath string) error {
- // Use a separate client with a longer timeout. The default client's 60s limit
- // causes downloads to fail on slow connections or for large Hi-Res files.
- downloadClient := &http.Client{
- Timeout: 5 * time.Minute, // 5 minutes for large files
- }
-
- resp, err := downloadClient.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)
- }
-
- out, err := os.Create(filepath)
- if err != nil {
- return fmt.Errorf("failed to create file: %w", err)
- }
- defer out.Close()
-
- 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
-}
-
-func (d *DeezerDownloader) DownloadCoverArt(coverURL, filepath string) error {
- if coverURL == "" {
- return fmt.Errorf("no cover URL provided")
- }
-
- resp, err := d.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 buildFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
- var filename string
-
- // Determine track number to use
- numberToUse := position
- if useAlbumTrackNumber && trackNumber > 0 {
- numberToUse = trackNumber
- }
-
- // Check if format is a template (contains {})
- if strings.Contains(format, "{") {
- filename = format
- filename = strings.ReplaceAll(filename, "{title}", title)
- filename = strings.ReplaceAll(filename, "{artist}", artist)
-
- // Handle track number - if numberToUse is 0, remove {track} and surrounding separators
- if numberToUse > 0 {
- filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", numberToUse))
- } else {
- // Remove {track} with common separators
- filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
- filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
- filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
- }
- } else {
- // Legacy format support
- 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 (legacy behavior)
- if includeTrackNumber && position > 0 {
- filename = fmt.Sprintf("%02d. %s", numberToUse, filename)
- }
- }
-
- return filename + ".flac"
-}
-
-func (d *DeezerDownloader) DownloadByURL(deezerURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
- fmt.Printf("Using Deezer URL: %s\n", deezerURL)
-
- // Extract track ID from URL
- trackID, err := d.GetTrackIDFromURL(deezerURL)
- if err != nil {
- return "", err
- }
-
- // Get track info by ID
- track, err := d.GetTrackByID(trackID)
- if err != nil {
- return "", err
- }
-
- // 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 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 {
- return "", err
- }
-
- safeArtist := sanitizeFilename(artists)
- safeTitle := sanitizeFilename(trackTitle)
-
- // Check if file with same ISRC already exists
- if existingFile, exists := CheckISRCExists(outputDir, track.ISRC); exists {
- fmt.Printf("File with ISRC %s already exists: %s\n", track.ISRC, existingFile)
- return "EXISTS:" + existingFile, nil
- }
-
- // Build filename based on format settings
- filename := buildFilename(safeTitle, safeArtist, track.TrackPos, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
- filepath := filepath.Join(outputDir, filename)
-
- if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 {
- fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(fileInfo.Size())/(1024*1024))
- return "EXISTS:" + filepath, nil
- }
-
- fmt.Println("Downloading FLAC file...")
- if err := d.DownloadFile(downloadURL, filepath); err != nil {
- return "", err
- }
-
- fmt.Printf("Downloaded: %s\n", filepath)
-
- coverPath := ""
- if track.Album.CoverXL != "" {
- coverPath = filepath + ".cover.jpg"
- fmt.Println("Downloading cover art...")
- if err := d.DownloadCoverArt(track.Album.CoverXL, 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...")
- // Use album track number if in album folder structure, otherwise use playlist position
- trackNumberToEmbed := 0
- if position > 0 {
- if useAlbumTrackNumber && track.TrackPos > 0 {
- trackNumberToEmbed = track.TrackPos
- } else {
- trackNumberToEmbed = position
- }
- }
-
- metadata := Metadata{
- Title: trackTitle,
- Artist: artists,
- Album: albumTitle,
- Date: track.ReleaseDate,
- TrackNumber: trackNumberToEmbed,
- DiscNumber: track.DiskNumber,
- 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!")
- fmt.Println("✓ Downloaded successfully from Deezer")
- return filepath, nil
-}
-
-func (d *DeezerDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
- // Get Deezer URL from Spotify track ID
- deezerURL, err := d.GetDeezerURLFromSpotify(spotifyTrackID)
- if err != nil {
- return "", err
- }
-
- return d.DownloadByURL(deezerURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
-}
diff --git a/backend/metadata.go b/backend/metadata.go
index aa1892a..9ca7172 100644
--- a/backend/metadata.go
+++ b/backend/metadata.go
@@ -18,11 +18,14 @@ type Metadata struct {
Title string
Artist string
Album string
- Date string
+ AlbumArtist string
+ Date string // Recorded date (year only)
+ ReleaseDate string // Release date (full date)
TrackNumber int
DiscNumber int
ISRC string
Lyrics string
+ Description string
}
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
@@ -50,6 +53,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
if metadata.Album != "" {
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
}
+ if metadata.AlbumArtist != "" {
+ _ = cmt.Add("ALBUMARTIST", metadata.AlbumArtist)
+ }
if metadata.Date != "" {
_ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date)
}
@@ -62,6 +68,10 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
if metadata.ISRC != "" {
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
}
+ if metadata.Description != "" {
+ _ = cmt.Add("DESCRIPTION", metadata.Description)
+ }
+ // Lyrics is added last to keep it at the bottom
if metadata.Lyrics != "" {
_ = cmt.Add("LYRICS", metadata.Lyrics) // Or "UNSYNCEDLYRICS" for unsynced
}
@@ -120,6 +130,19 @@ func fileExists(path string) bool {
return err == nil
}
+// extractYear extracts the year from a release date string
+// Handles formats: "YYYY-MM-DD", "YYYY-MM", "YYYY"
+func extractYear(releaseDate string) string {
+ if releaseDate == "" {
+ return ""
+ }
+ // Try to extract year (first 4 digits)
+ if len(releaseDate) >= 4 {
+ return releaseDate[:4]
+ }
+ return releaseDate
+}
+
// EmbedLyricsOnly adds lyrics to a FLAC file while preserving existing metadata
func EmbedLyricsOnly(filepath string, lyrics string) error {
if lyrics == "" {
diff --git a/backend/qobuz.go b/backend/qobuz.go
index 2771c41..df40451 100644
--- a/backend/qobuz.go
+++ b/backend/qobuz.go
@@ -307,7 +307,7 @@ func buildQobuzFilename(title, artist string, trackNumber int, format string, in
return filename + ".flac"
}
-func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
+func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool) (string, error) {
fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
// Create output directory if it doesn't exist
@@ -409,11 +409,6 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
fmt.Println("Embedding metadata and cover art...")
- releaseYear := ""
- if len(track.ReleaseDateOriginal) >= 4 {
- releaseYear = track.ReleaseDateOriginal[:4]
- }
-
// Use album track number if in album folder structure, otherwise use playlist position
trackNumberToEmbed := 0
if position > 0 {
@@ -422,16 +417,37 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
} else {
trackNumberToEmbed = position
}
+ } else if track.TrackNumber > 0 {
+ // Fallback to Qobuz track number if no position provided
+ trackNumberToEmbed = track.TrackNumber
+ }
+
+ // Use Spotify release date if provided, otherwise use Qobuz release date
+ finalReleaseDate := spotifyReleaseDate
+ if finalReleaseDate == "" {
+ finalReleaseDate = track.ReleaseDateOriginal
+ }
+
+ // Extract year from release date (format: YYYY-MM-DD or YYYY)
+ year := extractYear(finalReleaseDate)
+
+ // Use Spotify album artist if provided, otherwise use Qobuz performer
+ finalAlbumArtist := spotifyAlbumArtist
+ if finalAlbumArtist == "" && track.Performer.Name != "" {
+ finalAlbumArtist = track.Performer.Name
}
metadata := Metadata{
Title: trackTitle,
Artist: artists,
Album: albumTitle,
- Date: releaseYear,
+ AlbumArtist: finalAlbumArtist,
+ Date: year, // Recorded date (year only)
+ ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed,
DiscNumber: track.MediaNumber,
ISRC: track.ISRC,
+ Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(filepath, metadata, coverPath); err != nil {
diff --git a/backend/songlink.go b/backend/songlink.go
index 6b7e9d1..f9cdcb6 100644
--- a/backend/songlink.go
+++ b/backend/songlink.go
@@ -19,7 +19,6 @@ type SongLinkClient struct {
type SongLinkURLs struct {
TidalURL string `json:"tidal_url"`
- DeezerURL string `json:"deezer_url"`
AmazonURL string `json:"amazon_url"`
}
@@ -27,11 +26,9 @@ type SongLinkURLs struct {
type TrackAvailability struct {
SpotifyID string `json:"spotify_id"`
Tidal bool `json:"tidal"`
- Deezer bool `json:"deezer"`
Amazon bool `json:"amazon"`
Qobuz bool `json:"qobuz"`
TidalURL string `json:"tidal_url,omitempty"`
- DeezerURL string `json:"deezer_url,omitempty"`
AmazonURL string `json:"amazon_url,omitempty"`
QobuzURL string `json:"qobuz_url,omitempty"`
}
@@ -154,12 +151,6 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLink
fmt.Printf("✓ Tidal URL found\n")
}
- // Extract Deezer URL
- if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
- urls.DeezerURL = deezerLink.URL
- fmt.Printf("✓ Deezer URL found\n")
- }
-
// Extract Amazon URL
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
amazonURL := amazonLink.URL
@@ -171,7 +162,7 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLink
}
// Check if at least one URL was found
- if urls.TidalURL == "" && urls.DeezerURL == "" && urls.AmazonURL == "" {
+ if urls.TidalURL == "" && urls.AmazonURL == "" {
return nil, fmt.Errorf("no streaming URLs found")
}
@@ -290,12 +281,6 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri
availability.TidalURL = tidalLink.URL
}
- // Check Deezer
- if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
- availability.Deezer = true
- availability.DeezerURL = deezerLink.URL
- }
-
// Check Amazon
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
availability.Amazon = true
diff --git a/backend/spotify_metadata.go b/backend/spotify_metadata.go
index aba2982..c628741 100644
--- a/backend/spotify_metadata.go
+++ b/backend/spotify_metadata.go
@@ -57,16 +57,18 @@ func NewSpotifyMetadataClient() *SpotifyMetadataClient {
// TrackMetadata mirrors the filtered track payload returned by the Python script.
type TrackMetadata struct {
+ SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"`
Name string `json:"name"`
AlbumName string `json:"album_name"`
+ AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"`
Images string `json:"images"`
ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"`
+ DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
- SpotifyID string `json:"spotify_id,omitempty"`
}
// ArtistSimple holds basic artist info for clickable artists
@@ -78,17 +80,19 @@ type ArtistSimple struct {
// AlbumTrackMetadata holds per-track info for album / playlist formatting.
type AlbumTrackMetadata struct {
+ SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"`
Name string `json:"name"`
AlbumName string `json:"album_name"`
+ AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"`
Images string `json:"images"`
ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"`
+ DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"`
AlbumType string `json:"album_type,omitempty"`
- SpotifyID string `json:"spotify_id,omitempty"`
AlbumID string `json:"album_id,omitempty"`
AlbumURL string `json:"album_url,omitempty"`
ArtistID string `json:"artist_id,omitempty"`
@@ -227,6 +231,7 @@ type trackSimplified struct {
Name string `json:"name"`
DurationMS int `json:"duration_ms"`
TrackNumber int `json:"track_number"`
+ DiscNumber int `json:"disc_number"`
ExternalURL externalURL `json:"external_urls"`
Artists []artist `json:"artists"`
}
@@ -236,6 +241,7 @@ type trackFull struct {
Name string `json:"name"`
DurationMS int `json:"duration_ms"`
TrackNumber int `json:"track_number"`
+ DiscNumber int `json:"disc_number"`
ExternalURL externalURL `json:"external_urls"`
ExternalID externalID `json:"external_ids"`
Album albumSimplified `json:"album"`
@@ -502,16 +508,18 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes
})
}
tracks = append(tracks, AlbumTrackMetadata{
+ SpotifyID: item.Track.ID,
Artists: joinArtists(item.Track.Artists),
Name: item.Track.Name,
AlbumName: item.Track.Album.Name,
+ AlbumArtist: joinArtists(item.Track.Album.Artists),
DurationMS: item.Track.DurationMS,
Images: firstNonEmpty(firstImageURL(item.Track.Album.Images), info.Owner.Images),
ReleaseDate: item.Track.Album.ReleaseDate,
TrackNumber: item.Track.TrackNumber,
+ DiscNumber: item.Track.DiscNumber,
ExternalURL: item.Track.ExternalURL.Spotify,
ISRC: item.Track.ExternalID.ISRC,
- SpotifyID: item.Track.ID,
AlbumID: item.Track.Album.ID,
AlbumURL: item.Track.Album.ExternalURL.Spotify,
ArtistID: artistID,
@@ -551,16 +559,18 @@ func (c *SpotifyMetadataClient) formatAlbumData(ctx context.Context, raw *albumR
for _, item := range raw.Data.Tracks.Items {
isrc := c.fetchTrackISRC(ctx, item.ID, raw.Token, cache)
tracks = append(tracks, AlbumTrackMetadata{
+ SpotifyID: item.ID,
Artists: joinArtists(item.Artists),
Name: item.Name,
AlbumName: raw.Data.Name,
+ AlbumArtist: joinArtists(raw.Data.Artists),
DurationMS: item.DurationMS,
Images: albumImage,
ReleaseDate: raw.Data.ReleaseDate,
TrackNumber: item.TrackNumber,
+ DiscNumber: item.DiscNumber,
ExternalURL: item.ExternalURL.Spotify,
ISRC: isrc,
- SpotifyID: item.ID,
})
}
@@ -629,17 +639,19 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
})
}
allTracks = append(allTracks, AlbumTrackMetadata{
+ SpotifyID: tr.ID,
Artists: joinArtists(tr.Artists),
Name: tr.Name,
AlbumName: alb.Name,
+ AlbumArtist: joinArtists(alb.Artists),
AlbumType: alb.AlbumType,
DurationMS: tr.DurationMS,
Images: albumImage,
ReleaseDate: alb.ReleaseDate,
TrackNumber: tr.TrackNumber,
+ DiscNumber: tr.DiscNumber,
ExternalURL: tr.ExternalURL.Spotify,
ISRC: isrc,
- SpotifyID: tr.ID,
AlbumID: alb.ID,
AlbumURL: alb.ExternalURL.Spotify,
ArtistID: artistID,
@@ -676,16 +688,18 @@ func formatTrackData(raw *trackFull) TrackResponse {
}
return TrackResponse{
Track: TrackMetadata{
+ SpotifyID: raw.ID,
Artists: joinArtists(raw.Artists),
Name: raw.Name,
AlbumName: raw.Album.Name,
+ AlbumArtist: joinArtists(raw.Album.Artists),
DurationMS: raw.DurationMS,
Images: firstImageURL(raw.Album.Images),
ReleaseDate: raw.Album.ReleaseDate,
TrackNumber: raw.TrackNumber,
+ DiscNumber: raw.DiscNumber,
ExternalURL: raw.ExternalURL.Spotify,
ISRC: raw.ExternalID.ISRC,
- SpotifyID: raw.ID,
},
}
}
diff --git a/backend/tidal.go b/backend/tidal.go
index 95be9fa..a4b032f 100644
--- a/backend/tidal.go
+++ b/backend/tidal.go
@@ -801,7 +801,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
return nil
}
-func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
+func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool) (string, error) {
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err)
@@ -848,7 +848,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
artistName = strings.Join(artists, ", ")
}
}
- artistName = sanitizeFilename(artistName)
if trackTitle == "" {
trackTitle = trackInfo.Title
@@ -856,20 +855,23 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
- trackTitle = sanitizeFilename(trackTitle)
if albumTitle == "" {
albumTitle = trackInfo.Album.Title
}
+ // Sanitize for filename only (not for metadata)
+ artistNameForFile := sanitizeFilename(artistName)
+ trackTitleForFile := sanitizeFilename(trackTitle)
+
// Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil
}
- // Build filename based on format settings
- filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
+ // Build filename based on format settings (use sanitized versions for filename)
+ filename := buildTidalFilename(trackTitleForFile, artistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -905,11 +907,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
}
}
- releaseYear := ""
- if len(trackInfo.Album.ReleaseDate) >= 4 {
- releaseYear = trackInfo.Album.ReleaseDate[:4]
- }
-
// Use album track number if in album folder structure, otherwise use playlist position
trackNumberToEmbed := 0
if position > 0 {
@@ -918,16 +915,37 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
} else {
trackNumberToEmbed = position
}
+ } else if trackInfo.TrackNumber > 0 {
+ // Fallback to Tidal track number if no position provided
+ trackNumberToEmbed = trackInfo.TrackNumber
+ }
+
+ // Use Spotify release date if provided, otherwise use Tidal release date
+ finalReleaseDate := spotifyReleaseDate
+ if finalReleaseDate == "" {
+ finalReleaseDate = trackInfo.Album.ReleaseDate
+ }
+
+ // Extract year from release date (format: YYYY-MM-DD or YYYY)
+ year := extractYear(finalReleaseDate)
+
+ // Use Spotify album artist if provided, otherwise use first artist from Tidal
+ finalAlbumArtist := spotifyAlbumArtist
+ if finalAlbumArtist == "" && len(trackInfo.Artists) > 0 {
+ finalAlbumArtist = trackInfo.Artists[0].Name
}
metadata := Metadata{
Title: trackTitle,
Artist: artistName,
Album: albumTitle,
- Date: releaseYear,
+ AlbumArtist: finalAlbumArtist,
+ Date: year, // Recorded date (year only)
+ ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC,
+ Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -941,7 +959,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return outputFilename, nil
}
-func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
+func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool) (string, error) {
apis, err := t.GetAvailableAPIs()
if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err)
@@ -992,7 +1010,6 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
artistName = strings.Join(artists, ", ")
}
}
- artistName = sanitizeFilename(artistName)
if trackTitle == "" {
trackTitle = trackInfo.Title
@@ -1000,19 +1017,22 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
- trackTitle = sanitizeFilename(trackTitle)
if albumTitle == "" {
albumTitle = trackInfo.Album.Title
}
+ // Sanitize for filename only (not for metadata)
+ artistNameForFile := sanitizeFilename(artistName)
+ trackTitleForFile := sanitizeFilename(trackTitle)
+
// Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil
}
- filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
+ filename := buildTidalFilename(trackTitleForFile, artistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1051,11 +1071,6 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
}
}
- releaseYear := ""
- if len(trackInfo.Album.ReleaseDate) >= 4 {
- releaseYear = trackInfo.Album.ReleaseDate[:4]
- }
-
trackNumberToEmbed := 0
if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1063,16 +1078,37 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
} else {
trackNumberToEmbed = position
}
+ } else if trackInfo.TrackNumber > 0 {
+ // Fallback to Tidal track number if no position provided
+ trackNumberToEmbed = trackInfo.TrackNumber
+ }
+
+ // Use Spotify release date if provided, otherwise use Tidal release date
+ finalReleaseDate := spotifyReleaseDate
+ if finalReleaseDate == "" {
+ finalReleaseDate = trackInfo.Album.ReleaseDate
+ }
+
+ // Extract year from release date (format: YYYY-MM-DD or YYYY)
+ year := extractYear(finalReleaseDate)
+
+ // Use Spotify album artist if provided, otherwise use first artist from Tidal
+ finalAlbumArtist := spotifyAlbumArtist
+ if finalAlbumArtist == "" && len(trackInfo.Artists) > 0 {
+ finalAlbumArtist = trackInfo.Artists[0].Name
}
metadata := Metadata{
Title: trackTitle,
Artist: artistName,
Album: albumTitle,
- Date: releaseYear,
+ AlbumArtist: finalAlbumArtist,
+ Date: year, // Recorded date (year only)
+ ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC,
+ Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1086,7 +1122,7 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return outputFilename, nil
}
-func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
+func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool) (string, error) {
// Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
@@ -1096,11 +1132,11 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF
return t.DownloadBySearch(spotifyTrackName, spotifyArtistName, spotifyAlbumName, "", 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
}
- return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
+ return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
}
// DownloadWithISRC downloads a track with ISRC matching for search fallback
-func (t *TidalDownloader) DownloadWithISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool, expectedDuration int) (string, error) {
+func (t *TidalDownloader) DownloadWithISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, expectedDuration int) (string, error) {
// Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
@@ -1110,7 +1146,7 @@ func (t *TidalDownloader) DownloadWithISRC(spotifyTrackID, spotifyISRC, outputDi
return t.DownloadBySearchWithISRC(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
}
- return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
+ return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
}
// DownloadBySearch downloads a track by searching Tidal directly using metadata
@@ -1159,7 +1195,6 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
finalArtistName = "Unknown Artist"
}
}
- finalArtistName = sanitizeFilename(finalArtistName)
if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title
@@ -1167,12 +1202,15 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
- finalTrackTitle = sanitizeFilename(finalTrackTitle)
if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title
}
+ // Sanitize for filename only (not for metadata)
+ finalArtistNameForFile := sanitizeFilename(finalArtistName)
+ finalTrackTitleForFile := sanitizeFilename(finalTrackTitle)
+
// Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
@@ -1180,7 +1218,7 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
}
// Build filename
- filename := buildTidalFilename(finalTrackTitle, finalArtistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
+ filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1217,11 +1255,6 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
}
}
- releaseYear := ""
- if len(trackInfo.Album.ReleaseDate) >= 4 {
- releaseYear = trackInfo.Album.ReleaseDate[:4]
- }
-
trackNumberToEmbed := 0
if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1229,16 +1262,34 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
} else {
trackNumberToEmbed = position
}
+ } else if trackInfo.TrackNumber > 0 {
+ // Fallback to Tidal track number if no position provided
+ trackNumberToEmbed = trackInfo.TrackNumber
+ }
+
+ // Use Tidal release date (no Spotify metadata available in search fallback)
+ finalReleaseDate := trackInfo.Album.ReleaseDate
+
+ // Extract year from release date (format: YYYY-MM-DD or YYYY)
+ year := extractYear(finalReleaseDate)
+
+ // Use first artist from Tidal as album artist in search fallback
+ albumArtist := ""
+ if len(trackInfo.Artists) > 0 {
+ albumArtist = trackInfo.Artists[0].Name
}
metadata := Metadata{
Title: finalTrackTitle,
Artist: finalArtistName,
Album: finalAlbumTitle,
- Date: releaseYear,
+ AlbumArtist: albumArtist,
+ Date: year, // Recorded date (year only)
+ ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC,
+ Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1516,7 +1567,6 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
finalArtistName = "Unknown Artist"
}
}
- finalArtistName = sanitizeFilename(finalArtistName)
if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title
@@ -1524,19 +1574,22 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
- finalTrackTitle = sanitizeFilename(finalTrackTitle)
if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title
}
+ // Sanitize for filename only (not for metadata)
+ finalArtistNameForFile := sanitizeFilename(finalArtistName)
+ finalTrackTitleForFile := sanitizeFilename(finalTrackTitle)
+
// Check if file already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil
}
- filename := buildTidalFilename(finalTrackTitle, finalArtistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
+ filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1576,11 +1629,6 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
}
}
- releaseYear := ""
- if len(trackInfo.Album.ReleaseDate) >= 4 {
- releaseYear = trackInfo.Album.ReleaseDate[:4]
- }
-
trackNumberToEmbed := 0
if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1588,16 +1636,34 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
} else {
trackNumberToEmbed = position
}
+ } else if trackInfo.TrackNumber > 0 {
+ // Fallback to Tidal track number if no position provided
+ trackNumberToEmbed = trackInfo.TrackNumber
+ }
+
+ // Use Tidal release date (no Spotify metadata available in search fallback)
+ finalReleaseDate := trackInfo.Album.ReleaseDate
+
+ // Extract year from release date (format: YYYY-MM-DD or YYYY)
+ year := extractYear(finalReleaseDate)
+
+ // Use first artist from Tidal as album artist in search fallback
+ albumArtist := ""
+ if len(trackInfo.Artists) > 0 {
+ albumArtist = trackInfo.Artists[0].Name
}
metadata := Metadata{
Title: finalTrackTitle,
Artist: finalArtistName,
Album: finalAlbumTitle,
- Date: releaseYear,
+ AlbumArtist: albumArtist,
+ Date: year, // Recorded date (year only)
+ ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC,
+ Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1611,7 +1677,7 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
return outputFilename, nil
}
-func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
+func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool) (string, error) {
// Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
@@ -1622,12 +1688,12 @@ func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, qualit
}
// Use parallel API requests via DownloadByURLWithFallback
- return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
+ return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
}
// DownloadWithFallbackAndISRC downloads with ISRC matching for search fallback
// Uses parallel API requests for faster download
-func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool, expectedDuration int) (string, error) {
+func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, expectedDuration int) (string, error) {
// Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
@@ -1638,7 +1704,7 @@ func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISR
}
// Use parallel API requests via DownloadByURLWithFallback
- return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
+ return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
}
func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 5d27c86..090e1b2 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -58,7 +58,7 @@ export function Header({ version, hasUpdate, releaseDate }: HeaderProps) {
- Get Spotify tracks in true FLAC from Tidal, Deezer, Qobuz & Amazon Music — no account required.
+ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.