Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a405c5ed35 |
No files matched your search
+10
-2
@@ -121,14 +121,14 @@ class Journal:
|
||||
|
||||
def read_plan(path, root, force):
|
||||
"""Plan rows: (mkdirs, keeps, moves), header dropped, all paths under root."""
|
||||
mkdirs, keeps, moves = [], [], []
|
||||
mkdirs, keeps, moves, bad = [], [], [], []
|
||||
with open(path) as fh:
|
||||
header = fh.readline()
|
||||
if not header.startswith("action\t"):
|
||||
raise Refuse(f"{path} is not a plan (first column is "
|
||||
f"{header.split(chr(9))[0]!r}, expected 'action') —"
|
||||
" regenerate it with scripts/media-plan.py")
|
||||
for line in fh:
|
||||
for n, line in enumerate(fh, 2):
|
||||
f = line.rstrip("\n").split("\t")
|
||||
if f[0] == "mkdir":
|
||||
mkdirs.append(f[2])
|
||||
@@ -136,6 +136,14 @@ def read_plan(path, root, force):
|
||||
keeps.append(f[1])
|
||||
elif f[0] == "move":
|
||||
moves.append((f[1], f[2], f[3] if len(f) > 3 else ""))
|
||||
else:
|
||||
bad.append(f"line {n}: unknown action {f[0]!r} — this tool"
|
||||
" knows mkdir, keep and move (a rename is a move)")
|
||||
if bad:
|
||||
# A row this tool could not read is a plan bug, and a plan bug refuses:
|
||||
# silently dropping it turns a typo into a no-op run that reports success.
|
||||
head = "; ".join(bad[:3])
|
||||
raise Refuse(head + (f" (+{len(bad) - 3} more)" if len(bad) > 3 else ""))
|
||||
if not force:
|
||||
for p in mkdirs + keeps + [p for m in moves for p in m[:2]]:
|
||||
if p and not (p == root or p.startswith(root + os.sep)):
|
||||
|
||||
@@ -403,9 +403,12 @@ executor, the one-journal-per-arm law, the sweep and the rollback.
|
||||
- **The journal lives BESIDE the library root, never inside it** (default:
|
||||
`media-relayout.jsonl` next to the root): a log inside the library is a file
|
||||
Jellyfin indexes.
|
||||
- **Refusals are all-or-nothing.** An occupied target, a vanished source or a
|
||||
duplicate destination stops the run *before the first rename*, so the operator
|
||||
fixes one thing and re-runs instead of untangling a half-applied plan.
|
||||
- **Refusals are all-or-nothing.** An occupied target, a vanished source, a
|
||||
duplicate destination, or a plan row whose verb is not `mkdir`, `keep` or
|
||||
`move`, stops the run *before the first rename*, so the operator fixes one
|
||||
thing and re-runs instead of untangling a half-applied plan. An unread verb is
|
||||
refused by line number: silently dropping one turned a pass meant to rename
|
||||
two series folders into `applied: 0 moved` with exit 0 (2026-11-02).
|
||||
`--force` relaxes only the opinion guards; it never permits overwriting.
|
||||
- **A sweep deletes only what the journal proves the run emptied.** A folder is
|
||||
eligible when a journalled file moved out of it or out of something under it,
|
||||
|
||||
@@ -460,6 +460,39 @@ print(sum(1 for l in open(sys.argv[1]) if json.loads(l).get("op") == "move"))' "
|
||||
[ -e "$LIB/Alien (1979)/Alien.1979.1080p.BluRay.x265.10bit.mkv" ]
|
||||
}
|
||||
|
||||
# A hand-written plan row whose verb is not in the executor's vocabulary used
|
||||
# to be dropped in silence: read_plan's if/elif chain had no else, so a plan of
|
||||
# nothing but typos applied as a successful no-op (hit for real on 2026-11-02,
|
||||
# when a pass meant to rename two series folders reported "applied: 0 moved"
|
||||
# and exit 0). An unread row is a plan bug, and a plan bug refuses.
|
||||
@test "a plan row with a verb this tool does not know stops the whole run" {
|
||||
plan
|
||||
JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl"
|
||||
at "$LIB/Stray/Stray.S01E01.mkv" 4000
|
||||
printf 'rename\t%s\t%s\tfile\n' \
|
||||
"$LIB/Stray/Stray.S01E01.mkv" "$LIB/Stray/Stray - S01E01.mkv" >> "$OUT"
|
||||
run python3 "$APPLY" apply --plan "$OUT" --root "$LIB" --journal "$JOURNAL" --apply
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"rename"* ]]
|
||||
# all-or-nothing: the rows this tool DOES understand did not run either
|
||||
[ -e "$LIB/Stray/Stray.S01E01.mkv" ]
|
||||
[ ! -e "$JOURNAL" ]
|
||||
}
|
||||
|
||||
@test "a plan whose every row is an unknown verb refuses instead of reporting success" {
|
||||
JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl"
|
||||
at "$LIB/Stray/Stray.S01E01.mkv" 4000
|
||||
{ printf 'action\tsrc\tdst\tkind\n'
|
||||
printf 'rename\t%s\t%s\tfile\n' \
|
||||
"$LIB/Stray/Stray.S01E01.mkv" "$LIB/Stray/Stray - S01E01.mkv"
|
||||
} > "$BATS_TEST_TMPDIR/typo.tsv"
|
||||
run python3 "$APPLY" apply --plan "$BATS_TEST_TMPDIR/typo.tsv" --root "$LIB" --journal "$JOURNAL" --apply
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"unknown action"* ]]
|
||||
[[ "$output" != *"applied"* ]]
|
||||
[ -e "$LIB/Stray/Stray.S01E01.mkv" ]
|
||||
}
|
||||
|
||||
@test "a run interrupted between the journal write and the move finishes the job" {
|
||||
plan
|
||||
JOURNAL="$BATS_TEST_TMPDIR/journal.jsonl"
|
||||
|
||||
Reference in new issue
Block a user