Add mbpseudo plugin for pseudo-release proposals (#5888)

## Description
Adds the new `mbpseudo` plugin, that proactively searches for pseudo-releases during import and
adds them as candidates. Since it also depends on MusicBrainz, there are
some special considerations for the default logic (which is now a plugin
as well). However, at the very least it expects a list of desired [names
of scripts](https://en.wikipedia.org/wiki/ISO_15924) in the
configuration, for example:

```yaml
mbpseudo:
    scripts:
    - Latn
```

It will use that to search for pseudo-releases that match some of the
desired scripts, but will only do so if the input tracks match against
an official release that is not in one of the desired scripts.

## Standalone Usage

This would be the recommended approach, which involves disabling the
`musicbrainz` plugin. The `mbpseudo` plugin will manually delegate the
initial search to it. Since the data source of official releases will
still match MusicBrainz, weights are still relevant:

```yaml
mbpseudo:
    source_weight: 0.0
    scripts:
    - Latn

musicbrainz:
    source_weight: 0.1
```

A setup like that would ensure that the pseudo-releases have slightly
more preference when choosing the final proposal.

## Combined Usage

I initially thought it would be important to coexist with the
`musicbrainz` plugin when it's enabled, and reuse as much of its data as
possible to avoid redundant calls to the MusicBrainz API. I have the
impression this is not really important in the end, and maybe things
could be simplified if we decide that both plugins shouldn't coexist.

As it is right now, using both plugins at the same time would still
work, but it'll only avoid redundancy if `musicbrainz` emits its
candidates before `mbpseudo`, ~which is why I modified the
plugin-loading logic slightly to guarantee ordering. I'm not sure if you
think this could be an issue, but I think the `musicbrainz` plugin is
also used by other plugins and I can imagine it's good to guarantee the
order that is declared in the configuration?~

If the above is fulfilled, the `mbpseudo` plugin will use listeners to
intercept data emitted by the `musicbrainz` plugin and check if any of
them have pseudo-releases that might be desirable.
This commit is contained in:
Sebastian Mohr 2025-11-03 13:10:10 +01:00 committed by GitHub
commit beda6fc71b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1983 additions and 8 deletions

3
.github/CODEOWNERS vendored
View file

@ -2,4 +2,5 @@
* @beetbox/maintainers
# Specific ownerships:
/beets/metadata_plugins.py @semohr
/beets/metadata_plugins.py @semohr
/beetsplug/mbpseudo.py @asardaes

View file

@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
import lap
import numpy as np
from beets import config, logging, metadata_plugins
from beets import config, logging, metadata_plugins, plugins
from beets.autotag import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch, hooks
from beets.util import get_most_common_tags
@ -274,12 +274,17 @@ def tag_album(
log.debug("Searching for album ID: {}", search_id)
if info := metadata_plugins.album_for_id(search_id):
_add_candidate(items, candidates, info)
if opt_candidate := candidates.get(info.album_id):
plugins.send("album_matched", match=opt_candidate)
# Use existing metadata or text search.
else:
# Try search based on current ID.
if info := match_by_id(items):
_add_candidate(items, candidates, info)
for candidate in candidates.values():
plugins.send("album_matched", match=candidate)
rec = _recommendation(list(candidates.values()))
log.debug("Album ID match recommendation is {}", rec)
if candidates and not config["import"]["timid"]:
@ -313,6 +318,8 @@ def tag_album(
items, search_artist, search_album, va_likely
):
_add_candidate(items, candidates, matched_candidate)
if opt_candidate := candidates.get(matched_candidate.album_id):
plugins.send("album_matched", match=opt_candidate)
log.debug("Evaluating {} candidates.", len(candidates))
# Sort and get the recommendation.

View file

@ -72,6 +72,7 @@ EventType = Literal[
"album_imported",
"album_removed",
"albuminfo_received",
"album_matched",
"before_choose_candidate",
"before_item_moved",
"cli_exit",

364
beetsplug/mbpseudo.py Normal file
View file

@ -0,0 +1,364 @@
# This file is part of beets.
# Copyright 2025, Alexis Sarda-Espinosa.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Adds pseudo-releases from MusicBrainz as candidates during import."""
from __future__ import annotations
import itertools
import traceback
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Iterable, Sequence
import mediafile
import musicbrainzngs
from typing_extensions import override
from beets import config
from beets.autotag.distance import Distance, distance
from beets.autotag.hooks import AlbumInfo
from beets.autotag.match import assign_items
from beets.plugins import find_plugins
from beets.util.id_extractors import extract_release_id
from beetsplug.musicbrainz import (
RELEASE_INCLUDES,
MusicBrainzAPIError,
MusicBrainzPlugin,
_merge_pseudo_and_actual_album,
_preferred_alias,
)
if TYPE_CHECKING:
from beets.autotag import AlbumMatch
from beets.library import Item
from beetsplug._typing import JSONDict
_STATUS_PSEUDO = "Pseudo-Release"
class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):
def __init__(self) -> None:
super().__init__()
self._release_getter = musicbrainzngs.get_release_by_id
self.config.add(
{
"scripts": [],
"custom_tags_only": False,
"album_custom_tags": {
"album_transl": "album",
"album_artist_transl": "artist",
},
"track_custom_tags": {
"title_transl": "title",
"artist_transl": "artist",
},
}
)
self._scripts = self.config["scripts"].as_str_seq()
self._log.debug("Desired scripts: {0}", self._scripts)
album_custom_tags = self.config["album_custom_tags"].get().keys()
track_custom_tags = self.config["track_custom_tags"].get().keys()
self._log.debug(
"Custom tags for albums and tracks: {0} + {1}",
album_custom_tags,
track_custom_tags,
)
for custom_tag in album_custom_tags | track_custom_tags:
if not isinstance(custom_tag, str):
continue
media_field = mediafile.MediaField(
mediafile.MP3DescStorageStyle(custom_tag),
mediafile.MP4StorageStyle(
f"----:com.apple.iTunes:{custom_tag}"
),
mediafile.StorageStyle(custom_tag),
mediafile.ASFStorageStyle(custom_tag),
)
try:
self.add_media_field(custom_tag, media_field)
except ValueError:
# ignore errors due to duplicates
pass
self.register_listener("pluginload", self._on_plugins_loaded)
self.register_listener("album_matched", self._adjust_final_album_match)
# noinspection PyMethodMayBeStatic
def _on_plugins_loaded(self):
for plugin in find_plugins():
if isinstance(plugin, MusicBrainzPlugin) and not isinstance(
plugin, MusicBrainzPseudoReleasePlugin
):
raise RuntimeError(
"The musicbrainz plugin should not be enabled together with"
" the mbpseudo plugin"
)
@override
def candidates(
self,
items: Sequence[Item],
artist: str,
album: str,
va_likely: bool,
) -> Iterable[AlbumInfo]:
if len(self._scripts) == 0:
yield from super().candidates(items, artist, album, va_likely)
else:
for album_info in super().candidates(
items, artist, album, va_likely
):
if isinstance(album_info, PseudoAlbumInfo):
self._log.debug(
"Using {0} release for distance calculations for album {1}",
album_info.determine_best_ref(items),
album_info.album_id,
)
yield album_info # first yield pseudo to give it priority
yield album_info.get_official_release()
else:
yield album_info
@override
def album_info(self, release: JSONDict) -> AlbumInfo:
official_release = super().album_info(release)
if release.get("status") == _STATUS_PSEUDO:
return official_release
elif pseudo_release_ids := self._intercept_mb_release(release):
album_id = self._extract_id(pseudo_release_ids[0])
try:
raw_pseudo_release = self._release_getter(
album_id, RELEASE_INCLUDES
)["release"]
pseudo_release = super().album_info(raw_pseudo_release)
if self.config["custom_tags_only"].get(bool):
self._replace_artist_with_alias(
raw_pseudo_release, pseudo_release
)
self._add_custom_tags(official_release, pseudo_release)
return official_release
else:
return PseudoAlbumInfo(
pseudo_release=_merge_pseudo_and_actual_album(
pseudo_release, official_release
),
official_release=official_release,
)
except musicbrainzngs.MusicBrainzError as exc:
raise MusicBrainzAPIError(
exc,
"get pseudo-release by ID",
album_id,
traceback.format_exc(),
)
else:
return official_release
def _intercept_mb_release(self, data: JSONDict) -> 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-relation-list", [])
if (pr_id := self._wanted_pseudo_release_id(album_id, rel))
is not None
]
def _has_desired_script(self, release: JSONDict) -> bool:
if len(self._scripts) == 0:
return False
elif script := release.get("text-representation", {}).get("script"):
return script in self._scripts
else:
return False
def _wanted_pseudo_release_id(
self,
album_id: str,
relation: JSONDict,
) -> str | None:
if (
len(self._scripts) == 0
or relation.get("type", "") != "transl-tracklisting"
or relation.get("direction", "") != "forward"
or "release" not in relation
):
return None
release = relation["release"]
if "id" in release and self._has_desired_script(release):
self._log.debug(
"Adding pseudo-release {0} for main release {1}",
release["id"],
album_id,
)
return release["id"]
else:
return None
def _replace_artist_with_alias(
self,
raw_pseudo_release: JSONDict,
pseudo_release: AlbumInfo,
):
"""Use the pseudo-release's language to search for artist
alias if the user hasn't configured import languages."""
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", []
)
aliases = [
artist_credit.get("artist", {}).get("alias-list", [])
for artist_credit in artist_credits
]
if lang and len(lang) >= 2 and len(aliases) > 0:
locale = lang[0:2]
aliases_flattened = list(itertools.chain.from_iterable(aliases))
self._log.debug(
"Using locale '{0}' to search aliases {1}",
locale,
aliases_flattened,
)
if alias_dict := _preferred_alias(aliases_flattened, [locale]):
if alias := alias_dict.get("alias"):
self._log.debug("Got alias '{0}'", alias)
pseudo_release.artist = alias
for track in pseudo_release.tracks:
track.artist = alias
def _add_custom_tags(
self,
official_release: AlbumInfo,
pseudo_release: AlbumInfo,
):
for tag_key, pseudo_key in (
self.config["album_custom_tags"].get().items()
):
official_release[tag_key] = pseudo_release[pseudo_key]
track_custom_tags = self.config["track_custom_tags"].get().items()
for track, pseudo_track in zip(
official_release.tracks, pseudo_release.tracks
):
for tag_key, pseudo_key in track_custom_tags:
track[tag_key] = pseudo_track[pseudo_key]
def _adjust_final_album_match(self, match: AlbumMatch):
album_info = match.info
if isinstance(album_info, PseudoAlbumInfo):
self._log.debug(
"Switching {0} to pseudo-release source for final proposal",
album_info.album_id,
)
album_info.use_pseudo_as_ref()
mapping = match.mapping
new_mappings, _, _ = assign_items(
list(mapping.keys()), album_info.tracks
)
mapping.update(new_mappings)
if album_info.data_source == self.data_source:
album_info.data_source = "MusicBrainz"
@override
def _extract_id(self, url: str) -> str | None:
return extract_release_id("MusicBrainz", url)
class PseudoAlbumInfo(AlbumInfo):
"""This is a not-so-ugly hack.
We want the pseudo-release to result in a distance that is lower or equal to that of
the official release, otherwise it won't qualify as a good candidate. However, if
the input is in a script that's different from the pseudo-release (and we want to
translate/transliterate it in the library), it will receive unwanted penalties.
This class is essentially a view of the ``AlbumInfo`` of both official and
pseudo-releases, where it's possible to change the details that are exposed to other
parts of the auto-tagger, enabling a "fair" distance calculation based on the
current input's script but still preferring the translation/transliteration in the
final proposal.
"""
def __init__(
self,
pseudo_release: AlbumInfo,
official_release: AlbumInfo,
**kwargs,
):
super().__init__(pseudo_release.tracks, **kwargs)
self.__dict__["_pseudo_source"] = True
self.__dict__["_official_release"] = official_release
for k, v in pseudo_release.items():
if k not in kwargs:
self[k] = v
def get_official_release(self) -> AlbumInfo:
return self.__dict__["_official_release"]
def determine_best_ref(self, items: Sequence[Item]) -> str:
self.use_pseudo_as_ref()
pseudo_dist = self._compute_distance(items)
self.use_official_as_ref()
official_dist = self._compute_distance(items)
if official_dist < pseudo_dist:
self.use_official_as_ref()
return "official"
else:
self.use_pseudo_as_ref()
return "pseudo"
def _compute_distance(self, items: Sequence[Item]) -> Distance:
mapping, _, _ = assign_items(items, self.tracks)
return distance(items, self, mapping)
def use_pseudo_as_ref(self):
self.__dict__["_pseudo_source"] = True
def use_official_as_ref(self):
self.__dict__["_pseudo_source"] = False
def __getattr__(self, attr: str) -> Any:
# ensure we don't duplicate an official release's id, always return pseudo's
if self.__dict__["_pseudo_source"] or attr == "album_id":
return super().__getattr__(attr)
else:
return self.__dict__["_official_release"].__getattr__(attr)
def __deepcopy__(self, memo):
cls = self.__class__
result = cls.__new__(cls)
memo[id(self)] = result
result.__dict__.update(self.__dict__)
for k, v in self.items():
result[k] = deepcopy(v, memo)
return result

View file

@ -118,13 +118,15 @@ BROWSE_CHUNKSIZE = 100
BROWSE_MAXTRACKS = 500
def _preferred_alias(aliases: list[JSONDict]):
"""Given an list of alias structures for an artist credit, select
and return the user's preferred alias alias or None if no matching
def _preferred_alias(
aliases: list[JSONDict], languages: list[str] | None = None
) -> JSONDict | None:
"""Given a list of alias structures for an artist credit, select
and return the user's preferred alias or None if no matching
alias is found.
"""
if not aliases:
return
return None
# Only consider aliases that have locales set.
valid_aliases = [a for a in aliases if "locale" in a]
@ -134,7 +136,10 @@ def _preferred_alias(aliases: list[JSONDict]):
ignored_alias_types = [a.lower() for a in ignored_alias_types]
# Search configured locales in order.
for locale in config["import"]["languages"].as_str_seq():
if languages is None:
languages = config["import"]["languages"].as_str_seq()
for locale in languages:
# Find matching primary aliases for this locale that are not
# being ignored
matches = []
@ -152,6 +157,8 @@ def _preferred_alias(aliases: list[JSONDict]):
return matches[0]
return None
def _multi_artist_credit(
credit: list[JSONDict], include_join_phrase: bool
@ -323,7 +330,7 @@ def _find_actual_release_from_pseudo_release(
def _merge_pseudo_and_actual_album(
pseudo: beets.autotag.hooks.AlbumInfo, actual: beets.autotag.hooks.AlbumInfo
) -> beets.autotag.hooks.AlbumInfo | None:
) -> beets.autotag.hooks.AlbumInfo:
"""
Merges a pseudo release with its actual release.

View file

@ -18,6 +18,8 @@ New features:
to receive extra verbose logging around last.fm results and how they are
resolved. The ``extended_debug`` config setting and ``--debug`` option
have been removed.
- :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive
MusicBrainz pseudo-releases as recommendations during import.
- Added support for Python 3.13.
Bug fixes:
@ -28,6 +30,12 @@ Bug fixes:
features for all remaining tracks in the session, avoiding unnecessary API
calls and rate limit exhaustion.
For plugin developers:
- A new plugin event, ``album_matched``, is sent when an album that is being
imported has been matched to its metadata and the corresponding distance has
been calculated.
For packagers:
Other changes:

View file

@ -178,6 +178,13 @@ registration process in this case:
:Parameters: ``info`` (|AlbumInfo|)
:Description: Like ``trackinfo_received`` but for album-level metadata.
``album_matched``
:Parameters: ``match`` (``AlbumMatch``)
:Description: Called after ``Item`` objects from a folder that's being
imported have been matched to an ``AlbumInfo`` and the corresponding
distance has been calculated. Missing and extra tracks, if any, are
included in the match.
``before_choose_candidate``
:Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)
:Description: Called before prompting the user during interactive import.

