From bfeb678e419c1499ef0775146f35eedfbb331d62 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 8 Feb 2018 17:28:05 -0500 Subject: [PATCH 01/48] No media are ignored by default (#2776) We determined on the PR thread that ignoring video tracks is enough, and ignoring typically-video media has more pitfalls. --- beets/config_default.yaml | 2 +- docs/reference/config.rst | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/beets/config_default.yaml b/beets/config_default.yaml index 69c22da28..27aa3d4ca 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -127,7 +127,7 @@ match: original_year: no ignored: [] required: [] - ignored_media: ['Data CD', 'DVD', 'DVD-Video', 'Blu-ray', 'HD-DVD', 'VCD', 'SVCD', 'UMD', 'VHS'] + ignored_media: [] ignore_video_tracks: yes track_length_grace: 10 track_length_max: 30 diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 8c1279b5c..24ed1340d 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -789,13 +789,16 @@ No tags are required by default. ignored_media ~~~~~~~~~~~~~ -By default a list of release media formats considered not containing audio will -be ignored. If you want them to be included (for example if you would like to -consider the audio portion of DVD-Video tracks) you can alter the list -accordingly. +A list of media (i.e., formats) in metadata databases to ignore when matching +music. You can use this to ignore all media that usually contain video instead +of audio, for example:: + + match: + ignored_media: ['Data CD', 'DVD', 'DVD-Video', 'Blu-ray', 'HD-DVD', + 'VCD', 'SVCD', 'UMD', 'VHS'] + +No formats are ignored by default. -Default: ``['Data CD', 'DVD', 'DVD-Video', 'Blu-ray', 'HD-DVD', 'VCD', 'SVCD', -'UMD', 'VHS']``. .. _ignore_video_tracks: From b140f249c146113e165078c40111a228f04e7a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Mon, 19 Feb 2018 10:45:47 +0100 Subject: [PATCH 02/48] Allow plugins to define early import stage functions --- beets/importer.py | 2 ++ beets/plugins.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index aac21d77f..4e4084eec 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -313,6 +313,8 @@ class ImportSession(object): stages += [import_asis(self)] # Plugin stages. + for stage_func in plugins.early_import_stages(): + stages.append(plugin_stage(self, stage_func)) for stage_func in plugins.import_stages(): stages.append(plugin_stage(self, stage_func)) diff --git a/beets/plugins.py b/beets/plugins.py index d62f3c011..ace5170e3 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -81,6 +81,7 @@ class BeetsPlugin(object): self.template_fields = {} if not self.album_template_fields: self.album_template_fields = {} + self.early_import_stages = [] self.import_stages = [] self._log = log.getChild(self.name) @@ -94,6 +95,17 @@ class BeetsPlugin(object): """ return () + def get_early_import_stages(self): + """Return a list of functions that should be called as importer + pipelines stages early in the pipeline. + + The callables are wrapped versions of the functions in + `self.early_import_stages`. Wrapping provides some bookkeeping for the + plugin: specifically, the logging level is adjusted to WARNING. + """ + return [self._set_log_level_and_params(logging.WARNING, import_stage) + for import_stage in self.early_import_stages] + def get_import_stages(self): """Return a list of functions that should be called as importer pipelines stages. @@ -393,6 +405,14 @@ def template_funcs(): return funcs +def early_import_stages(): + """Get a list of early import stage functions defined by plugins.""" + stages = [] + for plugin in find_plugins(): + stages += plugin.get_early_import_stages() + return stages + + def import_stages(): """Get a list of import stage functions defined by plugins.""" stages = [] From d4625bced068bfd0cadff9e4a644bab2a8500eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Mon, 19 Feb 2018 10:46:06 +0100 Subject: [PATCH 03/48] Have convert plugin run early in the pipeline --- beetsplug/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 380061248..d1223596f 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -146,7 +146,7 @@ class ConvertPlugin(BeetsPlugin): u'copy_album_art': False, u'album_art_maxwidth': 0, }) - self.import_stages = [self.auto_convert] + self.early_import_stages = [self.auto_convert] self.register_listener('import_task_files', self._cleanup) From 492ff7359a246e8bfa4c3f226dcb102d31701b7f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 20 Feb 2018 22:57:58 -0500 Subject: [PATCH 04/48] Set up date tests for #2652 --- test/test_datequery.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/test_datequery.py b/test/test_datequery.py index 7b7776711..b8348ca53 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -171,36 +171,41 @@ class DateQueryTest(_common.LibTestCase): class DateQueryTestRelative(_common.LibTestCase): def setUp(self): super(DateQueryTestRelative, self).setUp() - self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M')) + + # We pick a date near a month changeover, which can reveal some time + # zone bugs. + self._now = datetime(2017, 12, 31, 22, 55, 4, 101332) + + self.i.added = _parsetime(self._now.strftime('%Y-%m-%d %H:%M')) self.i.store() def test_single_month_match_fast(self): - query = DateQuery('added', datetime.now().strftime('%Y-%m')) + query = DateQuery('added', self._now.strftime('%Y-%m')) matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_single_month_nonmatch_fast(self): - query = DateQuery('added', (datetime.now() + timedelta(days=30)) + query = DateQuery('added', (self._now + timedelta(days=30)) .strftime('%Y-%m')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) def test_single_month_match_slow(self): - query = DateQuery('added', datetime.now().strftime('%Y-%m')) + query = DateQuery('added', self._now.strftime('%Y-%m')) self.assertTrue(query.match(self.i)) def test_single_month_nonmatch_slow(self): - query = DateQuery('added', (datetime.now() + timedelta(days=30)) + query = DateQuery('added', (self._now + timedelta(days=30)) .strftime('%Y-%m')) self.assertFalse(query.match(self.i)) def test_single_day_match_fast(self): - query = DateQuery('added', datetime.now().strftime('%Y-%m-%d')) + query = DateQuery('added', self._now.strftime('%Y-%m-%d')) matched = self.lib.items(query) self.assertEqual(len(matched), 1) def test_single_day_nonmatch_fast(self): - query = DateQuery('added', (datetime.now() + timedelta(days=1)) + query = DateQuery('added', (self._now + timedelta(days=1)) .strftime('%Y-%m-%d')) matched = self.lib.items(query) self.assertEqual(len(matched), 0) From 502536f4b67b7ebdf66540db2db2475cc4e1fefe Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Tue, 20 Feb 2018 23:22:31 -0500 Subject: [PATCH 05/48] Fix #2652 by using local timestamps Because users write their queries in local time, we want to get a local time tuple from the timestamp stored in the database. --- beets/dbcore/query.py | 2 +- docs/changelog.rst | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index fbe6626c7..8fb64e206 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -708,7 +708,7 @@ class DateQuery(FieldQuery): if self.field not in item: return False timestamp = float(item[self.field]) - date = datetime.utcfromtimestamp(timestamp) + date = datetime.fromtimestamp(timestamp) return self.interval.contains(date) _clause_tmpl = "{0} {1} ?" diff --git a/docs/changelog.rst b/docs/changelog.rst index 4381edeeb..4f00b123b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,11 @@ Fixes: * Avoid a crash when importing a non-ASCII filename when using an ASCII locale on Unix under Python 3. :bug:`2793` :bug:`2803` +* Fix a problem caused by time zone misalignment that could make date queries + fail to match certain dates that are near the edges of a range. For example, + querying for dates within a certain month would fail to match dates within + hours of the end of that month. + :bug:`2652` 1.4.6 (December 21, 2017) From 1bb1bca77911198bc31223d52737bf8dd964c7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 21 Feb 2018 15:54:37 +0100 Subject: [PATCH 06/48] Move the logging wrapper code into a helper function --- beets/plugins.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index ace5170e3..4a2475f50 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -95,6 +95,12 @@ class BeetsPlugin(object): """ return () + def _set_stage_log_level(self, stages): + """Adjust all the stages in `stages` to WARNING logging level. + """ + return [self._set_log_level_and_params(logging.WARNING, stage) + for stage in stages] + def get_early_import_stages(self): """Return a list of functions that should be called as importer pipelines stages early in the pipeline. @@ -103,8 +109,7 @@ class BeetsPlugin(object): `self.early_import_stages`. Wrapping provides some bookkeeping for the plugin: specifically, the logging level is adjusted to WARNING. """ - return [self._set_log_level_and_params(logging.WARNING, import_stage) - for import_stage in self.early_import_stages] + return self._set_stage_log_level(self.early_import_stages) def get_import_stages(self): """Return a list of functions that should be called as importer @@ -114,8 +119,7 @@ class BeetsPlugin(object): `self.import_stages`. Wrapping provides some bookkeeping for the plugin: specifically, the logging level is adjusted to WARNING. """ - return [self._set_log_level_and_params(logging.WARNING, import_stage) - for import_stage in self.import_stages] + return self._set_stage_log_level(self.import_stages) def _set_log_level_and_params(self, base_log_level, func): """Wrap `func` to temporarily set this plugin's logger level to From 305f9f2dfb30c1148889da1cbba06bfef42725da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 21 Feb 2018 16:01:26 +0100 Subject: [PATCH 07/48] Document 'early_import_stages' in the docs --- docs/dev/plugins.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 4d41c8971..5b0e4d08b 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -432,6 +432,11 @@ to register it:: def stage(self, session, task): print('Importing something!') +It is also possible to request your function to run early in the pipeline by +adding the function to the plugin's ``early_import_stages`` field instead.:: + + self.early_import_stages = [self.stage] + .. _extend-query: Extend the Query Syntax From bbadb5f5e1cc2040c465330c7562affa4ed9031d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Koutensk=C3=BD?= Date: Wed, 21 Feb 2018 16:07:58 +0100 Subject: [PATCH 08/48] Changelog for #2814 --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4381edeeb..44558675b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,11 @@ Fixes: * Avoid a crash when importing a non-ASCII filename when using an ASCII locale on Unix under Python 3. :bug:`2793` :bug:`2803` +* Convert plugin now runs before all others in the pipeline to solve an issue + with generating ReplayGain data incompatible between the source and target + file formats. This option to request (part of) your plugin to run early in the + pipeline has been exposed in the plugin API as well (```early_import_stages```). + Thanks to :user:`autrimpo`. 1.4.6 (December 21, 2017) From d07c0ab85b6135f39fbca1c3d530556a479fa157 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 22 Feb 2018 15:50:46 -0500 Subject: [PATCH 09/48] Revise changelog for #2814 --- docs/changelog.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index effa6d687..a38d5504a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -51,12 +51,19 @@ Fixes: querying for dates within a certain month would fail to match dates within hours of the end of that month. :bug:`2652` -* Convert plugin now runs before all others in the pipeline to solve an issue - with generating ReplayGain data incompatible between the source and target - file formats. This option to request (part of) your plugin to run early in the - pipeline has been exposed in the plugin API as well (```early_import_stages```). +* :doc:`/plugins/convert`: The plugin now runs before other plugin-provided + import stages, which addresses an issue with generating ReplayGain data + incompatible between the source and target file formats. + :bug:`2814` Thanks to :user:`autrimpo`. +For developers: + +* Plugins can now run their import stages *early*, before other plugins. Use + the ``early_import_stages`` list instead of plain ``import_stages`` to + request this behavior. + :bug:`2814` + 1.4.6 (December 21, 2017) ------------------------- From 323d90db17f4c0530eae7c6feb5dac1a3aba3d05 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 22 Feb 2018 15:51:15 -0500 Subject: [PATCH 10/48] Tiny ReST markup tweaks (#2814) --- docs/dev/plugins.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 5b0e4d08b..bab0e604d 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -433,9 +433,9 @@ to register it:: print('Importing something!') It is also possible to request your function to run early in the pipeline by -adding the function to the plugin's ``early_import_stages`` field instead.:: +adding the function to the plugin's ``early_import_stages`` field instead:: - self.early_import_stages = [self.stage] + self.early_import_stages = [self.stage] .. _extend-query: From bd788544c20bad40421faa0b367587354e718f8a Mon Sep 17 00:00:00 2001 From: David Logie Date: Thu, 22 Feb 2018 18:46:14 +0000 Subject: [PATCH 11/48] Make sure release events are selected in preferred order. Previously, release events were selected based on the order that MusicBrainz listed them rather than the order defined in config.yaml. --- beets/autotag/mb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 385dc64fb..9ce449a8b 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -118,8 +118,8 @@ def _preferred_release_event(release): """ countries = config['match']['preferred']['countries'].as_str_seq() - for event in release.get('release-event-list', {}): - for country in countries: + for country in countries: + for event in release.get('release-event-list', {}): try: if country in event['area']['iso-3166-1-code-list']: return country, event['date'] From d6b6ebbeb96212d5477e6aaf5244e3ec61a836e7 Mon Sep 17 00:00:00 2001 From: David Logie Date: Thu, 22 Feb 2018 18:44:51 +0000 Subject: [PATCH 12/48] mbcollection: Make sure missing albums are removed from collections correctly. --- beetsplug/mbcollection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/mbcollection.py b/beetsplug/mbcollection.py index c01d544a4..d99c386c9 100644 --- a/beetsplug/mbcollection.py +++ b/beetsplug/mbcollection.py @@ -103,8 +103,8 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): offset = 0 albums_in_collection, release_count = _fetch(offset) for i in range(0, release_count, FETCH_CHUNK_SIZE): - offset += FETCH_CHUNK_SIZE albums_in_collection += _fetch(offset)[0] + offset += FETCH_CHUNK_SIZE return albums_in_collection @@ -122,7 +122,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): def remove_missing(self, collection_id, lib_albums): lib_ids = set([x.mb_albumid for x in lib_albums]) albums_in_collection = self._get_albums_in_collection(collection_id) - remove_me = list(lib_ids - set(albums_in_collection)) + remove_me = list(set(albums_in_collection) - lib_ids) for i in range(0, len(remove_me), FETCH_CHUNK_SIZE): chunk = remove_me[i:i + FETCH_CHUNK_SIZE] mb_call( From 5d2e203f2487c052e7d38180c1e7fa3d84683979 Mon Sep 17 00:00:00 2001 From: David Logie Date: Fri, 23 Feb 2018 21:43:12 +0000 Subject: [PATCH 13/48] Add changelog entry for #2816. --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a38d5504a..d639b0e20 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,6 +56,9 @@ Fixes: incompatible between the source and target file formats. :bug:`2814` Thanks to :user:`autrimpo`. +* Importing a release with multiple release events now selects the + event based on the order of your :ref:`preferred` countries rather than + the order of release events in MusicBrainz. :bug:`2816` For developers: From a6be28a65eef474cc98af35935eb4b9545115cd7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 23 Feb 2018 18:18:32 -0500 Subject: [PATCH 14/48] Fix #2817: `drop` in configuration was ignored This was overridden by the default CLI option. Now the default for the config option is None, meaning no change to the config. --- beetsplug/ftintitle.py | 2 +- docs/changelog.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 1060a2dd8..9303f9cfc 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -89,7 +89,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): self._command.parser.add_option( u'-d', u'--drop', dest='drop', - action='store_true', default=False, + action='store_true', default=None, help=u'drop featuring from artists and ignore title update') if self.config['auto']: diff --git a/docs/changelog.rst b/docs/changelog.rst index a38d5504a..d09bab826 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,6 +56,9 @@ Fixes: incompatible between the source and target file formats. :bug:`2814` Thanks to :user:`autrimpo`. +* :doc:`/plugins/ftintitle`: The ``drop`` config option had no effect; it now + does what it says it should do. + :bug:`2817` For developers: From 2c1e4d878b79ca3bdbba125d3e17b4139a56c609 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 10 Nov 2017 11:17:27 +0100 Subject: [PATCH 15/48] Advanced fetchart source config: add the (still unused) match_by constructor argument --- beetsplug/fetchart.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 8bee6e804..e4a12b84e 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -192,9 +192,12 @@ class RequestMixin(object): # ART SOURCES ################################################################ class ArtSource(RequestMixin): - def __init__(self, log, config): + VALID_MATCHING_CRITERIA = ['default'] + + def __init__(self, log, config, match_by=None): self._log = log self._config = config + self.match_by = match_by or self.VALID_MATCHING_CRITERIA def get(self, album, plugin, paths): raise NotImplementedError() @@ -289,6 +292,7 @@ class RemoteArtSource(ArtSource): class CoverArtArchive(RemoteArtSource): NAME = u"Cover Art Archive" + VALID_MATCHING_CRITERIA = ['release', 'releasegroup'] if util.SNI_SUPPORTED: URL = 'https://coverartarchive.org/release/{mbid}/front' @@ -301,10 +305,10 @@ class CoverArtArchive(RemoteArtSource): """Return the Cover Art Archive and Cover Art Archive release group URLs using album MusicBrainz release ID and release group ID. """ - if album.mb_albumid: + if 'release' in self.match_by and album.mb_albumid: yield self._candidate(url=self.URL.format(mbid=album.mb_albumid), match=Candidate.MATCH_EXACT) - if album.mb_releasegroupid: + if 'releasegroup' in self.match_by and album.mb_releasegroupid: yield self._candidate( url=self.GROUP_URL.format(mbid=album.mb_releasegroupid), match=Candidate.MATCH_FALLBACK) @@ -770,7 +774,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): sources_name.append(u'filesystem') except ValueError: pass - self.sources = [ART_SOURCES[s](self._log, self.config) + self.sources = [ART_SOURCES[s](self._log, self.config, None) for s in sources_name] # Asynchronous; after music is added to the library. From 971584b4b23978502d7b174a799fb8177e68e020 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Mon, 13 Nov 2017 01:54:39 +0100 Subject: [PATCH 16/48] Improve readability of plugins.sanitize_choices --- beets/plugins.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 4a2475f50..1dc1f6496 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -502,9 +502,12 @@ def sanitize_choices(choices, choices_all): others = [x for x in choices_all if x not in choices] res = [] for s in choices: - if s in list(choices_all) + ['*']: - if not (s in seen or seen.add(s)): - res.extend(list(others) if s == '*' else [s]) + if s not in seen: + if s in list(choices_all): + res.append(s) + elif s == '*': + res.extend(others) + seen.add(s) return res From 5a043f28bd99ddb630967eded0a0f74dbf46f4b6 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Mon, 13 Nov 2017 01:55:15 +0100 Subject: [PATCH 17/48] Advanced fetchart source config: add validation function --- beets/plugins.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/beets/plugins.py b/beets/plugins.py index 1dc1f6496..5e71c532e 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -511,6 +511,44 @@ def sanitize_choices(choices, choices_all): return res +def sanitize_pairs(pairs, pairs_all): + """Clean up a single-element mapping configuration attribute as returned + by `confit`'s `Pairs` template: keep only two-element tuples present in + pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*') + wildcards while keeping the original order. Note that ('*', '*') and + ('*', 'whatever') have the same effect. + + For example, + + >>> sanitize_pairs( + ... [('foo', 'baz bar'), ('key', '*'), ('*', '*')], + ... [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'), + ... ('key', 'value')] + ... ) + [('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')] + """ + pairs_all = list(pairs_all) + seen = set() + others = [x for x in pairs_all if x not in pairs] + res = [] + for k, values in pairs: + for v in values.split(): + x = (k, v) + if x in pairs_all: + if not x in seen: + seen.add(x) + res.append(x) + elif k == '*': + new = [o for o in others if o not in seen] + seen.update(new) + res.extend(new) + elif v == '*': + new = [o for o in others if o not in seen and o[0] == k] + seen.update(new) + res.extend(new) + return res + + def notify_info_yielded(event): """Makes a generator send the event 'event' every time it yields. This decorator is supposed to decorate a generator, but any function From 60bffbadbdee3652f65ff495a0436797a5296f71 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Mon, 13 Nov 2017 01:56:57 +0100 Subject: [PATCH 18/48] Advanced fetchart source config: write (restore?) confit' as_pairs() --- beets/util/confit.py | 79 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/beets/util/confit.py b/beets/util/confit.py index 73ae97abc..7b5d39f29 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -413,6 +413,12 @@ class ConfigView(object): """ return self.get(StrSeq(split=split)) + def as_pairs(self, default_value=None): + """Get the value as a sequence of pairs of two strings. Equivalent to + `get(Pairs())`. + """ + return self.get(Pairs(default_value=default_value)) + def as_str(self): """Get the value as a (Unicode) string. Equivalent to `get(unicode)` on Python 2 and `get(str)` on Python 3. @@ -1242,30 +1248,75 @@ class StrSeq(Template): super(StrSeq, self).__init__() self.split = split + def _convert_value(self, x, view): + if isinstance(x, STRING): + return x + elif isinstance(x, bytes): + return x.decode('utf-8', 'ignore') + else: + self.fail(u'must be a list of strings', view, True) + def convert(self, value, view): if isinstance(value, bytes): value = value.decode('utf-8', 'ignore') if isinstance(value, STRING): if self.split: - return value.split() + value = value.split() else: - return [value] + value = [value] + else: + try: + value = list(value) + except TypeError: + self.fail(u'must be a whitespace-separated string or a list', + view, True) + return [self._convert_value(v, view) for v in value] + + +class Pairs(StrSeq): + """A template for ordered key-value pairs. + + This can either be given with the same syntax as for `StrSeq` (i.e. without + values), or as a list of strings and/or single-element mappings such as:: + + - key: value + - [key, value] + - key + + The result is a list of two-element tuples. If no value is provided, the + `default_value` will be returned as the second element. + """ + + def __init__(self, default_value=None): + """Create a new template. + + `default` is the dictionary value returned for items that are not + a mapping, but a single string. + """ + super(Pairs, self).__init__(split=True) + self.default_value = default_value + + def _convert_value(self, x, view): try: - value = list(value) - except TypeError: - self.fail(u'must be a whitespace-separated string or a list', - view, True) - - def convert(x): - if isinstance(x, STRING): - return x - elif isinstance(x, bytes): - return x.decode('utf-8', 'ignore') + return (super(Pairs, self)._convert_value(x, view), + self.default_value) + except ConfigTypeError: + if isinstance(x, collections.Mapping): + if len(x) != 1: + self.fail(u'must be a single-element mapping', view, True) + k, v = iter_first(x.items()) + elif isinstance(x, collections.Sequence): + if len(x) != 2: + self.fail(u'must be a two-element list', view, True) + k, v = x else: - self.fail(u'must be a list of strings', view, True) - return list(map(convert, value)) + # Is this even possible? -> Likely, if some !directive cause + # YAML to parse this to some custom type. + self.fail(u'must be a single string, mapping, or a list' + str(x), view, True) + return (super(Pairs, self)._convert_value(k, view), + super(Pairs, self)._convert_value(v, view)) class Filename(Template): From e7a3e27ed9528fb175579eb602b1ba29638b4ebe Mon Sep 17 00:00:00 2001 From: wordofglass Date: Mon, 13 Nov 2017 02:03:13 +0100 Subject: [PATCH 19/48] Advanced fetchart source config: Actually use the new syntax --- beetsplug/fetchart.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index e4a12b84e..b98a68593 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -761,21 +761,30 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if not self.config['google_key'].get() and \ u'google' in available_sources: available_sources.remove(u'google') - sources_name = plugins.sanitize_choices( - self.config['sources'].as_str_seq(), available_sources) + available_sources = [(s, c) + for s in available_sources + for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] + sources = plugins.sanitize_pairs( + self.config['sources'].as_pairs(default_value='*'), + available_sources) + if 'remote_priority' in self.config: self._log.warning( u'The `fetch_art.remote_priority` configuration option has ' u'been deprecated. Instead, place `filesystem` at the end of ' u'your `sources` list.') if self.config['remote_priority'].get(bool): - try: - sources_name.remove(u'filesystem') - sources_name.append(u'filesystem') - except ValueError: - pass - self.sources = [ART_SOURCES[s](self._log, self.config, None) - for s in sources_name] + fs = [] + others = [] + for s, c in sources: + if s == 'filesystem': + fs.append((s, c)) + else: + others.append((s, c)) + sources = others + fs + + self.sources = [ART_SOURCES[s](self._log, self.config, match_by=[c]) + for s, c in sources] # Asynchronous; after music is added to the library. def fetch_art(self, session, task): From 318f0c4d16710712ed49d10c621fbf52705161dd Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Fri, 23 Feb 2018 16:00:41 +0100 Subject: [PATCH 20/48] Advanced fetchart source config: pep8 --- beets/plugins.py | 2 +- beets/util/confit.py | 4 +++- beetsplug/fetchart.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/beets/plugins.py b/beets/plugins.py index 5e71c532e..1bd2cacd5 100644 --- a/beets/plugins.py +++ b/beets/plugins.py @@ -535,7 +535,7 @@ def sanitize_pairs(pairs, pairs_all): for v in values.split(): x = (k, v) if x in pairs_all: - if not x in seen: + if x not in seen: seen.add(x) res.append(x) elif k == '*': diff --git a/beets/util/confit.py b/beets/util/confit.py index 7b5d39f29..b5513f48e 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -1314,7 +1314,9 @@ class Pairs(StrSeq): else: # Is this even possible? -> Likely, if some !directive cause # YAML to parse this to some custom type. - self.fail(u'must be a single string, mapping, or a list' + str(x), view, True) + self.fail(u'must be a single string, mapping, or a list' + u'' + str(x), + view, True) return (super(Pairs, self)._convert_value(k, view), super(Pairs, self)._convert_value(v, view)) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index b98a68593..0e106694d 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -762,8 +762,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): u'google' in available_sources: available_sources.remove(u'google') available_sources = [(s, c) - for s in available_sources - for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] + for s in available_sources + for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] sources = plugins.sanitize_pairs( self.config['sources'].as_pairs(default_value='*'), available_sources) From dee288545758e9670c816c964b5fb6461f293296 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Sat, 24 Feb 2018 10:54:50 +0100 Subject: [PATCH 21/48] docs/plugins/fetchart: fix internal link target location --- docs/plugins/fetchart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index e375f9f57..85d22078a 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -104,8 +104,6 @@ already have it; the ``-f`` or ``--force`` switch makes it search for art in Web databases regardless. If you specify a query, only matching albums will be processed; otherwise, the command processes every album in your library. -.. _image-resizing: - Display Only Missing Album Art ------------------------------ @@ -117,6 +115,8 @@ art:: By default the command will display all results, the ``-q`` or ``--quiet`` switch will only display results for album arts that are still missing. +.. _image-resizing: + Image Resizing -------------- From db85c28c7f4899e3db1c650b0fb04ff59a06c0ab Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Sat, 24 Feb 2018 10:55:29 +0100 Subject: [PATCH 22/48] Advanced fetchart source config: documentation --- docs/plugins/fetchart.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 85d22078a..58ca0b1f4 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -54,7 +54,8 @@ file. The available options are: matches at the cost of some speed. They are searched in the given order, thus in the default config, no remote (Web) art source are queried if local art is found in the filesystem. To use a local image as fallback, - move it to the end of the list. + move it to the end of the list. For even more fine-grained control over + the search order, see the section on :ref:`album-art-sources` below. - **google_key**: Your Google API key (to enable the Google Custom Search backend). Default: None. @@ -135,6 +136,8 @@ environment variable so that ImageMagick comes first or use Pillow instead. .. _Pillow: https://github.com/python-pillow/Pillow .. _ImageMagick: http://www.imagemagick.org/ +.. _album-art-sources: + Album Art Sources ----------------- @@ -150,6 +153,25 @@ file whose name contains "cover", "front", "art", "album" or "folder", but in the absence of well-known names, it will use any image file in the same folder as your music files. +For some of the art sources, the backend service can match artwork by various +criteria. If you want finer control over the search order in such cases, the +following alternative syntax for the ``sources`` option can be used:: + + fetchart: + sources: + - filesystem + - coverart: release + - itunes + - coverart: releasegroup + - '*' + +where listing a source without matching criteria will default to trying all +available strategies. Entries of the forms ``coverart: release releasegroup`` +and ``coverart: *`` are also valid. +Currently, the ``coverart`` source is the only backend to support several +such values, namely ``release`` and ``releasegroup``, which refer to the +respective MusicBrainz IDs. + When you choose to apply changes during an import, beets will search for art as described above. For "as-is" imports (and non-autotagged imports using the ``-A`` flag), beets only looks for art on the local filesystem. From a6ae1570f92437276f2fb926d32bf88badf92422 Mon Sep 17 00:00:00 2001 From: wisp3rwind <17089248+wisp3rwind@users.noreply.github.com> Date: Sat, 24 Feb 2018 11:29:36 +0100 Subject: [PATCH 23/48] Advanced fetchart source config: changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f423ea832..3121b3394 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,9 @@ New features: recording skipped directories to the incremental list, so you can revisit them later. Thanks to :user:`sekjun9878`. :bug:`2773` +* :doc:`/plugins/fetchart`: extended syntax for the ``sources`` option to give + fine-grained control over the search order for backends with several matching + strategies. Fixes: From 1254d48b7245f8f25560de18a36611c2cc55092a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 24 Feb 2018 10:42:32 -0500 Subject: [PATCH 24/48] Revise docs for #2739 --- docs/plugins/fetchart.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 58ca0b1f4..d6d9adeff 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -154,8 +154,8 @@ the absence of well-known names, it will use any image file in the same folder as your music files. For some of the art sources, the backend service can match artwork by various -criteria. If you want finer control over the search order in such cases, the -following alternative syntax for the ``sources`` option can be used:: +criteria. If you want finer control over the search order in such cases, you +can use this alternative syntax for the ``sources`` option:: fetchart: sources: @@ -168,8 +168,8 @@ following alternative syntax for the ``sources`` option can be used:: where listing a source without matching criteria will default to trying all available strategies. Entries of the forms ``coverart: release releasegroup`` and ``coverart: *`` are also valid. -Currently, the ``coverart`` source is the only backend to support several -such values, namely ``release`` and ``releasegroup``, which refer to the +Currently, only the ``coverart`` source supports multiple criteria: +namely, ``release`` and ``releasegroup``, which refer to the respective MusicBrainz IDs. When you choose to apply changes during an import, beets will search for art as From df83516086cd293e2074691fcbc77eecdb441674 Mon Sep 17 00:00:00 2001 From: waweic Date: Mon, 26 Feb 2018 17:01:06 +0100 Subject: [PATCH 25/48] Fix jumping time in beets.js Round was used instead of floor --- beetsplug/web/static/beets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index ec9aae9b3..51985c183 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -4,7 +4,7 @@ var timeFormat = function(secs) { return '0:00'; } secs = Math.round(secs); - var mins = '' + Math.round(secs / 60); + var mins = '' + Math.floor(secs / 60); secs = '' + (secs % 60); if (secs.length < 2) { secs = '0' + secs; From be96c1022a82774fbbc7d4de421c14c286ca14d8 Mon Sep 17 00:00:00 2001 From: waweic Date: Mon, 26 Feb 2018 18:33:30 +0100 Subject: [PATCH 26/48] Fix album_art() in __init__.py flask.send_file() expects a string, g.lib.get_album() returns bytes. Added decode() to album_art(). If g.lib.get_album() gets a non-existing id, it returns None. Python would throw an error in this case. Added check to prevent this. --- beetsplug/web/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 635c2f5a8..9de44dcb4 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -285,8 +285,8 @@ def album_query(queries): @app.route('/album//art') def album_art(album_id): album = g.lib.get_album(album_id) - if album.artpath: - return flask.send_file(album.artpath) + if album and album.artpath: + return flask.send_file(album.artpath.decode()) else: return flask.abort(404) From e3599742b44a8d2053d98e004d71a9451c001610 Mon Sep 17 00:00:00 2001 From: Samuel Loury Date: Mon, 26 Feb 2018 16:10:54 +0100 Subject: [PATCH 27/48] Add a support for supports_credentials If the web plugin is behind a credential based http server and is accessed by another in-browser client in another domain, the specification of CORS requires the server to indicate it supports such credentials. --- beetsplug/web/__init__.py | 8 +++++++- docs/changelog.rst | 3 +++ docs/plugins/web.rst | 18 +++++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 635c2f5a8..c78e7b73b 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -341,6 +341,7 @@ class WebPlugin(BeetsPlugin): 'host': u'127.0.0.1', 'port': 8337, 'cors': '', + 'cors_supports_credentials': False, 'reverse_proxy': False, 'include_paths': False, }) @@ -372,7 +373,12 @@ class WebPlugin(BeetsPlugin): app.config['CORS_RESOURCES'] = { r"/*": {"origins": self.config['cors'].get(str)} } - CORS(app) + CORS( + app, + supports_credentials=self.config[ + 'cors_supports_credentials' + ] + ) # Allow serving behind a reverse proxy if self.config['reverse_proxy']: diff --git a/docs/changelog.rst b/docs/changelog.rst index 3121b3394..ab1d53e48 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ New features: * :doc:`/plugins/fetchart`: extended syntax for the ``sources`` option to give fine-grained control over the search order for backends with several matching strategies. +* :doc:`/plugins/web`: added the boolean ``cors_supports_credentials`` option to + allow in-browser clients to login to the beet web server even when it is + protected by an authorization mechanism. Fixes: diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index 73a2b9147..fcf7e4934 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -63,6 +63,8 @@ configuration file. The available options are: Default: 8337. - **cors**: The CORS allowed origin (see :ref:`web-cors`, below). Default: CORS is disabled. +- **cors_supports_credentials**: Support credentials when using CORS (see :ref:`web-cors`, below). + Default: CORS_SUPPORTS_CREDENTIALS is disabled. - **reverse_proxy**: If true, enable reverse proxy support (see :ref:`reverse-proxy`, below). Default: false. @@ -100,13 +102,15 @@ default, browsers will only allow access from clients running on the same server as the API. (You will get an arcane error about ``XMLHttpRequest`` otherwise.) A technology called `CORS`_ lets you relax this restriction. -If you want to use an in-browser client hosted elsewhere (or running from -a different server on your machine), first install the `flask-cors`_ plugin by -typing ``pip install flask-cors``. Then set the ``cors`` configuration option -to the "origin" (protocol, host, and optional port number) where the client is -served. Or set it to ``'*'`` to enable access from all origins. Note that -there are security implications if you set the origin to ``'*'``, so please -research this before using it. +If you want to use an in-browser client hosted elsewhere (or running from a +different server on your machine), first install the `flask-cors`_ plugin by +typing ``pip install flask-cors``. Then set the ``cors`` configuration option to +the "origin" (protocol, host, and optional port number) where the client is +served. Or set it to ``'*'`` to enable access from all origins. Note that there +are security implications if you set the origin to ``'*'``, so please research +this before using it. In addition, if the ``web`` server is hidden via +credentials, you might want to set the ``cors_supports_credentials`` +configuration option to True for the in-browser client to be able to login. For example:: From 00f61e928130db7e33f9c8fc7652f268d1b0aa73 Mon Sep 17 00:00:00 2001 From: waweic Date: Mon, 26 Feb 2018 21:18:41 +0100 Subject: [PATCH 28/48] Update changelog.rst Add the changelog entry --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3121b3394..afd1ae407 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -65,6 +65,9 @@ Fixes: * Importing a release with multiple release events now selects the event based on the order of your :ref:`preferred` countries rather than the order of release events in MusicBrainz. :bug:`2816` +* :doc:`/plugins/web`: The time display in the web interface would incorrectly jump + at the 30-second mark of every minute. Now, it correctly changes over at zero + seconds. :bug:`2822` For developers: From 453fd372a3ff389ed928b54aa3c3812cf9bf781e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 26 Feb 2018 18:00:59 -0500 Subject: [PATCH 29/48] Flatten a config view (#2821) --- beetsplug/web/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index c78e7b73b..fd0060f4c 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -377,7 +377,7 @@ class WebPlugin(BeetsPlugin): app, supports_credentials=self.config[ 'cors_supports_credentials' - ] + ].get(bool) ) # Allow serving behind a reverse proxy From 6d5a1e9284ed10d11158ac22160ccf4554b20d88 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 26 Feb 2018 18:02:16 -0500 Subject: [PATCH 30/48] web docs: Split CORS credentials paragraph (#2821) --- docs/plugins/web.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/plugins/web.rst b/docs/plugins/web.rst index fcf7e4934..35287acc8 100644 --- a/docs/plugins/web.rst +++ b/docs/plugins/web.rst @@ -108,9 +108,11 @@ typing ``pip install flask-cors``. Then set the ``cors`` configuration option to the "origin" (protocol, host, and optional port number) where the client is served. Or set it to ``'*'`` to enable access from all origins. Note that there are security implications if you set the origin to ``'*'``, so please research -this before using it. In addition, if the ``web`` server is hidden via -credentials, you might want to set the ``cors_supports_credentials`` -configuration option to True for the in-browser client to be able to login. +this before using it. + +If the ``web`` server is behind a proxy that uses credentials, you might want +to set the ``cors_supports_credentials`` configuration option to true to let +in-browser clients log in. For example:: From 8793bb0882fa60c339470839ed72cf436627672e Mon Sep 17 00:00:00 2001 From: waweic Date: Tue, 27 Feb 2018 22:37:05 +0100 Subject: [PATCH 31/48] Update changelog.rst Add the changelog entry --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a4dfd2cb4..26de4dfaa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -71,6 +71,10 @@ Fixes: * :doc:`/plugins/web`: The time display in the web interface would incorrectly jump at the 30-second mark of every minute. Now, it correctly changes over at zero seconds. :bug:`2822` +* :doc:`/plugins/web`: In a python 3 enviroment, the function to fetch the + album art would not work and throw an exception. It now works as expected. + Additionally, the server will now return a 404 response when the album id + is unknown, instead of a 500 response and a thrown exception. :bug:`2823` For developers: From 4df313e3cea9dea1c82da6360a27a6fd5a1b6e32 Mon Sep 17 00:00:00 2001 From: Waweic Date: Thu, 1 Mar 2018 11:56:38 +0100 Subject: [PATCH 32/48] Fix unicode problems in web plugin Added Exception to the web plugin to catch non latin-1 characters and change them to ascii chars. Added Description to the changelog file --- beetsplug/web/__init__.py | 11 ++++++----- docs/changelog.rst | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index efbc3a99d..c85e6a8c0 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -24,6 +24,7 @@ import flask from flask import g from werkzeug.routing import BaseConverter, PathConverter import os +from unidecode import unidecode import json import base64 @@ -224,12 +225,12 @@ def item_file(item_id): item_path = util.syspath(item.path) else: item_path = util.py3_path(item.path) + try: + os.path.basename(util.py3_path(item.path)).encode("latin-1", "strict") #Imitate http.server behaviour + response = flask.send_file(item_path,as_attachment=True) + except UnicodeEncodeError: + response = flask.send_file(item_path,as_attachment=True, attachment_filename=unidecode(os.path.basename(util.py3_path(item.path)))) - response = flask.send_file( - item_path, - as_attachment=True, - attachment_filename=os.path.basename(util.py3_path(item.path)), - ) response.headers['Content-Length'] = os.path.getsize(item_path) return response diff --git a/docs/changelog.rst b/docs/changelog.rst index 26de4dfaa..2ae9ef074 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -75,6 +75,10 @@ Fixes: album art would not work and throw an exception. It now works as expected. Additionally, the server will now return a 404 response when the album id is unknown, instead of a 500 response and a thrown exception. :bug:`2823` +* :doc:`/plugins/web`: In a python 3 enviroment, the server would throw an + exception if non latin-1 characters where in the File name. + It now checks if non latin-1 characters are in the filename and changes + them to ascii-characters in that case :bug:`2815` For developers: From d0fd41b47484eca2fcf7c271ec35431f13cccd76 Mon Sep 17 00:00:00 2001 From: Waweic Date: Thu, 1 Mar 2018 19:45:44 +0100 Subject: [PATCH 33/48] Add unicode support for Python 2 and 3 Converts bytes to unicode using util.text_string, assuming that the string is a UTF-8 string. If that fails, it falls back to a hardcoded fallback filename. --- beetsplug/web/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index c85e6a8c0..f9512ddc1 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -225,12 +225,19 @@ def item_file(item_id): item_path = util.syspath(item.path) else: item_path = util.py3_path(item.path) - try: - os.path.basename(util.py3_path(item.path)).encode("latin-1", "strict") #Imitate http.server behaviour - response = flask.send_file(item_path,as_attachment=True) - except UnicodeEncodeError: - response = flask.send_file(item_path,as_attachment=True, attachment_filename=unidecode(os.path.basename(util.py3_path(item.path)))) + try: + unicode_item_path = util.text_string(item.path) + except: + unicode_item_path = u"fallback" + os.path.splitext(item_path)[1] + + try: + os.path.basename(unicode_item_path).encode("latin-1", "strict") #Imitate http.server behaviour + safe_filename = os.path.basename(unicode_item_path) + except: + safe_filename = unidecode(os.path.basename(unicode_item_path)) + + response = flask.send_file(item_path,as_attachment=True, attachment_filename=safe_filename) response.headers['Content-Length'] = os.path.getsize(item_path) return response From 3c3e579dccbe20936ad44e5a9f6c043dd496983f Mon Sep 17 00:00:00 2001 From: Waweic Date: Thu, 1 Mar 2018 20:20:38 +0100 Subject: [PATCH 34/48] Make programming style pep8 compliant --- beetsplug/web/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index f9512ddc1..29bb6b27a 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -228,16 +228,21 @@ def item_file(item_id): try: unicode_item_path = util.text_string(item.path) - except: + except (UnicodeDecodeError, UnicodeEncodeError): unicode_item_path = u"fallback" + os.path.splitext(item_path)[1] try: - os.path.basename(unicode_item_path).encode("latin-1", "strict") #Imitate http.server behaviour + # Imitate http.server behaviour + os.path.basename(unicode_item_path).encode("latin-1", "strict") safe_filename = os.path.basename(unicode_item_path) - except: + except (UnicodeDecodeError, UnicodeEncodeError): safe_filename = unidecode(os.path.basename(unicode_item_path)) - response = flask.send_file(item_path,as_attachment=True, attachment_filename=safe_filename) + response = flask.send_file( + item_path, + as_attachment=True, + attachment_filename=safe_filename + ) response.headers['Content-Length'] = os.path.getsize(item_path) return response From 06d4fe254d22a9f5315609f3a0908aef527a5d4c Mon Sep 17 00:00:00 2001 From: Waweic Date: Tue, 6 Mar 2018 17:49:20 +0100 Subject: [PATCH 35/48] Implement recommendations by sampsyo Implemented all recommendations. --- beetsplug/web/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/beetsplug/web/__init__.py b/beetsplug/web/__init__.py index 29bb6b27a..3cf43ed56 100644 --- a/beetsplug/web/__init__.py +++ b/beetsplug/web/__init__.py @@ -229,14 +229,16 @@ def item_file(item_id): try: unicode_item_path = util.text_string(item.path) except (UnicodeDecodeError, UnicodeEncodeError): - unicode_item_path = u"fallback" + os.path.splitext(item_path)[1] + unicode_item_path = util.displayable_path(item.path) + base_filename = os.path.basename(unicode_item_path) try: # Imitate http.server behaviour - os.path.basename(unicode_item_path).encode("latin-1", "strict") - safe_filename = os.path.basename(unicode_item_path) - except (UnicodeDecodeError, UnicodeEncodeError): - safe_filename = unidecode(os.path.basename(unicode_item_path)) + base_filename.encode("latin-1", "strict") + except UnicodeEncodeError: + safe_filename = unidecode(base_filename) + else: + safe_filename = base_filename response = flask.send_file( item_path, From 245cf1a74d2930a393459477907ac54ca4ca6bc8 Mon Sep 17 00:00:00 2001 From: jhermann Date: Sun, 11 Mar 2018 16:02:10 +0100 Subject: [PATCH 36/48] Partial fix for cmd names with dashes (ref #2836) This patch avoids errors during completion when a plugin uses names like "sub-command". It does not make completion fully working for such commands though, thus no close. --- beets/ui/commands.py | 4 ++-- beets/ui/completion_base.sh | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 338bceb83..610ae551e 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1758,7 +1758,7 @@ def completion_script(commands): # Command aliases yield u" local aliases='%s'\n" % ' '.join(aliases.keys()) for alias, cmd in aliases.items(): - yield u" local alias__%s=%s\n" % (alias, cmd) + yield u" local alias__%s=%s\n" % (alias.replace('-', '_'), cmd) yield u'\n' # Fields @@ -1775,7 +1775,7 @@ def completion_script(commands): if option_list: option_list = u' '.join(option_list) yield u" local %s__%s='%s'\n" % ( - option_type, cmd, option_list) + option_type, cmd.replace('-', '_'), option_list) yield u' _beet_dispatch\n' yield u'}\n' diff --git a/beets/ui/completion_base.sh b/beets/ui/completion_base.sh index ce3fb6e27..1eaa4db3d 100644 --- a/beets/ui/completion_base.sh +++ b/beets/ui/completion_base.sh @@ -70,7 +70,7 @@ _beet_dispatch() { # Replace command shortcuts if [[ -n $cmd ]] && _list_include_item "$aliases" "$cmd"; then - eval "cmd=\$alias__$cmd" + eval "cmd=\$alias__${cmd//-/_}" fi case $cmd in @@ -94,8 +94,8 @@ _beet_dispatch() { _beet_complete() { if [[ $cur == -* ]]; then local opts flags completions - eval "opts=\$opts__$cmd" - eval "flags=\$flags__$cmd" + eval "opts=\$opts__${cmd//-/_}" + eval "flags=\$flags__${cmd//-/_}" completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else @@ -129,7 +129,7 @@ _beet_complete_global() { COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) elif [[ -n $cur ]] && _list_include_item "$aliases" "$cur"; then local cmd - eval "cmd=\$alias__$cur" + eval "cmd=\$alias__${cur//-/_}" COMPREPLY+=( "$cmd" ) else COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) @@ -138,7 +138,7 @@ _beet_complete_global() { _beet_complete_query() { local opts - eval "opts=\$opts__$cmd" + eval "opts=\$opts__${cmd//-/_}" if [[ $cur == -* ]] || _list_include_item "$opts" "$prev"; then _beet_complete From 2941833f2227431ea1934da1e1bfb65d57bcf179 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 11 Mar 2018 21:31:56 -0400 Subject: [PATCH 37/48] Changelog for #2837 --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2ae9ef074..b55ec4904 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -79,6 +79,9 @@ Fixes: exception if non latin-1 characters where in the File name. It now checks if non latin-1 characters are in the filename and changes them to ascii-characters in that case :bug:`2815` +* Partially fix bash completion for subcommand names that contain hyphens. + :bug:`2836` :bug:`2837` + Thanks to :user:`jhermann`. For developers: From b9bac391a91e332d7eb2de913cd21dc41fc13cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Schieli?= Date: Sun, 18 Mar 2018 15:06:18 +0100 Subject: [PATCH 38/48] Really fix album replaygain calculation with gstreamer backend. Fixes #2845 --- beetsplug/replaygain.py | 21 ++++++++++----------- docs/changelog.rst | 1 + 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index a7064451a..a7eb81b5c 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -613,16 +613,6 @@ class GStreamerBackend(Backend): self._file = self._files.pop(0) - # Disconnect the decodebin element from the pipeline, set its - # state to READY to to clear it. - self._decbin.unlink(self._conv) - self._decbin.set_state(self.Gst.State.READY) - - # Set a new file on the filesrc element, can only be done in the - # READY state - self._src.set_state(self.Gst.State.READY) - self._src.set_property("location", py3_path(syspath(self._file.path))) - # Ensure the filesrc element received the paused state of the # pipeline in a blocking manner self._src.sync_state_with_parent() @@ -633,9 +623,18 @@ class GStreamerBackend(Backend): self._decbin.sync_state_with_parent() self._decbin.get_state(self.Gst.CLOCK_TIME_NONE) + # Disconnect the decodebin element from the pipeline, set its + # state to READY to to clear it. + self._decbin.unlink(self._conv) + self._decbin.set_state(self.Gst.State.READY) + + # Set a new file on the filesrc element, can only be done in the + # READY state + self._src.set_state(self.Gst.State.READY) + self._src.set_property("location", py3_path(syspath(self._file.path))) + self._decbin.link(self._conv) self._pipe.set_state(self.Gst.State.READY) - self._pipe.set_state(self.Gst.State.PLAYING) return True diff --git a/docs/changelog.rst b/docs/changelog.rst index b55ec4904..4828bcc2d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -82,6 +82,7 @@ Fixes: * Partially fix bash completion for subcommand names that contain hyphens. :bug:`2836` :bug:`2837` Thanks to :user:`jhermann`. +* Really fix album replaygain calculation with gstreamer backend. :bug:`2846` For developers: From 7f7124348ca0eb23b5717c24380563d03e599a4e Mon Sep 17 00:00:00 2001 From: Owen Campbell Date: Thu, 29 Mar 2018 00:22:04 -0700 Subject: [PATCH 39/48] Edit README.rst structure - Add repository title - Add `Features` header - Move installation instructions to dedicated `Install` header --- README.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 8172a1b9b..a0f58cf5e 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +Beets +===== .. image:: http://img.shields.io/pypi/v/beets.svg :target: https://pypi.python.org/pypi/beets @@ -26,6 +28,9 @@ Here's an example of beets' brainy tag corrector doing its thing:: * White Light Generation -> Whitelightgenerator * All the Way -> All the Way... +Features +--------- + Because beets is designed as a library, it can do almost anything you can imagine for your music collection. Via `plugins`_, beets becomes a panacea: @@ -72,17 +77,21 @@ shockingly simple if you know a little Python. .. _MusicBrainz: http://musicbrainz.org/ .. _Beatport: https://www.beatport.com +Install +--------- + +You can install beets by typing ``pip install beets``. Then check out the +`Getting Started`_ guide. + +.. _Getting Started: http://beets.readthedocs.org/page/guides/main.html + Read More --------- Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for news and updates. -You can install beets by typing ``pip install beets``. Then check out the -`Getting Started`_ guide. - .. _its Web site: http://beets.io/ -.. _Getting Started: http://beets.readthedocs.org/page/guides/main.html .. _@b33ts: http://twitter.com/b33ts/ Authors From cba8ae463097cffb0cdff3e414f3edf9387e9a30 Mon Sep 17 00:00:00 2001 From: Owen Campbell Date: Thu, 29 Mar 2018 00:31:52 -0700 Subject: [PATCH 40/48] Add link to wiki in contribution section --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index a0f58cf5e..cb032614e 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,11 @@ You can install beets by typing ``pip install beets``. Then check out the .. _Getting Started: http://beets.readthedocs.org/page/guides/main.html +Contributing +--------- + +Information for developers looking to contribute to beets can be found in the `wiki `_. + Read More --------- From e4f72c92528864e78872ea9375cec8fd78225d4f Mon Sep 17 00:00:00 2001 From: Owen Campbell Date: Thu, 29 Mar 2018 18:13:49 -0700 Subject: [PATCH 41/48] Add feedback - Move header under CI images and convert to lowercase - Remove Features header - Change wording on link to wiki - Add link to Hacking wiki page --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index cb032614e..c7777397c 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -Beets -===== .. image:: http://img.shields.io/pypi/v/beets.svg :target: https://pypi.python.org/pypi/beets @@ -10,6 +8,9 @@ Beets :target: https://travis-ci.org/beetbox/beets +beets +===== + Beets is the media library management system for obsessive-compulsive music geeks. @@ -28,9 +29,6 @@ Here's an example of beets' brainy tag corrector doing its thing:: * White Light Generation -> Whitelightgenerator * All the Way -> All the Way... -Features ---------- - Because beets is designed as a library, it can do almost anything you can imagine for your music collection. Via `plugins`_, beets becomes a panacea: @@ -88,7 +86,9 @@ You can install beets by typing ``pip install beets``. Then check out the Contributing --------- -Information for developers looking to contribute to beets can be found in the `wiki `_. +Developers can consult the technical documentation in the `wiki `_. + +To learn how to contribute, read the `Hacking page `_. Read More --------- From a60c40f930de0d5184794f4895eaf0717041d1a6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 29 Mar 2018 22:11:03 -0400 Subject: [PATCH 42/48] Refine the contributing section --- README.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index c7777397c..24e9d57d2 100644 --- a/README.rst +++ b/README.rst @@ -76,19 +76,20 @@ shockingly simple if you know a little Python. .. _Beatport: https://www.beatport.com Install ---------- +------- You can install beets by typing ``pip install beets``. Then check out the `Getting Started`_ guide. .. _Getting Started: http://beets.readthedocs.org/page/guides/main.html -Contributing ---------- +Contribute +---------- -Developers can consult the technical documentation in the `wiki `_. +Check out the `Hacking`_ page on the wiki for tips on how to help out. +You might also be interested in the `For Developers`_ section in the docs. -To learn how to contribute, read the `Hacking page `_. +.. _For Developers: http://docs.beets.io/page/dev/ Read More --------- From eb31d0d2072fcc4409ff42a6a0af7d175d558300 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 29 Mar 2018 22:11:46 -0400 Subject: [PATCH 43/48] My new URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 24e9d57d2..28219b6c4 100644 --- a/README.rst +++ b/README.rst @@ -107,4 +107,4 @@ Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help, please contact the `mailing list`_. .. _mailing list: https://groups.google.com/forum/#!forum/beets-users -.. _Adrian Sampson: http://homes.cs.washington.edu/~asampson/ +.. _Adrian Sampson: http://www.cs.cornell.edu/~asampson/ From b0fa39fc93e157f55fba8d5d9446645849455e2a Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 29 Mar 2018 22:13:02 -0400 Subject: [PATCH 44/48] Link to Discourse instead of Google Groups --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 28219b6c4..2d26cef3e 100644 --- a/README.rst +++ b/README.rst @@ -104,7 +104,7 @@ Authors ------- Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help, -please contact the `mailing list`_. +please visit our `forum`_. -.. _mailing list: https://groups.google.com/forum/#!forum/beets-users +.. _forum: https://discourse.beets.io .. _Adrian Sampson: http://www.cs.cornell.edu/~asampson/ From 7b65da3e51e5fe07c8e0333da8d5b90b758c69ce Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 29 Mar 2018 22:14:08 -0400 Subject: [PATCH 45/48] Fix missing link --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 2d26cef3e..a3ea6302f 100644 --- a/README.rst +++ b/README.rst @@ -89,6 +89,7 @@ Contribute Check out the `Hacking`_ page on the wiki for tips on how to help out. You might also be interested in the `For Developers`_ section in the docs. +.. _Hacking: https://github.com/beetbox/beets/wiki/Hacking .. _For Developers: http://docs.beets.io/page/dev/ Read More From feca2b9ffb50054b53ec9d870c7a07c7a192cb50 Mon Sep 17 00:00:00 2001 From: Laurent Kislaire Date: Sun, 1 Apr 2018 20:27:52 +0200 Subject: [PATCH 46/48] Plugins doc update Sort plugins within each section. Section "other plugins" kept as is. --- docs/plugins/index.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index e37a270fa..b17c4db98 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -54,10 +54,11 @@ like this:: embyupdate export fetchart + filefilter + freedesktop fromfilename ftintitle fuzzy - freedesktop gmusic hook ihate @@ -82,7 +83,6 @@ like this:: play plexupdate random - filefilter replaygain rewrite scrub @@ -147,6 +147,7 @@ Path Formats Interoperability ---------------- +* :doc:`badfiles`: Check audio file integrity. * :doc:`embyupdate`: Automatically notifies `Emby`_ whenever the beets library changes. * :doc:`importfeeds`: Keep track of imported files via ``.m3u`` playlist file(s) or symlinks. * :doc:`ipfs`: Import libraries from friends and get albums from them via ipfs. @@ -159,7 +160,6 @@ Interoperability changes. * :doc:`smartplaylist`: Generate smart playlists based on beets queries. * :doc:`thumbnails`: Get thumbnails with the cover art on your album folders. -* :doc:`badfiles`: Check audio file integrity. .. _Emby: http://emby.media @@ -175,6 +175,8 @@ Miscellaneous a different directory. * :doc:`duplicates`: List duplicate tracks or albums. * :doc:`export`: Export data from queries to a format. +* :doc:`filefilter`: Automatically skip files during the import process based + on regular expressions. * :doc:`fuzzy`: Search albums and tracks with fuzzy string matching. * :doc:`gmusic`: Search and upload files to Google Play Music. * :doc:`hook`: Run a command when an event is emitted by beets. @@ -184,8 +186,6 @@ Miscellaneous * :doc:`mbsubmit`: Print an album's tracks in a MusicBrainz-friendly format. * :doc:`missing`: List missing tracks. * :doc:`random`: Randomly choose albums and tracks from your library. -* :doc:`filefilter`: Automatically skip files during the import process based - on regular expressions. * :doc:`spotify`: Create Spotify playlists from the Beets library. * :doc:`types`: Declare types for flexible attributes. * :doc:`web`: An experimental Web-based GUI for beets. From a829fa348f203706fd4d1852d56385402a6354ae Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 4 Apr 2018 10:41:38 -0400 Subject: [PATCH 47/48] Fix #2863: nonexistent identical paths are equal --- beets/util/__init__.py | 2 ++ docs/changelog.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index db341a646..69870edf2 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -422,6 +422,8 @@ def syspath(path, prefix=True): def samefile(p1, p2): """Safer equality for paths.""" + if p1 == p2: + return True return shutil._samefile(syspath(p1), syspath(p2)) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4828bcc2d..aa930904a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -83,6 +83,8 @@ Fixes: :bug:`2836` :bug:`2837` Thanks to :user:`jhermann`. * Really fix album replaygain calculation with gstreamer backend. :bug:`2846` +* Avoid an error when doing a "no-op" move on non-existent files (i.e., moving + a file onto itself). :bug:`2863` For developers: From 0202d762bf51d0861a2f9970fa6f5df3d807c2d2 Mon Sep 17 00:00:00 2001 From: Denis Defreyne Date: Sat, 7 Apr 2018 22:20:48 +0200 Subject: [PATCH 48/48] Add artist_credit config option --- beets/autotag/__init__.py | 17 ++++++++++++----- beets/config_default.yaml | 1 + docs/changelog.rst | 7 +++++-- docs/reference/config.rst | 9 +++++++++ test/test_autotag.py | 21 ++++++++++++++++++++- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/beets/autotag/__init__.py b/beets/autotag/__init__.py index 4c5f09eb4..09564f49d 100644 --- a/beets/autotag/__init__.py +++ b/beets/autotag/__init__.py @@ -63,12 +63,19 @@ def apply_metadata(album_info, mapping): mapping from Items to TrackInfo objects. """ for item, track_info in mapping.items(): - # Album, artist, track count. - if track_info.artist: - item.artist = track_info.artist + # Artist or artist credit. + if config['artist_credit']: + item.artist = (track_info.artist_credit or + track_info.artist or + album_info.artist_credit or + album_info.artist) + item.albumartist = (album_info.artist_credit or + album_info.artist) else: - item.artist = album_info.artist - item.albumartist = album_info.artist + item.artist = (track_info.artist or album_info.artist) + item.albumartist = album_info.artist + + # Album. item.album = album_info.album # Artist sort and credit names. diff --git a/beets/config_default.yaml b/beets/config_default.yaml index 27aa3d4ca..273f94235 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -56,6 +56,7 @@ per_disc_numbering: no verbose: 0 terminal_encoding: original_date: no +artist_credit: no id3v23: no va_name: "Various Artists" diff --git a/docs/changelog.rst b/docs/changelog.rst index aa930904a..dd04b1d71 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,9 @@ New features: * :doc:`/plugins/web`: added the boolean ``cors_supports_credentials`` option to allow in-browser clients to login to the beet web server even when it is protected by an authorization mechanism. +* A new importer configuration ``artist_credit`` will tell beets to prefer the + artist credit over the artist when autotagging. + :bug:`1249` Fixes: @@ -75,9 +78,9 @@ Fixes: album art would not work and throw an exception. It now works as expected. Additionally, the server will now return a 404 response when the album id is unknown, instead of a 500 response and a thrown exception. :bug:`2823` -* :doc:`/plugins/web`: In a python 3 enviroment, the server would throw an +* :doc:`/plugins/web`: In a python 3 enviroment, the server would throw an exception if non latin-1 characters where in the File name. - It now checks if non latin-1 characters are in the filename and changes + It now checks if non latin-1 characters are in the filename and changes them to ascii-characters in that case :bug:`2815` * Partially fix bash completion for subcommand names that contain hyphens. :bug:`2836` :bug:`2837` diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 24ed1340d..94dab4345 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -253,6 +253,15 @@ Either ``yes`` or ``no``, indicating whether matched albums should have their That is, if this option is turned on, then ``year`` will always equal ``original_year`` and so on. Default: ``no``. +.. _artist_credit: + +artist_credit +~~~~~~~~~~~~~ + +Either ``yes`` or ``no``, indicating whether matched tracks and albums should +use the artist credit, rather than the artist. That is, if this option is turned +on, then ``artist`` will contain the artist as credited on the release. + .. _per_disc_numbering: per_disc_numbering diff --git a/test/test_autotag.py b/test/test_autotag.py index 6f107afa4..932616be1 100644 --- a/test/test_autotag.py +++ b/test/test_autotag.py @@ -616,12 +616,13 @@ class AssignmentTest(unittest.TestCase): class ApplyTestUtil(object): - def _apply(self, info=None, per_disc_numbering=False): + def _apply(self, info=None, per_disc_numbering=False, artist_credit=False): info = info or self.info mapping = {} for i, t in zip(self.items, info.tracks): mapping[i] = t config['per_disc_numbering'] = per_disc_numbering + config['artist_credit'] = artist_credit autotag.apply_metadata(info, mapping) @@ -706,6 +707,24 @@ class ApplyTest(_common.TestCase, ApplyTestUtil): self.assertEqual(self.items[0].tracktotal, 1) self.assertEqual(self.items[1].tracktotal, 1) + def test_artist_credit(self): + self._apply(artist_credit=True) + self.assertEqual(self.items[0].artist, 'trackArtistCredit') + self.assertEqual(self.items[1].artist, 'albumArtistCredit') + self.assertEqual(self.items[0].albumartist, 'albumArtistCredit') + self.assertEqual(self.items[1].albumartist, 'albumArtistCredit') + + def test_artist_credit_prefers_artist_over_albumartist_credit(self): + self.info.tracks[0].artist = 'oldArtist' + self.info.tracks[0].artist_credit = None + self._apply(artist_credit=True) + self.assertEqual(self.items[0].artist, 'oldArtist') + + def test_artist_credit_falls_back_to_albumartist(self): + self.info.artist_credit = None + self._apply(artist_credit=True) + self.assertEqual(self.items[1].artist, 'artistNew') + def test_mb_trackid_applied(self): self._apply() self.assertEqual(self.items[0].mb_trackid,