v6.8
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -37,25 +37,27 @@ type SpotifyMetadataRequest struct {
|
|||||||
|
|
||||||
// DownloadRequest represents the request structure for downloading tracks
|
// DownloadRequest represents the request structure for downloading tracks
|
||||||
type DownloadRequest struct {
|
type DownloadRequest struct {
|
||||||
ISRC string `json:"isrc"`
|
ISRC string `json:"isrc"`
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Query string `json:"query,omitempty"`
|
Query string `json:"query,omitempty"`
|
||||||
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"`
|
||||||
ApiURL string `json:"api_url,omitempty"`
|
AlbumArtist string `json:"album_artist,omitempty"`
|
||||||
OutputDir string `json:"output_dir,omitempty"`
|
ReleaseDate string `json:"release_date,omitempty"`
|
||||||
AudioFormat string `json:"audio_format,omitempty"`
|
ApiURL string `json:"api_url,omitempty"`
|
||||||
FilenameFormat string `json:"filename_format,omitempty"`
|
OutputDir string `json:"output_dir,omitempty"`
|
||||||
TrackNumber bool `json:"track_number,omitempty"`
|
AudioFormat string `json:"audio_format,omitempty"`
|
||||||
Position int `json:"position,omitempty"` // Position in playlist/album (1-based)
|
FilenameFormat string `json:"filename_format,omitempty"`
|
||||||
UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"` // Use album track number instead of playlist position
|
TrackNumber bool `json:"track_number,omitempty"`
|
||||||
SpotifyID string `json:"spotify_id,omitempty"` // Spotify track ID
|
Position int `json:"position,omitempty"` // Position in playlist/album (1-based)
|
||||||
EmbedLyrics bool `json:"embed_lyrics,omitempty"` // Whether to embed lyrics into the audio file
|
UseAlbumTrackNumber bool `json:"use_album_track_number,omitempty"` // Use album track number instead of playlist position
|
||||||
|
SpotifyID string `json:"spotify_id,omitempty"` // Spotify track ID
|
||||||
|
EmbedLyrics bool `json:"embed_lyrics,omitempty"` // Whether to embed lyrics into the audio file
|
||||||
EmbedMaxQualityCover bool `json:"embed_max_quality_cover,omitempty"` // Whether to embed max quality cover art
|
EmbedMaxQualityCover bool `json:"embed_max_quality_cover,omitempty"` // Whether to embed max quality cover art
|
||||||
ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call
|
ServiceURL string `json:"service_url,omitempty"` // Direct service URL (Tidal/Deezer/Amazon) to skip song.link API call
|
||||||
Duration int `json:"duration,omitempty"` // Track duration in seconds for better matching
|
Duration int `json:"duration,omitempty"` // Track duration in seconds for better matching
|
||||||
ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
|
ItemID string `json:"item_id,omitempty"` // Optional queue item ID for multi-service fallback tracking
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadResponse represents the response structure for download operations
|
// DownloadResponse represents the response structure for download operations
|
||||||
@@ -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()
|
return DownloadResponse{
|
||||||
if req.ServiceURL != "" {
|
Success: false,
|
||||||
// Use provided URL directly
|
Error: fmt.Sprintf("Unknown service: %s", req.Service),
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.UseAlbumTrackNumber)
|
}, fmt.Errorf("unknown service: %s", req.Service)
|
||||||
} else {
|
|
||||||
if req.SpotifyID == "" {
|
|
||||||
return DownloadResponse{
|
|
||||||
Success: false,
|
|
||||||
Error: "Spotify ID is required for Deezer",
|
|
||||||
}, fmt.Errorf("spotify ID is required for Deezer")
|
|
||||||
}
|
|
||||||
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 {
|
||||||
@@ -314,9 +307,9 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
fmt.Printf("Track: %s\n", trackName)
|
fmt.Printf("Track: %s\n", trackName)
|
||||||
fmt.Printf("Artist: %s\n", artistName)
|
fmt.Printf("Artist: %s\n", artistName)
|
||||||
fmt.Println("Searching all sources...")
|
fmt.Println("Searching all sources...")
|
||||||
|
|
||||||
lyricsClient := backend.NewLyricsClient()
|
lyricsClient := backend.NewLyricsClient()
|
||||||
|
|
||||||
// Try all sources with fallbacks
|
// Try all sources with fallbacks
|
||||||
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -324,29 +317,29 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if lyricsResp == nil || len(lyricsResp.Lines) == 0 {
|
if lyricsResp == nil || len(lyricsResp.Lines) == 0 {
|
||||||
fmt.Println("No lyrics content found")
|
fmt.Println("No lyrics content found")
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Lyrics found from: %s\n", source)
|
fmt.Printf("Lyrics found from: %s\n", source)
|
||||||
fmt.Printf("Sync type: %s\n", lyricsResp.SyncType)
|
fmt.Printf("Sync type: %s\n", lyricsResp.SyncType)
|
||||||
fmt.Printf("Total lines: %d\n", len(lyricsResp.Lines))
|
fmt.Printf("Total lines: %d\n", len(lyricsResp.Lines))
|
||||||
|
|
||||||
lyrics := lyricsClient.ConvertToLRC(lyricsResp, trackName, artistName)
|
lyrics := lyricsClient.ConvertToLRC(lyricsResp, trackName, artistName)
|
||||||
if lyrics == "" {
|
if lyrics == "" {
|
||||||
fmt.Println("No lyrics content to embed")
|
fmt.Println("No lyrics content to embed")
|
||||||
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show full lyrics in console for debugging
|
// Show full lyrics in console for debugging
|
||||||
fmt.Printf("\n--- Full LRC Content ---\n")
|
fmt.Printf("\n--- Full LRC Content ---\n")
|
||||||
fmt.Println(lyrics)
|
fmt.Println(lyrics)
|
||||||
fmt.Printf("--- End LRC Content ---\n\n")
|
fmt.Printf("--- End LRC Content ---\n\n")
|
||||||
|
|
||||||
fmt.Printf("Embedding into: %s\n", filePath)
|
fmt.Printf("Embedding into: %s\n", filePath)
|
||||||
if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil {
|
if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil {
|
||||||
fmt.Printf("Failed to embed lyrics: %v\n", err)
|
fmt.Printf("Failed to embed lyrics: %v\n", err)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user