View file

@ -102,6 +102,7 @@ databases. They share the following configuration options:
loadext
lyrics
mbcollection
mbpseudo
mbsubmit
mbsync
metasync
@ -153,6 +154,9 @@ Autotagger Extensions
:doc:`musicbrainz <musicbrainz>`
Search for releases in the MusicBrainz_ database.
:doc:`mbpseudo <mbpseudo>`
Search for releases and pseudo-releases in the MusicBrainz_ database.
:doc:`spotify <spotify>`
Search for releases in the Spotify_ database.

103
docs/plugins/mbpseudo.rst Normal file
View file

@ -0,0 +1,103 @@
MusicBrainz Pseudo-Release Plugin
=================================
The `mbpseudo` plugin can be used *instead of* the `musicbrainz` plugin to
search for MusicBrainz pseudo-releases_ during the import process, which are
added to the normal candidates from the MusicBrainz search.
.. _pseudo-releases: https://musicbrainz.org/doc/Style/Specific_types_of_releases/Pseudo-Releases
This is useful for releases whose title and track titles are written with a
script_ that can be translated or transliterated into a different one.
.. _script: https://en.wikipedia.org/wiki/ISO_15924
Pseudo-releases will only be included if the initial search in MusicBrainz
returns releases whose script is *not* desired and whose relationships include
pseudo-releases with desired scripts.
Configuration
-------------
Since this plugin first searches for official releases from MusicBrainz, all
options from the `musicbrainz` plugin's :ref:`musicbrainz-config` are supported,
but they must be specified under `mbpseudo` in the configuration file.
Additionally, the configuration expects an array of scripts that are desired for
the pseudo-releases. For ``artist`` in particular, keep in mind that even
pseudo-releases might specify it with the original script, so you should also
configure import :ref:`languages` to give artist aliases more priority.
Therefore, the minimum configuration for this plugin looks like this:
.. code-block:: yaml
plugins: mbpseudo # remove musicbrainz
import:
languages: en
mbpseudo:
scripts:
- Latn
Note that the `search_limit` configuration applies to the initial search for
official releases, and that the `data_source` in the database will be
"MusicBrainz". Nevertheless, `data_source_mismatch_penalty` must also be
specified under `mbpseudo` if desired (see also
:ref:`metadata-source-plugin-configuration`). An example with multiple data
sources may look like this:
.. code-block:: yaml
plugins: mbpseudo deezer
import:
languages: en
mbpseudo:
data_source_mismatch_penalty: 0
scripts:
- Latn
deezer:
data_source_mismatch_penalty: 0.2
By default, the data from the pseudo-release will be used to create a proposal
that is independent from the official release and sets all properties in its
metadata. It's possible to change the configuration so that some information
from the pseudo-release is instead added as custom tags, keeping the metadata
from the official release:
.. code-block:: yaml
mbpseudo:
# other config not shown
custom_tags_only: yes
The default custom tags with this configuration are specified as mappings where
the keys define the tag names and the values define the pseudo-release property
that will be used to set the tag's value:
.. code-block:: yaml
mbpseudo:
album_custom_tags:
album_transl: album
album_artist_transl: artist
track_custom_tags:
title_transl: title
artist_transl: artist
Note that the information for each set of custom tags corresponds to different
metadata levels (album or track level), which is why ``artist`` appears twice
even though it effectively references album artist and track artist
respectively.
If you want to modify any mapping under ``album_custom_tags`` or
``track_custom_tags``, you must specify *everything* for that set of tags in
your configuration file because any customization replaces the whole dictionary
of mappings for that level.
.. note::
These custom tags are also added to the music files, not only to the
database.

