Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Mike Cameron 2017-01-10 17:33:45 -05:00
commit 24b02e8215
31 changed files with 324 additions and 135 deletions

View file

@ -12,10 +12,10 @@ environment:
matrix:
- PYTHON: C:\Python27
TOX_ENV: py27-test
- PYTHON: C:\Python34
TOX_ENV: py34-test
- PYTHON: C:\Python35
TOX_ENV: py35-test
- PYTHON: C:\Python36
TOX_ENV: py36-test
# Install Tox for running tests.
install:

View file

@ -19,7 +19,7 @@ import os
from beets.util import confit
__version__ = u'1.4.3'
__version__ = u'1.4.4'
__author__ = u'Adrian Sampson <adrian@radbox.org>'

View file

@ -30,7 +30,11 @@ from beets import config
import six
VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377'
BASE_URL = 'http://musicbrainz.org/'
if util.SNI_SUPPORTED:
BASE_URL = 'https://musicbrainz.org/'
else:
BASE_URL = 'http://musicbrainz.org/'
musicbrainzngs.set_useragent('beets', beets.__version__,
'http://beets.io/')

View file

@ -503,9 +503,13 @@ def _to_epoch_time(date):
"""Convert a `datetime` object to an integer number of seconds since
the (local) Unix epoch.
"""
epoch = datetime.fromtimestamp(0)
delta = date - epoch
return int(delta.total_seconds())
if hasattr(date, 'timestamp'):
# The `timestamp` method exists on Python 3.3+.
return int(date.timestamp())
else:
epoch = datetime.fromtimestamp(0)
delta = date - epoch
return int(delta.total_seconds())
def _parse_periods(pattern):

View file

@ -36,15 +36,11 @@ data from the tags. In turn ``MediaField`` uses a number of
from __future__ import division, absolute_import, print_function
import mutagen
import mutagen.mp3
import mutagen.id3
import mutagen.oggopus
import mutagen.oggvorbis
import mutagen.mp4
import mutagen.flac
import mutagen.monkeysaudio
import mutagen.asf
import mutagen.aiff
import codecs
import datetime
import re
@ -77,6 +73,7 @@ TYPES = {
'mpc': 'Musepack',
'asf': 'Windows Media',
'aiff': 'AIFF',
'dsf': 'DSD Stream File',
}
PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'}
@ -732,7 +729,7 @@ class MP4ImageStorageStyle(MP4ListStorageStyle):
class MP3StorageStyle(StorageStyle):
"""Store data in ID3 frames.
"""
formats = ['MP3', 'AIFF']
formats = ['MP3', 'AIFF', 'DSF']
def __init__(self, key, id3_lang=None, **kwargs):
"""Create a new ID3 storage style. `id3_lang` is the value for
@ -1479,6 +1476,8 @@ class MediaFile(object):
self.type = 'asf'
elif type(self.mgfile).__name__ == 'AIFF':
self.type = 'aiff'
elif type(self.mgfile).__name__ == 'DSF':
self.type = 'dsf'
else:
raise FileTypeError(path, type(self.mgfile).__name__)

View file

@ -34,6 +34,7 @@ from unidecode import unidecode
MAX_FILENAME_LENGTH = 200
WINDOWS_MAGIC_PREFIX = u'\\\\?\\'
SNI_SUPPORTED = sys.version_info >= (2, 7, 9)
class HumanReadableException(Exception):

View file

@ -32,7 +32,10 @@ PIL = 1
IMAGEMAGICK = 2
WEBPROXY = 3
PROXY_URL = 'http://images.weserv.nl/'
if util.SNI_SUPPORTED:
PROXY_URL = 'https://images.weserv.nl/'
else:
PROXY_URL = 'http://images.weserv.nl/'
log = logging.getLogger('beets')

View file

@ -24,7 +24,7 @@ import os
import subprocess
import tempfile
import distutils
from distutils.spawn import find_executable
import requests
from beets import plugins
@ -79,9 +79,10 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):
# Extractor found, will exit with an error if not called with
# the correct amount of arguments.
pass
# Get the executable location on the system,
# needed to calculate the sha1 hash.
self.extractor = distutils.spawn.find_executable(self.extractor)
# Get the executable location on the system, which we need
# to calculate the SHA-1 hash.
self.extractor = find_executable(self.extractor)
# Calculate extractor hash.
self.extractor_sha = hashlib.sha1()

View file

@ -65,7 +65,9 @@ class BPMPlugin(BeetsPlugin):
return [cmd]
def command(self, lib, opts, args):
self.get_bpm(lib.items(ui.decargs(args)))
items = lib.items(ui.decargs(args))
write = ui.should_write()
self.get_bpm(items, write)
def get_bpm(self, items, write=False):
overwrite = self.config['overwrite'].get(bool)

View file

@ -6,6 +6,7 @@
host: localhost
port: 8096
username: user
apikey: apikey
password: password
"""
from __future__ import division, absolute_import, print_function
@ -150,7 +151,9 @@ class EmbyUpdate(BeetsPlugin):
# Adding defaults.
config['emby'].add({
u'host': u'http://localhost',
u'port': 8096
u'port': 8096,
u'apikey': None,
u'password': None,
})
self.register_listener('database_change', self.listen_for_db_change)
@ -171,6 +174,11 @@ class EmbyUpdate(BeetsPlugin):
password = config['emby']['password'].get()
token = config['emby']['apikey'].get()
# Check if at least a apikey or password is given.
if not any([password, token]):
self._log.warning(u'Provide at least Emby password or apikey.')
return
# Get user information from the Emby API.
user = get_user(host, port, username)
if not user:

View file

@ -292,8 +292,12 @@ class RemoteArtSource(ArtSource):
class CoverArtArchive(RemoteArtSource):
NAME = u"Cover Art Archive"
URL = 'http://coverartarchive.org/release/{mbid}/front'
GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front'
if util.SNI_SUPPORTED:
URL = 'https://coverartarchive.org/release/{mbid}/front'
GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}/front'
else:
URL = 'http://coverartarchive.org/release/{mbid}/front'
GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front'
def get(self, album, extra):
"""Return the Cover Art Archive and Cover Art Archive release group URLs
@ -394,8 +398,7 @@ class GoogleImages(RemoteArtSource):
class FanartTV(RemoteArtSource):
"""Art from fanart.tv requested using their API"""
NAME = u"fanart.tv"
API_URL = 'http://webservice.fanart.tv/v3/'
API_URL = 'https://webservice.fanart.tv/v3/'
API_ALBUMS = API_URL + 'music/albums/'
PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e'
@ -488,8 +491,8 @@ class ITunesStore(RemoteArtSource):
class Wikipedia(RemoteArtSource):
NAME = u"Wikipedia (queried through DBpedia)"
DBPEDIA_URL = 'http://dbpedia.org/sparql'
WIKIPEDIA_URL = 'http://en.wikipedia.org/w/api.php'
DBPEDIA_URL = 'https://dbpedia.org/sparql'
WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php'
SPARQL_QUERY = u'''PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX dbpprop: <http://dbpedia.org/property/>
PREFIX owl: <http://dbpedia.org/ontology/>

View file

@ -23,7 +23,7 @@ from beets import config
from beets import plugins
from beets.dbcore import types
API_URL = 'http://ws.audioscrobbler.com/2.0/'
API_URL = 'https://ws.audioscrobbler.com/2.0/'
class LastImportPlugin(plugins.BeetsPlugin):

View file

@ -655,7 +655,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
params = {
'client_id': 'beets',
'client_secret': self.config['bing_client_secret'],
'scope': 'http://api.microsofttranslator.com',
'scope': "https://api.microsofttranslator.com",
'grant_type': 'client_credentials',
}
@ -762,7 +762,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
if self.bing_auth_token:
# Extract unique lines to limit API request size per song
text_lines = set(text.split('\n'))
url = ('http://api.microsofttranslator.com/v2/Http.svc/'
url = ('https://api.microsofttranslator.com/v2/Http.svc/'
'Translate?text=%s&to=%s' % ('|'.join(text_lines), to_lang))
r = requests.get(url,
headers={"Authorization ": self.bing_auth_token})

View file

@ -35,14 +35,14 @@ import six
# easier.
class BufferedSocket(object):
"""Socket abstraction that allows reading by line."""
def __init__(self, host, port, sep='\n'):
def __init__(self, host, port, sep=b'\n'):
if host[0] in ['/', '~']:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(os.path.expanduser(host))
else:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, port))
self.buf = ''
self.buf = b''
self.sep = sep
def readline(self):
@ -51,11 +51,11 @@ class BufferedSocket(object):
if not data:
break
self.buf += data
if '\n' in self.buf:
if self.sep in self.buf:
res, self.buf = self.buf.split(self.sep, 1)
return res + self.sep
else:
return ''
return b''
def send(self, data):
self.sock.send(data)
@ -106,24 +106,24 @@ class MPDUpdatePlugin(BeetsPlugin):
return
resp = s.readline()
if 'OK MPD' not in resp:
if b'OK MPD' not in resp:
self._log.warning(u'MPD connection failed: {0!r}', resp)
return
if password:
s.send('password "%s"\n' % password)
s.send(b'password "%s"\n' % password.encode('utf8'))
resp = s.readline()
if 'OK' not in resp:
if b'OK' not in resp:
self._log.warning(u'Authentication failed: {0!r}', resp)
s.send('close\n')
s.send(b'close\n')
s.close()
return
s.send('update\n')
s.send(b'update\n')
resp = s.readline()
if 'updating_db' not in resp:
if b'updating_db' not in resp:
self._log.warning(u'Update failed: {0!r}', resp)
s.send('close\n')
s.send(b'close\n')
s.close()
self._log.info(u'Database updated.')

View file

@ -19,17 +19,41 @@ from __future__ import division, absolute_import, print_function
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand
from beets.ui.commands import PromptChoice
from beets import config
from beets import ui
from beets import util
from os.path import relpath
from tempfile import NamedTemporaryFile
import subprocess
# Indicate where arguments should be inserted into the command string.
# If this is missing, they're placed at the end.
ARGS_MARKER = '$args'
def play(command_str, selection, paths, open_args, log, item_type='track',
keep_open=False):
"""Play items in paths with command_str and optional arguments. If
keep_open, return to beets, otherwise exit once command runs.
"""
# Print number of tracks or albums to be played, log command to be run.
item_type += 's' if len(selection) > 1 else ''
ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
log.debug(u'executing command: {} {!r}', command_str, open_args)
try:
if keep_open:
command = util.shlex_split(command_str)
command = command + open_args
subprocess.call(command)
else:
util.interactive_open(open_args, command_str)
except OSError as exc:
raise ui.UserError(
"Could not play the query: {0}".format(exc))
class PlayPlugin(BeetsPlugin):
def __init__(self):
@ -40,11 +64,12 @@ class PlayPlugin(BeetsPlugin):
'use_folders': False,
'relative_to': None,
'raw': False,
# Backwards compatibility. See #1803 and line 74
'warning_threshold': -2,
'warning_treshold': 100,
'warning_threshold': 100,
})
self.register_listener('before_choose_candidate',
self.before_choose_candidate_listener)
def commands(self):
play_command = Subcommand(
'play',
@ -56,44 +81,17 @@ class PlayPlugin(BeetsPlugin):
action='store',
help=u'add additional arguments to the command',
)
play_command.func = self.play_music
play_command.func = self._play_command
return [play_command]
def play_music(self, lib, opts, args):
"""Execute query, create temporary playlist and execute player
command passing that playlist, at request insert optional arguments.
def _play_command(self, lib, opts, args):
"""The CLI command function for `beet play`. Create a list of paths
from query, determine if tracks or albums are to be played.
"""
command_str = config['play']['command'].get()
if not command_str:
command_str = util.open_anything()
use_folders = config['play']['use_folders'].get(bool)
relative_to = config['play']['relative_to'].get()
raw = config['play']['raw'].get(bool)
warning_threshold = config['play']['warning_threshold'].get(int)
# We use -2 as a default value for warning_threshold to detect if it is
# set or not. We can't use a falsey value because it would have an
# actual meaning in the configuration of this plugin, and we do not use
# -1 because some people might use it as a value to obtain no warning,
# which wouldn't be that bad of a practice.
if warning_threshold == -2:
# if warning_threshold has not been set by user, look for
# warning_treshold, to preserve backwards compatibility. See #1803.
# warning_treshold has the correct default value of 100.
warning_threshold = config['play']['warning_treshold'].get(int)
if relative_to:
relative_to = util.normpath(relative_to)
# Add optional arguments to the player command.
if opts.args:
if ARGS_MARKER in command_str:
command_str = command_str.replace(ARGS_MARKER, opts.args)
else:
command_str = u"{} {}".format(command_str, opts.args)
else:
# Don't include the marker in the command.
command_str = command_str.replace(" " + ARGS_MARKER, "")
# Perform search by album and add folders rather than tracks to
# playlist.
if opts.album:
@ -117,13 +115,52 @@ class PlayPlugin(BeetsPlugin):
paths = [relpath(path, relative_to) for path in paths]
item_type = 'track'
item_type += 's' if len(selection) > 1 else ''
if not selection:
ui.print_(ui.colorize('text_warning',
u'No {0} to play.'.format(item_type)))
return
open_args = self._playlist_or_paths(paths)
command_str = self._command_str(opts.args)
# Check if the selection exceeds configured threshold. If True,
# cancel, otherwise proceed with play command.
if not self._exceeds_threshold(selection, command_str, open_args,
item_type):
play(command_str, selection, paths, open_args, self._log,
item_type)
def _command_str(self, args=None):
"""Create a command string from the config command and optional args.
"""
command_str = config['play']['command'].get()
if not command_str:
return util.open_anything()
# Add optional arguments to the player command.
if args:
if ARGS_MARKER in command_str:
return command_str.replace(ARGS_MARKER, args)
else:
return u"{} {}".format(command_str, args)
else:
# Don't include the marker in the command.
return command_str.replace(" " + ARGS_MARKER, "")
def _playlist_or_paths(self, paths):
"""Return either the raw paths of items or a playlist of the items.
"""
if config['play']['raw']:
return paths
else:
return [self._create_tmp_playlist(paths)]
def _exceeds_threshold(self, selection, command_str, open_args,
item_type='track'):
"""Prompt user whether to abort if playlist exceeds threshold. If
True, cancel playback. If False, execute play command.
"""
warning_threshold = config['play']['warning_threshold'].get(int)
# Warn user before playing any huge playlists.
if warning_threshold and len(selection) > warning_threshold:
ui.print_(ui.colorize(
@ -132,20 +169,9 @@ class PlayPlugin(BeetsPlugin):
len(selection), item_type)))
if ui.input_options((u'Continue', u'Abort')) == 'a':
return
return True
ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type))
if raw:
open_args = paths
else:
open_args = [self._create_tmp_playlist(paths)]
self._log.debug(u'executing command: {} {!r}', command_str, open_args)
try:
util.interactive_open(open_args, command_str)
except OSError as exc:
raise ui.UserError(
"Could not play the query: {0}".format(exc))
return False
def _create_tmp_playlist(self, paths_list):
"""Create a temporary .m3u file. Return the filename.
@ -155,3 +181,21 @@ class PlayPlugin(BeetsPlugin):
m3u.write(item + b'\n')
m3u.close()
return m3u.name
def before_choose_candidate_listener(self, session, task):
"""Append a "Play" choice to the interactive importer prompt.
"""
return [PromptChoice('y', 'plaY', self.importer_play)]
def importer_play(self, session, task):
"""Get items from current import task and send to play function.
"""
selection = task.items
paths = [item.path for item in selection]
open_args = self._playlist_or_paths(paths)
command_str = self._command_str()
if not self._exceeds_threshold(selection, command_str, open_args):
play(command_str, selection, paths, open_args, self._log,
keep_open=True)

View file

@ -23,7 +23,6 @@ import warnings
import re
from six.moves import zip
from beets import logging
from beets import ui
from beets.plugins import BeetsPlugin
from beets.util import syspath, command_output, displayable_path, py3_path
@ -194,8 +193,8 @@ class Bs1770gainBackend(Backend):
"""
# Construct shell command.
cmd = [self.command]
cmd = cmd + [self.method]
cmd = cmd + ['-p']
cmd += [self.method]
cmd += ['-p']
# Workaround for Windows: the underlying tool fails on paths
# with the \\?\ prefix, so we don't use it here. This
@ -227,7 +226,7 @@ class Bs1770gainBackend(Backend):
':|done\\.\\s)', re.DOTALL | re.UNICODE)
results = re.findall(regex, data)
for parts in results[0:num_lines]:
part = parts.split(b'\n')
part = parts.split(u'\n')
if len(part) == 0:
self._log.debug(u'bad tool output: {0!r}', text)
raise ReplayGainError(u'bs1770gain failed')
@ -794,7 +793,7 @@ class ReplayGainPlugin(BeetsPlugin):
"command": CommandBackend,
"gstreamer": GStreamerBackend,
"audiotools": AudioToolsBackend,
"bs1770gain": Bs1770gainBackend
"bs1770gain": Bs1770gainBackend,
}
def __init__(self):
@ -934,8 +933,6 @@ class ReplayGainPlugin(BeetsPlugin):
"""Return the "replaygain" ui subcommand.
"""
def func(lib, opts, args):
self._log.setLevel(logging.INFO)
write = ui.should_write()
if opts.album:

View file

@ -1,10 +1,33 @@
Changelog
=========
1.4.3 (in development)
1.4.4 (in development)
----------------------
Features:
New features:
* Added support for DSF files, once a future version of Mutagen is released
that supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379`
Fixes:
* :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381`
* :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain``
backend. :bug:`2382`
1.4.3 (January 9, 2017)
-----------------------
Happy new year! This new version includes a cornucopia of new features from
contributors, including new tags related to classical music and a new
:doc:`/plugins/absubmit` for performing acoustic analysis on your music. The
:doc:`/plugins/random` has a new mode that lets you generate time-limited
music---for example, you might generate a random playlist that lasts the
perfect length for your walk to work. We also access as many Web services as
possible over secure connections now---HTTPS everywhere!
The most visible new features are:
* We now support the composer, lyricist, and arranger tags. The MusicBrainz
data source will fetch data for these fields when the next version of
@ -13,17 +36,28 @@ Features:
* A new :doc:`/plugins/absubmit` lets you run acoustic analysis software and
upload the results for others to use. Thanks to :user:`inytar`. :bug:`2253`
:bug:`2342`
* :doc:`/plugins/play`: The plugin now provides an importer prompt choice to
play the music you're about to import. Thanks to :user:`diomekes`.
:bug:`2008` :bug:`2360`
* We now use SSL to access Web services whenever possible. That includes
MusicBrainz itself, several album art sources, some lyrics sources, and
other servers. Thanks to :user:`tigranl`. :bug:`2307`
* :doc:`/plugins/random`: A new ``--time`` option lets you generate a random
playlist that takes a given amount of time. Thanks to :user:`diomekes`.
:bug:`2305` :bug:`2322`
* :doc:`/plugins/zero`: Added ``zero`` command to manually trigger the zero
Some smaller new features:
* :doc:`/plugins/zero`: A new ``zero`` command manually triggers the zero
plugin. Thanks to :user:`SJoshBrown`. :bug:`2274` :bug:`2329`
* :doc:`/plugins/acousticbrainz`: The plugin will avoid re-downloading data
for files that already have it by default. You can override this behavior
using a new ``force`` option. Thanks to :user:`SusannaMaria`. :bug:`2347`
:bug:`2349`
* :doc:`/plugins/bpm`: The ``import.write`` configuration option now
decides whether or not to write tracks after updating their BPM. :bug:`1992`
Fixes:
And the fixes:
* :doc:`/plugins/bpd`: Fix a crash on non-ASCII MPD commands. :bug:`2332`
* :doc:`/plugins/scrub`: Avoid a crash when files cannot be read or written.
@ -34,13 +68,21 @@ Fixes:
filesystem. :bug:`2353`
* :doc:`/plugins/discogs`: Improve the handling of releases that contain
subtracks. :bug:`2318`
* :doc:`/plugins/discogs`: Fix a crash when a release did not contain Format
information, and increased robustness when other fields are missing.
* :doc:`/plugins/discogs`: Fix a crash when a release does not contain format
information, and increase robustness when other fields are missing.
:bug:`2302`
* :doc:`/plugins/lyrics`: The plugin now reports a beets-specific User-Agent
header when requesting lyrics. :bug:`2357`
* :doc:`/plugins/embyupdate`: The plugin now checks whether an API key or a
password is provided in the configuration.
* :doc:`/plugins/play`: The misspelled configuration option
``warning_treshold`` is no longer supported.
For plugin developers: new importer prompt choices (see :ref:`append_prompt_choices`), you can now provide new candidates for the user to consider.
For plugin developers: when providing new importer prompt choices (see
:ref:`append_prompt_choices`), you can now provide new candidates for the user
to consider. For example, you might provide an alternative strategy for
picking between the available alternatives or for looking up a release on
MusicBrainz.
1.4.2 (December 16, 2016)

