From 2293e1e09d384b2e289f95efd18dc9ec364e2790 Mon Sep 17 00:00:00 2001 From: kooimens Date: Tue, 3 Nov 2015 20:03:24 +0100 Subject: [PATCH 01/39] Discogs: option to change 'various' album artist to 'Various Artists' (Musicbrainz naming) Just a simple config option to change 'various' album artist to the one that MusicBrainz uses: Various Artists. --- beetsplug/discogs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 904220685..4afdcd6fe 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -57,6 +57,7 @@ class DiscogsPlugin(BeetsPlugin): 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, + 'change_va': False, }) self.config['apikey'].redact = True self.config['apisecret'].redact = True @@ -224,6 +225,8 @@ class DiscogsPlugin(BeetsPlugin): albumtype = ', '.join( result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' + if va and self.config['change_va']: + artist = 'Various Artists' year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) From da0360bd902f9418f27adfeb8dbb032f9626643a Mon Sep 17 00:00:00 2001 From: kooimens Date: Thu, 5 Nov 2015 14:50:40 +0100 Subject: [PATCH 02/39] Update discogs.py --- beetsplug/discogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 4afdcd6fe..8d0f12e44 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -57,7 +57,7 @@ class DiscogsPlugin(BeetsPlugin): 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, - 'change_va': False, + 'va_name': 'Various Artists', }) self.config['apikey'].redact = True self.config['apisecret'].redact = True @@ -225,8 +225,8 @@ class DiscogsPlugin(BeetsPlugin): albumtype = ', '.join( result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' - if va and self.config['change_va']: - artist = 'Various Artists' + if va: + artist = self.config['va_name'].get() year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) From 1708939f99c6706a7854ff45f83e6054ba0eba04 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Fri, 22 May 2015 04:19:50 +0200 Subject: [PATCH 03/39] - Added hooks to manipulate fetched album and track info - Integrated support for plugins - Added documentation - Updated changelog --- beets/autotag/hooks.py | 28 ++++++++++++++++++++++++---- docs/changelog.rst | 4 ++++ docs/dev/plugins.rst | 16 ++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 5c4ce082e..abb474dcd 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -511,7 +511,10 @@ def album_for_mbid(release_id): if the ID is not found. """ try: - return mb.album_for_id(release_id) + album = mb.album_for_id(release_id) + if album: + plugins.send('albuminfo_received', info=album) + return album except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -521,7 +524,10 @@ def track_for_mbid(recording_id): if the ID is not found. """ try: - return mb.track_for_id(recording_id) + track = mb.track_for_id(recording_id) + if track: + plugins.send('trackinfo_received', info=track) + return track except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -529,14 +535,20 @@ def track_for_mbid(recording_id): def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] - candidates.extend(plugins.album_for_id(album_id)) + plugin_albums = plugins.album_for_id(album_id) + for a in plugin_albums: + plugins.send('trackinfo_received', info=a) + candidates.extend(plugin_albums) return filter(None, candidates) def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] - candidates.extend(plugins.track_for_id(track_id)) + plugin_tracks = plugins.track_for_id(track_id) + for t in plugin_tracks: + plugins.send('trackinfo_received', info=t) + candidates.extend(plugin_tracks) return filter(None, candidates) @@ -566,6 +578,10 @@ def album_candidates(items, artist, album, va_likely): # Candidates from plugins. out.extend(plugins.candidates(items, artist, album, va_likely)) + # Notify subscribed plugins about fetched album info + for a in out: + plugins.send('albuminfo_received', info=a) + return out @@ -586,4 +602,8 @@ def item_candidates(item, artist, title): # Plugin candidates. out.extend(plugins.item_candidates(item, artist, title)) + # Notify subscribed plugins about fetched album info + for i in out: + plugins.send('trackinfo_received', info=i) + return out diff --git a/docs/changelog.rst b/docs/changelog.rst index ac4202561..1b270ce4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -125,6 +125,10 @@ The new features: :bug:`1104` :bug:`1493` * :doc:`/plugins/plexupdate`: A new ``token`` configuration option lets you specify a key for Plex Home setups. Thanks to :user:`edcarroll`. :bug:`1494` +* :doc:`/dev/plugins`: New hooks ``albuminfo_received`` and + ``trackinfo_received`` have been added for developers who would like to + intercept fetched meta data, before they are applied in tag manipulation + operations. :bug:`872` Fixes: diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 798b6894c..59936725e 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -214,6 +214,22 @@ The events currently available are: * *import_begin*: called just before a ``beet import`` session starts up. Parameter: ``session``. +* *trackinfo_received*: called after meta data for a track item has been fetched + from disparate sources, such as MusicBrainz. Gives a developer the option to + intercept the fetched TrackInfo object. Can be used to modify tags on a ``beet + import`` operation or during later adjustments, such as ``mbsync``. Can be + slow, as event is fired for any fetched possible match *before* user or + autotagger selection was made. + Parameter: ``info``. + +* *albuminfo_received*: called after meta data for an album item has been + fetched from disparate sources, such as MusicBrainz. Gives a developer the + option to intercept the fetched AlbumInfo object. Can be used to modify tags + on a ``beet import`` operation or during later adjustments, such as + ``mbsync``. Can be slow, as event is fired for any fetched possible match + *before* user or autotagger selection was made. + Parameter: ``info``. + The included ``mpdupdate`` plugin provides an example use case for event listeners. Extend the Autotagger From ae0babb17e136a2abe9e596b5b895bf601827dd3 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Thu, 5 Nov 2015 16:17:19 +0100 Subject: [PATCH 04/39] Less duplicated text in documentation --- docs/dev/plugins.rst | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 59936725e..8c6e45c6d 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -222,13 +222,9 @@ The events currently available are: autotagger selection was made. Parameter: ``info``. -* *albuminfo_received*: called after meta data for an album item has been - fetched from disparate sources, such as MusicBrainz. Gives a developer the - option to intercept the fetched AlbumInfo object. Can be used to modify tags - on a ``beet import`` operation or during later adjustments, such as - ``mbsync``. Can be slow, as event is fired for any fetched possible match - *before* user or autotagger selection was made. - Parameter: ``info``. +* *albuminfo_received*: Like *trackinfo_received*, the event indicates new meta + data for album items, but supplies an *AlbumInfo* object instead of a + *TrackInfo*. The included ``mpdupdate`` plugin provides an example use case for event listeners. From 5e4c41ffe82bc0fed8cae19d25486594e82f7593 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Thu, 5 Nov 2015 16:37:35 +0100 Subject: [PATCH 05/39] Clarified documentation --- docs/dev/plugins.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 8c6e45c6d..f63d75aba 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -217,9 +217,9 @@ The events currently available are: * *trackinfo_received*: called after meta data for a track item has been fetched from disparate sources, such as MusicBrainz. Gives a developer the option to intercept the fetched TrackInfo object. Can be used to modify tags on a ``beet - import`` operation or during later adjustments, such as ``mbsync``. Can be - slow, as event is fired for any fetched possible match *before* user or - autotagger selection was made. + import`` operation or during later adjustments, such as ``mbsync``. Slow + handlers of the event can impact the operation, since the event is fired for + any fetched possible match *before* user or autotagger selection was made. Parameter: ``info``. * *albuminfo_received*: Like *trackinfo_received*, the event indicates new meta From 17b38a6be859f9021312159de6ddbed5a123730e Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Thu, 5 Nov 2015 16:43:28 +0100 Subject: [PATCH 06/39] Minor layout fixes to documentation --- docs/dev/plugins.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index f63d75aba..9a59351e3 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -216,15 +216,16 @@ The events currently available are: * *trackinfo_received*: called after meta data for a track item has been fetched from disparate sources, such as MusicBrainz. Gives a developer the option to - intercept the fetched TrackInfo object. Can be used to modify tags on a ``beet - import`` operation or during later adjustments, such as ``mbsync``. Slow - handlers of the event can impact the operation, since the event is fired for - any fetched possible match *before* user or autotagger selection was made. + intercept the fetched ``TrackInfo`` object. Can be used to modify tags on a + ``beet import`` operation or during later adjustments, such as ``mbsync``. + Slow handlers of the event can impact the operation, since the event is fired + for any fetched possible match *before* user or autotagger selection was made. Parameter: ``info``. * *albuminfo_received*: Like *trackinfo_received*, the event indicates new meta - data for album items, but supplies an *AlbumInfo* object instead of a - *TrackInfo*. + data for album items, but supplies an ``AlbumInfo`` object instead of a + ``TrackInfo``. + Parameter: ``info``. The included ``mpdupdate`` plugin provides an example use case for event listeners. From 33d3ebf53ff2134f02f20045e5c211df272f064b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Thu, 5 Nov 2015 18:31:16 -0800 Subject: [PATCH 07/39] Link to a 3rd-party plugin: beets-usertag (#1694) --- docs/plugins/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 0c95f366f..529a74da5 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -215,6 +215,8 @@ Here are a few of the plugins written by the beets community: * `whatlastgenre`_ fetches genres from various music sites. +* `beets-usertag`_ lets you use keywords to tag and organize your music. + .. _beets-check: https://github.com/geigerzaehler/beets-check .. _copyartifacts: https://github.com/sbarakat/beets-copyartifacts .. _dsedivec: https://github.com/dsedivec/beets-plugins @@ -231,3 +233,4 @@ Here are a few of the plugins written by the beets community: .. _beets-setlister: https://github.com/tomjaspers/beets-setlister .. _beets-noimport: https://github.com/ttsda/beets-noimport .. _whatlastgenre: https://github.com/YetAnotherNerd/whatlastgenre/tree/master/plugin/beets +.. _beets-usertag: https://github.com/igordertigor/beets-usertag From d84c776dc18ccbdd8e21028edf8a697360aa29ed Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Fri, 6 Nov 2015 13:44:38 +0100 Subject: [PATCH 08/39] list options in alphabetic order --- docs/plugins/duplicates.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/duplicates.rst b/docs/plugins/duplicates.rst index a0369a95e..4d8b35376 100644 --- a/docs/plugins/duplicates.rst +++ b/docs/plugins/duplicates.rst @@ -67,9 +67,6 @@ file. The available options mirror the command-line options: - **full**: List every track or album that has duplicates, not just the duplicates themselves. Default: ``no`` -- **strict**: Do not report duplicate matches if some of the - attributes are not defined (ie. null or empty). - Default: ``no`` - **keys**: Define in which track or album fields duplicates are to be searched. By default, the plugin uses the musicbrainz track and album IDs for this purpose. Using the ``keys`` option (as a YAML list in the configuration @@ -83,6 +80,9 @@ file. The available options mirror the command-line options: Default: none (disabled). - **path**: Output the path instead of metadata when listing duplicates. Default: ``no``. +- **strict**: Do not report duplicate matches if some of the + attributes are not defined (ie. null or empty). + Default: ``no`` - **tag**: A ``key=value`` pair. The plugin will add a new ``key`` attribute with ``value`` value as a flexattr to the database for duplicate items. Default: ``no``. From 414ae131a5c0c1656abcdf1af20a84edac633888 Mon Sep 17 00:00:00 2001 From: kooimens Date: Fri, 6 Nov 2015 19:27:36 +0100 Subject: [PATCH 09/39] Fix style error --- beetsplug/discogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 8d0f12e44..78a5ee4aa 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -226,7 +226,7 @@ class DiscogsPlugin(BeetsPlugin): result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' if va: - artist = self.config['va_name'].get() + artist = self.config['va_name'].get() year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) From b11533b98f69349dfeeb23d7294cff031aed44a7 Mon Sep 17 00:00:00 2001 From: kooimens Date: Fri, 6 Nov 2015 19:45:05 +0100 Subject: [PATCH 10/39] Update discogs docs --- docs/plugins/discogs.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 038718f9b..2961bb3bb 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -25,6 +25,14 @@ MusicBrainz. If you have a Discogs ID for an album you want to tag, you can also enter it at the "enter Id" prompt in the importer. +Configuration +------------- +To configure the plugin, make a ``discogs:`` section in your configuration +file. The available options are: + +- **va_name**: Change the name of the albumartist if an album is V.A. + Default: ``'Various Artists'`` (Same as MusicBrainz). + Troubleshooting --------------- From c20479515373fbd0f899be635b88c5ce84568479 Mon Sep 17 00:00:00 2001 From: kooimens Date: Fri, 6 Nov 2015 19:56:19 +0100 Subject: [PATCH 11/39] Update changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7c5b91217..a522b4ce3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,9 @@ Fixes: * :doc:`/plugins/thumbnails`: Fix a crash with Unicode paths. :bug:`1686` * :doc:`/plugins/embedart`: The ``remove_art_file`` option now works on import (as well as with the explicit command). :bug:`1662` :bug:`1675` +* :doc:`/plugins/discogs`: New config option to change the default album + artist name if album is V.A. The default is changed to 'Various Artists' to + be the same as for MusicBrainz. 1.3.15 (October 17, 2015) From 7d02d7da29c6ea27234860a2490e1c1635a76817 Mon Sep 17 00:00:00 2001 From: kooimens Date: Fri, 6 Nov 2015 20:00:36 +0100 Subject: [PATCH 12/39] Update changelog.rst --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a522b4ce3..1257c9132 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,9 +24,9 @@ Fixes: * :doc:`/plugins/thumbnails`: Fix a crash with Unicode paths. :bug:`1686` * :doc:`/plugins/embedart`: The ``remove_art_file`` option now works on import (as well as with the explicit command). :bug:`1662` :bug:`1675` -* :doc:`/plugins/discogs`: New config option to change the default album - artist name if album is V.A. The default is changed to 'Various Artists' to - be the same as for MusicBrainz. +* :doc:`/plugins/discogs`: New config option (va_name) to change the default + album artist name if album is V.A. The default is changed to 'Various Artists' + to be the same as for MusicBrainz. 1.3.15 (October 17, 2015) From 1150f65ee3752c73e6ccbda4f2b1465c413ed9b5 Mon Sep 17 00:00:00 2001 From: kooimens Date: Fri, 6 Nov 2015 20:01:56 +0100 Subject: [PATCH 13/39] Update changelog.rst --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1257c9132..84b79b723 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,7 +26,7 @@ Fixes: (as well as with the explicit command). :bug:`1662` :bug:`1675` * :doc:`/plugins/discogs`: New config option (va_name) to change the default album artist name if album is V.A. The default is changed to 'Various Artists' - to be the same as for MusicBrainz. + to be the same as MusicBrainz. 1.3.15 (October 17, 2015) From 1f6a26d2b4aae907eb78d9a44e6abd9bd8f3e421 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 6 Nov 2015 13:04:06 -0800 Subject: [PATCH 14/39] Spruce up the docs for #1692 --- docs/changelog.rst | 6 +++--- docs/plugins/discogs.rst | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c85644062..c04f246ff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,9 +31,9 @@ Fixes: * :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of iTunes. :bug:`1700` * :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699` -* :doc:`/plugins/discogs`: New config option (va_name) to change the default - album artist name if album is V.A. The default is changed to 'Various Artists' - to be the same as MusicBrainz. +* :doc:`/plugins/discogs`: A new option, ``va_name``, controls the album + artist name for various-artists albums. The default is now "Various + Artists," to match MusicBrainz. 1.3.15 (October 17, 2015) diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 2961bb3bb..a7983e7e0 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -27,11 +27,13 @@ at the "enter Id" prompt in the importer. Configuration ------------- + To configure the plugin, make a ``discogs:`` section in your configuration file. The available options are: -- **va_name**: Change the name of the albumartist if an album is V.A. - Default: ``'Various Artists'`` (Same as MusicBrainz). +- **va_name**: The albumartist name to use when an album is marked as being by + "various" artists. + Default: "Various Artists" (matching the MusicBrainz convention). Troubleshooting --------------- From e9ddb92c2d601276557d1a1d7b2ea74d1426ee87 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Thu, 5 Nov 2015 17:09:33 +0100 Subject: [PATCH 15/39] - Simplified code - Fixed wrong event being emitted --- beets/autotag/hooks.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index abb474dcd..d53961a6c 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -511,10 +511,7 @@ def album_for_mbid(release_id): if the ID is not found. """ try: - album = mb.album_for_id(release_id) - if album: - plugins.send('albuminfo_received', info=album) - return album + return mb.album_for_id(release_id) except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -524,10 +521,7 @@ def track_for_mbid(recording_id): if the ID is not found. """ try: - track = mb.track_for_id(recording_id) - if track: - plugins.send('trackinfo_received', info=track) - return track + return mb.track_for_id(recording_id) except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -535,20 +529,18 @@ def track_for_mbid(recording_id): def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] - plugin_albums = plugins.album_for_id(album_id) - for a in plugin_albums: - plugins.send('trackinfo_received', info=a) - candidates.extend(plugin_albums) + candidates.extend(plugins.album_for_id(album_id)) + for a in candidates: + plugins.send('albuminfo_received', info=a) return filter(None, candidates) def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] - plugin_tracks = plugins.track_for_id(track_id) - for t in plugin_tracks: + candidates.extend(plugins.track_for_id(track_id)) + for t in candidates: plugins.send('trackinfo_received', info=t) - candidates.extend(plugin_tracks) return filter(None, candidates) @@ -578,10 +570,6 @@ def album_candidates(items, artist, album, va_likely): # Candidates from plugins. out.extend(plugins.candidates(items, artist, album, va_likely)) - # Notify subscribed plugins about fetched album info - for a in out: - plugins.send('albuminfo_received', info=a) - return out @@ -602,8 +590,4 @@ def item_candidates(item, artist, title): # Plugin candidates. out.extend(plugins.item_candidates(item, artist, title)) - # Notify subscribed plugins about fetched album info - for i in out: - plugins.send('trackinfo_received', info=i) - return out From b2efd60162eb81f025c015df2160b4099dc957ad Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sat, 7 Nov 2015 17:51:44 +0100 Subject: [PATCH 16/39] Needed to find new spot to emit events because hooks.*_for_id -methods are only called when searching for explicit IDs --- beets/autotag/hooks.py | 4 ---- beets/autotag/match.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index d53961a6c..5c4ce082e 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -530,8 +530,6 @@ def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] candidates.extend(plugins.album_for_id(album_id)) - for a in candidates: - plugins.send('albuminfo_received', info=a) return filter(None, candidates) @@ -539,8 +537,6 @@ def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] candidates.extend(plugins.track_for_id(track_id)) - for t in candidates: - plugins.send('trackinfo_received', info=t) return filter(None, candidates) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index c747f47c2..bd15368d2 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -350,6 +350,10 @@ def _add_candidate(items, results, info): log.debug(u'Ignored. Missing required tag: {0}', req_tag) return + # Notify subscribed plugins about fetched album info and let them perform + # their manipulations + plugins.send('albuminfo_received', info=info) + # Find mapping between the items and the track info. mapping, extra_items, extra_tracks = assign_items(items, info.tracks) @@ -459,6 +463,10 @@ def tag_item(item, search_artist=None, search_title=None, if trackid: log.debug(u'Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): + # Notify subscribed plugins about fetched track info and let them perform + # their manipulations + plugins.send('trackinfo_received', info=track_info) + dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ hooks.TrackMatch(dist, track_info) @@ -482,6 +490,10 @@ def tag_item(item, search_artist=None, search_title=None, # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): + # Notify subscribed plugins about fetched track info and let them perform + # their manipulations + plugins.send('trackinfo_received', info=track_info) + dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) From d8851b97b8a039dc4974974583652131a1e77d74 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sat, 7 Nov 2015 17:59:25 +0100 Subject: [PATCH 17/39] Fixed Travis errors --- beets/autotag/match.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beets/autotag/match.py b/beets/autotag/match.py index bd15368d2..a892d48cc 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -463,8 +463,8 @@ def tag_item(item, search_artist=None, search_title=None, if trackid: log.debug(u'Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): - # Notify subscribed plugins about fetched track info and let them perform - # their manipulations + # Notify subscribed plugins about fetched track info and let them + # perform their manipulations plugins.send('trackinfo_received', info=track_info) dist = track_distance(item, track_info, incl_artist=True) @@ -490,8 +490,8 @@ def tag_item(item, search_artist=None, search_title=None, # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): - # Notify subscribed plugins about fetched track info and let them perform - # their manipulations + # Notify subscribed plugins about fetched track info and let them + # perform their manipulations plugins.send('trackinfo_received', info=track_info) dist = track_distance(item, track_info, incl_artist=True) From 6e980a977ec03dc46fb5fb9036666d9677ecf22e Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sat, 7 Nov 2015 19:02:08 +0100 Subject: [PATCH 18/39] Reverted to original approach --- beets/autotag/hooks.py | 28 ++++++++++++++++++++++++---- beets/autotag/match.py | 12 ------------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 5c4ce082e..b2b635ad2 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -511,7 +511,10 @@ def album_for_mbid(release_id): if the ID is not found. """ try: - return mb.album_for_id(release_id) + album = mb.album_for_id(release_id) + if album: + plugins.send('albuminfo_received', info=album) + return album except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -521,7 +524,10 @@ def track_for_mbid(recording_id): if the ID is not found. """ try: - return mb.track_for_id(recording_id) + track = mb.track_for_id(recording_id) + if track: + plugins.send('trackinfo_received', info=track) + return track except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -529,14 +535,20 @@ def track_for_mbid(recording_id): def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] - candidates.extend(plugins.album_for_id(album_id)) + plugin_albums = plugins.album_for_id(album_id) + for a in plugin_albums: + plugins.send('albuminfo_received', info=a) + candidates.extend(plugin_albums) return filter(None, candidates) def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] - candidates.extend(plugins.track_for_id(track_id)) + plugin_tracks = plugins.track_for_id(track_id) + for t in plugin_tracks: + plugins.send('trackinfo_received', info=t) + candidates.extend(plugin_tracks) return filter(None, candidates) @@ -566,6 +578,10 @@ def album_candidates(items, artist, album, va_likely): # Candidates from plugins. out.extend(plugins.candidates(items, artist, album, va_likely)) + # Notify subscribed plugins about fetched album info + for a in out: + plugins.send('albuminfo_received', info=a) + return out @@ -586,4 +602,8 @@ def item_candidates(item, artist, title): # Plugin candidates. out.extend(plugins.item_candidates(item, artist, title)) + # Notify subscribed plugins about fetched track info + for i in out: + plugins.send('trackinfo_received', info=i) + return out diff --git a/beets/autotag/match.py b/beets/autotag/match.py index a892d48cc..c747f47c2 100644 --- a/beets/autotag/match.py +++ b/beets/autotag/match.py @@ -350,10 +350,6 @@ def _add_candidate(items, results, info): log.debug(u'Ignored. Missing required tag: {0}', req_tag) return - # Notify subscribed plugins about fetched album info and let them perform - # their manipulations - plugins.send('albuminfo_received', info=info) - # Find mapping between the items and the track info. mapping, extra_items, extra_tracks = assign_items(items, info.tracks) @@ -463,10 +459,6 @@ def tag_item(item, search_artist=None, search_title=None, if trackid: log.debug(u'Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): - # Notify subscribed plugins about fetched track info and let them - # perform their manipulations - plugins.send('trackinfo_received', info=track_info) - dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ hooks.TrackMatch(dist, track_info) @@ -490,10 +482,6 @@ def tag_item(item, search_artist=None, search_title=None, # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): - # Notify subscribed plugins about fetched track info and let them - # perform their manipulations - plugins.send('trackinfo_received', info=track_info) - dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) From 4baf9eba564f89ce24904b64a71df3543eda1d02 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 12:07:17 -0800 Subject: [PATCH 19/39] Move changelog entry for #1499 --- docs/changelog.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 944307246..fce5d6552 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,13 @@ Changelog 1.3.16 (in development) ----------------------- +For developers: + +* :doc:`/dev/plugins`: New hooks ``albuminfo_received`` and + ``trackinfo_received`` have been added for developers who would like to + intercept fetched meta data, before they are applied in tag manipulation + operations. :bug:`872` + Fixes: * :doc:`/plugins/plexupdate`: Fix a crash when Plex libraries use non-ASCII @@ -128,10 +135,6 @@ The new features: :bug:`1104` :bug:`1493` * :doc:`/plugins/plexupdate`: A new ``token`` configuration option lets you specify a key for Plex Home setups. Thanks to :user:`edcarroll`. :bug:`1494` -* :doc:`/dev/plugins`: New hooks ``albuminfo_received`` and - ``trackinfo_received`` have been added for developers who would like to - intercept fetched meta data, before they are applied in tag manipulation - operations. :bug:`872` Fixes: From 4e20ddcef9971b984b00d79e6c719e12515fb537 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 12:11:05 -0800 Subject: [PATCH 20/39] Doc refinements for #1499 --- docs/changelog.rst | 7 +++---- docs/dev/plugins.rst | 18 +++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fce5d6552..5864d5c38 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,10 +6,9 @@ Changelog For developers: -* :doc:`/dev/plugins`: New hooks ``albuminfo_received`` and - ``trackinfo_received`` have been added for developers who would like to - intercept fetched meta data, before they are applied in tag manipulation - operations. :bug:`872` +* :doc:`/dev/plugins`: Two new hooks, ``albuminfo_received`` and + ``trackinfo_received``, let plugins intercept metadata as soon as it is + received, before it is applied to music in the database. :bug:`872` Fixes: diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index 9a59351e3..bf5f3d4a9 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -214,17 +214,17 @@ The events currently available are: * *import_begin*: called just before a ``beet import`` session starts up. Parameter: ``session``. -* *trackinfo_received*: called after meta data for a track item has been fetched - from disparate sources, such as MusicBrainz. Gives a developer the option to - intercept the fetched ``TrackInfo`` object. Can be used to modify tags on a - ``beet import`` operation or during later adjustments, such as ``mbsync``. - Slow handlers of the event can impact the operation, since the event is fired - for any fetched possible match *before* user or autotagger selection was made. +* *trackinfo_received*: called after metadata for a track item has been + fetched from a data source, such as MusicBrainz. You can modify the tags + that the rest of the pipeline sees on a ``beet import`` operation or during + later adjustments, such as ``mbsync``. Slow handlers of the event can impact + the operation, since the event is fired for any fetched possible match + *before* the user (or the autotagger machinery) gets to see the match. Parameter: ``info``. -* *albuminfo_received*: Like *trackinfo_received*, the event indicates new meta - data for album items, but supplies an ``AlbumInfo`` object instead of a - ``TrackInfo``. +* *albuminfo_received*: like *trackinfo_received*, the event indicates new + metadata for album items. The parameter is an ``AlbumInfo`` object instead + of a ``TrackInfo``. Parameter: ``info``. The included ``mpdupdate`` plugin provides an example use case for event listeners. From cb6edb46efe8380990fe0ff6718204ad66ad3f5d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 12:12:48 -0800 Subject: [PATCH 21/39] Use ` instead of * for plugin event names --- docs/dev/plugins.rst | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/dev/plugins.rst b/docs/dev/plugins.rst index bf5f3d4a9..885ef2222 100644 --- a/docs/dev/plugins.rst +++ b/docs/dev/plugins.rst @@ -133,39 +133,39 @@ registration process in this case:: The events currently available are: -* *pluginload*: called after all the plugins have been loaded after the ``beet`` +* `pluginload`: called after all the plugins have been loaded after the ``beet`` command starts -* *import*: called after a ``beet import`` command finishes (the ``lib`` keyword +* `import`: called after a ``beet import`` command finishes (the ``lib`` keyword argument is a Library object; ``paths`` is a list of paths (strings) that were imported) -* *album_imported*: called with an ``Album`` object every time the ``import`` +* `album_imported`: called with an ``Album`` object every time the ``import`` command finishes adding an album to the library. Parameters: ``lib``, ``album`` -* *item_copied*: called with an ``Item`` object whenever its file is copied. +* `item_copied`: called with an ``Item`` object whenever its file is copied. Parameters: ``item``, ``source`` path, ``destination`` path -* *item_imported*: called with an ``Item`` object every time the importer adds a +* `item_imported`: called with an ``Item`` object every time the importer adds a singleton to the library (not called for full-album imports). Parameters: ``lib``, ``item`` -* *before_item_moved*: called with an ``Item`` object immediately before its +* `before_item_moved`: called with an ``Item`` object immediately before its file is moved. Parameters: ``item``, ``source`` path, ``destination`` path -* *item_moved*: called with an ``Item`` object whenever its file is moved. +* `item_moved`: called with an ``Item`` object whenever its file is moved. Parameters: ``item``, ``source`` path, ``destination`` path -* *item_linked*: called with an ``Item`` object whenever a symlink is created +* `item_linked`: called with an ``Item`` object whenever a symlink is created for a file. Parameters: ``item``, ``source`` path, ``destination`` path -* *item_removed*: called with an ``Item`` object every time an item (singleton +* `item_removed`: called with an ``Item`` object every time an item (singleton or album's part) is removed from the library (even when its file is not deleted from disk). -* *write*: called with an ``Item`` object, a ``path``, and a ``tags`` +* `write`: called with an ``Item`` object, a ``path``, and a ``tags`` dictionary just before a file's metadata is written to disk (i.e., just before the file on disk is opened). Event handlers may change the ``tags`` dictionary to customize the tags that are written to the @@ -174,55 +174,55 @@ The events currently available are: operation. Beets will catch that exception, print an error message and continue. -* *after_write*: called with an ``Item`` object after a file's metadata is +* `after_write`: called with an ``Item`` object after a file's metadata is written to disk (i.e., just after the file on disk is closed). -* *import_task_created*: called immediately after an import task is +* `import_task_created`: called immediately after an import task is initialized. Plugins can use this to, for example, change imported files of a task before anything else happens. It's also possible to replace the task with another task by returning a list of tasks. This list can contain zero or more `ImportTask`s. Returning an empty list will stop the task. Parameters: ``task`` (an `ImportTask`) and ``session`` (an `ImportSession`). -* *import_task_start*: called when before an import task begins processing. +* `import_task_start`: called when before an import task begins processing. Parameters: ``task`` and ``session``. -* *import_task_apply*: called after metadata changes have been applied in an +* `import_task_apply`: called after metadata changes have been applied in an import task. This is called on the same thread as the UI, so use this sparingly and only for tasks that can be done quickly. For most plugins, an import pipeline stage is a better choice (see :ref:`plugin-stage`). Parameters: ``task`` and ``session``. -* *import_task_choice*: called after a decision has been made about an import +* `import_task_choice`: called after a decision has been made about an import task. This event can be used to initiate further interaction with the user. Use ``task.choice_flag`` to determine or change the action to be taken. Parameters: ``task`` and ``session``. -* *import_task_files*: called after an import task finishes manipulating the +* `import_task_files`: called after an import task finishes manipulating the filesystem (copying and moving files, writing metadata tags). Parameters: ``task`` and ``session``. -* *library_opened*: called after beets starts up and initializes the main +* `library_opened`: called after beets starts up and initializes the main Library object. Parameter: ``lib``. -* *database_change*: a modification has been made to the library database. The +* `database_change`: a modification has been made to the library database. The change might not be committed yet. Parameters: ``lib`` and ``model``. -* *cli_exit*: called just before the ``beet`` command-line program exits. +* `cli_exit`: called just before the ``beet`` command-line program exits. Parameter: ``lib``. -* *import_begin*: called just before a ``beet import`` session starts up. +* `import_begin`: called just before a ``beet import`` session starts up. Parameter: ``session``. -* *trackinfo_received*: called after metadata for a track item has been +* `trackinfo_received`: called after metadata for a track item has been fetched from a data source, such as MusicBrainz. You can modify the tags that the rest of the pipeline sees on a ``beet import`` operation or during later adjustments, such as ``mbsync``. Slow handlers of the event can impact the operation, since the event is fired for any fetched possible match - *before* the user (or the autotagger machinery) gets to see the match. + `before` the user (or the autotagger machinery) gets to see the match. Parameter: ``info``. -* *albuminfo_received*: like *trackinfo_received*, the event indicates new +* `albuminfo_received`: like `trackinfo_received`, the event indicates new metadata for album items. The parameter is an ``AlbumInfo`` object instead of a ``TrackInfo``. Parameter: ``info``. From 5b761f029e72d67b4229585bcfeb0004ea9410e5 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 13:14:33 -0800 Subject: [PATCH 22/39] A new `should_write` configuration wrapper The idea is that it is so common to check whether we need to write tags (or move files), and we're constantly re-implementing the same logic everywhere. It's not even the prettiest logic, as it commingles the importer settings with general settings. So it's important that we encapsulate the decision so we can make it better in the future. --- beets/ui/__init__.py | 27 ++++++++++++++++++++++++++- beets/ui/commands.py | 6 ++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 768eb76c7..a4c4c376b 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -70,7 +70,7 @@ class UserError(Exception): """ -# Utilities. +# Encoding utilities. def _out_encoding(): """Get the encoding to use for *outputting* strings to the console. @@ -137,6 +137,27 @@ def print_(*strings, **kwargs): sys.stdout.write(txt) +# Configuration wrappers. + +def _bool_fallback(opt, view): + """Given a boolean or None, return the original value or a + configuration option as a fallback. + """ + if opt is None: + return view.get(bool) + else: + return opt + + +def should_write(write_opt): + """Decide whether a command that updates metadata should also write tags, + using the importer configuration as the default. + """ + return _bool_fallback(write_opt, config['import']['write']) + + +# Input prompts. + def input_(prompt=None): """Like `raw_input`, but decodes the result to a Unicode string. Raises a UserError if stdin is not available. The prompt is sent to @@ -327,6 +348,8 @@ def input_yn(prompt, require=False): return sel == 'y' +# Human output formatting. + def human_bytes(size): """Formats size, a number of bytes, in a human-readable way.""" powers = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'H'] @@ -374,6 +397,8 @@ def human_seconds_short(interval): return u'%i:%02i' % (interval // 60, interval % 60) +# Colorization. + # ANSI terminal colorization code heavily inspired by pygments: # http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py # (pygments is by Tim Hatch, Armin Ronacher, et al.) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 348d12c88..69213ea97 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1293,10 +1293,8 @@ def modify_func(lib, opts, args): query, mods, dels = modify_parse_args(decargs(args)) if not mods and not dels: raise ui.UserError('no modifications specified') - write = opts.write if opts.write is not None else \ - config['import']['write'].get(bool) - modify_items(lib, mods, dels, query, write, opts.move, opts.album, - not opts.yes) + modify_items(lib, mods, dels, query, ui.should_write(opts.write), + opts.move, opts.album, not opts.yes) modify_cmd = ui.Subcommand( From 9f7aa866bd5d1b9d382e258ab4053bb6ebfc8750 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 13:20:09 -0800 Subject: [PATCH 23/39] Use `ui.should_write` everywhere There sure are a lot of plugins that want to write metadata! --- beets/ui/__init__.py | 2 +- beetsplug/chroma.py | 3 +-- beetsplug/echonest.py | 4 ++-- beetsplug/embedart.py | 2 +- beetsplug/ftintitle.py | 3 +-- beetsplug/keyfinder.py | 4 +--- beetsplug/lastgenre/__init__.py | 3 +-- beetsplug/lyrics.py | 4 ++-- beetsplug/mbsync.py | 5 ++--- beetsplug/replaygain.py | 3 +-- 10 files changed, 13 insertions(+), 20 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index a4c4c376b..e2af8593b 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -149,7 +149,7 @@ def _bool_fallback(opt, view): return opt -def should_write(write_opt): +def should_write(write_opt=None): """Decide whether a command that updates metadata should also write tags, using the importer configuration as the default. """ diff --git a/beetsplug/chroma.py b/beetsplug/chroma.py index f1ca233e0..3446661d3 100644 --- a/beetsplug/chroma.py +++ b/beetsplug/chroma.py @@ -194,8 +194,7 @@ class AcoustidPlugin(plugins.BeetsPlugin): def fingerprint_cmd_func(lib, opts, args): for item in lib.items(ui.decargs(args)): - fingerprint_item(self._log, item, - write=config['import']['write'].get(bool)) + fingerprint_item(self._log, item, write=ui.should_write()) fingerprint_cmd.func = fingerprint_cmd_func return [submit_cmd, fingerprint_cmd] diff --git a/beetsplug/echonest.py b/beetsplug/echonest.py index 2c8a35c69..312a8b620 100644 --- a/beetsplug/echonest.py +++ b/beetsplug/echonest.py @@ -24,7 +24,7 @@ import tempfile from string import Template import subprocess -from beets import util, config, plugins, ui +from beets import util, plugins, ui from beets.dbcore import types import pyechonest import pyechonest.song @@ -472,7 +472,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin): def fetch_func(lib, opts, args): self.config.set_args(opts) - write = config['import']['write'].get(bool) + write = ui.should_write() for item in lib.items(ui.decargs(args)): self._log.info(u'{0}', item) if self.config['force'] or self.requires_update(item): diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 9c0efa51e..6dc235979 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -128,7 +128,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): def process_album(self, album): """Automatically embed art after art has been set """ - if self.config['auto'] and config['import']['write']: + if self.config['auto'] and ui.should_write(): max_width = self.config['maxwidth'].get(int) art.embed_album(self._log, album, max_width, True, self.config['compare_threshold'].get(int), diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 8c435865c..d2369bf52 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -22,7 +22,6 @@ import re from beets import plugins from beets import ui from beets.util import displayable_path -from beets import config def split_on_feat(artist): @@ -102,7 +101,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): def func(lib, opts, args): self.config.set_args(opts) drop_feat = self.config['drop'].get(bool) - write = config['import']['write'].get(bool) + write = ui.should_write() for item in lib.items(ui.decargs(args)): self.ft_in_title(item, drop_feat) diff --git a/beetsplug/keyfinder.py b/beetsplug/keyfinder.py index e3e9a86ba..d76448a4c 100644 --- a/beetsplug/keyfinder.py +++ b/beetsplug/keyfinder.py @@ -23,7 +23,6 @@ import subprocess from beets import ui from beets import util from beets.plugins import BeetsPlugin -from beets import config class KeyFinderPlugin(BeetsPlugin): @@ -46,8 +45,7 @@ class KeyFinderPlugin(BeetsPlugin): return [cmd] def command(self, lib, opts, args): - self.find_key(lib.items(ui.decargs(args)), - write=config['import']['write'].get(bool)) + self.find_key(lib.items(ui.decargs(args)), write=ui.should_write()) def imported(self, session, task): self.find_key(task.items) diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index eab6ab440..85bd87f9b 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -31,7 +31,6 @@ import traceback from beets import plugins from beets import ui from beets.util import normpath, plurality -from beets import config from beets import library @@ -336,7 +335,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): ) def lastgenre_func(lib, opts, args): - write = config['import']['write'].get(bool) + write = ui.should_write() self.config.set_args(opts) for album in lib.albums(ui.decargs(args)): diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index 2f1e3529e..16f669f53 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -29,7 +29,7 @@ import warnings from HTMLParser import HTMLParseError from beets import plugins -from beets import config, ui +from beets import ui DIV_RE = re.compile(r'<(/?)div>?', re.I) @@ -557,7 +557,7 @@ class LyricsPlugin(plugins.BeetsPlugin): def func(lib, opts, args): # The "write to files" option corresponds to the # import_write config value. - write = config['import']['write'].get(bool) + write = ui.should_write() for item in lib.items(ui.decargs(args)): self.fetch_item_lyrics( lib, item, write, diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 974f7e894..03b79a48e 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -20,7 +20,6 @@ from __future__ import (division, absolute_import, print_function, from beets.plugins import BeetsPlugin from beets import autotag, library, ui, util from beets.autotag import hooks -from beets import config from collections import defaultdict @@ -50,7 +49,7 @@ class MBSyncPlugin(BeetsPlugin): default=True, dest='move', help="don't move files in library") cmd.parser.add_option('-W', '--nowrite', action='store_false', - default=config['import']['write'], dest='write', + default=None, dest='write', help="don't write updated metadata to files") cmd.parser.add_format_option() cmd.func = self.func @@ -61,7 +60,7 @@ class MBSyncPlugin(BeetsPlugin): """ move = opts.move pretend = opts.pretend - write = opts.write + write = ui.should_write(opts.write) query = ui.decargs(args) self.singletons(lib, query, move, pretend, write) diff --git a/beetsplug/replaygain.py b/beetsplug/replaygain.py index d57533e75..75f2ab947 100644 --- a/beetsplug/replaygain.py +++ b/beetsplug/replaygain.py @@ -27,7 +27,6 @@ from beets import logging from beets import ui from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path -from beets import config # Utilities. @@ -926,7 +925,7 @@ class ReplayGainPlugin(BeetsPlugin): def func(lib, opts, args): self._log.setLevel(logging.INFO) - write = config['import']['write'].get(bool) + write = ui.should_write() if opts.album: for album in lib.albums(ui.decargs(args)): From d78ee1cf28de87d3db01ce1e28cad9b21fc65358 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 13:26:21 -0800 Subject: [PATCH 24/39] Add a corresponding `should_move` wrapper --- beets/ui/__init__.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index e2af8593b..4c99b937a 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -139,21 +139,39 @@ def print_(*strings, **kwargs): # Configuration wrappers. -def _bool_fallback(opt, view): - """Given a boolean or None, return the original value or a - configuration option as a fallback. +def _bool_fallback(a, b): + """Given a boolean or None, return the original value or a fallback. """ - if opt is None: - return view.get(bool) + if a is None: + assert isinstance(b, bool) + return b else: - return opt + assert isinstance(a, bool) + return a def should_write(write_opt=None): - """Decide whether a command that updates metadata should also write tags, - using the importer configuration as the default. + """Decide whether a command that updates metadata should also write + tags, using the importer configuration as the default. """ - return _bool_fallback(write_opt, config['import']['write']) + return _bool_fallback(write_opt, config['import']['write'].get(bool)) + + +def should_move(move_opt=None): + """Decide whether a command that updates metadata should also move + files when they're inside the library, using the importer + configuration as the default. + + Specifically, commands should move files after metadata updates only + when the importer is configured *either* to move *or* to copy files. + They should avoid moving files when the importer is configured not + to touch any filenames. + """ + return _bool_fallback( + move_opt, + config['import']['move'].get(bool) or + config['import']['copy'].get(bool) + ) # Input prompts. From 80bfd186ae8dd9033f0417d7d87e74902a4f028f Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 13:34:47 -0800 Subject: [PATCH 25/39] Use `should_write` for modify, update, and mbsync This should address the surprising situation in #1697, where `import` went fine but then `update` unexpectedly changed filenames. --- beets/ui/commands.py | 17 +++++++++++++---- beetsplug/mbsync.py | 7 +++++-- docs/changelog.rst | 11 +++++++++++ docs/reference/config.rst | 4 ++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 69213ea97..fa2fc84af 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1055,7 +1055,8 @@ def update_items(lib, query, album, move, pretend): def update_func(lib, opts, args): - update_items(lib, decargs(args), opts.album, opts.move, opts.pretend) + update_items(lib, decargs(args), opts.album, ui.should_move(opts.move), + opts.pretend) update_cmd = ui.Subcommand( @@ -1064,7 +1065,11 @@ update_cmd = ui.Subcommand( update_cmd.parser.add_album_option() update_cmd.parser.add_format_option() update_cmd.parser.add_option( - '-M', '--nomove', action='store_false', default=True, dest='move', + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" +) +update_cmd.parser.add_option( + '-M', '--nomove', action='store_false', dest='move', help="don't move files in library" ) update_cmd.parser.add_option( @@ -1294,14 +1299,18 @@ def modify_func(lib, opts, args): if not mods and not dels: raise ui.UserError('no modifications specified') modify_items(lib, mods, dels, query, ui.should_write(opts.write), - opts.move, opts.album, not opts.yes) + ui.should_move(opts.move), opts.album, not opts.yes) modify_cmd = ui.Subcommand( 'modify', help='change metadata fields', aliases=('mod',) ) modify_cmd.parser.add_option( - '-M', '--nomove', action='store_false', default=True, dest='move', + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" +) +modify_cmd.parser.add_option( + '-M', '--nomove', action='store_false', dest='move', help="don't move files in library" ) modify_cmd.parser.add_option( diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 03b79a48e..d44f8f773 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -45,8 +45,11 @@ class MBSyncPlugin(BeetsPlugin): help='update metadata from musicbrainz') cmd.parser.add_option('-p', '--pretend', action='store_true', help='show all changes but do nothing') + cmd.parser.add_option('-m', '--move', action='store_true', + dest='move', + help="move files in the library directory") cmd.parser.add_option('-M', '--nomove', action='store_false', - default=True, dest='move', + dest='move', help="don't move files in library") cmd.parser.add_option('-W', '--nowrite', action='store_false', default=None, dest='write', @@ -58,7 +61,7 @@ class MBSyncPlugin(BeetsPlugin): def func(self, lib, opts, args): """Command handler for the mbsync function. """ - move = opts.move + move = ui.should_move(opts.move) pretend = opts.pretend write = ui.should_write(opts.write) query = ui.decargs(args) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5864d5c38..1c1fb0db6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,17 @@ Changelog 1.3.16 (in development) ----------------------- +New: + +* Three commands, ``modify``, ``update``, and ``mbsync``, would previously + move files by default after changing their metadata. Now, these commands + will only move files if you have the :ref:`config-import-copy` or + :ref:`config-import-move` options enabled in your importer configuration. + This way, if you configure the importer not to touch your filenames, other + commands will respect that decision by default too. Each command also + sprouted a ``--move`` command-line option to override this default (in + addition to the ``--nomove`` flag they already had). :bug:`1697` + For developers: * :doc:`/dev/plugins`: Two new hooks, ``albuminfo_received`` and diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 2d91bac14..7970d746c 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -370,6 +370,8 @@ Either ``yes`` or ``no``, controlling whether metadata (e.g., ID3) tags are written to files when using ``beet import``. Defaults to ``yes``. The ``-w`` and ``-W`` command-line options override this setting. +.. _config-import-copy: + copy ~~~~ @@ -380,6 +382,8 @@ overridden with the ``-c`` and ``-C`` command-line options. The option is ignored if ``move`` is enabled (i.e., beets can move or copy files but it doesn't make sense to do both). +.. _config-import-move: + move ~~~~ From 57cf7026681127f31fcc16f30d8673f6f88caeb0 Mon Sep 17 00:00:00 2001 From: Fabrice Laporte Date: Sat, 7 Nov 2015 23:18:30 +0100 Subject: [PATCH 26/39] Update index.rst --- docs/plugins/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 529a74da5..a45e0be5f 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -13,7 +13,7 @@ Using Plugins ------------- To use one of the plugins included with beets (see the rest of this page for a -list), just use the `plugins` option in your :doc:`config.yaml `: file, like so:: +list), just use the `plugins` option in your :doc:`config.yaml ` file, like so:: plugins: inline convert web From 34e6e39fe55cf1579701198638841954b231299e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 7 Nov 2015 15:37:13 -0800 Subject: [PATCH 27/39] mbsync: Debug logging Log *something* for each album, so you can tell that it's doing something even when there are no changes. To help diagnose #1707. --- beetsplug/mbsync.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index d44f8f773..6e7c208a7 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -139,6 +139,7 @@ class MBSyncPlugin(BeetsPlugin): break # Apply. + self._log.debug('applying changes to {}', album_formatted) with lib.transaction(): autotag.apply_metadata(album_info, mapping) changed = False From 485870f2888bc42fd2b521847fac802640524a0b Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sun, 8 Nov 2015 16:22:36 +0100 Subject: [PATCH 28/39] Made various artist title configurable --- beets/autotag/mb.py | 2 ++ beets/config_default.yaml | 1 + beetsplug/discogs.py | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 8589a62aa..78c5fb2f4 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -259,6 +259,8 @@ def album_info(release): data_url=album_url(release['id']), ) info.va = info.artist_id == VARIOUS_ARTISTS_ID + if info.va: + info.artist = config['va_name'].get(unicode) info.asin = release.get('asin') info.releasegroup_id = release['release-group']['id'] info.country = release.get('country') diff --git a/beets/config_default.yaml b/beets/config_default.yaml index f708702a8..ba58debe7 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -47,6 +47,7 @@ verbose: 0 terminal_encoding: original_date: no id3v23: no +va_name: "Various Artists" ui: terminal_width: 80 diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index c19e65a2f..2a90cdafe 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -20,6 +20,7 @@ from __future__ import (division, absolute_import, print_function, import beets.ui from beets import logging +from beets import config from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin from beets.util import confit @@ -55,8 +56,7 @@ class DiscogsPlugin(BeetsPlugin): 'apikey': 'rAzVUQYRaoFjeBjyWuWZ', 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', - 'source_weight': 0.5, - 'va_name': 'Various Artists', + 'source_weight': 0.5 }) self.config['apikey'].redact = True self.config['apisecret'].redact = True @@ -225,7 +225,7 @@ class DiscogsPlugin(BeetsPlugin): result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' if va: - artist = self.config['va_name'].get() + artist = config['va_name'].get(unicode) year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) From 95b80b37aa79610a321244d07b3b49ca0fd93592 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sun, 8 Nov 2015 17:04:36 +0100 Subject: [PATCH 29/39] Updated documentation --- docs/changelog.rst | 7 ++++--- docs/plugins/discogs.rst | 10 ---------- docs/reference/config.rst | 9 +++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1c1fb0db6..67be6cb0f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,9 +48,10 @@ Fixes: * :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of iTunes. :bug:`1700` * :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699` -* :doc:`/plugins/discogs`: A new option, ``va_name``, controls the album - artist name for various-artists albums. The default is now "Various - Artists," to match MusicBrainz. +* A new global option, ``va_name``, controls the album artist name for + various-artists albums. Defaults to "Various Artists" (MusicBrainz standard). + In order to match MusicBrainz, :doc:`/plugins/discogs` adapts to this, too. + 1.3.15 (October 17, 2015) diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index a7983e7e0..038718f9b 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -25,16 +25,6 @@ MusicBrainz. If you have a Discogs ID for an album you want to tag, you can also enter it at the "enter Id" prompt in the importer. -Configuration -------------- - -To configure the plugin, make a ``discogs:`` section in your configuration -file. The available options are: - -- **va_name**: The albumartist name to use when an album is marked as being by - "various" artists. - Default: "Various Artists" (matching the MusicBrainz convention). - Troubleshooting --------------- diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 7970d746c..1d8edf595 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -304,6 +304,15 @@ By default, beets writes MP3 tags using the ID3v2.4 standard, the latest version of ID3. Enable this option to instead use the older ID3v2.3 standard, which is preferred by certain older software such as Windows Media Player. +.. _va_name: + +va_name +~~~~~~~ + +Sets the albumartist for various-artist compilations. Defaults to ``'Various +Artists'`` (MusicBrainz standard). Affects other sources, such as +:doc:`/plugins/discogs`, too. + UI Options ---------- From 57faf015d9259ecbcd68005cea436346b1d4f000 Mon Sep 17 00:00:00 2001 From: Manfred Urban Date: Sun, 8 Nov 2015 17:28:55 +0100 Subject: [PATCH 30/39] Adapted va_name in other files --- beets/importer.py | 3 +-- beetsplug/lastgenre/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/importer.py b/beets/importer.py index 85d6e0824..5bdcaff8f 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -48,7 +48,6 @@ action = Enum('action', QUEUE_SIZE = 128 SINGLE_ARTIST_THRESH = 0.25 -VARIOUS_ARTISTS = u'Various Artists' PROGRESS_KEY = 'tagprogress' HISTORY_KEY = 'taghistory' @@ -631,7 +630,7 @@ class ImportTask(BaseImportTask): changes['comp'] = False else: # VA. - changes['albumartist'] = VARIOUS_ARTISTS + changes['albumartist'] = config['va_name'].get(unicode) changes['comp'] = True elif self.choice_flag == action.APPLY: diff --git a/beetsplug/lastgenre/__init__.py b/beetsplug/lastgenre/__init__.py index 85bd87f9b..f276fe4f1 100644 --- a/beetsplug/lastgenre/__init__.py +++ b/beetsplug/lastgenre/__init__.py @@ -30,6 +30,7 @@ import traceback from beets import plugins from beets import ui +from beets import config from beets.util import normpath, plurality from beets import library @@ -291,7 +292,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): result = None if isinstance(obj, library.Item): result = self.fetch_artist_genre(obj) - elif obj.albumartist != 'Various Artists': + elif obj.albumartist != config['va_name'].get(unicode): result = self.fetch_album_artist_genre(obj) else: # For "Various Artists", pick the most popular track genre. From 9165b7cf452748081b3c307d08a61c5f1604b592 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 8 Nov 2015 13:02:07 -0800 Subject: [PATCH 31/39] Trailing comma (#1708) --- beetsplug/discogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 2a90cdafe..7ab2d217b 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -56,7 +56,7 @@ class DiscogsPlugin(BeetsPlugin): 'apikey': 'rAzVUQYRaoFjeBjyWuWZ', 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', 'tokenfile': 'discogs_token.json', - 'source_weight': 0.5 + 'source_weight': 0.5, }) self.config['apikey'].redact = True self.config['apisecret'].redact = True From 98f48237f560dcc85cfac77a651e63bf9c52d31c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 8 Nov 2015 13:04:26 -0800 Subject: [PATCH 32/39] Tiny docs tweaks for #1708 --- docs/changelog.rst | 9 +++++---- docs/reference/config.rst | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 67be6cb0f..247e0efda 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,11 @@ New: commands will respect that decision by default too. Each command also sprouted a ``--move`` command-line option to override this default (in addition to the ``--nomove`` flag they already had). :bug:`1697` +* A new configuration option, ``va_name``, controls the album artist name for + various-artists albums. The setting defaults to "Various Artists," the + MusicBrainz standard. In order to match MusicBrainz, the + :doc:`/plugins/discogs` also adopts the same setting. + For developers: @@ -48,10 +53,6 @@ Fixes: * :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of iTunes. :bug:`1700` * :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699` -* A new global option, ``va_name``, controls the album artist name for - various-artists albums. Defaults to "Various Artists" (MusicBrainz standard). - In order to match MusicBrainz, :doc:`/plugins/discogs` adapts to this, too. - 1.3.15 (October 17, 2015) diff --git a/docs/reference/config.rst b/docs/reference/config.rst index 1d8edf595..84e4368d6 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -310,7 +310,7 @@ va_name ~~~~~~~ Sets the albumartist for various-artist compilations. Defaults to ``'Various -Artists'`` (MusicBrainz standard). Affects other sources, such as +Artists'`` (the MusicBrainz standard). Affects other sources, such as :doc:`/plugins/discogs`, too. From 48637f22e970288e23741dff46191f7a59f951db Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 9 Nov 2015 21:52:10 -0800 Subject: [PATCH 33/39] smartplaylist: Handle exceptional cases in setup --- beetsplug/smartplaylist.py | 70 ++++++++++++++++++++++---------------- docs/changelog.rst | 2 ++ 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/beetsplug/smartplaylist.py b/beetsplug/smartplaylist.py index 8889a2534..c88fe38c5 100644 --- a/beetsplug/smartplaylist.py +++ b/beetsplug/smartplaylist.py @@ -23,7 +23,7 @@ from beets import ui from beets.util import mkdirall, normpath, syspath from beets.library import Item, Album, parse_query_string from beets.dbcore import OrQuery -from beets.dbcore.query import MultipleSort +from beets.dbcore.query import MultipleSort, ParsingError import os @@ -93,36 +93,46 @@ class SmartPlaylistPlugin(BeetsPlugin): self._matched_playlists = set() for playlist in self.config['playlists'].get(list): - playlist_data = (playlist['name'],) - for key, Model in (('query', Item), ('album_query', Album)): - qs = playlist.get(key) - if qs is None: - query_and_sort = None, None - elif isinstance(qs, basestring): - query_and_sort = parse_query_string(qs, Model) - elif len(qs) == 1: - query_and_sort = parse_query_string(qs[0], Model) - else: - # multiple queries and sorts - queries, sorts = zip(*(parse_query_string(q, Model) - for q in qs)) - query = OrQuery(queries) - final_sorts = [] - for s in sorts: - if s: - if isinstance(s, MultipleSort): - final_sorts += s.sorts - else: - final_sorts.append(s) - if not final_sorts: - sort = None - elif len(final_sorts) == 1: - sort, = final_sorts - else: - sort = MultipleSort(final_sorts) - query_and_sort = query, sort + if 'name' not in playlist: + self._log.warn("playlist configuration is missing name") + continue - playlist_data += (query_and_sort,) + playlist_data = (playlist['name'],) + try: + for key, Model in (('query', Item), ('album_query', Album)): + qs = playlist.get(key) + if qs is None: + query_and_sort = None, None + elif isinstance(qs, basestring): + query_and_sort = parse_query_string(qs, Model) + elif len(qs) == 1: + query_and_sort = parse_query_string(qs[0], Model) + else: + # multiple queries and sorts + queries, sorts = zip(*(parse_query_string(q, Model) + for q in qs)) + query = OrQuery(queries) + final_sorts = [] + for s in sorts: + if s: + if isinstance(s, MultipleSort): + final_sorts += s.sorts + else: + final_sorts.append(s) + if not final_sorts: + sort = None + elif len(final_sorts) == 1: + sort, = final_sorts + else: + sort = MultipleSort(final_sorts) + query_and_sort = query, sort + + playlist_data += (query_and_sort,) + + except ParsingError as exc: + self._log.warn("invalid query in playlist {}: {}", + playlist['name'], exc) + continue self._unmatched_playlists.add(playlist_data) diff --git a/docs/changelog.rst b/docs/changelog.rst index 247e0efda..d86137b0e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,8 @@ Fixes: * :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of iTunes. :bug:`1700` * :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699` +* :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and + missing configuration. 1.3.15 (October 17, 2015) From 4b2b9fe2cee204e3eda0871764f2d4e090554aa8 Mon Sep 17 00:00:00 2001 From: Marvin Steadfast Date: Thu, 5 Nov 2015 09:46:01 +0100 Subject: [PATCH 34/39] Added embyupdate plugin Its a simple plugin that triggers a library refresh after the library got changed. It does the same thing like the plexupdate plugin. --- beetsplug/embyupdate.py | 133 ++++++++++++++++++++++ docs/changelog.rst | 5 +- docs/plugins/embyupdate.rst | 33 ++++++ docs/plugins/index.rst | 3 + test/test_embyupdate.py | 212 ++++++++++++++++++++++++++++++++++++ 5 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 beetsplug/embyupdate.py create mode 100644 docs/plugins/embyupdate.rst create mode 100644 test/test_embyupdate.py diff --git a/beetsplug/embyupdate.py b/beetsplug/embyupdate.py new file mode 100644 index 000000000..ffca7c30c --- /dev/null +++ b/beetsplug/embyupdate.py @@ -0,0 +1,133 @@ +"""Updates the Emby Library whenever the beets library is changed. + + emby: + host: localhost + port: 8096 + username: user + password: password +""" +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from beets import config +from beets.plugins import BeetsPlugin +from urllib import urlencode +from urlparse import urljoin, parse_qs, urlsplit, urlunsplit +import hashlib +import requests + + +def api_url(host, port, endpoint): + """Returns a joined url. + """ + joined = urljoin('http://{0}:{1}'.format(host, port), endpoint) + scheme, netloc, path, query_string, fragment = urlsplit(joined) + query_params = parse_qs(query_string) + + query_params['format'] = ['json'] + new_query_string = urlencode(query_params, doseq=True) + + return urlunsplit((scheme, netloc, path, new_query_string, fragment)) + + +def password_data(username, password): + """Returns a dict with username and its encoded password. + """ + return { + 'username': username, + 'password': hashlib.sha1(password).hexdigest(), + 'passwordMd5': hashlib.md5(password).hexdigest() + } + + +def create_headers(user_id, token=None): + """Return header dict that is needed to talk to the Emby API. + """ + headers = { + 'Authorization': 'MediaBrowser', + 'UserId': user_id, + 'Client': 'other', + 'Device': 'empy', + 'DeviceId': 'beets', + 'Version': '0.0.0' + } + + if token: + headers['X-MediaBrowser-Token'] = token + + return headers + + +def get_token(host, port, headers, auth_data): + """Return token for a user. + """ + url = api_url(host, port, '/Users/AuthenticateByName') + r = requests.post(url, headers=headers, data=auth_data) + + return r.json().get('AccessToken') + + +def get_user(host, port, username): + """Return user dict from server or None if there is no user. + """ + url = api_url(host, port, '/Users/Public') + r = requests.get(url) + user = [i for i in r.json() if i['Name'] == username] + + return user + + +class EmbyUpdate(BeetsPlugin): + def __init__(self): + super(EmbyUpdate, self).__init__() + + # Adding defaults. + config['emby'].add({ + u'host': u'localhost', + u'port': 8096 + }) + + self.register_listener('database_change', self.listen_for_db_change) + + def listen_for_db_change(self, lib, model): + """Listens for beets db change and register the update for the end. + """ + self.register_listener('cli_exit', self.update) + + def update(self, lib): + """When the client exists try to send refresh request to Emby. + """ + self._log.info(u'Updating Emby library...') + + host = config['emby']['host'].get() + port = config['emby']['port'].get() + username = config['emby']['username'].get() + password = config['emby']['password'].get() + + # Get user information from the Emby API. + user = get_user(host, port, username) + if not user: + self._log.warning(u'User {0} could not be found.'.format(username)) + return + + # Create Authentication data and headers. + auth_data = password_data(username, password) + headers = create_headers(user[0]['Id']) + + # Get authentication token. + token = get_token(host, port, headers, auth_data) + if not token: + self._log.warning( + u'Couldnt not get token for user {0}'.format(username)) + return + + # Recreate headers with a token. + headers = create_headers(user[0]['Id'], token=token) + + # Trigger the Update. + url = api_url(host, port, '/Library/Refresh') + r = requests.post(url, headers=headers) + if r.status_code != 204: + self._log.warning(u'Update could not be triggered') + else: + self._log.info(u'Update triggered.') diff --git a/docs/changelog.rst b/docs/changelog.rst index d86137b0e..27567f283 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,7 +18,8 @@ New: various-artists albums. The setting defaults to "Various Artists," the MusicBrainz standard. In order to match MusicBrainz, the :doc:`/plugins/discogs` also adopts the same setting. - +* :doc:`/plugins/embyupdate`: A plugin to trigger a library refresh on a + `Emby Server`_ if database changed. For developers: @@ -56,6 +57,8 @@ Fixes: * :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and missing configuration. +.. _Emby Server: http://emby.media + 1.3.15 (October 17, 2015) ------------------------- diff --git a/docs/plugins/embyupdate.rst b/docs/plugins/embyupdate.rst new file mode 100644 index 000000000..2a47826ae --- /dev/null +++ b/docs/plugins/embyupdate.rst @@ -0,0 +1,33 @@ +EmbyUpdate Plugin +================= + +``embyupdate`` is a plugin that lets you automatically update `Emby`_'s library whenever you change your beets library. + +To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll probably want to configure the specifics of your Emby server. You can do that using an ``emby:`` section in your ``config.yaml``, which looks like this:: + + emby: + host: localhost + port: 8096 + username: user + password: password + +To use the ``embyupdate`` plugin you need to install the `requests`_ library with:: + + pip install requests + +With that all in place, you'll see beets send the "update" command to your Emby server every time you change your beets library. + +.. _Emby: http://emby.media/ +.. _requests: http://docs.python-requests.org/en/latest/ + +Configuration +------------- + +The available options under the ``emby:`` section are: + +- **host**: The Emby server name. + Default: ``localhost`` +- **port**: The Emby server port. + Defailt: 8096 +- **username**: A username of a Emby user that is allowed to refresh the library. +- **password**: That users password. diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index a45e0be5f..d09139837 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -41,6 +41,7 @@ Each plugin has its own set of options that can be defined in a section bearing duplicates echonest embedart + embyupdate fetchart fromfilename ftintitle @@ -131,6 +132,7 @@ Path Formats Interoperability ---------------- +* :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. * :doc:`mpdupdate`: Automatically notifies `MPD`_ whenever the beets library @@ -143,6 +145,7 @@ Interoperability * :doc:`badfiles`: Check audio file integrity. +.. _Emby: http://emby.media .. _Plex: http://plex.tv Miscellaneous diff --git a/test/test_embyupdate.py b/test/test_embyupdate.py new file mode 100644 index 000000000..d21c19987 --- /dev/null +++ b/test/test_embyupdate.py @@ -0,0 +1,212 @@ +from __future__ import (division, absolute_import, print_function, + unicode_literals) + +from test._common import unittest +from test.helper import TestHelper +from beetsplug import embyupdate +import responses + + +class EmbyUpdateTest(unittest.TestCase, TestHelper): + def setUp(self): + self.setup_beets() + self.load_plugins('embyupdate') + + self.config['emby'] = { + u'host': u'localhost', + u'port': 8096, + u'username': u'username', + u'password': u'password' + } + + def tearDown(self): + self.teardown_beets() + self.unload_plugins() + + def test_api_url(self): + self.assertEqual( + embyupdate.api_url(self.config['emby']['host'].get(), + self.config['emby']['port'].get(), + '/Library/Refresh'), + 'http://localhost:8096/Library/Refresh?format=json' + ) + + def test_password_data(self): + self.assertEqual( + embyupdate.password_data(self.config['emby']['username'].get(), + self.config['emby']['password'].get()), + { + 'username': 'username', + 'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', + 'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99' + } + ) + + def test_create_header_no_token(self): + self.assertEqual( + embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721'), + { + 'Authorization': 'MediaBrowser', + 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', + 'Client': 'other', + 'Device': 'empy', + 'DeviceId': 'beets', + 'Version': '0.0.0' + } + ) + + def test_create_header_with_token(self): + self.assertEqual( + embyupdate.create_headers('e8837bc1-ad67-520e-8cd2-f629e3155721', + token='abc123'), + { + 'Authorization': 'MediaBrowser', + 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', + 'Client': 'other', + 'Device': 'empy', + 'DeviceId': 'beets', + 'Version': '0.0.0', + 'X-MediaBrowser-Token': 'abc123' + } + ) + + @responses.activate + def test_get_token(self): + body = ('{"User":{"Name":"username", ' + '"ServerId":"1efa5077976bfa92bc71652404f646ec",' + '"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,' + '"HasConfiguredPassword":true,' + '"HasConfiguredEasyPassword":false,' + '"LastLoginDate":"2015-11-09T08:35:03.6357440Z",' + '"LastActivityDate":"2015-11-09T08:35:03.6665060Z",' + '"Configuration":{"AudioLanguagePreference":"",' + '"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",' + '"DisplayMissingEpisodes":false,' + '"DisplayUnairedEpisodes":false,' + '"GroupMoviesIntoBoxSets":false,' + '"DisplayChannelsWithinViews":[],' + '"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],' + '"SubtitleMode":"Default","DisplayCollectionsView":true,' + '"DisplayFoldersView":false,"EnableLocalPassword":false,' + '"OrderedViews":[],"IncludeTrailersInSuggestions":true,' + '"EnableCinemaMode":true,"LatestItemsExcludes":[],' + '"PlainFolderViews":[],"HidePlayedInLatest":true,' + '"DisplayChannelsInline":false},' + '"Policy":{"IsAdministrator":true,"IsHidden":false,' + '"IsDisabled":false,"BlockedTags":[],' + '"EnableUserPreferenceAccess":true,"AccessSchedules":[],' + '"BlockUnratedItems":[],' + '"EnableRemoteControlOfOtherUsers":false,' + '"EnableSharedDeviceControl":true,' + '"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,' + '"EnableMediaPlayback":true,' + '"EnableAudioPlaybackTranscoding":true,' + '"EnableVideoPlaybackTranscoding":true,' + '"EnableContentDeletion":false,' + '"EnableContentDownloading":true,"EnableSync":true,' + '"EnableSyncTranscoding":true,"EnabledDevices":[],' + '"EnableAllDevices":true,"EnabledChannels":[],' + '"EnableAllChannels":true,"EnabledFolders":[],' + '"EnableAllFolders":true,"InvalidLoginAttemptCount":0,' + '"EnablePublicSharing":true}},' + '"SessionInfo":{"SupportedCommands":[],' + '"QueueableMediaTypes":[],"PlayableMediaTypes":[],' + '"Id":"89f3b33f8b3a56af22088733ad1d76b3",' + '"UserId":"2ec276a2642e54a19b612b9418a8bd3b",' + '"UserName":"username","AdditionalUsers":[],' + '"ApplicationVersion":"Unknown version",' + '"Client":"Unknown app",' + '"LastActivityDate":"2015-11-09T08:35:03.6665060Z",' + '"DeviceName":"Unknown device","DeviceId":"Unknown device id",' + '"SupportsRemoteControl":false,"PlayState":{"CanSeek":false,' + '"IsPaused":false,"IsMuted":false,"RepeatMode":"RepeatNone"}},' + '"AccessToken":"4b19180cf02748f7b95c7e8e76562fc8",' + '"ServerId":"1efa5077976bfa92bc71652404f646ec"}') + + responses.add(responses.POST, + ('http://localhost:8096' + '/Users/AuthenticateByName'), + body=body, + status=200, + content_type='application/json') + + headers = { + 'Authorization': 'MediaBrowser', + 'UserId': 'e8837bc1-ad67-520e-8cd2-f629e3155721', + 'Client': 'other', + 'Device': 'empy', + 'DeviceId': 'beets', + 'Version': '0.0.0' + } + + auth_data = { + 'username': 'username', + 'password': '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', + 'passwordMd5': '5f4dcc3b5aa765d61d8327deb882cf99' + } + + self.assertEqual( + embyupdate.get_token('localhost', 8096, headers, auth_data), + '4b19180cf02748f7b95c7e8e76562fc8') + + @responses.activate + def test_get_user(self): + body = ('[{"Name":"username",' + '"ServerId":"1efa5077976bfa92bc71652404f646ec",' + '"Id":"2ec276a2642e54a19b612b9418a8bd3b","HasPassword":true,' + '"HasConfiguredPassword":true,' + '"HasConfiguredEasyPassword":false,' + '"LastLoginDate":"2015-11-09T08:35:03.6357440Z",' + '"LastActivityDate":"2015-11-09T08:42:39.3693220Z",' + '"Configuration":{"AudioLanguagePreference":"",' + '"PlayDefaultAudioTrack":true,"SubtitleLanguagePreference":"",' + '"DisplayMissingEpisodes":false,' + '"DisplayUnairedEpisodes":false,' + '"GroupMoviesIntoBoxSets":false,' + '"DisplayChannelsWithinViews":[],' + '"ExcludeFoldersFromGrouping":[],"GroupedFolders":[],' + '"SubtitleMode":"Default","DisplayCollectionsView":true,' + '"DisplayFoldersView":false,"EnableLocalPassword":false,' + '"OrderedViews":[],"IncludeTrailersInSuggestions":true,' + '"EnableCinemaMode":true,"LatestItemsExcludes":[],' + '"PlainFolderViews":[],"HidePlayedInLatest":true,' + '"DisplayChannelsInline":false},' + '"Policy":{"IsAdministrator":true,"IsHidden":false,' + '"IsDisabled":false,"BlockedTags":[],' + '"EnableUserPreferenceAccess":true,"AccessSchedules":[],' + '"BlockUnratedItems":[],' + '"EnableRemoteControlOfOtherUsers":false,' + '"EnableSharedDeviceControl":true,' + '"EnableLiveTvManagement":true,"EnableLiveTvAccess":true,' + '"EnableMediaPlayback":true,' + '"EnableAudioPlaybackTranscoding":true,' + '"EnableVideoPlaybackTranscoding":true,' + '"EnableContentDeletion":false,' + '"EnableContentDownloading":true,' + '"EnableSync":true,"EnableSyncTranscoding":true,' + '"EnabledDevices":[],"EnableAllDevices":true,' + '"EnabledChannels":[],"EnableAllChannels":true,' + '"EnabledFolders":[],"EnableAllFolders":true,' + '"InvalidLoginAttemptCount":0,"EnablePublicSharing":true}}]') + + responses.add(responses.GET, + 'http://localhost:8096/Users/Public', + body=body, + status=200, + content_type='application/json') + + response = embyupdate.get_user('localhost', 8096, 'username') + + self.assertEqual(response[0]['Id'], + '2ec276a2642e54a19b612b9418a8bd3b') + + self.assertEqual(response[0]['Name'], + 'username') + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) + + +if __name__ == b'__main__': + unittest.main(defaultTest='suite') From 17dcc7ab214886c37ec903322f05c18c036b582f Mon Sep 17 00:00:00 2001 From: Marvin Steadfast Date: Wed, 11 Nov 2015 08:54:58 +0100 Subject: [PATCH 35/39] Fixed two typos in the embyupdate docs --- docs/plugins/embyupdate.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/embyupdate.rst b/docs/plugins/embyupdate.rst index 2a47826ae..3e4c9687f 100644 --- a/docs/plugins/embyupdate.rst +++ b/docs/plugins/embyupdate.rst @@ -28,6 +28,6 @@ The available options under the ``emby:`` section are: - **host**: The Emby server name. Default: ``localhost`` - **port**: The Emby server port. - Defailt: 8096 + Default: 8096 - **username**: A username of a Emby user that is allowed to refresh the library. -- **password**: That users password. +- **password**: That user's password. From 9c968456c1bc1e855d17d204be256c0df3a67afe Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 13 Nov 2015 12:21:36 -0800 Subject: [PATCH 36/39] Fix #1666: malformed binary data in SoundCheck --- beets/mediafile.py | 4 ++-- docs/changelog.rst | 2 ++ test/rsrc/soundcheck-nonascii.m4a | Bin 0 -> 5862 bytes test/test_mediafile_edge.py | 15 +++++++++++---- 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 test/rsrc/soundcheck-nonascii.m4a diff --git a/beets/mediafile.py b/beets/mediafile.py index 64ab49ac2..5fe1fa308 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -215,9 +215,9 @@ def _sc_decode(soundcheck): # SoundCheck tags consist of 10 numbers, each represented by 8 # characters of ASCII hex preceded by a space. try: - soundcheck = soundcheck.replace(' ', '').decode('hex') + soundcheck = soundcheck.replace(b' ', b'').decode('hex') soundcheck = struct.unpack(b'!iiiiiiiiii', soundcheck) - except (struct.error, TypeError, UnicodeEncodeError): + except (struct.error, TypeError): # SoundCheck isn't in the format we expect, so return default # values. return 0.0, 0.0 diff --git a/docs/changelog.rst b/docs/changelog.rst index 27567f283..700a16d6d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -56,6 +56,8 @@ Fixes: * :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699` * :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and missing configuration. +* Fix a crash with some files with unreadable iTunes SoundCheck metadata. + :bug:`1666` .. _Emby Server: http://emby.media diff --git a/test/rsrc/soundcheck-nonascii.m4a b/test/rsrc/soundcheck-nonascii.m4a new file mode 100644 index 0000000000000000000000000000000000000000..29f5de5310f799d5f17514d280f32d4cab82f9cd GIT binary patch literal 5862 zcmeGdX;hQP_5;GAvO_RHq+tgk7_yKAk$q7M$R;8x=F0}D34|ob(kfvQsZ>yl6}2KF zV53yAEr=EcErQmfzTy)ROYsR65i6}r)mqz`FXUCxp5yCzzk0kUH}}q6=590dAqYYe zDsrV!^dJ%f08)vRP8G>BByf!&h8rXq8LI&!maI%@|157w2j>b}B7N}t`sy$1> z7omV9N%*g>YjG!L=#a)2%QUX?jI4AGuc2DK5>dJktW4z+^-hx-C%#%vSOe5za_$GWSQBTK*(V0IAxtVqTO@0xXHDbW{SUB_GASVdoHGmKa() zsDk1W0JV8ur$`f!P?|Z#o@xWcSunJ{DpipF zDgz*v34ns6kgb9SgvlKMi-9)m1pqs~NDi-{)f7Dr(FIbXijU?3`a*?N3I)#CnkN;( zO3fxc;W@$%kF_)vl}=SarHuj<2B`V-9*7>0_^1>aP~$bJ!8$2Q$VwN;NvoM&K3-H$ zQf!upw<-q*+|CNS=lb=`VN8eT^OMH9JOS$wj&aa5e7Ot~bAv8?I3=a=~f$OqqT(ye8p4pel6sc`~+-k99OWDXJ;^zA+bS9!BE6$QWRKggiVnUS(rl%#W_hMS+KlN{O zJ8yloC36KgzrGcDf-mZf3F}uX4N3c(LT~hM_FuSIOv9ZEa=Pp;axa?Mfms9@I2I@k zbUl1&cR5L@LCf~4vyWrv9_Tn+RXRF^Jj55(I*^Ku&+APq??a~F6KcJiIn5wD`NPf{ zTAo(B(9u1*KhqUv0?kZTos(G|@4vj)DAlZD-*2v!V}^M5+}RCPS08@q*>t`+M{kdn zF^q~C%Zy5bO_LJC&Bw=?>1h`lh|$&W#r!NCI`%=j3FFAN4<#I595vqv&& zCl-DhnnyY0{_~^jHUr+_`K--Pj*Oh0<dUygo42}+14T`L@(Z?W3X?zfhc>IQHE5JQ*JJ|Ege(@OCy-(iD@q1#)gn~jz? z?y**+PZHq4)!m^Gta128FRO}0WpI;JnJ70b@*09akve{}w&-t-~xx(u6$bFD- zf@!hw435D;`+@~)veMEModU}2JcjqUB_Ak0S+M7t0i(hlOAY$atjmGm7&C2vFFj|oyM2Iq>(jOJfit~#H@e3BI6kk(<*7Xp ziy0Gv_?1(lza#b-7lb|9Zfm`==z*)Q_d4GPW>sY)cFVDenv+URJjFALGTSa4^DAid zAD!rNx%*Xh!$R7ctt-s=u}VuNRzWQ%yrzb=z~iG9U-bG%b1C)R{t}}9-0!1K+!?6Y z-Dgxk$2=9AjMQI=407XnBI})%b&*DSzH8@iWE4fwR0P+iAdgX(Vj*e$Wj92M&E1@0 z&+pKw-Ezp+#JM)4iH#5b{6}Mt70+nNCALRLc?)GFr*JJ=70|Yh6Q(rSgnF)Mc3&PW z%Ess0o$<^i-VFTeSZM3xT~A#*_u=Y4xu&Vf_+p1rkCJ(B$YdV3nji^2Z5)*Scz#o# zz2T0~`2Cw^u(0q9l064^ie9pBB;}_&dizs+o!HG6)}_kLI8n~)zD`b7>`z&|cd1(j zHk?`qLa9j|h5hjs$LN0@ar~w+&+h7yopTjNL1b@!?)R0nx|ACA5pp0oj3eRy$sTWe zQta8R(<;Ib-X&e!r~!yqR_T*Majp;x~SpKKq>qNrKaT%q@t^M^=VT{=_>Q$XglO&sAulxlTpoX?t1JRX1Iq-sx0|HAnl zw|j{;9W&L_2rn#9J}loAUJ|&Iw`R>m$SDh|zx9A$*Ud$Yz4k|A2j`^9LMtY)YmXX$ zP7K-JC#<)m>b(C&yjzOx!DQVEe*VO}4KW3EyLH#T-#<{*u-Rui5`PSPF>5VJ#b%xy zd>h$2J9-ca>agyqOp19sZBO>w1G Date: Sat, 14 Nov 2015 13:05:06 -0800 Subject: [PATCH 37/39] Move EDITOR discovery to a util function --- beets/ui/commands.py | 2 +- beets/util/__init__.py | 42 +++++++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index fa2fc84af..307945a03 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -1473,7 +1473,7 @@ def config_edit(): An empty config file is created if no existing config file exists. """ path = config.user_config_path() - editor = os.environ.get('EDITOR') + editor = util.editor_command() try: if not os.path.isfile(path): open(path, 'w+').close() diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 61e2f4ac8..a2a5688b5 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -736,28 +736,36 @@ def open_anything(): return base_cmd -def interactive_open(targets, command=None): - """Open the files in `targets` by `exec`ing a new command. (The new - program takes over, and Python execution ends: this does not fork a - subprocess.) +def editor_command(): + """Get a command for opening a text file. - If `command` is provided, use it. Otherwise, use an OS-specific - command (from `open_anything`) to open the file. + Use the `EDITOR` environment variable by default. If it is not + present, fall back to `open_anything()`, the platform-specific tool + for opening files in general. + """ + editor = os.environ.get('EDITOR') + if editor: + return editor + return open_anything() + + +def interactive_open(targets, command): + """Open the files in `targets` by `exec`ing a new `command`, given + as a Unicode string. (The new program takes over, and Python + execution ends: this does not fork a subprocess.) Can raise `OSError`. """ - if command: - command = command.encode('utf8') - try: - command = [c.decode('utf8') - for c in shlex.split(command)] - except ValueError: # Malformed shell tokens. - command = [command] - command.insert(0, command[0]) # for argv[0] - else: - base_cmd = open_anything() - command = [base_cmd, base_cmd] + # Split the command string into its arguments. + command = command.encode('utf8') + try: + command = [c.decode('utf8') + for c in shlex.split(command)] + except ValueError: # Malformed shell tokens. + command = [command] + command.insert(0, command[0]) # for argv[0] + # Add the explicit arguments. command += targets return os.execlp(*command) From 3a5dd47e3a1edd641d1dd328a349e9d8c66287c7 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 14 Nov 2015 13:17:44 -0800 Subject: [PATCH 38/39] Factor out shlex.split workaround --- beets/library.py | 8 +------- beets/util/__init__.py | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/beets/library.py b/beets/library.py index 186a674e7..cad39c232 100644 --- a/beets/library.py +++ b/beets/library.py @@ -19,7 +19,6 @@ from __future__ import (division, absolute_import, print_function, import os import sys -import shlex import unicodedata import time import re @@ -1139,13 +1138,8 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ assert isinstance(s, unicode), "Query is not unicode: {0!r}".format(s) - - # A bug in Python < 2.7.3 prevents correct shlex splitting of - # Unicode strings. - # http://bugs.python.org/issue6988 - s = s.encode('utf8') try: - parts = [p.decode('utf8') for p in shlex.split(s)] + parts = util.shlex_split(s) except ValueError as exc: raise dbcore.InvalidQueryError(s, exc) return parse_query_parts(parts, model_cls) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index a2a5688b5..2a861ce88 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -749,6 +749,26 @@ def editor_command(): return open_anything() +def shlex_split(s): + """Split a Unicode or bytes string according to shell lexing rules. + + Raise `ValueError` if the string is not a well-formed shell string. + This is a workaround for a bug in some versions of Python. + """ + if isinstance(s, bytes): + # Shlex works fine. + return shlex.split(s) + + elif isinstance(s, unicode): + # Work around a Python bug. + # http://bugs.python.org/issue6988 + bs = s.encode('utf8') + return [c.decode('utf8') for c in shlex.split(bs)] + + else: + raise TypeError('shlex_split called with non-string') + + def interactive_open(targets, command): """Open the files in `targets` by `exec`ing a new `command`, given as a Unicode string. (The new program takes over, and Python @@ -757,15 +777,12 @@ def interactive_open(targets, command): Can raise `OSError`. """ # Split the command string into its arguments. - command = command.encode('utf8') try: - command = [c.decode('utf8') - for c in shlex.split(command)] + command = shlex_split(command) except ValueError: # Malformed shell tokens. command = [command] command.insert(0, command[0]) # for argv[0] - # Add the explicit arguments. command += targets return os.execlp(*command) From e3f7da54676da58e46424d86ac872184b044d6cd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sat, 14 Nov 2015 13:26:04 -0800 Subject: [PATCH 39/39] Update test for simpler interactive_open --- test/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index 324a4d589..b72410c9c 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -43,7 +43,7 @@ class UtilTest(unittest.TestCase): @patch('beets.util.open_anything') def test_interactive_open(self, mock_open, mock_execlp): mock_open.return_value = 'tagada' - util.interactive_open(['foo']) + util.interactive_open(['foo'], util.open_anything()) mock_execlp.assert_called_once_with('tagada', 'tagada', 'foo') mock_execlp.reset_mock()