Skip to content

Commit 040b2dd

Browse files
committed
Add custom_tags_only mode for mbpseudo plugin
1 parent cb75898 commit 040b2dd

File tree

3 files changed

+163
-10
lines changed

3 files changed

+163
-10
lines changed

beetsplug/mbpseudo.py

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from copy import deepcopy
2121
from typing import TYPE_CHECKING, Any, Iterable, Sequence
2222

23+
import mediafile
2324
import musicbrainzngs
2425
from typing_extensions import override
2526

@@ -49,10 +50,49 @@ def __init__(self) -> None:
4950

5051
self._release_getter = musicbrainzngs.get_release_by_id
5152

52-
self.config.add({"scripts": []})
53+
self.config.add(
54+
{
55+
"scripts": [],
56+
"custom_tags_only": False,
57+
"album_custom_tags": {
58+
"album_transl": "album",
59+
"album_artist_transl": "artist",
60+
},
61+
"track_custom_tags": {
62+
"title_transl": "title",
63+
"artist_transl": "artist",
64+
},
65+
}
66+
)
67+
5368
self._scripts = self.config["scripts"].as_str_seq()
5469
self._log.debug("Desired scripts: {0}", self._scripts)
5570

71+
album_custom_tags = self.config["album_custom_tags"].get().keys()
72+
track_custom_tags = self.config["track_custom_tags"].get().keys()
73+
self._log.debug(
74+
"Custom tags for albums and tracks: {0} + {1}",
75+
album_custom_tags,
76+
track_custom_tags,
77+
)
78+
for custom_tag in album_custom_tags | track_custom_tags:
79+
if not isinstance(custom_tag, str):
80+
continue
81+
82+
media_field = mediafile.MediaField(
83+
mediafile.MP3DescStorageStyle(custom_tag),
84+
mediafile.MP4StorageStyle(
85+
f"----:com.apple.iTunes:{custom_tag}"
86+
),
87+
mediafile.StorageStyle(custom_tag),
88+
mediafile.ASFStorageStyle(custom_tag),
89+
)
90+
try:
91+
self.add_media_field(custom_tag, media_field)
92+
except ValueError:
93+
# ignore errors due to duplicates
94+
pass
95+
5696
self.register_listener("pluginload", self._on_plugins_loaded)
5797
self.register_listener("album_matched", self._adjust_final_album_match)
5898