View file

@ -16,7 +16,7 @@ project = u'beets'
copyright = u'2016, Adrian Sampson'
version = '1.4'
release = '1.4.3'
release = '1.4.4'
pygments_style = 'sphinx'

View file

@ -137,7 +137,7 @@ favorite text editor. The file will start out empty, but here's good
place to start::
directory: ~/music
library: ~/data/musiclibrary.blb
library: ~/data/musiclibrary.db
Change that first path to a directory where you'd like to keep your music. Then,
for ``library``, choose a good place to keep a database file that keeps an index

View file

@ -1,19 +1,36 @@
AcousticBrainz Submit Plugin
============================
The `absubmit` plugin uses the `streaming_extractor_music`_ program to analyze an audio file and calculate different acoustic properties of the audio. The plugin then uploads this metadata to the AcousticBrainz server. The plugin does this when calling the ``beet absumbit [QUERY]`` command or on importing if the `auto` configuration option is set to ``yes``.
The `absubmit` plugin lets you submit acoustic analysis results to the
`AcousticBrainz`_ server.
Installation
------------
The `absubmit` plugin requires the `streaming_extractor_music`_ program to run. Its source can be found on `GitHub`_, and while it is possible to compile the extractor from source, AcousticBrainz would prefer if you used their binary (see the AcousticBrainz `FAQ`_).
The `absubmit` also plugin requires `requests`_, which you can install using `pip_` by typing:
The `absubmit` also plugin requires `requests`_, which you can install using `pip_` by typing::
pip install requests
After installing both the extractor binary and requests you can enable the plugin ``absubmit`` in your configuration (see :ref:`using-plugins`).
Submitting Data
---------------
Type::
beet absubmit [QUERY]
to run the analysis program and upload its results.
The plugin works on music with a MusicBrainz track ID attached. The plugin
will also skip music that the analysis tool doesn't support.
`streaming_extractor_music`_ currently supports files with the extensions
``mp3``, ``ogg``, ``oga``, ``flac``, ``mp4``, ``m4a``, ``m4r``, ``m4b``,
``m4p``, ``aac``, ``wma``, ``asf``, ``mpc``, ``wv``, ``spx``, ``tta``,
``3g2``, ``aif``, ``aiff`` and ``ape``.
Configuration
-------------
@ -21,7 +38,7 @@ To configure the plugin, make a ``absubmit:`` section in your configuration file
- **auto**: Analyze every file on import. Otherwise, you need to use the ``beet absubmit`` command explicitly.
Default: ``no``
- **extractor**: The path to the `streaming_extractor_music`_ binary.
- **extractor**: The absolute path to the `streaming_extractor_music`_ binary.
Default: search for the program in your ``$PATH``
.. _streaming_extractor_music: http://acousticbrainz.org/download
@ -29,3 +46,4 @@ To configure the plugin, make a ``absubmit:`` section in your configuration file
.. _pip: http://www.pip-installer.org/
.. _requests: http://docs.python-requests.org/en/master/
.. _github: https://github.com/MTG/essentia
.. _AcousticBrainz: https://acousticbrainz.org

