From b85ed89af34e0cde70a02dc082778b4389398565 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Sun, 14 Dec 2025 15:35:11 +0700 Subject: [PATCH] v6.8 --- app.go | 18 +- backend/amazon.go | 57 +++- backend/cover.go | 40 +++ backend/metadata.go | 20 +- backend/qobuz.go | 84 ++--- backend/spotify_metadata.go | 6 + backend/tidal.go | 400 ++++++----------------- frontend/package.json | 2 +- frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 28 +- frontend/src/components/AlbumInfo.tsx | 2 +- frontend/src/components/ArtistInfo.tsx | 2 +- frontend/src/components/PlaylistInfo.tsx | 2 +- frontend/src/components/TrackInfo.tsx | 4 +- frontend/src/components/TrackList.tsx | 4 +- frontend/src/hooks/useDownload.ts | 98 ++++-- frontend/src/types/api.ts | 5 + 17 files changed, 353 insertions(+), 421 deletions(-) diff --git a/app.go b/app.go index aaec347..5a91f30 100644 --- a/app.go +++ b/app.go @@ -45,6 +45,7 @@ type DownloadRequest struct { AlbumName string `json:"album_name,omitempty"` AlbumArtist string `json:"album_artist,omitempty"` ReleaseDate string `json:"release_date,omitempty"` + CoverURL string `json:"cover_url,omitempty"` // Spotify cover URL for embedding ApiURL string `json:"api_url,omitempty"` OutputDir string `json:"output_dir,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 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 + 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 @@ -211,7 +215,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { downloader := backend.NewAmazonDownloader() if req.ServiceURL != "" { // Use provided URL directly - filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber) + 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 { if req.SpotifyID == "" { return DownloadResponse{ @@ -219,7 +223,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { Error: "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": @@ -227,7 +231,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { downloader := backend.NewTidalDownloader("") if req.ServiceURL != "" { // Use provided URL directly with fallback to multiple APIs - filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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 { if req.SpotifyID == "" { return DownloadResponse{ @@ -236,13 +240,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { }, fmt.Errorf("spotify ID is required for Tidal") } // Use ISRC matching for search fallback - filename, err = downloader.DownloadWithFallbackAndISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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 { downloader := backend.NewTidalDownloader(req.ApiURL) if req.ServiceURL != "" { // Use provided URL directly with specific API - filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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 { if req.SpotifyID == "" { return DownloadResponse{ @@ -251,7 +255,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { }, fmt.Errorf("spotify ID is required for Tidal") } // Use ISRC matching for search fallback - filename, err = downloader.DownloadWithISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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 == "" { 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: return DownloadResponse{ diff --git a/backend/amazon.go b/backend/amazon.go index 74bb27d..7b13c3a 100644 --- a/backend/amazon.go +++ b/backend/amazon.go @@ -372,7 +372,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (str 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 if outputDir != "." { 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) if spotifyTrackName != "" && spotifyArtistName != "" { - expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) + expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, false) expectedPath := filepath.Join(outputDir, expectedFilename) 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 } - // File already has embedded metadata, just rename if needed + // Rename file based on Spotify metadata if spotifyTrackName != "" && spotifyArtistName != "" { safeArtist := sanitizeFilename(spotifyArtistName) 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("✓ Downloaded successfully from Amazon Music") 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 amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID) if err != nil { 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) } diff --git a/backend/cover.go b/backend/cover.go index d051c7f..5e4425d 100644 --- a/backend/cover.go +++ b/backend/cover.go @@ -107,6 +107,46 @@ func (c *CoverClient) getMaxResolutionURL(coverURL string) string { 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 func (c *CoverClient) DownloadCover(req CoverDownloadRequest) (*CoverDownloadResponse, error) { if req.CoverURL == "" { diff --git a/backend/metadata.go b/backend/metadata.go index 9ca7172..9b49d64 100644 --- a/backend/metadata.go +++ b/backend/metadata.go @@ -19,9 +19,10 @@ type Metadata struct { Artist string Album string AlbumArtist string - Date string // Recorded date (year only) - ReleaseDate string // Release date (full date) + Date string // Recorded date (full date YYYY-MM-DD) + ReleaseDate string // Release date (full date) - kept for compatibility TrackNumber int + TotalTracks int // Total tracks in album DiscNumber int ISRC string Lyrics string @@ -62,6 +63,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error { if metadata.TrackNumber > 0 { _ = cmt.Add(flacvorbis.FIELD_TRACKNUMBER, strconv.Itoa(metadata.TrackNumber)) } + if metadata.TotalTracks > 0 { + _ = cmt.Add("TOTALTRACKS", strconv.Itoa(metadata.TotalTracks)) + } if metadata.DiscNumber > 0 { _ = 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 func ExtractCoverArt(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) - + switch ext { case ".mp3": return extractCoverFromMp3(filePath) @@ -324,7 +328,7 @@ func extractCoverFromMp3(filePath string) (string, error) { // extractCoverFromM4AOrFlac extracts cover art from M4A or FLAC file func extractCoverFromM4AOrFlac(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) - + if ext == ".flac" { f, err := flac.ParseFile(filePath) if err != nil { @@ -364,7 +368,7 @@ func extractCoverFromM4AOrFlac(filePath string) (string, error) { // ExtractLyrics extracts lyrics from an audio file func ExtractLyrics(filePath string) (string, error) { ext := strings.ToLower(pathfilepath.Ext(filePath)) - + switch ext { case ".mp3": return extractLyricsFromMp3(filePath) @@ -447,7 +451,7 @@ func EmbedCoverArtOnly(filePath string, coverPath string) error { } ext := strings.ToLower(pathfilepath.Ext(filePath)) - + switch ext { case ".mp3": return embedCoverToMp3(filePath, coverPath) @@ -500,7 +504,7 @@ func EmbedLyricsOnlyMP3(filepath string, lyrics string) error { if lyrics == "" { return nil } - + tag, err := id3v2.Open(filepath, id3v2.Options{Parse: true}) if err != nil { return fmt.Errorf("failed to open MP3 file: %w", err) @@ -583,7 +587,7 @@ func EmbedLyricsOnlyUniversal(filepath string, lyrics string) error { if lyrics == "" { return nil } - + ext := strings.ToLower(pathfilepath.Ext(filepath)) switch ext { case ".mp3": diff --git a/backend/qobuz.go b/backend/qobuz.go index df40451..905aec3 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -307,7 +307,7 @@ func buildQobuzFilename(title, artist string, trackNumber int, format string, in return filename + ".flac" } -func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, 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) // Create output directory if it doesn't exist @@ -322,29 +322,11 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma return "", err } - // Use Spotify metadata if provided, otherwise fallback to Qobuz metadata + // All metadata from Spotify - no fallback to Qobuz artists := spotifyArtistName trackTitle := spotifyTrackName albumTitle := spotifyAlbumName - if artists == "" { - artists = track.Performer.Name - if track.Album.Artist.Name != "" { - artists = track.Album.Artist.Name - } - } - - if trackTitle == "" { - trackTitle = track.Title - if track.Version != "" && track.Version != "null" { - trackTitle = fmt.Sprintf("%s (%s)", track.Title, track.Version) - } - } - - if albumTitle == "" { - albumTitle = track.Album.Title - } - fmt.Printf("Found track: %s - %s\n", artists, trackTitle) fmt.Printf("Album: %s\n", albumTitle) @@ -374,14 +356,14 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma safeArtist := sanitizeFilename(artists) safeTitle := sanitizeFilename(trackTitle) - // Check if file with same ISRC already exists - if existingFile, exists := CheckISRCExists(outputDir, track.ISRC); exists { - fmt.Printf("File with ISRC %s already exists: %s\n", track.ISRC, existingFile) + // Check if file with same ISRC already exists (use Spotify ISRC) + if existingFile, exists := CheckISRCExists(outputDir, isrc); exists { + fmt.Printf("File with ISRC %s already exists: %s\n", isrc, existingFile) return "EXISTS:" + existingFile, nil } - // Build filename based on format settings - filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) + // Build filename based on format settings (use Spotify track number) + filename := buildQobuzFilename(safeTitle, safeArtist, spotifyTrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filepath := filepath.Join(outputDir, filename) 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) 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" - fmt.Println("Downloading cover art...") - if err := q.DownloadCoverArt(track.Album.Image.Large, coverPath); err != nil { - fmt.Printf("Warning: Failed to download cover art: %v\n", err) + 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") } } fmt.Println("Embedding metadata and cover art...") - // Use album track number if in album folder structure, otherwise use playlist position - trackNumberToEmbed := 0 - if position > 0 { - if useAlbumTrackNumber && track.TrackNumber > 0 { - 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 + // Determine track number to embed - ALL from Spotify + trackNumberToEmbed := spotifyTrackNumber + if position > 0 && !useAlbumTrackNumber { + trackNumberToEmbed = position // Use playlist position } + // ALL metadata from Spotify metadata := Metadata{ Title: trackTitle, Artist: artists, Album: albumTitle, - AlbumArtist: finalAlbumArtist, - Date: year, // Recorded date (year only) - ReleaseDate: finalReleaseDate, // Release date (full date) + AlbumArtist: spotifyAlbumArtist, + Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD) TrackNumber: trackNumberToEmbed, - DiscNumber: track.MediaNumber, - ISRC: track.ISRC, + TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify + DiscNumber: spotifyDiscNumber, // Disc number from Spotify + ISRC: isrc, // ISRC from Spotify (passed as parameter) Description: "https://github.com/afkarxyz/SpotiFLAC", } diff --git a/backend/spotify_metadata.go b/backend/spotify_metadata.go index c628741..2027e04 100644 --- a/backend/spotify_metadata.go +++ b/backend/spotify_metadata.go @@ -66,6 +66,7 @@ type TrackMetadata struct { Images string `json:"images"` ReleaseDate string `json:"release_date"` TrackNumber int `json:"track_number"` + TotalTracks int `json:"total_tracks,omitempty"` DiscNumber int `json:"disc_number,omitempty"` ExternalURL string `json:"external_urls"` ISRC string `json:"isrc"` @@ -89,6 +90,7 @@ type AlbumTrackMetadata struct { Images string `json:"images"` ReleaseDate string `json:"release_date"` TrackNumber int `json:"track_number"` + TotalTracks int `json:"total_tracks,omitempty"` DiscNumber int `json:"disc_number,omitempty"` ExternalURL string `json:"external_urls"` 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), ReleaseDate: item.Track.Album.ReleaseDate, TrackNumber: item.Track.TrackNumber, + TotalTracks: item.Track.Album.TotalTracks, DiscNumber: item.Track.DiscNumber, ExternalURL: item.Track.ExternalURL.Spotify, ISRC: item.Track.ExternalID.ISRC, @@ -568,6 +571,7 @@ func (c *SpotifyMetadataClient) formatAlbumData(ctx context.Context, raw *albumR Images: albumImage, ReleaseDate: raw.Data.ReleaseDate, TrackNumber: item.TrackNumber, + TotalTracks: raw.Data.TotalTracks, DiscNumber: item.DiscNumber, ExternalURL: item.ExternalURL.Spotify, ISRC: isrc, @@ -649,6 +653,7 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, Images: albumImage, ReleaseDate: alb.ReleaseDate, TrackNumber: tr.TrackNumber, + TotalTracks: alb.TotalTracks, DiscNumber: tr.DiscNumber, ExternalURL: tr.ExternalURL.Spotify, ISRC: isrc, @@ -697,6 +702,7 @@ func formatTrackData(raw *trackFull) TrackResponse { Images: firstImageURL(raw.Album.Images), ReleaseDate: raw.Album.ReleaseDate, TrackNumber: raw.TrackNumber, + TotalTracks: raw.Album.TotalTracks, DiscNumber: raw.DiscNumber, ExternalURL: raw.ExternalURL.Spotify, ISRC: raw.ExternalID.ISRC, diff --git a/backend/tidal.go b/backend/tidal.go index a4b032f..3b72f88 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -801,7 +801,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e return nil } -func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, 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 err := os.MkdirAll(outputDir, 0755); err != nil { 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") } - // Use Spotify metadata if provided, otherwise fallback to Tidal metadata + // All metadata from Spotify - no fallback to Tidal artistName := spotifyArtistName trackTitle := spotifyTrackName 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) artistNameForFile := sanitizeFilename(artistName) trackTitleForFile := sanitizeFilename(trackTitle) @@ -892,59 +863,38 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo fmt.Println("Adding metadata...") coverPath := "" - if trackInfo.Album.Cover != "" { + // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify + if spotifyCoverURL != "" { coverPath = outputFilename + ".cover.jpg" - albumArt, err := t.DownloadAlbumArt(trackInfo.Album.Cover) - if err != nil { - fmt.Printf("Warning: Failed to download album art: %v\n", err) + coverClient := NewCoverClient() + if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { + fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) + coverPath = "" } else { - if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { - fmt.Printf("Warning: Failed to save album art: %v\n", err) - } else { - defer os.Remove(coverPath) - fmt.Println("Album art downloaded") - } + defer os.Remove(coverPath) + fmt.Println("Spotify cover downloaded") } } - // Use album track number if in album folder structure, otherwise use playlist position - trackNumberToEmbed := 0 - if position > 0 { - if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { - trackNumberToEmbed = trackInfo.TrackNumber - } 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 + // Determine track number to embed - ALL from Spotify + // - If position > 0 and !useAlbumTrackNumber: use playlist position + // - Otherwise: use Spotify track number + trackNumberToEmbed := spotifyTrackNumber + if position > 0 && !useAlbumTrackNumber { + trackNumberToEmbed = position // Use playlist position } + // ALL metadata from Spotify metadata := Metadata{ Title: trackTitle, Artist: artistName, Album: albumTitle, - AlbumArtist: finalAlbumArtist, - Date: year, // Recorded date (year only) - ReleaseDate: finalReleaseDate, // Release date (full date) + AlbumArtist: spotifyAlbumArtist, + Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD) TrackNumber: trackNumberToEmbed, - DiscNumber: trackInfo.VolumeNumber, - ISRC: trackInfo.ISRC, + TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify + DiscNumber: spotifyDiscNumber, // Disc number from Spotify + ISRC: spotifyISRC, // ISRC from Spotify Description: "https://github.com/afkarxyz/SpotiFLAC", } @@ -959,7 +909,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo return outputFilename, nil } -func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, 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() if err != nil { 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") } - // Use Spotify metadata if provided, otherwise fallback to Tidal metadata + // All metadata from Spotify - no fallback to Tidal artistName := spotifyArtistName trackTitle := spotifyTrackName 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) artistNameForFile := sanitizeFilename(artistName) trackTitleForFile := sanitizeFilename(trackTitle) @@ -1056,58 +978,36 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality fmt.Println("Adding metadata...") coverPath := "" - if trackInfo.Album.Cover != "" { + // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify + if spotifyCoverURL != "" { coverPath = outputFilename + ".cover.jpg" - albumArt, err := downloader.DownloadAlbumArt(trackInfo.Album.Cover) - if err != nil { - fmt.Printf("Warning: Failed to download album art: %v\n", err) + coverClient := NewCoverClient() + if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { + fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) + coverPath = "" } else { - if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { - fmt.Printf("Warning: Failed to save album art: %v\n", err) - } else { - defer os.Remove(coverPath) - fmt.Println("Album art downloaded") - } + defer os.Remove(coverPath) + fmt.Println("Spotify cover downloaded") } } - trackNumberToEmbed := 0 - if position > 0 { - if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { - trackNumberToEmbed = trackInfo.TrackNumber - } 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 + // Determine track number to embed - ALL from Spotify + trackNumberToEmbed := spotifyTrackNumber + if position > 0 && !useAlbumTrackNumber { + trackNumberToEmbed = position // Use playlist position } + // ALL metadata from Spotify metadata := Metadata{ Title: trackTitle, Artist: artistName, Album: albumTitle, - AlbumArtist: finalAlbumArtist, - Date: year, // Recorded date (year only) - ReleaseDate: finalReleaseDate, // Release date (full date) + AlbumArtist: spotifyAlbumArtist, + Date: spotifyReleaseDate, // Recorded date (full date YYYY-MM-DD) TrackNumber: trackNumberToEmbed, - DiscNumber: trackInfo.VolumeNumber, - ISRC: trackInfo.ISRC, + TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify + DiscNumber: spotifyDiscNumber, // Disc number from Spotify + ISRC: spotifyISRC, // ISRC from Spotify Description: "https://github.com/afkarxyz/SpotiFLAC", } @@ -1122,41 +1022,41 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality return outputFilename, nil } -func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, 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 tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) if err != nil { // Songlink failed to find Tidal URL, try search fallback fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) 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 -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 tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) if err != nil { // Songlink failed to find Tidal URL, try search fallback with ISRC fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) 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 // 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) { - return t.DownloadBySearchWithISRC(trackName, artistName, albumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) +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, 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 -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 err := os.MkdirAll(outputDir, 0755); err != nil { 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") } - // Use provided metadata, fallback to Tidal metadata + // All metadata from Spotify - no fallback to Tidal finalArtistName := artistName finalTrackTitle := trackName 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) finalArtistNameForFile := sanitizeFilename(finalArtistName) finalTrackTitleForFile := sanitizeFilename(finalTrackTitle) - // Check if file with same ISRC already exists - if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { - fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) + // Check if file with same ISRC already exists (use Spotify ISRC) + if existingFile, exists := CheckISRCExists(outputDir, spotifyISRC); exists { + fmt.Printf("File with ISRC %s already exists: %s\n", spotifyISRC, existingFile) return "EXISTS:" + existingFile, nil } // 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) 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...") coverPath := "" - if trackInfo.Album.Cover != "" { + // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify + if spotifyCoverURL != "" { coverPath = outputFilename + ".cover.jpg" - albumArt, err := t.DownloadAlbumArt(trackInfo.Album.Cover) - if err != nil { - fmt.Printf("Warning: Failed to download album art: %v\n", err) + coverClient := NewCoverClient() + if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { + fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) + coverPath = "" } else { - if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { - fmt.Printf("Warning: Failed to save album art: %v\n", err) - } else { - defer os.Remove(coverPath) - fmt.Println("Album art downloaded") - } + defer os.Remove(coverPath) + fmt.Println("Spotify cover downloaded") } } - trackNumberToEmbed := 0 - if position > 0 { - if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { - trackNumberToEmbed = trackInfo.TrackNumber - } 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 + // Determine track number to embed - ALL from Spotify + trackNumberToEmbed := spotifyTrackNumber + if position > 0 && !useAlbumTrackNumber { + trackNumberToEmbed = position // Use playlist position } + // ALL metadata from Spotify metadata := Metadata{ Title: finalTrackTitle, Artist: finalArtistName, Album: finalAlbumTitle, AlbumArtist: albumArtist, - Date: year, // Recorded date (year only) - ReleaseDate: finalReleaseDate, // Release date (full date) + Date: releaseDate, // Recorded date (full date YYYY-MM-DD) TrackNumber: trackNumberToEmbed, - DiscNumber: trackInfo.VolumeNumber, - ISRC: trackInfo.ISRC, + TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify + DiscNumber: spotifyDiscNumber, // Disc number from Spotify + ISRC: spotifyISRC, // ISRC from Spotify 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 // 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() if err != nil { 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) - // Prepare metadata + // All metadata from Spotify - no fallback to Tidal finalArtistName := artistName finalTrackTitle := trackName 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) finalArtistNameForFile := sanitizeFilename(finalArtistName) finalTrackTitleForFile := sanitizeFilename(finalTrackTitle) - // Check if file already exists - if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { - fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) + // Check if file already exists (use Spotify ISRC) + if existingFile, exists := CheckISRCExists(outputDir, spotifyISRC); exists { + fmt.Printf("File with ISRC %s already exists: %s\n", spotifyISRC, existingFile) 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) 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...") coverPath := "" - if trackInfo.Album.Cover != "" { + // Use Spotify cover URL (with max resolution if enabled) - all metadata from Spotify + if spotifyCoverURL != "" { coverPath = outputFilename + ".cover.jpg" - albumArt, err := downloader.DownloadAlbumArt(trackInfo.Album.Cover) - if err != nil { - fmt.Printf("Warning: Failed to download album art: %v\n", err) + coverClient := NewCoverClient() + if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil { + fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err) + coverPath = "" } else { - if err := os.WriteFile(coverPath, albumArt, 0644); err != nil { - fmt.Printf("Warning: Failed to save album art: %v\n", err) - } else { - defer os.Remove(coverPath) - fmt.Println("Album art downloaded") - } + defer os.Remove(coverPath) + fmt.Println("Spotify cover downloaded") } } - trackNumberToEmbed := 0 - if position > 0 { - if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { - trackNumberToEmbed = trackInfo.TrackNumber - } 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 + // Determine track number to embed - ALL from Spotify + trackNumberToEmbed := spotifyTrackNumber + if position > 0 && !useAlbumTrackNumber { + trackNumberToEmbed = position // Use playlist position } + // ALL metadata from Spotify metadata := Metadata{ Title: finalTrackTitle, Artist: finalArtistName, Album: finalAlbumTitle, AlbumArtist: albumArtist, - Date: year, // Recorded date (year only) - ReleaseDate: finalReleaseDate, // Release date (full date) + Date: releaseDate, // Recorded date (full date YYYY-MM-DD) TrackNumber: trackNumberToEmbed, - DiscNumber: trackInfo.VolumeNumber, - ISRC: trackInfo.ISRC, + TotalTracks: spotifyTotalTracks, // Total tracks in album from Spotify + DiscNumber: spotifyDiscNumber, // Disc number from Spotify + ISRC: spotifyISRC, // ISRC from Spotify Description: "https://github.com/afkarxyz/SpotiFLAC", } @@ -1677,34 +1481,34 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al return outputFilename, nil } -func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, 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 tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) if err != nil { // Songlink failed to find Tidal URL, try search fallback with all APIs fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) 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 - 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 // 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 tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) if err != nil { // Songlink failed to find Tidal URL, try search fallback with ISRC matching fmt.Printf("Songlink couldn't find Tidal URL: %v\n", err) 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 - 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 { diff --git a/frontend/package.json b/frontend/package.json index 3582c46..74ee19b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", - "@types/node": "^25.0.1", + "@types/node": "^25.0.2", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.2", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 14288a5..a2d85c8 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -7d8d0f3230f9ffbfba312943439831fc \ No newline at end of file +d4b3974abd992c8ff941c6fde9f62062 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7c8b143..ee254ee 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -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) '@tailwindcss/vite': 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: specifier: ^0.7.1 version: 0.7.1 @@ -85,8 +85,8 @@ importers: specifier: ^9.39.2 version: 9.39.2 '@types/node': - specifier: ^25.0.1 - version: 25.0.1 + specifier: ^25.0.2 + version: 25.0.2 '@types/react': specifier: ^19.2.7 version: 19.2.7 @@ -95,7 +95,7 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': 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: specifier: ^9.39.2 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) vite: 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: @@ -1289,8 +1289,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.0.1': - resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==} + '@types/node@25.0.2': + resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==} '@types/react-dom@19.2.3': 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-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: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 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': dependencies: @@ -3086,7 +3086,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.0.1': + '@types/node@25.0.2': dependencies: undici-types: 7.16.0 @@ -3189,7 +3189,7 @@ snapshots: '@typescript-eslint/types': 8.49.0 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: '@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 '@types/babel__core': 7.20.5 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: - supports-color @@ -3820,7 +3820,7 @@ snapshots: optionalDependencies: '@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: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -3829,7 +3829,7 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.2 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 diff --git a/frontend/src/components/AlbumInfo.tsx b/frontend/src/components/AlbumInfo.tsx index cd26b5c..e4f0ab1 100644 --- a/frontend/src/components/AlbumInfo.tsx +++ b/frontend/src/components/AlbumInfo.tsx @@ -51,7 +51,7 @@ interface AlbumInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => 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; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/ArtistInfo.tsx b/frontend/src/components/ArtistInfo.tsx index 9db7d38..bebc1d2 100644 --- a/frontend/src/components/ArtistInfo.tsx +++ b/frontend/src/components/ArtistInfo.tsx @@ -56,7 +56,7 @@ interface ArtistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => 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; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/PlaylistInfo.tsx b/frontend/src/components/PlaylistInfo.tsx index bc5f1b2..28ec179 100644 --- a/frontend/src/components/PlaylistInfo.tsx +++ b/frontend/src/components/PlaylistInfo.tsx @@ -55,7 +55,7 @@ interface PlaylistInfoProps { onSortChange: (value: string) => void; onToggleTrack: (isrc: string) => 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; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onCheckAvailability?: (spotifyId: string) => void; diff --git a/frontend/src/components/TrackInfo.tsx b/frontend/src/components/TrackInfo.tsx index d678571..7122304 100644 --- a/frontend/src/components/TrackInfo.tsx +++ b/frontend/src/components/TrackInfo.tsx @@ -25,7 +25,7 @@ interface TrackInfoProps { checkingAvailability?: boolean; availability?: TrackAvailability; 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; onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void; @@ -120,7 +120,7 @@ export function TrackInfo({ {track.isrc && (