1. scripts/media-tv.py — the other half of the refusal
scripts/media-plan.py refuses to file TV: it parks episode files in a _TV needs a TV library - <series> pile for a human. This is the mover. It
walks the movie library, groups the files whose own name carries an episode
marker by series — the name before the marker, the same key PR #95 settled
for the planner, imported from media-titles.py rather than restated — and
writes a plan in the executor's own format that places each series in the shape
both Jellyfin and Plex read without help:
<library>/<Series> (<year>)/Season <NN>/<the file's own name>.<ext>
Plan only, never a rename: media-apply.py apply owns every move, with its own
journal (media-relayout-tv.jsonl) so a TV move is not welded to the movie
relayout, and --root /mnt/entropy because the executor refuses any path
outside --root.
What it refuses, printing the reason and leaving the file alone:
fewer than --min (5) episode-shaped files under one series name — The Godfather Part 2 is a film, not a series;
an episode whose name states no season number: no invented Season 01;
an episode the destination library already holds at the same byte count;
a group whose destination two files both claim (the executor refuses the
whole plan before its first rename — only Frank decides the keeper).
Live dry run (read-only, nothing moved yet)
Series
Decision
Big Little Lies
7 episodes (S1) → TVShows/Big Little Lies/Season 01/
Leah Remini: Scientology and the Aftermath (2016)
37 episodes → new show folder
Monk (2002)
124 episodes refused — the TV library already holds them
14 shows with 1–2 episode-shaped files
left in place, reported
Plan: 44 moves, 8 folders.
The Monk finding is the reason this PR exists./mnt/entropy/Movies/Monk 2002 Seasons 1 to 8 and /mnt/entropy/TVShows/Monk 2002 Seasons 1 to 8 Complete Mixed x264 [i_c] are the same 124 episodes: same episode set (S01–S08, nothing unique
to either side), identical byte count for every one of them — ~31 GiB of
redundancy on a volume at 100 %. A mover that did not look where it was filing
would have put both copies inside the TV library. It does not delete anything:
which copy dies is Frank's call, and it is now a decision with a number on it.
Two naming defects the live run exposed, both test-first:
a show whose own name carries the year got it twice (X (2016) (2016)) — a
box set stamps the year into every file name, so the folder year is dropped
when the head already ends in one;
a series name that arrived lower-cased (monk) would have been filed among
properly-cased shows — a head with no capital at all is title-cased, a name
the publisher wrote with case is left exactly as written.
2. scripts/media-apply.py — a failed rmdir is not a removal
The gone set was built from the journal's intent records, so a folder whose rmdir failed (read-only parent, non-empty child) was permanently blacklisted: removable() returned False forever and no later sweep could ever finish the
job. The live movie sweep hit exactly this — 4,517 folders targeted, 146 left
standing, and those 146 could never be removed by any subsequent run.
gone now means actually gone: a path is out of scope only if it no longer
exists. A failed intent is retried by the next sweep, which is the whole point
of running the sweep twice — the second pass is where a chmod fix-up lands.
3. Tests / evidence
tests/media_relayout.bats: 9 new tests, each red against the previous
revision before the code existed — 8 TV (shape, film-not-series, sidecar,
idempotence, doubled year, casing, duplicate-episode refusal, second encode
still moved) + 1 sweep-retry (rmdir blocked by a chmod 500 parent, then the
blocker fixed, then the second sweep finishes the job).
Full ./tests/run.sh green on this commit; the Playwright gate is unaffected
(no fleet config path touched).
4. Not in this PR — nothing is applied
No live move has run. After merge, the application is two commands on
Dankserver with the --root /mnt/entropy guard and the TV journal, then the
movie sweep again (the 146 survivors are now removable), then both library
rescans. The Monk duplicate stays exactly where it is until Frank says which
copy dies.
## 1. `scripts/media-tv.py` — the other half of the refusal
`scripts/media-plan.py` refuses to file TV: it parks episode files in a
`_TV needs a TV library - <series>` pile for a human. This is the mover. It
walks the movie library, groups the files whose own name carries an episode
marker by series — the name **before** the marker, the same key PR #95 settled
for the planner, imported from `media-titles.py` rather than restated — and
writes a plan in the executor's own format that places each series in the shape
both Jellyfin and Plex read without help:
```
<library>/<Series> (<year>)/Season <NN>/<the file's own name>.<ext>
```
Plan only, never a rename: `media-apply.py apply` owns every move, with its own
journal (`media-relayout-tv.jsonl`) so a TV move is not welded to the movie
relayout, and `--root /mnt/entropy` because the executor refuses any path
outside `--root`.
What it refuses, printing the reason and leaving the file alone:
- fewer than `--min` (5) episode-shaped files under one series name — `The
Godfather Part 2` is a film, not a series;
- an episode whose name states no season number: no invented `Season 01`;
- an episode the **destination** library already holds at the same byte count;
- a group whose destination two files both claim (the executor refuses the
whole plan before its first rename — only Frank decides the keeper).
### Live dry run (read-only, nothing moved yet)
|Series|Decision|
|---|---|
|Big Little Lies|7 episodes (S1) → `TVShows/Big Little Lies/Season 01/`|
|Leah Remini: Scientology and the Aftermath (2016)|37 episodes → new show folder|
|**Monk (2002)**|**124 episodes refused — the TV library already holds them**|
|14 shows with 1–2 episode-shaped files|left in place, reported|
Plan: **44 moves, 8 folders.**
**The Monk finding is the reason this PR exists.** `/mnt/entropy/Movies/Monk 2002
Seasons 1 to 8` and `/mnt/entropy/TVShows/Monk 2002 Seasons 1 to 8 Complete Mixed
x264 [i_c]` are the same 124 episodes: same episode set (S01–S08, nothing unique
to either side), identical byte count for every one of them — ~31 GiB of
redundancy on a volume at 100 %. A mover that did not look where it was filing
would have put both copies inside the TV library. It does not delete anything:
which copy dies is Frank's call, and it is now a decision with a number on it.
Two naming defects the live run exposed, both test-first:
- a show whose own name carries the year got it twice (`X (2016) (2016)`) — a
box set stamps the year into every file name, so the folder year is dropped
when the head already ends in one;
- a series name that arrived lower-cased (`monk`) would have been filed among
properly-cased shows — a head with no capital at all is title-cased, a name
the publisher wrote with case is left exactly as written.
## 2. `scripts/media-apply.py` — a failed `rmdir` is not a removal
The `gone` set was built from the journal's *intent* records, so a folder whose
`rmdir` failed (read-only parent, non-empty child) was permanently blacklisted:
`removable()` returned False forever and **no later sweep could ever finish the
job**. The live movie sweep hit exactly this — 4,517 folders targeted, 146 left
standing, and those 146 could never be removed by any subsequent run.
`gone` now means *actually gone*: a path is out of scope only if it no longer
exists. A failed intent is retried by the next sweep, which is the whole point
of running the sweep twice — the second pass is where a `chmod` fix-up lands.
## 3. Tests / evidence
`tests/media_relayout.bats`: 9 new tests, each red against the previous
revision before the code existed — 8 TV (shape, film-not-series, sidecar,
idempotence, doubled year, casing, duplicate-episode refusal, second encode
still moved) + 1 sweep-retry (`rmdir` blocked by a `chmod 500` parent, then the
blocker fixed, then the second sweep finishes the job).
Full `./tests/run.sh` green on this commit; the Playwright gate is unaffected
(no fleet config path touched).
## 4. Not in this PR — nothing is applied
No live move has run. After merge, the application is two commands on
Dankserver with the `--root /mnt/entropy` guard and the TV journal, then the
movie sweep again (the 146 survivors are now removable), then both library
rescans. The Monk duplicate stays exactly where it is until Frank says which
copy dies.
---
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
Two fixes found by one live sweep, shipped together because the second is
what makes the first finishable.
1. scripts/media-tv.py — the other half of media-plan.py's refusal to file
TV. Groups episode-shaped files by the series name before the marker
(media-titles.ep_of, imported not restated), writes a plan in the
executor's own format: <library>/<Series> (<year>)/Season <NN>/<own
name>.<ext>. Refuses a group below --min, an episode naming no season,
and an episode the destination library already holds at the same byte
count — the movie share carries a second copy of all 124 Monk episodes
the TV library has had for years; a duplicate is Frank's call, never a
tool's copy. Applied by media-apply.py with its own journal and
--root /mnt/entropy.
2. scripts/media-apply.py — gone meant "rmdir intended", so a folder whose
rmdir failed was blacklisted forever and no later sweep could finish it
(live movie sweep: 4517 targeted, 146 permanently stranded). gone now
means "actually gone": a failed intent is retried by the next sweep.
tests: 9 new tests in media_relayout.bats, each red against the previous
revision — 8 TV + 1 sweep retry behind a chmod 500 parent. ./tests/run.sh
633 green. No live move run yet.
dank
merged commit 409a8d8b0c into main2026-10-30 20:31:38 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
1.
scripts/media-tv.py— the other half of the refusalscripts/media-plan.pyrefuses to file TV: it parks episode files in a_TV needs a TV library - <series>pile for a human. This is the mover. Itwalks the movie library, groups the files whose own name carries an episode
marker by series — the name before the marker, the same key PR #95 settled
for the planner, imported from
media-titles.pyrather than restated — andwrites a plan in the executor's own format that places each series in the shape
both Jellyfin and Plex read without help:
Plan only, never a rename:
media-apply.py applyowns every move, with its ownjournal (
media-relayout-tv.jsonl) so a TV move is not welded to the movierelayout, and
--root /mnt/entropybecause the executor refuses any pathoutside
--root.What it refuses, printing the reason and leaving the file alone:
--min(5) episode-shaped files under one series name —The Godfather Part 2is a film, not a series;Season 01;whole plan before its first rename — only Frank decides the keeper).
Live dry run (read-only, nothing moved yet)
TVShows/Big Little Lies/Season 01/Plan: 44 moves, 8 folders.
The Monk finding is the reason this PR exists.
/mnt/entropy/Movies/Monk 2002 Seasons 1 to 8and/mnt/entropy/TVShows/Monk 2002 Seasons 1 to 8 Complete Mixed x264 [i_c]are the same 124 episodes: same episode set (S01–S08, nothing uniqueto either side), identical byte count for every one of them — ~31 GiB of
redundancy on a volume at 100 %. A mover that did not look where it was filing
would have put both copies inside the TV library. It does not delete anything:
which copy dies is Frank's call, and it is now a decision with a number on it.
Two naming defects the live run exposed, both test-first:
X (2016) (2016)) — abox set stamps the year into every file name, so the folder year is dropped
when the head already ends in one;
monk) would have been filed amongproperly-cased shows — a head with no capital at all is title-cased, a name
the publisher wrote with case is left exactly as written.
2.
scripts/media-apply.py— a failedrmdiris not a removalThe
goneset was built from the journal's intent records, so a folder whosermdirfailed (read-only parent, non-empty child) was permanently blacklisted:removable()returned False forever and no later sweep could ever finish thejob. The live movie sweep hit exactly this — 4,517 folders targeted, 146 left
standing, and those 146 could never be removed by any subsequent run.
gonenow means actually gone: a path is out of scope only if it no longerexists. A failed intent is retried by the next sweep, which is the whole point
of running the sweep twice — the second pass is where a
chmodfix-up lands.3. Tests / evidence
tests/media_relayout.bats: 9 new tests, each red against the previousrevision before the code existed — 8 TV (shape, film-not-series, sidecar,
idempotence, doubled year, casing, duplicate-episode refusal, second encode
still moved) + 1 sweep-retry (
rmdirblocked by achmod 500parent, then theblocker fixed, then the second sweep finishes the job).
Full
./tests/run.shgreen on this commit; the Playwright gate is unaffected(no fleet config path touched).
4. Not in this PR — nothing is applied
No live move has run. After merge, the application is two commands on
Dankserver with the
--root /mnt/entropyguard and the TV journal, then themovie sweep again (the 146 survivors are now removable), then both library
rescans. The Monk duplicate stays exactly where it is until Frank says which
copy dies.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb