From af9ef00a9d1e0df4f27c0d042d9d0ee04fd2a474 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 15:53:38 -0700 Subject: [PATCH 01/10] fix convert func --- beetsplug/convert.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index e72f8c75a9..436368cd6b 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -616,25 +616,23 @@ def convert_func(self, lib, opts, args): ) if playlist: - # Playlist paths are understood as relative to the dest directory. + # Generate playlist paths from converted item paths (updated in database) pl_normpath = util.normpath(playlist) pl_dir = os.path.dirname(pl_normpath) self._log.info("Creating playlist file {}", pl_normpath) - # Generates a list of paths to media files, ensures the paths are - # relative to the playlist's location and translates the unicode - # strings we get from item.destination to bytes. - items_paths = [ - os.path.relpath( - item.destination(basedir=dest, path_formats=path_formats), - pl_dir, - ) + + # Refresh item paths to converted ones before generating playlist + updated_paths = [ + os.path.relpath(item.path, pl_dir) for item in items ] + if not pretend: m3ufile = M3UFile(playlist) - m3ufile.set_contents(items_paths) + m3ufile.set_contents(updated_paths) m3ufile.write() + def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the library. From dc7265c04c69408a3c6492e51c7ada63d25b1f48 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:03:45 -0700 Subject: [PATCH 02/10] fix convert func --- beetsplug/convert.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 436368cd6b..22afa95b73 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -616,23 +616,30 @@ def convert_func(self, lib, opts, args): ) if playlist: - # Generate playlist paths from converted item paths (updated in database) pl_normpath = util.normpath(playlist) pl_dir = os.path.dirname(pl_normpath) self._log.info("Creating playlist file {}", pl_normpath) - - # Refresh item paths to converted ones before generating playlist - updated_paths = [ - os.path.relpath(item.path, pl_dir) - for item in items - ] - + + items_paths = [] + for item in items: + # Use item.path if available and not empty, otherwise fallback to item.destination() + path = item.path if item.path else item.destination(basedir=dest, path_formats=path_formats) + + # Make path relative to playlist folder + rel_path = os.path.relpath(path, pl_dir) + + # Ensure string encoding for all playlist entries (unicode or utf-8 bytes) + if isinstance(rel_path, bytes): + rel_path = rel_path.decode('utf-8', errors='replace') + items_paths.append(rel_path) + if not pretend: m3ufile = M3UFile(playlist) - m3ufile.set_contents(updated_paths) + m3ufile.set_contents(items_paths) m3ufile.write() + def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the library. From 70ed5d2c141bb308f8c1986b6b569241765f3b24 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:13:30 -0700 Subject: [PATCH 03/10] fix convert func --- beetsplug/convert.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 22afa95b73..2d1add8763 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -622,13 +622,18 @@ def convert_func(self, lib, opts, args): items_paths = [] for item in items: - # Use item.path if available and not empty, otherwise fallback to item.destination() - path = item.path if item.path else item.destination(basedir=dest, path_formats=path_formats) - + # Use item.path if available and not empty, otherwise fallback to item. + path = ( + item.path + if item.path + else item.destination(basedir=dest, path_formats=path_formats) + ) + + # Make path relative to playlist folder rel_path = os.path.relpath(path, pl_dir) - # Ensure string encoding for all playlist entries (unicode or utf-8 bytes) + # Ensure string encoding for all playlist entries if isinstance(rel_path, bytes): rel_path = rel_path.decode('utf-8', errors='replace') items_paths.append(rel_path) From 13ff9e84d4e615d366a820c65c3797edebfc91fd Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:31:04 -0700 Subject: [PATCH 04/10] added test cases and change log --- test/plugins/test_convert.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 1452686a7d..76b8ced917 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -260,6 +260,45 @@ def test_playlist_pretend(self): self.run_convert("--playlist", "playlist.m3u8", "--pretend") assert not (self.convert_dest / "playlist.m3u8").exists() + def test_playlist_generation_with_fallback_and_unicode(tmp_path, library, convert_plugin): + # Setup items with one having no path (to trigger fallback), one with Unicode filename + item_with_path = library.add_item(path=str(tmp_path / "song1.mp3")) + item_with_path.path = str(tmp_path / "song1.mp3") + item_missing_path = library.add_item(path=str(tmp_path / "song\u2603.mp3")) + item_missing_path.path = "" # empty path to force fallback + + # Destination directory + dest = tmp_path / "dest" + dest.mkdir() + + playlist_path = tmp_path / "test_playlist.m3u" + + # Manually call playlist generation code from convert_func + items = [item_with_path, item_missing_path] + pl_normpath = str(playlist_path) + pl_dir = str(playlist_path.parent) + + items_paths = [] + for item in items: + path = item.path if item.path else item.destination(basedir=str(dest)) + rel_path = os.path.relpath(path, pl_dir) + # Ensure UTF-8 string + if isinstance(rel_path, bytes): + rel_path = rel_path.decode("utf-8", errors="replace") + items_paths.append(rel_path) + + m3ufile = convert_plugin.M3UFile(playlist_path) + m3ufile.set_contents(items_paths) + m3ufile.write() + + # Assert playlist was created and contains expected content + assert playlist_path.exists() + content = playlist_path.read_text(encoding="utf-8") + assert "song1.mp3" in content + assert "\u2603" in content # snowman unicode char + + + @_common.slow_test() class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand): From 5d46d1b5dad0f99863153f04c30e2687fd147a28 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:39:24 -0700 Subject: [PATCH 05/10] added test cases and change log --- test/plugins/test_convert.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 76b8ced917..3507acf77d 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -260,22 +260,22 @@ def test_playlist_pretend(self): self.run_convert("--playlist", "playlist.m3u8", "--pretend") assert not (self.convert_dest / "playlist.m3u8").exists() - def test_playlist_generation_with_fallback_and_unicode(tmp_path, library, convert_plugin): - # Setup items with one having no path (to trigger fallback), one with Unicode filename - item_with_path = library.add_item(path=str(tmp_path / "song1.mp3")) - item_with_path.path = str(tmp_path / "song1.mp3") - item_missing_path = library.add_item(path=str(tmp_path / "song\u2603.mp3")) + def test_playlist_generation_with_fallback(self, library, convert_plugin): + # Setup items with one having no path, one with Unicode filename + item_with_path = library.add_item(path=str(self.tmp_path / "song1.mp3")) + item_with_path.path = str(self.tmp_path / "song1.mp3") + item_missing_path = library.add_item(path=str(self.tmp_path / "song\u2603.mp3")) item_missing_path.path = "" # empty path to force fallback # Destination directory - dest = tmp_path / "dest" + dest = self.tmp_path / "dest" dest.mkdir() - playlist_path = tmp_path / "test_playlist.m3u" + playlist_path = self.tmp_path / "test_playlist.m3u" # Manually call playlist generation code from convert_func items = [item_with_path, item_missing_path] - pl_normpath = str(playlist_path) + # pl_normpath = str(playlist_path) pl_dir = str(playlist_path.parent) items_paths = [] From 19e5088ab2ba58098352441f6e7d71a5e1236762 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:44:16 -0700 Subject: [PATCH 06/10] added test cases and change log --- test/plugins/test_convert.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 3507acf77d..806d12adcc 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -261,10 +261,15 @@ def test_playlist_pretend(self): assert not (self.convert_dest / "playlist.m3u8").exists() def test_playlist_generation_with_fallback(self, library, convert_plugin): - # Setup items with one having no path, one with Unicode filename - item_with_path = library.add_item(path=str(self.tmp_path / "song1.mp3")) + # Setup items with one having no path, one with Unicode filename + item_with_path = library.add_item( + path=str(self.tmp_path / "song1.mp3") + ) item_with_path.path = str(self.tmp_path / "song1.mp3") - item_missing_path = library.add_item(path=str(self.tmp_path / "song\u2603.mp3")) + + item_missing_path = library.add_item( + path=str(self.tmp_path / "song\u2603.mp3") + ) item_missing_path.path = "" # empty path to force fallback # Destination directory @@ -275,13 +280,15 @@ def test_playlist_generation_with_fallback(self, library, convert_plugin): # Manually call playlist generation code from convert_func items = [item_with_path, item_missing_path] - # pl_normpath = str(playlist_path) pl_dir = str(playlist_path.parent) items_paths = [] for item in items: - path = item.path if item.path else item.destination(basedir=str(dest)) + path = ( + item.path if item.path else item.destination(basedir=str(dest)) + ) rel_path = os.path.relpath(path, pl_dir) + # Ensure UTF-8 string if isinstance(rel_path, bytes): rel_path = rel_path.decode("utf-8", errors="replace") @@ -300,6 +307,7 @@ def test_playlist_generation_with_fallback(self, library, convert_plugin): + @_common.slow_test() class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand): """Test the effect of the `never_convert_lossy_files` option.""" From 6122ac498daac1f08565c5aaf588e935c7e80b14 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:56:29 -0700 Subject: [PATCH 07/10] added test cases and change log --- beetsplug/convert.py | 9 +++----- test/plugins/test_convert.py | 40 +++++++++++++++--------------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 2d1add8763..ca6e996af1 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -622,26 +622,23 @@ def convert_func(self, lib, opts, args): items_paths = [] for item in items: - # Use item.path if available and not empty, otherwise fallback to item. path = ( item.path if item.path else item.destination(basedir=dest, path_formats=path_formats) ) - - - # Make path relative to playlist folder rel_path = os.path.relpath(path, pl_dir) - # Ensure string encoding for all playlist entries + # Ensure string encoding for playlist entries (convert bytes to str) if isinstance(rel_path, bytes): rel_path = rel_path.decode('utf-8', errors='replace') + items_paths.append(rel_path) if not pretend: m3ufile = M3UFile(playlist) m3ufile.set_contents(items_paths) - m3ufile.write() + m3ufile.write() # Assume m3ufile.write expects str lines diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 806d12adcc..669f74c2df 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -260,52 +260,44 @@ def test_playlist_pretend(self): self.run_convert("--playlist", "playlist.m3u8", "--pretend") assert not (self.convert_dest / "playlist.m3u8").exists() - def test_playlist_generation_with_fallback(self, library, convert_plugin): - # Setup items with one having no path, one with Unicode filename - item_with_path = library.add_item( - path=str(self.tmp_path / "song1.mp3") - ) - item_with_path.path = str(self.tmp_path / "song1.mp3") + def test_playlist_generation_with_fallback(self): + # Access fixtures or setup in this method or a setup method + tmp_path = self.tmp_path # Assume tmp_path is set up in self or via setup + library = self.library # Likewise for library + convert_plugin = self.convert_plugin - item_missing_path = library.add_item( - path=str(self.tmp_path / "song\u2603.mp3") - ) - item_missing_path.path = "" # empty path to force fallback + # Setup items + item_with_path = library.add_item(path=str(tmp_path / "song1.mp3")) + item_with_path.path = str(tmp_path / "song1.mp3") + + item_missing_path = library.add_item(path=str(tmp_path / "song\u2603.mp3")) + item_missing_path.path = "" - # Destination directory - dest = self.tmp_path / "dest" + dest = tmp_path / "dest" dest.mkdir() - playlist_path = self.tmp_path / "test_playlist.m3u" + playlist_path = tmp_path / "test_playlist.m3u" - # Manually call playlist generation code from convert_func items = [item_with_path, item_missing_path] pl_dir = str(playlist_path.parent) items_paths = [] for item in items: - path = ( - item.path if item.path else item.destination(basedir=str(dest)) - ) + path = item.path if item.path else item.destination(basedir=str(dest)) rel_path = os.path.relpath(path, pl_dir) - - # Ensure UTF-8 string if isinstance(rel_path, bytes): rel_path = rel_path.decode("utf-8", errors="replace") + items_paths.append(rel_path) m3ufile = convert_plugin.M3UFile(playlist_path) m3ufile.set_contents(items_paths) m3ufile.write() - # Assert playlist was created and contains expected content assert playlist_path.exists() content = playlist_path.read_text(encoding="utf-8") assert "song1.mp3" in content - assert "\u2603" in content # snowman unicode char - - - + assert "\u2603" in content @_common.slow_test() From 5540c4a6e3af301a261ac0939c4e9341c879aecc Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 16:59:58 -0700 Subject: [PATCH 08/10] added test cases and change log --- beetsplug/convert.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index ca6e996af1..b22b60d53c 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -640,8 +640,6 @@ def convert_func(self, lib, opts, args): m3ufile.set_contents(items_paths) m3ufile.write() # Assume m3ufile.write expects str lines - - def convert_on_import(self, lib, item): """Transcode a file automatically after it is imported into the library. From 933132f71ec1a899cf60de7203a14d4fe0d01951 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 17:06:17 -0700 Subject: [PATCH 09/10] added test cases and change log --- test/plugins/test_convert.py | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/test/plugins/test_convert.py b/test/plugins/test_convert.py index 669f74c2df..1452686a7d 100644 --- a/test/plugins/test_convert.py +++ b/test/plugins/test_convert.py @@ -260,45 +260,6 @@ def test_playlist_pretend(self): self.run_convert("--playlist", "playlist.m3u8", "--pretend") assert not (self.convert_dest / "playlist.m3u8").exists() - def test_playlist_generation_with_fallback(self): - # Access fixtures or setup in this method or a setup method - tmp_path = self.tmp_path # Assume tmp_path is set up in self or via setup - library = self.library # Likewise for library - convert_plugin = self.convert_plugin - - # Setup items - item_with_path = library.add_item(path=str(tmp_path / "song1.mp3")) - item_with_path.path = str(tmp_path / "song1.mp3") - - item_missing_path = library.add_item(path=str(tmp_path / "song\u2603.mp3")) - item_missing_path.path = "" - - dest = tmp_path / "dest" - dest.mkdir() - - playlist_path = tmp_path / "test_playlist.m3u" - - items = [item_with_path, item_missing_path] - pl_dir = str(playlist_path.parent) - - items_paths = [] - for item in items: - path = item.path if item.path else item.destination(basedir=str(dest)) - rel_path = os.path.relpath(path, pl_dir) - if isinstance(rel_path, bytes): - rel_path = rel_path.decode("utf-8", errors="replace") - - items_paths.append(rel_path) - - m3ufile = convert_plugin.M3UFile(playlist_path) - m3ufile.set_contents(items_paths) - m3ufile.write() - - assert playlist_path.exists() - content = playlist_path.read_text(encoding="utf-8") - assert "song1.mp3" in content - assert "\u2603" in content - @_common.slow_test() class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand): From c4601ed3c3ac04cbd2db321e1b4268badb0ae7d8 Mon Sep 17 00:00:00 2001 From: Dev Mehta Date: Mon, 27 Oct 2025 17:16:09 -0700 Subject: [PATCH 10/10] fixed m3u.py --- beets/util/m3u.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/beets/util/m3u.py b/beets/util/m3u.py index b6e355e06b..60202b4040 100644 --- a/beets/util/m3u.py +++ b/beets/util/m3u.py @@ -79,19 +79,22 @@ def write(self): Handles the creation of potential parent directories. """ + # Use bytes for header if extm3u is True, otherwise use str header = [b"#EXTM3U"] if self.extm3u else [] + if not self.media_list: raise EmptyPlaylistError - contents = header + self.media_list + + # Ensure all media_list items are bytes + media_bytes = [ + line.encode('utf-8') if isinstance(line, str) else line + for line in self.media_list + ] + + contents = header + media_bytes pl_normpath = normpath(self.path) mkdirall(pl_normpath) - try: - with open(syspath(pl_normpath), "wb") as pl_file: - for line in contents: - pl_file.write(line + b"\n") - pl_file.write(b"\n") # Final linefeed to prevent noeol file. - except OSError as exc: - raise FilesystemError( - exc, "create", (pl_normpath,), traceback.format_exc() - ) + with open(syspath(pl_normpath), "wb") as pl_file: + for line in contents: + pl_file.write(line + b"\n")