455 lines
12 KiB
Go
455 lines
12 KiB
Go
package backend
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type SongLinkClient struct {
|
|
client *http.Client
|
|
lastAPICallTime time.Time
|
|
apiCallCount int
|
|
apiCallResetTime time.Time
|
|
}
|
|
|
|
type SongLinkURLs struct {
|
|
TidalURL string `json:"tidal_url"`
|
|
AmazonURL string `json:"amazon_url"`
|
|
}
|
|
|
|
type TrackAvailability struct {
|
|
SpotifyID string `json:"spotify_id"`
|
|
Tidal bool `json:"tidal"`
|
|
Amazon bool `json:"amazon"`
|
|
Qobuz bool `json:"qobuz"`
|
|
TidalURL string `json:"tidal_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, region string) (*SongLinkURLs, error) {
|
|
|
|
now := time.Now()
|
|
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
|
s.apiCallCount = 0
|
|
s.apiCallResetTime = now
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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))
|
|
|
|
if region != "" {
|
|
apiURL += fmt.Sprintf("&userCountry=%s", region)
|
|
}
|
|
|
|
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...")
|
|
|
|
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)
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &songLinkResp); err != nil {
|
|
|
|
bodyStr := string(body)
|
|
if len(bodyStr) > 200 {
|
|
bodyStr = bodyStr[:200] + "..."
|
|
}
|
|
return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
|
|
}
|
|
|
|
urls := &SongLinkURLs{}
|
|
|
|
if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" {
|
|
urls.TidalURL = tidalLink.URL
|
|
fmt.Printf("✓ Tidal URL found\n")
|
|
}
|
|
|
|
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
|
|
amazonURL := amazonLink.URL
|
|
|
|
if len(amazonURL) > 0 {
|
|
urls.AmazonURL = amazonURL
|
|
fmt.Printf("✓ Amazon URL found\n")
|
|
}
|
|
}
|
|
|
|
if urls.TidalURL == "" && urls.AmazonURL == "" {
|
|
return nil, fmt.Errorf("no streaming URLs found")
|
|
}
|
|
|
|
return urls, nil
|
|
}
|
|
|
|
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) {
|
|
|
|
now := time.Now()
|
|
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
|
s.apiCallCount = 0
|
|
s.apiCallResetTime = now
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &songLinkResp); err != nil {
|
|
|
|
bodyStr := string(body)
|
|
if len(bodyStr) > 200 {
|
|
bodyStr = bodyStr[:200] + "..."
|
|
}
|
|
return nil, fmt.Errorf("failed to decode response: %w (response: %s)", err, bodyStr)
|
|
}
|
|
|
|
availability := &TrackAvailability{
|
|
SpotifyID: spotifyTrackID,
|
|
}
|
|
|
|
if tidalLink, ok := songLinkResp.LinksByPlatform["tidal"]; ok && tidalLink.URL != "" {
|
|
availability.Tidal = true
|
|
availability.TidalURL = tidalLink.URL
|
|
}
|
|
|
|
if amazonLink, ok := songLinkResp.LinksByPlatform["amazonMusic"]; ok && amazonLink.URL != "" {
|
|
availability.Amazon = true
|
|
availability.AmazonURL = amazonLink.URL
|
|
}
|
|
|
|
if deezerLink, ok := songLinkResp.LinksByPlatform["deezer"]; ok && deezerLink.URL != "" {
|
|
deezerURL := deezerLink.URL
|
|
|
|
deezerISRC, err := GetDeezerISRC(deezerURL)
|
|
if err == nil && deezerISRC != "" {
|
|
qobuzAvailable := checkQobuzAvailability(deezerISRC)
|
|
availability.Qobuz = qobuzAvailable
|
|
}
|
|
}
|
|
|
|
return availability, nil
|
|
}
|
|
|
|
func checkQobuzAvailability(isrc string) bool {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
appID := "798273057"
|
|
|
|
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
|
|
}
|
|
|
|
func (s *SongLinkClient) GetDeezerURLFromSpotify(spotifyTrackID string) (string, error) {
|
|
|
|
now := time.Now()
|
|
if now.Sub(s.apiCallResetTime) >= time.Minute {
|
|
s.apiCallCount = 0
|
|
s.apiCallResetTime = now
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 Deezer URL from song.link...")
|
|
|
|
maxRetries := 3
|
|
var resp *http.Response
|
|
for i := 0; i < maxRetries; i++ {
|
|
resp, err = s.client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get Deezer URL: %w", err)
|
|
}
|
|
|
|
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 "", fmt.Errorf("API rate limit exceeded after %d retries", maxRetries)
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
resp.Body.Close()
|
|
return "", 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 "", 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 GetDeezerISRC(deezerURL string) (string, error) {
|
|
|
|
var trackID string
|
|
if strings.Contains(deezerURL, "/track/") {
|
|
parts := strings.Split(deezerURL, "/track/")
|
|
if len(parts) > 1 {
|
|
trackID = strings.Split(parts[1], "?")[0]
|
|
trackID = strings.TrimSpace(trackID)
|
|
}
|
|
}
|
|
|
|
if trackID == "" {
|
|
return "", fmt.Errorf("could not extract track ID from Deezer URL: %s", deezerURL)
|
|
}
|
|
|
|
apiURL := fmt.Sprintf("https://api.deezer.com/track/%s", trackID)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Get(apiURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to call Deezer API: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("Deezer API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var deezerTrack struct {
|
|
ID int64 `json:"id"`
|
|
ISRC string `json:"isrc"`
|
|
Title string `json:"title"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&deezerTrack); err != nil {
|
|
return "", fmt.Errorf("failed to decode Deezer API response: %w", err)
|
|
}
|
|
|
|
if deezerTrack.ISRC == "" {
|
|
return "", fmt.Errorf("ISRC not found in Deezer API response for track %s", trackID)
|
|
}
|
|
|
|
fmt.Printf("Found ISRC from Deezer: %s (track: %s)\n", deezerTrack.ISRC, deezerTrack.Title)
|
|
return deezerTrack.ISRC, nil
|
|
}
|