# YAAR — YouTube Auto-Archiver and Retagger Self-hosted YouTube archival system for Proxmox LXC (Alpine Linux). Downloads YouTube content as H.264/AAC MKV video or audio-only files with embedded metadata, written directly into a Jellyfin-compatible folder structure. Repository: https://gogs.av2x.dev/av2x/yaar --- ## What it does - Web UI with a job queue — paste a URL and it auto-fetches title, channel, and upload date - Archives as one of three types: - **TV series episode** → `Shows//Season NN/SNNENN - .mkv` - **Movie** → `Movies/ (Year).mkv` *(flat — no per-movie folder)* - **Music** → `Music/<Artist>/<Album>/<Track>.<ext>` *(album optional)* - Video downloads prefer **H.264 + AAC** for Jellyfin direct-play (no transcoding), falling back to best available if only VP9/AV1 exists, then remuxing to MKV - Music downloads to a selectable audio format (m4a, opus, mp3, flac, aac, wav) - Embeds metadata (title, channel/artist, date, URL, thumbnail, chapters) and writes **NFO sidecars** for Jellyfin scraping of series and movies - Live job queue with progress bars, speed/ETA, retry, and error reporting - Jobs persist across restarts; queue survives service and container reboots - Discrete per-event logging (boot, mount checks, per-job archive logs) - Weekly automatic yt-dlp update via cron, plus a one-command app updater --- ## Requirements - Proxmox VE 7+ with LXC support - An Alpine Linux 3.20 LXC container - A media directory on the host (your Jellyfin library) to bind-mount into the container at `/nas/jellyfin`, containing `Movies/`, `Shows/`, and `Music/` --- ## Quick start YAAR installs entirely **inside your Alpine LXC container**. The only host-side step is attaching the Jellyfin bind-mount. ### Step 1 — Attach the media bind-mount (Proxmox host, one-time) See `proxmox-mount.md` for the full guide. In short, on the Proxmox host shell: ```sh pct set <CTID> -mp0 /mnt/nas/jellyfin,mp=/nas/jellyfin pct restart <CTID> ``` Replace `/mnt/nas/jellyfin` with wherever your media lives on the host. The `mp=/nas/jellyfin` target must stay exactly as-is — YAAR expects that path. Or via the web UI: Container → Resources → Add → Mount Point, host path `/mnt/nas/jellyfin`, container path `/nas/jellyfin`, read-only off. ### Step 2 — Install (inside the container) One line, run as root inside the container: ```sh curl -fsSL https://gogs.av2x.dev/av2x/yaar/raw/master/setup.sh | sh ``` Or trigger it from the Proxmox host without entering the container: ```sh pct exec <CTID> -- sh -c 'curl -fsSL https://gogs.av2x.dev/av2x/yaar/raw/master/setup.sh | sh' ``` `setup.sh` installs Python, ffmpeg, yt-dlp, Flask and Gunicorn, the app files, an OpenRC service, the weekly yt-dlp auto-update cron, and the `yaar-update` command. It refuses to run on the Proxmox host and fetches any files it needs directly from the repo. When it finishes, open `http://<container-ip>:7474`. --- ## Updating Pull the latest code, redeploy, and restart — with automatic backup and rollback if the new code fails to start: ```sh yaar-update # update app + restart service yaar-update --deps # also upgrade yt-dlp / Flask / Gunicorn yaar-update --reboot # reboot the whole container after updating ``` Or the self-contained one-liner (always fetches the newest update logic too): ```sh curl -fsSL https://gogs.av2x.dev/av2x/yaar/raw/master/update.sh | sh ``` The updater downloads both app files to a temp location and only deploys if both succeed, backs up the previous versions to `/opt/yaar/backups/` (keeping the last 5), and restarts the service. If the service fails its health check on the new code, it automatically restores the previous version so YAAR is never left down. --- ## Service management (Alpine / OpenRC) ```sh rc-service yaar start rc-service yaar stop rc-service yaar restart rc-service yaar status ``` --- ## Jellyfin integration Point Jellyfin at the library roots inside your mounted media directory: | Library type | Path | |---|---| | Movies | `/nas/jellyfin/Movies` | | Shows | `/nas/jellyfin/Shows` | | Music | `/nas/jellyfin/Music` | For the **Shows** library, enable NFO metadata reading so episode details from yt-dlp populate correctly. Movies use a flat layout (`Movies/<Title> (Year).mkv` with a matching `.nfo` sidecar), which Jellyfin scrapes by filename. --- ## Format choices **Video (series / movie):** | Setting | Value | Why | |---|---|---| | Video codec | H.264 (avc1) | Universal Jellyfin direct-play, no transcoding | | Audio codec | AAC (mp4a) | Universal, no transcoding on any client | | Container | MKV | Supports metadata, chapters, thumbnails | | Selector | `bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/…/best` | H.264+AAC first, graceful fallback | If a video is only offered as VP9/AV1, yt-dlp falls back to the best available stream and still remuxes to MKV. Jellyfin may transcode those on weaker clients — a YouTube-side limitation, not a YAAR bug. **Music:** selectable per job — `m4a` (AAC, default), `opus`, `mp3`, `flac`, `aac`, or `wav`. Note that YouTube source audio is itself lossy (AAC/Opus), so `flac`/`wav` won't add quality, only file size. --- ## File & folder layout ``` /opt/yaar/ web/ app.py ← Flask application templates/index.html ← Web UI venv/ ← Python virtualenv jobs/ ← Persisted job JSON (queue survives restarts) backups/ ← Previous app versions kept by yaar-update update.sh ← Update script (also runs as `yaar-update`) logs/ yaar.log ← Unified log (everything) gunicorn.log ← Web server log access.log ← HTTP access log log_boot/<ts>.log ← One file per service start log_mount/<ts>.log ← One file per mount check log_archive/<id>.log ← One file per archive job (named by video id) /nas/jellyfin/ ← Your bind-mounted media (mp0) Movies/ Fantastic Voyage (1966).mkv Fantastic Voyage (1966).nfo Shows/ Kurzgesagt/ Season 01/ S01E01 - The Egg.mkv S01E01 - The Egg.nfo Music/ Nujabes/ Modal Soul/ 03 - Luv(sic) Part 3.m4a ``` --- ## Configuration Environment variables (in `/etc/yaar.env`): | Variable | Default | Description | |---|---|---| | `YAAR_BASE` | `/opt/yaar` | App, logs, jobs, backups directory | | `YAAR_MEDIA` | `/nas/jellyfin` | Root media output directory (the mp0 mount) | | `YAAR_PORT` | `7474` | Web UI port | After changing any of these, restart: `rc-service yaar restart`. --- ## Logs ```sh tail -f /opt/yaar/logs/yaar.log # everything, live ls /opt/yaar/logs/log_archive/ # per-job archive logs cat /opt/yaar/logs/log_archive/<videoid>__*.log # a specific job's full trace ``` Archive logs are named by YouTube video id, so you can find a job's log by URL without knowing its internal id. --- ## Troubleshooting **Queue is frozen / blank, console shows a SyntaxError** The served `index.html` is out of date. Run `yaar-update` and hard-refresh the browser (Ctrl+Shift+R) to clear the cached script. **Mount banner is red / downloads fail immediately** `/nas/jellyfin` isn't mounted or isn't writable. Confirm the mp0 bind-mount is attached and the container was restarted after adding it: ```sh ls /nas/jellyfin # should list Movies/ Shows/ Music/ ``` For unprivileged containers with permission issues, map the mount UID/GID: `pct set <CTID> -mp0 /mnt/nas/jellyfin,mp=/nas/jellyfin,uid=0,gid=0`. **Download fails with "Sign in to confirm your age"** Age-restricted videos need cookies. Export cookies from a logged-in browser and add a `--cookies` reference in `build_ydl_opts()` in `app.py`. **Falls back to VP9 instead of H.264** Some videos are only published in VP9/AV1. YAAR still downloads and remuxes; Jellyfin may transcode on weaker clients. YouTube-side limitation. **Port already in use** Change `YAAR_PORT` in `/etc/yaar.env` and restart the service. --- ## Manual yt-dlp update The weekly cron handles this automatically, but to force it: ```sh /opt/yaar/venv/bin/pip install --upgrade yt-dlp rc-service yaar restart ``` Or just `yaar-update --deps`, which upgrades yt-dlp, Flask, and Gunicorn together.