From 18d5c3b0a035b6b1659f711cd784a95b23cff95a Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Mon, 24 Aug 2015 01:42:54 +0200 Subject: [PATCH 01/11] play-raw: Add the option to play the raw queried pathes I slightly rewrote the play plugin in order to improve the readability and to introduce the "raw" play config option which makes beet simply pass a list of pathes to the play command rather than a playlist. --- beetsplug/play.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index e6611ad3f..38264d9c0 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -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,31 @@ 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: + passedToCommand = self._concatenatePaths(paths) + else: + passedToCommand, m3u = self._createTmpPlaylist(paths) - self._log.debug('executing command: {} {}', command_str, m3u.name) + self._log.debug('executing command: {} {}', command_str, + passedToCommand) try: - util.interactive_open(m3u.name, command_str) + util.interactive_open(passedToCommand, 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: + util.remove(m3u.name) + + def _createTmpPlaylist(self, pathsList): + # Create temporary m3u file to hold our playlist. + m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False) + for item in pathsList: + m3u.write(item + b'\n') + m3u.close() + return m3u.name, m3u + + def _concatenatePaths(self, pathsList): + concatenatedPaths = b'"' + b'" "'.join(pathsList) + b'"' + return concatenatedPaths From a23c5d4f67824d405ed165c6d31bf4f46ea14db0 Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Mon, 24 Aug 2015 20:53:42 +0200 Subject: [PATCH 02/11] play-raw: Call vlc with one file par arg --- beets/util/__init__.py | 8 +++++--- beetsplug/play.py | 11 ++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 61a95baa2..2b2a13a25 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -735,7 +735,7 @@ def open_anything(): return base_cmd -def interactive_open(target, command=None): +def interactive_open(target=None, command=None, multiple_targets=[]): """Open the file `target` by `exec`ing a new command. (The new program takes over, and Python execution ends: this does not fork a subprocess.) @@ -756,6 +756,8 @@ def interactive_open(target, command=None): else: base_cmd = open_anything() command = [base_cmd, base_cmd] - - command.append(target) + if multiple_targets: + command = command + multiple_targets + else: + command.append(target) return os.execlp(*command) diff --git a/beetsplug/play.py b/beetsplug/play.py index 38264d9c0..b277349d0 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -117,14 +117,15 @@ class PlayPlugin(BeetsPlugin): ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) if raw: - passedToCommand = self._concatenatePaths(paths) + passedToCommand = paths else: passedToCommand, m3u = self._createTmpPlaylist(paths) self._log.debug('executing command: {} {}', command_str, passedToCommand) try: - util.interactive_open(passedToCommand, command_str) + util.interactive_open(multiple_targets=passedToCommand, + command=command_str) except OSError as exc: raise ui.UserError("Could not play the music playlist: " "{0}".format(exc)) @@ -138,8 +139,4 @@ class PlayPlugin(BeetsPlugin): for item in pathsList: m3u.write(item + b'\n') m3u.close() - return m3u.name, m3u - - def _concatenatePaths(self, pathsList): - concatenatedPaths = b'"' + b'" "'.join(pathsList) + b'"' - return concatenatedPaths + return [m3u.name], m3u From 4eb563a08c58f38bd828deca4dbb633856d9f224 Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Tue, 25 Aug 2015 12:52:08 +0200 Subject: [PATCH 03/11] pep8: Correct camelCase to snake_case --- beetsplug/play.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index b277349d0..66d70ce16 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -117,14 +117,14 @@ class PlayPlugin(BeetsPlugin): ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) if raw: - passedToCommand = paths + passed_to_command = paths else: - passedToCommand, m3u = self._createTmpPlaylist(paths) + passed_to_command, m3u = self._create_tmp_playlist(paths) self._log.debug('executing command: {} {}', command_str, - passedToCommand) + passed_to_command) try: - util.interactive_open(multiple_targets=passedToCommand, + util.interactive_open(multiple_targets=passed_to_command, command=command_str) except OSError as exc: raise ui.UserError("Could not play the music playlist: " @@ -133,10 +133,10 @@ class PlayPlugin(BeetsPlugin): if not raw: util.remove(m3u.name) - def _createTmpPlaylist(self, pathsList): + 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 pathsList: + for item in paths_list: m3u.write(item + b'\n') m3u.close() return [m3u.name], m3u From 9c663432bdaa5439128f78da6b11352b39bee942 Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Tue, 25 Aug 2015 15:05:54 +0200 Subject: [PATCH 04/11] Refactor util/interactive_open: multiple targets interactive_open should now be invoked with at least the list of targets and optionally the command to open the targets with. This allows beets-play to pass multiple file paths directly to the configured command. The changes to the existing invocations are pretty trivial in order to comply to this refactor. --- beets/ui/commands.py | 2 +- beets/util/__init__.py | 9 ++++----- beetsplug/play.py | 13 +++++++------ test/test_util.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 2a37a113d..f3386cf71 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -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: diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 2b2a13a25..261edd587 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -735,7 +735,7 @@ def open_anything(): return base_cmd -def interactive_open(target=None, command=None, multiple_targets=[]): +def interactive_open(targets, command=None): """Open the file `target` by `exec`ing a new command. (The new program takes over, and Python execution ends: this does not fork a subprocess.) @@ -756,8 +756,7 @@ def interactive_open(target=None, command=None, multiple_targets=[]): else: base_cmd = open_anything() command = [base_cmd, base_cmd] - if multiple_targets: - command = command + multiple_targets - else: - command.append(target) + + command += targets + return os.execlp(*command) diff --git a/beetsplug/play.py b/beetsplug/play.py index 66d70ce16..fc2e8e1b3 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -119,19 +119,20 @@ class PlayPlugin(BeetsPlugin): if raw: passed_to_command = paths else: - passed_to_command, m3u = self._create_tmp_playlist(paths) + passed_to_command = self._create_tmp_playlist(paths) self._log.debug('executing command: {} {}', command_str, - passed_to_command) + b'"' + b' '.join(passed_to_command) + b'"') try: - util.interactive_open(multiple_targets=passed_to_command, - command=command_str) + util.interactive_open(passed_to_command, command_str) except OSError as exc: raise ui.UserError("Could not play the music playlist: " "{0}".format(exc)) finally: if not raw: - util.remove(m3u.name) + self._log.debug('Removing temporary playlist: {}', + passed_to_command[0]) + util.remove(passed_to_command[0]) def _create_tmp_playlist(self, paths_list): # Create temporary m3u file to hold our playlist. @@ -139,4 +140,4 @@ class PlayPlugin(BeetsPlugin): for item in paths_list: m3u.write(item + b'\n') m3u.close() - return [m3u.name], m3u + return [m3u.name] diff --git a/test/test_util.py b/test/test_util.py index 464c7e678..324a4d589 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -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): From 7ed742b268a600df3b1ca547f5d50af334ba4bbf Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Tue, 1 Sep 2015 23:38:25 +0200 Subject: [PATCH 05/11] play-raw: Fix a docstring and add doc --- beets/util/__init__.py | 2 +- docs/plugins/play.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 261edd587..158a6c8e1 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -736,7 +736,7 @@ def open_anything(): def interactive_open(targets, command=None): - """Open the file `target` by `exec`ing a new command. (The new + """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.) diff --git a/docs/plugins/play.rst b/docs/plugins/play.rst index 08172c7c9..9af886c13 100644 --- a/docs/plugins/play.rst +++ b/docs/plugins/play.rst @@ -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 ------------------ From b9bc06d9d82c6021eafa877cb2f08ec7f686dd1f Mon Sep 17 00:00:00 2001 From: "nath@home" Date: Tue, 1 Sep 2015 23:39:35 +0200 Subject: [PATCH 06/11] play-raw: Rename the command target: passed_to_command -> open_args --- beetsplug/play.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/beetsplug/play.py b/beetsplug/play.py index fc2e8e1b3..038bfd42d 100644 --- a/beetsplug/play.py +++ b/beetsplug/play.py @@ -117,22 +117,22 @@ class PlayPlugin(BeetsPlugin): ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) if raw: - passed_to_command = paths + open_args = paths else: - passed_to_command = self._create_tmp_playlist(paths) + open_args = self._create_tmp_playlist(paths) self._log.debug('executing command: {} {}', command_str, - b'"' + b' '.join(passed_to_command) + b'"') + b'"' + b' '.join(open_args) + b'"') try: - util.interactive_open(passed_to_command, 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: if not raw: self._log.debug('Removing temporary playlist: {}', - passed_to_command[0]) - util.remove(passed_to_command[0]) + open_args[0]) + util.remove(open_args[0]) def _create_tmp_playlist(self, paths_list): # Create temporary m3u file to hold our playlist. From 086b97173bc467fde2617e584f6a5eb0fabd9cbf Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 2 Sep 2015 12:33:04 -0700 Subject: [PATCH 07/11] Changelog for #1578 --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 344702e34..2352787c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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: From 854a4539cb7fe5e595ec0ed5e0daa3dc2e08d184 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 2 Sep 2015 12:45:35 -0700 Subject: [PATCH 08/11] Fix #1588: fetchart gets PNGs, not GIFs --- beetsplug/fetchart.py | 2 +- docs/changelog.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index aef7c06ae..b807891e0 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -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' diff --git a/docs/changelog.rst b/docs/changelog.rst index 2352787c6..0e09d3303 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,6 +30,9 @@ 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` 1.3.14 (August 2, 2015) From dcbc2dafa1f6f76cb3874c9c71fc10e1e86bc9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Detrey?= Date: Wed, 2 Sep 2015 22:48:55 +0200 Subject: [PATCH 09/11] Fix #1303: process `DateItemField` tags in YMD order --- beets/mediafile.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index b2a72d84c..466ca985e 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1460,6 +1460,28 @@ class MediaFile(object): if isinstance(descriptor, MediaField): yield property.decode('utf8') + @classmethod + def field_sort_name(cls, name): + """Get field name for sorting purposes. 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. + """ + 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 by + 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 +1518,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) From 8b58af8dba35751c06a1e44a03c515a14e2a5012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Detrey?= Date: Wed, 2 Sep 2015 23:25:47 +0200 Subject: [PATCH 10/11] Follow style conventions. --- beets/mediafile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 466ca985e..e82b14aae 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1479,7 +1479,7 @@ class MediaFile(object): 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): + for property in sorted(cls.fields(), key=cls.field_sort_name): yield property @classmethod From bc137b174fee0266cd8c19cfb43bc9fb518787e8 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Wed, 2 Sep 2015 15:05:23 -0700 Subject: [PATCH 11/11] Doc tweaks & changelog for #1589 --- beets/mediafile.py | 24 +++++++++++++++--------- docs/changelog.rst | 3 +++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index e82b14aae..64ab49ac2 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -1461,11 +1461,14 @@ class MediaFile(object): yield property.decode('utf8') @classmethod - def field_sort_name(cls, name): - """Get field name for sorting purposes. 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. + 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) @@ -1475,11 +1478,14 @@ class MediaFile(object): @classmethod def sorted_fields(cls): - """Get the names of all writable metadata fields sorted by - lexicographic order (except for instances of :class:`DateItemField`, - which are sorted in year-month-day order). + """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): + for property in sorted(cls.fields(), key=cls._field_sort_name): yield property @classmethod diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e09d3303..3148119b1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,9 @@ Fixes: * :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` 1.3.14 (August 2, 2015)