This commit prepares the musiclink bot for production deployment on NixOS. Changes: - Refactored service layer to support Dependency Injection for API URLs. - Added 'internal/bot/bot_integration_test.go' for mock-based integration testing. - Added 'flake.nix' and 'flake.lock' for reproducible Nix builds. - Added 'README.md', 'LICENSE', and '.gitignore'. - Verified build and tests pass locally.
156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const defaultAPIURL = "https://idonthavespotify.sjdonado.com/api/search?v=1"
|
|
|
|
// Client calls the idonthavespotify API to resolve music links.
|
|
type Client struct {
|
|
httpClient *http.Client
|
|
apiURL string
|
|
}
|
|
|
|
// NewClient creates a new idonthavespotify API client.
|
|
func NewClient(apiURL string) *Client {
|
|
if apiURL == "" {
|
|
apiURL = defaultAPIURL
|
|
}
|
|
return &Client{
|
|
httpClient: &http.Client{Timeout: 15 * time.Second},
|
|
apiURL: apiURL,
|
|
}
|
|
}
|
|
|
|
// apiRequest is the request body for the idonthavespotify API.
|
|
type apiRequest struct {
|
|
Link string `json:"link"`
|
|
Adapters []string `json:"adapters,omitempty"`
|
|
}
|
|
|
|
// APIResponse is the response from the idonthavespotify API.
|
|
type APIResponse struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"` // "song", "album", "playlist", "artist", "podcast"
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Image string `json:"image"`
|
|
Audio string `json:"audio"`
|
|
Source string `json:"source"`
|
|
UniversalLink string `json:"universalLink"`
|
|
Links []APILink `json:"links"`
|
|
}
|
|
|
|
// APILink represents a single platform link in the API response.
|
|
type APILink struct {
|
|
Type string `json:"type"` // "spotify", "appleMusic", "youtube", "tidal", etc.
|
|
URL string `json:"url"`
|
|
IsVerified bool `json:"isVerified"`
|
|
NotAvailable bool `json:"notAvailable"`
|
|
}
|
|
|
|
// Resolve takes a music link and returns links to the same content on other platforms.
|
|
func (c *Client) Resolve(ctx context.Context, link string) (*APIResponse, error) {
|
|
reqBody := apiRequest{Link: link}
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", c.apiURL, bytes.NewReader(jsonBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("making request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var apiResp APIResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
|
return nil, fmt.Errorf("decoding response: %w", err)
|
|
}
|
|
|
|
return &apiResp, nil
|
|
}
|
|
|
|
// ToResolvedLinks converts an API response to our internal ResolvedLinks format.
|
|
func (r *APIResponse) ToResolvedLinks() *ResolvedLinks {
|
|
result := &ResolvedLinks{
|
|
Type: apiTypeToLinkType(r.Type),
|
|
Links: make(map[ServiceType]string),
|
|
}
|
|
|
|
// Set track info if available
|
|
if r.Type == "song" {
|
|
result.Track = &Track{
|
|
Title: r.Title,
|
|
ArtworkURL: r.Image,
|
|
PreviewURL: r.Audio,
|
|
}
|
|
// Parse artist from description if possible
|
|
// Description format: "Artist · Album · Song · Year"
|
|
}
|
|
|
|
// Map links to our service types
|
|
for _, link := range r.Links {
|
|
if link.NotAvailable {
|
|
continue
|
|
}
|
|
if svcType, ok := apiTypeToServiceType(link.Type); ok {
|
|
result.Links[svcType] = link.URL
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func apiTypeToLinkType(t string) LinkType {
|
|
switch t {
|
|
case "song":
|
|
return LinkTypeTrack
|
|
case "album":
|
|
return LinkTypeAlbum
|
|
case "artist":
|
|
return LinkTypeArtist
|
|
case "playlist":
|
|
return LinkTypePlaylist
|
|
default:
|
|
return LinkTypeTrack
|
|
}
|
|
}
|
|
|
|
func apiTypeToServiceType(t string) (ServiceType, bool) {
|
|
switch t {
|
|
case "spotify":
|
|
return ServiceSpotify, true
|
|
case "appleMusic":
|
|
return ServiceAppleMusic, true
|
|
case "youtube", "youtubeMusic":
|
|
return ServiceYouTube, true
|
|
case "deezer":
|
|
return ServiceDeezer, true
|
|
case "soundCloud":
|
|
return ServiceSoundCloud, true
|
|
case "tidal":
|
|
return ServiceTidal, true
|
|
case "bandcamp":
|
|
return ServiceBandcamp, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|