diff --git a/app.go b/app.go index bbe73cd..4dc9be4 100644 --- a/app.go +++ b/app.go @@ -307,6 +307,7 @@ type DownloadRequest struct { ReleaseDate string `json:"release_date,omitempty"` CoverURL string `json:"cover_url,omitempty"` TidalAPIURL string `json:"tidal_api_url,omitempty"` + TidalVariant string `json:"tidal_variant,omitempty"` OutputDir string `json:"output_dir,omitempty"` AudioFormat string `json:"audio_format,omitempty"` FilenameFormat string `json:"filename_format,omitempty"` @@ -661,7 +662,11 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { } case "tidal": - if req.TidalAPIURL == "" || req.TidalAPIURL == "auto" { + tidalVariant := strings.ToLower(strings.TrimSpace(req.TidalVariant)) + if tidalVariant == "alt" { + downloader := backend.NewTidalDownloader("") + filename, err = downloader.DownloadAlt(req.SpotifyID, req.OutputDir, 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.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre) + } else if req.TidalAPIURL == "" || req.TidalAPIURL == "auto" { downloader := backend.NewTidalDownloader("") if req.ServiceURL != "" { 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.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre) @@ -789,6 +794,11 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { backend.CompleteDownloadItem(itemID, filename, 0) } + historySource := req.Service + if req.Service == "tidal" && strings.EqualFold(strings.TrimSpace(req.TidalVariant), "alt") { + historySource = "tidal alt" + } + go func(fPath, track, artist, album, sID, cover, format, source string) { time.Sleep(2 * time.Second) @@ -834,7 +844,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { } backend.AddHistoryItem(item, "SpotiFLAC") - }(filename, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID, req.CoverURL, req.AudioFormat, req.Service) + }(filename, req.TrackName, req.ArtistName, req.AlbumName, req.SpotifyID, req.CoverURL, req.AudioFormat, historySource) } return DownloadResponse{ diff --git a/backend/amazon.go b/backend/amazon.go index 20f5e0a..51ecd33 100644 --- a/backend/amazon.go +++ b/backend/amazon.go @@ -288,6 +288,16 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename mbMeta = result.Metadata } + upc := "" + if spotifyURL != "" { + if identifiers, err := GetSpotifyTrackIdentifiersDirect(spotifyURL); err == nil || identifiers.ISRC != "" || identifiers.UPC != "" { + if strings.TrimSpace(isrc) == "" && strings.TrimSpace(identifiers.ISRC) != "" { + isrc = strings.TrimSpace(identifiers.ISRC) + } + upc = strings.TrimSpace(identifiers.UPC) + } + } + originalFileDir := filepath.Dir(filePath) originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) @@ -407,6 +417,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename Separator: metadataSeparator, Description: "https://github.com/spotbye/SpotiFLAC", ISRC: isrc, + UPC: upc, Genre: mbMeta.Genre, } diff --git a/backend/tidal.go b/backend/tidal.go index 33643de..a482058 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -9,7 +9,6 @@ import ( "net/http" "os" "os/exec" - "path/filepath" "regexp" "strings" "time" @@ -400,12 +399,6 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e } 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, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) { - if outputDir != "." { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", fmt.Errorf("directory error: %w", err) - } - } - fmt.Printf("Using Tidal URL: %s\n", tidalURL) trackID, err := t.GetTrackIDFromURL(tidalURL) @@ -417,25 +410,10 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo return "", fmt.Errorf("no track ID found") } - artistName := spotifyArtistName - trackTitle := spotifyTrackName - albumTitle := spotifyAlbumName - - artistNameForFile := sanitizeFilename(artistName) - albumArtistForFile := sanitizeFilename(spotifyAlbumArtist) - - if useFirstArtistOnly { - artistNameForFile = sanitizeFilename(GetFirstArtist(artistName)) - albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist)) + outputFilename, alreadyExists, err := buildTidalOutputPath(outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyTrackNumber, spotifyDiscNumber, isrcOverride, useFirstArtistOnly) + if err != nil { + return "", err } - - trackTitleForFile := sanitizeFilename(trackTitle) - albumTitleForFile := sanitizeFilename(albumTitle) - - filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrcOverride) - outputFilename := filepath.Join(outputDir, filename) - - outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting()) if alreadyExists { fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024)) return "EXISTS:" + outputFilename, nil @@ -447,56 +425,17 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo fmt.Println("⚠ HI_RES unavailable/failed, falling back to LOSSLESS...") downloadURL, err = t.GetDownloadURL(trackID, "LOSSLESS") if err != nil { - return "", fmt.Errorf("failed to get download URL (HI_RES & LOSSLESS both failed): %w", err) + return outputFilename, fmt.Errorf("failed to get download URL (HI_RES & LOSSLESS both failed): %w", err) } } else { - return "", err + return outputFilename, err } } - type mbResult struct { - ISRC string - Metadata Metadata - } - - metaChan := make(chan mbResult, 1) - if embedGenre && spotifyURL != "" { - go func() { - res := mbResult{} - var isrc string - parts := strings.Split(spotifyURL, "/") - if len(parts) > 0 { - sID := strings.Split(parts[len(parts)-1], "?")[0] - if sID != "" { - client := NewSongLinkClient() - if val, err := client.GetISRC(sID); err == nil { - isrc = val - } - } - } - res.ISRC = isrc - if isrc != "" { - if ShouldSkipMusicBrainzMetadataFetch() { - fmt.Println("Skipping MusicBrainz metadata fetch because status check is offline.") - } else { - fmt.Println("Fetching MusicBrainz metadata...") - if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, trackTitle, artistName, albumTitle, useSingleGenre, embedGenre); err == nil { - res.Metadata = fetchedMeta - fmt.Println("✓ MusicBrainz metadata fetched") - } else { - fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err) - } - } - } - metaChan <- res - }() - } else { - close(metaChan) - } - fmt.Printf("Downloading to: %s\n", outputFilename) if err := t.DownloadFile(downloadURL, outputFilename); err != nil { - return "", err + cleanupTidalDownloadArtifacts(outputFilename) + return outputFilename, err } if t.apiURL != "" { if err := RememberTidalAPIUsage(t.apiURL); err != nil { @@ -504,63 +443,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo } } - isrc := strings.TrimSpace(isrcOverride) - var mbMeta Metadata - if spotifyURL != "" { - result := <-metaChan - if isrc == "" { - isrc = result.ISRC - } - mbMeta = result.Metadata - } - - fmt.Println("Adding metadata...") - - coverPath := "" - - if spotifyCoverURL != "" { - coverPath = outputFilename + ".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") - } - } - - trackNumberToEmbed := spotifyTrackNumber - if trackNumberToEmbed == 0 { - trackNumberToEmbed = 1 - } - - metadata := Metadata{ - Title: trackTitle, - Artist: artistName, - Album: albumTitle, - AlbumArtist: spotifyAlbumArtist, - Date: spotifyReleaseDate, - TrackNumber: trackNumberToEmbed, - TotalTracks: spotifyTotalTracks, - DiscNumber: spotifyDiscNumber, - TotalDiscs: spotifyTotalDiscs, - URL: spotifyURL, - Comment: spotifyURL, - Copyright: spotifyCopyright, - Publisher: spotifyPublisher, - Composer: spotifyComposer, - Separator: metadataSeparator, - Description: "https://github.com/spotbye/SpotiFLAC", - ISRC: isrc, - Genre: mbMeta.Genre, - } - - if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { - fmt.Printf("Tagging failed: %v\n", err) - } else { - fmt.Println("Metadata saved") - } + finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useSingleGenre, embedGenre) fmt.Println("Done") fmt.Println("✓ Downloaded successfully from Tidal") @@ -568,12 +451,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo } 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, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) { - if outputDir != "." { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return "", fmt.Errorf("directory error: %w", err) - } - } - fmt.Printf("Using Tidal URL: %s\n", tidalURL) trackID, err := t.GetTrackIDFromURL(tidalURL) @@ -585,134 +462,24 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality return "", fmt.Errorf("no track ID found") } - artistName := spotifyArtistName - trackTitle := spotifyTrackName - albumTitle := spotifyAlbumName - - artistNameForFile := sanitizeFilename(artistName) - albumArtistForFile := sanitizeFilename(spotifyAlbumArtist) - - if useFirstArtistOnly { - artistNameForFile = sanitizeFilename(GetFirstArtist(artistName)) - albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist)) + outputFilename, alreadyExists, err := buildTidalOutputPath(outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyTrackNumber, spotifyDiscNumber, isrcOverride, useFirstArtistOnly) + if err != nil { + return "", err } - - trackTitleForFile := sanitizeFilename(trackTitle) - albumTitleForFile := sanitizeFilename(albumTitle) - - filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrcOverride) - outputFilename := filepath.Join(outputDir, filename) - - outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting()) if alreadyExists { fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024)) return "EXISTS:" + outputFilename, nil } - type mbResultFallback struct { - ISRC string - Metadata Metadata - } - - metaChan := make(chan mbResultFallback, 1) - if embedGenre && spotifyURL != "" { - go func() { - res := mbResultFallback{} - var isrc string - parts := strings.Split(spotifyURL, "/") - if len(parts) > 0 { - sID := strings.Split(parts[len(parts)-1], "?")[0] - if sID != "" { - client := NewSongLinkClient() - if val, err := client.GetISRC(sID); err == nil { - isrc = val - } - } - } - res.ISRC = isrc - if isrc != "" { - if ShouldSkipMusicBrainzMetadataFetch() { - fmt.Println("Skipping MusicBrainz metadata fetch because status check is offline.") - } else { - fmt.Println("Fetching MusicBrainz metadata...") - if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, trackTitle, artistName, albumTitle, useSingleGenre, embedGenre); err == nil { - res.Metadata = fetchedMeta - fmt.Println("✓ MusicBrainz metadata fetched") - } else { - fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err) - } - } - } - metaChan <- res - }() - } else { - close(metaChan) - } - fmt.Printf("Downloading to: %s\n", outputFilename) successAPI, err := t.downloadWithRotatingAPIs(trackID, outputFilename, quality, allowFallback) if err != nil { - return "", err + cleanupTidalDownloadArtifacts(outputFilename) + return outputFilename, err } fmt.Printf("✓ Downloaded using API: %s\n", successAPI) - isrc := strings.TrimSpace(isrcOverride) - var mbMeta Metadata - if spotifyURL != "" { - result := <-metaChan - if isrc == "" { - isrc = result.ISRC - } - mbMeta = result.Metadata - } - - fmt.Println("Adding metadata...") - - coverPath := "" - - if spotifyCoverURL != "" { - coverPath = outputFilename + ".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") - } - } - - trackNumberToEmbed := spotifyTrackNumber - if trackNumberToEmbed == 0 { - trackNumberToEmbed = 1 - } - - metadata := Metadata{ - Title: trackTitle, - Artist: artistName, - Album: albumTitle, - AlbumArtist: spotifyAlbumArtist, - Date: spotifyReleaseDate, - TrackNumber: trackNumberToEmbed, - TotalTracks: spotifyTotalTracks, - DiscNumber: spotifyDiscNumber, - TotalDiscs: spotifyTotalDiscs, - URL: spotifyURL, - Comment: spotifyURL, - Copyright: spotifyCopyright, - Publisher: spotifyPublisher, - Composer: spotifyComposer, - Separator: metadataSeparator, - Description: "https://github.com/spotbye/SpotiFLAC", - ISRC: isrc, - Genre: mbMeta.Genre, - } - - if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { - fmt.Printf("Tagging failed: %v\n", err) - } else { - fmt.Println("Metadata saved") - } + finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useSingleGenre, embedGenre) fmt.Println("Done") fmt.Println("✓ Downloaded successfully from Tidal") @@ -723,7 +490,7 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) if err != nil { - return "", fmt.Errorf("songlink couldn't find Tidal URL: %w", err) + return "", fmt.Errorf("songlink/songstats couldn't find Tidal URL: %w", err) } return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, allowFallback, useFirstArtistOnly, useSingleGenre, embedGenre) diff --git a/backend/tidal_alt.go b/backend/tidal_alt.go new file mode 100644 index 0000000..6ad4bb9 --- /dev/null +++ b/backend/tidal_alt.go @@ -0,0 +1,238 @@ +package backend + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +const tidalAltDownloadAPIBaseURL = "https://tidal.spotbye.qzz.io/get" + +type TidalAltAPIResponse struct { + Title string `json:"title"` + Link string `json:"link"` +} + +func buildTidalOutputPath(outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyTrackNumber, spotifyDiscNumber int, isrcOverride string, useFirstArtistOnly bool) (string, bool, error) { + if outputDir != "." { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return "", false, fmt.Errorf("directory error: %w", err) + } + } + + artistNameForFile := sanitizeFilename(spotifyArtistName) + albumArtistForFile := sanitizeFilename(spotifyAlbumArtist) + if useFirstArtistOnly { + artistNameForFile = sanitizeFilename(GetFirstArtist(spotifyArtistName)) + albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist)) + } + + trackTitleForFile := sanitizeFilename(spotifyTrackName) + albumTitleForFile := sanitizeFilename(spotifyAlbumName) + + filename := buildTidalFilename(trackTitleForFile, artistNameForFile, albumTitleForFile, albumArtistForFile, spotifyReleaseDate, spotifyTrackNumber, spotifyDiscNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber, isrcOverride) + outputFilename := filepath.Join(outputDir, filename) + + outputFilename, alreadyExists := ResolveOutputPathForDownload(outputFilename, GetRedownloadWithSuffixSetting()) + return outputFilename, alreadyExists, nil +} + +func finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, useSingleGenre bool, embedGenre bool) { + trackTitle := spotifyTrackName + artistName := spotifyArtistName + albumTitle := spotifyAlbumName + + type mbResult struct { + ISRC string + Metadata Metadata + } + + metaChan := make(chan mbResult, 1) + if embedGenre && spotifyURL != "" { + go func() { + res := mbResult{} + var isrc string + parts := strings.Split(spotifyURL, "/") + if len(parts) > 0 { + sID := strings.Split(parts[len(parts)-1], "?")[0] + if sID != "" { + client := NewSongLinkClient() + if val, err := client.GetISRC(sID); err == nil { + isrc = val + } + } + } + res.ISRC = isrc + if isrc != "" { + if ShouldSkipMusicBrainzMetadataFetch() { + fmt.Println("Skipping MusicBrainz metadata fetch because status check is offline.") + } else { + fmt.Println("Fetching MusicBrainz metadata...") + if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, trackTitle, artistName, albumTitle, useSingleGenre, embedGenre); err == nil { + res.Metadata = fetchedMeta + fmt.Println("✓ MusicBrainz metadata fetched") + } else { + fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err) + } + } + } + metaChan <- res + }() + } else { + close(metaChan) + } + + isrc := strings.TrimSpace(isrcOverride) + var mbMeta Metadata + if spotifyURL != "" { + result := <-metaChan + if isrc == "" { + isrc = result.ISRC + } + mbMeta = result.Metadata + } + + upc := "" + if spotifyURL != "" { + if identifiers, err := GetSpotifyTrackIdentifiersDirect(spotifyURL); err == nil || identifiers.ISRC != "" || identifiers.UPC != "" { + if strings.TrimSpace(isrc) == "" && strings.TrimSpace(identifiers.ISRC) != "" { + isrc = strings.TrimSpace(identifiers.ISRC) + } + upc = strings.TrimSpace(identifiers.UPC) + } + } + + fmt.Println("Adding metadata...") + + coverPath := "" + if spotifyCoverURL != "" { + coverPath = outputFilename + ".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") + } + } + + trackNumberToEmbed := spotifyTrackNumber + if trackNumberToEmbed == 0 { + trackNumberToEmbed = 1 + } + + metadata := Metadata{ + Title: trackTitle, + Artist: artistName, + Album: albumTitle, + AlbumArtist: spotifyAlbumArtist, + Date: spotifyReleaseDate, + TrackNumber: trackNumberToEmbed, + TotalTracks: spotifyTotalTracks, + DiscNumber: spotifyDiscNumber, + TotalDiscs: spotifyTotalDiscs, + URL: spotifyURL, + Comment: spotifyURL, + Copyright: spotifyCopyright, + Publisher: spotifyPublisher, + Composer: spotifyComposer, + Separator: metadataSeparator, + Description: "https://github.com/spotbye/SpotiFLAC", + ISRC: isrc, + UPC: upc, + Genre: mbMeta.Genre, + } + + if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { + fmt.Printf("Tagging failed: %v\n", err) + } else { + fmt.Println("Metadata saved") + } +} + +func (t *TidalDownloader) GetAltDownloadURLFromSpotify(spotifyTrackID string) (string, error) { + spotifyTrackID = strings.TrimSpace(spotifyTrackID) + if spotifyTrackID == "" { + return "", fmt.Errorf("spotify track ID is required") + } + + apiURL := fmt.Sprintf("%s/%s", tidalAltDownloadAPIBaseURL, spotifyTrackID) + fmt.Printf("Tidal Alt. API URL: %s\n", apiURL) + + req, err := NewRequestWithDefaultHeaders(http.MethodGet, apiURL, nil) + if err != nil { + return "", fmt.Errorf("failed to create Tidal Alt. request: %w", err) + } + + resp, err := t.client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to get Tidal Alt. download URL: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read Tidal Alt. response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + preview := strings.TrimSpace(string(body)) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + return "", fmt.Errorf("Tidal Alt. returned status %d: %s", resp.StatusCode, preview) + } + + var payload TidalAltAPIResponse + if err := json.Unmarshal(body, &payload); err != nil { + return "", fmt.Errorf("failed to decode Tidal Alt. response: %w", err) + } + + downloadURL := strings.TrimSpace(payload.Link) + if downloadURL == "" { + return "", fmt.Errorf("Tidal Alt. response did not include a download link") + } + + fmt.Println("✓ Tidal Alt. download URL found") + return downloadURL, nil +} + +func (t *TidalDownloader) DownloadAlt(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) { + spotifyTrackID = strings.TrimSpace(spotifyTrackID) + if spotifyTrackID == "" { + return "", fmt.Errorf("spotify track ID is required for Tidal Alt.") + } + + outputFilename, alreadyExists, err := buildTidalOutputPath(outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyTrackNumber, spotifyDiscNumber, isrcOverride, useFirstArtistOnly) + if err != nil { + return "", err + } + if alreadyExists { + fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024)) + return "EXISTS:" + outputFilename, nil + } + + fmt.Printf("Using Tidal Alt. for Spotify track: %s\n", spotifyTrackID) + + downloadURL, err := t.GetAltDownloadURLFromSpotify(spotifyTrackID) + if err != nil { + return outputFilename, err + } + + fmt.Printf("Downloading to: %s\n", outputFilename) + if err := t.DownloadFile(downloadURL, outputFilename); err != nil { + cleanupTidalDownloadArtifacts(outputFilename) + return outputFilename, err + } + + finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useSingleGenre, embedGenre) + + fmt.Println("Done") + fmt.Println("✓ Downloaded successfully from Tidal Alt.") + return outputFilename, nil +} diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 04e01e7..30e539a 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -102,6 +102,9 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => { setTempSettings((prev) => ({ ...prev, tidalQuality: value })); }; + const handleTidalVariantChange = (value: "tidal" | "alt") => { + setTempSettings((prev) => ({ ...prev, tidalVariant: value })); + }; const handleQobuzQualityChange = (value: "6" | "7" | "27") => { setTempSettings((prev) => ({ ...prev, qobuzQuality: value })); }; @@ -424,17 +427,19 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin >)} - {tempSettings.downloader === "tidal" && ()} + {tempSettings.downloader === "tidal" && (tempSettings.tidalVariant === "alt" ? (