diff --git a/beetsplug/mbpseudo.py b/beetsplug/mbpseudo.py index 8aca07366..e55847f81 100644 --- a/beetsplug/mbpseudo.py +++ b/beetsplug/mbpseudo.py @@ -20,6 +20,7 @@ import traceback from copy import deepcopy from typing import TYPE_CHECKING, Any, Iterable, Sequence +import mediafile import musicbrainzngs from typing_extensions import override @@ -49,10 +50,49 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin): self._release_getter = musicbrainzngs.get_release_by_id - self.config.add({"scripts": []}) + 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) @@ -107,12 +147,17 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin): pseudo_release = super().album_info( raw_pseudo_release["release"] ) - return PseudoAlbumInfo( - pseudo_release=_merge_pseudo_and_actual_album( - pseudo_release, official_release - ), - official_release=official_release, - ) + + if self.config["custom_tags_only"].get(bool): + 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, @@ -167,6 +212,23 @@ class MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin): else: return None + 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): diff --git a/docs/plugins/mbpseudo.rst b/docs/plugins/mbpseudo.rst index 186cb5a6f..56658db26 100644 --- a/docs/plugins/mbpseudo.rst +++ b/docs/plugins/mbpseudo.rst @@ -23,13 +23,18 @@ 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. Therefore, the minimum configuration for this plugin looks -like this: +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 @@ -37,7 +42,7 @@ like this: 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` (see also +specified under `mbpseudo` if desired (see also :ref:`metadata-source-plugin-configuration`). An example with multiple data sources may look like this: @@ -45,6 +50,9 @@ sources may look like this: plugins: mbpseudo deezer + import: + languages: en + mbpseudo: data_source_mismatch_penalty: 0 scripts: @@ -52,3 +60,44 @@ sources may look like this: 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. diff --git a/test/plugins/test_mbpseudo.py b/test/plugins/test_mbpseudo.py index b40bdbcc9..8046dd0e6 100644 --- a/test/plugins/test_mbpseudo.py +++ b/test/plugins/test_mbpseudo.py @@ -223,3 +223,45 @@ class TestMBPseudoPlugin(PluginMixin): 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["import"]["languages"] = ["en", "jp"] + 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, + ): + 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"