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) }