.tidal alt

This commit is contained in:
afkarxyz
2026-04-19 23:12:06 +07:00
parent a24ca370eb
commit 7c52c2d9b4
9 changed files with 369 additions and 285 deletions
+12 -2
View File
@@ -307,6 +307,7 @@ type DownloadRequest struct {
ReleaseDate string `json:"release_date,omitempty"` ReleaseDate string `json:"release_date,omitempty"`
CoverURL string `json:"cover_url,omitempty"` CoverURL string `json:"cover_url,omitempty"`
TidalAPIURL string `json:"tidal_api_url,omitempty"` TidalAPIURL string `json:"tidal_api_url,omitempty"`
TidalVariant string `json:"tidal_variant,omitempty"`
OutputDir string `json:"output_dir,omitempty"` OutputDir string `json:"output_dir,omitempty"`
AudioFormat string `json:"audio_format,omitempty"` AudioFormat string `json:"audio_format,omitempty"`
FilenameFormat string `json:"filename_format,omitempty"` FilenameFormat string `json:"filename_format,omitempty"`
@@ -661,7 +662,11 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
} }
case "tidal": 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("") downloader := backend.NewTidalDownloader("")
if req.ServiceURL != "" { 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) 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) 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) { go func(fPath, track, artist, album, sID, cover, format, source string) {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
@@ -834,7 +844,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
} }
backend.AddHistoryItem(item, "SpotiFLAC") 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{ return DownloadResponse{
+11
View File
@@ -288,6 +288,16 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
mbMeta = result.Metadata 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) originalFileDir := filepath.Dir(filePath)
originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
@@ -407,6 +417,7 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
Separator: metadataSeparator, Separator: metadataSeparator,
Description: "https://github.com/spotbye/SpotiFLAC", Description: "https://github.com/spotbye/SpotiFLAC",
ISRC: isrc, ISRC: isrc,
UPC: upc,
Genre: mbMeta.Genre, Genre: mbMeta.Genre,
} }
+15 -248
View File
@@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "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) { 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) fmt.Printf("Using Tidal URL: %s\n", tidalURL)
trackID, err := t.GetTrackIDFromURL(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") return "", fmt.Errorf("no track ID found")
} }
artistName := spotifyArtistName outputFilename, alreadyExists, err := buildTidalOutputPath(outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyTrackNumber, spotifyDiscNumber, isrcOverride, useFirstArtistOnly)
trackTitle := spotifyTrackName if err != nil {
albumTitle := spotifyAlbumName return "", err
artistNameForFile := sanitizeFilename(artistName)
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
if useFirstArtistOnly {
artistNameForFile = sanitizeFilename(GetFirstArtist(artistName))
albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
} }
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 { if alreadyExists {
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024)) fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil 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...") fmt.Println("⚠ HI_RES unavailable/failed, falling back to LOSSLESS...")
downloadURL, err = t.GetDownloadURL(trackID, "LOSSLESS") downloadURL, err = t.GetDownloadURL(trackID, "LOSSLESS")
if err != nil { 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 { } 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) fmt.Printf("Downloading to: %s\n", outputFilename)
if err := t.DownloadFile(downloadURL, outputFilename); err != nil { if err := t.DownloadFile(downloadURL, outputFilename); err != nil {
return "", err cleanupTidalDownloadArtifacts(outputFilename)
return outputFilename, err
} }
if t.apiURL != "" { if t.apiURL != "" {
if err := RememberTidalAPIUsage(t.apiURL); err != nil { if err := RememberTidalAPIUsage(t.apiURL); err != nil {
@@ -504,63 +443,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
} }
} }
isrc := strings.TrimSpace(isrcOverride) finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useSingleGenre, embedGenre)
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")
}
fmt.Println("Done") fmt.Println("Done")
fmt.Println("✓ Downloaded successfully from Tidal") 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) { 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) fmt.Printf("Using Tidal URL: %s\n", tidalURL)
trackID, err := t.GetTrackIDFromURL(tidalURL) trackID, err := t.GetTrackIDFromURL(tidalURL)
@@ -585,134 +462,24 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return "", fmt.Errorf("no track ID found") return "", fmt.Errorf("no track ID found")
} }
artistName := spotifyArtistName outputFilename, alreadyExists, err := buildTidalOutputPath(outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber, spotifyTrackNumber, spotifyDiscNumber, isrcOverride, useFirstArtistOnly)
trackTitle := spotifyTrackName if err != nil {
albumTitle := spotifyAlbumName return "", err
artistNameForFile := sanitizeFilename(artistName)
albumArtistForFile := sanitizeFilename(spotifyAlbumArtist)
if useFirstArtistOnly {
artistNameForFile = sanitizeFilename(GetFirstArtist(artistName))
albumArtistForFile = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
} }
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 { if alreadyExists {
fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024)) fmt.Printf("File already exists: %s (%.2f MB)\n", outputFilename, float64(mustFileSize(outputFilename))/(1024*1024))
return "EXISTS:" + outputFilename, nil 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) fmt.Printf("Downloading to: %s\n", outputFilename)
successAPI, err := t.downloadWithRotatingAPIs(trackID, outputFilename, quality, allowFallback) successAPI, err := t.downloadWithRotatingAPIs(trackID, outputFilename, quality, allowFallback)
if err != nil { if err != nil {
return "", err cleanupTidalDownloadArtifacts(outputFilename)
return outputFilename, err
} }
fmt.Printf("✓ Downloaded using API: %s\n", successAPI) fmt.Printf("✓ Downloaded using API: %s\n", successAPI)
isrc := strings.TrimSpace(isrcOverride) finalizeTidalDownload(outputFilename, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL, embedMaxQualityCover, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks, spotifyTotalDiscs, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL, useSingleGenre, embedGenre)
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")
}
fmt.Println("Done") fmt.Println("Done")
fmt.Println("✓ Downloaded successfully from Tidal") fmt.Println("✓ Downloaded successfully from Tidal")
@@ -723,7 +490,7 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { 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) 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)
+238
View File
@@ -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
}
+30 -11
View File
@@ -102,6 +102,9 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => { const handleTidalQualityChange = async (value: "LOSSLESS" | "HI_RES_LOSSLESS") => {
setTempSettings((prev) => ({ ...prev, tidalQuality: value })); setTempSettings((prev) => ({ ...prev, tidalQuality: value }));
}; };
const handleTidalVariantChange = (value: "tidal" | "alt") => {
setTempSettings((prev) => ({ ...prev, tidalVariant: value }));
};
const handleQobuzQualityChange = (value: "6" | "7" | "27") => { const handleQobuzQualityChange = (value: "6" | "7" | "27") => {
setTempSettings((prev) => ({ ...prev, qobuzQuality: value })); setTempSettings((prev) => ({ ...prev, qobuzQuality: value }));
}; };
@@ -424,17 +427,19 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</Select> </Select>
</>)} </>)}
{tempSettings.downloader === "tidal" && (<Select value={tempSettings.tidalQuality} onValueChange={handleTidalQualityChange}> {tempSettings.downloader === "tidal" && (tempSettings.tidalVariant === "alt" ? (<div className="h-9 px-3 flex items-center text-sm font-medium border border-input rounded-md bg-muted/30 text-muted-foreground whitespace-nowrap cursor-default">
<SelectTrigger className="h-9 w-fit"> 16-bit/44.1kHz
<SelectValue /> </div>) : (<Select value={tempSettings.tidalQuality} onValueChange={handleTidalQualityChange}>
</SelectTrigger> <SelectTrigger className="h-9 w-fit">
<SelectContent> <SelectValue />
<SelectItem value="LOSSLESS">16-bit/44.1kHz</SelectItem> </SelectTrigger>
<SelectItem value="HI_RES_LOSSLESS"> <SelectContent>
24-bit/48kHz <SelectItem value="LOSSLESS">16-bit/44.1kHz</SelectItem>
</SelectItem> <SelectItem value="HI_RES_LOSSLESS">
</SelectContent> 24-bit/48kHz
</Select>)} </SelectItem>
</SelectContent>
</Select>))}
{tempSettings.downloader === "qobuz" && (<Select value={tempSettings.qobuzQuality} onValueChange={handleQobuzQualityChange}> {tempSettings.downloader === "qobuz" && (<Select value={tempSettings.qobuzQuality} onValueChange={handleQobuzQualityChange}>
<SelectTrigger className="h-9 w-fit"> <SelectTrigger className="h-9 w-fit">
@@ -452,7 +457,21 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
</div> </div>
{(tempSettings.downloader === "tidal" || tempSettings.downloader === "auto") && (<div className="space-y-2 pt-2">
<Label htmlFor="tidal-variant">Tidal Variant</Label>
<Select value={tempSettings.tidalVariant || "tidal"} onValueChange={handleTidalVariantChange}>
<SelectTrigger id="tidal-variant" className="h-9 w-fit min-w-[160px]">
<SelectValue placeholder="Select Tidal variant"/>
</SelectTrigger>
<SelectContent>
<SelectItem value="tidal">Tidal</SelectItem>
<SelectItem value="alt">Tidal Alt.</SelectItem>
</SelectContent>
</Select>
</div>)}
{((tempSettings.downloader === "tidal" && {((tempSettings.downloader === "tidal" &&
tempSettings.tidalVariant !== "alt" &&
tempSettings.tidalQuality === "HI_RES_LOSSLESS") || tempSettings.tidalQuality === "HI_RES_LOSSLESS") ||
(tempSettings.downloader === "qobuz" && (tempSettings.downloader === "qobuz" &&
tempSettings.qobuzQuality === "27") || tempSettings.qobuzQuality === "27") ||
+51 -24
View File
@@ -52,6 +52,24 @@ async function resolveTemplateISRC(settings: {
return ""; return "";
} }
} }
function getTidalVariant(settings: any): "tidal" | "alt" {
return settings?.tidalVariant === "alt" ? "alt" : "tidal";
}
function isTidalAltVariant(settings: any): boolean {
return getTidalVariant(settings) === "alt";
}
function getTidalAudioFormat(settings: any, mode: "single" | "auto"): "LOSSLESS" | "HI_RES_LOSSLESS" {
if (isTidalAltVariant(settings)) {
return "LOSSLESS";
}
if (mode === "auto") {
return (settings.autoQuality || "24") === "24" ? "HI_RES_LOSSLESS" : "LOSSLESS";
}
return settings.tidalQuality || "LOSSLESS";
}
function shouldFetchStreamingURLs(order: string[], settings: any): boolean {
return order.includes("amazon") || (order.includes("tidal") && !isTidalAltVariant(settings));
}
export function useDownload(region: string) { export function useDownload(region: string) {
const [downloadProgress, setDownloadProgress] = useState<number>(0); const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
@@ -170,8 +188,11 @@ export function useDownload(region: string) {
itemID = await AddToDownloadQueue(id, trackName || "", displayArtist || "", albumName || ""); itemID = await AddToDownloadQueue(id, trackName || "", displayArtist || "", albumName || "");
} }
if (service === "auto") { if (service === "auto") {
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
const tidalVariant = getTidalVariant(settings);
const tidalLabel = tidalVariant === "alt" ? "Tidal Alt." : "Tidal";
let streamingURLs: any = null; let streamingURLs: any = null;
if (spotifyId) { if (spotifyId && shouldFetchStreamingURLs(order, settings)) {
try { try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App"); const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId, region); const urlsJson = await GetStreamingURLs(spotifyId, region);
@@ -182,16 +203,15 @@ export function useDownload(region: string) {
} }
} }
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined; const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" }; let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = []; const fallbackErrors: string[] = [];
const tidalQuality = getTidalAudioFormat(settings, "auto");
const is24Bit = (settings.autoQuality || "24") === "24"; const is24Bit = (settings.autoQuality || "24") === "24";
const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "27" : "6"; const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) { for (const s of order) {
if (s === "tidal" && streamingURLs?.tidal_url) { if (s === "tidal" && ((tidalVariant === "alt" && spotifyId) || streamingURLs?.tidal_url)) {
try { try {
logger.debug(`trying tidal for: ${trackName} - ${artistName}`); logger.debug(`trying ${tidalLabel} for: ${trackName} - ${artistName}`);
const response = await downloadTrack({ const response = await downloadTrack({
service: "tidal", service: "tidal",
query, query,
@@ -209,7 +229,8 @@ export function useDownload(region: string) {
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.tidal_url, service_url: tidalVariant === "alt" ? undefined : streamingURLs?.tidal_url,
tidal_variant: tidalVariant,
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, item_id: itemID,
audio_format: tidalQuality, audio_format: tidalQuality,
@@ -225,17 +246,17 @@ export function useDownload(region: string) {
embed_genre: settings.embedGenre, embed_genre: settings.embedGenre,
}); });
if (response.success) { if (response.success) {
logger.success(`tidal: ${trackName} - ${artistName}`); logger.success(`${tidalLabel}: ${trackName} - ${artistName}`);
return response; return response;
} }
const errMsg = response.error || response.message || "Failed"; const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Tidal] ${errMsg}`); fallbackErrors.push(`[${tidalLabel}] ${errMsg}`);
lastResponse = response; lastResponse = response;
logger.warning(`tidal failed, trying next...`); logger.warning(`${tidalLabel} failed, trying next...`);
} }
catch (err) { catch (err) {
logger.error(`tidal error: ${err}`); logger.error(`${tidalLabel} error: ${err}`);
fallbackErrors.push(`[Tidal] ${String(err)}`); fallbackErrors.push(`[${tidalLabel}] ${String(err)}`);
lastResponse = { success: false, error: String(err) }; lastResponse = { success: false, error: String(err) };
} }
} }
@@ -344,7 +365,7 @@ export function useDownload(region: string) {
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined; const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
let audioFormat: string | undefined; let audioFormat: string | undefined;
if (service === "tidal") { if (service === "tidal") {
audioFormat = settings.tidalQuality || "LOSSLESS"; audioFormat = getTidalAudioFormat(settings, "single");
} }
else if (service === "qobuz") { else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6"; audioFormat = settings.qobuzQuality || "6";
@@ -373,6 +394,7 @@ export function useDownload(region: string) {
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, item_id: itemID,
audio_format: audioFormat, audio_format: audioFormat,
tidal_variant: service === "tidal" ? getTidalVariant(settings) : undefined,
spotify_track_number: spotifyTrackNumber, spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
@@ -380,6 +402,7 @@ export function useDownload(region: string) {
isrc: resolvedTemplateISRC || undefined, isrc: resolvedTemplateISRC || undefined,
copyright: copyright, copyright: copyright,
publisher: publisher, publisher: publisher,
use_first_artist_only: settings.useFirstArtistOnly,
use_single_genre: settings.useSingleGenre, use_single_genre: settings.useSingleGenre,
embed_genre: settings.embedGenre, embed_genre: settings.embedGenre,
}); });
@@ -451,8 +474,11 @@ export function useDownload(region: string) {
} }
} }
if (service === "auto") { if (service === "auto") {
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
const tidalVariant = getTidalVariant(settings);
const tidalLabel = tidalVariant === "alt" ? "Tidal Alt." : "Tidal";
let streamingURLs: any = null; let streamingURLs: any = null;
if (spotifyId) { if (spotifyId && shouldFetchStreamingURLs(order, settings)) {
try { try {
const { GetStreamingURLs } = await import("../../wailsjs/go/main/App"); const { GetStreamingURLs } = await import("../../wailsjs/go/main/App");
const urlsJson = await GetStreamingURLs(spotifyId, region); const urlsJson = await GetStreamingURLs(spotifyId, region);
@@ -463,16 +489,15 @@ export function useDownload(region: string) {
} }
} }
const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined; const durationSeconds = durationMs ? Math.round(durationMs / 1000) : undefined;
const order = (settings.autoOrder || "tidal-amazon-qobuz").split("-");
let lastResponse: any = { success: false, error: "No matching services found" }; let lastResponse: any = { success: false, error: "No matching services found" };
const fallbackErrors: string[] = []; const fallbackErrors: string[] = [];
const tidalQuality = getTidalAudioFormat(settings, "auto");
const is24Bit = (settings.autoQuality || "24") === "24"; const is24Bit = (settings.autoQuality || "24") === "24";
const tidalQuality = is24Bit ? "HI_RES_LOSSLESS" : "LOSSLESS";
const qobuzQuality = is24Bit ? "27" : "6"; const qobuzQuality = is24Bit ? "27" : "6";
for (const s of order) { for (const s of order) {
if (s === "tidal" && streamingURLs?.tidal_url) { if (s === "tidal" && ((tidalVariant === "alt" && spotifyId) || streamingURLs?.tidal_url)) {
try { try {
logger.debug(`trying tidal for: ${trackName} - ${artistName}`); logger.debug(`trying ${tidalLabel} for: ${trackName} - ${artistName}`);
const response = await downloadTrack({ const response = await downloadTrack({
service: "tidal", service: "tidal",
query, query,
@@ -490,7 +515,8 @@ export function useDownload(region: string) {
spotify_id: spotifyId, spotify_id: spotifyId,
embed_lyrics: settings.embedLyrics, embed_lyrics: settings.embedLyrics,
embed_max_quality_cover: settings.embedMaxQualityCover, embed_max_quality_cover: settings.embedMaxQualityCover,
service_url: streamingURLs.tidal_url, service_url: tidalVariant === "alt" ? undefined : streamingURLs?.tidal_url,
tidal_variant: tidalVariant,
duration: durationSeconds, duration: durationSeconds,
item_id: itemID, item_id: itemID,
audio_format: tidalQuality, audio_format: tidalQuality,
@@ -506,17 +532,17 @@ export function useDownload(region: string) {
embed_genre: settings.embedGenre, embed_genre: settings.embedGenre,
}); });
if (response.success) { if (response.success) {
logger.success(`tidal: ${trackName} - ${artistName}`); logger.success(`${tidalLabel}: ${trackName} - ${artistName}`);
return response; return response;
} }
const errMsg = response.error || response.message || "Failed"; const errMsg = response.error || response.message || "Failed";
fallbackErrors.push(`[Tidal] ${errMsg}`); fallbackErrors.push(`[${tidalLabel}] ${errMsg}`);
lastResponse = response; lastResponse = response;
logger.warning(`tidal failed, trying next...`); logger.warning(`${tidalLabel} failed, trying next...`);
} }
catch (err) { catch (err) {
logger.error(`tidal error: ${err}`); logger.error(`${tidalLabel} error: ${err}`);
fallbackErrors.push(`[Tidal] ${String(err)}`); fallbackErrors.push(`[${tidalLabel}] ${String(err)}`);
lastResponse = { success: false, error: String(err) }; lastResponse = { success: false, error: String(err) };
} }
} }
@@ -628,7 +654,7 @@ export function useDownload(region: string) {
const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined; const durationSecondsForFallback = durationMs ? Math.round(durationMs / 1000) : undefined;
let audioFormat: string | undefined; let audioFormat: string | undefined;
if (service === "tidal") { if (service === "tidal") {
audioFormat = settings.tidalQuality || "LOSSLESS"; audioFormat = getTidalAudioFormat(settings, "single");
} }
else if (service === "qobuz") { else if (service === "qobuz") {
audioFormat = settings.qobuzQuality || "6"; audioFormat = settings.qobuzQuality || "6";
@@ -653,6 +679,7 @@ export function useDownload(region: string) {
duration: durationSecondsForFallback, duration: durationSecondsForFallback,
item_id: itemID, item_id: itemID,
audio_format: audioFormat, audio_format: audioFormat,
tidal_variant: service === "tidal" ? getTidalVariant(settings) : undefined,
spotify_track_number: spotifyTrackNumber, spotify_track_number: spotifyTrackNumber,
spotify_disc_number: spotifyDiscNumber, spotify_disc_number: spotifyDiscNumber,
spotify_total_tracks: spotifyTotalTracks, spotify_total_tracks: spotifyTotalTracks,
+3
View File
@@ -13,6 +13,9 @@ export async function fetchSpotifyMetadata(url: string, batch: boolean = true, d
} }
export async function downloadTrack(request: DownloadRequest): Promise<DownloadResponse> { export async function downloadTrack(request: DownloadRequest): Promise<DownloadResponse> {
const req = new main.DownloadRequest(request); const req = new main.DownloadRequest(request);
if (request.tidal_variant !== undefined) {
(req as any).tidal_variant = request.tidal_variant;
}
if (request.use_single_genre !== undefined) { if (request.use_single_genre !== undefined) {
(req as any).use_single_genre = request.use_single_genre; (req as any).use_single_genre = request.use_single_genre;
} }
+8
View File
@@ -22,6 +22,7 @@ export interface Settings {
embedLyrics: boolean; embedLyrics: boolean;
embedMaxQualityCover: boolean; embedMaxQualityCover: boolean;
operatingSystem: "Windows" | "linux/MacOS"; operatingSystem: "Windows" | "linux/MacOS";
tidalVariant: "tidal" | "alt";
tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS"; tidalQuality: "LOSSLESS" | "HI_RES_LOSSLESS";
qobuzQuality: "6" | "7" | "27"; qobuzQuality: "6" | "7" | "27";
amazonQuality: "original"; amazonQuality: "original";
@@ -110,6 +111,7 @@ export const DEFAULT_SETTINGS: Settings = {
embedLyrics: false, embedLyrics: false,
embedMaxQualityCover: false, embedMaxQualityCover: false,
operatingSystem: detectOS(), operatingSystem: detectOS(),
tidalVariant: "tidal",
tidalQuality: "LOSSLESS", tidalQuality: "LOSSLESS",
qobuzQuality: "6", qobuzQuality: "6",
amazonQuality: "original", amazonQuality: "original",
@@ -215,6 +217,9 @@ function getSettingsFromLocalStorage(): Settings {
if (!('tidalQuality' in parsed)) { if (!('tidalQuality' in parsed)) {
parsed.tidalQuality = "LOSSLESS"; parsed.tidalQuality = "LOSSLESS";
} }
if (!('tidalVariant' in parsed)) {
parsed.tidalVariant = "tidal";
}
if (!('qobuzQuality' in parsed)) { if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6"; parsed.qobuzQuality = "6";
} }
@@ -306,6 +311,9 @@ export async function loadSettings(): Promise<Settings> {
if (!('tidalQuality' in parsed)) { if (!('tidalQuality' in parsed)) {
parsed.tidalQuality = "LOSSLESS"; parsed.tidalQuality = "LOSSLESS";
} }
if (!('tidalVariant' in parsed)) {
parsed.tidalVariant = "tidal";
}
if (!('qobuzQuality' in parsed)) { if (!('qobuzQuality' in parsed)) {
parsed.qobuzQuality = "6"; parsed.qobuzQuality = "6";
} }
+1
View File
@@ -120,6 +120,7 @@ export interface DownloadRequest {
release_date?: string; release_date?: string;
cover_url?: string; cover_url?: string;
tidal_api_url?: string; tidal_api_url?: string;
tidal_variant?: "tidal" | "alt";
output_dir?: string; output_dir?: string;
audio_format?: string; audio_format?: string;
folder_name?: string; folder_name?: string;