diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py new file mode 100644 index 000000000..a73d41b4e --- /dev/null +++ b/beetsplug/albumtypes.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# This file is part of beets. +# Copyright 2021, Edgars Supe. +# +# 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 an album template field for formatted album types.""" + +from __future__ import division, absolute_import, print_function + +from beets.autotag.mb import VARIOUS_ARTISTS_ID +from beets.library import Album +from beets.plugins import BeetsPlugin + + +class AlbumTypesPlugin(BeetsPlugin): + """Adds an album template field for formatted album types.""" + + def __init__(self): + """Init AlbumTypesPlugin.""" + super(AlbumTypesPlugin, self).__init__() + self.album_template_fields['atypes'] = self._atypes + self.config.add({ + 'types': [ + ('ep', 'EP'), + ('single', 'Single'), + ('soundtrack', 'OST'), + ('live', 'Live'), + ('compilation', 'Anthology'), + ('remix', 'Remix') + ], + 'ignore_va': ['compilation'], + 'bracket': '[]' + }) + + def _atypes(self, item: Album): + """Returns a formatted string based on album's types.""" + types = self.config['types'].as_pairs() + ignore_va = self.config['ignore_va'].as_str_seq() + bracket = self.config['bracket'].as_str() + + # Assign a left and right bracket or leave blank if argument is empty. + if len(bracket) == 2: + bracket_l = bracket[0] + bracket_r = bracket[1] + else: + bracket_l = u'' + bracket_r = u'' + + res = '' + albumtypes = item.albumtypes.split('; ') + is_va = item.mb_albumartistid == VARIOUS_ARTISTS_ID + for type in types: + if type[0] in albumtypes and type[1]: + if not is_va or (type[0] not in ignore_va and is_va): + res += f'{bracket_l}{type[1]}{bracket_r}' + + return res diff --git a/docs/changelog.rst b/docs/changelog.rst index 85625011f..f32cae2b9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,9 @@ Major new features: ``albumtypes`` field. Thanks to :user:`edgars-supe`. :bug:`2200` +* :doc:`/plugins/albumtypes`: An accompanying plugin for formatting + ``albumtypes``. Thanks to :user:`edgars-supe`. + 1.5.0 (August 19, 2021) ----------------------- diff --git a/docs/plugins/albumtypes.rst b/docs/plugins/albumtypes.rst new file mode 100644 index 000000000..4a9f67c4a --- /dev/null +++ b/docs/plugins/albumtypes.rst @@ -0,0 +1,57 @@ +AlbumTypes Plugin +================= + +The ``albumtypes`` plugin adds the ability to format and output album types, +such as "Album", "EP", "Single", etc. For the list of available album types, +see the `MusicBrainz documentation`_. + +To use the ``albumtypes`` plugin, enable it in your configuration +(see :ref:`using-plugins`). The plugin defines a new field ``$atypes``, which +you can use in your path formats or elsewhere. + +.. _MusicBrainz documentation: https://musicbrainz.org/doc/Release_Group/Type + +Configuration +------------- + +To configure the plugin, make a ``albumtypes:`` section in your configuration +file. The available options are: + +- **types**: An ordered list of album type to format mappings. The order of the + mappings determines their order in the output. If a mapping is missing or + blank, it will not be in the output. +- **ignore_va**: A list of types that should not be output for Various Artists + albums. Useful for not adding redundant information - various artist albums + are often compilations. +- **bracket**: Defines the brackets to enclose each album type in the output. + +The default configuration looks like this:: + + albumtypes: + types: + - ep: 'EP' + - single: 'Single' + - soundtrack: 'OST' + - live: 'Live' + - compilation: 'Anthology' + - remix: 'Remix' + ignore_va: compilation + bracket: '[]' + +Examples +-------- +With path formats configured like:: + + paths: + default: $albumartist/[$year]$atypes $album/... + albumtype:soundtrack Various Artists/$album [$year]$atypes)/... + comp: Various Artists/$album [$year]$atypes/... + + +The default plugin configuration generates paths that look like this, for example:: + + Aphex Twin/[1993][EP][Remix] On Remixes + Pink Flow/[1995][Live] p·u·l·s·e + Various Artists/20th Century Lullabies [1999] + Various Artists/Ocean's Eleven [2001][OST] + diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index f3d587038..9c628951a 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -61,6 +61,7 @@ following to your configuration:: absubmit acousticbrainz + albumtypes aura badfiles bareasc @@ -176,6 +177,7 @@ Metadata Path Formats ------------ +* :doc:`albumtypes`: Format album type in path formats. * :doc:`bucket`: Group your files into bucket directories that cover different field values ranges. * :doc:`inline`: Use Python snippets to customize path format strings. diff --git a/test/test_albumtypes.py b/test/test_albumtypes.py new file mode 100644 index 000000000..a9db12c30 --- /dev/null +++ b/test/test_albumtypes.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2021, Edgars Supe. +# +# 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. + +"""Tests for the 'albumtypes' plugin.""" + +from __future__ import division, absolute_import, print_function + +import unittest + +from beets.autotag.mb import VARIOUS_ARTISTS_ID +from beetsplug.albumtypes import AlbumTypesPlugin +from test.helper import TestHelper + + +class AlbumTypesPluginTest(unittest.TestCase, TestHelper): + """Tests for albumtypes plugin.""" + + def setUp(self): + """Set up tests.""" + self.setup_beets() + self.load_plugins('albumtypes') + + def tearDown(self): + """Tear down tests.""" + self.unload_plugins() + self.teardown_beets() + + def test_renames_types(self): + """Tests if the plugin correctly renames the specified types.""" + self._set_config( + types=[('ep', 'EP'), ('remix', 'Remix')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(EP)(Remix)', result) + return + + def test_returns_only_specified_types(self): + """Tests if the plugin returns only non-blank types given in config.""" + self._set_config( + types=[('ep', 'EP'), ('soundtrack', '')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix', 'soundtrack']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(EP)', result) + + def test_respects_type_order(self): + """Tests if the types are returned in the same order as config.""" + self._set_config( + types=[('remix', 'Remix'), ('ep', 'EP')], + ignore_va=[], + bracket='()' + ) + album = self._create_album(album_types=['ep', 'remix']) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(Remix)(EP)', result) + return + + def test_ignores_va(self): + """Tests if the specified type is ignored for VA albums.""" + self._set_config( + types=[('ep', 'EP'), ('soundtrack', 'OST')], + ignore_va=['ep'], + bracket='()' + ) + album = self._create_album( + album_types=['ep', 'soundtrack'], + artist_id=VARIOUS_ARTISTS_ID + ) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('(OST)', result) + + def test_respects_defaults(self): + """Tests if the plugin uses the default values if config not given.""" + album = self._create_album( + album_types=['ep', 'single', 'soundtrack', 'live', 'compilation', + 'remix'], + artist_id=VARIOUS_ARTISTS_ID + ) + subject = AlbumTypesPlugin() + result = subject._atypes(album) + self.assertEqual('[EP][Single][OST][Live][Remix]', result) + + def _set_config(self, types: [(str, str)], ignore_va: [str], bracket: str): + self.config['albumtypes']['types'] = types + self.config['albumtypes']['ignore_va'] = ignore_va + self.config['albumtypes']['bracket'] = bracket + + def _create_album(self, album_types: [str], artist_id: str = 0): + return self.add_album( + albumtypes='; '.join(album_types), + mb_albumartistid=artist_id + )