mirror of
https://github.com/beetbox/beets.git
synced 2025-12-27 19:12:40 +01:00
Merge branch 'master' into date-value-field-validation-method
# Conflicts: # beets/dbcore/query.py
This commit is contained in:
commit
ab7cc8f1ab
14 changed files with 119 additions and 27 deletions
|
|
@ -39,6 +39,7 @@ replace:
|
|||
'\.$': _
|
||||
'\s+$': ''
|
||||
'^\s+': ''
|
||||
'^-': _
|
||||
path_sep_replace: _
|
||||
asciify_paths: false
|
||||
art_filename: cover
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue