Fix extension issue in convert plugin when exporting a playlist (#5203)

Fix extension substitution inside path of the exported playlist.

Before this, the exported playlist contained relative paths pointing to
the
converted files BUT the extension were not substituted comparing to
before and
the after the conversion. Therefore, running the playlist will fail for
files
which have been converted and where extension have changed.

Example:
1. Convert `/path/to/library/artist.flac` to
`/path/to/converted/artist.mp3` using the `-m playlist.m3u` command-line
flag.
2. Open the generated playlist, and find the incorrect path
`/path/to/converted/artist.flac` inside.
This commit is contained in:
Šarūnas Nejus 2026-03-04 17:04:27 +00:00 committed by GitHub
commit c1fa0a653d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 13 deletions

View file

@ -627,6 +627,28 @@ class ConvertPlugin(BeetsPlugin):
album, dest, path_formats, pretend, link, hardlink
)
# If the user supplied a playlist name, create a playlist for files
# copied to the destination.
pl_normpath = None
items_paths = None
if playlist:
_, ext = get_format(fmt)
# Playlist paths are understood as relative to the dest directory.
pl_normpath = util.normpath(playlist)
pl_dir = os.path.dirname(pl_normpath)
items_paths = []
for item in items:
item_path = item.destination(
basedir=dest, path_formats=path_formats
)
# When keeping new files in the library, destination paths
# keep original files and extensions.
if not opts.keep_new and should_transcode(item, fmt, force):
item_path = replace_ext(item_path, ext)
items_paths.append(os.path.relpath(item_path, pl_dir))
self._parallel_convert(
dest,
opts.keep_new,
@ -641,20 +663,7 @@ class ConvertPlugin(BeetsPlugin):
)
if playlist:
# Playlist paths are understood as relative to the dest directory.
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,
)
for item in items
]
if not pretend:
m3ufile = M3UFile(playlist)
m3ufile.set_contents(items_paths)

View file

@ -56,6 +56,8 @@ Bug fixes
- :ref:`import-cmd` Duplicate detection now works for as-is imports (when
``autotag`` is disabled). Previously, ``duplicate_keys`` and
``duplicate_action`` config options were silently ignored for as-is imports.
- :doc:`/plugins/convert`: Fix extension substitution inside path of the
exported playlist.
For plugin developers
~~~~~~~~~~~~~~~~~~~~~

View file

@ -288,6 +288,26 @@ class ConvertCliTest(ConvertTestCase, ConvertCommand):
converted = self.convert_dest / "converted.ops"
assert self.file_endswith(converted, "opus")
def assert_playlist_entry(self, expected_entry, *args):
self.io.addinput("y")
self.run_convert(*args, "--playlist", "playlist.m3u8")
lines = (self.convert_dest / "playlist.m3u8").read_text().splitlines()
assert lines[0] == "#EXTM3U"
assert lines[1] == expected_entry
def test_playlist_entry_uses_config_format(self):
self.assert_playlist_entry("converted.mp3")
def test_playlist_entry_uses_cli_format(self):
self.assert_playlist_entry("converted.ops", "--format", "opus")
def test_playlist_entry_keeps_original_extension_when_not_transcoded(self):
self.config["convert"]["no_convert"] = "format:ogg"
self.assert_playlist_entry("converted.ogg")
def test_playlist_entry_keep_new_points_to_destination_file(self):
self.assert_playlist_entry("converted.ogg", "--keep-new")
@_common.slow_test()
class NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand):