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: 30 * 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 }