Files
SpotifyRecAlg/apps/backend/internal/provider/unlocker/client.go
T
Tomas Dvorak 6e8fedf534 first commit
2026-04-13 17:46:58 +02:00

175 lines
4.2 KiB
Go

package unlocker
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
)
var (
ErrNotConfigured = errors.New("unlocker service not configured")
ErrTrackNotFound = errors.New("track not found")
)
// Client for the Python unlocker service (auth-free Spotify access)
type Client struct {
baseURL string
client *http.Client
}
func NewClient(baseURL string) *Client {
if baseURL == "" {
return nil
}
return &Client{
baseURL: baseURL,
client: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *Client) Configured() bool {
return c != nil && c.baseURL != ""
}
type TrackResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Artists []string `json:"artists"`
Album string `json:"album"`
DurationMS int `json:"duration_ms"`
Explicit bool `json:"explicit"`
ExternalURLs map[string]string `json:"external_urls"`
}
type ImportRequest struct {
URL string `json:"url"`
}
type ImportResponse struct {
Track *TrackResponse `json:"track"`
Links map[string]string `json:"links"`
Parsed *ParsedInfo `json:"parsed,omitempty"`
Note string `json:"note,omitempty"`
}
type ParsedInfo struct {
Service string `json:"service"`
Type string `json:"type"`
ID string `json:"id"`
}
type LinksResponse struct {
SpotifyID string `json:"spotify_id"`
ISRC string `json:"isrc"`
Links map[string]LinkDetails `json:"links"`
}
type LinkDetails struct {
URL string `json:"url"`
ID string `json:"id"`
}
// ImportFromURL imports a track from any streaming service URL (auth-free)
func (c *Client) ImportFromURL(ctx context.Context, url string) (*ImportResponse, error) {
if !c.Configured() {
return nil, ErrNotConfigured
}
reqBody, _ := json.Marshal(ImportRequest{URL: url})
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/import", bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("unlocker request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unlocker returned %d: %s", resp.StatusCode, string(body))
}
var result ImportResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse unlocker response: %w", err)
}
return &result, nil
}
// GetTrack gets a track by Spotify ID (auth-free)
func (c *Client) GetTrack(ctx context.Context, trackID string) (*TrackResponse, error) {
if !c.Configured() {
return nil, ErrNotConfigured
}
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/spotify/track/"+trackID, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("unlocker request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusNotFound {
return nil, ErrTrackNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unlocker returned %d: %s", resp.StatusCode, string(body))
}
var result TrackResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse unlocker response: %w", err)
}
return &result, nil
}
// GetLinks gets cross-platform links for a Spotify track
func (c *Client) GetLinks(ctx context.Context, spotifyID string) (*LinksResponse, error) {
if !c.Configured() {
return nil, ErrNotConfigured
}
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/links/"+spotifyID, nil)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("unlocker request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unlocker returned %d: %s", resp.StatusCode, string(body))
}
var result LinksResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse unlocker response: %w", err)
}
return &result, nil
}