Tom FluxandClaude Opus 5 339232c7f9 Phase 0: rolling 30-day window, delete on unsubscribe, shared password
Phase 0 is done apart from one external dependency, and what it measured
changed the shape of the plan more than the three answers it was meant to
confirm.

Measured the actual upload rates from the live UULF feeds rather than guessing
from catalogue size, and the numbers make the whole scale section boring. Pitch
Side publishes 0.60 long-form videos a day, The Pyramid Podcast 0.77 — so a
30-day window is about 20 episodes per channel and roughly 400 across a
20-channel library, not the 20,000 the previous draft braced for. The UULF feed
is also doing more work than expected: it excludes 60-74% of what these channels
publish, all of it Shorts and livestreams nobody wants as Jellyfin episodes.

A side effect worth recording: Pitch Side's 15-entry UULF feed spans 23.3 days,
because the feed holds 15 long-form items rather than 15 uploads. Free RSS
therefore covers most of a 30-day retention window on its own, which narrows
what the API is for. It still earns its place — RSS carries no duration, and
subscription reading has no alternative at all — but the honest version is that
it now buys correctness far more than it buys safety, and the plan says so
instead of keeping the more flattering 4-hours-of-yt-dlp argument.

Three decisions folded in.

Retention is a rolling 30 days, superseding the earlier "3 months or 300
videos". Those answered different questions — backfill depth versus retention —
and holding both would mean backfilling 90 days and deleting two thirds of it on
the next sweep. One number now governs both ends, so the library cannot grow.
This resurrects reap.py, which the previous draft deleted, and makes tombstones
load-bearing in two ways: without them the next poll re-materialises everything
the sweep just deleted, and episode ordinals for a given day would shift as
videos disappear. aged_out is deliberately never revivable, or raising
retention_days would resurrect months of episodes into Jellyfin as new.

Unsubscribing now deletes the channel rather than deactivating it, which is
reasonable when rebuilding costs one API page and ~20 files. The consequence is
that the removal-detection rules stop being precautionary and become the only
thing standing between a transient 403 and a wiped library, so the plan now says
that explicitly next to them.

Auth reverts to the single shared admin_password_hash, matching every other
service on susan. web/auth.py carries over untouched. What that gives up —
independent revocation, and knowing who approved what — is named once and then
dropped.

Added tools/verify_api.py so the remaining Phase 0 work is one command. It runs
all three API checks, paginates the subscription list, prints the
subsync_max_new value derived from totalResults, distinguishes 403
subscriptionForbidden from every other failure with the fix in the message, and
exits non-zero so it can be gated on. The ISO-8601 duration parser is
unit-checked.

Blocked on: there is no Google API key anywhere on this machine (searched
/opt/*, /home/susan, the settings table and the config trees for AIza-shaped
strings). Creating one needs a browser and a Google account. That plus the
brother unchecking "Keep all my subscriptions private" are the two external
prerequisites, and §16 records exactly what each unverified assumption costs if
it turns out false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:37:53 +01:00
S
Description
Streams YouTube subscriptions into Jellyfin — resolve proxy, channel discovery and NFO metadata, with a 345-test suite.
234 KiB
Languages
Python 96.8%
Shell 3.2%