From bee35885bb845ea77aa4586bca33da3e54b92ed2 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Thu, 9 Sep 2021 22:30:01 +0300 Subject: [PATCH 1/8] Add `albumtypes` plugin --- beetsplug/albumtypes.py | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 beetsplug/albumtypes.py diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py new file mode 100644 index 000000000..2f8c475ba --- /dev/null +++ b/beetsplug/albumtypes.py @@ -0,0 +1,60 @@ +# -*- 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 + + def _atypes(self, item: Album): + self.config.add({ + 'types': [], + 'ignore_va': [], + 'brackets': '[]' + }) + 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 (not type[0] in ignore_va and is_va): + res += bracket_l + type[1] + bracket_r + + return res From 6fdc5057983a980e4574561f0a0152568ace3d8b Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Thu, 9 Sep 2021 22:31:39 +0300 Subject: [PATCH 2/8] Add tests for `albumtypes` plugin --- test/test_albumtypes.py | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/test_albumtypes.py diff --git a/test/test_albumtypes.py b/test/test_albumtypes.py new file mode 100644 index 000000000..f69edbb8b --- /dev/null +++ b/test/test_albumtypes.py @@ -0,0 +1,102 @@ +# -*- 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 _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 + ) From 899136774b83ca87f6dba732aa7dbb7bccd1e9fe Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Thu, 9 Sep 2021 22:31:55 +0300 Subject: [PATCH 3/8] Add documentation for `albumtypes` plugin --- docs/plugins/albumtypes.rst | 57 +++++++++++++++++++++++++++++++++++++ docs/plugins/index.rst | 1 + 2 files changed, 58 insertions(+) create mode 100644 docs/plugins/albumtypes.rst diff --git a/docs/plugins/albumtypes.rst b/docs/plugins/albumtypes.rst new file mode 100644 index 000000000..dd32596d6 --- /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. List of available type can be found +`here`_. + +To use the ``albumtypes`` plugin, enable it in your configuration +(see :ref:`using-plugins`). Then, add ``$atypes`` to your path formats as +desired. + +.. _here: 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. + Default: ``[]``. +- **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. + Default: ``[]``. +- **bracket**: Defines the brackets to enclose each album type in the output. + Default: ``'[]'`` + +Examples +-------- +Example config:: + + albumtypes: + types: + - ep: 'EP' + - single: 'Single' + - soundtrack: 'OST' + - live: 'Live' + - compilation: 'Anthology' + - remix: 'Remix' + ignore_va: compilation + bracket: '()' + + paths: + default: $albumartist/($year)$atypes $album/... + albumtype:soundtrack Various Artists/$album ($year)$atypes)/... + comp: Various Artists/$album ($year)$atypes/... + +Example outputs:: + + 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 c19e557b7..0cdf79bef 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 From 0a4aa408b6f3eabbb21ef4127ccc960813c59219 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Thu, 9 Sep 2021 22:32:04 +0300 Subject: [PATCH 4/8] Add `albumtypes` plugin to changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) 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) ----------------------- From c59a22efa294eb0bf0869f92a2316b17de720528 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Thu, 9 Sep 2021 22:41:16 +0300 Subject: [PATCH 5/8] Add `albumtypes` to plugin index --- docs/plugins/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 0cdf79bef..9e3353ab7 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -177,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. From 131befa1320c89967ecc76396b89f784d9ca2b07 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Fri, 10 Sep 2021 09:45:15 +0300 Subject: [PATCH 6/8] Improve `albumtypes` plugin code --- beetsplug/albumtypes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py index 2f8c475ba..cb9eeb0bb 100644 --- a/beetsplug/albumtypes.py +++ b/beetsplug/albumtypes.py @@ -30,13 +30,14 @@ class AlbumTypesPlugin(BeetsPlugin): """Init AlbumTypesPlugin.""" super(AlbumTypesPlugin, self).__init__() self.album_template_fields['atypes'] = self._atypes - - def _atypes(self, item: Album): self.config.add({ 'types': [], 'ignore_va': [], 'brackets': '[]' }) + + 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() @@ -54,7 +55,7 @@ class AlbumTypesPlugin(BeetsPlugin): 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 (not type[0] in ignore_va and is_va): - res += bracket_l + type[1] + bracket_r + if not is_va or (type[0] not in ignore_va and is_va): + res += f'{bracket_l}{type[1]}{bracket_r}' return res From c5764df9893d9b4d65a3be473a5ea32fd58e9181 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Fri, 10 Sep 2021 09:51:56 +0300 Subject: [PATCH 7/8] Improve `albumtypes` plugin documentation --- docs/plugins/albumtypes.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/plugins/albumtypes.rst b/docs/plugins/albumtypes.rst index dd32596d6..f97a11a71 100644 --- a/docs/plugins/albumtypes.rst +++ b/docs/plugins/albumtypes.rst @@ -2,14 +2,14 @@ AlbumTypes Plugin ================= The ``albumtypes`` plugin adds the ability to format and output album types, -such as "Album", "EP", "Single", etc. List of available type can be found -`here`_. +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`). Then, add ``$atypes`` to your path formats as -desired. +(see :ref:`using-plugins`). The plugin defines a new field ``$atypes``, which +you can use in your path formats or elsewhere. -.. _here: https://musicbrainz.org/doc/Release_Group/Type +.. _MusicBrainz documentation: https://musicbrainz.org/doc/Release_Group/Type Configuration ------------- @@ -48,10 +48,9 @@ Example config:: albumtype:soundtrack Various Artists/$album ($year)$atypes)/... comp: Various Artists/$album ($year)$atypes/... -Example outputs:: +This 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) - From d40f0c8860b7f3cffd14472eda11b334b5bf6e34 Mon Sep 17 00:00:00 2001 From: Edgars Supe Date: Fri, 10 Sep 2021 10:18:13 +0300 Subject: [PATCH 8/8] Fill default config for `albumtypes` plugin --- beetsplug/albumtypes.py | 13 ++++++++++--- docs/plugins/albumtypes.rst | 31 ++++++++++++++++--------------- test/test_albumtypes.py | 11 +++++++++++ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/beetsplug/albumtypes.py b/beetsplug/albumtypes.py index cb9eeb0bb..a73d41b4e 100644 --- a/beetsplug/albumtypes.py +++ b/beetsplug/albumtypes.py @@ -31,9 +31,16 @@ class AlbumTypesPlugin(BeetsPlugin): super(AlbumTypesPlugin, self).__init__() self.album_template_fields['atypes'] = self._atypes self.config.add({ - 'types': [], - 'ignore_va': [], - 'brackets': '[]' + 'types': [ + ('ep', 'EP'), + ('single', 'Single'), + ('soundtrack', 'OST'), + ('live', 'Live'), + ('compilation', 'Anthology'), + ('remix', 'Remix') + ], + 'ignore_va': ['compilation'], + 'bracket': '[]' }) def _atypes(self, item: Album): diff --git a/docs/plugins/albumtypes.rst b/docs/plugins/albumtypes.rst index f97a11a71..4a9f67c4a 100644 --- a/docs/plugins/albumtypes.rst +++ b/docs/plugins/albumtypes.rst @@ -20,17 +20,12 @@ 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. - Default: ``[]``. - **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. - Default: ``[]``. - **bracket**: Defines the brackets to enclose each album type in the output. - Default: ``'[]'`` -Examples --------- -Example config:: +The default configuration looks like this:: albumtypes: types: @@ -41,16 +36,22 @@ Example config:: - compilation: 'Anthology' - remix: 'Remix' ignore_va: compilation - bracket: '()' + 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/... + default: $albumartist/[$year]$atypes $album/... + albumtype:soundtrack Various Artists/$album [$year]$atypes)/... + comp: Various Artists/$album [$year]$atypes/... -This 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) +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/test/test_albumtypes.py b/test/test_albumtypes.py index f69edbb8b..a9db12c30 100644 --- a/test/test_albumtypes.py +++ b/test/test_albumtypes.py @@ -90,6 +90,17 @@ class AlbumTypesPluginTest(unittest.TestCase, TestHelper): 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