From 14cad04d35c6e99ea72c17b61b2c6f42812363d1 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 19:07:21 +0100 Subject: [PATCH 01/14] test: Further improve Windows compatibility in playlist plugin test --- test/test_playlist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index 529f3631c..3dd80c35f 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -37,7 +37,7 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): i1 = _common.item() i1.path = beets.util.normpath(os.path.join( self.music_dir, - 'a/b/c.mp3', + 'a', 'b', 'c.mp3', )) i1.title = u'some item' i1.album = u'some album' @@ -47,7 +47,7 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): i2 = _common.item() i2.path = beets.util.normpath(os.path.join( self.music_dir, - 'd/e/f.mp3', + 'd', 'e', 'f.mp3', )) i2.title = 'another item' i2.album = 'another album' @@ -57,7 +57,7 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): i3 = _common.item() i3.path = beets.util.normpath(os.path.join( self.music_dir, - 'x/y/z.mp3', + 'x', 'y', 'z.mp3', )) i3.title = 'yet another item' i3.album = 'yet another album' From b00b38dab6fdea37e2fc7fe201388dea84768a7b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 19:09:46 +0100 Subject: [PATCH 02/14] test: Add test for relative playlists --- test/test_playlist.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index 3dd80c35f..b01a36d07 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -65,9 +65,12 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): self.lib.add_album([i3]) self.playlist_dir = tempfile.mkdtemp() - with open(os.path.join(self.playlist_dir, 'test.m3u'), 'w') as f: + with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: f.write('{0}\n'.format(beets.util.displayable_path(i1.path))) f.write('{0}\n'.format(beets.util.displayable_path(i2.path))) + with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) self.config['directory'] = self.music_dir self.config['playlist']['relative_to'] = 'library' @@ -79,18 +82,18 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): shutil.rmtree(self.playlist_dir) self.teardown_beets() - def test_query_name(self): - q = u'playlist:test' + def test_name_query_with_absolute_paths_in_playlist(self): + q = u'playlist:absolute' results = self.lib.items(q) self.assertEqual(set([i.title for i in results]), set([ u'some item', u'another item', ])) - def test_query_path(self): + def test_path_query_with_absolute_paths_in_playlist(self): q = u'playlist:{0}'.format(shlex_quote(os.path.join( self.playlist_dir, - 'test.m3u', + 'absolute.m3u', ))) results = self.lib.items(q) self.assertEqual(set([i.title for i in results]), set([ @@ -98,12 +101,31 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): u'another item', ])) - def test_query_name_nonexisting(self): + def test_name_query_with_relative_paths_in_playlist(self): + q = u'playlist:relative' + results = self.lib.items(q) + self.assertEqual(set([i.title for i in results]), set([ + u'some item', + u'another item', + ])) + + def test_path_query_with_relative_paths_in_playlist(self): + q = u'playlist:{0}'.format(shlex_quote(os.path.join( + self.playlist_dir, + 'relative.m3u', + ))) + results = self.lib.items(q) + self.assertEqual(set([i.title for i in results]), set([ + u'some item', + u'another item', + ])) + + def test_name_query_with_nonexisting_playlist(self): q = u'playlist:nonexisting'.format(self.playlist_dir) results = self.lib.items(q) self.assertEqual(set(results), set()) - def test_query_path_nonexisting(self): + def test_path_query_with_nonexisting_playlist(self): q = u'playlist:{0}'.format(shlex_quote(os.path.join( self.playlist_dir, self.playlist_dir, From 9f3acce2aef55595e67e0368e07aa9bec91e5472 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 19:14:37 +0100 Subject: [PATCH 03/14] test: Add non-existing item to playlist tests --- test/test_playlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_playlist.py b/test/test_playlist.py index b01a36d07..4408e69d7 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -68,9 +68,12 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: f.write('{0}\n'.format(beets.util.displayable_path(i1.path))) f.write('{0}\n'.format(beets.util.displayable_path(i2.path))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'nonexisting.mp3'))) with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{0}\n'.format('nonexisting.mp3')) self.config['directory'] = self.music_dir self.config['playlist']['relative_to'] = 'library' From 5b68d883466509a5736de0dd8aecf671fcdefd76 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 19:27:56 +0100 Subject: [PATCH 04/14] test: Add more playlist tests for the different relative_to settings --- test/test_playlist.py | 82 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index 4408e69d7..e05e61550 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -27,7 +27,7 @@ from test import helper import beets -class PlaylistTest(unittest.TestCase, helper.TestHelper): +class PlaylistTestHelper(helper.TestHelper): def setUp(self): self.setup_beets() self.lib = beets.library.Library(':memory:') @@ -65,21 +65,15 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): self.lib.add_album([i3]) self.playlist_dir = tempfile.mkdtemp() - with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: - f.write('{0}\n'.format(beets.util.displayable_path(i1.path))) - f.write('{0}\n'.format(beets.util.displayable_path(i2.path))) - f.write('{0}\n'.format(os.path.join( - self.music_dir, 'nonexisting.mp3'))) - with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: - f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) - f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) - f.write('{0}\n'.format('nonexisting.mp3')) - self.config['directory'] = self.music_dir - self.config['playlist']['relative_to'] = 'library' self.config['playlist']['playlist_dir'] = self.playlist_dir + + self.setup_test() self.load_plugins('playlist') + def setup_test(self): + raise NotImplementedError + def tearDown(self): self.unload_plugins() shutil.rmtree(self.playlist_dir) @@ -138,6 +132,70 @@ class PlaylistTest(unittest.TestCase, helper.TestHelper): self.assertEqual(set(results), set()) +class PlaylistTestRelativeToLib(PlaylistTestHelper, unittest.TestCase): + def setup_test(self): + with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'd', 'e', 'f.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'nonexisting.mp3'))) + + with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{0}\n'.format('nonexisting.mp3')) + + self.config['playlist']['relative_to'] = 'library' + + +class PlaylistTestRelativeToDir(PlaylistTestHelper, unittest.TestCase): + def setup_test(self): + with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'd', 'e', 'f.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'nonexisting.mp3'))) + + with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{0}\n'.format('nonexisting.mp3')) + + self.config['playlist']['relative_to'] = self.music_dir + + +class PlaylistTestRelativeToPls(PlaylistTestHelper, unittest.TestCase): + def setup_test(self): + with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'd', 'e', 'f.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'nonexisting.mp3'))) + + with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.relpath( + os.path.join(self.music_dir, 'a', 'b', 'c.mp3'), + start=self.playlist_dir, + ))) + f.write('{0}\n'.format(os.path.relpath( + os.path.join(self.music_dir, 'd', 'e', 'f.mp3'), + start=self.playlist_dir, + ))) + f.write('{0}\n'.format(os.path.relpath( + os.path.join(self.music_dir, 'nonexisting.mp3'), + start=self.playlist_dir, + ))) + + self.config['playlist']['relative_to'] = 'playlist' + self.config['playlist']['playlist_dir'] = self.playlist_dir + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From 6d420280571cc6ad4f4cf422013f0f036b800200 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 15 Feb 2019 19:50:26 +0100 Subject: [PATCH 05/14] playlist: Add playlist auto-update functionality --- beetsplug/playlist.py | 93 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index e5c80f129..393449217 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -14,6 +14,7 @@ import os import fnmatch +import tempfile import beets @@ -86,6 +87,98 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): def __init__(self): super(PlaylistPlugin, self).__init__() self.config.add({ + 'auto': False, 'playlist_dir': '.', 'relative_to': 'library', }) + + self.playlist_dir = self.config['playlist_dir'].as_filename() + self.changes = {} + + if self.config['relative_to'].get() == 'library': + self.relative_to = beets.util.bytestring_path( + beets.config['directory'].as_filename()) + elif self.config['relative_to'].get() != 'playlist': + print(repr(self.config['relative_to'].get())) + self.relative_to = beets.util.bytestring_path( + self.config['relative_to'].as_filename()) + else: + self.relative_to = None + + if self.config['auto'].get(bool): + self.register_listener('item_moved', self.item_moved) + self.register_listener('item_removed', self.item_removed) + self.register_listener('cli_exit', self.cli_exit) + + def item_moved(self, item, source, destination): + self.changes[source] = destination + + def item_removed(self, item): + if not os.path.exists(beets.util.syspath(item.path)): + self.changes[item.path] = None + + def cli_exit(self, lib): + for playlist in self.find_playlists(): + self._log.info('Updating playlist: {0}'.format(playlist)) + base_dir = beets.util.bytestring_path( + self.relative_to if self.relative_to + else os.path.dirname(playlist) + ) + + try: + self.update_playlist(playlist, base_dir) + except beets.util.FilesystemError: + self._log.error('Failed to update playlist: {0}'.format( + beets.util.displayable_path(playlist))) + + def find_playlists(self): + """Find M3U playlists in the playlist directory.""" + try: + dir_contents = os.listdir(beets.util.syspath(self.playlist_dir)) + except OSError: + self._log.warning('Unable to open playlist directory {0}'.format( + beets.util.displayable_path(self.playlist_dir))) + return + + for filename in dir_contents: + if fnmatch.fnmatch(filename, '*.[mM]3[uU]'): + yield os.path.join(self.playlist_dir, filename) + + def update_playlist(self, filename, base_dir): + """Find M3U playlists in the specified directory.""" + changes = 0 + deletions = 0 + + with tempfile.NamedTemporaryFile(mode='w+b') as tempfp: + with open(filename, mode='rb') as fp: + for line in fp: + original_path = line.rstrip(b'\r\n') + + # Ensure that path from playlist is absolute + is_relative = not os.path.isabs(beets.util.syspath(line)) + if is_relative: + lookup = os.path.join(base_dir, original_path) + else: + lookup = original_path + + try: + new_path = self.changes[lookup] + except KeyError: + tempfp.write(line) + else: + if new_path is None: + # Item has been deleted + deletions += 1 + continue + + changes += 1 + if is_relative: + new_path = os.path.relpath(new_path, base_dir) + + tempfp.write(line.replace(original_path, new_path)) + if changes or deletions: + self._log.info( + 'Updated playlist {0} ({1} changes, {2} deletions)'.format( + filename, changes, deletions)) + tempfp.flush() + beets.util.copy(tempfp.name, filename, replace=True) From 1af82cc450929713edfd9d8da1cc7e26d8dd0f21 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 16 Feb 2019 13:59:25 +0100 Subject: [PATCH 06/14] test: Split up playlist test helper class --- test/test_playlist.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index e05e61550..1fcdb0071 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -79,6 +79,8 @@ class PlaylistTestHelper(helper.TestHelper): shutil.rmtree(self.playlist_dir) self.teardown_beets() + +class PlaylistQueryTestHelper(PlaylistTestHelper): def test_name_query_with_absolute_paths_in_playlist(self): q = u'playlist:absolute' results = self.lib.items(q) @@ -132,7 +134,7 @@ class PlaylistTestHelper(helper.TestHelper): self.assertEqual(set(results), set()) -class PlaylistTestRelativeToLib(PlaylistTestHelper, unittest.TestCase): +class PlaylistTestRelativeToLib(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: f.write('{0}\n'.format(os.path.join( @@ -150,7 +152,7 @@ class PlaylistTestRelativeToLib(PlaylistTestHelper, unittest.TestCase): self.config['playlist']['relative_to'] = 'library' -class PlaylistTestRelativeToDir(PlaylistTestHelper, unittest.TestCase): +class PlaylistTestRelativeToDir(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: f.write('{0}\n'.format(os.path.join( @@ -168,7 +170,7 @@ class PlaylistTestRelativeToDir(PlaylistTestHelper, unittest.TestCase): self.config['playlist']['relative_to'] = self.music_dir -class PlaylistTestRelativeToPls(PlaylistTestHelper, unittest.TestCase): +class PlaylistTestRelativeToPls(PlaylistQueryTestHelper, unittest.TestCase): def setup_test(self): with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: f.write('{0}\n'.format(os.path.join( From a9dd5a7cdc72102fb85b85731542fd203161d589 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 16 Feb 2019 15:37:26 +0100 Subject: [PATCH 07/14] test: Add testcase for playlist plugin's update functionality --- test/test_playlist.py | 103 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/test/test_playlist.py b/test/test_playlist.py index 1fcdb0071..2bc461f76 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -198,6 +198,109 @@ class PlaylistTestRelativeToPls(PlaylistQueryTestHelper, unittest.TestCase): self.config['playlist']['playlist_dir'] = self.playlist_dir +class PlaylistUpdateTestHelper(PlaylistTestHelper): + def setup_test(self): + with open(os.path.join(self.playlist_dir, 'absolute.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'd', 'e', 'f.mp3'))) + f.write('{0}\n'.format(os.path.join( + self.music_dir, 'nonexisting.mp3'))) + + with open(os.path.join(self.playlist_dir, 'relative.m3u'), 'w') as f: + f.write('{0}\n'.format(os.path.join('a', 'b', 'c.mp3'))) + f.write('{0}\n'.format(os.path.join('d', 'e', 'f.mp3'))) + f.write('{0}\n'.format('nonexisting.mp3')) + + self.config['playlist']['auto'] = True + self.config['playlist']['relative_to'] = 'library' + + +class PlaylistTestItemMoved(PlaylistUpdateTestHelper, unittest.TestCase): + def test_item_moved(self): + # Emit item_moved event for an item that is in a playlist + results = self.lib.items('path:{0}'.format(shlex_quote( + os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) + item = results[0] + beets.plugins.send( + 'item_moved', item=item, source=item.path, + destination=beets.util.bytestring_path( + os.path.join(self.music_dir, 'g', 'h', 'i.mp3'))) + + # Emit item_moved event for an item that is not in a playlist + results = self.lib.items('path:{0}'.format(shlex_quote( + os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) + item = results[0] + beets.plugins.send( + 'item_moved', item=item, source=item.path, + destination=beets.util.bytestring_path( + os.path.join(self.music_dir, 'u', 'v', 'w.mp3'))) + + # Emit cli_exit event + beets.plugins.send('cli_exit', lib=self.lib) + + # Check playlist with absolute paths + playlist_path = os.path.join(self.playlist_dir, 'absolute.m3u') + with open(playlist_path, 'r') as f: + lines = [line.strip() for line in f.readlines()] + + self.assertEqual(lines, [ + os.path.join(self.music_dir, 'a', 'b', 'c.mp3'), + os.path.join(self.music_dir, 'g', 'h', 'i.mp3'), + os.path.join(self.music_dir, 'nonexisting.mp3'), + ]) + + # Check playlist with relative paths + playlist_path = os.path.join(self.playlist_dir, 'relative.m3u') + with open(playlist_path, 'r') as f: + lines = [line.strip() for line in f.readlines()] + + self.assertEqual(lines, [ + os.path.join('a', 'b', 'c.mp3'), + os.path.join('g', 'h', 'i.mp3'), + 'nonexisting.mp3', + ]) + + +class PlaylistTestItemRemoved(PlaylistUpdateTestHelper, unittest.TestCase): + def test_item_removed(self): + # Emit item_removed event for an item that is in a playlist + results = self.lib.items('path:{0}'.format(shlex_quote( + os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) + item = results[0] + beets.plugins.send('item_removed', item=item) + + # Emit item_removed event for an item that is not in a playlist + results = self.lib.items('path:{0}'.format(shlex_quote( + os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) + item = results[0] + beets.plugins.send('item_removed', item=item) + + # Emit cli_exit event + beets.plugins.send('cli_exit', lib=self.lib) + + # Check playlist with absolute paths + playlist_path = os.path.join(self.playlist_dir, 'absolute.m3u') + with open(playlist_path, 'r') as f: + lines = [line.strip() for line in f.readlines()] + + self.assertEqual(lines, [ + os.path.join(self.music_dir, 'a', 'b', 'c.mp3'), + os.path.join(self.music_dir, 'nonexisting.mp3'), + ]) + + # Check playlist with relative paths + playlist_path = os.path.join(self.playlist_dir, 'relative.m3u') + with open(playlist_path, 'r') as f: + lines = [line.strip() for line in f.readlines()] + + self.assertEqual(lines, [ + os.path.join('a', 'b', 'c.mp3'), + 'nonexisting.mp3', + ]) + + def suite(): return unittest.TestLoader().loadTestsFromName(__name__) From fdd41b301d272604ded7b980532ee4275e46a71e Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 16 Feb 2019 15:45:04 +0100 Subject: [PATCH 08/14] docs: Update documentation regarding playlist plugin --- docs/plugins/playlist.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/plugins/playlist.rst b/docs/plugins/playlist.rst index 1156e7f77..d9b400987 100644 --- a/docs/plugins/playlist.rst +++ b/docs/plugins/playlist.rst @@ -8,6 +8,7 @@ To use it, enable the ``playlist`` plugin in your configuration Then configure your playlists like this:: playlist: + auto: no relative_to: ~/Music playlist_dir: ~/.mpd/playlists @@ -22,6 +23,10 @@ name:: $ beet ls playlist:anotherplaylist +The plugin can also update playlists in the playlist directory automatically +every time an item is moved or deleted. This can be controlled by the ``auto`` +configuration option. + Configuration ------------- @@ -29,6 +34,10 @@ To configure the plugin, make a ``smartplaylist:`` section in your configuration file. In addition to the ``playlists`` described above, the other configuration options are: +- **auto**: If this is set to ``yes``, then anytime an item in the library is + moved or removed, the plugin will update all playlists in the + ``playlist_dir`` directory that contain that item to reflect the change. + Default: ``no`` - **playlist_dir**: Where to read playlist files from. Default: The current working directory (i.e., ``'.'``). - **relative_to**: Interpret paths in the playlist files relative to a base From 7ec55a5f3be4a768d5889dbe8eaf712c27f96700 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 21:27:09 +0100 Subject: [PATCH 09/14] test: Use unicode literals for library queries in playlist tests --- test/test_playlist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index 2bc461f76..1e800804e 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -220,7 +220,7 @@ class PlaylistUpdateTestHelper(PlaylistTestHelper): class PlaylistTestItemMoved(PlaylistUpdateTestHelper, unittest.TestCase): def test_item_moved(self): # Emit item_moved event for an item that is in a playlist - results = self.lib.items('path:{0}'.format(shlex_quote( + results = self.lib.items(u'path:{0}'.format(shlex_quote( os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) item = results[0] beets.plugins.send( @@ -229,7 +229,7 @@ class PlaylistTestItemMoved(PlaylistUpdateTestHelper, unittest.TestCase): os.path.join(self.music_dir, 'g', 'h', 'i.mp3'))) # Emit item_moved event for an item that is not in a playlist - results = self.lib.items('path:{0}'.format(shlex_quote( + results = self.lib.items(u'path:{0}'.format(shlex_quote( os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) item = results[0] beets.plugins.send( @@ -266,13 +266,13 @@ class PlaylistTestItemMoved(PlaylistUpdateTestHelper, unittest.TestCase): class PlaylistTestItemRemoved(PlaylistUpdateTestHelper, unittest.TestCase): def test_item_removed(self): # Emit item_removed event for an item that is in a playlist - results = self.lib.items('path:{0}'.format(shlex_quote( + results = self.lib.items(u'path:{0}'.format(shlex_quote( os.path.join(self.music_dir, 'd', 'e', 'f.mp3')))) item = results[0] beets.plugins.send('item_removed', item=item) # Emit item_removed event for an item that is not in a playlist - results = self.lib.items('path:{0}'.format(shlex_quote( + results = self.lib.items(u'path:{0}'.format(shlex_quote( os.path.join(self.music_dir, 'x', 'y', 'z.mp3')))) item = results[0] beets.plugins.send('item_removed', item=item) From 76a3e44aaddbd1aa23ffe8e3b6d8d2878d769738 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 21:27:37 +0100 Subject: [PATCH 10/14] test: Make music dir of playlist tests Windows-compatible --- test/test_playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_playlist.py b/test/test_playlist.py index 1e800804e..edd98e711 100644 --- a/test/test_playlist.py +++ b/test/test_playlist.py @@ -32,7 +32,7 @@ class PlaylistTestHelper(helper.TestHelper): self.setup_beets() self.lib = beets.library.Library(':memory:') - self.music_dir = os.path.expanduser('~/Music') + self.music_dir = os.path.expanduser(os.path.join('~', 'Music')) i1 = _common.item() i1.path = beets.util.normpath(os.path.join( From d991e2a7d8cf3a55dbad4797f084fe24f4d8016d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 21:51:09 +0100 Subject: [PATCH 11/14] playlist: Normalize path before lookup in changes dict --- beetsplug/playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index 393449217..68b2adb49 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -162,7 +162,7 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): lookup = original_path try: - new_path = self.changes[lookup] + new_path = self.changes[beets.util.normpath(lookup)] except KeyError: tempfp.write(line) else: From ee2cce4280b14c8ade99fd838bea330ebd1360f8 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 22:05:54 +0100 Subject: [PATCH 12/14] playlist: Work around Windows' Mandatory File Locking on playlist updates --- beetsplug/playlist.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index 68b2adb49..ae530aaec 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -149,7 +149,8 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): changes = 0 deletions = 0 - with tempfile.NamedTemporaryFile(mode='w+b') as tempfp: + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tempfp: + new_playlist = tempfp.name with open(filename, mode='rb') as fp: for line in fp: original_path = line.rstrip(b'\r\n') @@ -176,9 +177,10 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): new_path = os.path.relpath(new_path, base_dir) tempfp.write(line.replace(original_path, new_path)) - if changes or deletions: - self._log.info( - 'Updated playlist {0} ({1} changes, {2} deletions)'.format( - filename, changes, deletions)) - tempfp.flush() - beets.util.copy(tempfp.name, filename, replace=True) + + if changes or deletions: + self._log.info( + 'Updated playlist {0} ({1} changes, {2} deletions)'.format( + filename, changes, deletions)) + beets.util.copy(new_playlist, filename, replace=True) + beets.util.remove(new_playlist) From 7bca5cf549c9f4569c8f2616e67b4f52afb0bd82 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 17 Feb 2019 22:28:39 +0100 Subject: [PATCH 13/14] playlist: Don't use syspath() when checking if path is absolute --- beetsplug/playlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index ae530aaec..08c4bc8af 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -156,7 +156,7 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): original_path = line.rstrip(b'\r\n') # Ensure that path from playlist is absolute - is_relative = not os.path.isabs(beets.util.syspath(line)) + is_relative = not os.path.isabs(line) if is_relative: lookup = os.path.join(base_dir, original_path) else: From 4ba5dfaa43bffd3470b10e857388516e6590b19e Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Mon, 18 Feb 2019 09:13:39 +0100 Subject: [PATCH 14/14] playlist: Remove leftover print call and fix 'auto' option access style --- beetsplug/playlist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/playlist.py b/beetsplug/playlist.py index 08c4bc8af..04959c431 100644 --- a/beetsplug/playlist.py +++ b/beetsplug/playlist.py @@ -99,13 +99,12 @@ class PlaylistPlugin(beets.plugins.BeetsPlugin): self.relative_to = beets.util.bytestring_path( beets.config['directory'].as_filename()) elif self.config['relative_to'].get() != 'playlist': - print(repr(self.config['relative_to'].get())) self.relative_to = beets.util.bytestring_path( self.config['relative_to'].as_filename()) else: self.relative_to = None - if self.config['auto'].get(bool): + if self.config['auto']: self.register_listener('item_moved', self.item_moved) self.register_listener('item_removed', self.item_removed) self.register_listener('cli_exit', self.cli_exit)