diff --git a/scripts/media-apply.py b/scripts/media-apply.py index 751d77c..9351cfd 100755 --- a/scripts/media-apply.py +++ b/scripts/media-apply.py @@ -241,6 +241,11 @@ def sweep_targets(root, journal): journalled destination lives under it. A symlink anywhere inside disqualifies the whole subtree: this tool never follows a link, and deleting the folder that holds one would delete someone's mount point. + + Proof carries downward: once a folder qualifies, every empty directory inside + it qualifies too. A release's junk (``Screens/``, ``Sample/``) is never itself + a move source, so without that rule it holds its emptied parent on disk + forever — the parent can never be removed while the child stands. """ moves = [r for r in journal.records if r.get("op") == "move"] made = {r["dst"] for r in journal.records if r.get("op") == "mkdir"} @@ -268,8 +273,15 @@ def sweep_targets(root, journal): return False return True - return sorted((d for d in marked if removable(d)), - key=lambda q: (-q.count(os.sep), q)) + roots = [d for d in marked if removable(d)] + targets = set(roots) + for d in roots: + for dirpath, dirnames, _ in os.walk(d): + for c in dirnames: + p = os.path.join(dirpath, c) + if removable(p): + targets.add(p) + return sorted(targets, key=lambda q: (-q.count(os.sep), q)) def do_sweep(args, root, journal): diff --git a/serverAppConfigs-docs/Dankserver/Media Relayout.md b/serverAppConfigs-docs/Dankserver/Media Relayout.md index f9b17b2..c40ca0a 100644 --- a/serverAppConfigs-docs/Dankserver/Media Relayout.md +++ b/serverAppConfigs-docs/Dankserver/Media Relayout.md @@ -155,6 +155,10 @@ curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n" \ inside disqualifies the subtree, because the thing behind a link is not this tool's to delete. Every removal is journalled as `rmdir` and `rollback` recreates the folder, so the sweep is undone by the same command as the moves. + Proof carries downward: inside a folder the journal proves emptied, every + empty directory goes with it. A release's `Screens/` or `Sample/` is never a + move source itself, so 146 of them held their emptied parents on disk after + the first sweep. - **An episode-shaped name marks a SERIES, not a film — and the series is the name BEFORE the marker.** `Monk - S05E03 - Mr. Monk and the Big Game` is series `Monk`. Keyed on the whole title instead, every episode became a group of one diff --git a/tests/media_relayout.bats b/tests/media_relayout.bats index 900c92a..bd0a614 100755 --- a/tests/media_relayout.bats +++ b/tests/media_relayout.bats @@ -408,6 +408,17 @@ PY [ -d "$LIB/A Empty Placeholder" ] } +@test "a swept folder carries the empty junk folders inside it" { + mkdir -p "$LIB/AMERICAN POP/Screens" # screenshots, never a move source + plan + apply_plan + run python3 "$APPLY" sweep --root "$LIB" --journal "$JOURNAL" --apply + [ "$status" -eq 0 ] + [[ "$output" == *"0 left in place"* ]] + [ ! -e "$LIB/AMERICAN POP/Screens" ] + [ ! -e "$LIB/AMERICAN POP" ] +} + @test "sweep without --apply reports the husks and changes nothing" { plan apply_plan