This commit is contained in:
afkarxyz
2025-11-24 14:52:47 +07:00
parent 73d8205f6f
commit 6ee3c2f653
22 changed files with 865 additions and 253 deletions
+23 -5
View File
@@ -356,7 +356,7 @@ func (a *AmazonDownloader) DownloadFromService(amazonURL, outputDir string) (str
return "", fmt.Errorf("all regions failed. Last error: %v", lastError)
}
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
// Create output directory if needed
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
@@ -364,12 +364,19 @@ func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filena
}
}
// Get Amazon URL from Spotify track ID
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
if err != nil {
return "", err
// Check if file with expected name already exists (Amazon doesn't provide ISRC before download)
if spotifyTrackName != "" && spotifyArtistName != "" {
expectedFilename := BuildExpectedFilename(spotifyTrackName, spotifyArtistName, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
expectedPath := filepath.Join(outputDir, expectedFilename)
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024))
return "EXISTS:" + expectedPath, nil
}
}
fmt.Printf("Using Amazon URL: %s\n", amazonURL)
// Download from service
filePath, err := a.DownloadFromService(amazonURL, outputDir)
if err != nil {
@@ -410,5 +417,16 @@ func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filena
}
fmt.Println("Done")
fmt.Println("✓ Downloaded successfully from Amazon Music")
return filePath, nil
}
func (a *AmazonDownloader) DownloadBySpotifyID(spotifyTrackID, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
// Get Amazon URL from Spotify track ID
amazonURL, err := a.GetAmazonURLFromSpotify(spotifyTrackID)
if err != nil {
return "", err
}
return a.DownloadByURL(amazonURL, outputDir, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
}
+96 -7
View File
@@ -56,10 +56,75 @@ func NewDeezerDownloader() *DeezerDownloader {
}
}
func (d *DeezerDownloader) GetTrackByISRC(isrc string) (*DeezerTrack, error) {
func (d *DeezerDownloader) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
// Decode base64 API URL
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkuZGVlemVyLmNvbS8yLjAvdHJhY2svaXNyYzo=")
url := fmt.Sprintf("%s%s", string(apiBase), isrc)
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)
}
var songLinkResp struct {
LinksByPlatform map[string]struct {
URL string `json:"url"`
} `json:"linksByPlatform"`
}
if err := json.NewDecoder(resp.Body).Decode(&songLinkResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
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 {
@@ -77,7 +142,7 @@ func (d *DeezerDownloader) GetTrackByISRC(isrc string) (*DeezerTrack, error) {
}
if track.ID == 0 {
return nil, fmt.Errorf("track not found for ISRC: %s", isrc)
return nil, fmt.Errorf("track not found")
}
return &track, nil
@@ -187,10 +252,17 @@ func buildFilename(title, artist string, trackNumber int, format string, include
return filename + ".flac"
}
func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
fmt.Printf("Fetching track info for ISRC: %s\n", isrc)
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)
track, err := d.GetTrackByISRC(isrc)
// 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
}
@@ -234,6 +306,12 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string
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)
@@ -287,5 +365,16 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string
}
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)
}
+71
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"github.com/go-flac/flacpicture"
"github.com/go-flac/flacvorbis"
@@ -111,3 +112,73 @@ func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// ReadISRCFromFile reads ISRC metadata from a FLAC file
func ReadISRCFromFile(filepath string) (string, error) {
if !fileExists(filepath) {
return "", fmt.Errorf("file does not exist")
}
f, err := flac.ParseFile(filepath)
if err != nil {
return "", fmt.Errorf("failed to parse FLAC file: %w", err)
}
// Find VorbisComment block
for _, block := range f.Meta {
if block.Type == flac.VorbisComment {
cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
if err != nil {
continue
}
// Get ISRC field
isrcValues, err := cmt.Get(flacvorbis.FIELD_ISRC)
if err == nil && len(isrcValues) > 0 {
return isrcValues[0], nil
}
}
}
return "", nil // No ISRC found
}
// CheckISRCExists checks if a file with the given ISRC already exists in the directory
func CheckISRCExists(outputDir string, targetISRC string) (string, bool) {
if targetISRC == "" {
return "", false
}
// Read all .flac files in directory
entries, err := os.ReadDir(outputDir)
if err != nil {
return "", false
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
// Check only .flac files
filename := entry.Name()
if len(filename) < 5 || filename[len(filename)-5:] != ".flac" {
continue
}
filepath := fmt.Sprintf("%s/%s", outputDir, filename)
// Read ISRC from file
isrc, err := ReadISRCFromFile(filepath)
if err != nil {
continue
}
// Compare ISRC (case-insensitive)
if isrc != "" && strings.EqualFold(isrc, targetISRC) {
return filepath, true
}
}
return "", false
}
+6
View File
@@ -315,6 +315,12 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma
safeArtist := sanitizeFilename(artists)
safeTitle := sanitizeFilename(trackTitle)
// Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, track.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", track.ISRC, existingFile)
return "EXISTS:" + existingFile, nil
}
// Build filename based on format settings
filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
filepath := filepath.Join(outputDir, filename)
+150
View File
@@ -0,0 +1,150 @@
package backend
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type SongLinkClient struct {
client *http.Client
lastAPICallTime time.Time
apiCallCount int
apiCallResetTime time.Time
}
type SongLinkURLs struct {
TidalURL string `json:"tidal_url"`
DeezerURL string `json:"deezer_url"`
AmazonURL string `json:"amazon_url"`
}
func NewSongLinkClient() *SongLinkClient {
return &SongLinkClient{
client: &http.Client{
Timeout: 30 * time.Second,
},
apiCallResetTime: time.Now(),
}
}
func (s *SongLinkClient) GetAllURLsFromSpotify(spotifyTrackID string) (*SongLinkURLs, error) {
// Rate limiting: max 10 requests per minute (song.link API limit)
now := time.Now()
if now.Sub(s.apiCallResetTime) >= time.Minute {
s.apiCallCount = 0
s.apiCallResetTime = now
}
// If we've hit the limit, wait until the next minute
if s.apiCallCount >= 9 {
waitTime := time.Minute - now.Sub(s.apiCallResetTime)
if waitTime > 0 {
fmt.Printf("Rate limit reached, waiting %v...\n", waitTime.Round(time.Second))
time.Sleep(waitTime)
s.apiCallCount = 0
s.apiCallResetTime = time.Now()
}
}
// Add delay between requests (7 seconds to be safe)
if !s.lastAPICallTime.IsZero() {
timeSinceLastCall := now.Sub(s.lastAPICallTime)
minDelay := 7 * time.Second
if timeSinceLastCall < minDelay {
waitTime := minDelay - timeSinceLastCall
fmt.Printf("Rate limiting: waiting %v...\n", waitTime.Round(time.Second))
time.Sleep(waitTime)
}
}
// 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), url.QueryEscape(spotifyURL))
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
fmt.Println("Getting streaming URLs from song.link...")
// Retry logic for rate limit errors
maxRetries := 3
var resp *http.Response
for i := 0; i < maxRetries; i++ {
resp, err = s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get URLs: %w", err)
}
// Update rate limit tracking
s.lastAPICallTime = time.Now()
s.apiCallCount++
if resp.StatusCode == 429 {
resp.Body.Close()
if i < maxRetries-1 {
waitTime := 15 * time.Second
fmt.Printf("Rate limited by API, waiting %v before retry...\n", waitTime)
time.Sleep(waitTime)
continue
}
return nil, fmt.Errorf("API rate limit exceeded after %d retries", maxRetries)
}
if resp.StatusCode != 200 {
resp.Body.Close()
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
break
}
defer resp.Body.Close()
var songLinkResp struct {
LinksByPlatform map[string]struct {
URL string `json:"url"`
} `json:"linksByPlatform"`
}
if err := json.NewDecoder(resp.Body).Decode(&songLinkResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
urls := &SongLinkURLs{}
// Extract Tidal URL
if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" {
urls.TidalURL = tidalLink.URL
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
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
amazonURL := amazonLink.URL
// Convert album URL to track URL if needed
if len(amazonURL) > 0 {
urls.AmazonURL = amazonURL
fmt.Printf("✓ Amazon URL found\n")
}
}
// Check if at least one URL was found
if urls.TidalURL == "" && urls.DeezerURL == "" && urls.AmazonURL == "" {
return nil, fmt.Errorf("no streaming URLs found")
}
return urls, nil
}
+2 -7
View File
@@ -271,11 +271,6 @@ type artistResponse struct {
Popularity int `json:"popularity"`
}
type artistAlbumsResponse struct {
Items []albumSimplified `json:"items"`
Next string `json:"next"`
}
type playlistRaw struct {
Data playlistResponse
BatchEnabled bool
@@ -321,7 +316,7 @@ func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL
return nil, err
}
return c.processSpotifyData(ctx, raw, parsed.Type)
return c.processSpotifyData(ctx, raw)
}
func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, token string, batch bool, delay time.Duration) (interface{}, error) {
@@ -341,7 +336,7 @@ func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed sp
}
}
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}, dataType string) (interface{}, error) {
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}) (interface{}, error) {
switch payload := raw.(type) {
case *playlistRaw:
return c.formatPlaylistData(payload), nil
+168 -79
View File
@@ -22,13 +22,6 @@ type TidalDownloader struct {
apiURL string
}
type TidalSearchResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
TotalNumberOfItems int `json:"totalNumberOfItems"`
Items []TidalTrack `json:"items"`
}
type TidalTrack struct {
ID int64 `json:"id"`
Title string `json:"title"`
@@ -68,6 +61,26 @@ func NewTidalDownloader(apiURL string) *TidalDownloader {
clientID, _ := base64.StdEncoding.DecodeString("NkJEU1JkcEs5aHFFQlRnVQ==")
clientSecret, _ := base64.StdEncoding.DecodeString("eGV1UG1ZN25icFo5SUliTEFjUTkzc2hrYTFWTmhlVUFxTjZJY3N6alRHOD0=")
// If apiURL is empty, try to get first available API
if apiURL == "" {
downloader := &TidalDownloader{
client: &http.Client{
Timeout: 60 * time.Second,
},
timeout: 30 * time.Second,
maxRetries: 3,
clientID: string(clientID),
clientSecret: string(clientSecret),
apiURL: "",
}
// Try to get available APIs
apis, err := downloader.GetAvailableAPIs()
if err == nil && len(apis) > 0 {
apiURL = apis[0] // Use first available API
}
}
return &TidalDownloader{
client: &http.Client{
Timeout: 60 * time.Second,
@@ -155,17 +168,83 @@ func (t *TidalDownloader) GetAccessToken() (string, error) {
return result.AccessToken, nil
}
func (t *TidalDownloader) SearchTracks(query string) (*TidalSearchResponse, error) {
func (t *TidalDownloader) GetTidalURLFromSpotify(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), url.QueryEscape(spotifyURL))
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
fmt.Println("Getting Tidal URL...")
resp, err := t.client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to get Tidal URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("API returned status %d", resp.StatusCode)
}
var songLinkResp struct {
LinksByPlatform map[string]struct {
URL string `json:"url"`
} `json:"linksByPlatform"`
}
if err := json.NewDecoder(resp.Body).Decode(&songLinkResp); err != nil {
return "", fmt.Errorf("failed to decode response: %w", err)
}
tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]
if !ok || tidalLink.URL == "" {
return "", fmt.Errorf("tidal link not found")
}
tidalURL := tidalLink.URL
fmt.Printf("Found Tidal URL: %s\n", tidalURL)
return tidalURL, nil
}
func (t *TidalDownloader) GetTrackIDFromURL(tidalURL string) (int64, error) {
// Extract track ID from Tidal URL
// Format: https://listen.tidal.com/track/441821360
// or: https://tidal.com/browse/track/123456789
parts := strings.Split(tidalURL, "/track/")
if len(parts) < 2 {
return 0, fmt.Errorf("invalid tidal 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 (t *TidalDownloader) GetTrackInfoByID(trackID int64) (*TidalTrack, error) {
token, err := t.GetAccessToken()
if err != nil {
return nil, fmt.Errorf("failed to get access token: %w", err)
}
// Decode base64 API URL and encode the query parameter
searchBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkudGlkYWwuY29tL3YxL3NlYXJjaC90cmFja3M/cXVlcnk9")
searchURL := fmt.Sprintf("%s%s&limit=25&offset=0&countryCode=US", string(searchBase), url.QueryEscape(query))
// Decode base64 API URL
trackBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9hcGkudGlkYWwuY29tL3YxL3RyYWNrcy8=")
trackURL := fmt.Sprintf("%s%d?countryCode=US", string(trackBase), trackID)
req, err := http.NewRequest("GET", searchURL, nil)
req, err := http.NewRequest("GET", trackURL, nil)
if err != nil {
return nil, err
}
@@ -180,106 +259,55 @@ func (t *TidalDownloader) SearchTracks(query string) (*TidalSearchResponse, erro
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("search failed: HTTP %d - %s", resp.StatusCode, string(body))
return nil, fmt.Errorf("failed to get track info: HTTP %d - %s", resp.StatusCode, string(body))
}
var result TidalSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
var trackInfo TidalTrack
if err := json.NewDecoder(resp.Body).Decode(&trackInfo); err != nil {
return nil, err
}
return &result, nil
}
func (t *TidalDownloader) GetTrackInfo(query, isrc string) (*TidalTrack, error) {
fmt.Printf("Fetching: %s", query)
if isrc != "" {
fmt.Printf(" (ISRC: %s)", isrc)
}
fmt.Println()
result, err := t.SearchTracks(query)
if err != nil {
return nil, err
}
if len(result.Items) == 0 {
return nil, fmt.Errorf("no tracks found for query: %s", query)
}
var selectedTrack *TidalTrack
if isrc != "" {
var isrcMatches []TidalTrack
for _, item := range result.Items {
if item.ISRC == isrc {
isrcMatches = append(isrcMatches, item)
}
}
if len(isrcMatches) > 1 {
for _, item := range isrcMatches {
for _, tag := range item.MediaMetadata.Tags {
if tag == "HIRES_LOSSLESS" {
selectedTrack = &item
break
}
}
if selectedTrack != nil {
break
}
}
if selectedTrack == nil {
selectedTrack = &isrcMatches[0]
}
} else if len(isrcMatches) == 1 {
selectedTrack = &isrcMatches[0]
} else {
selectedTrack = &result.Items[0]
}
} else {
selectedTrack = &result.Items[0]
}
if selectedTrack == nil {
return nil, fmt.Errorf("track not found")
}
fmt.Printf("Found: %s (%s)\n", selectedTrack.Title, selectedTrack.AudioQuality)
return selectedTrack, nil
fmt.Printf("Found: %s (%s)\n", trackInfo.Title, trackInfo.AudioQuality)
return &trackInfo, nil
}
func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string, error) {
fmt.Println("Fetching URL...")
url := fmt.Sprintf("%s/track/?id=%d&quality=%s", t.apiURL, trackID, quality)
fmt.Printf("Tidal API URL: %s\n", url)
resp, err := t.client.Get(url)
if err != nil {
fmt.Printf("✗ Tidal API request failed: %v\n", err)
return "", fmt.Errorf("failed to get download URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("✗ Tidal API returned status code: %d\n", resp.StatusCode)
return "", fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
var apiResponses []TidalAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResponses); err != nil {
fmt.Printf("✗ Failed to decode Tidal API response: %v\n", err)
return "", fmt.Errorf("failed to decode response: %w", err)
}
if len(apiResponses) == 0 {
fmt.Println("✗ Tidal API returned empty response")
return "", fmt.Errorf("no download URL in response")
}
for _, item := range apiResponses {
if item.OriginalTrackURL != "" {
fmt.Println("URL found")
fmt.Println("✓ Tidal download URL found")
return item.OriginalTrackURL, nil
}
}
fmt.Println("✗ No valid download URL in Tidal API response")
return "", fmt.Errorf("download URL not found in response")
}
@@ -333,14 +361,23 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
return nil
}
func (t *TidalDownloader) Download(query, isrc, 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 string, useAlbumTrackNumber bool) (string, error) {
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err)
}
}
trackInfo, err := t.GetTrackInfo(query, isrc)
fmt.Printf("Using Tidal URL: %s\n", tidalURL)
// Extract track ID from URL
trackID, err := t.GetTrackIDFromURL(tidalURL)
if err != nil {
return "", err
}
// Get track info by ID
trackInfo, err := t.GetTrackInfoByID(trackID)
if err != nil {
return "", err
}
@@ -385,6 +422,12 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm
albumTitle = trackInfo.Album.Title
}
// Check if file with same ISRC already exists
if existingFile, exists := CheckISRCExists(outputDir, trackInfo.ISRC); exists {
fmt.Printf("File with ISRC %s already exists: %s\n", trackInfo.ISRC, existingFile)
return "EXISTS:" + existingFile, nil
}
// Build filename based on format settings
filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position, useAlbumTrackNumber)
outputFilename := filepath.Join(outputDir, filename)
@@ -454,22 +497,68 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm
}
fmt.Println("Done")
fmt.Println("✓ Downloaded successfully from Tidal")
return outputFilename, nil
}
func (t *TidalDownloader) DownloadWithFallback(query, isrc, 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 string, useAlbumTrackNumber bool) (string, error) {
apis, err := t.GetAvailableAPIs()
if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err)
}
var lastError error
for i, apiURL := range apis {
fmt.Printf("[Tidal API %d/%d] Trying: %s\n", i+1, len(apis), apiURL)
fallbackDownloader := NewTidalDownloader(apiURL)
result, err := fallbackDownloader.DownloadByURL(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
if err == nil {
fmt.Printf("✓ Success with: %s\n", apiURL)
return result, nil
}
lastError = err
errMsg := err.Error()
if len(errMsg) > 80 {
errMsg = errMsg[:80]
}
fmt.Printf("✗ Failed with %s: %s\n", apiURL, errMsg)
}
return "", fmt.Errorf("all %d Tidal APIs failed. Last error: %v", len(apis), lastError)
}
func (t *TidalDownloader) Download(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
// Get Tidal URL from Spotify track ID
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
return "", err
}
return t.DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
}
func (t *TidalDownloader) DownloadWithFallback(spotifyTrackID, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string, useAlbumTrackNumber bool) (string, error) {
apis, err := t.GetAvailableAPIs()
if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err)
}
// Get Tidal URL once
tidalURL, err := t.GetTidalURLFromSpotify(spotifyTrackID)
if err != nil {
return "", err
}
var lastError error
for i, apiURL := range apis {
fmt.Printf("[Auto Fallback %d/%d] Trying: %s\n", i+1, len(apis), apiURL)
fallbackDownloader := NewTidalDownloader(apiURL)
result, err := fallbackDownloader.Download(query, isrc, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
result, err := fallbackDownloader.DownloadByURL(tidalURL, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useAlbumTrackNumber)
if err == nil {
fmt.Printf("✓ Success with: %s\n", apiURL)
return result, nil