@@ -107,12 +147,17 @@ def album_info(self, release: JSONDict) -> AlbumInfo:
107147
pseudo_release = super().album_info(
108148
raw_pseudo_release["release"]
109149
)
110-
return PseudoAlbumInfo(
111-
pseudo_release=_merge_pseudo_and_actual_album(
112-
pseudo_release, official_release
113-
),
114-
official_release=official_release,
115-
)
150+
151+
if self.config["custom_tags_only"].get(bool):
152+
self._add_custom_tags(official_release, pseudo_release)
153+
return official_release
154+
else:
155+
return PseudoAlbumInfo(
156+
pseudo_release=_merge_pseudo_and_actual_album(
157+
pseudo_release, official_release
158+
),
159+
official_release=official_release,
160+
)
116161
except musicbrainzngs.MusicBrainzError as exc:
117162
raise MusicBrainzAPIError(
118163
exc,
@@ -167,6 +212,23 @@ def _wanted_pseudo_release_id(
167212
else:
168213
return None
169214

215+
def _add_custom_tags(
216+
self,
217+
official_release: AlbumInfo,
218+
pseudo_release: AlbumInfo,
219+
):
220+
for tag_key, pseudo_key in (
221+
self.config["album_custom_tags"].get().items()
222+
):
223+
official_release[tag_key] = pseudo_release[pseudo_key]
224+
225+
track_custom_tags = self.config["track_custom_tags"].get().items()
226+
for track, pseudo_track in zip(
227+
official_release.tracks, pseudo_release.tracks
228+
):
229+
for tag_key, pseudo_key in track_custom_tags:
230+
track[tag_key] = pseudo_track[pseudo_key]
231+
170232
def _adjust_final_album_match(self, match: AlbumMatch):
171233
album_info = match.info
172234
if isinstance(album_info, PseudoAlbumInfo):

docs/plugins/mbpseudo.rst

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,81 @@ Since this plugin first searches for official releases from MusicBrainz, all
2323
options from the `musicbrainz` plugin's :ref:`musicbrainz-config` are supported,
2424
but they must be specified under `mbpseudo` in the configuration file.
2525
Additionally, the configuration expects an array of scripts that are desired for
26-
the pseudo-releases. Therefore, the minimum configuration for this plugin looks
27-
like this:
26+
the pseudo-releases. For ``artist`` in particular, keep in mind that even
27+
pseudo-releases might specify it with the original script, so you should also
28+
configure import :ref:`languages` to give artist aliases more priority.
29+
Therefore, the minimum configuration for this plugin looks like this:
2830

2931
.. code-block:: yaml
3032
3133
plugins: mbpseudo # remove musicbrainz
3234
35+
import:
36+
languages: en
37+
3338
mbpseudo:
3439
scripts:
3540
- Latn
3641
3742
Note that the `search_limit` configuration applies to the initial search for
3843
official releases, and that the `data_source` in the database will be
3944
"MusicBrainz". Nevertheless, `data_source_mismatch_penalty` must also be
40-
specified under `mbpseudo` (see also
45+
specified under `mbpseudo` if desired (see also
4146
:ref:`metadata-source-plugin-configuration`). An example with multiple data
4247
sources may look like this:
4348

4449
.. code-block:: yaml
4550
4651
plugins: mbpseudo deezer
4752
53+
import:
54+
languages: en
55+
4856
mbpseudo:
4957
data_source_mismatch_penalty: 0
5058
scripts:
5159
- Latn
5260
5361
deezer:
5462
data_source_mismatch_penalty: 0.2
63+
64+
By default, the data from the pseudo-release will be used to create a proposal
65+
that is independent from the official release and sets all properties in its
66+
metadata. It's possible to change the configuration so that some information
67+
from the pseudo-release is instead added as custom tags, keeping the metadata
68+
from the official release:
69+
70+
.. code-block:: yaml
71+
72+
mbpseudo:
73+
# other config not shown
74+
custom_tags_only: yes
75+
76+
The default custom tags with this configuration are specified as mappings where
77+
the keys define the tag names and the values define the pseudo-release property
78+
that will be used to set the tag's value:
79+
80+
.. code-block:: yaml
81+
82+
mbpseudo:
83+
album_custom_tags:
84+
album_transl: album
85+
album_artist_transl: artist
86+
track_custom_tags:
87+
title_transl: title
88+
artist_transl: artist
89+
90+
Note that the information for each set of custom tags corresponds to different
91+
metadata levels (album or track level), which is why ``artist`` appears twice
92+
even though it effectively references album artist and track artist
93+
respectively.
94+
95+
If you want to modify any mapping under ``album_custom_tags`` or
96+
``track_custom_tags``, you must specify *everything* for that set of tags in
97+
your configuration file because any customization replaces the whole dictionary
98+
of mappings for that level.
99+
100+
.. note::
101+
102+
These custom tags are also added to the music files, not only to the
103+
database.

test/plugins/test_mbpseudo.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,45 @@ def test_final_adjustment(
223223
assert match.info.data_source == "MusicBrainz"
224224
assert match.info.album_id == "pseudo"
225225
assert match.info.album == "In Bloom"
226+
227+
228+
class TestMBPseudoPluginCustomTagsOnly(PluginMixin):
229+
plugin = "mbpseudo"
230+
231+
@pytest.fixture(scope="class")
232+
def mbpseudo_plugin(self) -> MusicBrainzPseudoReleasePlugin:
233+
self.config["import"]["languages"] = ["en", "jp"]
234+
self.config[self.plugin]["scripts"] = ["Latn"]
235+
self.config[self.plugin]["custom_tags_only"] = True
236+
return MusicBrainzPseudoReleasePlugin()
237+
238+
@pytest.fixture(scope="class")
239+
def official_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
240+
info_json = (rsrc_dir / "official_release.json").read_text(
241+
encoding="utf-8"
242+
)
243+
return json.loads(info_json)
244+
245+
@pytest.fixture(scope="class")
246+
def pseudo_release(self, rsrc_dir: pathlib.Path) -> JSONDict:
247+
info_json = (rsrc_dir / "pseudo_release.json").read_text(
248+
encoding="utf-8"
249+
)
250+
return json.loads(info_json)
251+
252+
def test_custom_tags(
253+
self,
254+
mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,
255+
official_release: JSONDict,
256+
pseudo_release: JSONDict,
257+
):
258+
mbpseudo_plugin._release_getter = (
259+
lambda album_id, includes: pseudo_release
260+
)
261+
album_info = mbpseudo_plugin.album_info(official_release["release"])
262+
assert not isinstance(album_info, PseudoAlbumInfo)
263+
assert album_info.data_source == "MusicBrainzPseudoRelease"
264+
assert album_info["album_transl"] == "In Bloom"
265+
assert album_info["album_artist_transl"] == "Lilas Ikuta"
266+
assert album_info.tracks[0]["title_transl"] == "In Bloom"
267+
assert album_info.tracks[0]["artist_transl"] == "Lilas Ikuta"

0 commit comments

Comments
 (0)