mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-03 20:13:03 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
// Package songlink provides a client for the Song.link/Odesli API.
|
||||
// Song.link offers free cross-platform music URL mapping.
|
||||
package songlink
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
apiBase = "https://api.song.link/v1-alpha.1"
|
||||
minRequestInterval = 7 * time.Second
|
||||
maxRequestsPerMinute = 9
|
||||
)
|
||||
|
||||
// PlatformLink represents a link to a track on a specific platform
|
||||
type PlatformLink struct {
|
||||
Platform string `json:"platform"`
|
||||
URL string `json:"url"`
|
||||
EntityType string `json:"entity_type"`
|
||||
ID string `json:"id,omitempty"`
|
||||
NativeURI string `json:"native_uri,omitempty"`
|
||||
}
|
||||
|
||||
// CrossPlatformLinks holds links for a track across multiple platforms
|
||||
type CrossPlatformLinks struct {
|
||||
SpotifyID string `json:"spotify_id"`
|
||||
ISRC string `json:"isrc,omitempty"`
|
||||
Links map[string]PlatformLink `json:"links"`
|
||||
}
|
||||
|
||||
// Client for Song.link API
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
lastRequestTime time.Time
|
||||
requestCount int
|
||||
countResetTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient creates a new Song.link client
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
countResetTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Configured always returns true for Song.link (no API key needed)
|
||||
func (c *Client) Configured() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) rateLimit() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Reset counter every minute
|
||||
if now.Sub(c.countResetTime) >= time.Minute {
|
||||
c.requestCount = 0
|
||||
c.countResetTime = now
|
||||
}
|
||||
|
||||
// Check if we've hit the per-minute limit
|
||||
if c.requestCount >= maxRequestsPerMinute {
|
||||
waitTime := time.Minute - now.Sub(c.countResetTime)
|
||||
if waitTime > 0 {
|
||||
time.Sleep(waitTime)
|
||||
c.requestCount = 0
|
||||
c.countResetTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure minimum interval between requests
|
||||
elapsed := now.Sub(c.lastRequestTime)
|
||||
if elapsed < minRequestInterval {
|
||||
time.Sleep(minRequestInterval - elapsed)
|
||||
}
|
||||
|
||||
c.lastRequestTime = time.Now()
|
||||
c.requestCount++
|
||||
}
|
||||
|
||||
// GetLinksFromSpotifyID gets cross-platform links from a Spotify track ID
|
||||
func (c *Client) GetLinksFromSpotifyID(spotifyID string) (*CrossPlatformLinks, error) {
|
||||
spotifyURL := fmt.Sprintf("https://open.spotify.com/track/%s", spotifyID)
|
||||
return c.GetLinks(spotifyURL)
|
||||
}
|
||||
|
||||
// GetLinks gets cross-platform links from any music URL
|
||||
func (c *Client) GetLinks(musicURL string) (*CrossPlatformLinks, error) {
|
||||
c.rateLimit()
|
||||
|
||||
params := url.Values{
|
||||
"url": {musicURL},
|
||||
"userCountry": {"US"},
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/links?%s", apiBase, params.Encode())
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "SpotifyRecAlg/1.0")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
// Rate limited - wait and retry once
|
||||
retryAfter := 15
|
||||
if ra := resp.Header.Get("Retry-After"); ra != "" {
|
||||
fmt.Sscanf(ra, "%d", &retryAfter)
|
||||
}
|
||||
time.Sleep(time.Duration(retryAfter) * time.Second)
|
||||
return c.GetLinks(musicURL)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("song.link API error: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
EntityUniqueID string `json:"entityUniqueId"`
|
||||
UserCountry string `json:"userCountry"`
|
||||
PageURL string `json:"pageUrl"`
|
||||
LinksByPlatform map[string]struct {
|
||||
URL string `json:"url"`
|
||||
EntityUniqueID string `json:"entityUniqueId"`
|
||||
} `json:"linksByPlatform"`
|
||||
EntitiesByUniqueID map[string]struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artistName"`
|
||||
ThumbnailURL string `json:"thumbnailUrl"`
|
||||
APIProvider string `json:"apiProvider"`
|
||||
Platforms []string `json:"platforms"`
|
||||
} `json:"entitiesByUniqueId"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links := &CrossPlatformLinks{
|
||||
Links: make(map[string]PlatformLink),
|
||||
}
|
||||
|
||||
// Extract Spotify ID
|
||||
for uniqueID, entity := range data.EntitiesByUniqueID {
|
||||
if entity.APIProvider == "spotify" {
|
||||
links.SpotifyID = entity.ID
|
||||
}
|
||||
if entity.Type == "song" {
|
||||
// ISRC can sometimes be derived from the unique ID format
|
||||
_ = uniqueID
|
||||
}
|
||||
}
|
||||
|
||||
// Platform name mapping
|
||||
platformNames := map[string]string{
|
||||
"spotify": "spotify",
|
||||
"tidal": "tidal",
|
||||
"qobuz": "qobuz",
|
||||
"amazonMusic": "amazonMusic",
|
||||
"amazonStore": "amazon",
|
||||
"deezer": "deezer",
|
||||
"appleMusic": "appleMusic",
|
||||
"youtube": "youtube",
|
||||
"youtubeMusic": "youtubeMusic",
|
||||
"soundcloud": "soundcloud",
|
||||
"napster": "napster",
|
||||
"pandora": "pandora",
|
||||
}
|
||||
|
||||
for platform, linkData := range data.LinksByPlatform {
|
||||
if name, ok := platformNames[platform]; ok {
|
||||
links.Links[name] = PlatformLink{
|
||||
Platform: platform,
|
||||
URL: linkData.URL,
|
||||
EntityType: "track",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links, nil
|
||||
}
|
||||
Reference in New Issue
Block a user