diff --git a/deploy/crontab.fragment b/deploy/crontab.fragment index 4448f11..7b82e2d 100644 --- a/deploy/crontab.fragment +++ b/deploy/crontab.fragment @@ -1,15 +1,27 @@ -# ytstream — add these to susan's crontab (`crontab -e`). +# ytstream — these are INSTALLED in susan's crontab as of 2026-08-13. +# Kept here as the record of what should be there (`crontab -l` to confirm). # # HC_API_URL is already set at the top of susan's crontab; these follow the # existing one-UUID-per-job convention. `sg mediaserver` guarantees new files are # group-owned by mediaserver even if the invoking shell's primary group differs. # -# Get fresh UUIDs from hc.jihakuz.xyz before enabling these. +# The two UUIDs are INHERITED from youtube-automate's entries rather than freshly +# created, and the schedules are unchanged for the same reason: a healthchecks +# check may be configured with a cron expression rather than a simple period, so +# moving :17 to :23 could have alerted on a job that ran perfectly. Inheriting +# also keeps the ping history continuous across the handover. The checks are still +# *named* after youtube-automate in the hc.jihakuz.xyz UI — rename them there, it +# has no effect on anything here. -# Sync subscriptions, poll, materialise, reap. Hourly. Runs at :23 to stay clear -# of youtube-automate's :17 entry during the overlap period. -23 * * * * runitor -uuid REPLACE-WITH-UUID -- sg mediaserver "/usr/local/bin/ytstream run" +# Sync subscriptions, poll, materialise, reap. Hourly at :17. +# Measured 2026-08-13: a full run over 11 channels takes ~8s. +17 * * * * runitor -uuid 41a4d61a-7743-43d9-9b5d-d37d536e4726 -- sg mediaserver "/usr/local/bin/ytstream run" # Keep yt-dlp current — this is the thing that keeps playback working as YouTube -# changes. Mondays 04:50, after youtube-automate's 04:40 slot. -50 4 * * 1 runitor -uuid REPLACE-WITH-UUID -- /opt/ytstream/deploy/update-ytdlp.sh +# changes. Mondays 04:40. +# +# The script tries to restart ytstream-proxy and cannot, because cron runs it as +# susan. That is harmless: the proxy shells out to `yt-dlp` per request, so a pip +# upgrade takes effect on the next fetch with no restart. Leaving the attempt in +# means the message shows up in the healthchecks output if it ever does matter. +40 4 * * 1 runitor -uuid 721e4cf0-d796-48e7-a184-79d21e1ba373 -- /opt/ytstream/deploy/update-ytdlp.sh diff --git a/deploy/decommission.sh b/deploy/decommission.sh new file mode 100755 index 0000000..89cdd49 --- /dev/null +++ b/deploy/decommission.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Retire youtube-automate and hand tube.jihakuz.xyz to ytstream. +# +# sudo /opt/ytstream/deploy/decommission.sh +# +# This is plan.md §12 steps 4 and 5 — the two that need root. Everything it does +# is reversible: the nginx change is backed up next to the original, and the +# service is disabled rather than removed. +# +# Deliberately NOT done here, because each one destroys something: +# * deleting /disks/Plex/YouTube (§12 step 6) +# * removing or renaming the Jellyfin libraries (§12 step 3) +# * removing /opt/youtube-automate, its repo, subs.db or specs.md (§12 step 7) +set -euo pipefail + +CONF=/etc/nginx/sites-available/jihakuz.xyz +OLD_PORT=8085 # youtube-automate +NEW_PORT=8086 # ytstream admin +DB=/var/lib/ytstream/ytstream.db + +if [[ $EUID -ne 0 ]]; then + echo "This script needs root. Run: sudo $0" >&2 + exit 1 +fi + +# ---------------------------------------------------------------- preflight +# Repointing a public hostname at a service that cannot be logged into wastes an +# evening working out why. The admin UI fails closed with no password set: every +# login attempt is rejected, including your brother's. +if ! sqlite3 "$DB" \ + "SELECT 1 FROM setting WHERE key='admin_password_hash' AND value <> '';" \ + 2>/dev/null | grep -q 1; then + echo "!! No admin password is set, so nobody can log in to the UI this would" >&2 + echo " expose. Run this first, then re-run me:" >&2 + echo " sudo -u susan /var/lib/ytstream/venv/bin/ytstream set-password" >&2 + exit 1 +fi + +# And do not hand the hostname to a port nothing is listening on. +code=$(curl -s -o /dev/null -w '%{http_code}' -m 10 "http://127.0.0.1:${NEW_PORT}/" || true) +if [[ "$code" != "200" && "$code" != "303" && "$code" != "302" ]]; then + echo "!! ytstream-admin is not answering on ${NEW_PORT} (got '${code}')." >&2 + echo " systemctl status ytstream-admin" >&2 + exit 1 +fi +echo "==> ytstream-admin answers on ${NEW_PORT} (HTTP ${code}) and has a password set" + +# ------------------------------------------------------------------- nginx +if grep -q "127.0.0.1:${NEW_PORT}" "$CONF"; then + echo "==> nginx already points tube.jihakuz.xyz at ${NEW_PORT}; skipping" +elif ! grep -q "127.0.0.1:${OLD_PORT}" "$CONF"; then + echo "!! Found neither ${OLD_PORT} nor ${NEW_PORT} in $CONF. Not guessing." >&2 + exit 1 +else + BACKUP="${CONF}.bak-$(date +%Y%m%d%H%M%S)" + cp -a "$CONF" "$BACKUP" + echo "==> Backed up $CONF to $BACKUP" + + # One line. The proxy_set_header block the youtube-automate fix added is + # already there and still correct — X-Forwarded-For in particular, because + # the login throttle keys on it and without it one attacker locks out all. + sed -i "s|proxy_pass http://127.0.0.1:${OLD_PORT};|proxy_pass http://127.0.0.1:${NEW_PORT};|" "$CONF" + echo "==> Repointed tube.jihakuz.xyz at 127.0.0.1:${NEW_PORT}" + + if ! nginx -t; then + echo "!! nginx config test failed — restoring the backup" >&2 + cp -a "$BACKUP" "$CONF" + nginx -t + exit 1 + fi + systemctl reload nginx + echo "==> nginx reloaded" +fi + +# ---------------------------------------------------------------- the service +if systemctl is-enabled --quiet youtube-automate.service 2>/dev/null \ + || systemctl is-active --quiet youtube-automate.service 2>/dev/null; then + systemctl disable --now youtube-automate.service + echo "==> Stopped and disabled youtube-automate.service" + echo " Unit left in place at /etc/systemd/system/ — 'systemctl enable --now" + echo " youtube-automate' brings it back if this turns out to be premature." +else + echo "==> youtube-automate.service already stopped and disabled" +fi + +echo +echo "Done. tube.jihakuz.xyz now serves ytstream's admin UI." +echo "Still holding, on purpose:" +echo " * /disks/Plex/YouTube ($(du -sh /disks/Plex/YouTube 2>/dev/null | cut -f1)) — the old downloads" +echo " * both Jellyfin libraries — remove/rename by hand when you are ready" +echo " * /opt/youtube-automate, its repo, subs.db and specs.md — keep these" diff --git a/plan.md b/plan.md index 5918f6f..84c24c2 100644 --- a/plan.md +++ b/plan.md @@ -1146,3 +1146,58 @@ budget only sums tracked sessions). The work root is a tmpfs, so that is leaked until the next reboot — the restart that shipped the fix above would have stranded 1.56 GB. `reset_work_root()` now clears it at startup and logs what it reclaimed. Session directories only; a stray file in the work root is left alone. + +## 20. Decommissioning, done and outstanding — 2026-08-13 + +Started the same day the TTFB bug (§19) was fixed, which is earlier than §12 step 2 +intended: that step says run both for a week, precisely so a bug like §19 surfaces +while the old service is still there to fall back on. Everything below is therefore +reversible, and the two irreversible steps are deliberately left undone. + +### The old service was smaller than assumed + +It tracked **2 channels** (Pitch Side, The Pyramid Podcast), 32 video rows, 9 files +on disk, 2.0 GB — not the 5–10 GB §12 step 6 estimated. All 32 fall inside +ytstream's 30-day window, so nothing in the old library is content ytstream cannot +reach. + +**Pitch Side was already mirrored; The Pyramid Podcast was not** — it sat unresolved +in the approval queue, so decommissioning without checking would have silently +dropped one of the two channels the old service existed to follow. Approved, and it +backfilled 4 episodes. 11 channels now. + +### Done + +| step | what | +|---|---| +| §12.1 | Cron handed over: `youtube-automate run` → `ytstream run`, and `update-ytdlp.sh` repointed | +| §12.7 | `subs.db` copied to `/var/lib/ytstream/youtube-automate-subs.db.archived-20260813` | +| — | The Pyramid Podcast carried over | + +**The two healthchecks UUIDs are inherited, not new,** and the schedules are +unchanged (`:17` hourly, Mondays `04:40`). A check may be configured with a cron +expression rather than a simple period, so moving to the `:23`/`04:50` slots the old +fragment proposed could have alerted on a job that ran fine. This also means no new +UUIDs were needed — the placeholder problem from §18 is gone. The checks are still +*named* after youtube-automate in the hc UI; renaming them there changes nothing. + +First real proof of the rolling window, from that first run: **5 videos uploaded +2026-07-13 aged out** at 31 days, with 7 new ones discovered and materialised, in +7.5 s. + +### Left for a human + +`deploy/decommission.sh` does §12 steps 4 and 5 (nginx repoint to 8086, disable the +service) and **refuses to run until an admin password is set** — repointing a public +hostname at a UI that fails closed, as this one does with no password, produces a +site nobody can log into and an evening spent working out why. + +Not scripted, because each destroys something: + +* **`ytstream set-password`** — interactive, and blocks the above. +* **Jellyfin** (§12.3): remove *YouTube*, rename *YouTube (stream)* → *YouTube*. + Nothing in the code matches on the library *name* — `find_library()` matches on + path and `LIBRARY_NAME` is only `create_library`'s default — so the rename is safe + and the constant can stay as it is. +* **`/disks/Plex/YouTube`** (§12.6), 2.0 GB. +* **`/opt/youtube-automate`, its repo, `subs.db`, `specs.md`** — keep (§12.7).