229 lines
6.0 KiB
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
|
|
}
|