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' 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 f859e48f7..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') @@ -31,16 +33,62 @@ 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', + 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, }) - self.discogs_client = Client('beets/%s +http://beets.radbox.org/' % - beets.__version__) + + 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: + 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') + + # 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): """Returns the album distance. 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. """ 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/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/changelog.rst b/docs/changelog.rst index c05d08bce..71bd27bab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,6 +50,11 @@ 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 + :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) 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), 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. 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())