View file

@ -0,0 +1,286 @@
import json
import pathlib
import pytest
from beets import config
from beets.autotag import AlbumMatch
from beets.autotag.distance import Distance
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Item
from beets.test.helper import PluginMixin
from beetsplug._typing import JSONDict
from beetsplug.mbpseudo import (
_STATUS_PSEUDO,
MusicBrainzPseudoReleasePlugin,
PseudoAlbumInfo,
)
@pytest.fixture(scope="module")
def official_release_info() -> AlbumInfo:
return AlbumInfo(
tracks=[TrackInfo(title="百花繚乱")],
album_id="official",
album="百花繚乱",
)
@pytest.fixture(scope="module")
def pseudo_release_info() -> AlbumInfo:
return AlbumInfo(
tracks=[TrackInfo(title="In Bloom")],
album_id="pseudo",
album="In Bloom",
)
class TestPseudoAlbumInfo:
def test_album_id_always_from_pseudo(
self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo
):
info = PseudoAlbumInfo(pseudo_release_info, official_release_info)
info.use_official_as_ref()
assert info.album_id == "pseudo"
def test_get_attr_from_pseudo(
self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo
):
info = PseudoAlbumInfo(pseudo_release_info, official_release_info)
assert info.album == "In Bloom"
def test_get_attr_from_official(
self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo
):
info = PseudoAlbumInfo(pseudo_release_info, official_release_info)
info.use_official_as_ref()
assert info.album == info.get_official_release().album
def test_determine_best_ref(
self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo
):
info = PseudoAlbumInfo(
pseudo_release_info, official_release_info, data_source="test"
)
item = Item()
item["title"] = "百花繚乱"
assert info.determine_best_ref([item]) == "official"
info.use_pseudo_as_ref()
assert info.data_source == "test"
@pytest.fixture(scope="module")
def rsrc_dir(pytestconfig: pytest.Config):
return pytestconfig.rootpath / "test" / "rsrc" / "mbpseudo"
class TestMBPseudoPlugin(PluginMixin):
plugin = "mbpseudo"
@pytest.fixture(scope="class")
def plugin_config(self):
return {"scripts": ["Latn", "Dummy"]}
@pytest.fixture(scope="class")
def mbpseudo_plugin(self, plugin_config) -> MusicBrainzPseudoReleasePlugin:
self.config[self.plugin].set(plugin_config)
return MusicBrainzPseudoReleasePlugin()
@pytest.fixture
def official_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
info_json = (rsrc_dir / "official_release.json").read_text(
encoding="utf-8"
)
return json.loads(info_json)
@pytest.fixture
def pseudo_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
info_json = (rsrc_dir / "pseudo_release.json").read_text(
encoding="utf-8"
)
return json.loads(info_json)
def test_scripts_init(
self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin
):
assert mbpseudo_plugin._scripts == ["Latn", "Dummy"]
@pytest.mark.parametrize(
"album_id",
[
"a5ce1d11-2e32-45a4-b37f-c1589d46b103",
"-5ce1d11-2e32-45a4-b37f-c1589d46b103",
],
)
def test_extract_id_uses_music_brainz_pattern(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
album_id: str,
):
if album_id.startswith("-"):
assert mbpseudo_plugin._extract_id(album_id) is None
else:
assert mbpseudo_plugin._extract_id(album_id) == album_id
def test_album_info_for_pseudo_release(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
pseudo_release: JSONDict,
):
album_info = mbpseudo_plugin.album_info(pseudo_release["release"])
assert not isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
assert album_info.albumstatus == _STATUS_PSEUDO
@pytest.mark.parametrize(
"json_key",
[
"type",
"direction",
"release",
],
)
def test_interception_skip_when_rel_values_dont_match(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
json_key: str,
):
del official_release["release"]["release-relation-list"][0][json_key]
album_info = mbpseudo_plugin.album_info(official_release["release"])
assert not isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
def test_interception_skip_when_script_doesnt_match(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
):
official_release["release"]["release-relation-list"][0]["release"][
"text-representation"
]["script"] = "Null"
album_info = mbpseudo_plugin.album_info(official_release["release"])
assert not isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
def test_interception(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
pseudo_release: JSONDict,
):
mbpseudo_plugin._release_getter = (
lambda album_id, includes: pseudo_release
)
album_info = mbpseudo_plugin.album_info(official_release["release"])
assert isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
def test_final_adjustment_skip(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
):
match = AlbumMatch(
distance=Distance(),
info=AlbumInfo(tracks=[], data_source="mb"),
mapping={},
extra_items=[],
extra_tracks=[],
)
mbpseudo_plugin._adjust_final_album_match(match)
assert match.info.data_source == "mb"
def test_final_adjustment(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release_info: AlbumInfo,
pseudo_release_info: AlbumInfo,
):
pseudo_album_info = PseudoAlbumInfo(
pseudo_release=pseudo_release_info,
official_release=official_release_info,
data_source=mbpseudo_plugin.data_source,
)
pseudo_album_info.use_official_as_ref()
item = Item()
item["title"] = "百花繚乱"
match = AlbumMatch(
distance=Distance(),
info=pseudo_album_info,
mapping={item: pseudo_album_info.tracks[0]},
extra_items=[],
extra_tracks=[],
)
mbpseudo_plugin._adjust_final_album_match(match)
assert match.info.data_source == "MusicBrainz"
assert match.info.album_id == "pseudo"
assert match.info.album == "In Bloom"
class TestMBPseudoPluginCustomTagsOnly(PluginMixin):
plugin = "mbpseudo"
@pytest.fixture(scope="class")
def mbpseudo_plugin(self) -> MusicBrainzPseudoReleasePlugin:
self.config[self.plugin]["scripts"] = ["Latn"]
self.config[self.plugin]["custom_tags_only"] = True
return MusicBrainzPseudoReleasePlugin()
@pytest.fixture(scope="class")
def official_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
info_json = (rsrc_dir / "official_release.json").read_text(
encoding="utf-8"
)
return json.loads(info_json)
@pytest.fixture(scope="class")
def pseudo_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
info_json = (rsrc_dir / "pseudo_release.json").read_text(
encoding="utf-8"
)
return json.loads(info_json)
def test_custom_tags(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
pseudo_release: JSONDict,
):
config["import"]["languages"] = []
mbpseudo_plugin._release_getter = (
lambda album_id, includes: pseudo_release
)
album_info = mbpseudo_plugin.album_info(official_release["release"])
assert not isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
assert album_info["album_transl"] == "In Bloom"
assert album_info["album_artist_transl"] == "Lilas Ikuta"
assert album_info.tracks[0]["title_transl"] == "In Bloom"
assert album_info.tracks[0]["artist_transl"] == "Lilas Ikuta"
def test_custom_tags_with_import_languages(
self,
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
official_release: JSONDict,
pseudo_release: JSONDict,
):
config["import"]["languages"] = ["en", "jp"]
mbpseudo_plugin._release_getter = (
lambda album_id, includes: pseudo_release
)
album_info = mbpseudo_plugin.album_info(official_release["release"])
assert not isinstance(album_info, PseudoAlbumInfo)
assert album_info.data_source == "MusicBrainzPseudoRelease"
assert album_info["album_transl"] == "In Bloom"
assert album_info["album_artist_transl"] == "Lilas Ikuta"
assert album_info.tracks[0]["title_transl"] == "In Bloom"
assert album_info.tracks[0]["artist_transl"] == "Lilas Ikuta"

