Files
Alexander a1f6701bac feat: initial implementation of metadata aggregator
- gRPC service with MusicBrainz provider
- PostgreSQL schema with migrations
- Service layer with database-first caching
- Repository pattern for data access
- YAML configuration support
- Research documentation for 17 music metadata projects
2026-04-28 16:28:53 +02:00

213 lines
4.0 KiB
Go

package musicbrainz
import (
"fmt"
"time"
"github.com/metadata-agregator/internal/domain"
)
func mapArtist(mb *mbArtist) *domain.Artist {
if mb == nil {
return nil
}
artist := &domain.Artist{
ID: mb.ID,
Name: mb.Name,
SortName: mb.SortName,
Type: mb.Type,
Country: mb.Country,
Description: mb.Disambiguation,
ExternalIDs: []domain.ExternalID{{
Source: "musicbrainz",
SourceID: mb.ID,
URL: fmt.Sprintf("https://musicbrainz.org/artist/%s", mb.ID),
}},
}
if mb.LifeSpan.Begin != "" {
if t := parseDate(mb.LifeSpan.Begin); t != nil {
artist.FormedDate = t
}
}
if mb.LifeSpan.End != "" {
if t := parseDate(mb.LifeSpan.End); t != nil {
artist.DisbandedDate = t
}
}
for _, g := range mb.Genres {
artist.Genres = append(artist.Genres, domain.Genre{
ID: g.ID,
Name: g.Name,
})
}
for _, rel := range mb.Relations {
if rel.Type == "image" && rel.URL != nil {
artist.ImageURL = rel.URL.Resource
break
}
}
return artist
}
func mapAlbum(mb *mbReleaseGroup, release *mbRelease) *domain.Album {
if mb == nil {
return nil
}
album := &domain.Album{
ID: mb.ID,
Title: mb.Title,
Type: mb.PrimaryType,
ExternalIDs: []domain.ExternalID{{
Source: "musicbrainz",
SourceID: mb.ID,
URL: fmt.Sprintf("https://musicbrainz.org/release-group/%s", mb.ID),
}},
}
if mb.FirstReleaseDate != "" {
album.ReleaseDate = parseDate(mb.FirstReleaseDate)
}
for _, ac := range mb.ArtistCredit {
album.Artists = append(album.Artists, mapArtistCredit(&ac, "primary"))
}
for _, g := range mb.Genres {
album.Genres = append(album.Genres, domain.Genre{
ID: g.ID,
Name: g.Name,
})
}
if release != nil {
album.UPC = release.Barcode
if len(release.LabelInfo) > 0 && release.LabelInfo[0].Label != nil {
album.Label = mapLabel(release.LabelInfo[0].Label)
}
for _, m := range release.Media {
album.TotalTracks += m.TrackCount
}
album.TotalDiscs = len(release.Media)
if release.CoverArtArchive.Front {
album.CoverURL = fmt.Sprintf("https://coverartarchive.org/release/%s/front", release.ID)
}
}
return album
}
func mapTrack(mb *mbRecording, discNum, trackNum int) *domain.Track {
if mb == nil {
return nil
}
track := &domain.Track{
ID: mb.ID,
Title: mb.Title,
DurationMs: mb.Length,
DiscNumber: discNum,
TrackNumber: trackNum,
ExternalIDs: []domain.ExternalID{{
Source: "musicbrainz",
SourceID: mb.ID,
URL: fmt.Sprintf("https://musicbrainz.org/recording/%s", mb.ID),
}},
}
if len(mb.ISRCs) > 0 {
track.ISRC = mb.ISRCs[0]
}
for _, ac := range mb.ArtistCredit {
track.Artists = append(track.Artists, mapArtistCredit(&ac, "primary"))
}
for _, rel := range mb.Relations {
if rel.TargetType == "work" && rel.Work != nil {
track.Work = mapWork(rel.Work)
break
}
}
return track
}
func mapWork(mb *mbWork) *domain.Work {
if mb == nil {
return nil
}
work := &domain.Work{
ID: mb.ID,
Title: mb.Title,
Type: mb.Type,
Language: mb.Language,
}
for _, rel := range mb.Relations {
if rel.TargetType == "artist" && rel.Artist != nil {
role := "writer"
if rel.Type == "composer" || rel.Type == "lyricist" || rel.Type == "writer" {
role = rel.Type
}
work.Composers = append(work.Composers, domain.ArtistCredit{
Artist: *mapArtist(rel.Artist),
Role: role,
})
}
}
return work
}
func mapLabel(mb *mbLabel) *domain.Label {
if mb == nil {
return nil
}
return &domain.Label{
ID: mb.ID,
Name: mb.Name,
Country: mb.Country,
}
}
func mapArtistCredit(ac *mbArtistCredit, defaultRole string) domain.ArtistCredit {
credit := domain.ArtistCredit{
Role: defaultRole,
JoinPhrase: ac.JoinPhrase,
}
if ac.Artist != nil {
credit.Artist = *mapArtist(ac.Artist)
}
return credit
}
func parseDate(s string) *time.Time {
formats := []string{
"2006-01-02",
"2006-01",
"2006",
}
for _, f := range formats {
if t, err := time.Parse(f, s); err == nil {
return &t
}
}
return nil
}