METANOIA/metadata/musicbrainz.org/source.go

730 lines
20 KiB
Go

package musicbrainz_org
import (
"encoding/json"
"fmt"
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)
var baseURL = "https://musicbrainz.org/"
var baseAPIURL = "https://musicbrainz.org/ws/2/"
var baseCoverAPIURL = "https://coverartarchive.org/"
type Source struct {
client *metadata.CachingClient
}
func NewSource() *Source {
s := &Source{}
s.client = metadata.NewCachingClient(s.GetURL(), time.Second)
return s
}
func (s *Source) GetName() string {
return "MusicBrainz"
}
func (s *Source) GetURL() string {
return baseURL
}
func (s *Source) GetLicense() metadata.License {
return metadata.License{
//Most core data is CC0
Code: metadata.CC_BY_NC_SA_30,
URL: baseURL + "doc/About/Data_License",
Attribution: fmt.Sprintf("%s (%s)", s.GetName(), s.GetURL()),
}
}
func (s *Source) FindByTOC(toc metadata.TOC) []*metadata.Album {
return s.FindByDiscIDTOC(toc.GetDiscID(), toc)
}
func (s *Source) FindByDiscID(discId metadata.DiscID) []*metadata.Album {
return s.FindByDiscIDTOC(discId, metadata.TOC{})
}
func (s *Source) FindByDiscIDTOC(discId metadata.DiscID, toc metadata.TOC) (albums []*metadata.Album) {
uri, _ := url.Parse(baseAPIURL)
uri.Path += "discid/" + string(discId)
query := uri.Query()
if len(toc) > 0 {
query.Add("toc", fmt.Sprintf("1 %s", toc.MusicBrainzString()))
}
query.Add("fmt", "json")
query.Add("cdstubs", "no")
query.Add("media-format", "all")
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*14)
if err != nil {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
type SearchResult struct {
Sectors int `json:"sectors"`
Id string `json:"id"`
Offsets []int `json:"offsets"`
OffsetCount int `json:"offset-count"`
Releases []struct {
Id string `json:"id"`
Title string `json:"title"`
Date string `json:"date"`
Country string `json:"country"`
Media []mediaEntry `json:"media"`
} `json:"releases"`
}
result := &SearchResult{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
for _, r := range result.Releases {
album := s.GetRelease(r.Id)
if album != nil {
albums = append(albums, album)
}
}
return
}
func (s *Source) FindByAlbumNames(names []metadata.Name) []*metadata.Album {
query := ""
for _, v := range names {
if len(query) == 0 {
query += fmt.Sprintf("release:(%q) OR releaseaccent:(%q)", v.Name, v.Name)
} else {
query += fmt.Sprintf("OR release:(%q) OR releaseaccent:(%q)", v.Name, v.Name)
}
}
return s.FindQueryArguments(query)
}
func (s *Source) FindByCatalogNumber(catalog metadata.CatalogNumber) []*metadata.Album {
query := fmt.Sprintf("catno:(%q)", string(catalog))
if strings.Index(string(catalog), "-") != -1 {
query += fmt.Sprintf("OR catno:(%q)", strings.Replace(string(catalog), "-", "", -1))
}
return s.FindQueryArguments(query)
}
type tagEntry struct {
Count int `json:"count"`
Name string `json:"name"`
}
type ratingEntry struct {
VotesCount int `json:"votes-count"`
Value *float64 `json:"value"`
}
type workEntry struct {
Id string `json:"id"`
Rating ratingEntry `json:"rating"`
Language string `json:"language"`
Title string `json:"title"`
Disambiguation string `json:"disambiguation"`
Languages []string `json:"languages"`
Attributes []string `json:"attributes"`
Relations []relationEntry `json:"relations"`
}
type urlEntry struct {
Id string `json:"id"`
Resource string `json:"resource"`
}
type eventEntry struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Disambiguation string `json:"disambiguation"`
}
type artistEntry struct {
Id string `json:"id"`
Name string `json:"name"`
SortName string `json:"sort-name"`
Disambiguation string `json:"disambiguation"`
Type string `json:"type"`
Rating ratingEntry `json:"rating"`
Aliases []aliasEntry `json:"aliases"`
}
type artistCreditEntry struct {
Name string `json:"name"`
Artist artistEntry `json:"artist"`
JoinPhrase string `json:"joinphrase"`
}
type relationEntry struct {
Id string `json:"id"`
Attributes []string `json:"attributes"`
TargetCredit string `json:"target-credit"`
Direction string `json:"direction"`
TargetType string `json:"target-type"`
SourceCredit string `json:"source-credit"`
Type string `json:"type"`
Artist *artistEntry `json:"artist"`
Work *workEntry `json:"work"`
Event *eventEntry `json:"event"`
Recording *recordingEntry `json:"recording"`
Url *urlEntry `json:"url"`
}
type recordingEntry struct {
Id string `json:"id"`
Video bool `json:"video"`
Title string `json:"title"`
Length int `json:"length"`
FirstReleaseDate string `json:"first-release-date"`
Rating ratingEntry `json:"rating"`
Relations []relationEntry `json:"relations"`
ISRCS []string `json:"isrcs"`
ArtistCredit []artistCreditEntry `json:"artist-credit"`
//TODO Aliases
}
type trackEntry struct {
Id string `json:"id"`
Title string `json:"title"`
Length int `json:"length"`
Position int `json:"position"`
Number string `json:"number"`
Recording recordingEntry `json:"recording"`
}
type discEntry struct {
OffsetCount int `json:"offset-count"`
Offsets []int `json:"offsets"`
Id string `json:"id"`
Sectors int `json:"sectors"`
}
type mediaEntry struct {
TrackOffset int `json:"track-offset"`
Position int `json:"position"`
Format string `json:"format"`
Tracks []trackEntry `json:"tracks"`
Discs []discEntry `json:"discs"`
TrackCount int `json:"track-count"`
Title string `json:"title"`
}
type aliasEntry struct {
Type string `json:"type"`
Name string `json:"name"`
SortName string `json:"sort-name"`
}
type labelEntry struct {
Id string `json:"id"`
Tags []tagEntry `json:"tags"`
Disambiguation string `json:"disambiguation"`
Name string `json:"name"`
ShortName string `json:"short-name"`
//TODO Aliases
//TODO genres
Type string `json:"type"`
}
func (s *Source) GetReleaseCoverArt(releaseId string) (urls []metadata.Name) {
uri, _ := url.Parse(baseCoverAPIURL)
uri.Path += "release/" + releaseId
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*60)
if err != nil {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
result := &struct {
Release string `json:"release"`
Images []struct {
Approved bool `json:"approved"`
Back bool `json:"back"`
Comment string `json:"comment"`
Edit int `json:"edit"`
Front bool `json:"front"`
Id int `json:"id"`
Image string `json:"image"`
Thumbnails map[string]string `json:"thumbnails"`
Types []string `json:"types"`
} `json:"images"`
}{}
err = json.Unmarshal(body, result)
if err == nil {
for _, image := range result.Images {
//fix http -> https
imageUrl := strings.Replace(image.Image, "http://", "https://", 1)
name := metadata.Name{
Kind: "",
Name: imageUrl,
}
if len(image.Types) == 0 {
name.Kind = "unknown"
} else {
for _, priority := range []string{
"Other",
"Booklet",
"Tray",
"Spine",
"Obi",
"Track",
"Liner",
"Sticker",
"Medium",
"Poster",
"Matrix/Runout",
"Front",
"Back",
"Watermark",
"Raw/Unedited",
} {
for _, t := range image.Types {
if t == priority {
if len(name.Kind) == 0 {
name.Kind += strings.ToLower(t)
} else {
name.Kind += ", " + strings.ToLower(t)
}
break
}
}
}
}
urls = append(urls, name)
}
}
return
}
func (s *Source) GetRelease(releaseId string) *metadata.Album {
uri, _ := url.Parse(baseAPIURL)
uri.Path += "release/" + releaseId
query := uri.Query()
query.Add("fmt", "json")
query.Add("inc", strings.Join([]string{
"aliases",
"artist-credits",
"artist-rels",
"artists",
"discids",
"event-rels",
"genres",
"isrcs",
"labels",
"media",
"ratings",
"recording-level-rels",
"recording-rels",
"recordings",
"release-rels",
"series-rels",
"tags",
"url-rels",
"work-level-rels",
"work-rels",
}, "+"))
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*60)
if err != nil {
return nil
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
release := &struct {
ArtistCredit []artistCreditEntry `json:"artist-credit"`
Country string `json:"country"`
Disambiguation string `json:"disambiguation"`
LabelInfo []struct {
CatalogNumber string `json:"catalog-number"`
Label labelEntry `json:"label"`
} `json:"label-info"`
Relations []relationEntry `json:"relations"`
Tags []tagEntry `json:"tags"`
CoverArtArchive struct {
Count int `json:"count"`
Front bool `json:"front"`
Darkened bool `json:"darkened"`
Back bool `json:"back"`
Artwork bool `json:"artwork"`
} `json:"cover-art-archive"`
Date string `json:"date"`
Media []mediaEntry `json:"media"`
Title string `json:"title"`
Aliases []aliasEntry `json:"aliases"`
Status string `json:"status"`
Id string `json:"id"`
Barcode string `json:"barcode"`
ASIN string `json:"asin"`
}{}
err = json.Unmarshal(body, release)
if err != nil {
return nil
}
album := &metadata.Album{
License: s.GetLicense(),
SourceUniqueIdentifier: baseURL + "release/" + release.Id,
Name: []metadata.Name{
{Kind: "original", Name: release.Title},
},
Identifiers: []metadata.Name{
{
Kind: "url",
Name: baseURL + "release/" + release.Id,
},
},
}
for _, n := range release.Aliases {
if n.Type == "Release name" {
album.Name = append(album.Name, metadata.Name{Kind: "original", Name: n.Name})
if n.SortName != n.Name {
album.Name = append(album.Name, metadata.Name{Kind: "sort", Name: n.SortName})
}
}
}
if release.CoverArtArchive.Count > 0 {
album.Art = s.GetReleaseCoverArt(release.Id)
}
album.ReleaseDate, _ = time.ParseInLocation("2006-01-02", release.Date, time.UTC)
for _, l := range release.LabelInfo {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "catalog",
Name: l.CatalogNumber,
})
}
if len(release.Barcode) > 0 {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "barcode",
Name: release.Barcode,
})
}
if len(release.ASIN) > 0 {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "asin",
Name: release.ASIN,
})
}
for _, t := range release.Tags {
album.Tags = append(album.Tags, metadata.Name{
Kind: "genre",
Name: t.Name,
})
}
for _, relation := range release.Relations {
if relation.Direction == "backward" {
if relation.Artist != nil {
role := metadata.Role{
Kind: relation.Type, //TODO: normalize
}
if relation.Artist.Type == "Group" {
role.Group = relation.Artist.Name
}
role.Name = []metadata.Name{{Kind: "original", Name: relation.Artist.Name}}
if relation.Artist.SortName != relation.Artist.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: relation.Artist.SortName})
}
if len(relation.Artist.Disambiguation) > 0 {
role.Name = append(role.Name, metadata.Name{Kind: "disambiguation", Name: relation.Artist.Disambiguation})
}
for _, n := range relation.Artist.Aliases {
role.Name = append(role.Name, metadata.Name{Kind: "original", Name: n.Name})
if n.SortName != n.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: n.SortName})
}
}
role.Name = append(role.Name, metadata.Name{Kind: "url", Name: baseURL + "artist/" + relation.Artist.Id})
album.Roles = append(album.Roles, role)
} else if relation.Event != nil && relation.Type == "available at" {
album.Links = append(album.Links, metadata.Link{
Kind: "release",
Name: []metadata.Name{
{Kind: "name", Name: relation.Event.Name},
{Kind: "url", Name: baseURL + "event/" + relation.Event.Id},
},
})
}
} else if relation.Direction == "forward" {
if relation.Url != nil {
album.Links = append(album.Links, metadata.Link{
Kind: relation.Type,
Name: []metadata.Name{{Kind: "url", Name: relation.Url.Resource}},
})
}
}
}
for _, media := range release.Media {
disc := metadata.Disc{}
if len(media.Title) > 0 {
disc.Name = append(disc.Name, metadata.Name{Kind: "original", Name: media.Title})
}
if len(media.Discs) > 0 {
d := media.Discs[0]
if len(d.Offsets) > 0 {
toc := append(metadata.TOC{d.Sectors}, d.Offsets...)
disc.Identifiers = append(disc.Identifiers, metadata.Name{
Kind: "toc",
Name: toc.String(),
})
disc.Identifiers = append(disc.Identifiers, metadata.Name{
Kind: "cddb1",
Name: toc.GetCDDB1().String(),
})
disc.Identifiers = append(disc.Identifiers, metadata.Name{
Kind: "tocid",
Name: string(toc.GetTocID()),
})
}
if len(d.Id) > 0 {
disc.Identifiers = append(disc.Identifiers, metadata.Name{
Kind: "discid",
Name: d.Id,
})
}
}
for _, t := range media.Tracks {
track := metadata.Track{
Name: metadata.NameSlice{
{Kind: "original", Name: t.Title},
},
Identifiers: metadata.NameSlice{
{Kind: "url", Name: baseURL + "track/" + t.Id},
{Kind: "url", Name: baseURL + "recording/" + t.Recording.Id},
},
Duration: time.Millisecond * time.Duration(t.Length),
}
for _, isrc := range t.Recording.ISRCS {
track.Identifiers = append(track.Identifiers, metadata.Name{
Kind: "isrc",
Name: isrc,
})
}
for _, relation := range t.Recording.Relations {
if relation.Direction == "backward" {
if relation.Artist != nil {
role := metadata.Role{
Kind: relation.Type, //TODO: normalize
}
if relation.Artist.Type == "Group" {
role.Group = relation.Artist.Name
}
if len(relation.Attributes) > 0 {
role.Kind += "; " + strings.Join(relation.Attributes, ", ")
}
role.Name = []metadata.Name{{Kind: "original", Name: relation.Artist.Name}}
if relation.Artist.SortName != relation.Artist.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: relation.Artist.SortName})
}
if len(relation.Artist.Disambiguation) > 0 {
role.Name = append(role.Name, metadata.Name{Kind: "disambiguation", Name: relation.Artist.Disambiguation})
}
for _, n := range relation.Artist.Aliases {
role.Name = append(role.Name, metadata.Name{Kind: "original", Name: n.Name})
if n.SortName != n.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: n.SortName})
}
}
role.Name = append(role.Name, metadata.Name{Kind: "url", Name: baseURL + "artist/" + relation.Artist.Id})
track.Roles = append(track.Roles, role)
}
} else if relation.Direction == "forward" {
if relation.Work != nil && relation.Type == "performance" {
link := metadata.Link{
Kind: "work",
Name: []metadata.Name{
{Kind: "original", Name: relation.Work.Title},
{Kind: "url", Name: baseURL + "work/" + relation.Work.Id},
},
}
if len(relation.Work.Disambiguation) > 0 {
link.Name = append(link.Name, metadata.Name{Kind: "disambiguation", Name: relation.Work.Disambiguation})
}
track.Links = append(track.Links, link)
for _, r2 := range relation.Work.Relations {
if r2.Direction == "backward" {
if r2.Artist != nil {
role := metadata.Role{
Kind: r2.Type, //TODO: normalize
}
if r2.Artist.Type == "Group" {
role.Group = r2.Artist.Name
}
role.Name = []metadata.Name{{Kind: "original", Name: r2.Artist.Name}}
if r2.Artist.SortName != r2.Artist.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: r2.Artist.SortName})
}
if len(r2.Artist.Disambiguation) > 0 {
role.Name = append(role.Name, metadata.Name{Kind: "disambiguation", Name: r2.Artist.Disambiguation})
}
for _, n := range r2.Artist.Aliases {
role.Name = append(role.Name, metadata.Name{Kind: "original", Name: n.Name})
if n.SortName != n.Name {
role.Name = append(role.Name, metadata.Name{Kind: "sort", Name: n.SortName})
}
}
role.Name = append(role.Name, metadata.Name{Kind: "url", Name: baseURL + "artist/" + r2.Artist.Id})
track.Roles = append(track.Roles, role)
}
}
}
} else if relation.Recording != nil && relation.Type == "remix" {
track.Links = append(track.Links, metadata.Link{
Kind: "original",
Name: []metadata.Name{
{Kind: "original", Name: relation.Recording.Title},
{Kind: "url", Name: baseURL + "recording/" + relation.Recording.Id},
},
})
}
}
}
disc.Tracks = append(disc.Tracks, track)
}
album.Discs = append(album.Discs, disc)
}
return album
}
func (s *Source) FindQueryArguments(queryArgs string) (albums []*metadata.Album) {
uri, _ := url.Parse(baseAPIURL)
uri.Path += "release/"
query := uri.Query()
query.Add("query", queryArgs)
query.Add("fmt", "json")
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*14)
if err != nil {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
type SearchResult struct {
Count int `json:"count"`
Releases []struct {
Id string `json:"id"`
Score int `json:"score"`
Count int `json:"count"`
Title string `json:"title"`
Date string `json:"date"`
Country string `json:"country"`
LabelInfo []struct {
CatalogNumber string `json:"catalog-number"`
Label struct {
Id string `json:"id"`
Name string `json:"name"`
} `json:"label"`
} `json:"label-info"`
TrackCount int `json:"track-count"`
Media []struct {
Format string `json:"format"`
DiscCount int `json:"disc-count"`
TrackCount int `json:"track-count"`
} `json:"media"`
} `json:"releases"`
}
result := &SearchResult{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
for _, r := range result.Releases {
album := s.GetRelease(r.Id)
if album != nil {
albums = append(albums, album)
}
}
return
}
func (s *Source) Test() {
album := s.FindByTOC(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305"))
//album := s.GetRelease("9ca2748b-88fd-44a7-bc5c-036574148571")
log.Print(album)
}