Added THwiki/vgmdb/musicbrainz/cuetools/accuraterip metadata sources

This commit is contained in:
DataHoarder 2022-02-14 16:48:05 +01:00
parent 03e94ad7f4
commit d37e263e89
5 changed files with 2677 additions and 0 deletions

View file

@ -0,0 +1,105 @@
package accuraterip_com
import (
"encoding/binary"
"fmt"
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
"log"
"net/http"
"net/url"
"time"
)
var baseURL = "http://www.accuraterip.com/"
type Source struct {
client *metadata.CachingClient
}
func NewSource() *Source {
s := &Source{}
s.client = metadata.NewCachingClient(s.GetURL())
return s
}
func (s *Source) GetName() string {
return "AccurateRip"
}
func (s *Source) GetURL() string {
return baseURL
}
func (s *Source) GetLicense() metadata.License {
return metadata.License{
//Most core data is CC0
Code: metadata.LicenseCode_NC,
URL: "http://www.accuraterip.com/",
Attribution: fmt.Sprintf("%s (%s)", s.GetName(), s.GetURL()),
}
}
func (s *Source) FindMetadataByTOC(toc metadata.TOC) (results []AccurateRipMetadata) {
uri, _ := url.Parse(baseURL)
var data AccurateRipDiscIdent
data.TrackCount, data.TrackOffsetsAdded, data.TrackOffsetsMultiplied, data.CDDB1 = toc.GetAccurateRipData()
uri.Path += fmt.Sprintf("accuraterip/%01x/%01x/%01x/dBAR-%03d-%08x-%08x-%08x.bin", data.TrackOffsetsAdded&0xF, data.TrackOffsetsAdded>>4&0xF, data.TrackOffsetsAdded>>8&0xF, data.TrackCount, data.TrackOffsetsAdded, data.TrackOffsetsMultiplied, uint32(data.CDDB1))
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*14)
if err != nil || response.StatusCode != http.StatusOK {
return nil
}
var discIdent AccurateRipDiscIdent
var discTrack AccurateRipDiscTrack
for {
err = binary.Read(response.Body, binary.LittleEndian, &discIdent)
if err != nil {
break
}
meta := AccurateRipMetadata{AccurateRipDiscIdent: discIdent}
for i := byte(0); i < discIdent.TrackCount; i++ {
err = binary.Read(response.Body, binary.LittleEndian, &discTrack)
if err != nil {
break
}
meta.Tracks = append(meta.Tracks, discTrack)
}
results = append(results, meta)
}
return
}
type AccurateRipDiscIdent struct {
TrackCount byte
TrackOffsetsAdded uint32
TrackOffsetsMultiplied uint32
CDDB1 metadata.CDDB1
}
type AccurateRipDiscTrack struct {
Confidence byte
TrackCRC uint32
OffsetFindCRC uint32
}
type AccurateRipMetadata struct {
AccurateRipDiscIdent
Tracks []AccurateRipDiscTrack
}
func (s *Source) Test() {
meta := s.FindMetadataByTOC(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305"))
log.Print(meta)
}

View file

@ -0,0 +1,388 @@
package cuetools_net
import (
"encoding/xml"
"fmt"
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
musicbrainz_org "git.gammaspectra.live/S.O.N.G/METANOIA/metadata/musicbrainz.org"
"io/ioutil"
"log"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
var baseURL = "https://db.cue.tools/"
var oldBaseURL = "http://db.cuetools.net/"
type Source struct {
client *metadata.CachingClient
}
func NewSource() *Source {
s := &Source{}
s.client = metadata.NewCachingClient(s.GetURL())
return s
}
func (s *Source) GetName() string {
return "CueTools DB"
}
func (s *Source) GetURL() string {
return baseURL
}
func (s *Source) GetLicense() metadata.License {
return metadata.License{
//Most core data is CC0
Code: metadata.LicenseCode_CC0,
URL: "https://cue.tools/wiki/CUETools_Database#What_about_the_license.3F",
Attribution: fmt.Sprintf("%s (%s)", s.GetName(), s.GetURL()),
}
}
func mergeByAlbumUniqueIdentifier(existing []*metadata.Album, albums ...*metadata.Album) []*metadata.Album {
for _, r := range albums {
if r == nil {
continue
}
exists := false
for _, m := range existing {
if m.SourceUniqueIdentifier == r.SourceUniqueIdentifier {
exists = true
break
}
}
if !exists {
existing = append(existing, r)
}
}
return existing
}
func (s *Source) FindByTocID(tocId metadata.TocID) (albums []*metadata.Album) {
uri, _ := url.Parse(baseURL)
uri.Path += "lookup.php"
query := uri.Query()
query.Add("tocid", string(tocId))
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
}
f := &struct {
Entries []struct {
Confidence int `xml:"confidence,attr"`
CRC32 string `xml:"crc32,attr"`
Id int `xml:"id,attr"`
NPar int `xml:"npar,attr"`
Stride int `xml:"stride,attr"`
Parity string `xml:"parity"`
TOC struct {
TrackCount int `xml:"trackcount,attr"`
AudioTracks int `xml:"audiotracks,attr"`
FirstAudio int `xml:"firstaudio,attr"`
Content string `xml:",chardata"`
} `xml:"toc"`
} `xml:"entry"`
}{}
err = xml.Unmarshal(body, f)
if err == nil {
//Sort highest at the top
sort.SliceStable(f.Entries, func(i, j int) bool {
return f.Entries[j].Confidence < f.Entries[i].Confidence
})
for _, e := range f.Entries {
if len(albums) > 0 && e.Confidence == 1 {
break
}
albums = mergeByAlbumUniqueIdentifier(albums, s.FindByTOC(metadata.NewTOCFromCTDBString(e.TOC.Content))...)
}
}
return nil
}
func (s *Source) FindByTOC(toc metadata.TOC) (albums []*metadata.Album) {
f := s.FindMetadataByTOC(toc)
tocs := make([]metadata.TOC, 0, len(f.Entries))
names := make([][]metadata.Name, len(f.Entries))
var trackCRC [][]metadata.Link
for i, e := range f.Entries {
tocs = append(tocs, metadata.NewTOCFromCTDB2String(e.TOC))
if len(trackCRC) <= i {
trackCRC = append(trackCRC, []metadata.Link{})
}
for k, c := range strings.Split(e.TrackCRC32, " ") {
if len(trackCRC) <= k {
trackCRC = append(trackCRC, []metadata.Link{})
}
if len(trackCRC[k]) <= i {
trackCRC[k] = append(trackCRC[k], metadata.Link{
Kind: "crc32",
})
}
trackCRC[k][i].Name = append(trackCRC[k][i].Name, metadata.Name{
Kind: "ctdb",
Name: c,
})
}
names[i] = append(names[i], metadata.Name{
Kind: "crc32",
Name: e.CRC32,
})
if len(e.Syndrome) > 0 {
names[i] = append(names[i], metadata.Name{
Kind: "syndrome",
Name: e.Syndrome,
})
}
if len(e.Parity) > 0 {
names[i] = append(names[i], metadata.Name{
Kind: "parity",
Name: e.Parity,
})
}
if len(e.HasParity) > 0 {
names[i] = append(names[i], metadata.Name{
Kind: "parity",
Name: e.HasParity,
})
}
}
/*
if len(f.Metadata) > 0 {
for _, e := range f.Metadata {
if e.Source == "musicbrainz" {
album := f.GetMusicbrainzAlbums()
if album != nil {
if len(album.Discs) >= e.DiscNumber {
var identifiers []metadata.Name
for i := range f.Entries {
identifiers = append(identifiers, metadata.Name{
Kind: "toc",
Name: tocs[i].String(),
})
identifiers = append(identifiers, metadata.Name{
Kind: "cddb1",
Name: tocs[i].GetCDDB1().String(),
})
identifiers = append(identifiers, metadata.Name{
Kind: "tocid",
Name: string(tocs[i].GetTocID()),
})
identifiers = append(identifiers, metadata.Name{
Kind: "discid",
Name: string(tocs[i].GetDiscID()),
})
identifiers = append(identifiers, names[i]...)
}
for ti := range album.Discs[e.DiscNumber-1].Tracks {
album.Discs[e.DiscNumber-1].Tracks[ti].Links = append(album.Discs[e.DiscNumber-1].Tracks[ti].Links, trackCRC[ti]...)
}
album.Discs[e.DiscNumber-1].Identifiers = append(album.Discs[e.DiscNumber-1].Identifiers, identifiers...)
}
albums = append(albums, album)
}
}
}
} else {
}
*/
return
}
func (s *Source) FindMetadataByTOC(toc metadata.TOC) *CueToolsMetadata {
uri, _ := url.Parse(baseURL)
uri.Path += "lookup2.php"
query := uri.Query()
query.Add("toc", toc.CTDBString())
query.Add("type", "xml")
query.Add("version", "3")
query.Add("fuzzy", "0")
query.Add("ctdb", "1")
query.Add("musicbrainz", "1")
query.Add("metadata", "extensive")
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
}
f := &CueToolsMetadata{}
err = xml.Unmarshal(body, f)
if err == nil {
//Sort highest at the top
sort.SliceStable(f.Entries, func(i, j int) bool {
return f.Entries[j].Confidence < f.Entries[i].Confidence
})
return f
}
return nil
}
func (s *Source) Test() {
album := s.FindByTocID(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305").GetTocID())
//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)
}
type CueToolsMetadata struct {
Entries []struct {
Confidence int `xml:"confidence,attr"`
CRC32 string `xml:"crc32,attr"`
Id int `xml:"id,attr"`
HasParity string `xml:"hasparity,attr"`
NPar int `xml:"npar,attr"`
Parity string `xml:"parity,attr"`
Syndrome string `xml:"syndrome,attr"`
Stride int `xml:"stride,attr"`
TOC string `xml:"toc,attr"`
TrackCRC32 string `xml:"trackcrcs,attr"`
} `xml:"entry"`
Metadata []struct {
Album string `xml:"album,attr"`
Artist string `xml:"artist,attr"`
DiscCount int `xml:"disccount,attr"`
DiscNumber int `xml:"discnumber,attr"`
DiscName string `xml:"discname,attr"`
Id string `xml:"id,attr"`
Relevance int `xml:"relevance,attr"`
Source string `xml:"source,attr"`
Year int `xml:"year,attr"`
Tracks []struct {
Artist string `xml:"artist,attr"`
Name string `xml:"name,attr"`
} `xml:"track"`
Label []struct {
CatalogNumber string `xml:"catno,attr"`
Name string `xml:"name,attr"`
} `xml:"label"`
Release struct {
Country string `xml:"country,attr"`
Date string `xml:"date,attr"`
} `xml:"release"`
CoverArt []struct {
Primary bool `xml:"primary,attr"`
URI string `xml:"uri,attr"`
} `xml:"coverart"`
} `xml:"metadata"`
}
func (m *CueToolsMetadata) GetTOCs() (tocs []metadata.TOC) {
for _, e := range m.Entries {
tocs = append(tocs, metadata.NewTOCFromCTDB2String(e.TOC))
}
return
}
func (m *CueToolsMetadata) GetParity() (parity []string) {
for _, e := range m.Entries {
if len(e.HasParity) > 0 {
parity = append(parity, e.HasParity)
} else {
parity = append(parity, e.Parity)
}
}
return
}
func (m *CueToolsMetadata) GetSyndrome() (syndrome []string) {
for _, e := range m.Entries {
syndrome = append(syndrome, e.Syndrome)
}
return
}
func (m *CueToolsMetadata) GetCRC32() (crc32 []string) {
for _, e := range m.Entries {
crc32 = append(crc32, e.CRC32)
}
return
}
//GetTracksCRC32 [trackIndex][resultIndex]crc32
func (m *CueToolsMetadata) GetTracksCRC32() (crc32 [][]string) {
for _, e := range m.Entries {
for k, c := range strings.Split(e.TrackCRC32, " ") {
if len(crc32) <= k {
crc32 = append(crc32, []string{})
}
crc32[k] = append(crc32[k], c)
}
}
return
}
func (m *CueToolsMetadata) GetConfidence() (conf []int) {
for _, e := range m.Entries {
conf = append(conf, e.Confidence)
}
return
}
func (m *CueToolsMetadata) GetResultId() (id []int) {
for _, e := range m.Entries {
id = append(id, e.Id)
}
return
}
func (m *CueToolsMetadata) GetResultURL() (id []string) {
for _, e := range m.Entries {
id = append(id, fmt.Sprintf("%scd/%d", oldBaseURL, e.Id))
}
return
}
func (m *CueToolsMetadata) GetMusicbrainzAlbums() (albums []*metadata.Album) {
for _, e := range m.Metadata {
if e.Source == "musicbrainz" {
album := musicbrainz_org.NewSource().GetRelease(e.Id)
if album != nil {
albums = append(albums, album)
}
}
}
return
}

View file

@ -0,0 +1,712 @@
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())
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.LicenseCode_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 int `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 []struct {
Name string `json:"name"`
SortName string `json:"sort-name"`
} `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"`
}
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 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
}
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"`
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,
},
},
}
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.Name{
{Kind: "original", Name: t.Title},
{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.Name = append(track.Name, 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)
}

View file

@ -0,0 +1,891 @@
package thwiki_cc
import (
"encoding/json"
"fmt"
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
"git.gammaspectra.live/S.O.N.G/METANOIA/utilities/wiki"
"github.com/oriser/regroup"
"io/ioutil"
"log"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
var baseURL = "https://thwiki.cc/"
var baseLyricsURL = "https://cd.thwiki.cc/"
type Source struct {
client *metadata.CachingClient
}
func NewSource() *Source {
s := &Source{}
s.client = metadata.NewCachingClient(s.GetURL())
return s
}
func (s *Source) GetName() string {
return "THBWiki"
}
func (s *Source) GetURL() string {
return baseURL
}
func (s *Source) GetLicense() metadata.License {
return metadata.License{
Code: metadata.LicenseCode_CC_BY_NC_SA_30,
URL: baseURL + "/THBWiki:关于#.E7.89.88.E6.9D.83.E4.BF.A1.E6.81.AF",
Attribution: fmt.Sprintf("%s (%s)", s.GetName(), s.GetURL()),
}
}
func (s *Source) FindByTOC(toc metadata.TOC) []*metadata.Album {
return s.FindByCDDB1Group([]metadata.CDDB1{toc.GetCDDB1()})
}
func (s *Source) FindByCDDB1(cddb metadata.CDDB1) []*metadata.Album {
return s.FindByCDDB1Group([]metadata.CDDB1{cddb})
}
func (s *Source) FindByCDDB1Group(group []metadata.CDDB1) []*metadata.Album {
tracks := 0
var duration time.Duration
discs := len(group)
secondsRange := 1.
gapLength := time.Second * 2
for _, cddb := range group {
duration += cddb.GetDuration()
tracks += cddb.GetTrackNumber()
offset := (gapLength * time.Duration(cddb.GetTrackNumber())) / 2 //pre-gap length
duration -= offset
secondsRange += offset.Seconds()
}
query := []string{
fmt.Sprintf("G3%d\t%d", discs, discs),
fmt.Sprintf("H3%d\t%d", tracks, tracks),
fmt.Sprintf("I3%d\t%d", int(math.Floor(duration.Seconds()-secondsRange)), int(math.Ceil(duration.Seconds()+secondsRange))),
}
return s.FindAdvancedQueryArguments(strings.Join(query, "\n") + "\n")
}
func (s *Source) FindByAlbumNames(names []metadata.Name) []*metadata.Album {
srsearch := ""
for _, v := range names {
if len(srsearch) > 0 {
srsearch += fmt.Sprintf(" OR %q OR %q", "名称 = "+v.Name, "译名 = "+v.Name)
} else {
srsearch += fmt.Sprintf("%q OR %q", "名称 = "+v.Name, "译名 = "+v.Name)
}
}
return s.FindQueryArguments(srsearch)
}
func (s *Source) FindByCatalogNumber(catalog metadata.CatalogNumber) []*metadata.Album {
srsearch := fmt.Sprintf("%q", "编号 = "+catalog)
if strings.Index(string(catalog), "-") != -1 {
srsearch += fmt.Sprintf(" OR %q", "编号 = "+strings.Replace(string(catalog), "-", "", -1))
}
return s.FindQueryArguments(srsearch)
}
func (s *Source) FindQueryArguments(queryArgs string) (albums []*metadata.Album) {
uri, _ := url.Parse(baseURL)
uri.Path += "api.php"
query := uri.Query()
query.Add("action", "query")
query.Add("format", "json")
query.Add("list", "search")
query.Add("srwhat", "text")
query.Add("srlimit", "500")
srsearch := "incategory:同人专辑 ("
srsearch += queryArgs
srsearch += ")"
query.Add("srsearch", srsearch)
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 {
Query struct {
SearchInfo struct {
TotalHits int `json:"totalhits"`
} `json:"searchinfo"`
Search []struct {
NameSpace int `json:"ns"`
Title string `json:"title"`
PageId int `json:"pageid"`
Snippet string `json:"snippet"`
} `json:"search"`
} `json:"query"`
}
result := &SearchResult{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
for _, r := range result.Query.Search {
album := s.GetAlbumInformation(wiki.NormalizeWikiTitle(r.Title))
if album != nil {
albums = append(albums, album)
}
}
return
}
func (s *Source) FindAdvancedQueryArguments(queryArgs string) (albums []*metadata.Album) {
uri, _ := url.Parse(baseURL)
uri.Path += "api.php"
query := uri.Query()
query.Add("action", "uask")
query.Add("pre", "专辑")
query.Add("sort", "")
query.Add("query", queryArgs)
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*30)
if err != nil {
return nil
}
if response.StatusCode != http.StatusOK {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
split := strings.Split(string(body), " ")
if len(split) != 2 {
return nil
}
numberOfResults, _ := strconv.Atoi(split[0])
if numberOfResults == 0 {
return nil
}
query.Add("token", split[1])
query.Add("order", "")
query.Add("limit", "25")
query.Add("offset", "0")
query.Add("result", "d")
uri.RawQuery = query.Encode()
response, err = s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*30)
if err != nil {
return nil
}
if response.StatusCode != http.StatusOK {
return nil
}
body, err = ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
type SearchResult struct {
From int `json:"from"`
Till int `json:"till"`
Total int `json:"total"`
Symbols string `json:"symbols"`
Results struct {
Link []string `json:"link"`
Text []string `json:"text"`
} `json:"resu"`
}
result := &SearchResult{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
for _, albumName := range result.Results.Text {
album := s.GetAlbumInformation(wiki.NormalizeWikiTitle(albumName))
if album != nil {
albums = append(albums, album)
}
}
return
}
func (s *Source) GetArticle(title string) ([]interface{}, error) {
uri, _ := url.Parse(baseURL)
uri.Path += "index.php"
query := uri.Query()
query.Add("title", title)
query.Add("action", "raw")
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*60)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
return wiki.ParseWikiText(string(body)), nil
}
func (s *Source) GetFileURL(title string) string {
uri, _ := url.Parse(baseURL)
uri.Path += "api.php"
query := uri.Query()
query.Add("titles", "File:"+title)
query.Add("action", "query")
query.Add("prop", "imageinfo")
query.Add("iiprop", "url")
query.Add("format", "json")
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*180)
if err != nil {
return ""
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return ""
}
type ImageData struct {
Query struct {
Pages map[string]struct {
ImageInfo []struct {
URL string `json:"url"`
} `json:"imageinfo"`
} `json:"pages"`
} `json:"query"`
}
var d ImageData
err = json.Unmarshal(body, &d)
if err != nil {
return ""
}
for _, v := range d.Query.Pages {
if len(v.ImageInfo) > 0 {
return v.ImageInfo[0].URL
}
}
return ""
}
func (s *Source) GetSongLyrics(songName string) *metadata.Lyrics {
uri, _ := url.Parse(baseLyricsURL)
uri.Path += "lyrics/" + wiki.NormalizeWikiTitle(songName) + ".lrc"
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*120)
if err != nil {
return nil
}
if response.StatusCode != http.StatusOK {
return nil
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil
}
var lineRE = regroup.MustCompile(`(?m)^\[(?P<time_minutes>[0-9]+):(?P<time_seconds>[0-9]+)\.(?P<time_decimals>[0-9]+)\](?P<line>.*)$`)
var lineMetaRE = regroup.MustCompile(`(?m)^\[(?P<kind>(ti|al|ar|re)):(?P<line>.*)\]$`)
type LineMatch struct {
TimeMinutes int `regroup:"time_minutes"`
TimeSeconds int `regroup:"time_seconds"`
TimeDecimals string `regroup:"time_decimals"`
Line string `regroup:"line"`
}
type LineMetaMatch struct {
Kind string `regroup:"kind"`
Line string `regroup:"line"`
}
m := &LineMatch{}
rets, err := lineRE.MatchAllToTarget(string(body), -1, m)
if err == nil {
lyrics := &metadata.Lyrics{
Identifiers: []metadata.Name{
{Kind: "url", Name: baseURL + "歌词:" + wiki.NormalizeWikiTitle(songName)},
},
}
m2 := &LineMetaMatch{}
rets2, _ := lineMetaRE.MatchAllToTarget(string(body), -1, m2)
for _, elem2 := range rets2 {
match2 := elem2.(*LineMetaMatch)
switch match2.Kind {
case "ti":
lyrics.Identifiers = append(lyrics.Identifiers, metadata.Name{
Kind: "title",
Name: strings.TrimSpace(match2.Line),
})
case "al":
lyrics.Identifiers = append(lyrics.Identifiers, metadata.Name{
Kind: "album",
Name: strings.TrimSpace(match2.Line),
})
case "ar":
lyrics.Identifiers = append(lyrics.Identifiers, metadata.Name{
Kind: "artist",
Name: strings.TrimSpace(match2.Line),
})
}
}
for _, elem := range rets {
match := elem.(*LineMatch)
var last *metadata.LyricTextEntry
if len(lyrics.Entries) > 0 {
last = &lyrics.Entries[len(lyrics.Entries)-1]
}
start := time.Minute*time.Duration(match.TimeMinutes) + time.Second*time.Duration(match.TimeSeconds)
if len(match.TimeDecimals) == 1 {
d, _ := strconv.Atoi(match.TimeDecimals)
start += time.Millisecond * time.Duration(d) * 100
} else if len(match.TimeDecimals) == 2 {
d, _ := strconv.Atoi(match.TimeDecimals)
start += time.Millisecond * time.Duration(d) * 10
} else if len(match.TimeDecimals) == 3 {
d, _ := strconv.Atoi(match.TimeDecimals)
start += time.Millisecond * time.Duration(d)
} else {
//TODO
}
if last != nil {
last.End = start
}
line := strings.TrimSpace(match.Line)
if len(line) > 0 {
lyrics.Entries = append(lyrics.Entries, metadata.LyricTextEntry{
Start: start,
End: -1,
Content: []metadata.Name{
{Kind: "japanese", Name: line},
},
})
}
}
return lyrics
}
return nil
}
func (s *Source) GetAlbumInformation(title string) *metadata.Album {
article, err := s.GetArticle(title)
if err != nil {
return nil
}
var album *metadata.Album
normalizeStringCharacters := func(text string) string {
for _, v := range [][2]string{
{"", ", "},
{"", "("},
{"", ")"},
} {
text = strings.Replace(text, v[0], v[1], -1)
}
return text
}
removeLinks := func(text string) (result string) {
index := 0
var isInternal bool
for i := 0; i < len(text); i++ {
c := text[i]
if index != 0 {
if c == ']' {
if isInternal {
segments := strings.Split(text[index:i], "|")
if len(segments) > 1 {
segments = segments[1:]
}
result += strings.Join(segments, "|")
} else {
segments := strings.Split(text[index:i], " ")
if len(segments) > 1 {
segments = segments[1:]
}
result += strings.Join(segments, " ")
}
if len(text) > i+1 && text[i+1] == ']' {
i++
}
index = 0
}
} else {
if c == '[' {
if len(text) > i+1 && text[i+1] == '[' {
i++
isInternal = true
} else {
isInternal = false
}
index = i + 1
} else {
result += text[i : i+1]
}
}
}
return
}
getStringValue := func(v []interface{}) (result []string) {
for _, value := range v {
text, ok := value.(string)
if ok {
result = append(result, normalizeStringCharacters(text))
} else {
template, ok := value.(*wiki.Template)
if ok {
switch template.Name {
case "PAGENAME":
fallthrough
case "SUBPAGENAME":
result = append(result, title)
case "CM":
result = append(result, fmt.Sprintf("Comiket %s", template.Parameters["1"][0].(string)))
case "红楼梦":
result = append(result, fmt.Sprintf("Touhou Kouroumu %s", template.Parameters["1"][0].(string)))
case "例大祭":
result = append(result, fmt.Sprintf("Hakurei Shrine Reitaisai %s", template.Parameters["1"][0].(string)))
default:
result = append(result, template.Name)
}
}
}
}
return
}
var staffMappings []map[string]string
listingStaff := false
var staffRE = regroup.MustCompile(`(?m)^;(?P<position>[^:]+):(?P<name>.+)$`)
var staffNameSplitRE = regroup.MustCompile(`(?P<name>[^<]+)(?P<position><br/?>|$)`)
var staffNameGroupRE = regroup.MustCompile(`(?P<position>.+)\((?P<name>.+)\)`)
var zunComposerRole = metadata.Role{
Kind: "composer",
Name: []metadata.Name{
{Kind: "original", Name: "ZUN"},
{Kind: "url", Name: baseURL + "ZUN"},
},
}
handleStaffMapping := func(discIndex int, val []interface{}, kind string, track *metadata.Track) {
if len(staffMappings) > discIndex {
for _, e := range strings.Split(strings.Join(getStringValue(val), ","), ",") {
entryValue := strings.TrimSpace(e)
if len(entryValue) > 0 {
groupValue, ok := staffMappings[discIndex][entryValue]
if ok && groupValue != entryValue {
track.Roles = append(track.Roles, metadata.Role{
Kind: kind,
Name: []metadata.Name{{Kind: "original", Name: entryValue}, {Kind: "url", Name: baseURL + wiki.NormalizeWikiTitle(entryValue)}},
Group: groupValue,
})
} else {
track.Roles = append(track.Roles, metadata.Role{
Kind: kind,
Name: []metadata.Name{{Kind: "original", Name: entryValue}, {Kind: "url", Name: baseURL + wiki.NormalizeWikiTitle(entryValue)}},
})
}
}
}
}
}
for _, v := range article {
text, ok := v.(string)
if ok {
if strings.Index(text, "== Staff ==") != -1 {
listingStaff = true
}
if listingStaff {
type StaffMatch struct {
Position string `regroup:"position"`
Name string `regroup:"name"`
}
m := &StaffMatch{}
rets, err := staffRE.MatchAllToTarget(normalizeStringCharacters(text), -1, m)
if err == nil {
for _, elem := range rets {
match := elem.(*StaffMatch)
for _, roleName := range strings.Split(match.Position, "/") {
roleName = strings.ToLower(strings.TrimSpace(roleName))
switch roleName {
case "total produce":
fallthrough
case "produce":
fallthrough
case "production":
roleName = "producer"
case "assistant":
roleName = "assistant"
case "illustration":
fallthrough
case "cover artwork":
roleName = "illustration"
case "mastering":
roleName = "mastering"
case "graphic design":
fallthrough
case "graphics":
fallthrough
case "design":
roleName = "design"
case "compose":
roleName = "composer"
}
rets2, err := staffNameSplitRE.MatchAllToTarget(removeLinks(strings.TrimSpace(match.Name)), -1, m)
if err == nil {
for _, elem := range rets2 {
match2 := elem.(*StaffMatch)
m3 := &StaffMatch{}
role := metadata.Role{
Kind: roleName,
Name: []metadata.Name{{Kind: "original", Name: strings.TrimSpace(match2.Name)}},
}
if staffNameGroupRE.MatchToTarget(strings.TrimSpace(match2.Name), m3) == nil {
//Is a group
role.Name = []metadata.Name{{Kind: "original", Name: strings.TrimSpace(m3.Position)}}
role.Group = strings.TrimSpace(m3.Name)
}
album.Roles = append(album.Roles, role)
}
}
}
}
}
}
} else {
template, ok := v.(*wiki.Template)
if ok {
switch template.Name {
case "专辑人员列表": //Album Person List
if album == nil {
continue
}
discIndex, _ := strconv.Atoi(strings.Join(getStringValue(template.Parameters["碟号"]), " ")) //disc number
discIndex--
staffMappings = append(staffMappings, make(map[string]string))
for _, val := range template.Parameters { //nested
for _, value := range getStringValue(val) {
parts := strings.Split(value, "=")
p0 := strings.TrimSpace(parts[0])
if len(parts) > 1 {
staffMappings[discIndex][p0] = strings.TrimSpace(parts[1])
} else {
staffMappings[discIndex][p0] = p0
}
}
}
case "专辑曲目列表": //Album Track List
if album == nil {
continue
}
listingStaff = false
discIndex := len(album.Discs)
disc := metadata.Disc{}
for _, value := range template.Parameters["嵌套"] { //nested
tpl, ok := value.(*wiki.Template)
if ok {
track := metadata.Track{}
for k, val := range tpl.Parameters {
switch k {
case "时长": //duration
d := getStringValue(val)[0]
sub := strings.Split(d, ":")
s1, _ := strconv.ParseInt(strings.TrimLeft(sub[0], "0"), 10, 0)
if len(sub) > 2 {
//TODO
} else if len(sub) > 1 {
s2, _ := strconv.ParseInt(strings.TrimLeft(sub[1], "0"), 10, 0)
track.Duration = time.Minute*time.Duration(s1) + time.Second*time.Duration(s2)
} else {
track.Duration = time.Second * time.Duration(s1)
}
case "名称": //title
trackTitle := strings.Join(getStringValue(val), " ")
track.Name = append(track.Name, metadata.Name{
Kind: "original",
Name: trackTitle,
})
track.Lyrics = func() *metadata.Lyrics {
return s.GetSongLyrics(trackTitle)
}
case "原专辑": //original release
track.Links = append(track.Links, metadata.Link{
Kind: "original release",
Name: []metadata.Name{{Kind: "original", Name: strings.Join(getStringValue(val), " ")}},
})
case "原名称": //original release title
track.Links = append(track.Links, metadata.Link{
Kind: "original release title",
Name: []metadata.Name{{Kind: "original", Name: strings.Join(getStringValue(val), " ")}},
})
case "原曲": //original song
case "编曲": //arranger/composer
if _, ok := tpl.Parameters["原曲"]; ok {
handleStaffMapping(discIndex, val, "arranger", &track)
track.Roles = append(track.Roles, zunComposerRole) //TODO check
} else {
handleStaffMapping(discIndex, val, "composer", &track)
}
case "再编曲": //re-arranger
handleStaffMapping(discIndex, val, "remix", &track)
case "演唱": //vocals
handleStaffMapping(discIndex, val, "vocals", &track)
case "作词": //lyrics
handleStaffMapping(discIndex, val, "lyrics", &track)
}
}
disc.Tracks = append(disc.Tracks, track)
}
}
album.Discs = append(album.Discs, disc)
case "同人专辑信息": //Doujin Album Information
album = &metadata.Album{
License: s.GetLicense(),
SourceUniqueIdentifier: baseURL + title,
Identifiers: []metadata.Name{
{
Kind: "url",
Name: baseURL + title,
},
},
}
album.Identifiers = append(album.Identifiers)
for k, val := range template.Parameters {
switch k {
case "封面": //cover: jpg
fileName := strings.Join(getStringValue(val), "")
if strings.Index(fileName, ".") == -1 {
fileName = title + "封面." + fileName
}
f := s.GetFileURL(fileName)
if len(f) > 0 {
album.Art = append(album.Art, metadata.Name{
Kind: "front",
Name: s.GetFileURL(fileName),
})
}
case "展会": //release
for _, value := range val {
tpl, ok := value.(*wiki.Template)
if ok {
switch tpl.Name {
case "CM":
album.Links = append(album.Links, metadata.Link{
Kind: "release",
Name: []metadata.Name{
{Kind: "name", Name: fmt.Sprintf("Comiket %s", tpl.Parameters["1"][0].(string))},
},
})
case "红楼梦":
album.Links = append(album.Links, metadata.Link{
Kind: "release",
Name: []metadata.Name{
{Kind: "name", Name: fmt.Sprintf("Touhou Kouroumu %s", tpl.Parameters["1"][0].(string))},
},
})
case "例大祭":
album.Links = append(album.Links, metadata.Link{
Kind: "release",
Name: []metadata.Name{
{Kind: "name", Name: fmt.Sprintf("Hakurei Shrine Reitaisai %s", tpl.Parameters["1"][0].(string))},
},
})
}
break
}
}
case "名称": //name
album.Name = append(album.Name, metadata.Name{
Kind: "original",
Name: strings.Join(getStringValue(val), " "),
})
case "制作方": //produced by
for _, producer := range strings.Split(strings.Join(getStringValue(val), ","), ",") {
album.Roles = append(album.Roles, metadata.Role{
Kind: "producer",
Name: []metadata.Name{{Kind: "original", Name: producer}, {Kind: "url", Name: baseURL + wiki.NormalizeWikiTitle(producer)}},
})
}
case "发行方": //issuer
for _, issuer := range strings.Split(strings.Join(getStringValue(val), ","), ",") {
album.Roles = append(album.Roles, metadata.Role{
Kind: "issuer",
Name: []metadata.Name{{Kind: "original", Name: issuer}, {Kind: "url", Name: baseURL + wiki.NormalizeWikiTitle(issuer)}},
})
}
case "编号": //catalog number
for _, catalog := range strings.Split(strings.Join(getStringValue(val), "+"), "+") {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "catalog",
Name: catalog,
})
}
case "风格类型": //style
for _, style := range strings.Split(strings.Join(getStringValue(val), ","), ",") {
switch strings.ToLower(style) {
default:
album.Tags = append(album.Tags, metadata.Name{
Kind: "genre",
Name: strings.ToLower(style), //TODO: normalize
})
}
}
case "官网页面": //official page
album.Links = append(album.Links, metadata.Link{
Kind: "official",
Name: []metadata.Name{{Kind: "url", Name: strings.Join(getStringValue(val), " ")}},
})
}
}
}
} else {
//unknown value
continue
}
}
}
return album
}
func (s *Source) Test() {
albumC := s.FindByCDDB1(0x730dec08)
albums := s.FindByAlbumNames([]metadata.Name{{Name: "Bayside Beat"}})
album := s.GetAlbumInformation("Haunted_House")
album2 := s.GetAlbumInformation("マジコカタストロフィ")
lyrics := album2.Discs[0].Tracks[1].Lyrics()
log.Print(album)
log.Print(albumC)
log.Print(album2)
log.Print(lyrics)
log.Print(albums)
}

