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
+33
View File
@@ -0,0 +1,33 @@
"""Filesystem paths and process-level constants.
Every path is overridable through the environment so the test suite can point the
whole application at a tmpdir without touching the real media tree.
"""
from __future__ import annotations
import os
from pathlib import Path
def _path(env: str, default: str) -> Path:
return Path(os.environ.get(env, default))
STATE_DIR = _path("YTA_STATE_DIR", "/var/lib/youtube-automate")
MEDIA_ROOT = _path("YTA_MEDIA_ROOT", "/disks/Plex/YouTube")
DB_PATH = _path("YTA_DB_PATH", str(STATE_DIR / "subs.db"))
LOCK_PATH = _path("YTA_LOCK_PATH", str(STATE_DIR / "run.lock"))
VENV_BIN = _path("YTA_VENV_BIN", str(STATE_DIR / "venv" / "bin"))
WORK_DIR = MEDIA_ROOT / ".work"
# Files created by the service must stay group-readable by `mediaserver`, which is
# how Jellyfin reaches the tree. See specs.md §2.
UMASK = 0o002
# Media-adjacent sidecars we own and therefore may delete on reap.
SIDECAR_SUFFIXES = (".nfo", "-thumb.jpg", ".en.srt", ".info.json")
USER_AGENT = "youtube-automate/1.0"