mirror of
https://github.com/beetbox/beets.git
synced 2026-01-09 09:22:55 +01:00
Do not remove everything that follows a dot in item destination path (#5774)
This PR addresses an issue where path legalization, specifically the `truncate_path` function, incorrectly removed parts of filenames that followed a dot. This occurred because `pathlib.Path.with_suffix` was used, which replaces the existing suffix (or what it considers a suffix) rather than just appending. The fix modifies `truncate_path` to manually append the original suffix after truncating the filename stem. This ensures that dots within the filename, not part of the actual extension, are preserved. Fixes #5771.
This commit is contained in:
commit
0379f68aea
4 changed files with 22 additions and 12 deletions
|
|
@ -349,6 +349,7 @@ class LibModel(dbcore.Model["Library"]):
|
|||
|
||||
# Config key that specifies how an instance should be formatted.
|
||||
_format_config_key: str
|
||||
path: bytes
|
||||
|
||||
@cached_classproperty
|
||||
def writable_media_fields(cls) -> set[str]:
|
||||
|
|
@ -644,7 +645,7 @@ class Item(LibModel):
|
|||
_format_config_key = "format_item"
|
||||
|
||||
# Cached album object. Read-only.
|
||||
__album = None
|
||||
__album: Album | None = None
|
||||
|
||||
@cached_classproperty
|
||||
def _relation(cls) -> type[Album]:
|
||||
|
|
@ -663,9 +664,9 @@ class Item(LibModel):
|
|||
)
|
||||
|
||||
@property
|
||||
def filepath(self) -> Path | None:
|
||||
def filepath(self) -> Path:
|
||||
"""The path to the item's file as pathlib.Path."""
|
||||
return Path(os.fsdecode(self.path)) if self.path else self.path
|
||||
return Path(os.fsdecode(self.path))
|
||||
|
||||
@property
|
||||
def _cached_album(self):
|
||||
|
|
@ -1126,7 +1127,7 @@ class Item(LibModel):
|
|||
)
|
||||
|
||||
lib_path_str, fallback = util.legalize_path(
|
||||
subpath, db.replacements, os.path.splitext(self.path)[1]
|
||||
subpath, db.replacements, self.filepath.suffix
|
||||
)
|
||||
if fallback:
|
||||
# Print an error message if legalization fell back to
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@ def truncate_path(str_path: str) -> str:
|
|||
path = Path(str_path)
|
||||
parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]]
|
||||
stem = truncate_str(path.stem, max_length - len(path.suffix))
|
||||
return str(Path(*parent_parts, stem).with_suffix(path.suffix))
|
||||
return str(Path(*parent_parts, stem)) + path.suffix
|
||||
|
||||
|
||||
def _legalize_stage(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ New features:
|
|||
|
||||
Bug fixes:
|
||||
|
||||
* :doc:`/reference/pathformat`: Fixed a regression where path legalization
|
||||
incorrectly removed parts of user-configured path formats that followed a dot
|
||||
(**.**).
|
||||
:bug:`5771`
|
||||
|
||||
For packagers:
|
||||
|
||||
Other changes:
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ class PathConversionTest(BeetsTestCase):
|
|||
|
||||
|
||||
class TestPathLegalization:
|
||||
_p = pytest.param
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_max_filename_length(self, monkeypatch):
|
||||
monkeypatch.setattr("beets.util.get_max_filename_length", lambda: 5)
|
||||
|
|
@ -178,20 +180,22 @@ class TestPathLegalization:
|
|||
@pytest.mark.parametrize(
|
||||
"path, expected",
|
||||
[
|
||||
("abcdeX/fgh", "abcde/fgh"),
|
||||
("abcde/fXX.ext", "abcde/f.ext"),
|
||||
("a🎹/a.ext", "a🎹/a.ext"),
|
||||
("ab🎹/a.ext", "ab/a.ext"),
|
||||
_p("abcdeX/fgh", "abcde/fgh", id="truncate-parent-dir"),
|
||||
_p("abcde/fXX.ext", "abcde/f.ext", id="truncate-filename"),
|
||||
# note that 🎹 is 4 bytes long:
|
||||
# >>> "🎹".encode("utf-8")
|
||||
# b'\xf0\x9f\x8e\xb9'
|
||||
_p("a🎹/a.ext", "a🎹/a.ext", id="unicode-fit"),
|
||||
_p("ab🎹/a.ext", "ab/a.ext", id="unicode-truncate-fully-one-byte-over-limit"),
|
||||
_p("f.a.e", "f.a.e", id="persist-dot-in-filename"), # see #5771
|
||||
],
|
||||
)
|
||||
) # fmt: skip
|
||||
def test_truncate(self, path, expected):
|
||||
path = path.replace("/", os.path.sep)
|
||||
expected = expected.replace("/", os.path.sep)
|
||||
|
||||
assert util.truncate_path(path) == expected
|
||||
|
||||
_p = pytest.param
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"replacements, expected_path, expected_truncated",
|
||||
[ # [ repl before truncation, repl after truncation ]
|
||||
|
|
|
|||
Loading…
Reference in a new issue