mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 15:10:26 +01:00
fix(naming): respect subfolder order when assigning audiobook part numbers (#1123)
## Summary Audiobooks whose files are split across nested subfolders now keep their on-disk folder order when "Rename and Organize" assigns part numbers. Previously `natural_sort_key` keyed on `Path(path).name`, so identically named files from different folders (`001.mp3` in `00_Introduction/` vs `06_Side 6/`) collapsed together and the part sequence scrambled. ## Background zazizou reported in #1007 that the total file count was right but `06_Side 6/001.mp3` came out as part 1 instead of `00_Introduction/001_About.mp3`. `scan.py` flattens every nested file into one list and `assign_part_numbers` sorts it with `natural_sort_key`, which discarded the parent-folder prefix. The fix pads the existing `PAD_NUMBERS_PATTERN` numbers over the full relative path (`str(path).lower()`) rather than the basename, so nested audiobooks sort by folder first and then by filename. Flat directories are unaffected because their files share a prefix, and `natural_sort_key` is only called from `assign_part_numbers`, so nothing else changes. I updated the test that pinned the old basename-only ordering and added nested-subfolder and duplicate-basename cases; `pytest tests/core/test_naming.py tests/core/test_part_number_extraction.py` passes (102 tests). Closes #1007 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
co-authored by
Matt Van Horn
parent
c9a2b5eccf
commit
91b1064689
@@ -91,8 +91,7 @@ PAD_NUMBERS_PATTERN = re.compile(r"\d+")
|
||||
|
||||
def natural_sort_key(path: str | Path) -> str:
|
||||
"""Generate a sort key with padded numbers for natural sorting."""
|
||||
filename = Path(path).name.lower()
|
||||
return PAD_NUMBERS_PATTERN.sub(lambda m: m.group().zfill(9), filename)
|
||||
return PAD_NUMBERS_PATTERN.sub(lambda m: m.group().zfill(9), str(path).lower())
|
||||
|
||||
|
||||
def assign_part_numbers(
|
||||
|
||||
@@ -43,10 +43,10 @@ class TestNaturalSortKey:
|
||||
"file10.mp3",
|
||||
]
|
||||
|
||||
def test_uses_filename_only(self):
|
||||
def test_uses_full_path(self):
|
||||
files = [Path("/z/dir/file1.mp3"), Path("/a/dir/file2.mp3")]
|
||||
sorted_files = sorted(files, key=natural_sort_key)
|
||||
assert sorted_files[0].name == "file1.mp3"
|
||||
assert sorted_files == [Path("/a/dir/file2.mp3"), Path("/z/dir/file1.mp3")]
|
||||
|
||||
|
||||
class TestAssignPartNumbers:
|
||||
@@ -72,6 +72,23 @@ class TestAssignPartNumbers:
|
||||
(Path("Chapter 10.mp3"), "03"),
|
||||
]
|
||||
|
||||
def test_nested_folders_are_numbered_in_folder_order(self):
|
||||
files = [
|
||||
Path("06_Side 6/002.mp3"),
|
||||
Path("01_Side 1/001.mp3"),
|
||||
Path("00_Introduction/001_About.mp3"),
|
||||
Path("06_Side 6/001.mp3"),
|
||||
Path("01_Side 1/002.mp3"),
|
||||
]
|
||||
|
||||
assert assign_part_numbers(files) == [
|
||||
(Path("00_Introduction/001_About.mp3"), "01"),
|
||||
(Path("01_Side 1/001.mp3"), "02"),
|
||||
(Path("01_Side 1/002.mp3"), "03"),
|
||||
(Path("06_Side 6/001.mp3"), "04"),
|
||||
(Path("06_Side 6/002.mp3"), "05"),
|
||||
]
|
||||
|
||||
def test_custom_zero_padding(self):
|
||||
files = [Path("a.mp3"), Path("b.mp3")]
|
||||
assert assign_part_numbers(files, zero_pad_width=3) == [
|
||||
@@ -190,7 +207,10 @@ class TestEdgeCases:
|
||||
def test_identical_filenames_different_dirs(self):
|
||||
files = [Path("/dir2/track.mp3"), Path("/dir1/track.mp3")]
|
||||
result = assign_part_numbers(files)
|
||||
assert len(result) == 2
|
||||
assert result == [
|
||||
(Path("/dir1/track.mp3"), "01"),
|
||||
(Path("/dir2/track.mp3"), "02"),
|
||||
]
|
||||
|
||||
def test_unicode_filenames(self):
|
||||
files = [Path("日本語タイトル 02.mp3"), Path("日本語タイトル 01.mp3")]
|
||||
|
||||
Reference in New Issue
Block a user