Files
ripsort/internal/clients/musicbrainz/musicbrainz.go

229 lines
6.0 KiB
Go

package musicbrainz
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
baseURL = "https://musicbrainz.org/ws/2"
userAgent = "customTool/1.0 (djeeberjr@gmail.com)"
)
type MusicBrainzClient struct {
httpClient *http.Client
userAgent string
}
func NewMusicBrainzClient() *MusicBrainzClient {
return &MusicBrainzClient{
httpClient: &http.Client{Timeout: 15 * time.Second},
userAgent: userAgent,
}
}
type Artist struct {
ID string `json:"id"`
Name string `json:"name"`
SortName string `json:"sort-name"`
Type string `json:"type"`
Country string `json:"country"`
Disambiguation string `json:"disambiguation"`
LifeSpan LifeSpan `json:"life-span"`
Tags []Tag `json:"tags"`
Releases []Release `json:"releases"`
ReleaseGroups []ReleaseGroup `json:"release-groups"`
}
type Release struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Date string `json:"date"`
Country string `json:"country"`
Disambiguation string `json:"disambiguation"`
ArtistCredit []Credit `json:"artist-credit"`
LabelInfo []LabelInfo `json:"label-info"`
TrackCount int `json:"track-count"`
}
type ReleaseGroup struct {
ID string `json:"id"`
Title string `json:"title"`
PrimaryType string `json:"primary-type"`
FirstRelease string `json:"first-release-date"`
ArtistCredit []Credit `json:"artist-credit"`
}
type Recording struct {
ID string `json:"id"`
Title string `json:"title"`
LengthInMS int `json:"length"`
Disambiguation string `json:"disambiguation"`
ArtistCredit []Credit `json:"artist-credit"`
Releases []Release `json:"releases"`
Tags []Tag `json:"tags"`
}
type Credit struct {
Name string `json:"name"`
JoinPhrase string `json:"joinphrase"`
Artist Artist `json:"artist"`
}
type LabelInfo struct {
CatalogNumber string `json:"catalog-number"`
Label struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"label"`
}
type LifeSpan struct {
Begin string `json:"begin"`
End string `json:"end"`
Ended bool `json:"ended"`
}
type Tag struct {
Name string `json:"name"`
Count int `json:"count"`
}
type ArtistSearchResult struct {
Created string `json:"created"`
Count int `json:"count"`
Offset int `json:"offset"`
Artists []Artist `json:"artists"`
}
type ReleaseSearchResult struct {
Created string `json:"created"`
Count int `json:"count"`
Offset int `json:"offset"`
Releases []Release `json:"releases"`
}
type RecordingSearchResult struct {
Created string `json:"created"`
Count int `json:"count"`
Offset int `json:"offset"`
Recordings []Recording `json:"recordings"`
}
type SearchOptions struct {
Limit int // max results (default 25, max 100)
Offset int // results offset for pagination
}
func (o SearchOptions) params() url.Values {
v := url.Values{}
if o.Limit > 0 {
v.Set("limit", fmt.Sprintf("%d", o.Limit))
}
if o.Offset > 0 {
v.Set("offset", fmt.Sprintf("%d", o.Offset))
}
return v
}
func (c *MusicBrainzClient) SearchByISRC(isrc string, opts SearchOptions) (*RecordingSearchResult, error) {
var result RecordingSearchResult
err := c.search("recording", fmt.Sprintf("isrc:%s", isrc), opts, &result)
return &result, err
}
func (c *MusicBrainzClient) SearchArtists(query string, opts SearchOptions) (*ArtistSearchResult, error) {
var result ArtistSearchResult
err := c.search("artist", query, opts, &result)
return &result, err
}
func (c *MusicBrainzClient) SearchReleases(query string, opts SearchOptions) (*ReleaseSearchResult, error) {
var result ReleaseSearchResult
err := c.search("release", query, opts, &result)
return &result, err
}
func (c *MusicBrainzClient) SearchRecordings(query string, opts SearchOptions) (*RecordingSearchResult, error) {
var result RecordingSearchResult
err := c.search("recording", query, opts, &result)
return &result, err
}
func (c *MusicBrainzClient) GetArtist(mbid string, inc []string) (*Artist, error) {
var artist Artist
err := c.lookup("artist", mbid, inc, &artist)
return &artist, err
}
func (c *MusicBrainzClient) GetRelease(mbid string, inc []string) (*Release, error) {
var release Release
err := c.lookup("release", mbid, inc, &release)
return &release, err
}
func (c *MusicBrainzClient) GetRecording(mbid string, inc []string) (*Recording, error) {
var recording Recording
err := c.lookup("recording", mbid, inc, &recording)
return &recording, err
}
func (c *MusicBrainzClient) search(entity, query string, opts SearchOptions, out any) error {
params := opts.params()
params.Set("query", query)
params.Set("fmt", "json")
endpoint := fmt.Sprintf("%s/%s?%s", baseURL, entity, params.Encode())
return c.get(endpoint, out)
}
func (c *MusicBrainzClient) lookup(entity, mbid string, inc []string, out any) error {
params := url.Values{"fmt": []string{"json"}}
if len(inc) > 0 {
incStr := ""
for i, s := range inc {
if i > 0 {
incStr += "+"
}
incStr += s
}
params.Set("inc", incStr)
}
endpoint := fmt.Sprintf("%s/%s/%s?%s", baseURL, entity, mbid, params.Encode())
return c.get(endpoint, out)
}
func (c *MusicBrainzClient) get(endpoint string, out any) error {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return fmt.Errorf("build request: %w", err)
}
req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("http: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
if err := json.Unmarshal(body, out); err != nil {
return fmt.Errorf("decode JSON: %w", err)
}
return nil
}