From e0684a1d1c0d6e9473ac175375a19c6d8fc4309e Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Sat, 27 Jun 2026 12:55:35 -0600 Subject: [PATCH] Add YouTube backup helper --- .gitignore | 4 + infra/youtube-backup/.env.example | 20 ++++ infra/youtube-backup/README.md | 65 +++++++++++++ infra/youtube-backup/backup-youtube.sh | 103 +++++++++++++++++++++ infra/youtube-backup/playlists.example.txt | 8 ++ 5 files changed, 200 insertions(+) create mode 100644 infra/youtube-backup/.env.example create mode 100644 infra/youtube-backup/README.md create mode 100755 infra/youtube-backup/backup-youtube.sh create mode 100644 infra/youtube-backup/playlists.example.txt diff --git a/.gitignore b/.gitignore index f2be90b..0abdf20 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ bootstrap/host/inventory/ *.tar.gz *.zip infra/gitea/data/ +infra/youtube-backup/.env +infra/youtube-backup/cookies.txt +infra/youtube-backup/archive.txt +infra/youtube-backup/logs/ # Ignore decrypted secret material *.dec.yaml diff --git a/infra/youtube-backup/.env.example b/infra/youtube-backup/.env.example new file mode 100644 index 0000000..602b489 --- /dev/null +++ b/infra/youtube-backup/.env.example @@ -0,0 +1,20 @@ +YOUTUBE_BACKUP_ROOT=/data/youtube-backup +YOUTUBE_BACKUP_PLAYLISTS_FILE=/data/youtube-backup/playlists.txt +YOUTUBE_BACKUP_ARCHIVE_FILE=/data/youtube-backup/archive.txt +YOUTUBE_BACKUP_VIDEOS_DIR=/data/youtube-backup/videos +YOUTUBE_BACKUP_LOG_DIR=/data/youtube-backup/logs + +# For private/unlisted uploads, prefer exported cookies on a headless server: +# YOUTUBE_BACKUP_COOKIES_FILE=/data/youtube-backup/cookies.txt +YOUTUBE_BACKUP_COOKIES_FILE= + +# Or use browser cookies when running interactively on a desktop: +# YOUTUBE_BACKUP_COOKIES_FROM_BROWSER=firefox +YOUTUBE_BACKUP_COOKIES_FROM_BROWSER= + +YOUTUBE_BACKUP_FORMAT=bv*+ba/best +YOUTUBE_BACKUP_MERGE_FORMAT=mp4 +YOUTUBE_BACKUP_SUB_LANGS=all,-live_chat +YOUTUBE_BACKUP_SLEEP_REQUESTS=1 +YOUTUBE_BACKUP_RATE_LIMIT= +YOUTUBE_BACKUP_EXTRA_ARGS= diff --git a/infra/youtube-backup/README.md b/infra/youtube-backup/README.md new file mode 100644 index 0000000..2ff20d9 --- /dev/null +++ b/infra/youtube-backup/README.md @@ -0,0 +1,65 @@ +# YouTube Backup + +Standalone helper for backing up your own uploaded YouTube videos with +`yt-dlp`. It is intentionally outside `lab.sh`, OpenTofu, Argo CD, and the +homelab deployment pipeline. + +The backup root defaults to `/data/youtube-backup`. + +## Install + +```bash +pipx install yt-dlp +``` + +If thumbnail embedding fails, install `ffmpeg` on the host. + +## Configure + +```bash +sudo mkdir -p /data/youtube-backup/{videos,logs} +sudo chown -R "$USER:$USER" /data/youtube-backup +cp infra/youtube-backup/playlists.example.txt /data/youtube-backup/playlists.txt +cp infra/youtube-backup/.env.example infra/youtube-backup/.env +``` + +Edit `/data/youtube-backup/playlists.txt` and add one playlist, channel, or +video URL per line. + +For private or unlisted uploads, export browser cookies and set: + +```bash +YOUTUBE_BACKUP_COOKIES_FILE=/data/youtube-backup/cookies.txt +``` + +Alternatively, when running on a desktop with a browser profile: + +```bash +YOUTUBE_BACKUP_COOKIES_FROM_BROWSER=firefox +``` + +## Run + +```bash +infra/youtube-backup/backup-youtube.sh +``` + +The script uses: + +```text +--download-archive /data/youtube-backup/archive.txt +``` + +so repeated runs skip videos that were already backed up. + +Downloaded videos and sidecar metadata are written under: + +```text +/data/youtube-backup/videos +``` + +Each run also writes a timestamped log under: + +```text +/data/youtube-backup/logs +``` diff --git a/infra/youtube-backup/backup-youtube.sh b/infra/youtube-backup/backup-youtube.sh new file mode 100755 index 0000000..15ac424 --- /dev/null +++ b/infra/youtube-backup/backup-youtube.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${YOUTUBE_BACKUP_ENV_FILE:-${SCRIPT_DIR}/.env}" + +if [[ -f "${ENV_FILE}" ]]; then + set -a + # shellcheck disable=SC1090 + . "${ENV_FILE}" + set +a +fi + +BACKUP_ROOT="${YOUTUBE_BACKUP_ROOT:-/data/youtube-backup}" +PLAYLISTS_FILE="${YOUTUBE_BACKUP_PLAYLISTS_FILE:-${BACKUP_ROOT}/playlists.txt}" +ARCHIVE_FILE="${YOUTUBE_BACKUP_ARCHIVE_FILE:-${BACKUP_ROOT}/archive.txt}" +VIDEOS_DIR="${YOUTUBE_BACKUP_VIDEOS_DIR:-${BACKUP_ROOT}/videos}" +LOG_DIR="${YOUTUBE_BACKUP_LOG_DIR:-${BACKUP_ROOT}/logs}" +COOKIES_FILE="${YOUTUBE_BACKUP_COOKIES_FILE:-}" +COOKIES_FROM_BROWSER="${YOUTUBE_BACKUP_COOKIES_FROM_BROWSER:-}" +FORMAT="${YOUTUBE_BACKUP_FORMAT:-bv*+ba/best}" +OUTPUT_TEMPLATE="${YOUTUBE_BACKUP_OUTPUT_TEMPLATE:-%(playlist_title)s/%(upload_date>%Y-%m-%d,release_date>%Y-%m-%d,epoch>%Y-%m-%d)s - %(title)s [%(id)s].%(ext)s}" +SUB_LANGS="${YOUTUBE_BACKUP_SUB_LANGS:-all,-live_chat}" +MERGE_FORMAT="${YOUTUBE_BACKUP_MERGE_FORMAT:-mp4}" +RATE_LIMIT="${YOUTUBE_BACKUP_RATE_LIMIT:-}" +SLEEP_REQUESTS="${YOUTUBE_BACKUP_SLEEP_REQUESTS:-1}" +EXTRA_ARGS="${YOUTUBE_BACKUP_EXTRA_ARGS:-}" + +if ! command -v yt-dlp >/dev/null 2>&1; then + echo "yt-dlp is required. Install it with: pipx install yt-dlp" >&2 + exit 1 +fi + +if [[ ! -f "${PLAYLISTS_FILE}" ]]; then + mkdir -p "$(dirname "${PLAYLISTS_FILE}")" + cat >"${PLAYLISTS_FILE}" <<'EOF' +# Add one YouTube playlist/channel/video URL per line. +# Examples: +# https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID +# https://www.youtube.com/@YOUR_CHANNEL/videos +EOF + echo "Created ${PLAYLISTS_FILE}. Add your playlist URLs and rerun." >&2 + exit 1 +fi + +if ! grep -Ev '^\s*($|#)' "${PLAYLISTS_FILE}" >/dev/null; then + echo "${PLAYLISTS_FILE} does not contain any playlist URLs." >&2 + exit 1 +fi + +mkdir -p "${VIDEOS_DIR}" "${LOG_DIR}" "$(dirname "${ARCHIVE_FILE}")" +touch "${ARCHIVE_FILE}" + +timestamp="$(date -u +%Y%m%dT%H%M%SZ)" +log_file="${LOG_DIR}/youtube-backup-${timestamp}.log" + +args=( + --batch-file "${PLAYLISTS_FILE}" + --download-archive "${ARCHIVE_FILE}" + --paths "home:${VIDEOS_DIR}" + --output "${OUTPUT_TEMPLATE}" + --format "${FORMAT}" + --merge-output-format "${MERGE_FORMAT}" + --ignore-errors + --continue + --no-overwrites + --restrict-filenames + --write-info-json + --write-description + --write-thumbnail + --write-subs + --write-auto-subs + --sub-langs "${SUB_LANGS}" + --embed-metadata + --embed-thumbnail + --sleep-requests "${SLEEP_REQUESTS}" +) + +if [[ -n "${COOKIES_FILE}" ]]; then + args+=(--cookies "${COOKIES_FILE}") +fi + +if [[ -n "${COOKIES_FROM_BROWSER}" ]]; then + args+=(--cookies-from-browser "${COOKIES_FROM_BROWSER}") +fi + +if [[ -n "${RATE_LIMIT}" ]]; then + args+=(--limit-rate "${RATE_LIMIT}") +fi + +if [[ -n "${EXTRA_ARGS}" ]]; then + # Intentionally split for operator-provided yt-dlp flags from a local .env. + # shellcheck disable=SC2206 + extra_args_array=(${EXTRA_ARGS}) + args+=("${extra_args_array[@]}") +fi + +echo "Backing up YouTube playlists from ${PLAYLISTS_FILE}" +echo "Videos: ${VIDEOS_DIR}" +echo "Archive: ${ARCHIVE_FILE}" +echo "Log: ${log_file}" + +yt-dlp "${args[@]}" 2>&1 | tee "${log_file}" diff --git a/infra/youtube-backup/playlists.example.txt b/infra/youtube-backup/playlists.example.txt new file mode 100644 index 0000000..229ce25 --- /dev/null +++ b/infra/youtube-backup/playlists.example.txt @@ -0,0 +1,8 @@ +# Add one playlist/channel/video URL per line. +# For backing up your uploaded videos, use your uploads playlist, a public +# playlist you maintain, or the channel videos URL if yt-dlp can access it with +# your cookies. +# +# Examples: +# https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID +# https://www.youtube.com/@YOUR_CHANNEL/videos