add(import): merge multi-part entries into one audiobook

This commit is contained in:
Steppenstreuner
2026-08-28 20:33:26 +02:00
parent 951f68ca7d
commit 36263d66fb
7 changed files with 173 additions and 7 deletions
+90
View File
@@ -414,3 +414,93 @@ def test_bulk_items_series_then_scan_matches_by_episode_number(client):
n = int(item["name"][:3])
assert item["suggested_request_id"] == by_volume[n], item
assert item["score"] >= 90
def test_merged_multipart_import(client):
"""A multi-part episode ("Teil 1/2/3") imported as one audiobook: the client
merges the scanned entries and posts their files as a single item."""
root = client.tmp_path / "library" / "ddf"
lib_id = client.post("/api/libraries", json={
"name": "DDF", "media_type": "audiobook", "root_path": str(root),
"folder_template": "{Series}/{Title}", "file_template": "{Title}",
}).json()["id"]
req_id = client.post("/api/requests", json={
"library_id": lib_id, "title": "Die drei ??? und die Toteninsel",
"series": "Die drei ???", "volume": 100,
}).json()["id"]
base = client.downloads / "Folgen" / "100 - Toteninsel"
for part in ("Teil 1", "Teil 2", "Teil 3"):
d = base / part
d.mkdir(parents=True)
for i in (1, 2):
(d / f"{i:02d}.mp3").write_bytes(b"")
(base / "Cover").mkdir()
(base / "Cover" / "front.jpg").write_bytes(b"")
# "Teil N" folders are disc folders, so the scan already yields one entry
items = client.get("/api/import/scan").json()["items"]
assert len(items) == 1 and len(items[0]["files"]) == 6
# a split scan yields the parts separately - that is what the merge button
# in the UI stitches back together
parts = client.get("/api/import/scan", params={"split_dirs": True}).json()["items"]
assert len(parts) == 6
merged_files = [f for p in parts for f in p["files"]]
res = client.post("/api/import", json={"items": [{
"path": str(base), "is_dir": True, "files": merged_files, "request_id": req_id,
}]}).json()["results"][0]
assert res["ok"], res
dest = sorted(p.name for p in Path(res["dest"]).iterdir())
assert dest == [f"Die drei ??? und die Toteninsel - Part 0{i}.mp3" for i in range(1, 7)]
# emptied part folders are gone, the cover folder survives
assert not (base / "Teil 1").exists()
assert (base / "Cover" / "front.jpg").exists()
def test_import_rejects_files_outside_download_dir(client):
lib_id = client.post("/api/libraries", json={
"name": "L", "media_type": "audiobook", "root_path": str(client.tmp_path / "l"),
}).json()["id"]
req_id = client.post("/api/requests",
json={"library_id": lib_id, "title": "X"}).json()["id"]
outside = client.tmp_path / "elsewhere"
outside.mkdir()
(outside / "a.mp3").write_bytes(b"")
folder = client.downloads / "ok"
folder.mkdir()
(folder / "b.mp3").write_bytes(b"")
res = client.post("/api/import", json={"items": [{
"path": str(folder), "is_dir": True,
"files": [str(folder / "b.mp3"), str(outside / "a.mp3")], "request_id": req_id,
}]}).json()["results"][0]
assert not res["ok"] and "outside" in res["error"]
assert (outside / "a.mp3").exists()
def test_merge_at_download_root_keeps_the_download_dir(client):
"""Merging entries that sit directly in the download dir makes the item path
the download dir itself - which must never be removed."""
lib_id = client.post("/api/libraries", json={
"name": "L", "media_type": "audiobook", "root_path": str(client.tmp_path / "l"),
}).json()["id"]
req_id = client.post("/api/requests",
json={"library_id": lib_id, "title": "Toteninsel"}).json()["id"]
files = []
for part in ("Teil 1", "Teil 2"):
d = client.downloads / part
d.mkdir()
(d / "a.mp3").write_bytes(b"")
(d / "b.mp3").write_bytes(b"")
files += [str(d / "a.mp3"), str(d / "b.mp3")]
res = client.post("/api/import", json={"items": [{
"path": str(client.downloads), "is_dir": True, "files": files,
"request_id": req_id,
}]}).json()["results"][0]
assert res["ok"], res
assert client.downloads.is_dir()
assert not (client.downloads / "Teil 1").exists()