151 lines
3.9 KiB
Go
151 lines
3.9 KiB
Go
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
|
|
}
|