mirror of
https://github.com/beetbox/beets.git
synced 2026-02-21 23:03:26 +01:00
Merge branch 'master' into play_opt_arg,
to make AppVeyor-builds possible.
This commit is contained in:
commit
d05e251a14
23 changed files with 260 additions and 77 deletions
|
|
@ -271,6 +271,13 @@ class Model(object):
|
|||
else:
|
||||
return base_keys
|
||||
|
||||
@classmethod
|
||||
def all_keys(self):
|
||||
"""Get a list of available keys for objects of this type.
|
||||
Includes fixed and computed fields.
|
||||
"""
|
||||
return list(self._fields) + self._getters().keys()
|
||||
|
||||
# Act like a dictionary.
|
||||
|
||||
def update(self, values):
|
||||
|
|
|
|||
|
|
@ -766,7 +766,10 @@ class FixedFieldSort(FieldSort):
|
|||
def order_clause(self):
|
||||
order = "ASC" if self.ascending else "DESC"
|
||||
if self.case_insensitive:
|
||||
field = 'LOWER({})'.format(self.field)
|
||||
field = '(CASE ' \
|
||||
'WHEN TYPEOF({0})="text" THEN LOWER({0}) ' \
|
||||
'WHEN TYPEOF({0})="blob" THEN LOWER({0}) ' \
|
||||
'ELSE {0} END)'.format(self.field)
|
||||
else:
|
||||
field = self.field
|
||||
return "{0} {1}".format(field, order)
|
||||
|
|
|
|||
|
|
@ -1408,7 +1408,8 @@ def group_albums(session):
|
|||
if task.skip:
|
||||
continue
|
||||
tasks = []
|
||||
for _, items in itertools.groupby(task.items, group):
|
||||
sorted_items = sorted(task.items, key=group)
|
||||
for _, items in itertools.groupby(sorted_items, group):
|
||||
items = list(items)
|
||||
task = ImportTask(task.toppath, [i.path for i in items],
|
||||
items)
|
||||
|
|
|
|||
|
|
@ -479,15 +479,6 @@ class Item(LibModel):
|
|||
i.mtime = i.current_mtime() # Initial mtime.
|
||||
return i
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls):
|
||||
"""Returns Item fields available for queries and format strings."""
|
||||
plugin_fields = []
|
||||
for plugin in plugins.find_plugins():
|
||||
plugin_fields += plugin.template_fields.keys()
|
||||
return (cls._fields.keys() + cls._getters().keys() +
|
||||
cls._types.keys()), plugin_fields
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set the item's value for a standard field or a flexattr.
|
||||
"""
|
||||
|
|
@ -790,26 +781,21 @@ class Item(LibModel):
|
|||
if beets.config['asciify_paths']:
|
||||
subpath = unidecode(subpath)
|
||||
|
||||
# Truncate components and remove forbidden characters.
|
||||
subpath = util.sanitize_path(subpath, self._db.replacements)
|
||||
|
||||
# Encode for the filesystem.
|
||||
if not fragment:
|
||||
subpath = bytestring_path(subpath)
|
||||
|
||||
# Preserve extension.
|
||||
_, extension = os.path.splitext(self.path)
|
||||
if fragment:
|
||||
# Outputting Unicode.
|
||||
extension = extension.decode('utf8', 'ignore')
|
||||
subpath += extension.lower()
|
||||
|
||||
# Truncate too-long components.
|
||||
maxlen = beets.config['max_filename_length'].get(int)
|
||||
if not maxlen:
|
||||
# When zero, try to determine from filesystem.
|
||||
maxlen = util.max_filename_length(self._db.directory)
|
||||
subpath = util.truncate_path(subpath, maxlen)
|
||||
|
||||
subpath, fellback = util.legalize_path(
|
||||
subpath, self._db.replacements, maxlen,
|
||||
os.path.splitext(self.path)[1], fragment
|
||||
)
|
||||
if fellback:
|
||||
# Print an error message if legalization fell back to
|
||||
# default replacements because of the maximum length.
|
||||
log.warning('Fell back to default replacements when naming '
|
||||
'file {}. Configure replacements to avoid lengthening '
|
||||
'the filename.', subpath)
|
||||
|
||||
if fragment:
|
||||
return subpath
|
||||
|
|
@ -915,15 +901,6 @@ class Album(LibModel):
|
|||
getters['albumtotal'] = Album._albumtotal
|
||||
return getters
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls):
|
||||
"""Returns Album fields available for queries and format strings."""
|
||||
plugin_fields = []
|
||||
for plugin in plugins.find_plugins():
|
||||
plugin_fields += plugin.album_template_fields.keys()
|
||||
return (cls._fields.keys() + cls._getters().keys() +
|
||||
cls._types.keys()), plugin_fields
|
||||
|
||||
def items(self):
|
||||
"""Returns an iterable over the items associated with this
|
||||
album.
|
||||
|
|
|
|||
|
|
@ -270,10 +270,23 @@ def _sc_encode(gain, peak):
|
|||
|
||||
# Cover art and other images.
|
||||
|
||||
def _wider_test_jpeg(data):
|
||||
"""Test for a jpeg file following the UNIX file implementation which
|
||||
uses the magic bytes rather than just looking for the bytes b'JFIF'
|
||||
or b'EXIF' at a fixed position.
|
||||
"""
|
||||
if data[:2] == b'\xff\xd8':
|
||||
return 'jpeg'
|
||||
|
||||
|
||||
def _image_mime_type(data):
|
||||
"""Return the MIME type of the image data (a bytestring).
|
||||
"""
|
||||
kind = imghdr.what(None, h=data)
|
||||
# This checks for a jpeg file with only the magic bytes (unrecognized by
|
||||
# imghdr.what). imghdr.what returns none for that type of file, so
|
||||
# _wider_test_jpeg is run in that case. It still returns None if it didn't
|
||||
# match such a jpeg file.
|
||||
kind = imghdr.what(None, h=data) or _wider_test_jpeg(data)
|
||||
if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']:
|
||||
return 'image/{0}'.format(kind)
|
||||
elif kind == 'pgm':
|
||||
|
|
|
|||
|
|
@ -82,17 +82,11 @@ def fields_func(lib, opts, args):
|
|||
names.sort()
|
||||
print_(" " + "\n ".join(names))
|
||||
|
||||
fs, pfs = library.Item.get_fields()
|
||||
print_("Item fields:")
|
||||
_print_rows(fs)
|
||||
print_("Template fields from plugins:")
|
||||
_print_rows(pfs)
|
||||
_print_rows(library.Item.all_keys())
|
||||
|
||||
fs, pfs = library.Album.get_fields()
|
||||
print_("Album fields:")
|
||||
_print_rows(fs)
|
||||
print_("Template fields from plugins:")
|
||||
_print_rows(pfs)
|
||||
_print_rows(library.Album.all_keys())
|
||||
|
||||
|
||||
fields_cmd = ui.Subcommand(
|
||||
|
|
@ -1482,11 +1476,13 @@ def config_func(lib, opts, args):
|
|||
|
||||
def config_edit():
|
||||
"""Open a program to edit the user configuration.
|
||||
An empty config file is created if no existing config file exists.
|
||||
"""
|
||||
path = config.user_config_path()
|
||||
|
||||
editor = os.environ.get('EDITOR')
|
||||
try:
|
||||
if not os.path.isfile(path):
|
||||
open(path, 'w+').close()
|
||||
util.interactive_open(path, editor)
|
||||
except OSError as exc:
|
||||
message = "Could not edit configuration: {0}".format(exc)
|
||||
|
|
|
|||
|
|
@ -545,6 +545,78 @@ def truncate_path(path, length=MAX_FILENAME_LENGTH):
|
|||
return os.path.join(*out)
|
||||
|
||||
|
||||
def _legalize_stage(path, replacements, length, extension, fragment):
|
||||
"""Perform a single round of path legalization steps
|
||||
(sanitation/replacement, encoding from Unicode to bytes,
|
||||
extension-appending, and truncation). Return the path (Unicode if
|
||||
`fragment` is set, `bytes` otherwise) and whether truncation was
|
||||
required.
|
||||
"""
|
||||
# Perform an initial sanitization including user replacements.
|
||||
path = sanitize_path(path, replacements)
|
||||
|
||||
# Encode for the filesystem.
|
||||
if not fragment:
|
||||
path = bytestring_path(path)
|
||||
|
||||
# Preserve extension.
|
||||
path += extension.lower()
|
||||
|
||||
# Truncate too-long components.
|
||||
pre_truncate_path = path
|
||||
path = truncate_path(path, length)
|
||||
|
||||
return path, path != pre_truncate_path
|
||||
|
||||
|
||||
def legalize_path(path, replacements, length, extension, fragment):
|
||||
"""Given a path-like Unicode string, produce a legal path. Return
|
||||
the path and a flag indicating whether some replacements had to be
|
||||
ignored (see below).
|
||||
|
||||
The legalization process (see `_legalize_stage`) consists of
|
||||
applying the sanitation rules in `replacements`, encoding the string
|
||||
to bytes (unless `fragment` is set), truncating components to
|
||||
`length`, appending the `extension`.
|
||||
|
||||
This function performs up to three calls to `_legalize_stage` in
|
||||
case truncation conflicts with replacements (as can happen when
|
||||
truncation creates whitespace at the end of the string, for
|
||||
example). The limited number of iterations iterations avoids the
|
||||
possibility of an infinite loop of sanitation and truncation
|
||||
operations, which could be caused by replacement rules that make the
|
||||
string longer. The flag returned from this function indicates that
|
||||
the path has to be truncated twice (indicating that replacements
|
||||
made the string longer again after it was truncated); the
|
||||
application should probably log some sort of warning.
|
||||
"""
|
||||
|
||||
if fragment:
|
||||
# Outputting Unicode.
|
||||
extension = extension.decode('utf8', 'ignore')
|
||||
|
||||
first_stage_path, _ = _legalize_stage(
|
||||
path, replacements, length, extension, fragment
|
||||
)
|
||||
|
||||
# Convert back to Unicode with extension removed.
|
||||
first_stage_path, _ = os.path.splitext(displayable_path(first_stage_path))
|
||||
|
||||
# Re-sanitize following truncation (including user replacements).
|
||||
second_stage_path, retruncated = _legalize_stage(
|
||||
first_stage_path, replacements, length, extension, fragment
|
||||
)
|
||||
|
||||
# If the path was once again truncated, discard user replacements
|
||||
# and run through one last legalization stage.
|
||||
if retruncated:
|
||||
second_stage_path, _ = _legalize_stage(
|
||||
first_stage_path, None, length, extension, fragment
|
||||
)
|
||||
|
||||
return second_stage_path, retruncated
|
||||
|
||||
|
||||
def str2bool(value):
|
||||
"""Returns a boolean reflecting a human-entered string."""
|
||||
return value.lower() in ('yes', '1', 'true', 't', 'y')
|
||||
|
|
@ -588,8 +660,8 @@ def cpu_count():
|
|||
num = 0
|
||||
elif sys.platform == b'darwin':
|
||||
try:
|
||||
num = int(command_output([b'sysctl', b'-n', b'hw.ncpu']))
|
||||
except ValueError:
|
||||
num = int(command_output([b'/usr/sbin/sysctl', b'-n', b'hw.ncpu']))
|
||||
except (ValueError, OSError, subprocess.CalledProcessError):
|
||||
num = 0
|
||||
else:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -212,8 +212,8 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
return key, checksum
|
||||
|
||||
def _group_by(self, objs, keys, strict):
|
||||
"""Return a dictionary with keys arbitrary concatenations of attributes and
|
||||
values lists of objects (Albums or Items) with those keys.
|
||||
"""Return a dictionary with keys arbitrary concatenations of attributes
|
||||
and values lists of objects (Albums or Items) with those keys.
|
||||
|
||||
If strict, all attributes must be defined for a duplicate match.
|
||||
"""
|
||||
|
|
@ -237,9 +237,13 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
return counts
|
||||
|
||||
def _order(self, objs, tiebreak=None):
|
||||
"""Return objs sorted by descending order of fields in tiebreak dict.
|
||||
"""Return the objects (Items or Albums) sorted by descending
|
||||
order of priority.
|
||||
|
||||
Default ordering is based on attribute completeness.
|
||||
If provided, the `tiebreak` dict indicates the field to use to
|
||||
prioritize the objects. Otherwise, Items are placed in order of
|
||||
"completeness" (objects with more non-null fields come first)
|
||||
and Albums are ordered by their track count.
|
||||
"""
|
||||
if tiebreak:
|
||||
kind = 'items' if all(isinstance(o, Item)
|
||||
|
|
@ -248,9 +252,14 @@ class DuplicatesPlugin(BeetsPlugin):
|
|||
else:
|
||||
kind = Item if all(isinstance(o, Item) for o in objs) else Album
|
||||
if kind is Item:
|
||||
fields = [f for sublist in kind.get_fields() for f in sublist]
|
||||
key = lambda x: len([(a, getattr(x, a, None)) for a in fields
|
||||
if getattr(x, a, None) not in (None, '')])
|
||||
def truthy(v):
|
||||
# Avoid a Unicode warning by avoiding comparison
|
||||
# between a bytes object and the empty Unicode
|
||||
# string ''.
|
||||
return v is not None and \
|
||||
(v != '' if isinstance(v, unicode) else True)
|
||||
fields = kind.all_keys()
|
||||
key = lambda x: sum(1 for f in fields if truthy(getattr(x, f)))
|
||||
else:
|
||||
key = lambda x: len(x.items())
|
||||
|
||||
|
|
|
|||
|
|
@ -436,7 +436,9 @@ class FetchArtPlugin(plugins.BeetsPlugin):
|
|||
self._log.debug(u'downloaded art to: {0}',
|
||||
util.displayable_path(fh.name))
|
||||
return fh.name
|
||||
except (IOError, requests.RequestException):
|
||||
except (IOError, requests.RequestException, TypeError):
|
||||
# Handling TypeError works around a urllib3 bug:
|
||||
# https://github.com/shazow/urllib3/issues/556
|
||||
self._log.debug(u'error fetching art')
|
||||
|
||||
def _is_valid_image_candidate(self, candidate):
|
||||
|
|
|
|||
|
|
@ -388,6 +388,12 @@ class GStreamerBackend(Backend):
|
|||
self._res = self.Gst.ElementFactory.make("audioresample", "res")
|
||||
self._rg = self.Gst.ElementFactory.make("rganalysis", "rg")
|
||||
|
||||
if self._src is None or self._decbin is None or self._conv is None \
|
||||
or self._res is None or self._rg is None:
|
||||
raise FatalReplayGainError(
|
||||
"Failed to load required GStreamer plugins"
|
||||
)
|
||||
|
||||
# We check which files need gain ourselves, so all files given
|
||||
# to rganalsys should have their gain computed, even if it
|
||||
# already exists.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class ZeroPlugin(BeetsPlugin):
|
|||
|
||||
self.config.add({
|
||||
'fields': [],
|
||||
'update_database': False,
|
||||
})
|
||||
|
||||
self.patterns = {}
|
||||
|
|
@ -99,3 +100,5 @@ class ZeroPlugin(BeetsPlugin):
|
|||
if match:
|
||||
self._log.debug(u'{0}: {1} -> None', field, value)
|
||||
tags[field] = None
|
||||
if self.config['update_database']:
|
||||
item[field] = None
|
||||
|
|
|
|||
|
|
@ -26,9 +26,13 @@ New features:
|
|||
:bug:`1104` :bug:`1493`
|
||||
* :doc:`/plugins/plexupdate`: A new ``token`` configuration option lets you
|
||||
specify a key for Plex Home setups. Thanks to :user:`edcarroll`. :bug:`1494`
|
||||
* :doc:`/plugins/zero`: A new ``update_database`` configuration option
|
||||
allows the database to be updated along with files' tags. :bug:`1516`
|
||||
* :doc:`/plugins/play`: A new option `--args`/`-A` has been added, used to
|
||||
hand over options to the player.
|
||||
|
||||
>>>>>>> master
|
||||
|
||||
Fixes:
|
||||
|
||||
* :doc:`/plugins/importfeeds`: Avoid generating incorrect m3u filename when
|
||||
|
|
@ -66,6 +70,26 @@ Fixes:
|
|||
:user:`Somasis`. :bug:`1512`
|
||||
* Some messages in the console UI now use plural nouns correctly. Thanks to
|
||||
:user:`JesseWeinstein`. :bug:`1521`
|
||||
* Sorting numerical fields (such as track) now works again. :bug:`1511`
|
||||
* :doc:`/plugins/replaygain`: Missing GStreamer plugins now cause a helpful
|
||||
error message instead of a crash. :bug:`1518`
|
||||
* Fix an edge case when producing sanitized filenames where the maximum path
|
||||
length conflicted with the :ref:`replace` rules. Thanks to Ben Ockmore.
|
||||
:bug:`496` :bug:`1361`
|
||||
* Fix an incompatibility with OS X 10.11 (where ``/usr/sbin`` seems not to be
|
||||
on the user's path by default).
|
||||
* Fix an incompatibility with certain JPEG files. Here's a relevant `Python
|
||||
bug`_. Thanks to :user:`nathdwek`. :bug:`1545`
|
||||
* Fix the :ref:`group_albums` importer mode so it can handle when files are
|
||||
not already in order by album. :bug:`1550`
|
||||
* The ``fields`` command no longer separates built-in fields from
|
||||
plugin-provided ones. This distinction was becoming increasingly unreliable.
|
||||
* :doc:`/plugins/duplicates`: Fix a Unicode warning when paths contained
|
||||
non-ASCII characters. :bug:`1551`
|
||||
* :doc:`/plugins/fetchart`: Work around a urllib3 bug that could cause a
|
||||
crash. :bug:`1555` :bug:`1556`
|
||||
|
||||
.. _Python bug: http://bugs.python.org/issue16512
|
||||
|
||||
|
||||
1.3.13 (April 24, 2015)
|
||||
|
|
|
|||
|
|
@ -361,11 +361,11 @@ method.
|
|||
|
||||
Here's an example plugin that provides a meaningless new field "foo"::
|
||||
|
||||
class fooplugin(beetsplugin):
|
||||
class FooPlugin(BeetsPlugin):
|
||||
def __init__(self):
|
||||
field = mediafile.mediafield(
|
||||
mediafile.mp3descstoragestyle(u'foo')
|
||||
mediafile.storagestyle(u'foo')
|
||||
field = mediafile.MediaField(
|
||||
mediafile.MP3DescStorageStyle(u'foo'),
|
||||
mediafile.StorageStyle(u'foo')
|
||||
)
|
||||
self.add_media_field('foo', field)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,10 +49,10 @@ configuration file. The available options are:
|
|||
- **google_engine_ID**: The custom search engine to use.
|
||||
Default: The `beets custom search engine`_, which gathers an updated list of
|
||||
sources known to be scrapeable.
|
||||
- **sources**: List of sources to search for lyrics. An asterisk `*` expands
|
||||
- **sources**: List of sources to search for lyrics. An asterisk ``*`` expands
|
||||
to all available sources.
|
||||
Default: ``google lyricwiki lyrics.com musixmatch``, i.e., all sources.
|
||||
*google* source will be automatically deactivated if no `google_engine_ID` is
|
||||
*google* source will be automatically deactivated if no ``google_API_key`` is
|
||||
setup.
|
||||
|
||||
Here's an example of ``config.yaml``::
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ Zero Plugin
|
|||
|
||||
The ``zero`` plugin allows you to null fields in files' metadata tags. Fields
|
||||
can be nulled unconditionally or conditioned on a pattern match. For example,
|
||||
the plugin can strip useless comments like "ripped by MyGreatRipper." This
|
||||
plugin only affects files' tags ; the beets database is left unchanged.
|
||||
the plugin can strip useless comments like "ripped by MyGreatRipper."
|
||||
|
||||
To use the ``zero`` plugin, enable the plugin in your configuration
|
||||
(see :ref:`using-plugins`).
|
||||
|
|
@ -21,6 +20,8 @@ fields to nullify and the conditions for nullifying them:
|
|||
embedded in the media file.
|
||||
* To conditionally filter a field, use ``field: [regexp, regexp]`` to specify
|
||||
regular expressions.
|
||||
* By default this plugin only affects files' tags ; the beets database is left
|
||||
unchanged. To update the tags in the database, set the ``update_database`` option.
|
||||
|
||||
For example::
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ For example::
|
|||
fields: month day genre comments
|
||||
comments: [EAC, LAME, from.+collection, 'ripped by']
|
||||
genre: [rnb, 'power metal']
|
||||
update_database: true
|
||||
|
||||
If a custom pattern is not defined for a given field, the field will be nulled
|
||||
unconditionally.
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ compatibility with Windows-influenced network filesystems like Samba).
|
|||
Trailing dots and trailing whitespace, which can cause problems on Windows
|
||||
clients, are also removed.
|
||||
|
||||
When replacements other than the defaults are used, it is possible that they
|
||||
will increase the length of the path. In the scenario where this leads to a
|
||||
conflict with the maximum filename length, the default replacements will be
|
||||
used to resolve the conflict and beets will display a warning.
|
||||
|
||||
Note that paths might contain special characters such as typographical
|
||||
quotes (``“”``). With the configuration above, those will not be
|
||||
replaced as they don't match the typewriter quote (``"``). To also strip these
|
||||
|
|
|
|||
16
extra/_beet
16
extra/_beet
|
|
@ -18,6 +18,10 @@ function _beet_field_values()
|
|||
{
|
||||
local -a output fieldvals
|
||||
local library="$(beet config|grep library|cut -f 2 -d ' ')"
|
||||
if [ -z "$library" ]; then
|
||||
# Use default library location if there is no user defined one
|
||||
library="~/.config/beets/library.db"
|
||||
fi
|
||||
output=$(sqlite3 ${~library} "select distinct $1 from items;")
|
||||
case $1
|
||||
in
|
||||
|
|
@ -50,7 +54,7 @@ query=( \( "$matchquery" ":query:query string:{_beet_query}" \) \( "$matchquery"
|
|||
modify=( \( "$matchmodify" ":modify:modify string:$modifyelem" \) \( "$matchmodify" ":modify:modify string:$modifyelem" \) \# )
|
||||
|
||||
# arguments to _regex_arguments for completing files and directories
|
||||
local -a files dirs
|
||||
local -a files dirs
|
||||
files=("$matchany" ':file:file:_files')
|
||||
dirs=("$matchany" ':dir:directory:_dirs')
|
||||
|
||||
|
|
@ -82,7 +86,7 @@ retagopt='-L:retag items matching a query:${query[@]}'
|
|||
skipopt='-i:skip already-imported directories'
|
||||
noskipopt='-I:do not skip already-imported directories'
|
||||
flatopt='--flat:import an entire tree as a single album'
|
||||
groupopt='-g:group tracks in a folder into seperate albums'
|
||||
groupopt='-g:group tracks in a folder into seperate albums'
|
||||
editopt='-e:edit user configuration with $EDITOR'
|
||||
defaultopt='-d:include the default configuration'
|
||||
copynomoveopt='-c:copy instead of moving'
|
||||
|
|
@ -184,7 +188,7 @@ do
|
|||
options=( "${reply[@]}" \# "${query[@]}" )
|
||||
;;
|
||||
(stats)
|
||||
_regex_words options "stats options" "$helpopt" "$exactopt"
|
||||
_regex_words options "stats options" "$helpopt" "$exactopt"
|
||||
options=( "${reply[@]}" \# "${query[@]}" )
|
||||
;;
|
||||
(update)
|
||||
|
|
@ -200,9 +204,9 @@ do
|
|||
;;
|
||||
(help)
|
||||
# The help subcommand is treated separately
|
||||
continue
|
||||
continue
|
||||
;;
|
||||
(*) # completions for plugin commands are generated using _beet_subcmd_options
|
||||
(*) # completions for plugin commands are generated using _beet_subcmd_options
|
||||
_beet_subcmd_options "$subcmd"
|
||||
options=( \( "${reply[@]}" \# "${query[@]}" \) )
|
||||
;;
|
||||
|
|
@ -237,6 +241,6 @@ zstyle ":completion:${curcontext}:" tag-order '! options'
|
|||
# Execute the completion function
|
||||
_beet "$@"
|
||||
|
||||
# Local Variables:
|
||||
# Local Variables:
|
||||
# mode:shell-script
|
||||
# End:
|
||||
|
|
|
|||
BIN
test/rsrc/only-magic-bytes.jpg
Normal file
BIN
test/rsrc/only-magic-bytes.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 622 B |
|
|
@ -452,8 +452,7 @@ class DestinationTest(_common.TestCase):
|
|||
self.assertEqual(self.i.destination(),
|
||||
np('base/one/_.mp3'))
|
||||
|
||||
@unittest.skip('unimplemented: #496')
|
||||
def test_truncation_does_not_conflict_with_replacement(self):
|
||||
def test_legalize_path_one_for_one_replacement(self):
|
||||
# Use a replacement that should always replace the last X in any
|
||||
# path component with a Z.
|
||||
self.lib.replacements = [
|
||||
|
|
@ -466,7 +465,23 @@ class DestinationTest(_common.TestCase):
|
|||
|
||||
# The final path should reflect the replacement.
|
||||
dest = self.i.destination()
|
||||
self.assertTrue('XZ' in dest)
|
||||
self.assertEqual(dest[-2:], 'XZ')
|
||||
|
||||
def test_legalize_path_one_for_many_replacement(self):
|
||||
# Use a replacement that should always replace the last X in any
|
||||
# path component with four Zs.
|
||||
self.lib.replacements = [
|
||||
(re.compile(r'X$'), u'ZZZZ'),
|
||||
]
|
||||
|
||||
# Construct an item whose untruncated path ends with a Y but whose
|
||||
# truncated version ends with an X.
|
||||
self.i.title = 'X' * 300 + 'Y'
|
||||
|
||||
# The final path should ignore the user replacement and create a path
|
||||
# of the correct length, containing Xs.
|
||||
dest = self.i.destination()
|
||||
self.assertEqual(dest[-2:], 'XX')
|
||||
|
||||
|
||||
class ItemFormattedMappingTest(_common.LibTestCase):
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ class MBLibraryTest(unittest.TestCase):
|
|||
ai = list(mb.match_album('hello', 'there'))[0]
|
||||
|
||||
sp.assert_called_with(artist='hello', release='there', limit=5)
|
||||
gp.assert_calledwith(mbid)
|
||||
gp.assert_called_with(mbid, mock.ANY)
|
||||
self.assertEqual(ai.tracks[0].title, 'foo')
|
||||
self.assertEqual(ai.album, 'hi')
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,17 @@ class EdgeTest(unittest.TestCase):
|
|||
f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, 'oldape.ape'))
|
||||
self.assertEqual(f.bitrate, 0)
|
||||
|
||||
def test_only_magic_bytes_jpeg(self):
|
||||
# Some jpeg files can only be recognized by their magic bytes and as
|
||||
# such aren't recognized by imghdr. Ensure that this still works thanks
|
||||
# to our own follow up mimetype detection based on
|
||||
# https://github.com/file/file/blob/master/magic/Magdir/jpeg#L12
|
||||
f = open(os.path.join(_common.RSRC, 'only-magic-bytes.jpg'), 'rb')
|
||||
jpg_data = f.read()
|
||||
self.assertEqual(
|
||||
beets.mediafile._image_mime_type(jpg_data),
|
||||
'image/jpeg')
|
||||
|
||||
|
||||
class InvalidValueToleranceTest(unittest.TestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -34,21 +34,21 @@ class DummyDataTestCase(_common.TestCase):
|
|||
albums = [_common.album() for _ in range(3)]
|
||||
albums[0].album = "Album A"
|
||||
albums[0].genre = "Rock"
|
||||
albums[0].year = "2001"
|
||||
albums[0].year = 2001
|
||||
albums[0].flex1 = "Flex1-1"
|
||||
albums[0].flex2 = "Flex2-A"
|
||||
albums[0].albumartist = "Foo"
|
||||
albums[0].albumartist_sort = None
|
||||
albums[1].album = "Album B"
|
||||
albums[1].genre = "Rock"
|
||||
albums[1].year = "2001"
|
||||
albums[1].year = 2001
|
||||
albums[1].flex1 = "Flex1-2"
|
||||
albums[1].flex2 = "Flex2-A"
|
||||
albums[1].albumartist = "Bar"
|
||||
albums[1].albumartist_sort = None
|
||||
albums[2].album = "Album C"
|
||||
albums[2].genre = "Jazz"
|
||||
albums[2].year = "2005"
|
||||
albums[2].year = 2005
|
||||
albums[2].flex1 = "Flex1-1"
|
||||
albums[2].flex2 = "Flex2-B"
|
||||
albums[2].albumartist = "Baz"
|
||||
|
|
@ -67,6 +67,7 @@ class DummyDataTestCase(_common.TestCase):
|
|||
items[0].album_id = albums[0].id
|
||||
items[0].artist_sort = None
|
||||
items[0].path = "/path0.mp3"
|
||||
items[0].track = 1
|
||||
items[1].title = 'Baz qux'
|
||||
items[1].artist = 'Two'
|
||||
items[1].album = 'Baz'
|
||||
|
|
@ -77,6 +78,7 @@ class DummyDataTestCase(_common.TestCase):
|
|||
items[1].album_id = albums[0].id
|
||||
items[1].artist_sort = None
|
||||
items[1].path = "/patH1.mp3"
|
||||
items[1].track = 2
|
||||
items[2].title = 'Beets 4 eva'
|
||||
items[2].artist = 'Three'
|
||||
items[2].album = 'Foo'
|
||||
|
|
@ -87,6 +89,7 @@ class DummyDataTestCase(_common.TestCase):
|
|||
items[2].album_id = albums[1].id
|
||||
items[2].artist_sort = None
|
||||
items[2].path = "/paTH2.mp3"
|
||||
items[2].track = 3
|
||||
items[3].title = 'Beets 4 eva'
|
||||
items[3].artist = 'Three'
|
||||
items[3].album = 'Foo2'
|
||||
|
|
@ -97,6 +100,7 @@ class DummyDataTestCase(_common.TestCase):
|
|||
items[3].album_id = albums[2].id
|
||||
items[3].artist_sort = None
|
||||
items[3].path = "/PATH3.mp3"
|
||||
items[3].track = 4
|
||||
for item in items:
|
||||
self.lib.add(item)
|
||||
|
||||
|
|
@ -399,6 +403,7 @@ class CaseSensitivityTest(DummyDataTestCase, _common.TestCase):
|
|||
item.flex2 = "flex2-A"
|
||||
item.album_id = album.id
|
||||
item.artist_sort = None
|
||||
item.track = 10
|
||||
self.lib.add(item)
|
||||
|
||||
self.new_album = album
|
||||
|
|
@ -451,6 +456,17 @@ class CaseSensitivityTest(DummyDataTestCase, _common.TestCase):
|
|||
self.assertEqual(results[0].flex1, 'Flex1-0')
|
||||
self.assertEqual(results[-1].flex1, 'flex1')
|
||||
|
||||
def test_case_sensitive_only_affects_text(self):
|
||||
config['sort_case_insensitive'] = True
|
||||
q = 'track+'
|
||||
results = list(self.lib.items(q))
|
||||
# If the numerical values were sorted as strings,
|
||||
# then ['1', '10', '2'] would be valid.
|
||||
print([r.track for r in results])
|
||||
self.assertEqual(results[0].track, 1)
|
||||
self.assertEqual(results[1].track, 2)
|
||||
self.assertEqual(results[-1].track, 10)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
|
|
|||
|
|
@ -88,6 +88,23 @@ class ZeroPluginTest(unittest.TestCase, TestHelper):
|
|||
self.assertEqual(item['year'], 2000)
|
||||
self.assertIsNone(mediafile.year)
|
||||
|
||||
def test_change_database(self):
|
||||
item = self.add_item_fixture(year=2000)
|
||||
item.write()
|
||||
mediafile = MediaFile(item.path)
|
||||
self.assertEqual(2000, mediafile.year)
|
||||
|
||||
config['zero'] = {
|
||||
'fields': ['year'],
|
||||
'update_database': True,
|
||||
}
|
||||
self.load_plugins('zero')
|
||||
|
||||
item.write()
|
||||
mediafile = MediaFile(item.path)
|
||||
self.assertEqual(item['year'], 0)
|
||||
self.assertIsNone(mediafile.year)
|
||||
|
||||
def test_album_art(self):
|
||||
path = self.create_mediafile_fixture(images=['jpg'])
|
||||
item = Item.from_path(path)
|
||||
|
|
|
|||
Loading…
Reference in a new issue