This commit is contained in:
afkarxyz
2025-12-14 15:35:11 +07:00
parent 8e78a882a3
commit b85ed89af3
17 changed files with 353 additions and 421 deletions
+11 -7
View File
@@ -45,6 +45,7 @@ type DownloadRequest struct {
AlbumName string `json:"album_name,omitempty"` AlbumName string `json:"album_name,omitempty"`
AlbumArtist string `json:"album_artist,omitempty"` AlbumArtist string `json:"album_artist,omitempty"`
ReleaseDate string `json:"release_date,omitempty"` ReleaseDate string `json:"release_date,omitempty"`
CoverURL string `json:"cover_url,omitempty"` // Spotify cover URL for embedding
ApiURL string `json:"api_url,omitempty"` ApiURL string `json:"api_url,omitempty"`
OutputDir string `json:"output_dir,omitempty"` OutputDir string `json:"output_dir,omitempty"`
AudioFormat string `json:"audio_format,omitempty"` AudioFormat string `json:"audio_format,omitempty"`
@@ -58,6 +59,9 @@ type DownloadRequest struct {
ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call 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 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 ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
SpotifyTrackNumber int `json:"spotify_track_number,omitempty"` // Track number from Spotify album
SpotifyDiscNumber int `json:"spotify_disc_number,omitempty"` // Disc number from Spotify album
SpotifyTotalTracks int `json:"spotify_total_tracks,omitempty"` // Total tracks in album from Spotify
} }
// DownloadResponse represents the response structure for download operations // DownloadResponse represents the response structure for download operations
@@ -211,7 +215,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
downloader := backend.NewAmazonDownloader() downloader := backend.NewAmazonDownloader()
if req.ServiceURL != "" { if req.ServiceURL != "" {
// Use provided URL directly // 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) filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.ISRC, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover)
} else { } else {
if req.SpotifyID == "" { if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
@@ -219,7 +223,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
Error: "Spotify ID is required for Amazon Music", Error: "Spotify ID is required for Amazon Music",
}, fmt.Errorf("spotify ID is required for Amazon Music") }, fmt.Errorf("spotify ID is required for Amazon Music")
} }
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber) filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.CoverURL, req.ISRC, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.EmbedMaxQualityCover)
} }
case "tidal": case "tidal":
@@ -227,7 +231,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
downloader := backend.NewTidalDownloader("") downloader := backend.NewTidalDownloader("")
if req.ServiceURL != "" { if req.ServiceURL != "" {
// Use provided URL directly with fallback to multiple APIs // 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.AlbumArtist, req.ReleaseDate, 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, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.ISRC)
} else { } else {
if req.SpotifyID == "" { if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
@@ -236,13 +240,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal") }, fmt.Errorf("spotify ID is required for Tidal")
} }
// Use ISRC matching for search fallback // 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.AlbumArtist, req.ReleaseDate, 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, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks)
} }
} else { } else {
downloader := backend.NewTidalDownloader(req.ApiURL) downloader := backend.NewTidalDownloader(req.ApiURL)
if req.ServiceURL != "" { if req.ServiceURL != "" {
// Use provided URL directly with specific API // 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.AlbumArtist, req.ReleaseDate, 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, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.ISRC)
} else { } else {
if req.SpotifyID == "" { if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
@@ -251,7 +255,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal") }, fmt.Errorf("spotify ID is required for Tidal")
} }
// Use ISRC matching for search fallback // 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.AlbumArtist, req.ReleaseDate, 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, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks)
} }
} }
@@ -262,7 +266,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
if quality == "" { if quality == "" {
quality = "6" quality = "6"
} }
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) 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, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks)
default: default:
return DownloadResponse{ return DownloadResponse{
+52 -5
View File
@@ -372,7 +372,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (str
return "", fmt.Errorf("all regions failed. Last error: %v", lastError) return "", fmt.Errorf("all regions failed. Last error: %v", lastError)
} }
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) { func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool) (string, error) {
// Create output directory if needed // Create output directory if needed
if outputDir != "." { if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil { if err := os.MkdirAll(outputDir, 0755); err != nil {
@@ -382,7 +382,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
// Check if file with expected name already exists (Amazon doesn't provide ISRC before download) // Check if file with expected name already exists (Amazon doesn't provide ISRC before download)
if spotifyTrackName != "" && spotifyArtistName != "" { if spotifyTrackName != "" && spotifyArtistName != "" {
expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, false)
expectedPath := filepath.Join(outputDir, expectedFilename) expectedPath := filepath.Join(outputDir, expectedFilename)
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
@@ -399,7 +399,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
return "", err return "", err
} }
// File already has embedded metadata, just rename if needed // Rename file based on Spotify metadata
if spotifyTrackName != "" && spotifyArtistName != "" { if spotifyTrackName != "" && spotifyArtistName != "" {
safeArtist := sanitizeFilename(spotifyArtistName) safeArtist := sanitizeFilename(spotifyArtistName)
safeTitle := sanitizeFilename(spotifyTrackName) safeTitle := sanitizeFilename(spotifyTrackName)
@@ -451,17 +451,64 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat st
} }
} }
// Embed Spotify metadata (replace Amazon's embedded metadata)
fmt.Println("Embedding Spotify metadata...")
coverPath := ""
// Download Spotify cover (with max resolution if enabled)
if spotifyCoverURL != "" {
coverPath = filePath + ".cover.jpg"
coverClient := NewCoverClient()
if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else {
defer os.Remove(coverPath)
fmt.Println("Spotify cover downloaded")
}
}
// Determine track number to embed
// Use Spotify track number (album track number) if available, otherwise use position
trackNumberToEmbed := spotifyTrackNumber
if trackNumberToEmbed == 0 {
trackNumberToEmbed = position // Fallback to playlist position
}
if trackNumberToEmbed == 0 {
trackNumberToEmbed = 1 // Default to track 1 for single track downloads without track number
}
// Build metadata from Spotify
metadata := Metadata{
Title: spotifyTrackName,
Artist: spotifyArtistName,
Album: spotifyAlbumName,
AlbumArtist: spotifyAlbumArtist,
Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD)
TrackNumber: trackNumberToEmbed,
TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: spotifyISRC, // Use ISRC from Spotify
Description: "https://github.com/afkarxyz/SpotiFLAC",
}
if err := EmbedMetadata(filePath, metadata, coverPath); err != nil {
fmt.Printf("Warning: Failed to embed metadata: %v\n", err)
} else {
fmt.Println("Metadata embedded successfully")
}
fmt.Println("Done") fmt.Println("Done")
fmt.Println("✓ Downloaded successfully from Amazon Music") fmt.Println("✓ Downloaded successfully from Amazon Music")
return filePath, nil return filePath, nil
} }
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) { func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool) (string, error) {
// Get Amazon URL from Spotify track ID // Get Amazon URL from Spotify track ID
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID) amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
return "", err return "", err
} }
return a.DownloadByURL(amazonURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber) return a.DownloadByURL(amazonURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, spotifyISRC, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, embedMaxQualityCover)
} }
+40
View File
@@ -107,6 +107,46 @@ func (c *CoverClient) getMaxResolutionURL(coverURL string) string {
return coverURL return coverURL
} }
// DownloadCoverToPath downloads cover art from URL to a specific path
// If embedMaxQualityCover is true, it will try to get max resolution
func (c *CoverClient) DownloadCoverToPath(coverURL, outputPath string, embedMaxQualityCover bool) error {
if coverURL == "" {
return fmt.Errorf("cover URL is required")
}
// Use max quality URL if setting is enabled
downloadURL := coverURL
if embedMaxQualityCover {
downloadURL = c.getMaxResolutionURL(coverURL)
}
// Download cover image
resp, err := c.httpClient.Get(downloadURL)
if err != nil {
return fmt.Errorf("failed to download cover: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download cover: HTTP %d", resp.StatusCode)
}
// Create file
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %v", err)
}
defer file.Close()
// Write content to file
_, err = io.Copy(file, resp.Body)
if err != nil {
return fmt.Errorf("failed to write cover file: %v", err)
}
return nil
}
// DownloadCover downloads cover art for a single track // DownloadCover downloads cover art for a single track
func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadResponse, error) { func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadResponse, error) {
if req.CoverURL == "" { if req.CoverURL == "" {
+12 -8
View File
@@ -19,9 +19,10 @@ type Metadata struct {
Artist string Artist string
Album string Album string
AlbumArtist string AlbumArtist string
Date string // Recorded date (year only) Date string // Recorded date (full date YYYY-MM-DD)
ReleaseDate string // Release date (full date) ReleaseDate string // Release date (full date) - kept for compatibility
TrackNumber int TrackNumber int
TotalTracks int // Total tracks in album
DiscNumber int DiscNumber int
ISRC string ISRC string
Lyrics string Lyrics string
@@ -62,6 +63,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
if metadata.TrackNumber > 0 { if metadata.TrackNumber > 0 {
_ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber)) _ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber))
} }
if metadata.TotalTracks > 0 {
_ = cmt.Add("TOTALTRACKS", strconv.Itoa(metadata.TotalTracks))
}
if metadata.DiscNumber > 0 { if metadata.DiscNumber > 0 {
_ = cmt.Add("DISCNUMBER", strconv.Itoa(metadata.DiscNumber)) _ = cmt.Add("DISCNUMBER", strconv.Itoa(metadata.DiscNumber))
} }
@@ -277,7 +281,7 @@ func CheckISRCExists(outputDir string, targetISRC string) (string, bool) {
// ExtractCoverArt extracts cover art from an audio file and saves it to a temporary file // ExtractCoverArt extracts cover art from an audio file and saves it to a temporary file
func ExtractCoverArt(filePath string) (string, error) { func ExtractCoverArt(filePath string) (string, error) {
ext := strings.ToLower(pathfilepath.Ext(filePath)) ext := strings.ToLower(pathfilepath.Ext(filePath))
switch ext { switch ext {
case ".mp3": case ".mp3":
return extractCoverFromMp3(filePath) return extractCoverFromMp3(filePath)
@@ -324,7 +328,7 @@ func extractCoverFromMp3(filePath string) (string, error) {
// extractCoverFromM4AOrFlac extracts cover art from M4A or FLAC file // extractCoverFromM4AOrFlac extracts cover art from M4A or FLAC file
func extractCoverFromM4AOrFlac(filePath string) (string, error) { func extractCoverFromM4AOrFlac(filePath string) (string, error) {
ext := strings.ToLower(pathfilepath.Ext(filePath)) ext := strings.ToLower(pathfilepath.Ext(filePath))
if ext == ".flac" { if ext == ".flac" {
f, err := flac.ParseFile(filePath) f, err := flac.ParseFile(filePath)
if err != nil { if err != nil {
@@ -364,7 +368,7 @@ func extractCoverFromM4AOrFlac(filePath string) (string, error) {
// ExtractLyrics extracts lyrics from an audio file // ExtractLyrics extracts lyrics from an audio file
func ExtractLyrics(filePath string) (string, error) { func ExtractLyrics(filePath string) (string, error) {
ext := strings.ToLower(pathfilepath.Ext(filePath)) ext := strings.ToLower(pathfilepath.Ext(filePath))
switch ext { switch ext {
case ".mp3": case ".mp3":
return extractLyricsFromMp3(filePath) return extractLyricsFromMp3(filePath)
@@ -447,7 +451,7 @@ func EmbedCoverArtOnly(filePath string, coverPath string) error {
} }
ext := strings.ToLower(pathfilepath.Ext(filePath)) ext := strings.ToLower(pathfilepath.Ext(filePath))
switch ext { switch ext {
case ".mp3": case ".mp3":
return embedCoverToMp3(filePath, coverPath) return embedCoverToMp3(filePath, coverPath)
@@ -500,7 +504,7 @@ func EmbedLyricsOnlyMP3(filepath string, lyrics string) error {
if lyrics == "" { if lyrics == "" {
return nil return nil
} }
tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true}) tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true})
if err != nil { if err != nil {
return fmt.Errorf("failed to open MP3 file: %w", err) return fmt.Errorf("failed to open MP3 file: %w", err)
@@ -583,7 +587,7 @@ func EmbedLyricsOnlyUniversal(filepath string, lyrics string) error {
if lyrics == "" { if lyrics == "" {
return nil return nil
} }
ext := strings.ToLower(pathfilepath.Ext(filepath)) ext := strings.ToLower(pathfilepath.Ext(filepath))
switch ext { switch ext {
case ".mp3": case ".mp3":
+24 -60
View File
@@ -307,7 +307,7 @@ func buildQobuzFilename(title, artist string, trackNumber int, format string, in
return filename + ".flac" return filename + ".flac"
} }
func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
fmt.Printf("Fetching track info for ISRC: %s\n", isrc) fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
// Create output directory if it doesn't exist // Create output directory if it doesn't exist
@@ -322,29 +322,11 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
return "", err return "", err
} }
// Use Spotify metadata if provided, otherwise fallback to Qobuz metadata // All metadata from Spotify - no fallback to Qobuz
artists := spotifyArtistName artists := spotifyArtistName
trackTitle := spotifyTrackName trackTitle := spotifyTrackName
albumTitle := spotifyAlbumName 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("Found track: %s - %s\n", artists, trackTitle)
fmt.Printf("Album: %s\n", albumTitle) fmt.Printf("Album: %s\n", albumTitle)
@@ -374,14 +356,14 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
safeArtist := sanitizeFilename(artists) safeArtist := sanitizeFilename(artists)
safeTitle := sanitizeFilename(trackTitle) safeTitle := sanitizeFilename(trackTitle)
// Check if file with same ISRC already exists // Check if file with same ISRC already exists (use Spotify ISRC)
if existingFile, exists := CheckISRCExists(outputDir, track.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, isrc); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", track.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", isrc, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
// Build filename based on format settings // Build filename based on format settings (use Spotify track number)
filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildQobuzFilename(safeTitle, safeArtist, spotifyTrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
filepath := filepath.Join(outputDir, filename) filepath := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 {
@@ -397,56 +379,38 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
fmt.Printf("Downloaded: %s\n", filepath) fmt.Printf("Downloaded: %s\n", filepath)
coverPath := "" coverPath := ""
if track.Album.Image.Large != "" { // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify
if spotifyCoverURL != "" {
coverPath = filepath + ".cover.jpg" coverPath = filepath + ".cover.jpg"
fmt.Println("Downloading cover art...") coverClient := NewCoverClient()
if err := q.DownloadCoverArt(track.Album.Image.Large, coverPath); err != nil { if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download cover art: %v\n", err) fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else { } else {
defer os.Remove(coverPath) defer os.Remove(coverPath)
fmt.Println("Spotify cover downloaded")
} }
} }
fmt.Println("Embedding metadata and cover art...") fmt.Println("Embedding metadata and cover art...")
// Use album track number if in album folder structure, otherwise use playlist position // Determine track number to embed - ALL from Spotify
trackNumberToEmbed := 0 trackNumberToEmbed := spotifyTrackNumber
if position > 0 { if position > 0 && !useAlbumTrackNumber {
if useAlbumTrackNumber && track.TrackNumber > 0 { trackNumberToEmbed = position // Use playlist position
trackNumberToEmbed = track.TrackNumber
} 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
} }
// ALL metadata from Spotify
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artists, Artist: artists,
Album: albumTitle, Album: albumTitle,
AlbumArtist: finalAlbumArtist, AlbumArtist: spotifyAlbumArtist,
Date: year, // Recorded date (year only) Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: track.MediaNumber, TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
ISRC: track.ISRC, DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: isrc, // ISRC from Spotify (passed as parameter)
Description: "https://github.com/afkarxyz/SpotiFLAC", Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
+6
View File
@@ -66,6 +66,7 @@ type TrackMetadata struct {
Images string `json:"images"` Images string `json:"images"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"` DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"` ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"` ISRC string `json:"isrc"`
@@ -89,6 +90,7 @@ type AlbumTrackMetadata struct {
Images string `json:"images"` Images string `json:"images"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
TotalTracks int `json:"total_tracks,omitempty"`
DiscNumber int `json:"disc_number,omitempty"` DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"` ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"` ISRC string `json:"isrc"`
@@ -517,6 +519,7 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes
Images: firstNonEmpty(firstImageURL(item.Track.Album.Images), info.Owner.Images), Images: firstNonEmpty(firstImageURL(item.Track.Album.Images), info.Owner.Images),
ReleaseDate: item.Track.Album.ReleaseDate, ReleaseDate: item.Track.Album.ReleaseDate,
TrackNumber: item.Track.TrackNumber, TrackNumber: item.Track.TrackNumber,
TotalTracks: item.Track.Album.TotalTracks,
DiscNumber: item.Track.DiscNumber, DiscNumber: item.Track.DiscNumber,
ExternalURL: item.Track.ExternalURL.Spotify, ExternalURL: item.Track.ExternalURL.Spotify,
ISRC: item.Track.ExternalID.ISRC, ISRC: item.Track.ExternalID.ISRC,
@@ -568,6 +571,7 @@ func (c *SpotifyMetadataClient) formatAlbumData(ctx context.Context, raw *albumR
Images: albumImage, Images: albumImage,
ReleaseDate: raw.Data.ReleaseDate, ReleaseDate: raw.Data.ReleaseDate,
TrackNumber: item.TrackNumber, TrackNumber: item.TrackNumber,
TotalTracks: raw.Data.TotalTracks,
DiscNumber: item.DiscNumber, DiscNumber: item.DiscNumber,
ExternalURL: item.ExternalURL.Spotify, ExternalURL: item.ExternalURL.Spotify,
ISRC: isrc, ISRC: isrc,
@@ -649,6 +653,7 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
Images: albumImage, Images: albumImage,
ReleaseDate: alb.ReleaseDate, ReleaseDate: alb.ReleaseDate,
TrackNumber: tr.TrackNumber, TrackNumber: tr.TrackNumber,
TotalTracks: alb.TotalTracks,
DiscNumber: tr.DiscNumber, DiscNumber: tr.DiscNumber,
ExternalURL: tr.ExternalURL.Spotify, ExternalURL: tr.ExternalURL.Spotify,
ISRC: isrc, ISRC: isrc,
@@ -697,6 +702,7 @@ func formatTrackData(raw *trackFull) TrackResponse {
Images: firstImageURL(raw.Album.Images), Images: firstImageURL(raw.Album.Images),
ReleaseDate: raw.Album.ReleaseDate, ReleaseDate: raw.Album.ReleaseDate,
TrackNumber: raw.TrackNumber, TrackNumber: raw.TrackNumber,
TotalTracks: raw.Album.TotalTracks,
DiscNumber: raw.DiscNumber, DiscNumber: raw.DiscNumber,
ExternalURL: raw.ExternalURL.Spotify, ExternalURL: raw.ExternalURL.Spotify,
ISRC: raw.ExternalID.ISRC, ISRC: raw.ExternalID.ISRC,
+102 -298
View File
@@ -801,7 +801,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
return nil return nil
} }
func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyISRC string) (string, error) {
if outputDir != "." { if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil { if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err) return "", fmt.Errorf("directory error: %w", err)
@@ -826,40 +826,11 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return "", fmt.Errorf("no track ID found") return "", fmt.Errorf("no track ID found")
} }
// Use Spotify metadata if provided, otherwise fallback to Tidal metadata // All metadata from Spotify - no fallback to Tidal
artistName := spotifyArtistName artistName := spotifyArtistName
trackTitle := spotifyTrackName trackTitle := spotifyTrackName
albumTitle := spotifyAlbumName albumTitle := spotifyAlbumName
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, ", ")
}
}
if trackTitle == "" {
trackTitle = trackInfo.Title
if trackTitle == "" {
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
if albumTitle == "" {
albumTitle = trackInfo.Album.Title
}
// Sanitize for filename only (not for metadata) // Sanitize for filename only (not for metadata)
artistNameForFile := sanitizeFilename(artistName) artistNameForFile := sanitizeFilename(artistName)
trackTitleForFile := sanitizeFilename(trackTitle) trackTitleForFile := sanitizeFilename(trackTitle)
@@ -892,59 +863,38 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
fmt.Println("Adding metadata...") fmt.Println("Adding metadata...")
coverPath := "" coverPath := ""
if trackInfo.Album.Cover != "" { // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify
if spotifyCoverURL != "" {
coverPath = outputFilename + ".cover.jpg" coverPath = outputFilename + ".cover.jpg"
albumArt, err := t.DownloadAlbumArt(trackInfo.Album.Cover) coverClient := NewCoverClient()
if err != nil { if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download album art: %v\n", err) fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else { } else {
if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { defer os.Remove(coverPath)
fmt.Printf("Warning: Failed to save album art: %v\n", err) fmt.Println("Spotify cover downloaded")
} else {
defer os.Remove(coverPath)
fmt.Println("Album art downloaded")
}
} }
} }
// Use album track number if in album folder structure, otherwise use playlist position // Determine track number to embed - ALL from Spotify
trackNumberToEmbed := 0 // - If position > 0 and !useAlbumTrackNumber: use playlist position
if position > 0 { // - Otherwise: use Spotify track number
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { trackNumberToEmbed := spotifyTrackNumber
trackNumberToEmbed = trackInfo.TrackNumber if position > 0 && !useAlbumTrackNumber {
} else { trackNumberToEmbed = position // Use playlist position
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
} }
// ALL metadata from Spotify
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artistName, Artist: artistName,
Album: albumTitle, Album: albumTitle,
AlbumArtist: finalAlbumArtist, AlbumArtist: spotifyAlbumArtist,
Date: year, // Recorded date (year only) Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
ISRC: trackInfo.ISRC, DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: spotifyISRC, // ISRC from Spotify
Description: "https://github.com/afkarxyz/SpotiFLAC", Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
@@ -959,7 +909,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyISRC string) (string, error) {
apis, err := t.GetAvailableAPIs() apis, err := t.GetAvailableAPIs()
if err != nil { if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err) return "", fmt.Errorf("no APIs available for fallback: %w", err)
@@ -989,39 +939,11 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return "", fmt.Errorf("no track ID found") return "", fmt.Errorf("no track ID found")
} }
// Use Spotify metadata if provided, otherwise fallback to Tidal metadata // All metadata from Spotify - no fallback to Tidal
artistName := spotifyArtistName artistName := spotifyArtistName
trackTitle := spotifyTrackName trackTitle := spotifyTrackName
albumTitle := spotifyAlbumName albumTitle := spotifyAlbumName
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, ", ")
}
}
if trackTitle == "" {
trackTitle = trackInfo.Title
if trackTitle == "" {
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
if albumTitle == "" {
albumTitle = trackInfo.Album.Title
}
// Sanitize for filename only (not for metadata) // Sanitize for filename only (not for metadata)
artistNameForFile := sanitizeFilename(artistName) artistNameForFile := sanitizeFilename(artistName)
trackTitleForFile := sanitizeFilename(trackTitle) trackTitleForFile := sanitizeFilename(trackTitle)
@@ -1056,58 +978,36 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
fmt.Println("Adding metadata...") fmt.Println("Adding metadata...")
coverPath := "" coverPath := ""
if trackInfo.Album.Cover != "" { // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify
if spotifyCoverURL != "" {
coverPath = outputFilename + ".cover.jpg" coverPath = outputFilename + ".cover.jpg"
albumArt, err := downloader.DownloadAlbumArt(trackInfo.Album.Cover) coverClient := NewCoverClient()
if err != nil { if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download album art: %v\n", err) fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else { } else {
if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { defer os.Remove(coverPath)
fmt.Printf("Warning: Failed to save album art: %v\n", err) fmt.Println("Spotify cover downloaded")
} else {
defer os.Remove(coverPath)
fmt.Println("Album art downloaded")
}
} }
} }
trackNumberToEmbed := 0 // Determine track number to embed - ALL from Spotify
if position > 0 { trackNumberToEmbed := spotifyTrackNumber
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if position > 0 && !useAlbumTrackNumber {
trackNumberToEmbed = trackInfo.TrackNumber trackNumberToEmbed = position // Use playlist position
} 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
} }
// ALL metadata from Spotify
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artistName, Artist: artistName,
Album: albumTitle, Album: albumTitle,
AlbumArtist: finalAlbumArtist, AlbumArtist: spotifyAlbumArtist,
Date: year, // Recorded date (year only) Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
ISRC: trackInfo.ISRC, DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: spotifyISRC, // ISRC from Spotify
Description: "https://github.com/afkarxyz/SpotiFLAC", Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
@@ -1122,41 +1022,41 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyISRC string) (string, error) {
// Get Tidal URL from Spotify track ID // Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
// Songlink failed to find Tidal URL, try search fallback // Songlink failed to find Tidal URL, try search fallback
fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err)
fmt.Println("Trying Tidal search fallback...") fmt.Println("Trying Tidal search fallback...")
return t.DownloadBySearch(spotifyTrackName, spotifyArtistName, spotifyAlbumName, "", 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearch(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyISRC, 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks)
} }
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyISRC)
} }
// DownloadWithISRC downloads a track with ISRC matching for search fallback // 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, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
// Get Tidal URL from Spotify track ID // Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
// Songlink failed to find Tidal URL, try search fallback with ISRC // Songlink failed to find Tidal URL, try search fallback with ISRC
fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err)
fmt.Println("Trying Tidal search fallback with ISRC matching...") fmt.Println("Trying Tidal search fallback with ISRC matching...")
return t.DownloadBySearchWithISRC(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearchWithISRC(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks)
} }
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyISRC)
} }
// DownloadBySearch downloads a track by searching Tidal directly using metadata // DownloadBySearch downloads a track by searching Tidal directly using metadata
// This is used as a fallback when Songlink API doesn't find a Tidal URL // This is used as a fallback when Songlink API doesn't find a Tidal URL
func (t *TidalDownloader) DownloadBySearch(trackName, artistName, albumName, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) (string, error) { func (t *TidalDownloader) DownloadBySearch(trackName, artistName, albumName, albumArtist, releaseDate, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
return t.DownloadBySearchWithISRC(trackName, artistName, albumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearchWithISRC(trackName, artistName, albumName, albumArtist, releaseDate, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks)
} }
// DownloadBySearchWithISRC downloads a track by searching Tidal with ISRC matching // DownloadBySearchWithISRC downloads a track by searching Tidal with ISRC matching
func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumName, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) (string, error) { func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumName, albumArtist, releaseDate, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
if outputDir != "." { if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil { if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err) return "", fmt.Errorf("directory error: %w", err)
@@ -1173,52 +1073,23 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
return "", fmt.Errorf("no track ID found from search") return "", fmt.Errorf("no track ID found from search")
} }
// Use provided metadata, fallback to Tidal metadata // All metadata from Spotify - no fallback to Tidal
finalArtistName := artistName finalArtistName := artistName
finalTrackTitle := trackName finalTrackTitle := trackName
finalAlbumTitle := albumName finalAlbumTitle := albumName
if finalArtistName == "" {
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)
}
if len(artists) > 0 {
finalArtistName = strings.Join(artists, ", ")
} else {
finalArtistName = "Unknown Artist"
}
}
if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title
if finalTrackTitle == "" {
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title
}
// Sanitize for filename only (not for metadata) // Sanitize for filename only (not for metadata)
finalArtistNameForFile := sanitizeFilename(finalArtistName) finalArtistNameForFile := sanitizeFilename(finalArtistName)
finalTrackTitleForFile := sanitizeFilename(finalTrackTitle) finalTrackTitleForFile := sanitizeFilename(finalTrackTitle)
// Check if file with same ISRC already exists // Check if file with same ISRC already exists (use Spotify ISRC)
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, spotifyISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", spotifyISRC, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
// Build filename // Build filename
filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, spotifyTrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1240,55 +1111,36 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
fmt.Println("Adding metadata...") fmt.Println("Adding metadata...")
coverPath := "" coverPath := ""
if trackInfo.Album.Cover != "" { // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify
if spotifyCoverURL != "" {
coverPath = outputFilename + ".cover.jpg" coverPath = outputFilename + ".cover.jpg"
albumArt, err := t.DownloadAlbumArt(trackInfo.Album.Cover) coverClient := NewCoverClient()
if err != nil { if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download album art: %v\n", err) fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else { } else {
if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { defer os.Remove(coverPath)
fmt.Printf("Warning: Failed to save album art: %v\n", err) fmt.Println("Spotify cover downloaded")
} else {
defer os.Remove(coverPath)
fmt.Println("Album art downloaded")
}
} }
} }
trackNumberToEmbed := 0 // Determine track number to embed - ALL from Spotify
if position > 0 { trackNumberToEmbed := spotifyTrackNumber
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if position > 0 && !useAlbumTrackNumber {
trackNumberToEmbed = trackInfo.TrackNumber trackNumberToEmbed = position // Use playlist position
} 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
} }
// ALL metadata from Spotify
metadata := Metadata{ metadata := Metadata{
Title: finalTrackTitle, Title: finalTrackTitle,
Artist: finalArtistName, Artist: finalArtistName,
Album: finalAlbumTitle, Album: finalAlbumTitle,
AlbumArtist: albumArtist, AlbumArtist: albumArtist,
Date: year, // Recorded date (year only) Date: releaseDate, // Recorded date (full date YYYY-MM-DD)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
ISRC: trackInfo.ISRC, DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: spotifyISRC, // ISRC from Spotify
Description: "https://github.com/afkarxyz/SpotiFLAC", Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
@@ -1520,7 +1372,7 @@ func getDownloadURLParallel(apis []string, trackID int64, quality string) (strin
// DownloadBySearchWithFallback tries multiple APIs when downloading via search // DownloadBySearchWithFallback tries multiple APIs when downloading via search
// Search is done ONCE, then requests all APIs in PARALLEL for download URL // Search is done ONCE, then requests all APIs in PARALLEL for download URL
func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, albumName, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) (string, error) { func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, albumName, albumArtist, releaseDate, spotifyISRC string, expectedDuration int, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
apis, err := t.GetAvailableAPIs() apis, err := t.GetAvailableAPIs()
if err != nil { if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err) return "", fmt.Errorf("no APIs available for fallback: %w", err)
@@ -1545,51 +1397,22 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
fmt.Printf("Track found: %s - %s (ID: %d)\n", trackInfo.Artist.Name, trackInfo.Title, trackInfo.ID) fmt.Printf("Track found: %s - %s (ID: %d)\n", trackInfo.Artist.Name, trackInfo.Title, trackInfo.ID)
// Prepare metadata // All metadata from Spotify - no fallback to Tidal
finalArtistName := artistName finalArtistName := artistName
finalTrackTitle := trackName finalTrackTitle := trackName
finalAlbumTitle := albumName finalAlbumTitle := albumName
if finalArtistName == "" {
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)
}
if len(artists) > 0 {
finalArtistName = strings.Join(artists, ", ")
} else {
finalArtistName = "Unknown Artist"
}
}
if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title
if finalTrackTitle == "" {
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
}
}
if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title
}
// Sanitize for filename only (not for metadata) // Sanitize for filename only (not for metadata)
finalArtistNameForFile := sanitizeFilename(finalArtistName) finalArtistNameForFile := sanitizeFilename(finalArtistName)
finalTrackTitleForFile := sanitizeFilename(finalTrackTitle) finalTrackTitleForFile := sanitizeFilename(finalTrackTitle)
// Check if file already exists // Check if file already exists (use Spotify ISRC)
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, spotifyISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", spotifyISRC, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, spotifyTrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1614,55 +1437,36 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
fmt.Println("Adding metadata...") fmt.Println("Adding metadata...")
coverPath := "" coverPath := ""
if trackInfo.Album.Cover != "" { // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify
if spotifyCoverURL != "" {
coverPath = outputFilename + ".cover.jpg" coverPath = outputFilename + ".cover.jpg"
albumArt, err := downloader.DownloadAlbumArt(trackInfo.Album.Cover) coverClient := NewCoverClient()
if err != nil { if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
fmt.Printf("Warning: Failed to download album art: %v\n", err) fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
coverPath = ""
} else { } else {
if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { defer os.Remove(coverPath)
fmt.Printf("Warning: Failed to save album art: %v\n", err) fmt.Println("Spotify cover downloaded")
} else {
defer os.Remove(coverPath)
fmt.Println("Album art downloaded")
}
} }
} }
trackNumberToEmbed := 0 // Determine track number to embed - ALL from Spotify
if position > 0 { trackNumberToEmbed := spotifyTrackNumber
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if position > 0 && !useAlbumTrackNumber {
trackNumberToEmbed = trackInfo.TrackNumber trackNumberToEmbed = position // Use playlist position
} 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
} }
// ALL metadata from Spotify
metadata := Metadata{ metadata := Metadata{
Title: finalTrackTitle, Title: finalTrackTitle,
Artist: finalArtistName, Artist: finalArtistName,
Album: finalAlbumTitle, Album: finalAlbumTitle,
AlbumArtist: albumArtist, AlbumArtist: albumArtist,
Date: year, // Recorded date (year only) Date: releaseDate, // Recorded date (full date YYYY-MM-DD)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify
ISRC: trackInfo.ISRC, DiscNumber: spotifyDiscNumber, // Disc number from Spotify
ISRC: spotifyISRC, // ISRC from Spotify
Description: "https://github.com/afkarxyz/SpotiFLAC", Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
@@ -1677,34 +1481,34 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate 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, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyISRC string) (string, error) {
// Get Tidal URL once // Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
// Songlink failed to find Tidal URL, try search fallback with all APIs // Songlink failed to find Tidal URL, try search fallback with all APIs
fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err)
fmt.Println("Trying Tidal search fallback with all APIs...") fmt.Println("Trying Tidal search fallback with all APIs...")
return t.DownloadBySearchWithFallback(spotifyTrackName, spotifyArtistName, spotifyAlbumName, "", 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearchWithFallback(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyISRC, 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks)
} }
// Use parallel API requests via DownloadByURLWithFallback // Use parallel API requests via DownloadByURLWithFallback
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyISRC)
} }
// DownloadWithFallbackAndISRC downloads with ISRC matching for search fallback // DownloadWithFallbackAndISRC downloads with ISRC matching for search fallback
// Uses parallel API requests for faster download // Uses parallel API requests for faster download
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) { func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, expectedDuration int, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int) (string, error) {
// Get Tidal URL once // Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
// Songlink failed to find Tidal URL, try search fallback with ISRC matching // Songlink failed to find Tidal URL, try search fallback with ISRC matching
fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err)
fmt.Println("Trying Tidal search fallback with ISRC matching...") fmt.Println("Trying Tidal search fallback with ISRC matching...")
return t.DownloadBySearchWithFallback(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearchWithFallback(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks)
} }
// Use parallel API requests via DownloadByURLWithFallback // Use parallel API requests via DownloadByURLWithFallback
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyISRC)
} }
func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
+1 -1
View File
@@ -39,7 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@types/node": "^25.0.1", "@types/node": "^25.0.2",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
+1 -1
View File
@@ -1 +1 @@
7d8d0f3230f9ffbfba312943439831fc d4b3974abd992c8ff941c6fde9f62062
+14 -14
View File
@@ -52,7 +52,7 @@ importers:
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.18(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2))
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@@ -85,8 +85,8 @@ importers:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2 version: 9.39.2
'@types/node': '@types/node':
specifier: ^25.0.1 specifier: ^25.0.2
version: 25.0.1 version: 25.0.2
'@types/react': '@types/react':
specifier: ^19.2.7 specifier: ^19.2.7
version: 19.2.7 version: 19.2.7
@@ -95,7 +95,7 @@ importers:
version: 19.2.3(@types/react@19.2.7) version: 19.2.3(@types/react@19.2.7)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.1.2(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)) version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2))
eslint: eslint:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2(jiti@2.6.1) version: 9.39.2(jiti@2.6.1)
@@ -122,7 +122,7 @@ importers:
version: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) version: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
vite: vite:
specifier: ^7.2.7 specifier: ^7.2.7
version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2) version: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)
packages: packages:
@@ -1289,8 +1289,8 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@25.0.1': '@types/node@25.0.2':
resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==} resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==}
'@types/react-dom@19.2.3': '@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
@@ -3054,12 +3054,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2))': '@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@tailwindcss/node': 4.1.18 '@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18 '@tailwindcss/oxide': 4.1.18
tailwindcss: 4.1.18 tailwindcss: 4.1.18
vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
dependencies: dependencies:
@@ -3086,7 +3086,7 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/node@25.0.1': '@types/node@25.0.2':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.16.0
@@ -3189,7 +3189,7 @@ snapshots:
'@typescript-eslint/types': 8.49.0 '@typescript-eslint/types': 8.49.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2))': '@vitejs/plugin-react@5.1.2(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
@@ -3197,7 +3197,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.53 '@rolldown/pluginutils': 1.0.0-beta.53
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
react-refresh: 0.18.0 react-refresh: 0.18.0
vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3820,7 +3820,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.7 '@types/react': 19.2.7
vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2): vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -3829,7 +3829,7 @@ snapshots:
rollup: 4.53.3 rollup: 4.53.3
tinyglobby: 0.2.15 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 25.0.1 '@types/node': 25.0.2
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
lightningcss: 1.30.2 lightningcss: 1.30.2
+1 -1
View File
@@ -51,7 +51,7 @@ interface AlbumInfoProps {
onSortChange: (value: string) => void; onSortChange: (value: string) => void;
onToggleTrack: (isrc: string) => void; onToggleTrack: (isrc: string) => void;
onToggleSelectAll: (tracks: TrackMetadata[]) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void;
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
onCheckAvailability?: (spotifyId: string) => void; onCheckAvailability?: (spotifyId: string) => void;
+1 -1
View File
@@ -56,7 +56,7 @@ interface ArtistInfoProps {
onSortChange: (value: string) => void; onSortChange: (value: string) => void;
onToggleTrack: (isrc: string) => void; onToggleTrack: (isrc: string) => void;
onToggleSelectAll: (tracks: TrackMetadata[]) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void;
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
onCheckAvailability?: (spotifyId: string) => void; onCheckAvailability?: (spotifyId: string) => void;
+1 -1
View File
@@ -55,7 +55,7 @@ interface PlaylistInfoProps {
onSortChange: (value: string) => void; onSortChange: (value: string) => void;
onToggleTrack: (isrc: string) => void; onToggleTrack: (isrc: string) => void;
onToggleSelectAll: (tracks: TrackMetadata[]) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void;
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
onCheckAvailability?: (spotifyId: string) => void; onCheckAvailability?: (spotifyId: string) => void;
+2 -2
View File
@@ -25,7 +25,7 @@ interface TrackInfoProps {
checkingAvailability?: boolean; checkingAvailability?: boolean;
availability?: TrackAvailability; availability?: TrackAvailability;
downloadingCover?: boolean; downloadingCover?: boolean;
onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string) => void; onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string) => void;
onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void;
@@ -120,7 +120,7 @@ export function TrackInfo({
{track.isrc && ( {track.isrc && (
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button <Button
onClick={() => onDownload(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, undefined, track.album_artist, track.release_date)} onClick={() => onDownload(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, undefined, track.duration_ms, track.track_number, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks)}
disabled={isDownloading || downloadingTrack === track.isrc} disabled={isDownloading || downloadingTrack === track.isrc}
> >
{downloadingTrack === track.isrc ? ( {downloadingTrack === track.isrc ? (
+2 -2
View File
@@ -49,7 +49,7 @@ interface TrackListProps {
downloadingCoverTrack?: string | null; downloadingCoverTrack?: string | null;
onToggleTrack: (isrc: string) => void; onToggleTrack: (isrc: string) => void;
onToggleSelectAll: (tracks: TrackMetadata[]) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void;
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string, coverUrl?: string, spotifyTrackNumber?: number, spotifyDiscNumber?: number, spotifyTotalTracks?: number) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
@@ -301,7 +301,7 @@ export function TrackList({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
onClick={() => onClick={() =>
onDownloadTrack(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date) onDownloadTrack(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1, track.album_artist, track.release_date, track.images, track.track_number, track.disc_number, track.total_tracks)
} }
size="sm" size="sm"
disabled={isDownloading || downloadingTrack === track.isrc} disabled={isDownloading || downloadingTrack === track.isrc}
+78 -20
View File
@@ -33,7 +33,11 @@ export function useDownload() {
durationMs?: number, durationMs?: number,
releaseYear?: string, releaseYear?: string,
albumArtist?: string, albumArtist?: string,
releaseDate?: string releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => { ) => {
const service = settings.downloader; const service = settings.downloader;
@@ -113,6 +117,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -125,6 +130,9 @@ export function useDownload() {
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, // Pass the same itemID through all attempts item_id: itemID, // Pass the same itemID through all attempts
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
if (tidalResponse.success) { if (tidalResponse.success) {
@@ -150,6 +158,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -160,6 +169,9 @@ export function useDownload() {
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.amazon_url, service_url: streamingURLs.amazon_url,
item_id: itemID, item_id: itemID,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
if (amazonResponse.success) { if (amazonResponse.success) {
@@ -183,18 +195,22 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
position, position,
use_album_track_number: useAlbumTrackNumber, use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined, duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID, item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
}); spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
});
// If Qobuz also failed, mark the item as failed // If Qobuz also failed, mark the item as failed
if (!qobuzResponse.success) { if (!qobuzResponse.success) {
@@ -226,6 +242,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -233,9 +250,13 @@ export function useDownload() {
use_album_track_number: useAlbumTrackNumber, use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, // Pass itemID for tracking item_id: itemID, // Pass itemID for tracking
audio_format: audioFormat, audio_format: audioFormat,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
// Mark as failed if download failed for single-service attempt // Mark as failed if download failed for single-service attempt
@@ -262,7 +283,11 @@ export function useDownload() {
isAlbum?: boolean, isAlbum?: boolean,
releaseYear?: string, releaseYear?: string,
albumArtist?: string, albumArtist?: string,
releaseDate?: string releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => { ) => {
const service = settings.downloader; const service = settings.downloader;
@@ -337,6 +362,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -349,6 +375,9 @@ export function useDownload() {
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, item_id: itemID,
audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode audio_format: settings.tidalQuality || "LOSSLESS", // Use default LOSSLESS for auto mode
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
if (tidalResponse.success) { if (tidalResponse.success) {
@@ -371,6 +400,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -381,6 +411,9 @@ export function useDownload() {
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.amazon_url, service_url: streamingURLs.amazon_url,
item_id: itemID, item_id: itemID,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
if (amazonResponse.success) { if (amazonResponse.success) {
@@ -401,18 +434,22 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
position, position,
use_album_track_number: useAlbumTrackNumber, use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined, duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID, item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
}); spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
});
// If Qobuz also failed, mark the item as failed // If Qobuz also failed, mark the item as failed
if (!qobuzResponse.success) { if (!qobuzResponse.success) {
@@ -443,6 +480,7 @@ export function useDownload() {
album_name: albumName, album_name: albumName,
album_artist: albumArtist, album_artist: albumArtist,
release_date: releaseDate, release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -450,9 +488,13 @@ export function useDownload() {
use_album_track_number: useAlbumTrackNumber, use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, item_id: itemID,
audio_format: audioFormat, audio_format: audioFormat,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
}); });
// Mark as failed if download failed for single-service attempt // Mark as failed if download failed for single-service attempt
@@ -474,7 +516,11 @@ export function useDownload() {
durationMs?: number, durationMs?: number,
position?: number, position?: number,
albumArtist?: string, albumArtist?: string,
releaseDate?: string releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => { ) => {
if (!isrc) { if (!isrc) {
toast.error("No ISRC found for this track"); toast.error("No ISRC found for this track");
@@ -502,7 +548,11 @@ export function useDownload() {
durationMs, durationMs,
releaseYear, releaseYear,
albumArtist || "", albumArtist || "",
releaseDate releaseDate,
coverUrl,
spotifyTrackNumber, // Spotify album track number
spotifyDiscNumber, // Spotify disc number
spotifyTotalTracks // Total tracks in album
); );
if (response.success) { if (response.success) {
@@ -603,7 +653,11 @@ export function useDownload() {
isAlbum, isAlbum,
releaseYear, releaseYear,
track?.album_artist || "", // Use album_artist from Spotify metadata track?.album_artist || "", // Use album_artist from Spotify metadata
track?.release_date track?.release_date,
track?.images, // Spotify cover URL
track?.track_number, // Spotify album track number
track?.disc_number, // Spotify disc number
track?.total_tracks // Total tracks in album
); );
if (response.success) { if (response.success) {
@@ -736,7 +790,11 @@ export function useDownload() {
isAlbum, isAlbum,
releaseYear, releaseYear,
track.album_artist || "", // Use album_artist from Spotify metadata track.album_artist || "", // Use album_artist from Spotify metadata
track.release_date track.release_date,
track.images, // Spotify cover URL
track.track_number, // Spotify album track number
track.disc_number, // Spotify disc number
track.total_tracks // Total tracks in album
); );
if (response.success) { if (response.success) {
+5
View File
@@ -13,6 +13,7 @@ export interface TrackMetadata {
images: string; images: string;
release_date: string; release_date: string;
track_number: number; track_number: number;
total_tracks?: number; // Total tracks in album
disc_number?: number; disc_number?: number;
external_urls: string; external_urls: string;
isrc: string; isrc: string;
@@ -118,6 +119,7 @@ export interface DownloadRequest {
album_name?: string; album_name?: string;
album_artist?: string; album_artist?: string;
release_date?: string; release_date?: string;
cover_url?: string; // Spotify cover URL for embedding
api_url?: string; api_url?: string;
output_dir?: string; output_dir?: string;
audio_format?: string; audio_format?: string;
@@ -132,6 +134,9 @@ export interface DownloadRequest {
service_url?: string; service_url?: string;
duration?: number; // Track duration in seconds for better matching duration?: number; // Track duration in seconds for better matching
item_id?: string; // Optional queue item ID for multi-service fallback tracking item_id?: string; // Optional queue item ID for multi-service fallback tracking
spotify_track_number?: number; // Track number from Spotify album
spotify_disc_number?: number; // Disc number from Spotify album
spotify_total_tracks?: number; // Total tracks in album from Spotify
} }
export interface DownloadResponse { export interface DownloadResponse {