View file

@ -22,6 +22,21 @@ for instance, with ``mpc`` you can do something like::
beet bpm $(mpc |head -1|tr -d "-")
If :ref:`import.write <config-import-write>` is ``yes``, the song's tags are
written to disk.
Configuration
-------------
To configure the plugin, make a ``bpm:`` section in your configuration file.
The available options are:
- **max_strokes**: The maximum number of strokes to accept when tapping out the
BPM.
Default: 3.
- **overwrite**: Overwrite the track's existing BPM.
Default: ``yes``.
Credit
------

View file

@ -3,7 +3,7 @@ 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::
To use ``embyupdate`` plugin, enable it in your configuration (see :ref:`using-plugins`). Then, you'll 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

View file

@ -4,8 +4,8 @@ Play Plugin
The ``play`` plugin allows you to pass the results of a query to a music
player in the form of an m3u playlist or paths on the command line.
Usage
-----
Command Line Usage
------------------
To use the ``play`` plugin, enable it in your configuration (see
:ref:`using-plugins`). Then use it by invoking the ``beet play`` command with
@ -29,6 +29,18 @@ would on the command-line)::
While playing you'll be able to interact with the player if it is a
command-line oriented, and you'll get its output in real time.
Interactive Usage
-----------------
The `play` plugin can also be invoked during an import. If enabled, the plugin
adds a `plaY` option to the prompt, so pressing `y` will execute the configured
command and play the items currently being imported.
Once the configured command exits, you will be returned to the import
decision prompt. If your player is configured to run in the background (in a
client/server setup), the music will play until you choose to stop it, and the
import operation continues immediately.
Configuration
-------------

