fix(media): the series is the name before the marker; a sweep carries its own empty subfolders #95

Merged
dank merged 2 commits from fix/media-series-key into main 2026-10-30 20:20:25 -07:00
3 changed files with 29 additions and 2 deletions
Showing only changes of commit a803aa046a - Show all commits

No files matched your search

+14 -2
View File
@@ -241,6 +241,11 @@ def sweep_targets(root, journal):
journalled destination lives under it. A symlink anywhere inside disqualifies journalled destination lives under it. A symlink anywhere inside disqualifies
the whole subtree: this tool never follows a link, and deleting the folder the whole subtree: this tool never follows a link, and deleting the folder
that holds one would delete someone's mount point. 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"] 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"} 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 False
return True return True
return sorted((d for d in marked if removable(d)), roots = [d for d in marked if removable(d)]
key=lambda q: (-q.count(os.sep), q)) 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): def do_sweep(args, root, journal):
@@ -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 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` 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. 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 - **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 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 `Monk`. Keyed on the whole title instead, every episode became a group of one
+11
View File
@@ -408,6 +408,17 @@ PY
[ -d "$LIB/A Empty Placeholder" ] [ -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" { @test "sweep without --apply reports the husks and changes nothing" {
plan plan
apply_plan apply_plan