#!/bin/bash
# update-yt-dlp-nightly.sh
#
# Installs the latest yt-dlp NIGHTLY build as a standalone binary at
# /usr/local/bin/yt-dlp. Run it however and whenever YOU decide — by
# hand, from a systemd timer, from cron, from your BBS software's own
# event scheduler (e.g. Synchronet's Timed Events). This script itself
# does none of that scheduling; it just does the one update-check-and-
# swap when invoked. Must run as root (writes to /usr/local/bin). See
# "Scheduling this yourself" below for copy-paste examples.
#
# Why nightly, and why worth scheduling at all: yt-dlp's stable releases
# lag behind YouTube's frequent extractor/player-client changes by design
# (stable is for general-purpose reliability across every site yt-dlp
# supports, not fast-tracking one site's breakage). In practice this means
# YouTube can break in a way stable doesn't fix for weeks, while nightly
# usually has it fixed within a day or two — see README's "Fixing
# intermittent HTTP 403 errors" section for the specific incident that
# prompted this script: yt-dlp stable@2026.07.04 hit a 100%-failure-rate
# YouTube extractor break; nightly@2026.08.17 had already fixed it.
# Running this on a schedule means the door catches up on its own instead
# of a sysop needing to notice the door is broken, diagnose it, and fix it
# under time pressure — but when and how that schedule runs is yours to
# decide, not baked into the install script.
#
# Why a standalone binary, not `pip install`: this door's own findTool
# (main.go) checks `~/.local/bin/yt-dlp` first, then `/usr/local/bin/
# yt-dlp`, falling back to a bare `yt-dlp` on $PATH. Whichever account
# actually launches the door (commonly a dedicated BBS service account,
# not whichever account a sysop happens to be SSHed in as) determines
# which one of those resolves. Confirmed the hard way: `pip install
# --user` as one account only updates that account's own
# ~/.local/lib/pythonX.Y/site-packages — invisible to any other account,
# including the one actually running the door — so an update can
# "succeed" on the command line while the live door keeps running the
# old, broken version. A standalone binary at /usr/local/bin/yt-dlp has
# no per-account ambiguity: whoever runs it gets the same file. It also
# sidesteps a real compatibility problem: older system `pip3` (e.g. stock
# Ubuntu 22.04, ~22.0.2) can fail outright trying to build yt-dlp's
# modern pyproject.toml-based package from source ("metadata-generation-
# failed") — no such problem with a prebuilt executable.
#
# Trade-off, stated plainly: nightly is explicitly NOT the vetted-stable
# channel — an occasional nightly build can introduce its own regression.
# Running this on a schedule accepts that small, usually-short-lived risk
# in exchange for not being stuck broken against YouTube for weeks at a
# time. If a nightly update ever makes things worse, the fix is the same
# either way: re-run install-bbs-deps.sh's normal yt-dlp install step (pip
# install --upgrade yt-dlp, the PyPI stable release) to go back to
# stable, then investigate.
#
# ---- Scheduling this yourself ----
#
# Manual (also how to test it once before scheduling anything):
#   sudo /usr/local/bin/update-yt-dlp-nightly.sh    # or wherever you put it
#
# cron (as root, e.g. daily at 03:17 — offset from :00 on purpose, so you
# aren't hammering GitHub's API at the same instant as every other cron
# job on earth):
#   17 3 * * *  /path/to/update-yt-dlp-nightly.sh >> /var/log/update-yt-dlp-nightly.log 2>&1
#
# systemd timer — create these two files yourself if you use systemd
# (adjust the path in ExecStart to wherever you placed this script):
#
#   /etc/systemd/system/update-yt-dlp-nightly.service
#     [Unit]
#     Description=Update yt-dlp to the latest nightly build
#     [Service]
#     Type=oneshot
#     ExecStart=/usr/local/bin/update-yt-dlp-nightly.sh
#
#   /etc/systemd/system/update-yt-dlp-nightly.timer
#     [Unit]
#     Description=Daily yt-dlp nightly update check
#     [Timer]
#     OnCalendar=daily
#     RandomizedDelaySec=1h
#     Persistent=true
#     [Install]
#     WantedBy=timers.target
#
#   Then:  systemctl daemon-reload && systemctl enable --now update-yt-dlp-nightly.timer
#
# Your BBS software's own event/task scheduler: this is just a normal
# shell script with a normal exit code (0 = success or "already current",
# 1 = failed, left yt-dlp untouched either way) and one log line per run
# on stdout — point whatever "run this external command on a schedule"
# feature your BBS package has at it (e.g. Synchronet's Timed Events)
# exactly like you would any other maintenance script.

