Merge branch 'master' into RobustCaseSensitiveDetection

# Conflicts:
#	docs/changelog.rst
This commit is contained in:
Malte Ried 2015-09-03 22:12:33 +02:00
commit 636f0af8b2
8 changed files with 72 additions and 20 deletions

View file

@ -1460,6 +1460,34 @@ class MediaFile(object):
if isinstance(descriptor, MediaField):
yield property.decode('utf8')
@classmethod
def _field_sort_name(cls, name):
"""Get a sort key for a field name that determines the order
fields should be written in.
Fields names are kept unchanged, unless they are instances of
:class:`DateItemField`, in which case `year`, `month`, and `day`
are replaced by `date0`, `date1`, and `date2`, respectively, to
make them appear in that order.
"""
if isinstance(cls.__dict__[name], DateItemField):
name = re.sub('year', 'date0', name)
name = re.sub('month', 'date1', name)
name = re.sub('day', 'date2', name)
return name
@classmethod
def sorted_fields(cls):
"""Get the names of all writable metadata fields, sorted in the
order that they should be written.
This is a lexicographic order, except for instances of
:class:`DateItemField`, which are sorted in year-month-day
order.
"""
for property in sorted(cls.fields(), key=cls._field_sort_name):
yield property
@classmethod
def readable_fields(cls):
"""Get all metadata fields: the writable ones from
@ -1496,7 +1524,7 @@ class MediaFile(object):
the `MediaFile`. If a key has the value `None`, the
corresponding property is deleted from the `MediaFile`.
"""
for field in self.fields():
for field in self.sorted_fields():
if field in dict:
if dict[field] is None:
delattr(self, field)

View file

@ -1485,7 +1485,7 @@ def config_edit():
try:
if not os.path.isfile(path):
open(path, 'w+').close()
util.interactive_open(path, editor)
util.interactive_open([path], editor)
except OSError as exc:
message = "Could not edit configuration: {0}".format(exc)
if not editor:

View file

@ -735,8 +735,8 @@ def open_anything():
return base_cmd
def interactive_open(target, command=None):
"""Open the file `target` by `exec`ing a new command. (The new
def interactive_open(targets, command=None):
"""Open the files in `targets` by `exec`ing a new command. (The new
program takes over, and Python execution ends: this does not fork a
subprocess.)
@ -757,7 +757,8 @@ def interactive_open(target, command=None):
base_cmd = open_anything()
command = [base_cmd, base_cmd]
command.append(target)
command += targets
return os.execlp(*command)

View file

@ -38,7 +38,7 @@ except ImportError:
HAVE_ITUNES = False
IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg']
CONTENT_TYPES = ('image/jpeg', 'image/gif')
CONTENT_TYPES = ('image/jpeg', 'image/png')
DOWNLOAD_EXTENSION = '.jpg'

View file

@ -39,6 +39,7 @@ class PlayPlugin(BeetsPlugin):
'command': None,
'use_folders': False,
'relative_to': None,
'raw': False,
})
def commands(self):
@ -62,6 +63,7 @@ class PlayPlugin(BeetsPlugin):
command_str = config['play']['command'].get()
use_folders = config['play']['use_folders'].get(bool)
relative_to = config['play']['relative_to'].get()
raw = config['play']['raw'].get(bool)
if relative_to:
relative_to = util.normpath(relative_to)
@ -91,6 +93,8 @@ class PlayPlugin(BeetsPlugin):
else:
selection = lib.items(ui.decargs(args))
paths = [item.path for item in selection]
if relative_to:
paths = [relpath(path, relative_to) for path in paths]
item_type = 'track'
item_type += 's' if len(selection) > 1 else ''
@ -111,22 +115,29 @@ class PlayPlugin(BeetsPlugin):
if ui.input_options(('Continue', 'Abort')) == 'a':
return
# Create temporary m3u file to hold our playlist.
m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False)
for item in paths:
if relative_to:
m3u.write(relpath(item, relative_to) + b'\n')
else:
m3u.write(item + b'\n')
m3u.close()
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('executing command: {} {}', command_str, m3u.name)
self._log.debug('executing command: {} {}', command_str,
b'"' + b' '.join(open_args) + b'"')
try:
util.interactive_open(m3u.name, command_str)
util.interactive_open(open_args, command_str)
except OSError as exc:
raise ui.UserError("Could not play the music playlist: "
"{0}".format(exc))
finally:
util.remove(m3u.name)
if not raw:
self._log.debug('Removing temporary playlist: {}',
open_args[0])
util.remove(open_args[0])
def _create_tmp_playlist(self, paths_list):
# Create temporary m3u file to hold our playlist.
m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False)
for item in paths_list:
m3u.write(item + b'\n')
m3u.close()
return [m3u.name]

View file

@ -12,6 +12,9 @@ The new features:
the player command. :bug:`1532`
* A new :doc:`/plugins/badfiles` helps you scan for corruption in your music
collection. Thanks to :user:`fxthomas`. :bug:`1568`
* :doc:`/plugins/play`: A new ``raw`` configuration option lets the command
work with players (such as VLC) that expect music filenames as arguments,
rather than in a playlist. Thanks to :user:`nathdwek`. :bug:`1578`
Fixes:
@ -27,6 +30,12 @@ Fixes:
option.
* The :ref:`list-cmd` command's help output now has a small query and format
string example. Thanks to :user:`pkess`. :bug:`1582`
* :doc:`/plugins/fetchart`: The plugin now fetches PNGs but not GIFs. (It
still fetches JPEGs.) This avoids an error when trying to embed images,
since not all formats support GIFs. :bug:`1588`
* Date fields are now written in the correct order (year-month-day), which
eliminates an intermittent bug where the latter two fields would not get
written to files. Thanks to :user:`jdetrey`. :bug:`1303` :bug:`1589`
* The check whether the file system is case sensitive or not could lead to
wrong results. It is much more robust now.

View file

@ -44,6 +44,9 @@ configuration file. The available options are:
paths to each track on the matched albums. Enable this option to
store paths to folders instead.
Default: ``no``.
- **raw**: Instead of creating a temporary m3u playlist and then opening it,
simply call the command with the paths returned by the query as arguments.
Default: ``no``.
Optional Arguments
------------------

View file

@ -43,11 +43,11 @@ class UtilTest(unittest.TestCase):
@patch('beets.util.open_anything')
def test_interactive_open(self, mock_open, mock_execlp):
mock_open.return_value = 'tagada'
util.interactive_open('foo')
util.interactive_open(['foo'])
mock_execlp.assert_called_once_with('tagada', 'tagada', 'foo')
mock_execlp.reset_mock()
util.interactive_open('foo', 'bar')
util.interactive_open(['foo'], 'bar')
mock_execlp.assert_called_once_with('bar', 'bar', 'foo')
def test_sanitize_unix_replaces_leading_dot(self):