This commit is contained in:
afkarxyz
2025-12-14 12:22:08 +07:00
parent b44a9abdd6
commit 237ee777c3
17 changed files with 277 additions and 679 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<div align="center"> <div align="center">
Get Spotify tracks in true FLAC from Tidal, Deezer, Qobuz & Amazon Music — no account required. Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.
![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgMjAgMjAiPjxwYXRoIGZpbGw9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTIwIDEwLjg3M1YyMEw4LjQ3OSAxOC41MzdsLjAwMS03LjY2NEgyMFptLTEzLjEyIDBsLS4wMDEgNy40NjFMMCAxNy40NjF2LTYuNTg4aDYuODhaTTIwIDkuMjczSDguNDhsLS4wMDEtNy44MUwyMCAwdjkuMjczWk02Ljg3OSAxLjY2NmwuMDAxIDcuNjA3SDBWMi41MzlsNi44NzktLjg3M1oiLz48L3N2Zz4=) ![Windows](https://img.shields.io/badge/Windows-10%2B-0078D6?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiB2aWV3Qm94PSIwIDAgMjAgMjAiPjxwYXRoIGZpbGw9IiNmZmZmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTIwIDEwLjg3M1YyMEw4LjQ3OSAxOC41MzdsLjAwMS03LjY2NEgyMFptLTEzLjEyIDBsLS4wMDEgNy40NjFMMCAxNy40NjF2LTYuNTg4aDYuODhaTTIwIDkuMjczSDguNDhsLS4wMDEtNy44MUwyMCAwdjkuMjczWk02Ljg3OSAxLjY2NmwuMDAxIDcuNjA3SDBWMi41MzlsNi44NzktLjg3M1oiLz48L3N2Zz4=)
![macOS](https://img.shields.io/badge/macOS-10.13%2B-000000?style=for-the-badge&logo=apple&logoColor=white) ![macOS](https://img.shields.io/badge/macOS-10.13%2B-000000?style=for-the-badge&logo=apple&logoColor=white)
+11 -18
View File
@@ -43,6 +43,8 @@ type DownloadRequest struct {
TrackName string `json:"track_name,omitempty"` TrackName string `json:"track_name,omitempty"`
ArtistName string `json:"artist_name,omitempty"` ArtistName string `json:"artist_name,omitempty"`
AlbumName string `json:"album_name,omitempty"` AlbumName string `json:"album_name,omitempty"`
AlbumArtist string `json:"album_artist,omitempty"`
ReleaseDate string `json:"release_date,omitempty"`
ApiURL string `json:"api_url,omitempty"` ApiURL string `json:"api_url,omitempty"`
OutputDir string `json:"output_dir,omitempty"` OutputDir string `json:"output_dir,omitempty"`
AudioFormat string `json:"audio_format,omitempty"` AudioFormat string `json:"audio_format,omitempty"`
@@ -128,7 +130,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
} }
if req.Service == "" { if req.Service == "" {
req.Service = "deezer" req.Service = "tidal"
} }
if req.OutputDir == "" { if req.OutputDir == "" {
@@ -225,7 +227,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
downloader := backend.NewTidalDownloader("") downloader := backend.NewTidalDownloader("")
if req.ServiceURL != "" { if req.ServiceURL != "" {
// Use provided URL directly with fallback to multiple APIs // Use provided URL directly with fallback to multiple APIs
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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)
} else { } else {
if req.SpotifyID == "" { if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
@@ -234,13 +236,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal") }, fmt.Errorf("spotify ID is required for Tidal")
} }
// Use ISRC matching for search fallback // Use ISRC matching for search fallback
filename, err = downloader.DownloadWithFallbackAndISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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)
} }
} else { } else {
downloader := backend.NewTidalDownloader(req.ApiURL) downloader := backend.NewTidalDownloader(req.ApiURL)
if req.ServiceURL != "" { if req.ServiceURL != "" {
// Use provided URL directly with specific API // Use provided URL directly with specific API
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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)
} else { } else {
if req.SpotifyID == "" { if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
@@ -249,7 +251,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
}, fmt.Errorf("spotify ID is required for Tidal") }, fmt.Errorf("spotify ID is required for Tidal")
} }
// Use ISRC matching for search fallback // Use ISRC matching for search fallback
filename, err = downloader.DownloadWithISRC(req.SpotifyID, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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)
} }
} }
@@ -260,22 +262,13 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
if quality == "" { if quality == "" {
quality = "6" quality = "6"
} }
filename, err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, quality, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.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)
default: // deezer default:
downloader := backend.NewDeezerDownloader()
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)
} else {
if req.SpotifyID == "" {
return DownloadResponse{ return DownloadResponse{
Success: false, Success: false,
Error: "Spotify ID is required for Deezer", Error: fmt.Sprintf("Unknown service: %s", req.Service),
}, fmt.Errorf("spotify ID is required for Deezer") }, fmt.Errorf("unknown service: %s", req.Service)
}
filename, err = downloader.DownloadBySpotifyID(req.SpotifyID, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
}
} }
if err != nil { if err != nil {
-451
View File
@@ -1,451 +0,0 @@
package backend
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
type DeezerDownloader struct {
client *http.Client
}
type DeezerTrack struct {
ID int64 `json:"id"`
Title string `json:"title"`
TitleShort string `json:"title_short"`
Duration int `json:"duration"`
TrackPos int `json:"track_position"`
DiskNumber int `json:"disk_number"`
ISRC string `json:"isrc"`
ReleaseDate string `json:"release_date"`
Artist struct {
Name string `json:"name"`
ID int64 `json:"id"`
} `json:"artist"`
Album struct {
Title string `json:"title"`
ID int64 `json:"id"`
CoverXL string `json:"cover_xl"`
CoverBig string `json:"cover_big"`
} `json:"album"`
Contributors []struct {
Name string `json:"name"`
Role string `json:"role"`
} `json:"contributors"`
}
type DeezMateResponse struct {
Success bool `json:"success"`
Links struct {
FLAC string `json:"flac"`
} `json:"links"`
}
func NewDeezerDownloader() *DeezerDownloader {
return &DeezerDownloader{
client: &http.Client{
Timeout: 60 * time.Second,
},
}
}
func (d *DeezerDownloader) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
// Decode base64 API URL
spotifyBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLw==")
spotifyURL := fmt.Sprintf("%s%s", string(spotifyBase), spotifyTrackID)
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuc29uZy5saW5rL3YxLWFscGhhLjEvbGlua3M/dXJsPQ==")
apiURL := fmt.Sprintf("%s%s", string(apiBase), spotifyURL)
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
fmt.Println("Getting Deezer URL...")
resp, err := d.client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to get Deezer URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("API returned status %d", resp.StatusCode)
}
// Read body first to handle encoding issues
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
if len(body) == 0 {
return "", fmt.Errorf("API returned empty response")
}
var songLinkResp struct {
LinksByPlatform map[string]struct {
URL string `json:"url"`
} `json:"linksByPlatform"`
}
if err := json.Unmarshal(body, &songLinkResp); err != nil {
// Truncate body for error message (max 200 chars)
bodyStr := string(body)
if len(bodyStr) > 200 {
bodyStr = bodyStr[:200] + "..."
}
return "", fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
}
deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]
if !ok || deezerLink.URL == "" {
return "", fmt.Errorf("deezer link not found")
}
deezerURL := deezerLink.URL
fmt.Printf("Found Deezer URL: %s\n", deezerURL)
return deezerURL, nil
}
func (d *DeezerDownloader) GetTrackIDFromURL(deezerURL string) (int64, error) {
// Extract track ID from Deezer URL
// Format: https://www.deezer.com/track/3412534581
parts := strings.Split(deezerURL, "/track/")
if len(parts) < 2 {
return 0, fmt.Errorf("invalid Deezer URL format")
}
// Get the track ID part and remove any query parameters
trackIDStr := strings.Split(parts[1], "?")[0]
trackIDStr = strings.TrimSpace(trackIDStr)
var trackID int64
_, err := fmt.Sscanf(trackIDStr, "%d", &trackID)
if err != nil {
return 0, fmt.Errorf("failed to parse track ID: %w", err)
}
return trackID, nil
}
func (d *DeezerDownloader) GetTrackByID(trackID int64) (*DeezerTrack, error) {
// Decode base64 API URL
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlemVyLmNvbS8yLjAvdHJhY2sv")
url := fmt.Sprintf("%s%d", string(apiBase), trackID)
resp, err := d.client.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch track: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
// Read body first to handle encoding issues and provide better error messages
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if len(body) == 0 {
return nil, fmt.Errorf("API returned empty response")
}
var track DeezerTrack
if err := json.Unmarshal(body, &track); err != nil {
// Truncate body for error message (max 200 chars)
bodyStr := string(body)
if len(bodyStr) > 200 {
bodyStr = bodyStr[:200] + "..."
}
return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
}
if track.ID == 0 {
return nil, fmt.Errorf("track not found")
}
return &track, nil
}
func (d *DeezerDownloader) GetDownloadURL(trackID int64) (string, error) {
// Decode base64 API URL
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlem1hdGUuY29tL2RsLw==")
url := fmt.Sprintf("%s%d", string(apiBase), trackID)
resp, err := d.client.Get(url)
if err != nil {
return "", fmt.Errorf("failed to get download URL: %w", err)
}
defer resp.Body.Close()
// Read body first to handle encoding issues and provide better error messages
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
if len(body) == 0 {
return "", fmt.Errorf("API returned empty response")
}
var apiResp DeezMateResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
// Truncate body for error message (max 200 chars)
bodyStr := string(body)
if len(bodyStr) > 200 {
bodyStr = bodyStr[:200] + "..."
}
return "", fmt.Errorf("failed to decode API response: %w (response: %s)", err, bodyStr)
}
if !apiResp.Success || apiResp.Links.FLAC == "" {
return "", fmt.Errorf("no FLAC download link available")
}
return apiResp.Links.FLAC, nil
}
func (d *DeezerDownloader) DownloadFile(url, filepath string) error {
// Use a separate client with a longer timeout. The default client's 60s limit
// causes downloads to fail on slow connections or for large Hi-Res files.
downloadClient := &http.Client{
Timeout: 5 * time.Minute, // 5 minutes for large files
}
resp, err := downloadClient.Get(url)
if err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("download failed with status %d", resp.StatusCode)
}
out, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer out.Close()
fmt.Println("Downloading...")
// Use progress writer to track download
pw := NewProgressWriter(out)
_, err = io.Copy(pw, resp.Body)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
// Print final size
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
return nil
}
func (d *DeezerDownloader) DownloadCoverArt(coverURL, filepath string) error {
if coverURL == "" {
return fmt.Errorf("no cover URL provided")
}
resp, err := d.client.Get(coverURL)
if err != nil {
return fmt.Errorf("failed to download cover: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("cover download failed with status %d", resp.StatusCode)
}
out, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("failed to create cover file: %w", err)
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func buildFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
var filename string
// Determine track number to use
numberToUse := position
if useAlbumTrackNumber && trackNumber > 0 {
numberToUse = trackNumber
}
// Check if format is a template (contains {})
if strings.Contains(format, "{") {
filename = format
filename = strings.ReplaceAll(filename, "{title}", title)
filename = strings.ReplaceAll(filename, "{artist}", artist)
// Handle track number - if numberToUse is 0, remove {track} and surrounding separators
if numberToUse > 0 {
filename = strings.ReplaceAll(filename, "{track}", fmt.Sprintf("%02d", numberToUse))
} else {
// Remove {track} with common separators
filename = regexp.MustCompile(`\{track\}\.\s*`).ReplaceAllString(filename, "")
filename = regexp.MustCompile(`\{track\}\s*-\s*`).ReplaceAllString(filename, "")
filename = regexp.MustCompile(`\{track\}\s*`).ReplaceAllString(filename, "")
}
} else {
// Legacy format support
switch format {
case "artist-title":
filename = fmt.Sprintf("%s - %s", artist, title)
case "title":
filename = title
default: // "title-artist"
filename = fmt.Sprintf("%s - %s", title, artist)
}
// Add track number prefix if enabled (legacy behavior)
if includeTrackNumber && position > 0 {
filename = fmt.Sprintf("%02d. %s", numberToUse, filename)
}
}
return filename + ".flac"
}
func (d *DeezerDownloader) DownloadByURL(deezerURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
fmt.Printf("Using Deezer URL: %s\n", deezerURL)
// Extract track ID from URL
trackID, err := d.GetTrackIDFromURL(deezerURL)
if err != nil {
return "", err
}
// Get track info by ID
track, err := d.GetTrackByID(trackID)
if err != nil {
return "", err
}
// Use Spotify metadata if provided, otherwise fallback to Deezer metadata
artists := spotifyArtistName
trackTitle := spotifyTrackName
albumTitle := spotifyAlbumName
if artists == "" {
artists = track.Artist.Name
if len(track.Contributors) > 0 {
var mainArtists []string
for _, contrib := range track.Contributors {
if contrib.Role == "Main" {
mainArtists = append(mainArtists, contrib.Name)
}
}
if len(mainArtists) > 0 {
artists = strings.Join(mainArtists, ", ")
}
}
}
if trackTitle == "" {
trackTitle = track.Title
}
if albumTitle == "" {
albumTitle = track.Album.Title
}
fmt.Printf("Found track: %s - %s\n", artists, trackTitle)
fmt.Printf("Album: %s\n", albumTitle)
downloadURL, err := d.GetDownloadURL(track.ID)
if err != nil {
return "", err
}
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)
return "EXISTS:" + existingFile, nil
}
// Build filename based on format settings
filename := buildFilename(safeTitle, safeArtist, track.TrackPos, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
filepath := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(filepath); err == nil && fileInfo.Size() > 0 {
fmt.Printf("File already exists: %s (%.2f MB)\n", filepath, float64(fileInfo.Size())/(1024*1024))
return "EXISTS:" + filepath, nil
}
fmt.Println("Downloading FLAC file...")
if err := d.DownloadFile(downloadURL, filepath); err != nil {
return "", err
}
fmt.Printf("Downloaded: %s\n", filepath)
coverPath := ""
if track.Album.CoverXL != "" {
coverPath = filepath + ".cover.jpg"
fmt.Println("Downloading cover art...")
if err := d.DownloadCoverArt(track.Album.CoverXL, coverPath); err != nil {
fmt.Printf("Warning: Failed to download cover art: %v\n", err)
} else {
defer os.Remove(coverPath)
}
}
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.TrackPos > 0 {
trackNumberToEmbed = track.TrackPos
} else {
trackNumberToEmbed = position
}
}
metadata := Metadata{
Title: trackTitle,
Artist: artists,
Album: albumTitle,
Date: track.ReleaseDate,
TrackNumber: trackNumberToEmbed,
DiscNumber: track.DiskNumber,
ISRC: track.ISRC,
}
if err := EmbedMetadata(filepath, metadata, coverPath); err != nil {
return "", fmt.Errorf("failed to embed metadata: %w", err)
}
fmt.Println("Metadata embedded successfully!")
fmt.Println("✓ Downloaded successfully from Deezer")
return filepath, nil
}
func (d *DeezerDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
// Get Deezer URL from Spotify track ID
deezerURL, err := d.GetDeezerURLFromSpotify(spotifyTrackID)
if err != nil {
return "", err
}
return d.DownloadByURL(deezerURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
}
+24 -1
View File
@@ -18,11 +18,14 @@ type Metadata struct {
Title string Title string
Artist string Artist string
Album string Album string
Date string AlbumArtist string
Date string // Recorded date (year only)
ReleaseDate string // Release date (full date)
TrackNumber int TrackNumber int
DiscNumber int DiscNumber int
ISRC string ISRC string
Lyrics string Lyrics string
Description string
} }
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error { func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
@@ -50,6 +53,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
if metadata.Album != "" { if metadata.Album != "" {
_ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album) _ = cmt.Add(flacvorbis.FIELD_ALBUM, metadata.Album)
} }
if metadata.AlbumArtist != "" {
_ = cmt.Add("ALBUMARTIST", metadata.AlbumArtist)
}
if metadata.Date != "" { if metadata.Date != "" {
_ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date) _ = cmt.Add(flacvorbis.FIELD_DATE, metadata.Date)
} }
@@ -62,6 +68,10 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
if metadata.ISRC != "" { if metadata.ISRC != "" {
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC) _ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
} }
if metadata.Description != "" {
_ = cmt.Add("DESCRIPTION", metadata.Description)
}
// Lyrics is added last to keep it at the bottom
if metadata.Lyrics != "" { if metadata.Lyrics != "" {
_ = cmt.Add("LYRICS", metadata.Lyrics) // Or "UNSYNCEDLYRICS" for unsynced _ = cmt.Add("LYRICS", metadata.Lyrics) // Or "UNSYNCEDLYRICS" for unsynced
} }
@@ -120,6 +130,19 @@ func fileExists(path string) bool {
return err == nil return err == nil
} }
// extractYear extracts the year from a release date string
// Handles formats: "YYYY-MM-DD", "YYYY-MM", "YYYY"
func extractYear(releaseDate string) string {
if releaseDate == "" {
return ""
}
// Try to extract year (first 4 digits)
if len(releaseDate) >= 4 {
return releaseDate[:4]
}
return releaseDate
}
// EmbedLyricsOnly adds lyrics to a FLAC file while preserving existing metadata // EmbedLyricsOnly adds lyrics to a FLAC file while preserving existing metadata
func EmbedLyricsOnly(filepath string, lyrics string) error { func EmbedLyricsOnly(filepath string, lyrics string) error {
if lyrics == "" { if lyrics == "" {
+23 -7
View File
@@ -307,7 +307,7 @@ func buildQobuzFilename(title, artist string, trackNumber int, format string, in
return filename + ".flac" return filename + ".flac"
} }
func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
fmt.Printf("Fetching track info for ISRC: %s\n", isrc) fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
// Create output directory if it doesn't exist // Create output directory if it doesn't exist
@@ -409,11 +409,6 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
fmt.Println("Embedding metadata and cover art...") fmt.Println("Embedding metadata and cover art...")
releaseYear := ""
if len(track.ReleaseDateOriginal) >= 4 {
releaseYear = track.ReleaseDateOriginal[:4]
}
// Use album track number if in album folder structure, otherwise use playlist position // Use album track number if in album folder structure, otherwise use playlist position
trackNumberToEmbed := 0 trackNumberToEmbed := 0
if position > 0 { if position > 0 {
@@ -422,16 +417,37 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
} else { } else {
trackNumberToEmbed = position 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
} }
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artists, Artist: artists,
Album: albumTitle, Album: albumTitle,
Date: releaseYear, AlbumArtist: finalAlbumArtist,
Date: year, // Recorded date (year only)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: track.MediaNumber, DiscNumber: track.MediaNumber,
ISRC: track.ISRC, ISRC: track.ISRC,
Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
if err := EmbedMetadata(filepath, metadata, coverPath); err != nil { if err := EmbedMetadata(filepath, metadata, coverPath); err != nil {
+1 -16
View File
@@ -19,7 +19,6 @@ type SongLinkClient struct {
type SongLinkURLs struct { type SongLinkURLs struct {
TidalURL string `json:"tidal_url"` TidalURL string `json:"tidal_url"`
DeezerURL string `json:"deezer_url"`
AmazonURL string `json:"amazon_url"` AmazonURL string `json:"amazon_url"`
} }
@@ -27,11 +26,9 @@ type SongLinkURLs struct {
type TrackAvailability struct { type TrackAvailability struct {
SpotifyID string `json:"spotify_id"` SpotifyID string `json:"spotify_id"`
Tidal bool `json:"tidal"` Tidal bool `json:"tidal"`
Deezer bool `json:"deezer"`
Amazon bool `json:"amazon"` Amazon bool `json:"amazon"`
Qobuz bool `json:"qobuz"` Qobuz bool `json:"qobuz"`
TidalURL string `json:"tidal_url,omitempty"` TidalURL string `json:"tidal_url,omitempty"`
DeezerURL string `json:"deezer_url,omitempty"`
AmazonURL string `json:"amazon_url,omitempty"` AmazonURL string `json:"amazon_url,omitempty"`
QobuzURL string `json:"qobuz_url,omitempty"` QobuzURL string `json:"qobuz_url,omitempty"`
} }
@@ -154,12 +151,6 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLink
fmt.Printf("✓ Tidal URL found\n") fmt.Printf("✓ Tidal URL found\n")
} }
// Extract Deezer URL
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
urls.DeezerURL = deezerLink.URL
fmt.Printf("✓ Deezer URL found\n")
}
// Extract Amazon URL // Extract Amazon URL
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
amazonURL := amazonLink.URL amazonURL := amazonLink.URL
@@ -171,7 +162,7 @@ func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLink
} }
// Check if at least one URL was found // Check if at least one URL was found
if urls.TidalURL == "" && urls.DeezerURL == "" && urls.AmazonURL == "" { if urls.TidalURL == "" && urls.AmazonURL == "" {
return nil, fmt.Errorf("no streaming URLs found") return nil, fmt.Errorf("no streaming URLs found")
} }
@@ -290,12 +281,6 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri
availability.TidalURL = tidalLink.URL availability.TidalURL = tidalLink.URL
} }
// Check Deezer
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
availability.Deezer = true
availability.DeezerURL = deezerLink.URL
}
// Check Amazon // Check Amazon
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
availability.Amazon = true availability.Amazon = true
+20 -6
View File
@@ -57,16 +57,18 @@ func NewSpotifyMetadataClient() *SpotifyMetadataClient {
// TrackMetadata mirrors the filtered track payload returned by the Python script. // TrackMetadata mirrors the filtered track payload returned by the Python script.
type TrackMetadata struct { type TrackMetadata struct {
SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"` Artists string `json:"artists"`
Name string `json:"name"` Name string `json:"name"`
AlbumName string `json:"album_name"` AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"` DurationMS int `json:"duration_ms"`
Images string `json:"images"` Images string `json:"images"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"` ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"` ISRC string `json:"isrc"`
SpotifyID string `json:"spotify_id,omitempty"`
} }
// ArtistSimple holds basic artist info for clickable artists // ArtistSimple holds basic artist info for clickable artists
@@ -78,17 +80,19 @@ type ArtistSimple struct {
// AlbumTrackMetadata holds per-track info for album / playlist formatting. // AlbumTrackMetadata holds per-track info for album / playlist formatting.
type AlbumTrackMetadata struct { type AlbumTrackMetadata struct {
SpotifyID string `json:"spotify_id,omitempty"`
Artists string `json:"artists"` Artists string `json:"artists"`
Name string `json:"name"` Name string `json:"name"`
AlbumName string `json:"album_name"` AlbumName string `json:"album_name"`
AlbumArtist string `json:"album_artist,omitempty"`
DurationMS int `json:"duration_ms"` DurationMS int `json:"duration_ms"`
Images string `json:"images"` Images string `json:"images"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number,omitempty"`
ExternalURL string `json:"external_urls"` ExternalURL string `json:"external_urls"`
ISRC string `json:"isrc"` ISRC string `json:"isrc"`
AlbumType string `json:"album_type,omitempty"` AlbumType string `json:"album_type,omitempty"`
SpotifyID string `json:"spotify_id,omitempty"`
AlbumID string `json:"album_id,omitempty"` AlbumID string `json:"album_id,omitempty"`
AlbumURL string `json:"album_url,omitempty"` AlbumURL string `json:"album_url,omitempty"`
ArtistID string `json:"artist_id,omitempty"` ArtistID string `json:"artist_id,omitempty"`
@@ -227,6 +231,7 @@ type trackSimplified struct {
Name string `json:"name"` Name string `json:"name"`
DurationMS int `json:"duration_ms"` DurationMS int `json:"duration_ms"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
ExternalURL externalURL `json:"external_urls"` ExternalURL externalURL `json:"external_urls"`
Artists []artist `json:"artists"` Artists []artist `json:"artists"`
} }
@@ -236,6 +241,7 @@ type trackFull struct {
Name string `json:"name"` Name string `json:"name"`
DurationMS int `json:"duration_ms"` DurationMS int `json:"duration_ms"`
TrackNumber int `json:"track_number"` TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
ExternalURL externalURL `json:"external_urls"` ExternalURL externalURL `json:"external_urls"`
ExternalID externalID `json:"external_ids"` ExternalID externalID `json:"external_ids"`
Album albumSimplified `json:"album"` Album albumSimplified `json:"album"`
@@ -502,16 +508,18 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *playlistRaw) PlaylistRes
}) })
} }
tracks = append(tracks, AlbumTrackMetadata{ tracks = append(tracks, AlbumTrackMetadata{
SpotifyID: item.Track.ID,
Artists: joinArtists(item.Track.Artists), Artists: joinArtists(item.Track.Artists),
Name: item.Track.Name, Name: item.Track.Name,
AlbumName: item.Track.Album.Name, AlbumName: item.Track.Album.Name,
AlbumArtist: joinArtists(item.Track.Album.Artists),
DurationMS: item.Track.DurationMS, DurationMS: item.Track.DurationMS,
Images: firstNonEmpty(firstImageURL(item.Track.Album.Images), info.Owner.Images), Images: firstNonEmpty(firstImageURL(item.Track.Album.Images), info.Owner.Images),
ReleaseDate: item.Track.Album.ReleaseDate, ReleaseDate: item.Track.Album.ReleaseDate,
TrackNumber: item.Track.TrackNumber, TrackNumber: item.Track.TrackNumber,
DiscNumber: item.Track.DiscNumber,
ExternalURL: item.Track.ExternalURL.Spotify, ExternalURL: item.Track.ExternalURL.Spotify,
ISRC: item.Track.ExternalID.ISRC, ISRC: item.Track.ExternalID.ISRC,
SpotifyID: item.Track.ID,
AlbumID: item.Track.Album.ID, AlbumID: item.Track.Album.ID,
AlbumURL: item.Track.Album.ExternalURL.Spotify, AlbumURL: item.Track.Album.ExternalURL.Spotify,
ArtistID: artistID, ArtistID: artistID,
@@ -551,16 +559,18 @@ func (c *SpotifyMetadataClient) formatAlbumData(ctx context.Context, raw *albumR
for _, item := range raw.Data.Tracks.Items { for _, item := range raw.Data.Tracks.Items {
isrc := c.fetchTrackISRC(ctx, item.ID, raw.Token, cache) isrc := c.fetchTrackISRC(ctx, item.ID, raw.Token, cache)
tracks = append(tracks, AlbumTrackMetadata{ tracks = append(tracks, AlbumTrackMetadata{
SpotifyID: item.ID,
Artists: joinArtists(item.Artists), Artists: joinArtists(item.Artists),
Name: item.Name, Name: item.Name,
AlbumName: raw.Data.Name, AlbumName: raw.Data.Name,
AlbumArtist: joinArtists(raw.Data.Artists),
DurationMS: item.DurationMS, DurationMS: item.DurationMS,
Images: albumImage, Images: albumImage,
ReleaseDate: raw.Data.ReleaseDate, ReleaseDate: raw.Data.ReleaseDate,
TrackNumber: item.TrackNumber, TrackNumber: item.TrackNumber,
DiscNumber: item.DiscNumber,
ExternalURL: item.ExternalURL.Spotify, ExternalURL: item.ExternalURL.Spotify,
ISRC: isrc, ISRC: isrc,
SpotifyID: item.ID,
}) })
} }
@@ -629,17 +639,19 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
}) })
} }
allTracks = append(allTracks, AlbumTrackMetadata{ allTracks = append(allTracks, AlbumTrackMetadata{
SpotifyID: tr.ID,
Artists: joinArtists(tr.Artists), Artists: joinArtists(tr.Artists),
Name: tr.Name, Name: tr.Name,
AlbumName: alb.Name, AlbumName: alb.Name,
AlbumArtist: joinArtists(alb.Artists),
AlbumType: alb.AlbumType, AlbumType: alb.AlbumType,
DurationMS: tr.DurationMS, DurationMS: tr.DurationMS,
Images: albumImage, Images: albumImage,
ReleaseDate: alb.ReleaseDate, ReleaseDate: alb.ReleaseDate,
TrackNumber: tr.TrackNumber, TrackNumber: tr.TrackNumber,
DiscNumber: tr.DiscNumber,
ExternalURL: tr.ExternalURL.Spotify, ExternalURL: tr.ExternalURL.Spotify,
ISRC: isrc, ISRC: isrc,
SpotifyID: tr.ID,
AlbumID: alb.ID, AlbumID: alb.ID,
AlbumURL: alb.ExternalURL.Spotify, AlbumURL: alb.ExternalURL.Spotify,
ArtistID: artistID, ArtistID: artistID,
@@ -676,16 +688,18 @@ func formatTrackData(raw *trackFull) TrackResponse {
} }
return TrackResponse{ return TrackResponse{
Track: TrackMetadata{ Track: TrackMetadata{
SpotifyID: raw.ID,
Artists: joinArtists(raw.Artists), Artists: joinArtists(raw.Artists),
Name: raw.Name, Name: raw.Name,
AlbumName: raw.Album.Name, AlbumName: raw.Album.Name,
AlbumArtist: joinArtists(raw.Album.Artists),
DurationMS: raw.DurationMS, DurationMS: raw.DurationMS,
Images: firstImageURL(raw.Album.Images), Images: firstImageURL(raw.Album.Images),
ReleaseDate: raw.Album.ReleaseDate, ReleaseDate: raw.Album.ReleaseDate,
TrackNumber: raw.TrackNumber, TrackNumber: raw.TrackNumber,
DiscNumber: raw.DiscNumber,
ExternalURL: raw.ExternalURL.Spotify, ExternalURL: raw.ExternalURL.Spotify,
ISRC: raw.ExternalID.ISRC, ISRC: raw.ExternalID.ISRC,
SpotifyID: raw.ID,
}, },
} }
} }
+113 -47
View File
@@ -801,7 +801,7 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
return nil return nil
} }
func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
if outputDir != "." { if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil { if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err) return "", fmt.Errorf("directory error: %w", err)
@@ -848,7 +848,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
artistName = strings.Join(artists, ", ") artistName = strings.Join(artists, ", ")
} }
} }
artistName = sanitizeFilename(artistName)
if trackTitle == "" { if trackTitle == "" {
trackTitle = trackInfo.Title trackTitle = trackInfo.Title
@@ -856,20 +855,23 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID) trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
} }
} }
trackTitle = sanitizeFilename(trackTitle)
if albumTitle == "" { if albumTitle == "" {
albumTitle = trackInfo.Album.Title albumTitle = trackInfo.Album.Title
} }
// Sanitize for filename only (not for metadata)
artistNameForFile := sanitizeFilename(artistName)
trackTitleForFile := sanitizeFilename(trackTitle)
// Check if file with same ISRC already exists // Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
// Build filename based on format settings // Build filename based on format settings (use sanitized versions for filename)
filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(trackTitleForFile, artistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -905,11 +907,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
} }
} }
releaseYear := ""
if len(trackInfo.Album.ReleaseDate) >= 4 {
releaseYear = trackInfo.Album.ReleaseDate[:4]
}
// Use album track number if in album folder structure, otherwise use playlist position // Use album track number if in album folder structure, otherwise use playlist position
trackNumberToEmbed := 0 trackNumberToEmbed := 0
if position > 0 { if position > 0 {
@@ -918,16 +915,37 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
} else { } else {
trackNumberToEmbed = position trackNumberToEmbed = position
} }
} else if trackInfo.TrackNumber > 0 {
// Fallback to Tidal track number if no position provided
trackNumberToEmbed = trackInfo.TrackNumber
}
// Use Spotify release date if provided, otherwise use Tidal release date
finalReleaseDate := spotifyReleaseDate
if finalReleaseDate == "" {
finalReleaseDate = trackInfo.Album.ReleaseDate
}
// Extract year from release date (format: YYYY-MM-DD or YYYY)
year := extractYear(finalReleaseDate)
// Use Spotify album artist if provided, otherwise use first artist from Tidal
finalAlbumArtist := spotifyAlbumArtist
if finalAlbumArtist == "" && len(trackInfo.Artists) > 0 {
finalAlbumArtist = trackInfo.Artists[0].Name
} }
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artistName, Artist: artistName,
Album: albumTitle, Album: albumTitle,
Date: releaseYear, AlbumArtist: finalAlbumArtist,
Date: year, // Recorded date (year only)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC, ISRC: trackInfo.ISRC,
Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -941,7 +959,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
apis, err := t.GetAvailableAPIs() apis, err := t.GetAvailableAPIs()
if err != nil { if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err) return "", fmt.Errorf("no APIs available for fallback: %w", err)
@@ -992,7 +1010,6 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
artistName = strings.Join(artists, ", ") artistName = strings.Join(artists, ", ")
} }
} }
artistName = sanitizeFilename(artistName)
if trackTitle == "" { if trackTitle == "" {
trackTitle = trackInfo.Title trackTitle = trackInfo.Title
@@ -1000,19 +1017,22 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
trackTitle = fmt.Sprintf("track_%d", trackInfo.ID) trackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
} }
} }
trackTitle = sanitizeFilename(trackTitle)
if albumTitle == "" { if albumTitle == "" {
albumTitle = trackInfo.Album.Title albumTitle = trackInfo.Album.Title
} }
// Sanitize for filename only (not for metadata)
artistNameForFile := sanitizeFilename(artistName)
trackTitleForFile := sanitizeFilename(trackTitle)
// Check if file with same ISRC already exists // Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(trackTitleForFile, artistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1051,11 +1071,6 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
} }
} }
releaseYear := ""
if len(trackInfo.Album.ReleaseDate) >= 4 {
releaseYear = trackInfo.Album.ReleaseDate[:4]
}
trackNumberToEmbed := 0 trackNumberToEmbed := 0
if position > 0 { if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1063,16 +1078,37 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
} else { } else {
trackNumberToEmbed = position trackNumberToEmbed = position
} }
} else if trackInfo.TrackNumber > 0 {
// Fallback to Tidal track number if no position provided
trackNumberToEmbed = trackInfo.TrackNumber
}
// Use Spotify release date if provided, otherwise use Tidal release date
finalReleaseDate := spotifyReleaseDate
if finalReleaseDate == "" {
finalReleaseDate = trackInfo.Album.ReleaseDate
}
// Extract year from release date (format: YYYY-MM-DD or YYYY)
year := extractYear(finalReleaseDate)
// Use Spotify album artist if provided, otherwise use first artist from Tidal
finalAlbumArtist := spotifyAlbumArtist
if finalAlbumArtist == "" && len(trackInfo.Artists) > 0 {
finalAlbumArtist = trackInfo.Artists[0].Name
} }
metadata := Metadata{ metadata := Metadata{
Title: trackTitle, Title: trackTitle,
Artist: artistName, Artist: artistName,
Album: albumTitle, Album: albumTitle,
Date: releaseYear, AlbumArtist: finalAlbumArtist,
Date: year, // Recorded date (year only)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC, ISRC: trackInfo.ISRC,
Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1086,7 +1122,7 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
// Get Tidal URL from Spotify track ID // Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
@@ -1096,11 +1132,11 @@ func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameF
return t.DownloadBySearch(spotifyTrackName, spotifyArtistName, spotifyAlbumName, "", 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearch(spotifyTrackName, spotifyArtistName, spotifyAlbumName, "", 0, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
} }
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
} }
// DownloadWithISRC downloads a track with ISRC matching for search fallback // DownloadWithISRC downloads a track with ISRC matching for search fallback
func (t *TidalDownloader) DownloadWithISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
// Get Tidal URL from Spotify track ID // Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
@@ -1110,7 +1146,7 @@ func (t *TidalDownloader) DownloadWithISRC(spotifyTrackID, spotifyISRC, outputDi
return t.DownloadBySearchWithISRC(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) return t.DownloadBySearchWithISRC(spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyISRC, expectedDuration, outputDir, quality, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
} }
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
} }
// DownloadBySearch downloads a track by searching Tidal directly using metadata // DownloadBySearch downloads a track by searching Tidal directly using metadata
@@ -1159,7 +1195,6 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
finalArtistName = "Unknown Artist" finalArtistName = "Unknown Artist"
} }
} }
finalArtistName = sanitizeFilename(finalArtistName)
if finalTrackTitle == "" { if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title finalTrackTitle = trackInfo.Title
@@ -1167,12 +1202,15 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID) finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
} }
} }
finalTrackTitle = sanitizeFilename(finalTrackTitle)
if finalAlbumTitle == "" { if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title 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 // Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
@@ -1180,7 +1218,7 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
} }
// Build filename // Build filename
filename := buildTidalFilename(finalTrackTitle, finalArtistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1217,11 +1255,6 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
} }
} }
releaseYear := ""
if len(trackInfo.Album.ReleaseDate) >= 4 {
releaseYear = trackInfo.Album.ReleaseDate[:4]
}
trackNumberToEmbed := 0 trackNumberToEmbed := 0
if position > 0 { if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1229,16 +1262,34 @@ func (t *TidalDownloader) DownloadBySearchWithISRC(trackName, artistName, albumN
} else { } else {
trackNumberToEmbed = position 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
} }
metadata := Metadata{ metadata := Metadata{
Title: finalTrackTitle, Title: finalTrackTitle,
Artist: finalArtistName, Artist: finalArtistName,
Album: finalAlbumTitle, Album: finalAlbumTitle,
Date: releaseYear, AlbumArtist: albumArtist,
Date: year, // Recorded date (year only)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC, ISRC: trackInfo.ISRC,
Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1516,7 +1567,6 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
finalArtistName = "Unknown Artist" finalArtistName = "Unknown Artist"
} }
} }
finalArtistName = sanitizeFilename(finalArtistName)
if finalTrackTitle == "" { if finalTrackTitle == "" {
finalTrackTitle = trackInfo.Title finalTrackTitle = trackInfo.Title
@@ -1524,19 +1574,22 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID) finalTrackTitle = fmt.Sprintf("track_%d", trackInfo.ID)
} }
} }
finalTrackTitle = sanitizeFilename(finalTrackTitle)
if finalAlbumTitle == "" { if finalAlbumTitle == "" {
finalAlbumTitle = trackInfo.Album.Title finalAlbumTitle = trackInfo.Album.Title
} }
// Sanitize for filename only (not for metadata)
finalArtistNameForFile := sanitizeFilename(finalArtistName)
finalTrackTitleForFile := sanitizeFilename(finalTrackTitle)
// Check if file already exists // Check if file already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists { if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile) fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil return "EXISTS:" + existingFile, nil
} }
filename := buildTidalFilename(finalTrackTitle, finalArtistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber) filename := buildTidalFilename(finalTrackTitleForFile, finalArtistNameForFile, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename) outputFilename := filepath.Join(outputDir, filename)
if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 {
@@ -1576,11 +1629,6 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
} }
} }
releaseYear := ""
if len(trackInfo.Album.ReleaseDate) >= 4 {
releaseYear = trackInfo.Album.ReleaseDate[:4]
}
trackNumberToEmbed := 0 trackNumberToEmbed := 0
if position > 0 { if position > 0 {
if useAlbumTrackNumber && trackInfo.TrackNumber > 0 { if useAlbumTrackNumber && trackInfo.TrackNumber > 0 {
@@ -1588,16 +1636,34 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
} else { } else {
trackNumberToEmbed = position 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
} }
metadata := Metadata{ metadata := Metadata{
Title: finalTrackTitle, Title: finalTrackTitle,
Artist: finalArtistName, Artist: finalArtistName,
Album: finalAlbumTitle, Album: finalAlbumTitle,
Date: releaseYear, AlbumArtist: albumArtist,
Date: year, // Recorded date (year only)
ReleaseDate: finalReleaseDate, // Release date (full date)
TrackNumber: trackNumberToEmbed, TrackNumber: trackNumberToEmbed,
DiscNumber: trackInfo.VolumeNumber, DiscNumber: trackInfo.VolumeNumber,
ISRC: trackInfo.ISRC, ISRC: trackInfo.ISRC,
Description: "https://github.com/afkarxyz/SpotiFLAC",
} }
if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil { if err := EmbedMetadata(outputFilename, metadata, coverPath); err != nil {
@@ -1611,7 +1677,7 @@ func (t *TidalDownloader) DownloadBySearchWithFallback(trackName, artistName, al
return outputFilename, nil return outputFilename, nil
} }
func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
// Get Tidal URL once // Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
@@ -1622,12 +1688,12 @@ func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, qualit
} }
// Use parallel API requests via DownloadByURLWithFallback // Use parallel API requests via DownloadByURLWithFallback
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
} }
// DownloadWithFallbackAndISRC downloads with ISRC matching for search fallback // DownloadWithFallbackAndISRC downloads with ISRC matching for search fallback
// Uses parallel API requests for faster download // Uses parallel API requests for faster download
func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISRC, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName 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) (string, error) {
// Get Tidal URL once // Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID) tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil { if err != nil {
@@ -1638,7 +1704,7 @@ func (t *TidalDownloader) DownloadWithFallbackAndISRC(spotifyTrackID, spotifyISR
} }
// Use parallel API requests via DownloadByURLWithFallback // Use parallel API requests via DownloadByURLWithFallback
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber) return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, useAlbumTrackNumber)
} }
func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string { func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool) string {
+1 -1
View File
@@ -58,7 +58,7 @@ export function Header({ version, hasUpdate, releaseDate }: HeaderProps) {
</div> </div>
</div> </div>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Get Spotify tracks in true FLAC from Tidal, Deezer, Qobuz & Amazon Music no account required. Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music no account required.
</p> </p>
</div> </div>
</div> </div>
@@ -7,12 +7,6 @@ export const TidalIcon = ({ className = "w-4 h-4" }: { className?: string }) =>
</svg> </svg>
); );
export const DeezerIcon = ({ className = "w-4 h-4" }: { className?: string }) => (
<svg viewBox="0 0 24 24" className={`${className} fill-current`}>
<path d="M18.77 5.55c.19-1.07.46-1.75.76-1.75.56 0 1.02 2.34 1.02 5.23 0 2.89-.46 5.23-1.02 5.23-.23 0-.44-.4-.61-1.06-.27 2.43-.83 4.11-1.48 4.11-.5 0-.96-1-1.26-2.6-.2 3.03-.73 5.17-1.33 5.17-.39 0-.73-.85-.99-2.23-.31 2.85-1.03 4.85-1.86 4.85-.83 0-1.55-2-1.86-4.85-.25 1.38-.6 2.23-.99 2.23-.6 0-1.12-2.14-1.33-5.16-.3 1.58-.75 2.6-1.26 2.6-.65 0-1.2-1.68-1.48-4.12-.17.66-.38 1.06-.61 1.06-.56 0-1.02-2.34-1.02-5.23 0-2.89.46-5.23 1.02-5.23.3 0 .57.68.76 1.75C5.53 3.7 6 2.5 6.56 2.5c.66 0 1.22 1.7 1.49 4.17.26-1.8.66-2.94 1.1-2.94.63 0 1.16 2.25 1.36 5.4.36-1.62.9-2.63 1.5-2.63.58 0 1.12 1.01 1.49 2.62.2-3.14.72-5.4 1.35-5.4.44 0 .84 1.15 1.1 2.95.27-2.47.84-4.17 1.49-4.17.55 0 1.03 1.2 1.33 3.05ZM2 8.52c0-1.3.26-2.34.58-2.34.32 0 .57 1.05.57 2.34 0 1.29-.25 2.34-.57 2.34-.32 0-.58-1.05-.58-2.34Zm18.85 0c0-1.3.25-2.34.57-2.34.32 0 .58 1.05.58 2.34 0 1.29-.26 2.34-.58 2.34-.32 0-.57-1.05-.57-2.34Z"></path>
</svg>
);
export const QobuzIcon = ({ className = "w-4 h-4" }: { className?: string }) => ( export const QobuzIcon = ({ className = "w-4 h-4" }: { className?: string }) => (
<svg viewBox="0 0 24 24" className={`${className} fill-current`}> <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
<path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path> <path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path>
+1 -10
View File
@@ -26,12 +26,6 @@ const TidalIcon = () => (
</svg> </svg>
); );
const DeezerIcon = () => (
<svg viewBox="0 0 24 24" className="inline-block w-[1.1em] h-[1.1em] mr-2 fill-muted-foreground">
<path d="M18.77 5.55c.19-1.07.46-1.75.76-1.75.56 0 1.02 2.34 1.02 5.23 0 2.89-.46 5.23-1.02 5.23-.23 0-.44-.4-.61-1.06-.27 2.43-.83 4.11-1.48 4.11-.5 0-.96-1-1.26-2.6-.2 3.03-.73 5.17-1.33 5.17-.39 0-.73-.85-.99-2.23-.31 2.85-1.03 4.85-1.86 4.85-.83 0-1.55-2-1.86-4.85-.25 1.38-.6 2.23-.99 2.23-.6 0-1.12-2.14-1.33-5.16-.3 1.58-.75 2.6-1.26 2.6-.65 0-1.2-1.68-1.48-4.12-.17.66-.38 1.06-.61 1.06-.56 0-1.02-2.34-1.02-5.23 0-2.89.46-5.23 1.02-5.23.3 0 .57.68.76 1.75C5.53 3.7 6 2.5 6.56 2.5c.66 0 1.22 1.7 1.49 4.17.26-1.8.66-2.94 1.1-2.94.63 0 1.16 2.25 1.36 5.4.36-1.62.9-2.63 1.5-2.63.58 0 1.12 1.01 1.49 2.62.2-3.14.72-5.4 1.35-5.4.44 0 .84 1.15 1.1 2.95.27-2.47.84-4.17 1.49-4.17.55 0 1.03 1.2 1.33 3.05ZM2 8.52c0-1.3.26-2.34.58-2.34.32 0 .57 1.05.57 2.34 0 1.29-.25 2.34-.57 2.34-.32 0-.58-1.05-.58-2.34Zm18.85 0c0-1.3.25-2.34.57-2.34.32 0 .58 1.05.58 2.34 0 1.29-.26 2.34-.58 2.34-.32 0-.57-1.05-.57-2.34Z"></path>
</svg>
);
const QobuzIcon = () => ( const QobuzIcon = () => (
<svg viewBox="0 0 24 24" className="inline-block w-[1.1em] h-[1.1em] mr-2 fill-muted-foreground"> <svg viewBox="0 0 24 24" className="inline-block w-[1.1em] h-[1.1em] mr-2 fill-muted-foreground">
<path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path> <path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path>
@@ -224,7 +218,7 @@ export function SettingsPage() {
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Select
value={tempSettings.downloader} value={tempSettings.downloader}
onValueChange={(value: "auto" | "deezer" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))} onValueChange={(value: "auto" | "tidal" | "qobuz" | "amazon") => setTempSettings((prev) => ({ ...prev, downloader: value }))}
> >
<SelectTrigger id="downloader" className="h-9 w-fit"> <SelectTrigger id="downloader" className="h-9 w-fit">
<SelectValue placeholder="Select a source" /> <SelectValue placeholder="Select a source" />
@@ -234,9 +228,6 @@ export function SettingsPage() {
<SelectItem value="tidal"> <SelectItem value="tidal">
<span className="flex items-center"><TidalIcon />Tidal</span> <span className="flex items-center"><TidalIcon />Tidal</span>
</SelectItem> </SelectItem>
<SelectItem value="deezer">
<span className="flex items-center"><DeezerIcon />Deezer</span>
</SelectItem>
<SelectItem value="qobuz"> <SelectItem value="qobuz">
<span className="flex items-center"><QobuzIcon />Qobuz</span> <span className="flex items-center"><QobuzIcon />Qobuz</span>
</SelectItem> </SelectItem>
+3 -4
View File
@@ -9,7 +9,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import type { TrackMetadata, TrackAvailability } from "@/types/api"; import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalIcon, DeezerIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons"; import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
interface TrackInfoProps { interface TrackInfoProps {
track: TrackMetadata & { album_name: string; release_date: string }; track: TrackMetadata & { album_name: string; release_date: string };
@@ -25,7 +25,7 @@ interface TrackInfoProps {
checkingAvailability?: boolean; checkingAvailability?: boolean;
availability?: TrackAvailability; availability?: TrackAvailability;
downloadingCover?: boolean; downloadingCover?: boolean;
onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string) => void; onDownload: (isrc: string, name: string, artists: string, albumName?: string, spotifyId?: string, playlistName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName?: string) => void;
onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName?: string) => void;
@@ -120,7 +120,7 @@ export function TrackInfo({
{track.isrc && ( {track.isrc && (
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button <Button
onClick={() => onDownload(track.isrc, track.name, track.artists, track.album_name, track.spotify_id)} 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)}
disabled={isDownloading || downloadingTrack === track.isrc} disabled={isDownloading || downloadingTrack === track.isrc}
> >
{downloadingTrack === track.isrc ? ( {downloadingTrack === track.isrc ? (
@@ -179,7 +179,6 @@ export function TrackInfo({
{availability ? ( {availability ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TidalIcon className={`w-4 h-4 ${availability.tidal ? "text-green-500" : "text-red-500"}`} /> <TidalIcon className={`w-4 h-4 ${availability.tidal ? "text-green-500" : "text-red-500"}`} />
<DeezerIcon className={`w-4 h-4 ${availability.deezer ? "text-green-500" : "text-red-500"}`} />
<QobuzIcon className={`w-4 h-4 ${availability.qobuz ? "text-green-500" : "text-red-500"}`} /> <QobuzIcon className={`w-4 h-4 ${availability.qobuz ? "text-green-500" : "text-red-500"}`} />
<AmazonIcon className={`w-4 h-4 ${availability.amazon ? "text-green-500" : "text-red-500"}`} /> <AmazonIcon className={`w-4 h-4 ${availability.amazon ? "text-green-500" : "text-red-500"}`} />
</div> </div>
+3 -4
View File
@@ -16,7 +16,7 @@ import {
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import type { TrackMetadata, TrackAvailability } from "@/types/api"; import type { TrackMetadata, TrackAvailability } from "@/types/api";
import { TidalIcon, DeezerIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons"; import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
interface TrackListProps { interface TrackListProps {
tracks: TrackMetadata[]; tracks: TrackMetadata[];
@@ -49,7 +49,7 @@ interface TrackListProps {
downloadingCoverTrack?: string | null; downloadingCoverTrack?: string | null;
onToggleTrack: (isrc: string) => void; onToggleTrack: (isrc: string) => void;
onToggleSelectAll: (tracks: TrackMetadata[]) => void; onToggleSelectAll: (tracks: TrackMetadata[]) => void;
onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number) => void; onDownloadTrack: (isrc: string, name: string, artists: string, albumName: string, spotifyId?: string, folderName?: string, durationMs?: number, position?: number, albumArtist?: string, releaseDate?: string) => void;
onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void; onDownloadLyrics?: (spotifyId: string, name: string, artists: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number) => void;
onCheckAvailability?: (spotifyId: string, isrc?: string) => void; onCheckAvailability?: (spotifyId: string, isrc?: string) => void;
onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void; onDownloadCover?: (coverUrl: string, trackName: string, artistName: string, albumName: string, folderName?: string, isArtistDiscography?: boolean, position?: number, trackId?: string) => void;
@@ -301,7 +301,7 @@ export function TrackList({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
onClick={() => onClick={() =>
onDownloadTrack(track.isrc, track.name, track.artists, track.album_name, track.spotify_id, folderName, track.duration_ms, startIndex + index + 1) 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)
} }
size="sm" size="sm"
disabled={isDownloading || downloadingTrack === track.isrc} disabled={isDownloading || downloadingTrack === track.isrc}
@@ -415,7 +415,6 @@ export function TrackList({
{availabilityMap?.has(track.spotify_id) ? ( {availabilityMap?.has(track.spotify_id) ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TidalIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.tidal ? "text-green-500" : "text-red-500"}`} /> <TidalIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.tidal ? "text-green-500" : "text-red-500"}`} />
<DeezerIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.deezer ? "text-green-500" : "text-red-500"}`} />
<QobuzIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.qobuz ? "text-green-500" : "text-red-500"}`} /> <QobuzIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.qobuz ? "text-green-500" : "text-red-500"}`} />
<AmazonIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.amazon ? "text-green-500" : "text-red-500"}`} /> <AmazonIcon className={`w-4 h-4 ${availabilityMap.get(track.spotify_id)?.amazon ? "text-green-500" : "text-red-500"}`} />
</div> </div>
+43 -74
View File
@@ -31,7 +31,9 @@ export function useDownload() {
position?: number, position?: number,
spotifyId?: string, spotifyId?: string,
durationMs?: number, durationMs?: number,
releaseYear?: string releaseYear?: string,
albumArtist?: string,
releaseDate?: string
) => { ) => {
const service = settings.downloader; const service = settings.downloader;
@@ -109,6 +111,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -127,46 +131,13 @@ export function useDownload() {
logger.success(`tidal: ${trackName} - ${artistName}`); logger.success(`tidal: ${trackName} - ${artistName}`);
return tidalResponse; return tidalResponse;
} }
logger.warning(`tidal failed, trying deezer...`); logger.warning(`tidal failed, trying amazon...`);
} catch (tidalErr) { } catch (tidalErr) {
logger.error(`tidal error: ${tidalErr}`); logger.error(`tidal error: ${tidalErr}`);
} }
} }
// Try Deezer second // Try Amazon second
if (streamingURLs?.deezer_url) {
try {
logger.debug(`trying deezer for: ${trackName} - ${artistName}`);
const deezerResponse = await downloadTrack({
isrc,
service: "deezer",
query,
track_name: trackName,
artist_name: artistName,
album_name: albumName,
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,
service_url: streamingURLs.deezer_url,
item_id: itemID,
});
if (deezerResponse.success) {
logger.success(`deezer: ${trackName} - ${artistName}`);
return deezerResponse;
}
logger.warning(`deezer failed, trying amazon...`);
} catch (deezerErr) {
logger.error(`deezer error: ${deezerErr}`);
}
}
// Try Amazon third
if (streamingURLs?.amazon_url) { if (streamingURLs?.amazon_url) {
try { try {
logger.debug(`trying amazon for: ${trackName} - ${artistName}`); logger.debug(`trying amazon for: ${trackName} - ${artistName}`);
@@ -177,6 +148,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -208,6 +181,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -244,11 +219,13 @@ export function useDownload() {
const singleServiceResponse = await downloadTrack({ const singleServiceResponse = await downloadTrack({
isrc, isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon", service: service as "tidal" | "qobuz" | "amazon",
query, query,
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -283,7 +260,9 @@ export function useDownload() {
spotifyId?: string, spotifyId?: string,
durationMs?: number, durationMs?: number,
isAlbum?: boolean, isAlbum?: boolean,
releaseYear?: string releaseYear?: string,
albumArtist?: string,
releaseDate?: string
) => { ) => {
const service = settings.downloader; const service = settings.downloader;
@@ -356,6 +335,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -378,37 +359,7 @@ export function useDownload() {
} }
} }
// Try Deezer second // Try Amazon second
if (streamingURLs?.deezer_url) {
try {
const deezerResponse = await downloadTrack({
isrc,
service: "deezer",
query,
track_name: trackName,
artist_name: artistName,
album_name: albumName,
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,
service_url: streamingURLs.deezer_url,
item_id: itemID,
});
if (deezerResponse.success) {
return deezerResponse;
}
} catch (deezerErr) {
console.error("Deezer error:", deezerErr);
}
}
// Try Amazon third
if (streamingURLs?.amazon_url) { if (streamingURLs?.amazon_url) {
try { try {
const amazonResponse = await downloadTrack({ const amazonResponse = await downloadTrack({
@@ -418,6 +369,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -446,6 +399,8 @@ export function useDownload() {
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -481,11 +436,13 @@ export function useDownload() {
const singleServiceResponse = await downloadTrack({ const singleServiceResponse = await downloadTrack({
isrc, isrc,
service: service as "deezer" | "tidal" | "qobuz" | "amazon", service: service as "tidal" | "qobuz" | "amazon",
query, query,
track_name: trackName, track_name: trackName,
artist_name: artistName, artist_name: artistName,
album_name: albumName, album_name: albumName,
album_artist: albumArtist,
release_date: releaseDate,
output_dir: outputDir, output_dir: outputDir,
filename_format: settings.filenameTemplate, filename_format: settings.filenameTemplate,
track_number: settings.trackNumber, track_number: settings.trackNumber,
@@ -515,7 +472,9 @@ export function useDownload() {
spotifyId?: string, spotifyId?: string,
playlistName?: string, playlistName?: string,
durationMs?: number, durationMs?: number,
position?: number position?: number,
albumArtist?: string,
releaseDate?: string
) => { ) => {
if (!isrc) { if (!isrc) {
toast.error("No ISRC found for this track"); toast.error("No ISRC found for this track");
@@ -528,6 +487,9 @@ export function useDownload() {
try { try {
// Single track download - use playlistName if provided for folder structure // Single track download - use playlistName if provided for folder structure
// Extract year from release_date (format: YYYY-MM-DD or YYYY)
const releaseYear = releaseDate?.substring(0, 4);
const response = await downloadWithAutoFallback( const response = await downloadWithAutoFallback(
isrc, isrc,
settings, settings,
@@ -537,7 +499,10 @@ export function useDownload() {
playlistName, playlistName,
position, // Pass position for track numbering position, // Pass position for track numbering
spotifyId, spotifyId,
durationMs durationMs,
releaseYear,
albumArtist || "",
releaseDate
); );
if (response.success) { if (response.success) {
@@ -636,7 +601,9 @@ export function useDownload() {
track?.spotify_id, track?.spotify_id,
track?.duration_ms, track?.duration_ms,
isAlbum, isAlbum,
releaseYear releaseYear,
track?.album_artist || "", // Use album_artist from Spotify metadata
track?.release_date
); );
if (response.success) { if (response.success) {
@@ -767,7 +734,9 @@ export function useDownload() {
track.spotify_id, track.spotify_id,
track.duration_ms, track.duration_ms,
isAlbum, isAlbum,
releaseYear releaseYear,
track.album_artist || "", // Use album_artist from Spotify metadata
track.release_date
); );
if (response.success) { if (response.success) {
+1 -1
View File
@@ -10,7 +10,7 @@ export type FilenamePreset = "title" | "title-artist" | "artist-title" | "track-
export interface Settings { export interface Settings {
downloadPath: string; downloadPath: string;
downloader: "auto" | "deezer" | "tidal" | "qobuz" | "amazon"; downloader: "auto" | "tidal" | "qobuz" | "amazon";
theme: string; theme: string;
themeMode: "auto" | "light" | "dark"; themeMode: "auto" | "light" | "dark";
fontFamily: FontFamily; fontFamily: FontFamily;
+5 -3
View File
@@ -8,10 +8,12 @@ export interface TrackMetadata {
artists: string; artists: string;
name: string; name: string;
album_name: string; album_name: string;
album_artist?: string;
duration_ms: number; duration_ms: number;
images: string; images: string;
release_date: string; release_date: string;
track_number: number; track_number: number;
disc_number?: number;
external_urls: string; external_urls: string;
isrc: string; isrc: string;
album_type?: string; album_type?: string;
@@ -109,11 +111,13 @@ export type SpotifyMetadataResponse =
export interface DownloadRequest { export interface DownloadRequest {
isrc: string; isrc: string;
service: "deezer" | "tidal" | "qobuz" | "amazon"; service: "tidal" | "qobuz" | "amazon";
query?: string; query?: string;
track_name?: string; track_name?: string;
artist_name?: string; artist_name?: string;
album_name?: string; album_name?: string;
album_artist?: string;
release_date?: string;
api_url?: string; api_url?: string;
output_dir?: string; output_dir?: string;
audio_format?: string; audio_format?: string;
@@ -193,11 +197,9 @@ export interface LyricsDownloadResponse {
export interface TrackAvailability { export interface TrackAvailability {
spotify_id: string; spotify_id: string;
tidal: boolean; tidal: boolean;
deezer: boolean;
amazon: boolean; amazon: boolean;
qobuz: boolean; qobuz: boolean;
tidal_url?: string; tidal_url?: string;
deezer_url?: string;
amazon_url?: string; amazon_url?: string;
qobuz_url?: string; qobuz_url?: string;
} }
-2
View File
@@ -2,8 +2,6 @@
"vogel.qqdl.site", "vogel.qqdl.site",
"maus.qqdl.site", "maus.qqdl.site",
"hund.qqdl.site", "hund.qqdl.site",
"eu-maus.qqdl.site",
"eu-katze.qqdl.site",
"katze.qqdl.site", "katze.qqdl.site",
"wolf.qqdl.site", "wolf.qqdl.site",
"tidal.kinoplus.online", "tidal.kinoplus.online",