Normalize dashes to underscores in musicbrainz data fields

This commit is contained in:
Šarūnas Nejus 2026-01-13 00:43:23 +00:00
parent 680473b9e5
commit e0842c44db
No known key found for this signature in database
8 changed files with 723 additions and 665 deletions

View file

@ -169,7 +169,7 @@ class MusicBrainzAPI(RequestHandler):
if includes:
kwargs["inc"] = "+".join(includes)
return self._group_relations(
return self._normalize_data(
self.get_json(f"{self.api_root}/{resource}", params=kwargs)
)
@ -240,29 +240,39 @@ class MusicBrainzAPI(RequestHandler):
@singledispatchmethod
@classmethod
def _group_relations(cls, data: Any) -> Any:
"""Normalize MusicBrainz 'relations' into type-keyed fields recursively.
def _normalize_data(cls, data: Any) -> Any:
"""Normalize MusicBrainz relation structures into easier-to-use shapes.
This helper rewrites payloads that use a generic 'relations' list into
a structure that is easier to consume downstream. When a mapping
contains 'relations', those entries are regrouped by their 'target-type'
and stored under keys like '<target-type>-relations'. The original
'relations' key is removed to avoid ambiguous access patterns.
The transformation is applied recursively so that nested objects and
sequences are normalized consistently, while non-container values are
left unchanged.
This default handler is a no-op that returns non-container values
unchanged. Specialized handlers for sequences and mappings perform the
actual transformations described below.
"""
return data
@_group_relations.register(list)
@_normalize_data.register(list)
@classmethod
def _(cls, data: list[Any]) -> list[Any]:
return [cls._group_relations(i) for i in data]
"""Apply normalization to each element of a sequence recursively.
@_group_relations.register(dict)
Sequences received from the MusicBrainz API may contain nested mappings
that require transformation. This handler maps the normalization step
over the sequence and preserves order.
"""
return [cls._normalize_data(i) for i in data]
@_normalize_data.register(dict)
@classmethod
def _(cls, data: JSONDict) -> JSONDict:
"""Transform mappings by regrouping relationships and normalizing keys.
When a mapping contains a generic 'relations' list, entries are grouped
by their 'target-type' and placed under keys like
'<target-type>_relations' with the 'target-type' field removed from each
entry. All other mapping keys have hyphens converted to underscores and
their values are normalized recursively to ensure a consistent shape
throughout the payload.
"""
output_data = {}
for k, v in list(data.items()):
if k == "relations":
get_target_type = operator.methodcaller("get", "target-type")
@ -273,13 +283,12 @@ class MusicBrainzAPI(RequestHandler):
{k: v for k, v in item.items() if k != "target-type"}
for item in group
]
data[f"{target_type}-relations"] = cls._group_relations(
relations
output_data[f"{target_type}_relations"] = (
cls._normalize_data(relations)
)
data.pop("relations")
else:
data[k] = cls._group_relations(v)
return data
output_data[k.replace("-", "_")] = cls._normalize_data(v)
return output_data
class MusicBrainzAPIMixin:

View file

@ -168,7 +168,7 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
return [
pr_id
for rel in data.get("release-relations", [])
for rel in data.get("release_relations", [])
if (pr_id := self._wanted_pseudo_release_id(album_id, rel))
is not None
]
@ -176,7 +176,7 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
def _has_desired_script(self, release: JSONDict) -> bool:
if len(self._scripts) == 0:
return False
elif script := release.get("text-representation", {}).get("script"):
elif script := release.get("text_representation", {}).get("script"):
return script in self._scripts
else:
return False
@ -216,9 +216,9 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
if len(config["import"]["languages"].as_str_seq()) > 0:
return
lang = raw_pseudo_release.get("text-representation", {}).get("language")
artist_credits = raw_pseudo_release.get("release-group", {}).get(
"artist-credit", []
lang = raw_pseudo_release.get("text_representation", {}).get("language")
artist_credits = raw_pseudo_release.get("release_group", {}).get(
"artist_credit", []
)
aliases = [
artist_credit.get("artist", {}).get("aliases", [])

View file

@ -160,9 +160,9 @@ def _multi_artist_credit(
# Artist sort name.
if alias:
artist_sort_parts.append(alias["sort-name"])
elif "sort-name" in el["artist"]:
artist_sort_parts.append(el["artist"]["sort-name"])
artist_sort_parts.append(alias["sort_name"])
elif "sort_name" in el["artist"]:
artist_sort_parts.append(el["artist"]["sort_name"])
else:
artist_sort_parts.append(cur_artist_name)
@ -245,10 +245,10 @@ def _preferred_release_event(
].as_str_seq()
for country in preferred_countries:
for event in release.get("release-events", {}):
for event in release.get("release_events", {}):
try:
if area := event.get("area"):
if country in area["iso-3166-1-codes"]:
if country in area["iso_3166_1_codes"]:
return country, event["date"]
except KeyError:
pass
@ -381,28 +381,28 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
data_url=track_url(recording["id"]),
)
if recording.get("artist-credit"):
if recording.get("artist_credit"):
# Get the artist names.
(
info.artist,
info.artist_sort,
info.artist_credit,
) = _flatten_artist_credit(recording["artist-credit"])
) = _flatten_artist_credit(recording["artist_credit"])
(
info.artists,
info.artists_sort,
info.artists_credit,
) = _multi_artist_credit(
recording["artist-credit"], include_join_phrase=False
recording["artist_credit"], include_join_phrase=False
)
info.artists_ids = _artist_ids(recording["artist-credit"])
info.artists_ids = _artist_ids(recording["artist_credit"])
info.artist_id = info.artists_ids[0]
if recording.get("artist-relations"):
if recording.get("artist_relations"):
info.remixer = _get_related_artist_names(
recording["artist-relations"], relation_type="remixer"
recording["artist_relations"], relation_type="remixer"
)
if recording.get("length"):
@ -416,7 +416,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
lyricist = []
composer = []
composer_sort = []
for work_relation in recording.get("work-relations", ()):
for work_relation in recording.get("work_relations", ()):
if work_relation["type"] != "performance":
continue
info.work = work_relation["work"]["title"]
@ -425,7 +425,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
info.work_disambig = work_relation["work"]["disambiguation"]
for artist_relation in work_relation["work"].get(
"artist-relations", ()
"artist_relations", ()
):
if "type" in artist_relation:
type = artist_relation["type"]
@ -434,7 +434,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
elif type == "composer":
composer.append(artist_relation["artist"]["name"])
composer_sort.append(
artist_relation["artist"]["sort-name"]
artist_relation["artist"]["sort_name"]
)
if lyricist:
info.lyricist = ", ".join(lyricist)
@ -443,7 +443,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
info.composer_sort = ", ".join(composer_sort)
arranger = []
for artist_relation in recording.get("artist-relations", ()):
for artist_relation in recording.get("artist_relations", ()):
if "type" in artist_relation:
type = artist_relation["type"]
if type == "arranger":
@ -464,7 +464,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
"""
# Get artist name using join phrases.
artist_name, artist_sort_name, artist_credit_name = (
_flatten_artist_credit(release["artist-credit"])
_flatten_artist_credit(release["artist_credit"])
)
(
@ -472,7 +472,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
artists_sort_names,
artists_credit_names,
) = _multi_artist_credit(
release["artist-credit"], include_join_phrase=False
release["artist_credit"], include_join_phrase=False
)
ntracks = sum(len(m["tracks"]) for m in release["media"])
@ -508,10 +508,10 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
all_tracks = medium["tracks"]
if (
"data-tracks" in medium
"data_tracks" in medium
and not config["match"]["ignore_data_tracks"]
):
all_tracks += medium["data-tracks"]
all_tracks += medium["data_tracks"]
track_count = len(all_tracks)
if "pregap" in medium:
@ -548,30 +548,30 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
# Prefer track data, where present, over recording data.
if track.get("title"):
ti.title = track["title"]
if track.get("artist-credit"):
if track.get("artist_credit"):
# Get the artist names.
(
ti.artist,
ti.artist_sort,
ti.artist_credit,
) = _flatten_artist_credit(track["artist-credit"])
) = _flatten_artist_credit(track["artist_credit"])
(
ti.artists,
ti.artists_sort,
ti.artists_credit,
) = _multi_artist_credit(
track["artist-credit"], include_join_phrase=False
track["artist_credit"], include_join_phrase=False
)
ti.artists_ids = _artist_ids(track["artist-credit"])
ti.artists_ids = _artist_ids(track["artist_credit"])
ti.artist_id = ti.artists_ids[0]
if track.get("length"):
ti.length = int(track["length"]) / (1000.0)
track_infos.append(ti)
album_artist_ids = _artist_ids(release["artist-credit"])
album_artist_ids = _artist_ids(release["artist_credit"])
info = beets.autotag.hooks.AlbumInfo(
album=release["title"],
album_id=release["id"],
@ -593,38 +593,38 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
if info.va:
info.artist = config["va_name"].as_str()
info.asin = release.get("asin")
info.releasegroup_id = release["release-group"]["id"]
info.releasegroup_id = release["release_group"]["id"]
info.albumstatus = release.get("status")
if release["release-group"].get("title"):
info.release_group_title = release["release-group"].get("title")
if release["release_group"].get("title"):
info.release_group_title = release["release_group"].get("title")
# Get the disambiguation strings at the release and release group level.
if release["release-group"].get("disambiguation"):
info.releasegroupdisambig = release["release-group"].get(
if release["release_group"].get("disambiguation"):
info.releasegroupdisambig = release["release_group"].get(
"disambiguation"
)
if release.get("disambiguation"):
info.albumdisambig = release.get("disambiguation")
if reltype := release["release-group"].get("primary-type"):
if reltype := release["release_group"].get("primary_type"):
info.albumtype = reltype.lower()
# Set the new-style "primary" and "secondary" release types.
albumtypes = []
if "primary-type" in release["release-group"]:
rel_primarytype = release["release-group"]["primary-type"]
if "primary_type" in release["release_group"]:
rel_primarytype = release["release_group"]["primary_type"]
if rel_primarytype:
albumtypes.append(rel_primarytype.lower())
if "secondary-types" in release["release-group"]:
if release["release-group"]["secondary-types"]:
for sec_type in release["release-group"]["secondary-types"]:
if "secondary_types" in release["release_group"]:
if release["release_group"]["secondary_types"]:
for sec_type in release["release_group"]["secondary_types"]:
albumtypes.append(sec_type.lower())
info.albumtypes = albumtypes
# Release events.
info.country, release_date = _preferred_release_event(release)
release_group_date = release["release-group"].get("first-release-date")
release_group_date = release["release_group"].get("first_release_date")
if not release_date:
# Fall back if release-specific date is not available.
release_date = release_group_date
@ -634,17 +634,17 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
_set_date_str(info, release_group_date, True)
# Label name.
if release.get("label-info"):
label_info = release["label-info"][0]
if release.get("label_info"):
label_info = release["label_info"][0]
if label_info.get("label"):
label = label_info["label"]["name"]
if label != "[no label]":
info.label = label
info.catalognum = label_info.get("catalog-number")
info.catalognum = label_info.get("catalog_number")
# Text representation data.
if release.get("text-representation"):
rep = release["text-representation"]
if release.get("text_representation"):
rep = release["text_representation"]
info.script = rep.get("script")
info.language = rep.get("language")
@ -659,7 +659,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
if self.config["genres"]:
sources = [
release["release-group"].get(self.genres_field, []),
release["release_group"].get(self.genres_field, []),
release.get(self.genres_field, []),
]
genres: Counter[str] = Counter()
@ -676,7 +676,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
wanted_sources = {
site for site, wanted in external_ids.items() if wanted
}
if wanted_sources and (url_rels := release.get("url-relations")):
if wanted_sources and (url_rels := release.get("url_relations")):
urls = {}
for source, url in product(wanted_sources, url_rels):
@ -800,7 +800,7 @@ class MusicBrainzPlugin(MusicBrainzAPIMixin, MetadataSourcePlugin):
actual_res = None
if res.get("status") == "Pseudo-Release" and (
relations := res.get("release-relations")
relations := res.get("release_relations")
):
for rel in relations:
if (

View file

@ -163,7 +163,7 @@ class TestMBPseudoPlugin(TestMBPseudoMixin):
official_release: JSONDict,
json_key: str,
):
del official_release["release-relations"][0][json_key]
del official_release["release_relations"][0][json_key]
album_info = mbpseudo_plugin.album_info(official_release)
assert not isinstance(album_info, PseudoAlbumInfo)
@ -174,8 +174,8 @@ class TestMBPseudoPlugin(TestMBPseudoMixin):
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
):
official_release["release-relations"][0]["release"][
"text-representation"
official_release["release_relations"][0]["release"][
"text_representation"
]["script"] = "Null"
album_info = mbpseudo_plugin.album_info(official_release)

View file

@ -51,18 +51,18 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
"id": "ALBUM ID",
"asin": "ALBUM ASIN",
"disambiguation": "R_DISAMBIGUATION",
"release-group": {
"primary-type": "Album",
"first-release-date": date_str,
"release_group": {
"primary_type": "Album",
"first_release_date": date_str,
"id": "RELEASE GROUP ID",
"disambiguation": "RG_DISAMBIGUATION",
},
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "ARTIST NAME",
"id": "ARTIST ID",
"sort-name": "ARTIST SORT NAME",
"sort_name": "ARTIST SORT NAME",
},
"name": "ARTIST CREDIT",
}
@ -71,30 +71,30 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
"media": [],
"genres": [{"count": 1, "name": "GENRE"}],
"tags": [{"count": 1, "name": "TAG"}],
"label-info": [
"label_info": [
{
"catalog-number": "CATALOG NUMBER",
"catalog_number": "CATALOG NUMBER",
"label": {"name": "LABEL NAME"},
}
],
"text-representation": {
"text_representation": {
"script": "SCRIPT",
"language": "LANGUAGE",
},
"country": "COUNTRY",
"status": "STATUS",
"barcode": "BARCODE",
"release-events": [{"area": None, "date": "2021-03-26"}],
"release_events": [{"area": None, "date": "2021-03-26"}],
}
if multi_artist_credit:
release["artist-credit"][0]["joinphrase"] = " & "
release["artist-credit"].append(
release["artist_credit"][0]["joinphrase"] = " & "
release["artist_credit"].append(
{
"artist": {
"name": "ARTIST 2 NAME",
"id": "ARTIST 2 ID",
"sort-name": "ARTIST 2 SORT NAME",
"sort_name": "ARTIST 2 SORT NAME",
},
"name": "ARTIST MULTI CREDIT",
}
@ -117,25 +117,25 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
if track_artist:
# Similarly, track artists can differ from recording
# artists.
track["artist-credit"] = [
track["artist_credit"] = [
{
"artist": {
"name": "TRACK ARTIST NAME",
"id": "TRACK ARTIST ID",
"sort-name": "TRACK ARTIST SORT NAME",
"sort_name": "TRACK ARTIST SORT NAME",
},
"name": "TRACK ARTIST CREDIT",
}
]
if multi_artist_credit:
track["artist-credit"][0]["joinphrase"] = " & "
track["artist-credit"].append(
track["artist_credit"][0]["joinphrase"] = " & "
track["artist_credit"].append(
{
"artist": {
"name": "TRACK ARTIST 2 NAME",
"id": "TRACK ARTIST 2 ID",
"sort-name": "TRACK ARTIST 2 SORT NAME",
"sort_name": "TRACK ARTIST 2 SORT NAME",
},
"name": "TRACK ARTIST 2 CREDIT",
}
@ -157,7 +157,7 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
{
"position": "1",
"tracks": track_list,
"data-tracks": data_track_list,
"data_tracks": data_track_list,
"format": medium_format,
"title": "MEDIUM TITLE",
}
@ -182,39 +182,39 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
if duration is not None:
track["length"] = duration
if artist:
track["artist-credit"] = [
track["artist_credit"] = [
{
"artist": {
"name": "RECORDING ARTIST NAME",
"id": "RECORDING ARTIST ID",
"sort-name": "RECORDING ARTIST SORT NAME",
"sort_name": "RECORDING ARTIST SORT NAME",
},
"name": "RECORDING ARTIST CREDIT",
}
]
if multi_artist_credit:
track["artist-credit"][0]["joinphrase"] = " & "
track["artist-credit"].append(
track["artist_credit"][0]["joinphrase"] = " & "
track["artist_credit"].append(
{
"artist": {
"name": "RECORDING ARTIST 2 NAME",
"id": "RECORDING ARTIST 2 ID",
"sort-name": "RECORDING ARTIST 2 SORT NAME",
"sort_name": "RECORDING ARTIST 2 SORT NAME",
},
"name": "RECORDING ARTIST 2 CREDIT",
}
)
if remixer:
track["artist-relations"] = [
track["artist_relations"] = [
{
"type": "remixer",
"type-id": "RELATION TYPE ID",
"type_id": "RELATION TYPE ID",
"direction": "RECORDING RELATION DIRECTION",
"artist": {
"id": "RECORDING REMIXER ARTIST ID",
"type": "RECORDING REMIXER ARTIST TYPE",
"name": "RECORDING REMIXER ARTIST NAME",
"sort-name": "RECORDING REMIXER ARTIST SORT NAME",
"sort_name": "RECORDING REMIXER ARTIST SORT NAME",
},
}
]
@ -354,7 +354,7 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
def test_detect_various_artists(self):
release = self._make_release(None)
release["artist-credit"][0]["artist"]["id"] = (
release["artist_credit"][0]["artist"]["id"] = (
musicbrainz.VARIOUS_ARTISTS_ID
)
d = self.mb.album_info(release)
@ -429,7 +429,7 @@ class MBAlbumInfoTest(MusicBrainzTestCase):
def test_missing_language(self):
release = self._make_release(None)
del release["text-representation"]["language"]
del release["text_representation"]["language"]
d = self.mb.album_info(release)
assert d.language is None
@ -697,7 +697,7 @@ class ArtistFlatteningTest(unittest.TestCase):
return {
"artist": {
"name": f"NAME{suffix}",
"sort-name": f"SORT{suffix}",
"sort_name": f"SORT{suffix}",
},
"name": f"CREDIT{suffix}",
}
@ -706,7 +706,7 @@ class ArtistFlatteningTest(unittest.TestCase):
alias = {
"name": f"ALIAS{suffix}",
"locale": locale,
"sort-name": f"ALIASSORT{suffix}",
"sort_name": f"ALIASSORT{suffix}",
}
if primary:
alias["primary"] = "primary"
@ -810,7 +810,7 @@ class MBLibraryTest(MusicBrainzTestCase):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "some-artist",
@ -818,10 +818,10 @@ class MBLibraryTest(MusicBrainzTestCase):
},
}
],
"release-group": {
"release_group": {
"id": "another-id",
},
"release-relations": [
"release_relations": [
{
"type": "transl-tracklisting",
"direction": "backward",
@ -852,7 +852,7 @@ class MBLibraryTest(MusicBrainzTestCase):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "some-artist",
@ -860,7 +860,7 @@ class MBLibraryTest(MusicBrainzTestCase):
},
}
],
"release-group": {
"release_group": {
"id": "another-id",
},
"country": "COUNTRY",
@ -897,7 +897,7 @@ class MBLibraryTest(MusicBrainzTestCase):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "some-artist",
@ -905,7 +905,7 @@ class MBLibraryTest(MusicBrainzTestCase):
},
}
],
"release-group": {
"release_group": {
"id": "another-id",
},
}
@ -941,7 +941,7 @@ class MBLibraryTest(MusicBrainzTestCase):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "some-artist",
@ -949,7 +949,7 @@ class MBLibraryTest(MusicBrainzTestCase):
},
}
],
"release-group": {
"release_group": {
"id": "another-id",
},
}
@ -985,7 +985,7 @@ class MBLibraryTest(MusicBrainzTestCase):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{
"artist": {
"name": "some-artist",
@ -993,10 +993,10 @@ class MBLibraryTest(MusicBrainzTestCase):
},
}
],
"release-group": {
"release_group": {
"id": "another-id",
},
"release-relations": [
"release_relations": [
{
"type": "remaster",
"direction": "backward",
@ -1097,10 +1097,10 @@ class TestMusicBrainzPlugin(PluginMixin):
"position": 5,
}
],
"artist-credit": [
"artist_credit": [
{"artist": {"name": "some-artist", "id": "some-id"}}
],
"release-group": {"id": "another-id"},
"release_group": {"id": "another-id"},
},
)
candidates = list(mb.candidates([], "hello", "there", False))

View file

@ -1,26 +1,44 @@
from beetsplug._utils.musicbrainz import MusicBrainzAPI
def test_group_relations():
def test_normalize_data():
raw_release = {
"id": "r1",
"relations": [
{"target-type": "artist", "type": "vocal", "name": "A"},
{"target-type": "url", "type": "streaming", "url": "http://s"},
{"target-type": "url", "type": "purchase", "url": "http://p"},
{
"target-type": "artist",
"type": "vocal",
"type-id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa",
"name": "A",
},
{
"target-type": "url",
"type": "streaming",
"type-id": "b5f3058a-666c-406f-aafb-f9249fc7b122",
"url": "http://s",
},
{
"target-type": "url",
"type": "purchase for download",
"type-id": "92777657-504c-4acb-bd33-51a201bd57e1",
"url": "http://p",
},
{
"target-type": "work",
"type": "performance",
"type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
"work": {
"relations": [
{
"artist": {"name": "幾田りら"},
"target-type": "artist",
"type": "composer",
"type-id": "d59d99ea-23d4-4a80-b066-edca32ee158f",
},
{
"target-type": "url",
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"resource": "https://utaten.com/lyric/tt24121002/"
},
@ -29,10 +47,12 @@ def test_group_relations():
"artist": {"name": "幾田りら"},
"target-type": "artist",
"type": "lyricist",
"type-id": "3e48faba-ec01-47fd-8e89-30e81161661c",
},
{
"target-type": "url",
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"resource": "https://www.uta-net.com/song/366579/"
},
@ -45,30 +65,59 @@ def test_group_relations():
],
}
assert MusicBrainzAPI._group_relations(raw_release) == {
assert MusicBrainzAPI._normalize_data(raw_release) == {
"id": "r1",
"artist-relations": [{"type": "vocal", "name": "A"}],
"url-relations": [
{"type": "streaming", "url": "http://s"},
{"type": "purchase", "url": "http://p"},
"artist_relations": [
{
"type": "vocal",
"type_id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa",
"name": "A",
}
],
"work-relations": [
"url_relations": [
{
"type": "streaming",
"type_id": "b5f3058a-666c-406f-aafb-f9249fc7b122",
"url": "http://s",
},
{
"type": "purchase for download",
"type_id": "92777657-504c-4acb-bd33-51a201bd57e1",
"url": "http://p",
},
],
"work_relations": [
{
"type": "performance",
"type_id": "a3005666-a872-32c3-ad06-98af558e99b0",
"work": {
"artist-relations": [
{"type": "composer", "artist": {"name": "幾田りら"}},
{"type": "lyricist", "artist": {"name": "幾田りら"}},
"artist_relations": [
{
"type": "composer",
"type_id": "d59d99ea-23d4-4a80-b066-edca32ee158f",
"artist": {
"name": "幾田りら",
},
},
{
"type": "lyricist",
"type_id": "3e48faba-ec01-47fd-8e89-30e81161661c",
"artist": {
"name": "幾田りら",
},
},
],
"url-relations": [
"url_relations": [
{
"type": "lyrics",
"type_id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"resource": "https://utaten.com/lyric/tt24121002/"
},
},
{
"type": "lyrics",
"type_id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"resource": "https://www.uta-net.com/song/366579/"
},

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"aliases": [],
"artist-credit": [
"artist_credit": [
{
"artist": {
"aliases": [
@ -11,9 +11,9 @@
"locale": "en",
"name": "Lilas Ikuta",
"primary": true,
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Artist name",
"type-id": "894afba6-2816-3c24-8072-eadb66bd04bc"
"type_id": "894afba6-2816-3c24-8072-eadb66bd04bc"
}
],
"country": "JP",
@ -34,7 +34,7 @@
],
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"tags": [
{
"count": 1,
@ -46,7 +46,7 @@
}
],
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"joinphrase": "",
"name": "Lilas Ikuta"
@ -54,7 +54,7 @@
],
"asin": null,
"barcode": null,
"cover-art-archive": {
"cover_art_archive": {
"artwork": false,
"back": false,
"count": 0,
@ -64,19 +64,19 @@
"disambiguation": "",
"genres": [],
"id": "dc3ee2df-0bc1-49eb-b8c4-34473d279a43",
"label-info": [],
"label_info": [],
"media": [
{
"format": "Digital Media",
"format-id": "907a28d9-b3b2-3ef6-89a8-7b18d91d4794",
"format_id": "907a28d9-b3b2-3ef6-89a8-7b18d91d4794",
"id": "606faab7-60fa-3a8b-a40f-2c66150cce81",
"position": 1,
"title": "",
"track-count": 1,
"track-offset": 0,
"track_count": 1,
"track_offset": 0,
"tracks": [
{
"artist-credit": [
"artist_credit": [
{
"artist": {
"aliases": [
@ -87,18 +87,18 @@
"locale": "en",
"name": "Lilas Ikuta",
"primary": true,
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Artist name",
"type-id": "894afba6-2816-3c24-8072-eadb66bd04bc"
"type_id": "894afba6-2816-3c24-8072-eadb66bd04bc"
}
],
"country": "JP",
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"joinphrase": "",
"name": "Lilas Ikuta"
@ -110,43 +110,43 @@
"position": 1,
"recording": {
"aliases": [],
"artist-credit": [
"artist_credit": [
{
"artist": {
"country": "JP",
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"joinphrase": "",
"name": "幾田りら"
}
],
"artist-relations": [
"artist_relations": [
{
"artist": {
"country": "JP",
"disambiguation": "Japanese composer/arranger/guitarist, agehasprings",
"id": "f24241fb-4d89-4bf2-8336-3f2a7d2c0025",
"name": "KOHD",
"sort-name": "KOHD",
"sort_name": "KOHD",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "arranger",
"type-id": "22661fb8-cdb7-4f67-8385-b2a8be6c9f0d"
"type_id": "22661fb8-cdb7-4f67-8385-b2a8be6c9f0d"
},
{
"artist": {
@ -154,21 +154,21 @@
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": "2025",
"direction": "backward",
"end": "2025",
"ended": true,
"source-credit": "",
"target-credit": "Lilas Ikuta",
"source_credit": "",
"target_credit": "Lilas Ikuta",
"type": "phonographic copyright",
"type-id": "7fd5fbc0-fbf4-4d04-be23-417d50a4dc30"
"type_id": "7fd5fbc0-fbf4-4d04-be23-417d50a4dc30"
},
{
"artist": {
@ -176,21 +176,21 @@
"disambiguation": "",
"id": "1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05",
"name": "山本秀哉",
"sort-name": "Yamamoto, Shuya",
"sort_name": "Yamamoto, Shuya",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "producer",
"type-id": "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0"
"type_id": "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0"
},
{
"artist": {
@ -198,25 +198,25 @@
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "vocal",
"type-id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa"
"type_id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa"
}
],
"disambiguation": "",
"first-release-date": "2025-01-10",
"first_release_date": "2025-01-10",
"genres": [],
"id": "781724c1-a039-41e6-bd9b-770c3b9d5b8e",
"isrcs": [
@ -225,53 +225,53 @@
"length": 179546,
"tags": [],
"title": "百花繚乱",
"url-relations": [
"url_relations": [
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "forward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "free streaming",
"type-id": "7e41ef12-a124-4324-afdb-fdbae687a89c",
"type_id": "7e41ef12-a124-4324-afdb-fdbae687a89c",
"url": {
"id": "d076eaf9-5fde-4f6e-a946-cde16b67aa3b",
"resource": "https://open.spotify.com/track/782PTXsbAWB70ySDZ5NHmP"
}
},
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "forward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "purchase for download",
"type-id": "92777657-504c-4acb-bd33-51a201bd57e1",
"type_id": "92777657-504c-4acb-bd33-51a201bd57e1",
"url": {
"id": "64879627-6eca-4755-98b5-b2234a8dbc61",
"resource": "https://music.apple.com/jp/song/1857886416"
}
},
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "forward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "streaming",
"type-id": "b5f3058a-666c-406f-aafb-f9249fc7b122",
"type_id": "b5f3058a-666c-406f-aafb-f9249fc7b122",
"url": {
"id": "64879627-6eca-4755-98b5-b2234a8dbc61",
"resource": "https://music.apple.com/jp/song/1857886416"
@ -279,42 +279,42 @@
}
],
"video": false,
"work-relations": [
"work_relations": [
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "forward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "performance",
"type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
"type_id": "a3005666-a872-32c3-ad06-98af558e99b0",
"work": {
"artist-relations": [
"artist_relations": [
{
"artist": {
"country": "JP",
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "composer",
"type-id": "d59d99ea-23d4-4a80-b066-edca32ee158f"
"type_id": "d59d99ea-23d4-4a80-b066-edca32ee158f"
},
{
"artist": {
@ -322,21 +322,21 @@
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "lyricist",
"type-id": "3e48faba-ec01-47fd-8e89-30e81161661c"
"type_id": "3e48faba-ec01-47fd-8e89-30e81161661c"
}
],
"attributes": [],
@ -349,37 +349,37 @@
],
"title": "百花繚乱",
"type": "Song",
"type-id": "f061270a-2fd6-32f1-a641-f0f8676d14e6",
"url-relations": [
"type_id": "f061270a-2fd6-32f1-a641-f0f8676d14e6",
"url_relations": [
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"type_id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"id": "dfac3640-6b23-4991-a59c-7cb80e8eb950",
"resource": "https://utaten.com/lyric/tt24121002/"
}
},
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"type_id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"url": {
"id": "b1b5d5df-e79d-4cda-bb2a-8014e5505415",
"resource": "https://www.uta-net.com/song/366579/"
@ -396,11 +396,11 @@
}
],
"packaging": null,
"packaging-id": null,
"packaging_id": null,
"quality": "normal",
"release-group": {
"release_group": {
"aliases": [],
"artist-credit": [
"artist_credit": [
{
"artist": {
"aliases": [
@ -411,54 +411,54 @@
"locale": "en",
"name": "Lilas Ikuta",
"primary": true,
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Artist name",
"type-id": "894afba6-2816-3c24-8072-eadb66bd04bc"
"type_id": "894afba6-2816-3c24-8072-eadb66bd04bc"
}
],
"country": "JP",
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": "Person",
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df"
"type_id": "b6e035f4-3ce9-331c-97df-83397230b0df"
},
"joinphrase": "",
"name": "幾田りら"
}
],
"disambiguation": "",
"first-release-date": "2025-01-10",
"first_release_date": "2025-01-10",
"genres": [],
"id": "da0d6bbb-f44b-4fff-8739-9d72db0402a1",
"primary-type": "Single",
"primary-type-id": "d6038452-8ee0-3f68-affc-2de9a1ede0b9",
"secondary-type-ids": [],
"secondary-types": [],
"primary_type": "Single",
"primary_type_id": "d6038452-8ee0-3f68-affc-2de9a1ede0b9",
"secondary_type_ids": [],
"secondary_types": [],
"tags": [],
"title": "百花繚乱"
},
"release-relations": [
"release_relations": [
{
"attribute-ids": {},
"attribute-values": {},
"attribute_ids": {},
"attribute_values": {},
"attributes": [],
"begin": null,
"direction": "backward",
"end": null,
"ended": false,
"release": {
"artist-credit": [
"artist_credit": [
{
"artist": {
"country": "JP",
"disambiguation": "",
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"sort_name": "Ikuta, Lilas",
"type": null,
"type-id": null
"type_id": null
},
"joinphrase": "",
"name": "幾田りら"
@ -471,43 +471,43 @@
"id": "a5ce1d11-2e32-45a4-b37f-c1589d46b103",
"media": [],
"packaging": null,
"packaging-id": null,
"packaging_id": null,
"quality": "normal",
"release-events": [
"release_events": [
{
"area": {
"disambiguation": "",
"id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
"iso-3166-1-codes": [
"iso_3166_1_codes": [
"XW"
],
"name": "[Worldwide]",
"sort-name": "[Worldwide]",
"sort_name": "[Worldwide]",
"type": null,
"type-id": null
"type_id": null
},
"date": "2025-01-10"
}
],
"release-group": null,
"release_group": null,
"status": null,
"status-id": null,
"text-representation": {
"status_id": null,
"text_representation": {
"language": "jpn",
"script": "Jpan"
},
"title": "百花繚乱"
},
"source-credit": "",
"target-credit": "",
"source_credit": "",
"target_credit": "",
"type": "transl-tracklisting",
"type-id": "fc399d47-23a7-4c28-bfcf-0607a562b644"
"type_id": "fc399d47-23a7-4c28-bfcf-0607a562b644"
}
],
"status": "Pseudo-Release",
"status-id": "41121bb9-3413-3818-8a9a-9742318349aa",
"status_id": "41121bb9-3413-3818-8a9a-9742318349aa",
"tags": [],
"text-representation": {
"text_representation": {
"language": "eng",
"script": "Latn"
},