Skip to content

Scan a large library a day at a time, and choose which episodes to write - #1

Merged
Pasithea0 merged 3 commits into
mainfrom
feat/large-library-scanning-and-episode-selection
Sep 23, 2026
Merged

Pasithea0 merged 3 commits into
mainfrom
feat/large-library-scanning-and-episode-selection

Conversation

@Pasithea0

Copy link
Copy Markdown
Member

Two problems with a companion app run against a real library: the cron re-asks
about items it already knows, so a large library never converges, and the
preview screen shows what would change but gives you no way to decide.

What was wrong

The ledger cached answers with a TTL — 14 days for a 404, 30 for a 200 — and the
TTL was what decided when a request was made. On a library of tens of thousands
of items against a daily allowance of 500 (or 1000 with a key) that means the
library is re-covered forever and never finishes: today's 500 requests go on
items whose entries happened to expire, not on the items that have never been
asked about.

What this does

Scan records. A new scans table records that an item has been looked up, and
what came back. It never expires. A lookup that finds one is answered from the
ledger with no request, however long ago it was scanned; the cache TTLs still
expire the stored body, but they no longer decide when a request happens. The
404 cache is kept — it is the right thing for a 404, which becomes a 200 the
moment someone submits.

Progressive daily scanning. When the day's allowance is spent, the client
refuses the next request and a plan treats that as a stopping point: the run
ends, logs how many items are still to scan, and is recorded as paused rather
than as an error. The next run continues where the last one stopped. A run is no
longer a failure for having run out of requests, so a nightly timer makes
progress every night instead of reporting an error every night. A 40,000-item
library at 1000 requests a day converges in about 40 days, writing the markers it
has data for each run.

Re-scanning is deliberate. Nothing is asked about twice on a timer. The only
thing that re-asks is LookupForced, reached by naming the item:
--rescan <lookup-key>, --rescan-all, or a plan that carries the key.

Episode selection on the preview screen. Up/down move a cursor, space turns
the item under it on or off, A/N select all or none, R marks one for a
re-scan (planning again is what asks). An item that is off stays visible in the
plan and is left out of the write. The plan carries a Selection (unselected
rating keys, and the keys named for a re-scan) so it travels with the plan file
and a container applying a host-made plan writes the same subset.

New flags on preview, apply and sync/schedule: --rescan,
--rescan-all, --select, --deselect. status reports how many items have
been scanned and how they split, which is the progress figure for a library this
size.

Safety

Nothing about the write path changed: still --yes, still a preflight that
refuses while Plex is running unannounced or something is streaming, still a
backup and an undo journal. The ledger schema goes to version 2; opening an older
ledger adds the table and advances the recorded version, and every migration
statement is idempotent.

Tests

go build ./..., go vet ./..., gofumpt -l . clean; go test -count=1 ./...
green. New coverage: ledger scan-record lifecycle and the older-ledger upgrade,
the permanent-skip and forced re-scan behaviour at the client, the plan
selection and the sync-side filter, and the preview screen's cursor, selection,
re-scan marking and rendering. The three tests that asserted the old TTL
re-fetch behaviour now assert what replaced it.

… item

A library of tens of thousands of items cannot be looked up against a daily
allowance of 500 or 1000 requests, and the lookup cache was not helping: a
14-day miss TTL and a 30-day hit TTL mean every item comes back round for
another request eventually, so a large library never converges. Reported as
"the cache is good for 404s, but doesn't help with large libraries".

Separate the two questions the ledger was answering with one piece of state:

- whether a cached body is still trustworthy, which the lookup cache with its
  TTLs answers, and
- whether the item has been asked about at all, which a new `scans` table
  answers and which never expires.

A lookup that finds a scan record is answered from the ledger with no request,
however long ago it was scanned. The only thing that asks about a scanned item
again is `LookupForced`, reached by naming the item for a re-scan: --rescan,
--rescan-all, or a plan that carries the key in its selection.

When the day's allowance is spent the client refuses the next request, and a
plan now treats that as a stopping point rather than a failure: the run ends,
records how many items are still to scan, and is recorded as `paused` rather
than as an error, so the nightly timer makes progress every night instead of
failing every night. The next run continues where the last one stopped, which
is what a 40,000-item library at 1000 requests a day needs.

A plan also carries a `Selection` now: which rating keys were turned off, and
which lookup keys were named for a re-scan. It travels with the plan file, so a
plan made on a host and applied in a container writes the same subset, and the
writer honours the interface's selection instead of ignoring it.

The schema version advances to 2; opening an older ledger adds the table and
records the version, since every migration statement is idempotent.
The preview screen listed what a run would change but gave no way to act on it:
the arrow keys scrolled, and the whole plan was written whatever was on screen.

Give it a cursor. Up and down move over the items, space turns the one under the
cursor on or off, A selects everything and N none, and an item that is off stays
in the preview and is left out of the write, so nothing is hidden. R marks the
item under the cursor for a re-scan; planning again is what asks TheIntroDB about
it, which is how an item that was scanned months ago is refreshed without
anything re-scanning on a timer.

The selection is applied when the plan is written and is counted in the
confirmation, so "Write N markers across M items" is what will actually happen
rather than what the plan found.
Add a "Large libraries" section to the README and docs/scheduling.md, and a
"Scanning a large library" section to docs/architecture.md, covering the thing
that was missing: an item is scanned once, a run uses the whole day's allowance
on items it has never seen, a run that stops early is not a failure, and asking
about something again is deliberate (--rescan, --rescan-all, or R on the preview
screen).

The TTL knobs stay, described honestly: they expire a cached body, and no longer
decide when a request is made.
@Pasithea0
Pasithea0 merged commit 03d972a into main Sep 23, 2026
4 checks passed
@Pasithea0
Pasithea0 deleted the feat/large-library-scanning-and-episode-selection branch September 23, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant