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"` } // TrackAvailability represents the availability of a track on different platforms type TrackAvailability struct { SpotifyID string `json:"spotify_id"` Tidal bool `json:"tidal"` Deezer bool `json:"deezer"` Amazon bool `json:"amazon"` Qobuz bool `json:"qobuz"` TidalURL string `json:"tidal_url,omitempty"` DeezerURL string `json:"deezer_url,omitempty"` AmazonURL string `json:"amazon_url,omitempty"` QobuzURL string `json:"qobuz_url,omitempty"` } 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 } // CheckTrackAvailability checks the availability of a track on different platforms func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, 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.Printf("Checking availability for track: %s\n", spotifyTrackID) // 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 check availability: %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) } availability := &TrackAvailability{ SpotifyID: spotifyTrackID, } // Check Tidal if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" { availability.Tidal = true availability.TidalURL = tidalLink.URL } // Check Deezer if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" { availability.Deezer = true availability.DeezerURL = deezerLink.URL } // Check Amazon if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" { availability.Amazon = true availability.AmazonURL = amazonLink.URL } // Check Qobuz using ISRC (song.link doesn't support Qobuz) if isrc != "" { qobuzAvailable := checkQobuzAvailability(isrc) availability.Qobuz = qobuzAvailable } return availability, nil } // checkQobuzAvailability checks if a track is available on Qobuz using ISRC func checkQobuzAvailability(isrc string) bool { client := &http.Client{Timeout: 10 * time.Second} appID := "798273057" // Decode base64 API URL apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly93d3cucW9idXouY29tL2FwaS5qc29uLzAuMi90cmFjay9zZWFyY2g/cXVlcnk9") searchURL := fmt.Sprintf("%s%s&limit=1&app_id=%s", string(apiBase), isrc, appID) resp, err := client.Get(searchURL) if err != nil { return false } defer resp.Body.Close() if resp.StatusCode != 200 { return false } var searchResp struct { Tracks struct { Total int `json:"total"` } `json:"tracks"` } if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { return false } return searchResp.Tracks.Total > 0 }