feat(media): a TV mover that will not file a show the library already has
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.
This commit is contained in:
1 parent
5cd9a0c4c2
commit
e83162a2ac
4 files changed
+456
-6
No files matched your search
@@ -11,15 +11,16 @@ timestamp: 2026-10-30T00:00:00Z
|
||||
The movie library on [[Entropy Volume]] arrived as release folders —
|
||||
`Some.Movie.2019.1080p.WiKiTaLx.x265-FOO/` with the video inside named after
|
||||
the release. [[Jellyfin]] matches a movie by folder and file name, so the
|
||||
release noise cost posters, extras and versions. These three scripts turn that
|
||||
library into Jellyfin's own shape, and nothing else in the repo moves a media
|
||||
file.
|
||||
release noise cost posters, extras and versions. These scripts turn that
|
||||
library into Jellyfin's own shape — and move TV back out of it — and nothing
|
||||
else in the repo touches a media file.
|
||||
|
||||
| Script | Touches media? | Output |
|
||||
|---|---|---|
|
||||
| `scripts/media-census.py` | reads headers only | `enum`, `probed`, `plex` TSVs |
|
||||
| `scripts/media-plan.py` | no — TSVs in, TSVs out | the plan + the exception report |
|
||||
| `scripts/media-apply.py` | **renames, never copies**; sweeps only what a run emptied | the moves, plus a journal |
|
||||
| `scripts/media-tv.py` | no — TSV out | a plan that moves TV into the TV library |
|
||||
|
||||
The seam is `tests/media_relayout.bats`; the target contract is quoted from
|
||||
[jellyfin.org/docs/general/server/media/movies](https://jellyfin.org/docs/general/server/media/movies)
|
||||
@@ -68,8 +69,8 @@ scripts/media-apply.py rollback --journal /mnt/entropy/media-relayout.jsonl --ro
|
||||
```
|
||||
|
||||
An apply also leaves the release folders it emptied standing where they were —
|
||||
the live run left 1675 of them at the library root — which is what `sweep`
|
||||
removes, dry-run first, from the same journal.
|
||||
hundreds of them at the library root — which is what `sweep` removes, dry-run
|
||||
first, from the same journal.
|
||||
|
||||
After it lands, both servers need a scan or the change is invisible.
|
||||
|
||||
@@ -103,6 +104,42 @@ curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n" \
|
||||
-X POST http://127.0.0.1:8096/Library/Refresh
|
||||
```
|
||||
|
||||
## TV in the movie library
|
||||
|
||||
A box set that arrives in the movie share stays there until someone moves it:
|
||||
`media-plan.py` refuses to file TV and parks each series in
|
||||
`_TV needs a TV library - <series>`. `scripts/media-tv.py` is the mover. It
|
||||
walks the movie library itself (no census needed), groups episode-shaped names
|
||||
by series, and writes a plan in the executor's own format:
|
||||
|
||||
```bash
|
||||
scripts/media-tv.py --root /mnt/entropy/Movies --library /mnt/entropy/TVShows \
|
||||
--out /mnt/entropy/media-tv-plan.tsv
|
||||
scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv --root /mnt/entropy
|
||||
scripts/media-apply.py apply --plan /mnt/entropy/media-tv-plan.tsv --root /mnt/entropy \
|
||||
--journal /mnt/entropy/media-relayout-tv.jsonl --apply
|
||||
scripts/media-apply.py sweep --journal /mnt/entropy/media-relayout-tv.jsonl \
|
||||
--root /mnt/entropy --apply
|
||||
```
|
||||
|
||||
The shape it writes is the one both matchers read — `Monk (2002)/Season 05/
|
||||
Monk - S05E03 - Mr. Monk and the Big Game.mkv` — keeping the file's own name,
|
||||
because the publisher's spelling beats a re-derivation, with the year taken
|
||||
from the box-set folder. What it refuses it prints and leaves alone:
|
||||
|
||||
- fewer than `--min` (5) files under one series name: one file named `Part 2`
|
||||
is a film, not a series;
|
||||
- an episode whose name states no season number — no invented `Season 01`;
|
||||
- a destination two files both claim: the executor refuses the whole plan
|
||||
before its first rename, and only Frank decides which copy is the keeper.
|
||||
- an episode the destination library already holds at the same byte count. The
|
||||
movie share carried a second copy of all 124 Monk episodes — the TV library
|
||||
has had the same box set for years, every episode the same size, ~31 GiB of
|
||||
it. The mover leaves such a series in place and says so; a duplicate is
|
||||
deleted or kept by Frank, never copied into one library by a tool.
|
||||
|
||||
A sidecar that shares an episode's own name (`.srt`, `.nfo`) moves with it.
|
||||
|
||||
## Laws
|
||||
|
||||
- **A moved file is a 404 in [[Plex]] until its section is rescanned.** The
|
||||
@@ -159,6 +196,8 @@ curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n" \
|
||||
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.
|
||||
A failed `rmdir` is not a removal: only a folder that is really gone leaves
|
||||
scope, so the next sweep finishes what a permission problem stopped.
|
||||
- **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
|
||||
@@ -167,6 +206,10 @@ curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n" \
|
||||
keep: a lone film named `Part 2` stays a film. Episodes of a series are parked
|
||||
in `_TV needs a TV library - <series>` for a human to move to a TV source —
|
||||
the movie tool never files TV.
|
||||
- **One journal is one undo.** The TV move runs on its own journal, and with
|
||||
`--root` set to the share that holds BOTH libraries: the executor refuses any
|
||||
path outside `--root`, and a journal shared with the movie relayout would
|
||||
undo both runs when only one of them is wanted.
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
Reference in new issue
Block a user