From edbb97bd09451a74aa620f37019d09cb71b7aebe Mon Sep 17 00:00:00 2001 From: David Date: Sat, 25 Oct 2014 16:16:32 -0400 Subject: [PATCH 1/9] Dropping unused argument --- beetsplug/ftintitle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/ftintitle.py b/beetsplug/ftintitle.py index 8a92d20df..1f42ba49c 100644 --- a/beetsplug/ftintitle.py +++ b/beetsplug/ftintitle.py @@ -72,7 +72,7 @@ def update_metadata(item, feat_part, drop_feat): item.title = new_title -def ft_in_title(item, drop_feat, write): +def ft_in_title(item, drop_feat): """Look for featured artists in the item's artist fields and move them to the title. """ From d9c9e06674bf70703ffa872817da2caff44ea6cd Mon Sep 17 00:00:00 2001 From: David Date: Sun, 26 Oct 2014 16:26:59 -0400 Subject: [PATCH 2/9] Add authentication to discogs plugin --- beetsplug/discogs.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index f859e48f7..909015cfc 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -36,11 +36,34 @@ class DiscogsPlugin(BeetsPlugin): def __init__(self): super(DiscogsPlugin, self).__init__() + try: + c_key = beets.config['discogs']['consumer_key'].get(unicode) + c_secret = beets.config['discogs']['consumer_secret'].get(unicode) + except beets.confit.NotFoundError: + raise beets.ui.UserError('discogs API keys not configured.') + try: + token = beets.config['discogs']['token'].get(unicode) + secret = beets.config['discogs']['secret'].get(unicode) + except beets.confit.NotFoundError: + token, secret = self.authenticate(c_key, c_secret) + self.config.add({ 'source_weight': 0.5, }) self.discogs_client = Client('beets/%s +http://beets.radbox.org/' % - beets.__version__) + beets.__version__, c_key, c_secret, + token, secret) + + def authenticate(self, c_key, c_secret): + auth_client = Client('beets/%s +http://beets.radbox.org/' % + beets.__version__, c_key, c_secret) + _, _, url = auth_client.get_authorize_url() + beets.ui.print_("To authenticate to discogs please visit %s" % url) + code = beets.ui.input_("Enter the code: ") + token, secret = auth_client.get_access_token(code) + beets.ui.print_("token: %s\nsecret: %s" % (token, secret)) + beets.ui.print_("Add the above to beets config!") + return token, secret def album_distance(self, items, album_info, mapping): """Returns the album distance. From a8a5191cf5b38cc027557b41e38c37cdb497bc13 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 26 Oct 2014 15:24:17 -0700 Subject: [PATCH 3/9] discogs #1040: Standard API key, handle failure --- beetsplug/discogs.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 909015cfc..554c6023d 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -31,36 +31,40 @@ log = logging.getLogger('beets') urllib3_logger = logging.getLogger('requests.packages.urllib3') urllib3_logger.setLevel(logging.CRITICAL) +USER_AGENT = 'beets/{0} +http://beets.radbox.org/'.format(beets.__version__) + class DiscogsPlugin(BeetsPlugin): def __init__(self): super(DiscogsPlugin, self).__init__() + self.config.add({ + 'apikey': 'rAzVUQYRaoFjeBjyWuWZ', + 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', + 'source_weight': 0.5, + }) + + c_key = self.config['apikey'].get(unicode) + c_secret = self.config['apisecret'].get(unicode) try: - c_key = beets.config['discogs']['consumer_key'].get(unicode) - c_secret = beets.config['discogs']['consumer_secret'].get(unicode) - except beets.confit.NotFoundError: - raise beets.ui.UserError('discogs API keys not configured.') - try: - token = beets.config['discogs']['token'].get(unicode) - secret = beets.config['discogs']['secret'].get(unicode) + token = self.config['token'].get(unicode) + secret = self.config['secret'].get(unicode) except beets.confit.NotFoundError: token, secret = self.authenticate(c_key, c_secret) - self.config.add({ - 'source_weight': 0.5, - }) - self.discogs_client = Client('beets/%s +http://beets.radbox.org/' % - beets.__version__, c_key, c_secret, + self.discogs_client = Client(USER_AGENT, c_key, c_secret, token, secret) def authenticate(self, c_key, c_secret): - auth_client = Client('beets/%s +http://beets.radbox.org/' % - beets.__version__, c_key, c_secret) + auth_client = Client(USER_AGENT, c_key, c_secret) _, _, url = auth_client.get_authorize_url() - beets.ui.print_("To authenticate to discogs please visit %s" % url) - code = beets.ui.input_("Enter the code: ") - token, secret = auth_client.get_access_token(code) + beets.ui.print_("To authenticate with Discogs, visit:") + beets.ui.print_(url) + code = beets.ui.input_("Enter the code:") + try: + token, secret = auth_client.get_access_token(code) + except DiscogsAPIError: + raise beets.ui.UserError('Discogs authorization failed') beets.ui.print_("token: %s\nsecret: %s" % (token, secret)) beets.ui.print_("Add the above to beets config!") return token, secret From eb35d7084ade8225f03f23ae047a5fd5a0f30f3b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 26 Oct 2014 15:45:34 -0700 Subject: [PATCH 4/9] Save Discogs token (#1040) --- beets/util/confit.py | 16 +++++++++++++--- beetsplug/discogs.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/beets/util/confit.py b/beets/util/confit.py index c9051de53..de22e0adf 100644 --- a/beets/util/confit.py +++ b/beets/util/confit.py @@ -1105,12 +1105,19 @@ class Filename(Template): they are relative to the current working directory. This helps attain the expected behavior when using command-line options. """ - def __init__(self, default=REQUIRED, cwd=None, relative_to=None): + def __init__(self, default=REQUIRED, cwd=None, relative_to=None, + in_app_dir=False): """ `relative_to` is the name of a sibling value that is being validated at the same time. + + `in_app_dir` indicates whether the path should be resolved + inside the application's config directory (even when the setting + does not come from a file). """ super(Filename, self).__init__(default) - self.cwd, self.relative_to = cwd, relative_to + self.cwd = cwd + self.relative_to = relative_to + self.in_app_dir = in_app_dir def __repr__(self): args = [] @@ -1124,6 +1131,9 @@ class Filename(Template): if self.relative_to is not None: args.append('relative_to=' + repr(self.relative_to)) + if self.in_app_dir: + args.append('in_app_dir=True') + return 'Filename({0})'.format(', '.join(args)) def resolve_relative_to(self, view, template): @@ -1198,7 +1208,7 @@ class Filename(Template): path, ) - elif source.filename: + elif source.filename or self.in_app_dir: # From defaults: relative to the app's directory. path = os.path.join(view.root().config_dir(), path) diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index 554c6023d..f025c9775 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -17,6 +17,7 @@ discogs-client library. """ from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin +from beets.util import confit from discogs_client import Release, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError @@ -24,6 +25,7 @@ import beets import logging import re import time +import json log = logging.getLogger('beets') @@ -41,32 +43,51 @@ class DiscogsPlugin(BeetsPlugin): self.config.add({ 'apikey': 'rAzVUQYRaoFjeBjyWuWZ', 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', + 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, }) c_key = self.config['apikey'].get(unicode) c_secret = self.config['apisecret'].get(unicode) + + # Get the OAuth token from a file or log in. try: - token = self.config['token'].get(unicode) - secret = self.config['secret'].get(unicode) - except beets.confit.NotFoundError: + with open(self._tokenfile()) as f: + tokendata = json.load(f) + except IOError: + # No token yet. Generate one. token, secret = self.authenticate(c_key, c_secret) + else: + token = tokendata['token'] + secret = tokendata['secret'] self.discogs_client = Client(USER_AGENT, c_key, c_secret, token, secret) + def _tokenfile(self): + """Get the path to the JSON file for storing the OAuth token. + """ + return self.config['tokenfile'].get(confit.Filename(in_app_dir=True)) + def authenticate(self, c_key, c_secret): + # Get the link for the OAuth page. auth_client = Client(USER_AGENT, c_key, c_secret) _, _, url = auth_client.get_authorize_url() beets.ui.print_("To authenticate with Discogs, visit:") beets.ui.print_(url) + + # Ask for the code and validate it. code = beets.ui.input_("Enter the code:") try: token, secret = auth_client.get_access_token(code) except DiscogsAPIError: raise beets.ui.UserError('Discogs authorization failed') - beets.ui.print_("token: %s\nsecret: %s" % (token, secret)) - beets.ui.print_("Add the above to beets config!") + + # Save the token for later use. + log.debug('Discogs token {0}, secret {1}'.format(token, secret)) + with open(self._tokenfile(), 'w') as f: + json.dump({'token': token, 'secret': secret}, f) + return token, secret def album_distance(self, items, album_info, mapping): From a3a6c017312cd1e4ad4dba3fc36ebd0c61e47518 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 26 Oct 2014 15:50:23 -0700 Subject: [PATCH 5/9] Docs for #1040 (fix #1027) --- docs/changelog.rst | 3 +++ docs/plugins/discogs.rst | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c05d08bce..695635c06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,9 @@ Fixes: during the import process. * Fix a crash in the autotagger when files had only whitespace in their metadata. +* :doc:`/plugins/discogs`: Authenticate with the Discogs server. The plugin + now requires a Discogs account due to new API restrictions. Thanks to + multikatt. 1.3.8 (September 17, 2014) diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index 1afee17d3..31d111dd6 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -14,8 +14,13 @@ install the `discogs-client`_ library by typing:: pip install discogs-client -That's it! Matches from Discogs will now show up during import alongside -matches from MusicBrainz. +You will also need to register for a `Discogs`_ account. The first time you +run beets after enabling the plugin, it will ask you to authorize with Discogs +by visiting the site in a browser. Subsequent runs will not require +re-authorization. + +Matches from Discogs will now show up during import alongside matches from +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. From da1624def7c6c12fadd5e512b169ec3a041eab8d Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 26 Oct 2014 15:56:55 -0700 Subject: [PATCH 6/9] Fun with Sphinx extlinks --- docs/changelog.rst | 2 +- docs/conf.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 695635c06..26a8e5b3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,7 +52,7 @@ Fixes: metadata. * :doc:`/plugins/discogs`: Authenticate with the Discogs server. The plugin now requires a Discogs account due to new API restrictions. Thanks to - multikatt. + :user:`multikatt`. :bug:`1027`, :bug:`1040` 1.3.8 (September 17, 2014) diff --git a/docs/conf.py b/docs/conf.py index e5c81a4c5..a185d9643 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,7 +2,7 @@ AUTHOR = u'Adrian Sampson' # General configuration -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.extlinks'] exclude_patterns = ['_build'] source_suffix = '.rst' @@ -16,20 +16,23 @@ release = '1.3.9' pygments_style = 'sphinx' -# Options for HTML output +# External links to the bug tracker. +extlinks = { + 'bug': ('https://github.com/sampsyo/beets/issues/%s', '#'), + 'user': ('https://github.com/%s', ''), +} +# Options for HTML output html_theme = 'default' htmlhelp_basename = 'beetsdoc' # Options for LaTeX output - latex_documents = [ ('index', 'beets.tex', u'beets Documentation', AUTHOR, 'manual'), ] # Options for manual page output - man_pages = [ ('reference/cli', 'beet', u'music tagger and library organizer', [AUTHOR], 1), From 464f8cdc04ba36a28aaeaed0bf6a85490d7d629c Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 27 Oct 2014 10:54:21 +0100 Subject: [PATCH 7/9] Types plugin: Field types for albums and documentation example --- beetsplug/types.py | 7 +++++++ docs/plugins/types.rst | 9 +++++++++ test/test_types_plugin.py | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/beetsplug/types.py b/beetsplug/types.py index 68aea35c7..e351c8add 100644 --- a/beetsplug/types.py +++ b/beetsplug/types.py @@ -22,6 +22,13 @@ class TypesPlugin(BeetsPlugin): @property def item_types(self): + return self._types() + + @property + def album_types(self): + return self._types() + + def _types(self): if not self.config.exists(): return {} diff --git a/docs/plugins/types.rst b/docs/plugins/types.rst index 978654e8d..b6503f353 100644 --- a/docs/plugins/types.rst +++ b/docs/plugins/types.rst @@ -15,3 +15,12 @@ Here's an example:: types: rating: int + +Now you can assign numeric ratings to tracks and albums and use :ref:`range +queries ` to filter them.:: + + beet modify "My favorite track" rating=5 + beet ls rating:4..5 + + beet modify --album "My favorite album" rating=5 + beet modify --album rating:4..5 diff --git a/test/test_types_plugin.py b/test/test_types_plugin.py index 5e34db9e2..d175525be 100644 --- a/test/test_types_plugin.py +++ b/test/test_types_plugin.py @@ -47,6 +47,22 @@ class TypesPluginTest(unittest.TestCase, TestHelper): out = self.list('myint:1..3') self.assertIn('aaa', out) + def test_album_integer_modify_and_query(self): + self.config['types'] = {'myint': 'int'} + album = self.add_album(albumartist='aaa') + + # Do not match unset values + out = self.list_album('myint:1..3') + self.assertEqual('', out) + + self.modify('-a', 'myint=2') + album.load() + self.assertEqual(album['myint'], 2) + + # Match in range + out = self.list_album('myint:1..3') + self.assertIn('aaa', out) + def test_float_modify_and_query(self): self.config['types'] = {'myfloat': 'float'} item = self.add_item(artist='aaa') @@ -121,6 +137,9 @@ class TypesPluginTest(unittest.TestCase, TestHelper): def list(self, query, fmt='$artist - $album - $title'): return self.run_with_output('ls', '-f', fmt, query).strip() + def list_album(self, query, fmt='$albumartist - $album - $title'): + return self.run_with_output('ls', '-a', '-f', fmt, query).strip() + def mktime(*args): return time.mktime(datetime(*args).timetuple()) From 72db4af7f8b9a35e6324c8e0f71d97ec0645506c Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 27 Oct 2014 09:24:59 -0700 Subject: [PATCH 8/9] Fix #1041: play unicode logging crash --- beetsplug/play.py | 5 ++++- docs/changelog.rst | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index 38ef379dd..c98be40a0 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -101,7 +101,10 @@ def play_music(lib, opts, args): # Invoke the command and log the output. output = util.command_output(command) if output: - log.debug(u'Output of {0}: {1}'.format(command[0], output)) + log.debug(u'Output of {0}: {1}'.format( + util.displayable_path(command[0]), + output.decode('utf8', 'ignore'), + )) ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26a8e5b3e..71bd27bab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,8 @@ Fixes: * :doc:`/plugins/discogs`: Authenticate with the Discogs server. The plugin now requires a Discogs account due to new API restrictions. Thanks to :user:`multikatt`. :bug:`1027`, :bug:`1040` +* :doc:`/plugins/play`: Fix a potential crash when the command outputs special + characters. :bug:`1041` 1.3.8 (September 17, 2014) From dd7cd2f8c213b58e6f2629b80f8e030e922105dd Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Mon, 27 Oct 2014 17:11:39 -0700 Subject: [PATCH 9/9] Detect ALAC with bitrate --- beets/mediafile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 95bbd309d..dddd8a57a 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1272,11 +1272,11 @@ class MediaFile(object): elif (type(self.mgfile).__name__ == 'M4A' or type(self.mgfile).__name__ == 'MP4'): # This hack differentiates AAC and ALAC until we find a more - # deterministic approach. Mutagen only sets the sample rate + # deterministic approach. Mutagen only sets the bitrate # for AAC files. See: # https://github.com/sampsyo/beets/pull/295 - if hasattr(self.mgfile.info, 'sample_rate') and \ - self.mgfile.info.sample_rate > 0: + if hasattr(self.mgfile.info, 'bitrate') and \ + self.mgfile.info.bitrate > 0: self.type = 'aac' else: self.type = 'alac'