mirror of
https://github.com/beetbox/beets.git
synced 2025-12-21 08:04:03 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
e9a532ddac
11 changed files with 125 additions and 16 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
11
docs/conf.py
11
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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <numericquery>` 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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Reference in a new issue