set -u
LOG_PREFIX="update-yt-dlp-nightly:"
TARGET=/usr/local/bin/yt-dlp

log() { printf '%s %s\n' "$LOG_PREFIX" "$*"; }

if [ "$EUID" -ne 0 ]; then
    log "ERROR: must run as root (writes to $TARGET)"
    exit 1
fi

CURRENT_VERSION=$("$TARGET" --version 2>/dev/null || echo "unknown")
log "current version: $CURRENT_VERSION"

API_URL="https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest"
RELEASE_JSON=$(curl -fsSL "$API_URL") || {
    log "ERROR: failed to reach $API_URL — leaving yt-dlp untouched"
    exit 1
}

TAG=$(printf '%s' "$RELEASE_JSON" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
if [ -z "$TAG" ]; then
    log "ERROR: couldn't parse a release tag from the GitHub API response — leaving yt-dlp untouched"
    exit 1
fi

if [ "$CURRENT_VERSION" = "$TAG" ]; then
    # This script runs as root, and root can read/execute a file
    # regardless of its permission bits — so the version check above can
    # succeed even when $TARGET is NOT readable by whatever non-root
    # account actually runs the door. Confirmed live: a door failed with
    # "[PYI-xxxxx:ERROR] Could not load PyInstaller's embedded PKG
    # archive" against a target this script had reported as already
    # current on every prior run, because this early-exit path only ever
    # checked the version, never the permissions — chmod 755 only ran on
    # the download-and-replace path below, which a version match always
    # skips. Check and correct it here too, so a previously-broken
    # install (or one hand-installed with a bare `chmod +x` under a
    # restrictive umask) gets fixed on the very next scheduled run
    # instead of silently staying broken forever.
    if [ -e "$TARGET" ] && [ "$(stat -c '%a' "$TARGET" 2>/dev/null)" != "755" ]; then
        log "already on latest nightly ($TAG), but permissions were wrong — fixing"
        chmod 755 "$TARGET"
    else
        log "already on latest nightly ($TAG) — nothing to do"
    fi
    exit 0
fi

log "installing nightly $TAG (was: $CURRENT_VERSION)"

BINARY_URL="https://github.com/yt-dlp/yt-dlp-nightly-builds/releases/download/${TAG}/yt-dlp_linux"
# Temp file must be on the same filesystem as $TARGET for the final `mv`
# to be an atomic rename(2) rather than a copy+delete — mktemp defaults
# to /tmp, which is commonly a separate (e.g. tmpfs) mount from
# /usr/local/bin, so build it right next to the real target instead.
TMPFILE=$(mktemp "${TARGET}.XXXXXX")
trap 'rm -f "$TMPFILE"' EXIT

if ! curl -fsSL -o "$TMPFILE" "$BINARY_URL"; then
    log "ERROR: failed to download $BINARY_URL — leaving yt-dlp untouched"
    exit 1
fi

# 755, not a bare "+x": curl creates $TMPFILE under whatever umask this
# script is running with, which for a root/sudo/cron context is commonly
# restrictive enough to leave it unreadable by anyone but root (observed
# live: 600 from curl, then "+x" alone only ever ADDS bits on top of
# that, producing rwx--x--x — executable but NOT READABLE by the door's
# own (non-root) user). A door process running as a regular user could
# still exec() that fine (execute doesn't require read), but yt-dlp is a
# PyInstaller onefile binary that reads its OWN file at startup to find
# its embedded archive — no read permission there fails with
# "[PYI-xxxxx:ERROR] Could not load PyInstaller's embedded PKG archive",
# even though the file itself is perfectly intact. 755 matches normal
# /usr/local/bin conventions: root can rewrite it, everyone can read+run it.
chmod 755 "$TMPFILE"
NEW_VERSION=$("$TMPFILE" --version 2>/dev/null || echo "")
if [ "$NEW_VERSION" != "$TAG" ]; then
    log "ERROR: downloaded binary reports '$NEW_VERSION', expected '$TAG' — not installing, leaving yt-dlp at $CURRENT_VERSION"
    exit 1
fi

# Verified good — now swap it in. mv (same filesystem) is atomic, so a
# door mid-launch never sees a partially-written binary. mv preserves
# $TMPFILE's mode bits, but set them explicitly again anyway rather than
# assume that holds on every filesystem.
mv "$TMPFILE" "$TARGET"
chmod 755 "$TARGET"
log "success: yt-dlp is now $("$TARGET" --version)"
