package providers import ( "context" "fmt" "net/url" "strconv" "strings" ) // deezer uses Deezer's public catalogue API: no key, generous limits and // 1000px cover art. type deezer struct { r *Registry kind string // "album" or "song" } func (d *deezer) Name() string { return "deezer" } func (d *deezer) Label() string { return "Deezer" } type deezerErr struct { Error *struct { Message string `json:"message"` } `json:"error"` } // Deezer reports errors with a 200 status and an "error" object. func (e *deezerErr) check() error { if e.Error != nil { return fmt.Errorf("Deezer: %s", e.Error.Message) } return nil } func (d *deezer) get(ctx context.Context, path string, out any, e *deezerErr) error { if err := d.r.getJSON(ctx, "https://api.deezer.com"+path, nil, out); err != nil { return err } return e.check() } type deezerArtist struct { Name string `json:"name"` } type deezerAlbum struct { deezerErr ID int64 `json:"id"` Title string `json:"title"` Link string `json:"link"` CoverXL string `json:"cover_xl"` CoverBig string `json:"cover_big"` RecordType string `json:"record_type"` NbTracks int `json:"nb_tracks"` ReleaseDate string `json:"release_date"` Label string `json:"label"` Artist deezerArtist `json:"artist"` Genres struct { Data []struct { Name string `json:"name"` } `json:"data"` } `json:"genres"` } type deezerTrack struct { deezerErr ID int64 `json:"id"` Title string `json:"title"` Link string `json:"link"` Duration int `json:"duration"` ReleaseDate string `json:"release_date"` Artist deezerArtist `json:"artist"` Album deezerAlbum `json:"album"` } func cover(a deezerAlbum) string { if a.CoverXL != "" { return a.CoverXL } return a.CoverBig } func (d *deezer) Search(ctx context.Context, query string) ([]Result, error) { q := url.Values{"q": {query}, "limit": {"25"}}.Encode() var out []Result if d.kind == "album" { var resp struct { deezerErr Data []deezerAlbum `json:"data"` } if err := d.get(ctx, "/search/album?"+q, &resp, &resp.deezerErr); err != nil { return nil, err } for _, a := range resp.Data { r := Result{ Provider: "deezer", ProviderID: "album:" + strconv.FormatInt(a.ID, 10), Title: a.Title, Subtitle: a.Artist.Name, ImageURL: cover(a), ExternalURL: a.Link, Extra: map[string]string{}, } if a.RecordType != "" && a.RecordType != "album" { r.Extra["type"] = strings.ToUpper(a.RecordType[:1]) + a.RecordType[1:] } if a.NbTracks > 0 { r.Hint = fmt.Sprintf("%d tracks", a.NbTracks) } out = append(out, r) } return out, nil } var resp struct { deezerErr Data []deezerTrack `json:"data"` } if err := d.get(ctx, "/search/track?"+q, &resp, &resp.deezerErr); err != nil { return nil, err } for _, t := range resp.Data { out = append(out, Result{ Provider: "deezer", ProviderID: "track:" + strconv.FormatInt(t.ID, 10), Title: t.Title, Subtitle: t.Artist.Name, ImageURL: cover(t.Album), ExternalURL: t.Link, Hint: "From " + t.Album.Title, Extra: map[string]string{"album": t.Album.Title}, }) } return out, nil } func (d *deezer) Details(ctx context.Context, res Result) (Result, error) { kind, id, _ := strings.Cut(res.ProviderID, ":") if res.Extra == nil { res.Extra = map[string]string{} } switch kind { case "album": var a deezerAlbum if err := d.get(ctx, "/album/"+id, &a, &a.deezerErr); err != nil { return res, err } res.Year = yearOf(a.ReleaseDate) if a.Label != "" { res.Extra["label"] = a.Label } var g []string for _, x := range a.Genres.Data { g = append(g, x.Name) } if len(g) > 0 { res.Extra["genres"] = strings.Join(g, ", ") } case "track": var t deezerTrack if err := d.get(ctx, "/track/"+id, &t, &t.deezerErr); err != nil { return res, err } res.Year = yearOf(t.ReleaseDate) if res.Year == "" { res.Year = yearOf(t.Album.ReleaseDate) } if t.Duration > 0 { res.Extra["length"] = fmt.Sprintf("%d:%02d", t.Duration/60, t.Duration%60) } } return res, nil } // musicbrainz uses the MusicBrainz database with art from the Cover Art // Archive. Fully open data, but limited to one request per second. type musicbrainz struct { r *Registry kind string } func (m *musicbrainz) Name() string { return "musicbrainz" } func (m *musicbrainz) Label() string { return "MusicBrainz" } type mbCredit struct { Name string `json:"name"` JoinPhrase string `json:"joinphrase"` } func mbArtists(credits []mbCredit) string { var b strings.Builder for _, c := range credits { b.WriteString(c.Name) b.WriteString(c.JoinPhrase) } return strings.TrimSpace(b.String()) } func (m *musicbrainz) Search(ctx context.Context, query string) ([]Result, error) { if err := m.r.mb.wait(ctx); err != nil { return nil, err } var out []Result if m.kind == "album" { var resp struct { ReleaseGroups []struct { ID string `json:"id"` Title string `json:"title"` PrimaryType string `json:"primary-type"` FirstReleaseDate string `json:"first-release-date"` ArtistCredit []mbCredit `json:"artist-credit"` } `json:"release-groups"` } u := "https://musicbrainz.org/ws/2/release-group?" + url.Values{"query": {query}, "fmt": {"json"}, "limit": {"25"}}.Encode() if err := m.r.getJSON(ctx, u, nil, &resp); err != nil { return nil, err } for _, rg := range resp.ReleaseGroups { r := Result{ Provider: "musicbrainz", ProviderID: "release-group:" + rg.ID, Title: rg.Title, Subtitle: mbArtists(rg.ArtistCredit), Year: yearOf(rg.FirstReleaseDate), ImageURL: "https://coverartarchive.org/release-group/" + rg.ID + "/front-500", ExternalURL: "https://musicbrainz.org/release-group/" + rg.ID, Extra: map[string]string{}, } if rg.PrimaryType != "" && rg.PrimaryType != "Album" { r.Extra["type"] = rg.PrimaryType } out = append(out, r) } return out, nil } var resp struct { Recordings []struct { ID string `json:"id"` Title string `json:"title"` Length int `json:"length"` FirstReleaseDate string `json:"first-release-date"` ArtistCredit []mbCredit `json:"artist-credit"` Releases []struct { Title string `json:"title"` ReleaseGroup struct { ID string `json:"id"` PrimaryType string `json:"primary-type"` } `json:"release-group"` } `json:"releases"` } `json:"recordings"` } u := "https://musicbrainz.org/ws/2/recording?" + url.Values{"query": {query}, "fmt": {"json"}, "limit": {"25"}}.Encode() if err := m.r.getJSON(ctx, u, nil, &resp); err != nil { return nil, err } for _, rec := range resp.Recordings { r := Result{ Provider: "musicbrainz", ProviderID: "recording:" + rec.ID, Title: rec.Title, Subtitle: mbArtists(rec.ArtistCredit), Year: yearOf(rec.FirstReleaseDate), ExternalURL: "https://musicbrainz.org/recording/" + rec.ID, Extra: map[string]string{}, } // Prefer an album release for the artwork. for i, rel := range rec.Releases { if i == 0 || rel.ReleaseGroup.PrimaryType == "Album" { r.ImageURL = "https://coverartarchive.org/release-group/" + rel.ReleaseGroup.ID + "/front-500" r.Extra["album"] = rel.Title r.Hint = "From " + rel.Title if rel.ReleaseGroup.PrimaryType == "Album" { break } } } if rec.Length > 0 { s := rec.Length / 1000 r.Extra["length"] = fmt.Sprintf("%d:%02d", s/60, s%60) } out = append(out, r) } return out, nil }