Initial implementation of youtube-automate

A DVR for YouTube subscriptions, laid out so Jellyfin presents each channel
as a show and each video as an episode. Cron-driven, idempotent, with a
public admin UI for a non-operator.

Verified end to end on susan against three real channels: PO tokens, h264
downloads, Jellyfin resolution from local NFOs with all providers disabled,
retention and tombstones.

Corrections to the original design handover (specs.md documents each with
the evidence, and specs.handover-original.md preserves the original):

- The format sort selected 360p. Ranking acodec above res makes `bv*` prefer
  the combined 360p stream, which carries AAC, over the 720p video-only
  stream whose acodec is none. vcodec now leads, so a video without h264 at
  720p yields h264 lower down rather than VP9 this hardware cannot transcode.
- yt-dlp now requires a JS runtime and the yt-dlp-ejs solver scripts, which
  only ship with the [default] extra. Without them the n challenge fails and
  the mweb formats disappear entirely.
- --flat-playlist carries no upload dates, so the specced client-side date
  filter for backfill was impossible. Backfill is RSS-first.
- skipped_old was terminal, so raising a channel's retention appeared to do
  nothing. Added an explicit rescan.
- is_upcoming premieres now defer and retry instead of being skipped forever.
- TubeArchivist is gone, so the media root and the tube.jihakuz.xyz vhost
  were both reclaimed; the latter still pointed at its dead port.

240 offline tests, no network and no real yt-dlp invocation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Flux
2026-08-11 21:42:48 +01:00
co-authored by Claude Opus 5
commit 18bb2e420b
44 changed files with 7188 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Root-requiring installation steps for youtube-automate.
#
# susan has no passwordless sudo, so these are collected here for the operator
# to run in one go:
#
# sudo /opt/youtube-automate/deploy/deploy.sh
#
# Everything that does NOT need root (the venv, the database, the POT provider
# container, subscriptions) is already handled by the application itself.
set -euo pipefail
REPO=/opt/youtube-automate
VENV=/var/lib/youtube-automate/venv
HOSTNAME_=tube.jihakuz.xyz
if [[ $EUID -ne 0 ]]; then
echo "This script needs root. Run: sudo $0" >&2
exit 1
fi
say() { printf '\n\033[1m==> %s\033[0m\n' "$1"; }
say "Installing the /usr/local/bin shim"
cat > /usr/local/bin/youtube-automate <<EOF
#!/bin/sh
# Thin shim onto the venv entry point.
exec $VENV/bin/youtube-automate "\$@"
EOF
chmod 0755 /usr/local/bin/youtube-automate
chown root:automation /usr/local/bin/youtube-automate
echo " /usr/local/bin/youtube-automate"
say "Making the weekly updater executable"
chmod 0755 "$REPO/deploy/update-ytdlp.sh"
say "Installing the systemd unit"
install -m 0644 "$REPO/deploy/youtube-automate.service" \
/etc/systemd/system/youtube-automate.service
systemctl daemon-reload
systemctl enable --now youtube-automate.service
systemctl --no-pager --lines=5 status youtube-automate.service || true
say "Installing the nginx vhost"
# tube.jihakuz.xyz may already be claimed by the leftover TubeArchivist server
# block in sites-available/jihakuz.xyz. nginx uses the FIRST server block
# matching a name, and that file loads first, so installing a second vhost for
# the same name silently does nothing (and yields 502 from the dead upstream).
if grep -rql --dereference-recursive "server_name $HOSTNAME_" /etc/nginx/sites-enabled/ \
2>/dev/null | grep -qv "$HOSTNAME_\$"; then
echo " $HOSTNAME_ is already served by an existing vhost."
echo " Skipping the standalone file — run deploy/fix-nginx-tube.sh instead."
else
install -m 0644 "$REPO/deploy/nginx-tube.jihakuz.xyz.conf" \
"/etc/nginx/sites-available/$HOSTNAME_"
ln -sfn "/etc/nginx/sites-available/$HOSTNAME_" \
"/etc/nginx/sites-enabled/$HOSTNAME_"
nginx -t
systemctl reload nginx
fi
cat <<EOF
==> Done. Remaining manual steps, in order:
1. Issue the certificate (needs port 80 reachable from the internet):
sudo certbot --nginx -d $HOSTNAME_
2. Set the admin password (prompts; never pass it as an argument):
youtube-automate set-password
3. Add the two cron entries as susan (NOT root):
crontab -e
# then paste $REPO/deploy/crontab.fragment
4. Confirm everything is healthy:
youtube-automate doctor
Note: the bgutil POT provider container starts itself on boot via
--restart unless-stopped, so it needs nothing here. If it is ever missing:
docker run --name bgutil-provider -d --restart unless-stopped --init \\
-p 127.0.0.1:4416:4416 brainicism/bgutil-ytdlp-pot-provider:1.3.1-deno
EOF