View file

@ -229,7 +229,7 @@ Default sort order to use when fetching items from the database. Defaults to
sort_album
~~~~~~~~~~
Default sort order to use when fetching items from the database. Defaults to
Default sort order to use when fetching albums from the database. Defaults to
``albumartist+ album+``. Explicit sort orders override this default.
.. _sort_case_insensitive:
@ -387,6 +387,8 @@ file that looks like this::
These options are available in this section:
.. _config-import-write:
write
~~~~~
@ -814,21 +816,14 @@ Example
Here's an example file::
library: /var/music.blb
directory: /var/mp3
import:
copy: yes
write: yes
resume: ask
quiet_fallback: skip
timid: no
log: beetslog.txt
ignore: .AppleDouble ._* *~ .DS_Store
ignore_hidden: yes
art_filename: albumart
plugins: bpd
pluginpath: ~/beets/myplugins
threaded: yes
ui:
color: yes

View file

@ -56,7 +56,7 @@ if 'sdist' in sys.argv:
setup(
name='beets',
version='1.4.3',
version='1.4.4',
description='music tagger and library organizer',
author='Adrian Sampson',
author_email='adrian@radbox.org',

BIN
test/rsrc/empty.dsf Normal file

Binary file not shown.

BIN
test/rsrc/full.dsf Normal file

Binary file not shown.

BIN
test/rsrc/unparseable.dsf Normal file

Binary file not shown.

View file

@ -154,7 +154,7 @@ class CombinedTest(FetchImageHelper, UseThePlugin):
.format(ASIN)
AAO_URL = 'http://www.albumart.org/index_detail.php?asin={0}' \
.format(ASIN)
CAA_URL = 'http://coverartarchive.org/release/{0}/front' \
CAA_URL = 'coverartarchive.org/release/{0}/front' \
.format(MBID)
def setUp(self):
@ -202,12 +202,17 @@ class CombinedTest(FetchImageHelper, UseThePlugin):
self.assertEqual(responses.calls[-1].request.url, self.AAO_URL)
def test_main_interface_uses_caa_when_mbid_available(self):
self.mock_response(self.CAA_URL)
self.mock_response("http://" + self.CAA_URL)
self.mock_response("https://" + self.CAA_URL)
album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN)
candidate = self.plugin.art_for_album(album, None)
self.assertIsNotNone(candidate)
self.assertEqual(len(responses.calls), 1)
self.assertEqual(responses.calls[0].request.url, self.CAA_URL)
if util.SNI_SUPPORTED:
url = "https://" + self.CAA_URL
else:
url = "http://" + self.CAA_URL
self.assertEqual(responses.calls[0].request.url, url)
def test_local_only_does_not_access_network(self):
album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN)

