mirror of
https://github.com/beetbox/beets.git
synced 2026-02-12 02:12:10 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
24b02e8215
31 changed files with 324 additions and 135 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>'
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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/')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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
BIN
test/rsrc/empty.dsf
Normal file
Binary file not shown.
BIN
test/rsrc/full.dsf
Normal file
BIN
test/rsrc/full.dsf
Normal file
Binary file not shown.
BIN
test/rsrc/unparseable.dsf
Normal file
BIN
test/rsrc/unparseable.dsf
Normal file
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue