mirror of
https://github.com/beetbox/beets.git
synced 2026-01-16 05:02:28 +01:00
Merge branch 'master' into RobustCaseSensitiveDetection
# Conflicts: # docs/changelog.rst
This commit is contained in:
commit
636f0af8b2
8 changed files with 72 additions and 20 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------------
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue