diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index d31887f805..741c1cd68c 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -120,6 +120,406 @@ class BrowseRecordingsKwargs(BrowseReleaseGroupsKwargs, total=False): R = TypeVar("R") +class _Period(TypedDict): + begin: str | None + end: str | None + ended: bool + + +class Alias(_Period): + locale: str | None + name: str + primary: bool | None + sort_name: str + type: ( + Literal[ + "Artist name", + "Label name", + "Legal name", + "Recording name", + "Release name", + "Release group name", + "Search hint", + ] + | None + ) + type_id: str | None + + +class Artist(TypedDict): + country: str | None + disambiguation: str + id: str + name: str + sort_name: str + type: ( + Literal["Character", "Choir", "Group", "Orchestra", "Other", "Person"] + | None + ) + type_id: str | None + aliases: NotRequired[list[Alias]] + genres: NotRequired[list[Genre]] + tags: NotRequired[list[Tag]] + + +class ArtistCredit(TypedDict): + artist: Artist + joinphrase: str + name: str + + +class Genre(TypedDict): + count: int + disambiguation: str + id: str + name: str + + +class Tag(TypedDict): + count: int + name: str + + +ReleaseStatus = Literal[ + "Bootleg", + "Cancelled", + "Expunged", + "Official", + "Promotion", + "Pseudo-Release", + "Withdrawn", +] + +ReleasePackaging = Literal[ + "Book", + "Box", + "Cardboard/Paper Sleeve", + "Cassette Case", + "Clamshell Case", + "Digibook", + "Digifile", + "Digipak", + "Discbox Slider", + "Fatbox", + "Gatefold Cover", + "Jewel Case", + "None", + "Keep Case", + "Longbox", + "Metal Tin", + "Other", + "Plastic Sleeve", + "Slidepack", + "Slipcase", + "Snap Case", + "SnapPack", + "Slim Jewel Case", + "Super Jewel Box", +] + + +ReleaseQuality = Literal["high", "low", "normal"] + + +class ReleaseGroup(TypedDict): + aliases: list[Alias] + artist_credit: list[ArtistCredit] + disambiguation: str + first_release_date: str + genres: list[Genre] + id: str + primary_type: Literal["Album", "Broadcast", "EP", "Other", "Single"] | None + primary_type_id: str | None + secondary_type_ids: list[str] + secondary_types: list[ + Literal[ + "Audiobook", + "Audio drama", + "Compilation", + "DJ-mix", + "Demo", + "Field recording", + "Interview", + "Live", + "Mixtape/Street", + "Remix", + "Soundtrack", + "Spokenword", + ] + ] + tags: list[Tag] + title: str + + +class CoverArtArchive(TypedDict): + artwork: bool + back: bool + count: int + darkened: bool + front: bool + + +class TextRepresentation(TypedDict): + language: str | None + script: str | None + + +class Area(TypedDict): + disambiguation: str + id: str + iso_3166_1_codes: list[str] + iso_3166_2_codes: NotRequired[list[str]] + name: str + sort_name: str + type: None + type_id: None + + +class ReleaseEvent(TypedDict): + area: Area | None + date: str + + +class Label(TypedDict): + aliases: list[Alias] + disambiguation: str + genres: list[Genre] + id: str + label_code: int | None + name: str + sort_name: str + tags: list[Tag] + type: ( + Literal[ + "Bootleg Production", + "Broadcaster", + "Distributor", + "Holding", + "Imprint", + "Manufacturer", + "Original Production", + "Publisher", + "Reissue Production", + "Rights Society", + ] + | None + ) + type_id: str | None + + +class LabelInfo(TypedDict): + catalog_number: str | None + label: Label + + +class Url(TypedDict): + id: str + resource: str + + +class RelationBase(_Period): + attribute_ids: dict[str, str] + attribute_values: dict[str, str] + attributes: list[str] + direction: Literal["backward", "forward"] + source_credit: str + target_credit: str + type_id: str + + +ArtistRelationType = Literal[ + "arranger", + "art direction", + "artwork", + "composer", + "conductor", + "copyright", + "design", + "design/illustration", + "editor", + "engineer", + "graphic design", + "illustration", + "instrument", + "instrument arranger", + "liner notes", + "lyricist", + "mastering", + "misc", + "mix", + "mix-DJ", + "performer", + "phonographic copyright", + "photography", + "previous attribution", + "producer", + "programming", + "recording", + "remixer", + "sound", + "vocal", + "vocal arranger", + "writer", +] + + +class ArtistRelation(RelationBase): + type: ArtistRelationType + artist: Artist + attribute_credits: NotRequired[dict[str, str]] + + +class UrlRelation(RelationBase): + type: Literal[ + "IMDB samples", + "IMDb", + "allmusic", + "amazon asin", + "discography entry", + "discogs", + "download for free", + "fanpage", + "free streaming", + "lyrics", + "other databases", + "purchase for download", + "purchase for mail-order", + "secondhandsongs", + "show notes", + "songfacts", + "streaming", + "wikidata", + "wikipedia", + ] + url: Url + + +class WorkRelation(RelationBase): + type: Literal[ + "adaptation", + "arrangement", + "based on", + "included works", + "lyrical quotation", + "medley", + "musical quotation", + "named after work", + "orchestration", + "other version", + "parts", + "revision of", + ] + ordering_key: NotRequired[int] + work: Work + + +class Work(TypedDict): + attributes: list[str] + disambiguation: str + id: str + iswcs: list[str] + language: str | None + languages: list[str] + title: str + type: str | None + type_id: str | None + artist_relations: NotRequired[list[ArtistRelation]] + url_relations: NotRequired[list[UrlRelation]] + work_relations: NotRequired[list[WorkRelation]] + + +class Recording(TypedDict): + aliases: list[Alias] + artist_credit: list[ArtistCredit] + disambiguation: str + id: str + isrcs: list[str] + length: int | None + title: str + video: bool + artist_relations: NotRequired[list[ArtistRelation]] + first_release_date: NotRequired[str] + genres: NotRequired[list[Genre]] + tags: NotRequired[list[Tag]] + url_relations: NotRequired[list[UrlRelation]] + work_relations: NotRequired[list[WorkRelation]] + + +class Track(TypedDict): + artist_credit: list[ArtistCredit] + id: str + length: int | None + number: str + position: int + recording: Recording + title: str + + +class Medium(TypedDict): + format: str | None + format_id: str | None + id: str + position: int + title: str + track_count: int + data_tracks: NotRequired[list[Track]] + pregap: NotRequired[Track] + track_offset: NotRequired[int] + tracks: NotRequired[list[Track]] + + +class ReleaseRelationRelease(TypedDict): + artist_credit: list[ArtistCredit] + barcode: str | None + disambiguation: str + id: str + media: list[Medium] + packaging: ReleasePackaging | None + packaging_id: str | None + quality: ReleaseQuality + release_group: ReleaseGroup | None + status: ReleaseStatus | None + status_id: str | None + text_representation: TextRepresentation + title: str + country: NotRequired[str | None] + date: NotRequired[str] + release_events: NotRequired[list[ReleaseEvent]] + + +class ReleaseRelation(RelationBase): + type: Literal["remaster", "transl-tracklisting", "replaced by"] + release: ReleaseRelationRelease + + +class Release(TypedDict): + aliases: list[Alias] + artist_credit: list[ArtistCredit] + asin: str | None + barcode: str | None + cover_art_archive: CoverArtArchive + disambiguation: str + genres: list[Genre] + id: str + label_info: list[LabelInfo] + media: list[Medium] + packaging: ReleasePackaging | None + packaging_id: str | None + quality: ReleaseQuality + release_group: ReleaseGroup + status: ReleaseStatus | None + status_id: str | None + tags: list[Tag] + text_representation: TextRepresentation + title: str + artist_relations: NotRequired[list[ArtistRelation]] + country: NotRequired[str | None] + date: NotRequired[str] + release_events: NotRequired[list[ReleaseEvent]] + release_relations: NotRequired[list[ReleaseRelation]] + url_relations: NotRequired[list[UrlRelation]] + + def require_one_of(*keys: str) -> Callable[[Callable[P, R]], Callable[P, R]]: required = frozenset(keys) @@ -191,6 +591,10 @@ def request(self, *args, **kwargs) -> Response: kwargs["params"]["fmt"] = "json" return super().request(*args, **kwargs) + def get_json(self, *args, **kwargs): + """Fetch JSON data from MusicBrainz and normalize its field names.""" + return self._normalize_data(super().get_json(*args, **kwargs)) + def _get_resource( self, resource: str, includes: list[str] | None = None, **kwargs ) -> JSONDict: @@ -203,17 +607,18 @@ def _get_resource( if includes: kwargs["inc"] = "+".join(includes) - return self._group_relations( - self.get_json(f"{self.api_root}/{resource}", params=kwargs) - ) + return self.get_json(f"{self.api_root}/{resource}", params=kwargs) def _lookup( self, entity: Entity, id_: str, **kwargs: Unpack[LookupKwargs] - ) -> JSONDict: + ) -> Any: return self._get_resource(f"{entity}/{id_}", **kwargs) - def _browse(self, entity: Entity, **kwargs) -> list[JSONDict]: - return self._get_resource(entity, **kwargs).get(f"{entity}s", []) + def _browse(self, entity: Entity, **kwargs) -> list[Any]: + normalised_entity = entity.replace("-", "_") + return self._get_resource(entity, **kwargs).get( + f"{normalised_entity}s", [] + ) @staticmethod def format_search_term(field: str, term: str) -> str: @@ -249,28 +654,29 @@ def search( ) log.debug("Searching for MusicBrainz {}s with: {!r}", entity, query) kwargs["query"] = query - return self._get_resource(entity, **kwargs)[f"{entity}s"] + normalised_entity = entity.replace("-", "_") + return self._get_resource(entity, **kwargs)[f"{normalised_entity}s"] - def get_release(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSONDict: + def get_release(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> Release: """Retrieve a release by its MusicBrainz ID.""" kwargs.setdefault("includes", RELEASE_INCLUDES) return self._lookup("release", id_, **kwargs) def get_recording( self, id_: str, **kwargs: Unpack[LookupKwargs] - ) -> JSONDict: + ) -> Recording: """Retrieve a recording by its MusicBrainz ID.""" kwargs.setdefault("includes", RECORDING_INCLUDES) return self._lookup("recording", id_, **kwargs) - def get_work(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSONDict: + def get_work(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> Work: """Retrieve a work by its MusicBrainz ID.""" return self._lookup("work", id_, **kwargs) @require_one_of("artist", "collection", "release", "work") def browse_recordings( self, **kwargs: Unpack[BrowseRecordingsKwargs] - ) -> list[JSONDict]: + ) -> list[Recording]: """Browse recordings related to the given entities. At least one of artist, collection, release, or work must be provided. @@ -280,7 +686,7 @@ def browse_recordings( @require_one_of("artist", "collection", "release") def browse_release_groups( self, **kwargs: Unpack[BrowseReleaseGroupsKwargs] - ) -> list[JSONDict]: + ) -> list[ReleaseGroup]: """Browse release groups related to the given entities. At least one of artist, collection, or release must be provided. @@ -290,29 +696,39 @@ def browse_release_groups( @singledispatchmethod @classmethod - def _group_relations(cls, data: Any) -> Any: - """Normalize MusicBrainz 'relations' into type-keyed fields recursively. - - 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 '-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. + def _normalize_data(cls, data: Any) -> Any: + """Normalize MusicBrainz relation structures into easier-to-use shapes. + + 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. + + 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] - @_group_relations.register(dict) + @_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 + '_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") @@ -323,13 +739,13 @@ def _(cls, data: JSONDict) -> JSONDict: {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 + normalized_target_type = target_type.replace("-", "_") + output_data[f"{normalized_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: diff --git a/beetsplug/listenbrainz.py b/beetsplug/listenbrainz.py index 228ca7c348..859759b459 100644 --- a/beetsplug/listenbrainz.py +++ b/beetsplug/listenbrainz.py @@ -299,7 +299,7 @@ def get_track_info(self, tracks): identifier, includes=["releases", "artist-credits"] ) title = recording.get("title") - artist_credit = recording.get("artist-credit", []) + artist_credit = recording.get("artist_credit", []) if artist_credit: artist = artist_credit[0].get("artist", {}).get("name") else: diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index 9027904cbb..9326305aca 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -101,7 +101,7 @@ def id(self) -> str: @property def release_count(self) -> int: """Total number of releases recorded in the collection.""" - return self.data["release-count"] + return self.data["release_count"] @property def releases_url(self) -> str: @@ -173,7 +173,7 @@ def collection(self) -> MBCollection: # Get all release collection IDs, avoiding event collections if not ( collection_by_id := { - c["id"]: c for c in collections if c["entity-type"] == "release" + c["id"]: c for c in collections if c["entity_type"] == "release" } ): raise ui.UserError("No release collection found.") diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py index d04b275000..9c8b8cdfd3 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -41,7 +41,12 @@ from beets.autotag.distance import Distance from beets.autotag.hooks import AlbumMatch from beets.library import Item - from beetsplug._typing import JSONDict + + from ._utils.musicbrainz import ( + Release, + ReleaseRelation, + ReleaseRelationRelease, + ) _STATUS_PSEUDO = "Pseudo-Release" @@ -133,7 +138,7 @@ def candidates( yield album_info @override - def album_info(self, release: JSONDict) -> AlbumInfo: + def album_info(self, release: Release) -> AlbumInfo: official_release = super().album_info(release) if release.get("status") == _STATUS_PSEUDO: @@ -161,22 +166,24 @@ def album_info(self, release: JSONDict) -> AlbumInfo: else: return official_release - def _intercept_mb_release(self, data: JSONDict) -> list[str]: + def _intercept_mb_release(self, data: Release) -> list[str]: album_id = data["id"] if "id" in data else None if self._has_desired_script(data) or not isinstance(album_id, str): return [] 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 ] - def _has_desired_script(self, release: JSONDict) -> bool: + def _has_desired_script( + self, release: Release | ReleaseRelationRelease + ) -> 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 @@ -184,7 +191,7 @@ def _has_desired_script(self, release: JSONDict) -> bool: def _wanted_pseudo_release_id( self, album_id: str, - relation: JSONDict, + relation: ReleaseRelation, ) -> str | None: if ( len(self._scripts) == 0 @@ -206,9 +213,7 @@ def _wanted_pseudo_release_id( return None def _replace_artist_with_alias( - self, - raw_pseudo_release: JSONDict, - pseudo_release: AlbumInfo, + self, raw_pseudo_release: Release, pseudo_release: AlbumInfo ): """Use the pseudo-release's language to search for artist alias if the user hasn't configured import languages.""" @@ -216,9 +221,9 @@ def _replace_artist_with_alias( 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", []) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index d3ddba9b14..d7ae127272 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -20,7 +20,7 @@ from contextlib import suppress from functools import cached_property from itertools import product -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal from urllib.parse import urljoin from confuse.exceptions import NotFoundError @@ -40,7 +40,13 @@ from beets.library import Item from beets.metadata_plugins import QueryType, SearchParams - from ._typing import JSONDict + from ._utils.musicbrainz import ( + Alias, + ArtistCredit, + Recording, + Release, + ReleaseGroup, + ) VARIOUS_ARTISTS_ID = "89ad4ac3-39f7-470e-963a-56509c546377" @@ -70,9 +76,14 @@ BROWSE_MAXTRACKS = 500 +UrlSource = Literal[ + "discogs", "bandcamp", "spotify", "deezer", "tidal", "beatport" +] + + def _preferred_alias( - aliases: list[JSONDict], languages: list[str] | None = None -) -> JSONDict | None: + aliases: list[Alias], languages: list[str] | None = None +) -> Alias | None: """Given a list of alias structures for an artist credit, select and return the user's preferred alias or None if no matching """ @@ -104,13 +115,15 @@ def _preferred_alias( return None -def _key_with_preferred_alias(obj: JSONDict, key: str) -> str: - alias = _preferred_alias(obj.get("aliases", ())) +def _key_with_preferred_alias( + obj: ReleaseGroup | Release | Recording, key: Literal["title"] +) -> str: + alias = _preferred_alias(obj.get("aliases", [])) return alias["name"] if alias else obj[key] def _multi_artist_credit( - credit: list[JSONDict], include_join_phrase: bool + credit: list[ArtistCredit], include_join_phrase: bool ) -> tuple[list[str], list[str], list[str]]: """Given a list representing an ``artist-credit`` block, accumulate data into a triple of joined artist name lists: canonical, sort, and @@ -120,7 +133,7 @@ def _multi_artist_credit( artist_sort_parts = [] artist_credit_parts = [] for el in credit: - alias = _preferred_alias(el["artist"].get("aliases", ())) + alias = _preferred_alias(el["artist"].get("aliases", [])) # An artist. cur_artist_name = alias["name"] if alias else el["artist"]["name"] @@ -128,9 +141,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) @@ -156,7 +169,7 @@ def track_url(trackid: str) -> str: return urljoin(BASE_URL, f"recording/{trackid}") -def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]: +def _flatten_artist_credit(credit: list[ArtistCredit]) -> tuple[str, str, str]: """Given a list representing an ``artist-credit`` block, flatten the data into a triple of joined artist name strings: canonical, sort, and credit. @@ -171,7 +184,7 @@ def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]: ) -def _artist_ids(credit: list[JSONDict]) -> list[str]: +def _artist_ids(credit: list[ArtistCredit]) -> list[str]: """ Given a list representing an ``artist-credit``, return a list of artist IDs @@ -188,9 +201,7 @@ def album_url(albumid: str) -> str: return urljoin(BASE_URL, f"release/{albumid}") -def _preferred_release_event( - release: dict[str, Any], -) -> tuple[str | None, str | None]: +def _preferred_release_event(release: Release) -> tuple[str | None, str | None]: """Given a release, select and return the user's preferred release event as a tuple of (country, release_date). Fall back to the default release event if a preferred event is not found. @@ -200,10 +211,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 @@ -211,7 +222,9 @@ def _preferred_release_event( return release.get("country"), release.get("date") -def _set_date_str(info: AlbumInfo, date_str: str, original: bool = False): +def _set_date_str( + info: AlbumInfo, date_str: str, original: bool = False +) -> None: """Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo object, set the object's release date fields appropriately. If `original`, then set the original_year, etc., fields. @@ -275,10 +288,14 @@ class MusicBrainzPlugin( MusicBrainzAPIMixin, SearchApiMetadataSourcePlugin[IDResponse] ): @cached_property - def genres_field(self) -> str: - return f"{self.config['genres_tag'].as_choice(['genre', 'tag'])}s" - - def __init__(self): + def genres_field(self) -> Literal["genres", "tags"]: + choices: list[Literal["genre", "tag"]] = ["genre", "tag"] + choice = self.config["genres_tag"].as_choice(choices) + if choice == "genre": + return "genres" + return "tags" + + def __init__(self) -> None: """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. """ @@ -310,7 +327,7 @@ def __init__(self): def track_info( self, - recording: JSONDict, + recording: Recording, index: int | None = None, medium: int | None = None, medium_index: int | None = None, @@ -336,39 +353,39 @@ def track_info( 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("length"): - info.length = int(recording["length"]) / 1000.0 + if length := recording.get("length"): + info.length = length / 1000.0 info.trackdisambig = recording.get("disambiguation") if recording.get("isrcs"): info.isrc = ";".join(recording["isrcs"]) - lyricists = [] - lyricists_ids = [] - composers = [] - composers_ids = [] - composer_sort = [] - for work_relation in recording.get("work-relations", ()): + lyricists: list[str] = [] + lyricists_ids: list[str] = [] + composers: list[str] = [] + composers_ids: list[str] = [] + composer_sort: list[str] = [] + for work_relation in recording.get("work_relations", ()): if work_relation["type"] != "performance": continue info.work = work_relation["work"]["title"] @@ -377,7 +394,7 @@ def track_info( 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"] @@ -388,7 +405,7 @@ def track_info( composers.append(artist_relation["artist"]["name"]) composers_ids.append(artist_relation["artist"]["id"]) composer_sort.append( - artist_relation["artist"]["sort-name"] + artist_relation["artist"]["sort_name"] ) if lyricists: info.lyricists = lyricists @@ -402,7 +419,7 @@ def track_info( arrangers_ids = [] remixers = [] remixers_ids = [] - 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": @@ -425,13 +442,13 @@ def track_info( return info - def album_info(self, release: JSONDict) -> AlbumInfo: + def album_info(self, release: Release) -> AlbumInfo: """Takes a MusicBrainz release result dictionary and returns a beets AlbumInfo object containing the interesting data about that release. """ # 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"]) ) ( @@ -439,7 +456,7 @@ def album_info(self, release: JSONDict) -> AlbumInfo: 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.get("tracks", [])) for m in release["media"]) @@ -449,7 +466,7 @@ def album_info(self, release: JSONDict) -> AlbumInfo: # on chunks of tracks to recover the same information in this case. if ntracks > BROWSE_MAXTRACKS: self._log.debug("Album {} has too many tracks", release["id"]) - recording_list = [] + recording_list: list[Recording] = [] for i in range(0, ntracks, BROWSE_CHUNKSIZE): self._log.debug("Retrieving tracks starting at {}", i) recording_list.extend( @@ -479,10 +496,10 @@ def album_info(self, release: JSONDict) -> AlbumInfo: all_tracks = medium.get("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: @@ -519,33 +536,33 @@ def album_info(self, release: JSONDict) -> AlbumInfo: # Prefer track data, where present, over recording data except # if a preferred recording alias is available. if track.get("title") and not _preferred_alias( - track["recording"].get("aliases", ()) + track["recording"].get("aliases", []) ): 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) + if length := track.get("length"): + ti.length = length / 1000.0 track_infos.append(ti) - album_artist_ids = _artist_ids(release["artist-credit"]) + album_artist_ids = _artist_ids(release["artist_credit"]) release_title = _key_with_preferred_alias(release, key="title") info = AlbumInfo( album=release_title, @@ -574,40 +591,40 @@ def album_info(self, release: JSONDict) -> AlbumInfo: info.artist_credit = va_name info.artists_credit = [va_name] 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"): + if release["release_group"].get("title"): info.release_group_title = _key_with_preferred_alias( - release["release-group"], key="title" + release["release_group"], key="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"]["first_release_date"] if not release_date: # Fall back if release-specific date is not available. release_date = release_group_date @@ -617,17 +634,17 @@ def album_info(self, release: JSONDict) -> AlbumInfo: _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") @@ -642,7 +659,7 @@ def album_info(self, release: JSONDict) -> AlbumInfo: 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() @@ -659,23 +676,27 @@ def album_info(self, release: JSONDict) -> AlbumInfo: # We might find links to external sources (Discogs, Bandcamp, ...) external_ids = self.config["external_ids"].get() - wanted_sources = { + wanted_sources: set[UrlSource] = { 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): - if f"{source}.com" in (target := url["url"]["resource"]): - urls[source] = target + for url_source, url_relation in product(wanted_sources, url_rels): + if f"{url_source}.com" in ( + target := url_relation["url"]["resource"] + ): + urls[url_source] = target self._log.debug( "Found link to {} release via MusicBrainz", - source.capitalize(), + url_source.capitalize(), ) - for source, url in urls.items(): + for url_source, url in urls.items(): setattr( - info, f"{source}_album_id", extract_release_id(source, url) + info, + f"{url_source}_album_id", + extract_release_id(url_source, url), ) extra_albumdatas = plugins.send("mb_album_extract", data=release) @@ -774,7 +795,7 @@ def album_for_id(self, album_id: str) -> AlbumInfo | None: 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 ( diff --git a/beetsplug/parentwork.py b/beetsplug/parentwork.py index 15fcdefa8e..c2bf8cf497 100644 --- a/beetsplug/parentwork.py +++ b/beetsplug/parentwork.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING import requests @@ -27,6 +27,9 @@ from ._utils.musicbrainz import MusicBrainzAPIMixin +if TYPE_CHECKING: + from beetsplug._utils.musicbrainz import Work + class ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin): def __init__(self): @@ -90,11 +93,11 @@ def get_info(self, item, work_info): parentwork_info = {} composer_exists = False - for artist in work_info.get("artist-relations", []): + for artist in work_info.get("artist_relations", []): if artist["type"] == "composer": composer_exists = True parent_composer.append(artist["artist"]["name"]) - parent_composer_sort.append(artist["artist"]["sort-name"]) + parent_composer_sort.append(artist["artist"]["sort_name"]) if "end" in artist.keys(): parentwork_info["parentwork_date"] = artist["end"] @@ -191,9 +194,7 @@ def find_work(self, item, force, verbose): ], ) - def find_parentwork_info( - self, mb_workid: str - ) -> tuple[dict[str, Any], str | None]: + def find_parentwork_info(self, mb_workid: str) -> tuple[Work, str | None]: """Get the MusicBrainz information dict about a parent work, including the artist relations, and the composition date for a work's parent work. """ @@ -209,7 +210,7 @@ def find_parentwork_info( work_date = work_date or next( ( end - for a in work_info.get("artist-relations", []) + for a in work_info.get("artist_relations", []) if a["type"] == "composer" and (end := a.get("end")) ), None, @@ -217,7 +218,7 @@ def find_parentwork_info( parent_id = next( ( w["work"]["id"] - for w in work_info.get("work-relations", []) + for w in work_info.get("work_relations", []) if w["type"] == "parts" and w["direction"] == "backward" ), None, diff --git a/docs/changelog.rst b/docs/changelog.rst index c45963249a..5da02797c9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -32,9 +32,16 @@ Bug fixes - :doc:`plugins/listenbrainz`: Retry listenbrainz requests for temporary failures. -.. - For plugin developers - ~~~~~~~~~~~~~~~~~~~~~ +For plugin developers +~~~~~~~~~~~~~~~~~~~~~ + +- Consumers of :py:class:`beetsplug._utils.musicbrainz.MusicBrainzAPI` now + receive normalized MusicBrainz payloads with underscore-separated field names + (for example ``artist_credit`` and ``release_group``) and grouped relation + lists such as ``work_relations``, ``release_relations``, and + ``url_relations``. The API responses are also now fully typed with concrete + ``TypedDict`` models for releases, recordings, works, and relations. Update + direct access to raw MusicBrainz response keys if needed. .. Other changes diff --git a/setup.cfg b/setup.cfg index 7c880e41f2..3615c6aee3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,3 +50,12 @@ check_untyped_defs = true [[mypy-beets.metadata_plugins]] disallow_untyped_decorators = true check_untyped_defs = true + +[[mypy-beetsplug.musicbrainz]] +strict = true + +[[mypy-beetsplug.mbpseudo]] +strict = true + +[[mypy-beetsplug._utils]] +strict = true diff --git a/test/plugins/test_mbcollection.py b/test/plugins/test_mbcollection.py index adfadc1039..b61741d608 100644 --- a/test/plugins/test_mbcollection.py +++ b/test/plugins/test_mbcollection.py @@ -41,15 +41,15 @@ def helper(self): ), ), ( - [{"id": "c1", "entity-type": "event"}], + [{"id": "c1", "entity_type": "event"}], pytest.raises(UserError, match=r"No release collection found."), ), ( - [{"id": "c1", "entity-type": "release"}], + [{"id": "c1", "entity_type": "release"}], pytest.raises(UserError, match=r"invalid collection ID"), ), ( - [{"id": COLLECTION_ID, "entity-type": "release"}], + [{"id": COLLECTION_ID, "entity_type": "release"}], does_not_raise(), ), ], @@ -93,8 +93,8 @@ def test_mbupdate(self, helper, requests_mock, monkeypatch): "collections": [ { "id": self.COLLECTION_ID, - "entity-type": "release", - "release-count": 3, + "entity_type": "release", + "release_count": 3, } ] }, diff --git a/test/plugins/test_mbpseudo.py b/test/plugins/test_mbpseudo.py index f49f522152..7f83f0bc42 100644 --- a/test/plugins/test_mbpseudo.py +++ b/test/plugins/test_mbpseudo.py @@ -20,6 +20,7 @@ import pathlib from beetsplug._typing import JSONDict + from beetsplug._utils.musicbrainz import Release @pytest.fixture(scope="module") @@ -28,13 +29,13 @@ def rsrc_dir(pytestconfig: pytest.Config): @pytest.fixture -def official_release(rsrc_dir: pathlib.Path) -> JSONDict: +def official_release(rsrc_dir: pathlib.Path) -> Release: info_json = (rsrc_dir / "official_release.json").read_text(encoding="utf-8") return json.loads(info_json) @pytest.fixture -def pseudo_release(rsrc_dir: pathlib.Path) -> JSONDict: +def pseudo_release(rsrc_dir: pathlib.Path) -> Release: info_json = (rsrc_dir / "pseudo_release.json").read_text(encoding="utf-8") return json.loads(info_json) @@ -141,7 +142,7 @@ def test_extract_id_uses_music_brainz_pattern( def test_album_info_for_pseudo_release( self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - pseudo_release: JSONDict, + pseudo_release: Release, ): album_info = mbpseudo_plugin.album_info(pseudo_release) assert not isinstance(album_info, PseudoAlbumInfo) @@ -159,10 +160,10 @@ def test_album_info_for_pseudo_release( def test_interception_skip_when_rel_values_dont_match( self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - official_release: JSONDict, + official_release: Release, json_key: str, ): - del official_release["release-relations"][0][json_key] + del official_release["release_relations"][0][json_key] # type: ignore[misc] album_info = mbpseudo_plugin.album_info(official_release) assert not isinstance(album_info, PseudoAlbumInfo) @@ -171,10 +172,10 @@ def test_interception_skip_when_rel_values_dont_match( def test_interception_skip_when_script_doesnt_match( self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - official_release: JSONDict, + official_release: Release, ): - 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) @@ -184,7 +185,7 @@ def test_interception_skip_when_script_doesnt_match( def test_interception( self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - official_release: JSONDict, + official_release: Release, ): album_info = mbpseudo_plugin.album_info(official_release) assert isinstance(album_info, PseudoAlbumInfo) @@ -245,7 +246,7 @@ def test_custom_tags( self, config, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - official_release: JSONDict, + official_release: Release, ): config["import"]["languages"] = ["en", "jp"] album_info = mbpseudo_plugin.album_info(official_release) @@ -260,7 +261,7 @@ def test_custom_tags_with_import_languages( self, config, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin, - official_release: JSONDict, + official_release: Release, ): config["import"]["languages"] = [] album_info = mbpseudo_plugin.album_info(official_release) diff --git a/test/plugins/test_musicbrainz.py b/test/plugins/test_musicbrainz.py index 8c014b9779..935a05c591 100644 --- a/test/plugins/test_musicbrainz.py +++ b/test/plugins/test_musicbrainz.py @@ -32,7 +32,7 @@ def make_alias(suffix: str, locale: str, primary: bool = False): alias: dict[str, Any] = { "name": f"ALIAS{suffix}", "locale": locale, - "sort-name": f"ALIASSORT{suffix}", + "sort_name": f"ALIASSORT{suffix}", } if primary: alias["primary"] = True @@ -62,19 +62,19 @@ def _make_release( "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", "title": "RELEASE GROUP TITLE", }, - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "ARTIST NAME", "id": "ARTIST ID", - "sort-name": "ARTIST SORT NAME", + "sort_name": "ARTIST SORT NAME", }, "name": "ARTIST CREDIT", } @@ -83,30 +83,30 @@ def _make_release( "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", } @@ -129,25 +129,25 @@ def _make_release( 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", } @@ -169,7 +169,7 @@ def _make_release( { "position": "1", "tracks": track_list, - "data-tracks": data_track_list, + "data_tracks": data_track_list, "format": medium_format, "title": "MEDIUM TITLE", } @@ -194,24 +194,24 @@ def _make_track( 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", } @@ -397,7 +397,7 @@ def test_various_artists_defaults_false(self): 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) @@ -415,7 +415,7 @@ def test_parse_releasegroupid(self): def test_parse_release_group_title(self): release = self._make_release(None) - release["release-group"]["aliases"] = [ + release["release_group"]["aliases"] = [ make_alias(suffix="en", locale="en", primary=True), ] @@ -488,7 +488,7 @@ def test_parse_disctitle(self): 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 @@ -571,7 +571,7 @@ def test_track_artist_overrides_recording_artist_multi(self): def test_parse_recording_artist_credits(self): tracks = [self._make_track("a", "b", 1)] - tracks[0]["artist-relations"] = [ + tracks[0]["artist_relations"] = [ { "type": "remixer", "artist": { @@ -594,13 +594,13 @@ def test_parse_recording_artist_credits(self): }, }, ] - tracks[0]["work-relations"] = [ + tracks[0]["work_relations"] = [ { "type": "performance", "work": { "id": "WORK ID", "title": "WORK TITLE", - "artist-relations": [ + "artist_relations": [ { "type": "lyricist", "artist": { @@ -620,7 +620,7 @@ def test_parse_recording_artist_credits(self): "artist": { "name": "RECORDING COMPOSER ARTIST NAME", "id": "RECORDING COMPOSER ARTIST ID", - "sort-name": ( + "sort_name": ( "RECORDING COMPOSER ARTIST SORT NAME" ), }, @@ -630,7 +630,7 @@ def test_parse_recording_artist_credits(self): "artist": { "name": "RECORDING COMPOSER 2 ARTIST NAME", "id": "RECORDING COMPOSER 2 ARTIST ID", - "sort-name": ( + "sort_name": ( "RECORDING COMPOSER 2 ARTIST SORT NAME" ), }, @@ -856,7 +856,7 @@ def test_missing_tracks(self): release = self._make_release(tracks=tracks) release["media"].append(release["media"][0]) del release["media"][0]["tracks"] - del release["media"][0]["data-tracks"] + del release["media"][0]["data_tracks"] d = self.mb.album_info(release) assert d.mediums == 2 @@ -866,7 +866,7 @@ def _credit_dict(self, suffix=""): return { "artist": { "name": f"NAME{suffix}", - "sort-name": f"SORT{suffix}", + "sort_name": f"SORT{suffix}", }, "name": f"CREDIT{suffix}", } @@ -966,7 +966,7 @@ def test_follow_pseudo_releases(self): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "some-artist", @@ -974,10 +974,11 @@ def test_follow_pseudo_releases(self): }, } ], - "release-group": { + "release_group": { "id": "another-id", + "first_release_date": "2009", }, - "release-relations": [ + "release_relations": [ { "type": "transl-tracklisting", "direction": "backward", @@ -1008,7 +1009,7 @@ def test_follow_pseudo_releases(self): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "some-artist", @@ -1016,8 +1017,9 @@ def test_follow_pseudo_releases(self): }, } ], - "release-group": { + "release_group": { "id": "another-id", + "first_release_date": "2009", }, "country": "COUNTRY", }, @@ -1053,7 +1055,7 @@ def test_pseudo_releases_with_empty_links(self): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "some-artist", @@ -1061,8 +1063,9 @@ def test_pseudo_releases_with_empty_links(self): }, } ], - "release-group": { + "release_group": { "id": "another-id", + "first_release_date": "2009", }, } ] @@ -1097,7 +1100,7 @@ def test_pseudo_releases_without_links(self): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "some-artist", @@ -1105,8 +1108,9 @@ def test_pseudo_releases_without_links(self): }, } ], - "release-group": { + "release_group": { "id": "another-id", + "first_release_date": "2009", }, } ] @@ -1141,7 +1145,7 @@ def test_pseudo_releases_with_unsupported_links(self): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "name": "some-artist", @@ -1149,10 +1153,11 @@ def test_pseudo_releases_with_unsupported_links(self): }, } ], - "release-group": { + "release_group": { "id": "another-id", + "first_release_date": "2009", }, - "release-relations": [ + "release_relations": [ { "type": "remaster", "direction": "backward", @@ -1257,10 +1262,13 @@ def test_candidates(self, monkeypatch, mb): "position": 5, } ], - "artist-credit": [ + "artist_credit": [ {"artist": {"name": "some-artist", "id": "some-id"}} ], - "release-group": {"id": "another-id"}, + "release_group": { + "id": "another-id", + "first_release_date": "2009", + }, }, ) candidates = list(mb.candidates([], "hello", "there", False)) diff --git a/test/plugins/utils/test_musicbrainz.py b/test/plugins/utils/test_musicbrainz.py index c7363f5167..fc7a009c36 100644 --- a/test/plugins/utils/test_musicbrainz.py +++ b/test/plugins/utils/test_musicbrainz.py @@ -3,26 +3,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/" }, @@ -31,10 +49,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/" }, @@ -47,30 +67,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", + } + ], + "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": [ + "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/" }, diff --git a/test/rsrc/mbpseudo/official_release.json b/test/rsrc/mbpseudo/official_release.json index cd6bb3ba98..2778c9fba4 100644 --- a/test/rsrc/mbpseudo/official_release.json +++ b/test/rsrc/mbpseudo/official_release.json @@ -7,12 +7,12 @@ "locale": "en", "name": "In Bloom", "primary": true, - "sort-name": "In Bloom", + "sort_name": "In Bloom", "type": "Release name", - "type-id": "df187855-059b-3514-9d5e-d240de0b4228" + "type_id": "df187855-059b-3514-9d5e-d240de0b4228" } ], - "artist-credit": [ + "artist_credit": [ { "artist": { "aliases": [ @@ -23,9 +23,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", @@ -46,7 +46,7 @@ ], "id": "55e42264-ef27-49d8-93fd-29f930dc96e4", "name": "幾田りら", - "sort-name": "Ikuta, Lilas", + "sort_name": "Ikuta, Lilas", "tags": [ { "count": 1, @@ -58,34 +58,34 @@ } ], "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": "", "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": "copyright", - "type-id": "730b5251-7432-4896-8fc6-e1cba943bfe1" + "type_id": "730b5251-7432-4896-8fc6-e1cba943bfe1" }, { "artist": { @@ -93,27 +93,27 @@ "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": "01d3488d-8d2a-4cff-9226-5250404db4dc" + "type_id": "01d3488d-8d2a-4cff-9226-5250404db4dc" } ], "asin": "B0DR8Y2YDC", "barcode": "199066336168", "country": "XW", - "cover-art-archive": { + "cover_art_archive": { "artwork": true, "back": false, "count": 1, @@ -124,9 +124,9 @@ "disambiguation": "", "genres": [], "id": "a5ce1d11-2e32-45a4-b37f-c1589d46b103", - "label-info": [ + "label_info": [ { - "catalog-number": "Lilas-020", + "catalog_number": "Lilas-020", "label": { "aliases": [ { @@ -136,9 +136,9 @@ "locale": null, "name": "2636621 Records DK", "primary": null, - "sort-name": "2636621 Records DK", + "sort_name": "2636621 Records DK", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -147,9 +147,9 @@ "locale": null, "name": "Antipole", "primary": null, - "sort-name": "Antipole", + "sort_name": "Antipole", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -158,9 +158,9 @@ "locale": null, "name": "Auto production", "primary": null, - "sort-name": "Auto production", + "sort_name": "Auto production", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -169,9 +169,9 @@ "locale": null, "name": "Auto-Edición", "primary": null, - "sort-name": "Auto-Edición", + "sort_name": "Auto-Edición", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -180,9 +180,9 @@ "locale": null, "name": "Auto-Product", "primary": null, - "sort-name": "Auto-Product", + "sort_name": "Auto-Product", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -191,9 +191,9 @@ "locale": null, "name": "Autoedición", "primary": null, - "sort-name": "Autoedición", + "sort_name": "Autoedición", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -202,9 +202,9 @@ "locale": null, "name": "Autoeditado", "primary": null, - "sort-name": "Autoeditado", + "sort_name": "Autoeditado", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -213,9 +213,9 @@ "locale": null, "name": "Autoproduit", "primary": null, - "sort-name": "Autoproduit", + "sort_name": "Autoproduit", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -224,9 +224,9 @@ "locale": null, "name": "Banana Skin Records", "primary": null, - "sort-name": "Banana Skin Records", + "sort_name": "Banana Skin Records", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -235,9 +235,9 @@ "locale": null, "name": "Cannelle", "primary": null, - "sort-name": "Cannelle", + "sort_name": "Cannelle", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -246,9 +246,9 @@ "locale": null, "name": "Cece Natalie", "primary": null, - "sort-name": "Cece Natalie", + "sort_name": "Cece Natalie", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -257,9 +257,9 @@ "locale": null, "name": "Cherry X", "primary": null, - "sort-name": "Cherry X", + "sort_name": "Cherry X", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -268,9 +268,9 @@ "locale": null, "name": "Chung", "primary": null, - "sort-name": "Chung", + "sort_name": "Chung", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -279,9 +279,9 @@ "locale": null, "name": "Cody Johnson", "primary": null, - "sort-name": "Cody Johnson", + "sort_name": "Cody Johnson", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -290,9 +290,9 @@ "locale": null, "name": "Cowgirl Clue", "primary": null, - "sort-name": "Cowgirl Clue", + "sort_name": "Cowgirl Clue", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -301,9 +301,9 @@ "locale": null, "name": "D.I.Y.", "primary": null, - "sort-name": "D.I.Y.", + "sort_name": "D.I.Y.", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -312,9 +312,9 @@ "locale": null, "name": "Damjan Mravunac Self-released)", "primary": null, - "sort-name": "Damjan Mravunac Self-released)", + "sort_name": "Damjan Mravunac Self-released)", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -323,9 +323,9 @@ "locale": null, "name": "Demo", "primary": null, - "sort-name": "Demo", + "sort_name": "Demo", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -334,9 +334,9 @@ "locale": null, "name": "DistroKid", "primary": null, - "sort-name": "DistroKid", + "sort_name": "DistroKid", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -345,9 +345,9 @@ "locale": null, "name": "Egzod", "primary": null, - "sort-name": "Egzod", + "sort_name": "Egzod", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -356,9 +356,9 @@ "locale": null, "name": "Eigenverlag", "primary": null, - "sort-name": "Eigenverlag", + "sort_name": "Eigenverlag", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -367,9 +367,9 @@ "locale": null, "name": "Eigenvertrieb", "primary": null, - "sort-name": "Eigenvertrieb", + "sort_name": "Eigenvertrieb", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -378,9 +378,9 @@ "locale": null, "name": "GRIND MODE", "primary": null, - "sort-name": "GRIND MODE", + "sort_name": "GRIND MODE", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -389,9 +389,9 @@ "locale": null, "name": "INDIPENDANT", "primary": null, - "sort-name": "INDIPENDANT", + "sort_name": "INDIPENDANT", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -400,9 +400,9 @@ "locale": null, "name": "Indepandant", "primary": null, - "sort-name": "Indepandant", + "sort_name": "Indepandant", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -411,9 +411,9 @@ "locale": null, "name": "Independant release", "primary": null, - "sort-name": "Independant release", + "sort_name": "Independant release", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -422,9 +422,9 @@ "locale": null, "name": "Independent", "primary": null, - "sort-name": "Independent", + "sort_name": "Independent", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -433,9 +433,9 @@ "locale": null, "name": "Independente", "primary": null, - "sort-name": "Independente", + "sort_name": "Independente", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -444,9 +444,9 @@ "locale": null, "name": "Independiente", "primary": null, - "sort-name": "Independiente", + "sort_name": "Independiente", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -455,9 +455,9 @@ "locale": null, "name": "Indie", "primary": null, - "sort-name": "Indie", + "sort_name": "Indie", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -466,9 +466,9 @@ "locale": null, "name": "Joost Klein", "primary": null, - "sort-name": "Joost Klein", + "sort_name": "Joost Klein", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -477,9 +477,9 @@ "locale": null, "name": "Millington Records", "primary": null, - "sort-name": "Millington Records", + "sort_name": "Millington Records", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -488,9 +488,9 @@ "locale": null, "name": "MoroseSound", "primary": null, - "sort-name": "MoroseSound", + "sort_name": "MoroseSound", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -499,9 +499,9 @@ "locale": null, "name": "N/A", "primary": null, - "sort-name": "N/A", + "sort_name": "N/A", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -510,9 +510,9 @@ "locale": null, "name": "No Label", "primary": null, - "sort-name": "No Label", + "sort_name": "No Label", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -521,9 +521,9 @@ "locale": null, "name": "None", "primary": null, - "sort-name": "None", + "sort_name": "None", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -532,9 +532,9 @@ "locale": null, "name": "None Like Joshua", "primary": null, - "sort-name": "None Like Joshua", + "sort_name": "None Like Joshua", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -543,9 +543,9 @@ "locale": null, "name": "Not On A Lebel", "primary": null, - "sort-name": "Not On A Lebel", + "sort_name": "Not On A Lebel", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -554,9 +554,9 @@ "locale": null, "name": "Not On Label", "primary": null, - "sort-name": "Not On Label", + "sort_name": "Not On Label", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -565,9 +565,9 @@ "locale": null, "name": "Offensively Average Productions", "primary": null, - "sort-name": "Offensively Average Productions", + "sort_name": "Offensively Average Productions", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -576,9 +576,9 @@ "locale": null, "name": "Ours", "primary": null, - "sort-name": "Ours", + "sort_name": "Ours", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -587,9 +587,9 @@ "locale": null, "name": "P2019", "primary": null, - "sort-name": "P2019", + "sort_name": "P2019", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -598,9 +598,9 @@ "locale": null, "name": "P2020", "primary": null, - "sort-name": "P2020", + "sort_name": "P2020", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -609,9 +609,9 @@ "locale": null, "name": "P2021", "primary": null, - "sort-name": "P2021", + "sort_name": "P2021", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -620,9 +620,9 @@ "locale": null, "name": "P2022", "primary": null, - "sort-name": "P2022", + "sort_name": "P2022", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -631,9 +631,9 @@ "locale": null, "name": "P2023", "primary": null, - "sort-name": "P2023", + "sort_name": "P2023", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -642,9 +642,9 @@ "locale": null, "name": "P2024", "primary": null, - "sort-name": "P2024", + "sort_name": "P2024", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -653,9 +653,9 @@ "locale": null, "name": "P2025", "primary": null, - "sort-name": "P2025", + "sort_name": "P2025", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -664,9 +664,9 @@ "locale": null, "name": "Patriarchy", "primary": null, - "sort-name": "Patriarchy", + "sort_name": "Patriarchy", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -675,9 +675,9 @@ "locale": null, "name": "Plini", "primary": null, - "sort-name": "Plini", + "sort_name": "Plini", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -686,9 +686,9 @@ "locale": null, "name": "Records DK", "primary": null, - "sort-name": "Records DK", + "sort_name": "Records DK", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -697,9 +697,9 @@ "locale": null, "name": "Self Digital", "primary": null, - "sort-name": "Self Digital", + "sort_name": "Self Digital", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -708,9 +708,9 @@ "locale": null, "name": "Self Release", "primary": null, - "sort-name": "Self Release", + "sort_name": "Self Release", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -719,9 +719,9 @@ "locale": null, "name": "Self Released", "primary": null, - "sort-name": "Self Released", + "sort_name": "Self Released", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -730,9 +730,9 @@ "locale": null, "name": "Self-release", "primary": null, - "sort-name": "Self-release", + "sort_name": "Self-release", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -741,9 +741,9 @@ "locale": null, "name": "Self-released", "primary": null, - "sort-name": "Self-released", + "sort_name": "Self-released", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -752,9 +752,9 @@ "locale": null, "name": "Self-released/independent", "primary": null, - "sort-name": "Self-released/independent", + "sort_name": "Self-released/independent", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -763,9 +763,9 @@ "locale": null, "name": "Sevdaliza", "primary": null, - "sort-name": "Sevdaliza", + "sort_name": "Sevdaliza", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -774,9 +774,9 @@ "locale": null, "name": "TOMMY CASH", "primary": null, - "sort-name": "TOMMY CASH", + "sort_name": "TOMMY CASH", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -785,9 +785,9 @@ "locale": null, "name": "Take Van", "primary": null, - "sort-name": "Take Van", + "sort_name": "Take Van", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -796,9 +796,9 @@ "locale": null, "name": "Talwiinder", "primary": null, - "sort-name": "Talwiinder", + "sort_name": "Talwiinder", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -807,9 +807,9 @@ "locale": null, "name": "Unsigned", "primary": null, - "sort-name": "Unsigned", + "sort_name": "Unsigned", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -818,9 +818,9 @@ "locale": null, "name": "VGR", "primary": null, - "sort-name": "VGR", + "sort_name": "VGR", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -829,9 +829,9 @@ "locale": null, "name": "Woo Da Savage", "primary": null, - "sort-name": "Woo Da Savage", + "sort_name": "Woo Da Savage", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -840,9 +840,9 @@ "locale": null, "name": "YANAA", "primary": null, - "sort-name": "YANAA", + "sort_name": "YANAA", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -851,9 +851,9 @@ "locale": "fi", "name": "[ei levymerkkiä]", "primary": true, - "sort-name": "ei levymerkkiä", + "sort_name": "ei levymerkkiä", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -862,9 +862,9 @@ "locale": "nl", "name": "[geen platenmaatschappij]", "primary": true, - "sort-name": "[geen platenmaatschappij]", + "sort_name": "[geen platenmaatschappij]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -873,9 +873,9 @@ "locale": "et", "name": "[ilma plaadifirmata]", "primary": false, - "sort-name": "[ilma plaadifirmata]", + "sort_name": "[ilma plaadifirmata]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -884,9 +884,9 @@ "locale": "es", "name": "[nada]", "primary": true, - "sort-name": "[nada]", + "sort_name": "[nada]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -895,9 +895,9 @@ "locale": "en", "name": "[no label]", "primary": true, - "sort-name": "[no label]", + "sort_name": "[no label]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -906,9 +906,9 @@ "locale": null, "name": "[nolabel]", "primary": null, - "sort-name": "[nolabel]", + "sort_name": "[nolabel]", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -917,9 +917,9 @@ "locale": null, "name": "[none]", "primary": null, - "sort-name": "[none]", + "sort_name": "[none]", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -928,9 +928,9 @@ "locale": "lt", "name": "[nėra leidybinės kompanijos]", "primary": false, - "sort-name": "[nėra leidybinės kompanijos]", + "sort_name": "[nėra leidybinės kompanijos]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -939,9 +939,9 @@ "locale": "lt", "name": "[nėra leidyklos]", "primary": false, - "sort-name": "[nėra leidyklos]", + "sort_name": "[nėra leidyklos]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -950,9 +950,9 @@ "locale": "lt", "name": "[nėra įrašų kompanijos]", "primary": true, - "sort-name": "[nėra įrašų kompanijos]", + "sort_name": "[nėra įrašų kompanijos]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -961,9 +961,9 @@ "locale": "et", "name": "[puudub]", "primary": false, - "sort-name": "[puudub]", + "sort_name": "[puudub]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -972,9 +972,9 @@ "locale": "ru", "name": "[самиздат]", "primary": false, - "sort-name": "samizdat", + "sort_name": "samizdat", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -983,9 +983,9 @@ "locale": "ja", "name": "[レーベルなし]", "primary": true, - "sort-name": "[レーベルなし]", + "sort_name": "[レーベルなし]", "type": "Label name", - "type-id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" + "type_id": "3a1a0c48-d885-3b89-87b2-9e8a483c5675" }, { "begin": null, @@ -994,9 +994,9 @@ "locale": null, "name": "annapantsu music", "primary": null, - "sort-name": "annapantsu music", + "sort_name": "annapantsu music", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1005,9 +1005,9 @@ "locale": null, "name": "auto-release", "primary": null, - "sort-name": "auto-release", + "sort_name": "auto-release", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1016,9 +1016,9 @@ "locale": null, "name": "autoprod.", "primary": null, - "sort-name": "autoprod.", + "sort_name": "autoprod.", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1027,9 +1027,9 @@ "locale": null, "name": "ayesha erotica", "primary": null, - "sort-name": "ayesha erotica", + "sort_name": "ayesha erotica", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1038,9 +1038,9 @@ "locale": null, "name": "blank", "primary": null, - "sort-name": "blank", + "sort_name": "blank", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1049,9 +1049,9 @@ "locale": null, "name": "cupcakKe", "primary": null, - "sort-name": "cupcakKe", + "sort_name": "cupcakKe", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1060,9 +1060,9 @@ "locale": null, "name": "d.silvestre", "primary": null, - "sort-name": "d.silvestre", + "sort_name": "d.silvestre", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1071,9 +1071,9 @@ "locale": null, "name": "dj-Jo", "primary": null, - "sort-name": "dj-Jo", + "sort_name": "dj-Jo", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1082,9 +1082,9 @@ "locale": null, "name": "independent release", "primary": null, - "sort-name": "independent release", + "sort_name": "independent release", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1093,9 +1093,9 @@ "locale": null, "name": "lor2mg", "primary": null, - "sort-name": "lor2mg", + "sort_name": "lor2mg", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1104,9 +1104,9 @@ "locale": null, "name": "nyamura", "primary": null, - "sort-name": "nyamura", + "sort_name": "nyamura", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1115,9 +1115,9 @@ "locale": null, "name": "pls dnt stp", "primary": null, - "sort-name": "pls dnt stp", + "sort_name": "pls dnt stp", "type": null, - "type-id": null + "type_id": null }, { "begin": null, @@ -1126,9 +1126,9 @@ "locale": null, "name": "self", "primary": null, - "sort-name": "self", + "sort_name": "self", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1137,9 +1137,9 @@ "locale": null, "name": "self issued", "primary": null, - "sort-name": "self issued", + "sort_name": "self issued", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1148,9 +1148,9 @@ "locale": null, "name": "self-issued", "primary": null, - "sort-name": "self-issued", + "sort_name": "self-issued", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1159,9 +1159,9 @@ "locale": null, "name": "white label", "primary": null, - "sort-name": "white label", + "sort_name": "white label", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1170,9 +1170,9 @@ "locale": null, "name": "но лабел", "primary": null, - "sort-name": "но лабел", + "sort_name": "но лабел", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" }, { "begin": null, @@ -1181,17 +1181,17 @@ "locale": null, "name": "独立发行", "primary": null, - "sort-name": "独立发行", + "sort_name": "独立发行", "type": "Search hint", - "type-id": "829662f2-a781-3ec8-8b46-fbcea6196f81" + "type_id": "829662f2-a781-3ec8-8b46-fbcea6196f81" } ], "disambiguation": "Special purpose label – white labels, self-published releases and other “no label” releases", "genres": [], "id": "157afde4-4bf5-4039-8ad2-5a15acc85176", - "label-code": null, + "label_code": null, "name": "[no label]", - "sort-name": "[no label]", + "sort_name": "[no label]", "tags": [ { "count": 12, @@ -1203,22 +1203,22 @@ } ], "type": "Production", - "type-id": "a2426aab-2dd4-339c-b47d-b4923a241678" + "type_id": "a2426aab-2dd4-339c-b47d-b4923a241678" } } ], "media": [ { "format": "Digital Media", - "format-id": "907a28d9-b3b2-3ef6-89a8-7b18d91d4794", + "format_id": "907a28d9-b3b2-3ef6-89a8-7b18d91d4794", "id": "43f08d54-a896-3561-be75-b881cbc832d5", "position": 1, "title": "", - "track-count": 1, - "track-offset": 0, + "track_count": 1, + "track_offset": 0, "tracks": [ { - "artist-credit": [ + "artist_credit": [ { "artist": { "aliases": [ @@ -1229,18 +1229,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": "幾田りら" @@ -1252,43 +1252,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": { @@ -1296,21 +1296,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": { @@ -1318,21 +1318,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": { @@ -1340,25 +1340,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": [ @@ -1367,53 +1367,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" @@ -1421,42 +1421,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": { @@ -1464,21 +1464,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": [], @@ -1491,37 +1491,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/" @@ -1538,27 +1538,27 @@ } ], "packaging": "None", - "packaging-id": "119eba76-b343-3e02-a292-f0f00644bb9b", + "packaging_id": "119eba76-b343-3e02-a292-f0f00644bb9b", "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": { + "release_group": { "aliases": [], - "artist-credit": [ + "artist_credit": [ { "artist": { "aliases": [ @@ -1569,54 +1569,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": "forward", "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": "Lilas Ikuta" @@ -1627,248 +1627,248 @@ "id": "dc3ee2df-0bc1-49eb-b8c4-34473d279a43", "media": [], "packaging": null, - "packaging-id": null, + "packaging_id": null, "quality": "normal", - "release-group": null, + "release_group": null, "status": null, - "status-id": null, - "text-representation": { + "status_id": null, + "text_representation": { "language": "eng", "script": "Latn" }, "title": "In Bloom" }, - "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": "Official", - "status-id": "4e304316-386d-3409-af2e-78857eec5cfe", + "status_id": "4e304316-386d-3409-af2e-78857eec5cfe", "tags": [], - "text-representation": { + "text_representation": { "language": "jpn", "script": "Jpan" }, "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": "amazon asin", - "type-id": "4f2e710d-166c-480c-a293-2e2c8d658d87", + "type_id": "4f2e710d-166c-480c-a293-2e2c8d658d87", "url": { "id": "b50c7fb8-2327-4a05-b989-f2211a41afee", "resource": "https://www.amazon.co.jp/gp/product/B0DR8Y2YDC" } }, { - "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": "08445ccf-7b99-4438-9f9a-fb9ac18099ee", + "type_id": "08445ccf-7b99-4438-9f9a-fb9ac18099ee", "url": { "id": "5106a7b0-1443-4803-91a2-28cac2cfb5e0", "resource": "https://open.spotify.com/album/3LDV2xGL9HiqCsQujEPQLb" } }, { - "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": "08445ccf-7b99-4438-9f9a-fb9ac18099ee", + "type_id": "08445ccf-7b99-4438-9f9a-fb9ac18099ee", "url": { "id": "d481d94b-a7bf-4e82-8da0-1757fedcda62", "resource": "https://www.deezer.com/album/687686261" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "6156d2e4-d107-43f9-8f44-52f04d39c78e", "resource": "https://mora.jp/package/43000011/199066336168/" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "a4eabb88-1746-4aa2-ab09-c28cfbe65efb", "resource": "https://mora.jp/package/43000011/199066336168_HD/" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "ab8440f0-3b13-4436-b3ad-f4695c9d8875", "resource": "https://mora.jp/package/43000011/199066336168_LL/" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "9a8ee8d1-f946-44a1-be16-8f7a77c951e9", "resource": "https://music.apple.com/jp/album/1786972161" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "c6faaa80-38fb-46a4-aa2b-78cddc5cbe70", "resource": "https://ototoy.jp/_/default/p/2501951" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "0e7e8bc5-0779-492d-a9db-9ab58f96d23b", "resource": "https://www.qobuz.com/jp-ja/album/lilas-ikuta-/fl9tx2j78reza" } }, { - "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": "98e08c20-8402-4163-8970-53504bb6a1e4", + "type_id": "98e08c20-8402-4163-8970-53504bb6a1e4", "url": { "id": "c0cf8fe0-3413-4544-a026-37d346a59a77", "resource": "https://www.qobuz.com/jp-ja/album/lilas-ikuta-/l1dnc4xoi6l7a" } }, { - "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": "320adf26-96fa-4183-9045-1f5f32f833cb", + "type_id": "320adf26-96fa-4183-9045-1f5f32f833cb", "url": { "id": "e4ce55a9-a5e1-4842-b42d-11be6a31fdab", "resource": "https://music.amazon.co.jp/albums/B0DR8Y2YDC" } }, { - "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": "320adf26-96fa-4183-9045-1f5f32f833cb", + "type_id": "320adf26-96fa-4183-9045-1f5f32f833cb", "url": { "id": "9a8ee8d1-f946-44a1-be16-8f7a77c951e9", "resource": "https://music.apple.com/jp/album/1786972161" } }, { - "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": "vgmdb", - "type-id": "6af0134a-df6a-425a-96e2-895f9cd342ba", + "type_id": "6af0134a-df6a-425a-96e2-895f9cd342ba", "url": { "id": "1885772a-4004-4d45-9512-d0c8822506c9", "resource": "https://vgmdb.net/album/145936" diff --git a/test/rsrc/mbpseudo/pseudo_release.json b/test/rsrc/mbpseudo/pseudo_release.json index ae4bf7b6b8..1b5d9857c8 100644 --- a/test/rsrc/mbpseudo/pseudo_release.json +++ b/test/rsrc/mbpseudo/pseudo_release.json @@ -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" },