View file

@ -0,0 +1,841 @@
{
"release": {
"id": "a5ce1d11-2e32-45a4-b37f-c1589d46b103",
"title": "百花繚乱",
"status": "Official",
"quality": "normal",
"packaging": "None",
"text-representation": {
"language": "jpn",
"script": "Jpan"
},
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"release-group": {
"id": "da0d6bbb-f44b-4fff-8739-9d72db0402a1",
"type": "Single",
"title": "百花繚乱",
"first-release-date": "2025-01-10",
"primary-type": "Single",
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"artist-credit-phrase": "幾田りら"
},
"date": "2025-01-10",
"country": "XW",
"release-event-list": [
{
"date": "2025-01-10",
"area": {
"id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
"name": "[Worldwide]",
"sort-name": "[Worldwide]",
"iso-3166-1-code-list": [
"XW"
]
}
}
],
"release-event-count": 1,
"barcode": "199066336168",
"asin": "B0DR8Y2YDC",
"cover-art-archive": {
"artwork": "true",
"count": "1",
"front": "true",
"back": "false"
},
"label-info-list": [
{
"catalog-number": "Lilas-020",
"label": {
"id": "157afde4-4bf5-4039-8ad2-5a15acc85176",
"type": "Production",
"name": "[no label]",
"sort-name": "[no label]",
"disambiguation": "Special purpose label white labels, self-published releases and other “no label” releases",
"alias-list": [
{
"sort-name": "2636621 Records DK",
"alias": "2636621 Records DK"
},
{
"sort-name": "Auto production",
"type": "Search hint",
"alias": "Auto production"
},
{
"sort-name": "Auto-Edición",
"type": "Search hint",
"alias": "Auto-Edición"
},
{
"sort-name": "Auto-Product",
"type": "Search hint",
"alias": "Auto-Product"
},
{
"sort-name": "Autoedición",
"type": "Search hint",
"alias": "Autoedición"
},
{
"sort-name": "Autoeditado",
"type": "Search hint",
"alias": "Autoeditado"
},
{
"sort-name": "Autoproduit",
"type": "Search hint",
"alias": "Autoproduit"
},
{
"sort-name": "D.I.Y.",
"type": "Search hint",
"alias": "D.I.Y."
},
{
"sort-name": "Demo",
"type": "Search hint",
"alias": "Demo"
},
{
"sort-name": "DistroKid",
"type": "Search hint",
"alias": "DistroKid"
},
{
"sort-name": "Eigenverlag",
"type": "Search hint",
"alias": "Eigenverlag"
},
{
"sort-name": "Eigenvertrieb",
"type": "Search hint",
"alias": "Eigenvertrieb"
},
{
"sort-name": "GRIND MODE",
"alias": "GRIND MODE"
},
{
"sort-name": "INDIPENDANT",
"type": "Search hint",
"alias": "INDIPENDANT"
},
{
"sort-name": "Indepandant",
"type": "Search hint",
"alias": "Indepandant"
},
{
"sort-name": "Independant release",
"type": "Search hint",
"alias": "Independant release"
},
{
"sort-name": "Independent",
"type": "Search hint",
"alias": "Independent"
},
{
"sort-name": "Independente",
"type": "Search hint",
"alias": "Independente"
},
{
"sort-name": "Independiente",
"type": "Search hint",
"alias": "Independiente"
},
{
"sort-name": "Indie",
"type": "Search hint",
"alias": "Indie"
},
{
"sort-name": "Joost Klein",
"alias": "Joost Klein"
},
{
"sort-name": "MoroseSound",
"alias": "MoroseSound"
},
{
"sort-name": "N/A",
"type": "Search hint",
"alias": "N/A"
},
{
"sort-name": "No Label",
"type": "Search hint",
"alias": "No Label"
},
{
"sort-name": "None",
"type": "Search hint",
"alias": "None"
},
{
"sort-name": "Not On A Lebel",
"type": "Search hint",
"alias": "Not On A Lebel"
},
{
"sort-name": "Not On Label",
"type": "Search hint",
"alias": "Not On Label"
},
{
"sort-name": "P2019",
"alias": "P2019"
},
{
"sort-name": "P2020",
"alias": "P2020"
},
{
"sort-name": "P2021",
"alias": "P2021"
},
{
"sort-name": "P2022",
"alias": "P2022"
},
{
"sort-name": "P2023",
"alias": "P2023"
},
{
"sort-name": "P2024",
"alias": "P2024"
},
{
"sort-name": "P2025",
"alias": "P2025"
},
{
"sort-name": "Records DK",
"type": "Search hint",
"alias": "Records DK"
},
{
"sort-name": "Self Digital",
"type": "Search hint",
"alias": "Self Digital"
},
{
"sort-name": "Self Release",
"type": "Search hint",
"alias": "Self Release"
},
{
"sort-name": "Self Released",
"type": "Search hint",
"alias": "Self Released"
},
{
"sort-name": "Self-release",
"type": "Search hint",
"alias": "Self-release"
},
{
"sort-name": "Self-released",
"type": "Search hint",
"alias": "Self-released"
},
{
"sort-name": "Self-released/independent",
"type": "Search hint",
"alias": "Self-released/independent"
},
{
"sort-name": "Sevdaliza",
"alias": "Sevdaliza"
},
{
"sort-name": "TOMMY CASH",
"alias": "TOMMY CASH"
},
{
"sort-name": "Talwiinder",
"alias": "Talwiinder"
},
{
"sort-name": "Unsigned",
"type": "Search hint",
"alias": "Unsigned"
},
{
"locale": "fi",
"sort-name": "ei levymerkkiä",
"type": "Label name",
"primary": "primary",
"alias": "[ei levymerkkiä]"
},
{
"locale": "nl",
"sort-name": "[geen platenmaatschappij]",
"type": "Label name",
"primary": "primary",
"alias": "[geen platenmaatschappij]"
},
{
"locale": "et",
"sort-name": "[ilma plaadifirmata]",
"type": "Label name",
"alias": "[ilma plaadifirmata]"
},
{
"locale": "es",
"sort-name": "[nada]",
"type": "Label name",
"primary": "primary",
"alias": "[nada]"
},
{
"locale": "en",
"sort-name": "[no label]",
"type": "Label name",
"primary": "primary",
"alias": "[no label]"
},
{
"sort-name": "[nolabel]",
"type": "Search hint",
"alias": "[nolabel]"
},
{
"sort-name": "[none]",
"type": "Search hint",
"alias": "[none]"
},
{
"locale": "lt",
"sort-name": "[nėra leidybinės kompanijos]",
"type": "Label name",
"alias": "[nėra leidybinės kompanijos]"
},
{
"locale": "lt",
"sort-name": "[nėra leidyklos]",
"type": "Label name",
"alias": "[nėra leidyklos]"
},
{
"locale": "lt",
"sort-name": "[nėra įrašų kompanijos]",
"type": "Label name",
"primary": "primary",
"alias": "[nėra įrašų kompanijos]"
},
{
"locale": "et",
"sort-name": "[puudub]",
"type": "Label name",
"alias": "[puudub]"
},
{
"locale": "ru",
"sort-name": "samizdat",
"type": "Label name",
"alias": "[самиздат]"
},
{
"locale": "ja",
"sort-name": "[レーベルなし]",
"type": "Label name",
"primary": "primary",
"alias": "[レーベルなし]"
},
{
"sort-name": "auto-release",
"type": "Search hint",
"alias": "auto-release"
},
{
"sort-name": "autoprod.",
"type": "Search hint",
"alias": "autoprod."
},
{
"sort-name": "blank",
"type": "Search hint",
"alias": "blank"
},
{
"sort-name": "d.silvestre",
"alias": "d.silvestre"
},
{
"sort-name": "independent release",
"type": "Search hint",
"alias": "independent release"
},
{
"sort-name": "nyamura",
"alias": "nyamura"
},
{
"sort-name": "pls dnt stp",
"alias": "pls dnt stp"
},
{
"sort-name": "self",
"type": "Search hint",
"alias": "self"
},
{
"sort-name": "self issued",
"type": "Search hint",
"alias": "self issued"
},
{
"sort-name": "self-issued",
"type": "Search hint",
"alias": "self-issued"
},
{
"sort-name": "white label",
"type": "Search hint",
"alias": "white label"
},
{
"sort-name": "но лабел",
"type": "Search hint",
"alias": "но лабел"
},
{
"sort-name": "独立发行",
"type": "Search hint",
"alias": "独立发行"
}
],
"alias-count": 71,
"tag-list": [
{
"count": "12",
"name": "special purpose"
},
{
"count": "18",
"name": "special purpose label"
}
]
}
}
],
"label-info-count": 1,
"medium-list": [
{
"position": "1",
"format": "Digital Media",
"track-list": [
{
"id": "0bd01e8b-18e1-4708-b0a3-c9603b89ab97",
"position": "1",
"number": "1",
"length": "179239",
"recording": {
"id": "781724c1-a039-41e6-bd9b-770c3b9d5b8e",
"title": "百花繚乱",
"length": "179546",
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"isrc-list": [
"JPP302400868"
],
"isrc-count": 1,
"artist-relation-list": [
{
"type": "arranger",
"type-id": "22661fb8-cdb7-4f67-8385-b2a8be6c9f0d",
"target": "f24241fb-4d89-4bf2-8336-3f2a7d2c0025",
"direction": "backward",
"artist": {
"id": "f24241fb-4d89-4bf2-8336-3f2a7d2c0025",
"type": "Person",
"name": "KOHD",
"sort-name": "KOHD",
"country": "JP",
"disambiguation": "Japanese composer/arranger/guitarist, agehasprings"
}
},
{
"type": "phonographic copyright",
"type-id": "7fd5fbc0-fbf4-4d04-be23-417d50a4dc30",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"begin": "2025",
"end": "2025",
"ended": "true",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
},
"target-credit": "Lilas Ikuta"
},
{
"type": "producer",
"type-id": "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0",
"target": "1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05",
"direction": "backward",
"artist": {
"id": "1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05",
"type": "Person",
"name": "山本秀哉",
"sort-name": "Yamamoto, Shuya",
"country": "JP"
}
},
{
"type": "vocal",
"type-id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"work-relation-list": [
{
"type": "performance",
"type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
"target": "9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed",
"direction": "forward",
"work": {
"id": "9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed",
"type": "Song",
"title": "百花繚乱",
"language": "jpn",
"artist-relation-list": [
{
"type": "composer",
"type-id": "d59d99ea-23d4-4a80-b066-edca32ee158f",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
},
{
"type": "lyricist",
"type-id": "3e48faba-ec01-47fd-8e89-30e81161661c",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"url-relation-list": [
{
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"target": "https://utaten.com/lyric/tt24121002/",
"direction": "backward"
},
{
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"target": "https://www.uta-net.com/song/366579/",
"direction": "backward"
}
]
}
}
],
"artist-credit-phrase": "幾田りら"
},
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"artist-credit-phrase": "幾田りら",
"track_or_recording_length": "179239"
}
],
"track-count": 1
}
],
"medium-count": 1,
"artist-relation-list": [
{
"type": "copyright",
"type-id": "730b5251-7432-4896-8fc6-e1cba943bfe1",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"begin": "2025",
"end": "2025",
"ended": "true",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
},
"target-credit": "Lilas Ikuta"
},
{
"type": "phonographic copyright",
"type-id": "01d3488d-8d2a-4cff-9226-5250404db4dc",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"begin": "2025",
"end": "2025",
"ended": "true",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
},
"target-credit": "Lilas Ikuta"
}
],
"release-relation-list": [
{
"type": "transl-tracklisting",
"type-id": "fc399d47-23a7-4c28-bfcf-0607a562b644",
"target": "dc3ee2df-0bc1-49eb-b8c4-34473d279a43",
"direction": "forward",
"release": {
"id": "dc3ee2df-0bc1-49eb-b8c4-34473d279a43",
"title": "In Bloom",
"quality": "normal",
"text-representation": {
"language": "eng",
"script": "Latn"
},
"artist-credit": [
{
"name": "Lilas Ikuta",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"medium-list": [],
"medium-count": 0,
"artist-credit-phrase": "Lilas Ikuta"
}
}
],
"url-relation-list": [
{
"type": "amazon asin",
"type-id": "4f2e710d-166c-480c-a293-2e2c8d658d87",
"target": "https://www.amazon.co.jp/gp/product/B0DR8Y2YDC",
"direction": "forward"
},
{
"type": "free streaming",
"type-id": "08445ccf-7b99-4438-9f9a-fb9ac18099ee",
"target": "https://open.spotify.com/album/3LDV2xGL9HiqCsQujEPQLb",
"direction": "forward"
},
{
"type": "free streaming",
"type-id": "08445ccf-7b99-4438-9f9a-fb9ac18099ee",
"target": "https://www.deezer.com/album/687686261",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://mora.jp/package/43000011/199066336168/",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://mora.jp/package/43000011/199066336168_HD/",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://mora.jp/package/43000011/199066336168_LL/",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://music.apple.com/jp/album/1786972161",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://ototoy.jp/_/default/p/2501951",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://www.qobuz.com/jp-ja/album/lilas-ikuta-/fl9tx2j78reza",
"direction": "forward"
},
{
"type": "purchase for download",
"type-id": "98e08c20-8402-4163-8970-53504bb6a1e4",
"target": "https://www.qobuz.com/jp-ja/album/lilas-ikuta-/l1dnc4xoi6l7a",
"direction": "forward"
},
{
"type": "streaming",
"type-id": "320adf26-96fa-4183-9045-1f5f32f833cb",
"target": "https://music.amazon.co.jp/albums/B0DR8Y2YDC",
"direction": "forward"
},
{
"type": "streaming",
"type-id": "320adf26-96fa-4183-9045-1f5f32f833cb",
"target": "https://music.apple.com/jp/album/1786972161",
"direction": "forward"
},
{
"type": "vgmdb",
"type-id": "6af0134a-df6a-425a-96e2-895f9cd342ba",
"target": "https://vgmdb.net/album/145936",
"direction": "forward"
}
],
"artist-credit-phrase": "幾田りら"
}
}

View file

@ -0,0 +1,346 @@
{
"release": {
"id": "dc3ee2df-0bc1-49eb-b8c4-34473d279a43",
"title": "In Bloom",
"status": "Pseudo-Release",
"quality": "normal",
"text-representation": {
"language": "eng",
"script": "Latn"
},
"artist-credit": [
{
"name": "Lilas Ikuta",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"release-group": {
"id": "da0d6bbb-f44b-4fff-8739-9d72db0402a1",
"type": "Single",
"title": "百花繚乱",
"first-release-date": "2025-01-10",
"primary-type": "Single",
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"artist-credit-phrase": "幾田りら"
},
"cover-art-archive": {
"artwork": "false",
"count": "0",
"front": "false",
"back": "false"
},
"label-info-list": [],
"label-info-count": 0,
"medium-list": [
{
"position": "1",
"format": "Digital Media",
"track-list": [
{
"id": "2018b012-a184-49a2-a464-fb4628a89588",
"position": "1",
"number": "1",
"title": "In Bloom",
"length": "179239",
"artist-credit": [
{
"name": "Lilas Ikuta",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"recording": {
"id": "781724c1-a039-41e6-bd9b-770c3b9d5b8e",
"title": "百花繚乱",
"length": "179546",
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP",
"alias-list": [
{
"locale": "en",
"sort-name": "Ikuta, Lilas",
"type": "Artist name",
"primary": "primary",
"alias": "Lilas Ikuta"
}
],
"alias-count": 1,
"tag-list": [
{
"count": "1",
"name": "j-pop"
},
{
"count": "1",
"name": "singer-songwriter"
}
]
}
}
],
"isrc-list": [
"JPP302400868"
],
"isrc-count": 1,
"artist-relation-list": [
{
"type": "arranger",
"type-id": "22661fb8-cdb7-4f67-8385-b2a8be6c9f0d",
"target": "f24241fb-4d89-4bf2-8336-3f2a7d2c0025",
"direction": "backward",
"artist": {
"id": "f24241fb-4d89-4bf2-8336-3f2a7d2c0025",
"type": "Person",
"name": "KOHD",
"sort-name": "KOHD",
"country": "JP",
"disambiguation": "Japanese composer/arranger/guitarist, agehasprings"
}
},
{
"type": "phonographic copyright",
"type-id": "7fd5fbc0-fbf4-4d04-be23-417d50a4dc30",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"begin": "2025",
"end": "2025",
"ended": "true",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
},
"target-credit": "Lilas Ikuta"
},
{
"type": "producer",
"type-id": "5c0ceac3-feb4-41f0-868d-dc06f6e27fc0",
"target": "1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05",
"direction": "backward",
"artist": {
"id": "1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05",
"type": "Person",
"name": "山本秀哉",
"sort-name": "Yamamoto, Shuya",
"country": "JP"
}
},
{
"type": "vocal",
"type-id": "0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"work-relation-list": [
{
"type": "performance",
"type-id": "a3005666-a872-32c3-ad06-98af558e99b0",
"target": "9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed",
"direction": "forward",
"work": {
"id": "9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed",
"type": "Song",
"title": "百花繚乱",
"language": "jpn",
"artist-relation-list": [
{
"type": "composer",
"type-id": "d59d99ea-23d4-4a80-b066-edca32ee158f",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
},
{
"type": "lyricist",
"type-id": "3e48faba-ec01-47fd-8e89-30e81161661c",
"target": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"direction": "backward",
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"type": "Person",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"url-relation-list": [
{
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"target": "https://utaten.com/lyric/tt24121002/",
"direction": "backward"
},
{
"type": "lyrics",
"type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538",
"target": "https://www.uta-net.com/song/366579/",
"direction": "backward"
}
]
}
}
],
"artist-credit-phrase": "幾田りら"
},
"artist-credit-phrase": "Lilas Ikuta",
"track_or_recording_length": "179239"
}
],
"track-count": 1
}
],
"medium-count": 1,
"release-relation-list": [
{
"type": "transl-tracklisting",
"type-id": "fc399d47-23a7-4c28-bfcf-0607a562b644",
"target": "a5ce1d11-2e32-45a4-b37f-c1589d46b103",
"direction": "backward",
"release": {
"id": "a5ce1d11-2e32-45a4-b37f-c1589d46b103",
"title": "百花繚乱",
"quality": "normal",
"text-representation": {
"language": "jpn",
"script": "Jpan"
},
"artist-credit": [
{
"artist": {
"id": "55e42264-ef27-49d8-93fd-29f930dc96e4",
"name": "幾田りら",
"sort-name": "Ikuta, Lilas",
"country": "JP"
}
}
],
"date": "2025-01-10",
"country": "XW",
"release-event-list": [
{
"date": "2025-01-10",
"area": {
"id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
"name": "[Worldwide]",
"sort-name": "[Worldwide]",
"iso-3166-1-code-list": [
"XW"
]
}
}
],
"release-event-count": 1,
"barcode": "199066336168",
"medium-list": [],
"medium-count": 0,
"artist-credit-phrase": "幾田りら"
}
}
],
"artist-credit-phrase": "Lilas Ikuta"
}
}