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"`
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{
+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)
}
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)
}
+40
View File
@@ -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 == "" {
+6 -2
View File
@@ -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))
}
+24 -60
View File
@@ -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",
}
+6
View File
@@ -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,
+102 -298
View File
@@ -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 {
+1 -1
View File
@@ -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",
+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)
'@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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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 && (
<div className="flex gap-2 flex-wrap">
<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}
>
{downloadingTrack === track.isrc ? (
+2 -2
View File
@@ -49,7 +49,7 @@ interface TrackListProps {
downloadingCoverTrack?: string | null;
onToggleTrack: (isrc: string) => 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;
onCheckAvailability?: (spotifyId: string, isrc?: 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>
<Button
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"
disabled={isDownloading || downloadingTrack === track.isrc}
+78 -20
View File
@@ -33,7 +33,11 @@ export function useDownload() {
durationMs?: number,
releaseYear?: string,
albumArtist?: string,
releaseDate?: string
releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => {
const service = settings.downloader;
@@ -113,6 +117,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -125,6 +130,9 @@ export function useDownload() {
duration: durationSeconds,
item_id: itemID, // Pass the same itemID through all attempts
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) {
@@ -150,6 +158,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -160,6 +169,9 @@ export function useDownload() {
embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.amazon_url,
item_id: itemID,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
});
if (amazonResponse.success) {
@@ -183,18 +195,22 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
position,
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
});
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
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 (!qobuzResponse.success) {
@@ -226,6 +242,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -233,9 +250,13 @@ export function useDownload() {
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSecondsForFallback,
item_id: itemID, // Pass itemID for tracking
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
@@ -262,7 +283,11 @@ export function useDownload() {
isAlbum?: boolean,
releaseYear?: string,
albumArtist?: string,
releaseDate?: string
releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => {
const service = settings.downloader;
@@ -337,6 +362,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -349,6 +375,9 @@ export function useDownload() {
duration: durationSeconds,
item_id: itemID,
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) {
@@ -371,6 +400,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -381,6 +411,9 @@ export function useDownload() {
embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.amazon_url,
item_id: itemID,
spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks,
});
if (amazonResponse.success) {
@@ -401,18 +434,22 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
position,
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
audio_format: settings.qobuzQuality || "6", // Use default 6 (16-bit) for auto mode
});
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationMs ? Math.round(durationMs / 1000) : undefined,
item_id: itemID,
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 (!qobuzResponse.success) {
@@ -443,6 +480,7 @@ export function useDownload() {
album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
cover_url: coverUrl,
output_dir: outputDir,
filename_format: settings.filenameTemplate,
track_number: settings.trackNumber,
@@ -450,9 +488,13 @@ export function useDownload() {
use_album_track_number: useAlbumTrackNumber,
spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover,
duration: durationSecondsForFallback,
item_id: itemID,
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
@@ -474,7 +516,11 @@ export function useDownload() {
durationMs?: number,
position?: number,
albumArtist?: string,
releaseDate?: string
releaseDate?: string,
coverUrl?: string,
spotifyTrackNumber?: number,
spotifyDiscNumber?: number,
spotifyTotalTracks?: number
) => {
if (!isrc) {
toast.error("No ISRC found for this track");
@@ -502,7 +548,11 @@ export function useDownload() {
durationMs,
releaseYear,
albumArtist || "",
releaseDate
releaseDate,
coverUrl,
spotifyTrackNumber, // Spotify album track number
spotifyDiscNumber, // Spotify disc number
spotifyTotalTracks // Total tracks in album
);
if (response.success) {
@@ -603,7 +653,11 @@ export function useDownload() {
isAlbum,
releaseYear,
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) {
@@ -736,7 +790,11 @@ export function useDownload() {
isAlbum,
releaseYear,
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) {
+5
View File
@@ -13,6 +13,7 @@ export interface TrackMetadata {
images: string;
release_date: string;
track_number: number;
total_tracks?: number; // Total tracks in album
disc_number?: number;
external_urls: string;
isrc: string;
@@ -118,6 +119,7 @@ export interface DownloadRequest {
album_name?: string;
album_artist?: string;
release_date?: string;
cover_url?: string; // Spotify cover URL for embedding
api_url?: string;
output_dir?: string;
audio_format?: string;
@@ -132,6 +134,9 @@ export interface DownloadRequest {
service_url?: string;
duration?: number; // Track duration in seconds for better matching
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 {