From a816dcb27adc19a2b2912fc7a0f63e82947e473d Mon Sep 17 00:00:00 2001 From: Steppenstreuner Date: Fri, 28 Aug 2026 20:49:32 +0200 Subject: [PATCH] fix(import): name merged parts after their folder and clean up empty ones --- static/app.js | 6 +++++- tests/test_import_flow.py | 34 ++++++++++++++++++++++++++++++++++ wordarr/importer/mover.py | 13 ++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/static/app.js b/static/app.js index de3d2d3..da169ba 100644 --- a/static/app.js +++ b/static/app.js @@ -816,7 +816,11 @@ $("#import-merge").addEventListener("click", () => { toast("Die Einträge liegen in verschiedenen Ordnern", true); return; } - const name = commonPrefix(chosen.map((it) => it.name)) || chosen[0].name; + // parts may share no prefix at all ("A - Sphinx", "B - Volk"): then the + // folder holding them carries the real title ("100 - Toteninsel") + const parentName = (chosen[0].rel_dir || "").split("/").filter(Boolean).pop(); + const name = + commonPrefix(chosen.map((it) => it.name)) || parentName || chosen[0].name; const merged = { path: chosen[0].path.slice(0, chosen[0].path.length - chosen[0].name.length - 1) || chosen[0].path, name, diff --git a/tests/test_import_flow.py b/tests/test_import_flow.py index b8732d7..98fc5df 100644 --- a/tests/test_import_flow.py +++ b/tests/test_import_flow.py @@ -504,3 +504,37 @@ def test_merge_at_download_root_keeps_the_download_dir(client): assert res["ok"], res assert client.downloads.is_dir() assert not (client.downloads / "Teil 1").exists() + + +def test_merged_parts_clean_up_nested_source_folders(client): + """"100 - Toteninsel/A - Sphinx/CD/*.mp3" - every emptied level below the + merged item goes away, the cover folder keeps its parent alive.""" + lib_id = client.post("/api/libraries", json={ + "name": "L", "media_type": "audiobook", "root_path": str(client.tmp_path / "l"), + "folder_template": "{Title}", "file_template": "{Title}", + }).json()["id"] + req_id = client.post("/api/requests", + json={"library_id": lib_id, "title": "Toteninsel"}).json()["id"] + + base = client.downloads / "100 - Toteninsel" + files = [] + for part in ("A - Sphinx", "B - Volk", "C - Graeber"): + cd = base / part / "CD" + cd.mkdir(parents=True) + for i in (1, 2): + f = cd / f"{i:02d}.mp3" + f.write_bytes(b"") + files.append(str(f)) + (base / "Cover").mkdir() + (base / "Cover" / "front.jpg").write_bytes(b"") + + res = client.post("/api/import", json={"items": [{ + "path": str(base), "is_dir": True, "files": files, "request_id": req_id, + }]}).json()["results"][0] + assert res["ok"], res + assert sorted(p.name for p in Path(res["dest"]).iterdir()) == [ + f"Toteninsel - Part 0{i}.mp3" for i in range(1, 7) + ] + assert not (base / "A - Sphinx").exists() + assert not (base / "B - Volk").exists() + assert (base / "Cover" / "front.jpg").exists() diff --git a/wordarr/importer/mover.py b/wordarr/importer/mover.py index 1734c90..c553c59 100644 --- a/wordarr/importer/mover.py +++ b/wordarr/importer/mover.py @@ -27,10 +27,17 @@ def import_item(item_path: str, files: list[str], is_dir: bool, request, library # files (cover art, booklets) is left alone. src_dir = Path(item_path) download_root = Path(config.DOWNLOAD_DIR).resolve() - candidates = {src_dir} | {p.parent for p in paths} + # collect every folder the files came from, up to (and including) the + # item itself - "…/100 - Toteninsel/A - Sphinx/CD" contributes all three + candidates = {src_dir} + for p in paths: + d = p.parent + while d == src_dir or src_dir in d.parents: + candidates.add(d) + if d == src_dir: + break + d = d.parent for d in sorted(candidates, key=lambda p: len(p.parts), reverse=True): - if d != src_dir and src_dir not in d.parents: - continue # never touch anything outside the item if d.resolve() == download_root: continue # merged items can point at the download dir itself if d.is_dir() and not any(f.is_file() for f in d.rglob("*")):