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 }