Merge branch 'master' into date-value-field-validation-method

# Conflicts:
#	beets/dbcore/query.py
This commit is contained in:
discopatrick 2017-04-20 13:03:25 +01:00
commit ab7cc8f1ab
14 changed files with 119 additions and 27 deletions

View file

@ -39,6 +39,7 @@ replace:
'\.$': _
'\s+$': ''
'^\s+': ''
'^-': _
path_sep_replace: _
asciify_paths: false
art_filename: cover

View file

@ -33,6 +33,15 @@ from .query import MatchQuery, NullSort, TrueQuery
import six
class DBAccessError(Exception):
"""The SQLite database became inaccessible.
This can happen when trying to read or write the database when, for
example, the database file is deleted or otherwise disappears. There
is probably no way to recover from this error.
"""
class FormattedMapping(collections.Mapping):
"""A `dict`-like formatted view of a model.
@ -680,8 +689,18 @@ class Transaction(object):
"""Execute an SQL statement with substitution values and return
the row ID of the last affected row.
"""
cursor = self.db._connection().execute(statement, subvals)
return cursor.lastrowid
try:
cursor = self.db._connection().execute(statement, subvals)
return cursor.lastrowid
except sqlite3.OperationalError as e:
# In two specific cases, SQLite reports an error while accessing
# the underlying database file. We surface these exceptions as
# DBAccessError so the application can abort.
if e.args[0] in ("attempt to write a readonly database",
"unable to open database file"):
raise DBAccessError(e.args[0])
else:
raise
def script(self, statements):
"""Execute a string containing multiple SQL statements."""

View file

@ -41,6 +41,7 @@ from beets import config
from beets.util import confit, as_string
from beets.autotag import mb
from beets.dbcore import query as db_query
from beets.dbcore import db
import six
# On Windows platforms, use colorama to support "ANSI" terminal colors.
@ -1122,9 +1123,11 @@ def _configure(options):
# special handling lets specified plugins get loaded before we
# finish parsing the command line.
if getattr(options, 'config', None) is not None:
config_path = options.config
overlay_path = options.config
del options.config
config.set_file(config_path)
config.set_file(overlay_path)
else:
overlay_path = None
config.set_args(options)
# Configure the logger.
@ -1133,6 +1136,10 @@ def _configure(options):
else:
log.set_global_level(logging.INFO)
if overlay_path:
log.debug(u'overlaying configuration: {0}',
util.displayable_path(overlay_path))
config_path = config.user_config_path()
if os.path.isfile(config_path):
log.debug(u'user configuration: {0}',
@ -1247,3 +1254,10 @@ def main(args=None):
except KeyboardInterrupt:
# Silently ignore ^C except in verbose mode.
log.debug(u'{}', traceback.format_exc())
except db.DBAccessError as exc:
log.error(
u'database access error: {0}\n'
u'the library file might have a permissions problem',
exc
)
sys.exit(1)

View file

@ -394,10 +394,10 @@ class BeatportPlugin(BeetsPlugin):
# cause a query to return no results, even if they match the artist or
# album title. Use `re.UNICODE` flag to avoid stripping non-english
# word characters.
query = re.sub(r'\W+', ' ', query, re.UNICODE)
query = re.sub(r'\W+', ' ', query, flags=re.UNICODE)
# Strip medium information from query, Things like "CD1" and "disk 1"
# can also negate an otherwise positive result.
query = re.sub(r'\b(CD|disc)\s*\d+', '', query, re.I)
query = re.sub(r'\b(CD|disc)\s*\d+', '', query, flags=re.I)
albums = [self._get_album_info(x)
for x in self.client.search(query)]
return albums

View file

@ -24,6 +24,7 @@ import tempfile
import shlex
import six
from string import Template
import platform
from beets import ui, util, plugins, config
from beets.plugins import BeetsPlugin
@ -183,12 +184,22 @@ class ConvertPlugin(BeetsPlugin):
if not quiet and not pretend:
self._log.info(u'Encoding {0}', util.displayable_path(source))
# Substitute $source and $dest in the argument list.
# On Python 3, we need to construct the command to invoke as a
# Unicode string. On Unix, this is a little unfortunate---the OS is
# expecting bytes---so we use surrogate escaping and decode with the
# argument encoding, which is the same encoding that will then be
# *reversed* to recover the same bytes before invoking the OS. On
# Windows, we want to preserve the Unicode filename "as is."
if not six.PY2:
command = command.decode(util.arg_encoding(), 'surrogateescape')
source = source.decode(util.arg_encoding(), 'surrogateescape')
dest = dest.decode(util.arg_encoding(), 'surrogateescape')
if platform.system() == 'Windows':
source = source.decode(util._fsencoding())
dest = dest.decode(util._fsencoding())
else:
source = source.decode(util.arg_encoding(), 'surrogateescape')
dest = dest.decode(util.arg_encoding(), 'surrogateescape')
# Substitute $source and $dest in the argument list.
args = shlex.split(command)
encode_cmd = []
for i, arg in enumerate(args):

View file

@ -49,29 +49,28 @@ def find_feat_part(artist, albumartist):
"""Attempt to find featured artists in the item's artist fields and
return the results. Returns None if no featured artist found.
"""
feat_part = None
# Look for the album artist in the artist field. If it's not
# present, give up.
albumartist_split = artist.split(albumartist, 1)
if len(albumartist_split) <= 1:
return feat_part
return None
# If the last element of the split (the right-hand side of the
# album artist) is nonempty, then it probably contains the
# featured artist.
elif albumartist_split[-1] != '':
elif albumartist_split[1] != '':
# Extract the featured artist from the right-hand side.
_, feat_part = split_on_feat(albumartist_split[-1])
_, feat_part = split_on_feat(albumartist_split[1])
return feat_part
# Otherwise, if there's nothing on the right-hand side, look for a
# featuring artist on the left-hand side.
else:
lhs, rhs = split_on_feat(albumartist_split[0])
if lhs:
feat_part = lhs
return lhs
return feat_part
return None
class FtInTitlePlugin(plugins.BeetsPlugin):

View file

@ -81,6 +81,11 @@ class PlayPlugin(BeetsPlugin):
action='store',
help=u'add additional arguments to the command',
)
play_command.parser.add_option(
u'-y', u'--yes',
action="store_true",
help=u'skip the warning threshold',
)
play_command.func = self._play_command
return [play_command]
@ -125,8 +130,8 @@ class PlayPlugin(BeetsPlugin):
# 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):
if opts.yes or not self._exceeds_threshold(
selection, command_str, open_args, item_type):
play(command_str, selection, paths, open_args, self._log,
item_type)

View file

@ -49,9 +49,14 @@ New features:
:bug:`2366` :bug:`2495`
* Importing a release with multiple release events now selects the
event based on your :ref:`preferred` countries. :bug:`2501`
* :doc:`/plugins/play`: A new ``-y`` or ``--yes`` parameter lets you skip
the warning message if you enqueue more items than the warning threshold
usually allows.
Fixes:
* In the :ref:`replace` configuration option, we now replace a leading hyphen
(-) with an underscore. :bug:`549` :bug:`2509`
* :doc:`/plugins/absubmit`: Do not filter for supported formats. :bug:`2471`
* :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381`
* :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain``
@ -86,6 +91,10 @@ Fixes:
AAC codec instead of faac. Thanks to :user:`jansol`. :bug:`2484`
* Fix import of multidisc releases with subdirectories, which previously
made each disc be imported separately in different releases. :bug:`2493`
* Invalid date queries now print an error message instead of being silently
ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517`
* When the SQLite database stops being accessible, we now print a friendly
error message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508`
1.4.3 (January 9, 2017)
@ -770,6 +779,8 @@ Fixes:
does not exist, beets creates an empty file before editing it. This fixes an
error on OS X, where the ``open`` command does not work with non-existent
files. :bug:`1480`
* :doc:`/plugins/convert`: Fix a problem with filename encoding on Windows
under Python 3. :bug:`2515` :bug:`2516`
.. _Python bug: http://bugs.python.org/issue16512
.. _ipfs: http://ipfs.io

View file

@ -95,6 +95,10 @@ example::
indicates that you need to insert extra arguments before specifying the
playlist.
The ``--yes`` (or ``-y``) flag to the ``play`` command will skip the warning
message if you choose to play more items than the **warning_threshold**
value usually allows.
Note on the Leakage of the Generated Playlists
----------------------------------------------

View file

@ -72,7 +72,7 @@ box. To extract `rar` files, install the `rarfile`_ package and the
Optional command flags:
* By default, the command copies files your the library directory and
updates the ID3 tags on your music. In order to move the files, instead of
updates the ID3 tags on your music. In order to move the files, instead of
copying, use the ``-m`` (move) option. If you'd like to leave your music
files untouched, try the ``-C`` (don't copy) and ``-W`` (don't write tags)
options. You can also disable this behavior by default in the
@ -409,7 +409,11 @@ import ...``.
* ``-v``: verbose mode; prints out a deluge of debugging information. Please use
this flag when reporting bugs. You can use it twice, as in ``-vv``, to make
beets even more verbose.
* ``-c FILE``: read a specified YAML :doc:`configuration file <config>`.
* ``-c FILE``: read a specified YAML :doc:`configuration file <config>`. This
configuration works as an overlay: rather than replacing your normal
configuration options entirely, the two are merged. Any individual options set
in this config file will override the corresponding settings in your base
configuration.
Beets also uses the ``BEETSDIR`` environment variable to look for
configuration and data.

View file

@ -131,6 +131,7 @@ unexpected behavior on all popular platforms::
'\.$': _
'\s+$': ''
'^\s+': ''
'^-': _
These substitutions remove forward and back slashes, leading dots, and
control characters—all of which is a good idea on any OS. The fourth line

View file

@ -28,12 +28,9 @@ def convert(in_file, out_file, tag):
if not isinstance(tag, bytes):
tag = tag.encode('utf-8')
# On Windows, use Unicode paths. (The test harness gives them to us
# as UTF-8 bytes.)
if platform.system() == 'Windows':
if not PY2:
in_file = in_file.encode(arg_encoding())
out_file = out_file.encode(arg_encoding())
# On Windows, use Unicode paths. On Python 3, we get the actual,
# Unicode filenames. On Python 2, we get them as UTF-8 byes.
if platform.system() == 'Windows' and PY2:
in_file = in_file.decode('utf-8')
out_file = out_file.decode('utf-8')

View file

@ -15,6 +15,7 @@
from __future__ import division, absolute_import, print_function
import sys
import re
import os.path
import unittest
@ -27,6 +28,15 @@ from beets.mediafile import MediaFile
from beets import util
def shell_quote(text):
if sys.version_info[0] < 3:
import pipes
return pipes.quote(text)
else:
import shlex
return shlex.quote(text)
class TestHelper(helper.TestHelper):
def tagged_copy_cmd(self, tag):
@ -39,7 +49,8 @@ class TestHelper(helper.TestHelper):
# A Python script that copies the file and appends a tag.
stub = os.path.join(_common.RSRC, b'convert_stub.py').decode('utf-8')
return u"python '{}' $source $dest {}".format(stub, tag)
return u"{} {} $source $dest {}".format(shell_quote(sys.executable),
shell_quote(stub), tag)
def assertFileTag(self, path, tag): # noqa
"""Assert that the path is a file and the files content ends with `tag`.
@ -273,5 +284,6 @@ class NeverConvertLossyFilesTest(unittest.TestCase, TestHelper,
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')

View file

@ -115,6 +115,20 @@ class PlayPluginTest(unittest.TestCase, TestHelper):
open_mock.assert_not_called()
def test_skip_warning_threshold_bypass(self, open_mock):
self.config['play']['warning_threshold'] = 1
self.other_item = self.add_item(title='another NiceTitle')
expected_playlist = u'{0}\n{1}'.format(
self.item.path.decode('utf-8'),
self.other_item.path.decode('utf-8'))
with control_stdin("a"):
self.run_and_assert(
open_mock,
[u'-y', u'NiceTitle'],
expected_playlist=expected_playlist)
def test_command_failed(self, open_mock):
open_mock.side_effect = OSError(u"some reason")