View file

@ -0,0 +1,581 @@
package vgmdb_net
import (
"encoding/json"
"fmt"
"git.gammaspectra.live/S.O.N.G/METANOIA/metadata"
"git.gammaspectra.live/S.O.N.G/METANOIA/utilities"
"github.com/oriser/regroup"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
var baseURL = "https://vgmdb.net/"
var baseAPIURL = "https://vgmdb.info/"
type Source struct {
client *metadata.CachingClient
}
func NewSource() *Source {
s := &Source{}
s.client = metadata.NewCachingClient(s.GetURL())
return s
}
func (s *Source) GetName() string {
return "VGMdb"
}
func (s *Source) GetURL() string {
return baseURL
}
func (s *Source) GetLicense() metadata.License {
return metadata.License{
Code: metadata.LicenseCode_Unknown,
URL: "",
Attribution: fmt.Sprintf("%s (%s)", s.GetName(), s.GetURL()),
}
}
func (s *Source) FindByTOC(toc metadata.TOC) (results []*metadata.Album) {
uri, _ := url.Parse(baseURL)
uri.Path += "cddb"
query := uri.Query()
query.Add("cmd", fmt.Sprintf("cddb query %s", toc.CDDBString()))
query.Add("hello", "anonymous localhost METANOIA "+utilities.Version)
query.Add("proto", "6")
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
}
matchLineRE := regroup.MustCompile(`(?m)^(200 )?(?P<group>[A-Za-z][A-Za-z0-9]+) (?P<cddb1>[a-f0-9]{8}) (?P<match>.+)$`)
matchURLRE := regroup.MustCompile(`(?m)^EXTD=https://vgmdb.net/album/(?P<album>[0-9]+)$`)
type matchResult struct {
Group string `regroup:"group"`
CDDB1 string `regroup:"cddb1"`
Match string `regroup:"match"`
}
m := &matchResult{}
rets, err := matchLineRE.MatchAllToTarget(string(body), -1, m)
if err == nil {
for _, elem := range rets {
match := elem.(*matchResult)
uri, _ := url.Parse(baseURL)
uri.Path += "cddb"
query := uri.Query()
query.Add("cmd", fmt.Sprintf("cddb read %s %s", match.Group, match.CDDB1))
query.Add("hello", "anonymous localhost METANOIA "+utilities.Version)
query.Add("proto", "6")
uri.RawQuery = query.Encode()
response, err := s.client.Request(&http.Request{
Method: "GET",
URL: uri,
}, time.Hour*24*30)
if err != nil {
continue
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
continue
}
t := &struct {
AlbumId int `regroup:"album"`
}{}
if matchURLRE.MatchToTarget(string(body), t) == nil {
results = mergeByAlbumUniqueIdentifier(results, s.GetAlbumInformation(t.AlbumId))
}
}
}
return
}
func mergeByAlbumUniqueIdentifier(existing []*metadata.Album, albums ...*metadata.Album) []*metadata.Album {
for _, r := range albums {
if r == nil {
continue
}
exists := false
for _, m := range existing {
if m.SourceUniqueIdentifier == r.SourceUniqueIdentifier {
exists = true
break
}
}
if !exists {
existing = append(existing, r)
}
}
return existing
}
func (s *Source) FindByAlbumNames(names []metadata.Name) (results []*metadata.Album) {
for _, v := range names {
results = mergeByAlbumUniqueIdentifier(results, s.FindQueryArguments(v.Name)...)
}
return
}
func (s *Source) FindByCatalogNumber(catalog metadata.CatalogNumber) (results []*metadata.Album) {
results = s.FindQueryArguments(string(catalog))
if strings.Index(string(catalog), "-") != -1 {
results = mergeByAlbumUniqueIdentifier(results, s.FindQueryArguments(strings.Replace(string(catalog), "-", "", -1))...)
}
return
}
type namesEntry struct {
English string `json:"en"`
Japanese string `json:"ja"`
Romaji string `json:"ja-latn"`
}
type titlesEntry struct {
English string `json:"English"`
Japanese string `json:"Japanese"`
Romaji string `json:"Romaji"`
}
type entityEntry struct {
Link string `json:"link"`
Names namesEntry `json:"names"`
}
type imageEntry struct {
Name string `json:"name"`
Full string `json:"full"`
Medium string `json:"medium"`
Thumb string `json:"thumb"`
}
type trackEntry struct {
Names titlesEntry `json:"names"`
Duration string `json:"track_length"`
}
type discEntry struct {
Length string `json:"disc_length"`
Name string `json:"name"`
Tracks []trackEntry `json:"tracks"`
}
func (s *Source) FindQueryArguments(queryArgs string) (albums []*metadata.Album) {
uri, _ := url.Parse(baseAPIURL)
uri.Path += "search/albums"
query := uri.Query()
query.Add("q", queryArgs)
query.Add("format", "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 {
Link string `json:"link"`
Meta map[string]interface{} `json:"meta"`
Query string `json:"query"`
Results struct {
Albums []struct {
Catalog string `json:"catalog"`
Category string `json:"category"`
Link string `json:"link"`
MediaFormat string `json:"media_format"`
ReleaseDate string `json:"release_date"`
Titles namesEntry `json:"titles"`
} `json:"albums"`
} `json:"results"`
}
result := &SearchResult{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
for _, r := range result.Results.Albums {
parts := strings.Split(r.Link, "/")
if len(parts) < 2 {
continue
}
albumId, _ := strconv.Atoi(parts[1])
if albumId == 0 {
continue
}
album := s.GetAlbumInformation(albumId)
if album != nil {
albums = append(albums, album)
}
}
return
}
func (s *Source) GetAlbumInformation(id int) *metadata.Album {
uri, _ := url.Parse(baseAPIURL)
uri.Path += fmt.Sprintf("album/%d", id)
query := uri.Query()
query.Add("format", "json")
uri.RawQuery = query.Encode()
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 {
Arrangers []entityEntry `json:"arrangers"`
Catalog string `json:"catalog"`
Categories []string `json:"categories"`
Category string `json:"category"`
Classification string `json:"classification"`
Composers []entityEntry `json:"composers"`
Covers []imageEntry `json:"covers"`
Discs []discEntry `json:"discs"`
Link string `json:"link"`
Lyricists []entityEntry `json:"lyricists"`
MediaFormat string `json:"media_format"`
Meta struct {
AddedDate string `json:"added_date"`
EditedDate string `json:"edited_date"`
FetchedDate string `json:"fetched_date"`
TTL int `json:"ttl"`
Visitors int `json:"visitors"`
}
Name string `json:"name"`
Names namesEntry `json:"names"`
Notes string `json:"notes"`
Organizations []struct {
Link string `json:"link"`
Role string `json:"role"`
Names namesEntry `json:"names"`
} `json:"organizations"`
Performers []entityEntry `json:"performers"`
PictureFull string `json:"picture_full"`
PictureMedium string `json:"picture_medium"`
PictureThumb string `json:"picture_thumb"`
Platforms []string `json:"platforms"`
Products []entityEntry `json:"products"`
PublishFormat string `json:"publish_format"`
Publisher entityEntry `json:"publisher"`
Related []struct {
Catalog string `json:"catalog"`
Link string `json:"link"`
Names namesEntry `json:"names"`
Type string `json:"type"`
} `json:"related"`
ReleaseDate string `json:"release_date"`
ReleaseEvents []struct {
Link string `json:"link"`
Name string `json:"name"`
ShortName string `json:"shortname"`
} `json:"release_events"`
ReleasePrice struct {
Currency string `json:"currency"`
Price float64 `json:"price"`
} `json:"release_price"`
//reprints
Votes int `json:"votes"`
Websites map[string][]struct {
Link string `json:"link"`
Name string `json:"name"`
} `json:"websites"`
}{}
err = json.Unmarshal(body, result)
if err != nil {
return nil
}
album := &metadata.Album{
License: s.GetLicense(),
SourceUniqueIdentifier: baseURL + fmt.Sprintf("album/%d", id),
}
getNamesFromNameEntry := func(entry namesEntry) (results map[string]string) {
results = make(map[string]string)
if len(entry.English) > 0 {
results["english"] = entry.English
if len(entry.Japanese) > 0 && entry.Japanese != entry.English {
results["japanese"] = entry.Japanese
}
if len(entry.Romaji) > 0 && entry.Romaji != entry.English && entry.Japanese != entry.Romaji {
results["romaji"] = entry.Romaji
}
} else if len(entry.Japanese) > 0 {
if entry.Romaji == entry.Japanese {
results["romaji"] = entry.Romaji
} else {
results["japanese"] = entry.Japanese
if len(entry.Romaji) > 0 && entry.Romaji != entry.Japanese {
results["romaji"] = entry.Romaji
}
}
}
return
}
addEntityRoles := func(roleName string, entities []entityEntry) {
for _, r := range entities {
role := metadata.Role{
Kind: roleName,
}
for k, n := range getNamesFromNameEntry(r.Names) {
role.Name = append(role.Name, metadata.Name{
Kind: k,
Name: n,
})
}
if len(r.Link) > 0 {
role.Name = append(role.Name, metadata.Name{
Kind: "url",
Name: baseURL + r.Link,
})
}
album.Roles = append(album.Roles, role)
}
}
album.Name = append(album.Name, metadata.Name{
Kind: "original",
Name: result.Name,
})
for k, n := range getNamesFromNameEntry(result.Names) {
if result.Name != n {
album.Name = append(album.Name, metadata.Name{
Kind: k,
Name: n,
})
}
}
addEntityRoles("arranger", result.Arrangers)
addEntityRoles("composer", result.Composers)
addEntityRoles("lyrics", result.Lyricists)
addEntityRoles("performer", result.Performers)
for _, o := range result.Organizations {
org := metadata.Role{
Kind: o.Role,
}
for k, n := range getNamesFromNameEntry(o.Names) {
org.Name = append(org.Name, metadata.Name{
Kind: k,
Name: n,
})
}
if len(o.Link) > 0 {
org.Name = append(org.Name, metadata.Name{
Kind: "url",
Name: baseURL + o.Link,
})
}
album.Roles = append(album.Roles, org)
}
publisher := metadata.Role{
Kind: "publisher",
}
for k, n := range getNamesFromNameEntry(result.Publisher.Names) {
publisher.Name = append(publisher.Name, metadata.Name{
Kind: k,
Name: n,
})
}
if len(result.Publisher.Link) > 0 {
publisher.Name = append(publisher.Name, metadata.Name{
Kind: "url",
Name: baseURL + result.Publisher.Link,
})
}
if len(publisher.Name) > 0 {
album.Roles = append(album.Roles, publisher)
}
if len(result.Catalog) > 0 {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "catalog",
Name: result.Catalog,
})
}
if len(result.Link) > 0 {
album.Identifiers = append(album.Identifiers, metadata.Name{
Kind: "url",
Name: baseURL + result.Link,
})
}
for _, e := range result.ReleaseEvents {
album.Links = append(album.Links, metadata.Link{
Kind: "release",
Name: []metadata.Name{
{Kind: "url", Name: baseURL + e.Link},
{Kind: "name", Name: e.Name},
},
})
}
for _, c := range result.Covers {
if len(c.Full) > 0 {
album.Art = append(album.Art, metadata.Name{
Kind: strings.ToLower(c.Name),
Name: c.Full,
})
}
}
for _, g := range strings.Split(result.Classification, ",") {
n := strings.ToLower(strings.TrimSpace(g))
if len(n) > 0 {
album.Tags = append(album.Tags, metadata.Name{
Kind: "genre",
Name: n,
})
}
}
for _, p := range result.Products {
product := metadata.Link{
Kind: "work",
}
for k, n := range getNamesFromNameEntry(p.Names) {
product.Name = append(product.Name, metadata.Name{
Kind: k,
Name: n,
})
}
if len(p.Link) > 0 {
product.Name = append(product.Name, metadata.Name{
Kind: "url",
Name: baseURL + p.Link,
})
}
album.Links = append(album.Links, product)
}
for k, l := range result.Websites {
for _, link := range l {
album.Links = append(album.Links, metadata.Link{
Kind: strings.ToLower(k),
Name: []metadata.Name{{Kind: "url", Name: link.Link}},
})
}
}
for _, r := range result.Related {
related := metadata.Link{
Kind: "related",
}
if len(r.Catalog) > 0 {
related.Name = append(related.Name, metadata.Name{
Kind: "catalog",
Name: r.Catalog,
})
}
for k, n := range getNamesFromNameEntry(r.Names) {
related.Name = append(related.Name, metadata.Name{
Kind: k,
Name: n,
})
}
if len(r.Link) > 0 {
related.Name = append(related.Name, metadata.Name{
Kind: "url",
Name: baseURL + r.Link,
})
}
album.Links = append(album.Links, related)
}
album.ReleaseDate, _ = time.ParseInLocation("2006-01-02", result.ReleaseDate, time.UTC)
for _, d := range result.Discs {
disc := metadata.Disc{}
for _, t := range d.Tracks {
track := metadata.Track{}
for k, n := range getNamesFromNameEntry(namesEntry(t.Names)) {
track.Name = append(track.Name, metadata.Name{
Kind: k,
Name: n,
})
}
sub := strings.Split(t.Duration, ":")
s1, _ := strconv.ParseInt(strings.TrimLeft(sub[0], "0"), 10, 0)
if len(sub) > 2 {
//TODO
} else if len(sub) > 1 {
s2, _ := strconv.ParseInt(strings.TrimLeft(sub[1], "0"), 10, 0)
track.Duration = time.Minute*time.Duration(s1) + time.Second*time.Duration(s2)
} else {
track.Duration = time.Second * time.Duration(s1)
}
disc.Tracks = append(disc.Tracks, track)
}
album.Discs = append(album.Discs, disc)
}
return album
}
func (s *Source) Test() {
album := s.FindByTOC(metadata.NewTOCFromString("267453 150 24647 71194 95579 139576 174573 199089 244305"))
//album := s.GetAlbumInformation(42266)
log.Print(album)
}