View file

@ -175,7 +175,11 @@ class ImageStructureTestMixin(ArtTestMixin):
class ExtendedImageStructureTestMixin(ImageStructureTestMixin):
"""Checks for additional attributes in the image structure."""
"""Checks for additional attributes in the image structure.
Like the base `ImageStructureTestMixin`, per-format test classes
should include this mixin to add image-related tests.
"""
def assertExtendedImageAttributes(self, image, desc=None, type=None): # noqa
self.assertEqual(image.desc, desc)
@ -294,8 +298,23 @@ class GenreListTestMixin(object):
class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
_common.TempDirMixin):
"""Test writing and reading tags. Subclasses must set ``extension`` and
``audio_properties``.
"""Test writing and reading tags. Subclasses must set ``extension``
and ``audio_properties``.
The basic tests for all audio formats encompass three files provided
in our `rsrc` folder: `full.*`, `empty.*`, and `unparseable.*`.
Respectively, they should contain a full slate of common fields
listed in `full_initial_tags` below; no fields contents at all; and
an unparseable release date field.
To add support for a new file format to MediaFile, add these three
files and then create a `ReadWriteTestBase` subclass by copying n'
pasting one of the existing subclasses below. You will want to
update the `format` field in that subclass, and you will probably
need to fiddle with the `bitrate` and other format-specific fields.
You can also add image tests (using an additional `image.*` fixture
file) by including one of the image-related mixins.
"""
full_initial_tags = {
@ -554,6 +573,9 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
self.assertEqual(mediafile.disctotal, None)
def test_unparseable_date(self):
"""The `unparseable.*` fixture should not crash but should return None
for all parts of the release date.
"""
mediafile = self._mediafile_fixture('unparseable')
self.assertIsNone(mediafile.date)
@ -887,6 +909,29 @@ class AIFFTest(ReadWriteTestBase, unittest.TestCase):
}
# Check whether we have a Mutagen version with DSF support. We can
# remove this once we require a version that includes the feature.
try:
import mutagen.dsf # noqa
except:
HAVE_DSF = False
else:
HAVE_DSF = True
@unittest.skipIf(not HAVE_DSF, "Mutagen does not have DSF support")
class DSFTest(ReadWriteTestBase, unittest.TestCase):
extension = 'dsf'
audio_properties = {
'length': 0.01,
'bitrate': 11289600,
'format': u'DSD Stream File',
'samplerate': 5644800,
'bitdepth': 1,
'channels': 2,
}
class MediaFieldTest(unittest.TestCase):
def test_properties_from_fields(self):

View file

@ -115,15 +115,6 @@ class PlayPluginTest(unittest.TestCase, TestHelper):
open_mock.assert_not_called()
def test_warning_threshold_backwards_compat(self, open_mock):
self.config['play']['warning_treshold'] = 1
self.add_item(title=u'another NiceTitle')
with control_stdin("a"):
self.run_command(u'play', u'nice')
open_mock.assert_not_called()
def test_command_failed(self, open_mock):
open_mock.side_effect = OSError(u"some reason")