fix(import): never overwrite an existing file when importing a folder
This commit is contained in:
@@ -28,7 +28,7 @@ docker compose up -d --build
|
|||||||
Audible führt Reihen manchmal anders, als man sie ablegen möchte (*Harry Potter* steht dort unter *Wizarding World*, und das landet über `{Series}` im Ordnernamen). Deshalb:
|
Audible führt Reihen manchmal anders, als man sie ablegen möchte (*Harry Potter* steht dort unter *Wizarding World*, und das landet über `{Series}` im Ordnernamen). Deshalb:
|
||||||
|
|
||||||
- **Vor dem Anlegen**: Im Import-Dialog übernimmt ✎ an einem Treffer dessen Felder (Titel, Autor, Serie, Band) in die Eingabemaske — Sprecher, Cover, Jahr und ASIN bleiben erhalten. Erst *Anlegen & verbinden* erzeugt die Anfrage.
|
- **Vor dem Anlegen**: Im Import-Dialog übernimmt ✎ an einem Treffer dessen Felder (Titel, Autor, Serie, Band) in die Eingabemaske — Sprecher, Cover, Jahr und ASIN bleiben erhalten. Erst *Anlegen & verbinden* erzeugt die Anfrage.
|
||||||
- **Danach**: Im Tab *Missing* bzw. *Importiert* öffnet ein Klick auf die Karte alle Felder. *Speichern & neu taggen* schreibt die ID3-Tags neu, **Speichern & neu ablegen** wendet das Namensschema der Library auf die bereits verschobenen Dateien an — der Ordner wird also umbenannt bzw. verschoben, Cover ziehen mit um und leer gewordene Ordner verschwinden. Ein belegter Zielordner bricht den Vorgang ab, statt etwas zu überschreiben.
|
- **Danach**: Im Tab *Missing* bzw. *Importiert* öffnet ein Klick auf die Karte alle Felder. *Speichern & neu taggen* schreibt die ID3-Tags neu, **Speichern & neu ablegen** wendet das Namensschema der Library auf die bereits verschobenen Dateien an — der Ordner wird also umbenannt bzw. verschoben, Cover ziehen mit um und leer gewordene Ordner verschwinden. Ein belegter Zielordner bricht den Vorgang ab, statt etwas zu überschreiben — dasselbe gilt beim Import selbst: Rendern zwei Titel nach dem Namensschema denselben Pfad (etwa zwei Bände mit identischem Titel bei einem Schema ohne `{Volume}`), scheitert der zweite mit einer Meldung, statt den ersten zu überschreiben.
|
||||||
|
|
||||||
## Sprachen
|
## Sprachen
|
||||||
|
|
||||||
|
|||||||
@@ -889,3 +889,42 @@ def test_imported_request_with_files_in_place_is_still_refused(client):
|
|||||||
"request_id": req_id,
|
"request_id": req_id,
|
||||||
}]}).json()["results"][0]
|
}]}).json()["results"][0]
|
||||||
assert not res["ok"] and "bereits importiert" in res["error"]
|
assert not res["ok"] and "bereits importiert" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_titles_rendering_to_the_same_name_do_not_overwrite(client):
|
||||||
|
"""Two books whose templates produce the same folder and file names must not
|
||||||
|
silently replace each other - that loses files and confuses ABS."""
|
||||||
|
root = client.tmp_path / "library" / "dcc"
|
||||||
|
lib_id = client.post("/api/libraries", json={
|
||||||
|
"name": "DCC", "media_type": "audiobook", "root_path": str(root),
|
||||||
|
"folder_template": "{Series}", "file_template": "{Title}",
|
||||||
|
}).json()["id"]
|
||||||
|
ids = [client.post("/api/requests", json={
|
||||||
|
"library_id": lib_id, "title": "Dungeon Crawler Carl",
|
||||||
|
"series": "Dungeon Crawler Carl", "volume": v,
|
||||||
|
}, params={"allow_duplicate": True}).json()["id"] for v in (1, 7)]
|
||||||
|
|
||||||
|
dests = []
|
||||||
|
for n, req_id in enumerate(ids, 1):
|
||||||
|
src = client.downloads / f"teil{n}"
|
||||||
|
src.mkdir()
|
||||||
|
for i in (1, 2):
|
||||||
|
(src / f"{i:02d}.mp3").write_bytes(f"Teil {n} Datei {i}".encode())
|
||||||
|
res = client.post("/api/import", json={"items": [{
|
||||||
|
"path": str(src), "is_dir": True,
|
||||||
|
"files": [str(f) for f in sorted(src.iterdir())], "request_id": req_id,
|
||||||
|
}]}).json()["results"][0]
|
||||||
|
dests.append(res)
|
||||||
|
|
||||||
|
assert dests[0]["ok"]
|
||||||
|
assert not dests[1]["ok"]
|
||||||
|
assert "already exists" in dests[1]["error"]
|
||||||
|
# the first title is untouched and the second one still has its files
|
||||||
|
folder = root / "Dungeon Crawler Carl"
|
||||||
|
assert sorted(p.name for p in folder.iterdir()) == [
|
||||||
|
"Dungeon Crawler Carl - Part 01.mp3", "Dungeon Crawler Carl - Part 02.mp3",
|
||||||
|
]
|
||||||
|
content = (folder / "Dungeon Crawler Carl - Part 01.mp3").read_bytes()
|
||||||
|
assert b"Teil 1 Datei 1" in content and b"Teil 2" not in content
|
||||||
|
assert sorted(p.name for p in (client.downloads / "teil2").iterdir()) == \
|
||||||
|
["01.mp3", "02.mp3"]
|
||||||
|
|||||||
@@ -38,8 +38,21 @@ def import_item(item_path: str, files: list[str], is_dir: bool, request, library
|
|||||||
paths = [Path(f) for f in files]
|
paths = [Path(f) for f in files]
|
||||||
if is_dir and len(paths) > 1:
|
if is_dir and len(paths) > 1:
|
||||||
width = max(2, len(str(len(paths))))
|
width = max(2, len(str(len(paths))))
|
||||||
for i, src in enumerate(paths, 1):
|
targets = [
|
||||||
dest = folder / sanitize(f"{base} - Part {i:0{width}d}{src.suffix.lower()}")
|
folder / sanitize(f"{base} - Part {i:0{width}d}{src.suffix.lower()}")
|
||||||
|
for i, src in enumerate(paths, 1)
|
||||||
|
]
|
||||||
|
# check every target first: moving file by file would leave a half
|
||||||
|
# imported folder behind, and without the check a second title rendering
|
||||||
|
# to the same name would silently overwrite the first one
|
||||||
|
taken = [d for d in targets if d.exists()]
|
||||||
|
if taken:
|
||||||
|
raise FileExistsError(
|
||||||
|
f"Destination already exists: {taken[0]}"
|
||||||
|
+ (f" (und {len(taken) - 1} weitere)" if len(taken) > 1 else "")
|
||||||
|
+ " — trägt ein anderer Titel nach diesem Namensschema denselben Namen?"
|
||||||
|
)
|
||||||
|
for i, (src, dest) in enumerate(zip(paths, targets), 1):
|
||||||
shutil.move(str(src), dest)
|
shutil.move(str(src), dest)
|
||||||
if request.media_type == "audiobook":
|
if request.media_type == "audiobook":
|
||||||
tagger.tag_audio(dest, request, track=i, total=len(paths))
|
tagger.tag_audio(dest, request, track=i, total=len(paths))
|
||||||
|
|||||||
Reference in New Issue
Block a user