|
@@ -1,86 +1,103 @@
|
|
|
# YAAR — YouTube Auto-Archiver and Retagger
|
|
# YAAR — YouTube Auto-Archiver and Retagger
|
|
|
|
|
|
|
|
-Self-hosted YouTube archival system designed for Proxmox LXC (Alpine Linux).
|
|
|
|
|
-Archives YouTube videos as H.264/AAC MKVs with embedded metadata, written
|
|
|
|
|
-directly into a Jellyfin-compatible folder structure.
|
|
|
|
|
|
|
+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
|
|
## What it does
|
|
|
|
|
|
|
|
-- Web UI for submitting YouTube URLs with one click
|
|
|
|
|
-- **Probes** the URL first to pre-fill title, author, and upload date
|
|
|
|
|
-- Archives as one of three Jellyfin-native types:
|
|
|
|
|
- - **YouTube video** → `/media/yaar/YouTube/<Channel>/<Title>.mkv`
|
|
|
|
|
- - **TV series episode** → `/media/yaar/Shows/<Series>/Season NN/SNNENN - <Title>.mkv`
|
|
|
|
|
- - **Movie** → `/media/yaar/Movies/<Title> (Year)/<Title> (Year).mkv`
|
|
|
|
|
-- Downloads best quality with **H.264 video + AAC audio** (Jellyfin-safe, no transcoding needed)
|
|
|
|
|
-- Remuxes to **MKV** with:
|
|
|
|
|
- - Embedded metadata: title, artist/channel, date, original URL, description
|
|
|
|
|
- - Embedded thumbnail
|
|
|
|
|
- - Chapter markers (if available)
|
|
|
|
|
-- Writes **NFO sidecar files** for Jellyfin scraping (series and movies)
|
|
|
|
|
-- Real-time job queue with progress bars and error reporting
|
|
|
|
|
-- Jobs persist across restarts (JSON files in `/opt/yaar/jobs/`)
|
|
|
|
|
-- Weekly **auto-update** of yt-dlp via cron
|
|
|
|
|
|
|
+- 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/<Series>/Season NN/SNNENN - <Episode>.mkv`
|
|
|
|
|
+ - **Movie** → `Movies/<Title> (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
|
|
## Requirements
|
|
|
|
|
|
|
|
-- Proxmox VE 7+ (any version with LXC support)
|
|
|
|
|
-- Alpine Linux 3.20 LXC template
|
|
|
|
|
-- A storage volume or directory on the host for media (bind-mounted into the container)
|
|
|
|
|
|
|
+- 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
|
|
## Quick start
|
|
|
|
|
|
|
|
-YAAR installs entirely from **inside your existing Alpine LXC container**.
|
|
|
|
|
-The only thing done on the Proxmox host is attaching the Jellyfin bind-mount.
|
|
|
|
|
-
|
|
|
|
|
-### Step 1 — Attach the bind-mount in Proxmox (one-time, host only)
|
|
|
|
|
|
|
+YAAR installs entirely **inside your Alpine LXC container**. The only host-side
|
|
|
|
|
+step is attaching the Jellyfin bind-mount.
|
|
|
|
|
|
|
|
-See `proxmox-mount.md` for the full guide. In short:
|
|
|
|
|
|
|
+### Step 1 — Attach the media bind-mount (Proxmox host, one-time)
|
|
|
|
|
|
|
|
-**Web UI:** Container → Resources → Add → Mount Point
|
|
|
|
|
-- Host path: `/mnt/nas/jellyfin` (your NAS location on the host)
|
|
|
|
|
-- Container path: `/nas/jellyfin`
|
|
|
|
|
-- Read-only: No
|
|
|
|
|
|
|
+See `proxmox-mount.md` for the full guide. In short, on the Proxmox host shell:
|
|
|
|
|
|
|
|
-**Or via the Proxmox host shell:**
|
|
|
|
|
```sh
|
|
```sh
|
|
|
pct set <CTID> -mp0 /mnt/nas/jellyfin,mp=/nas/jellyfin
|
|
pct set <CTID> -mp0 /mnt/nas/jellyfin,mp=/nas/jellyfin
|
|
|
pct restart <CTID>
|
|
pct restart <CTID>
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### Step 2 — Get YAAR into the container
|
|
|
|
|
|
|
+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
|
|
```sh
|
|
|
-# From the Proxmox host:
|
|
|
|
|
-pct push <CTID> yaar.tar.gz /root/yaar.tar.gz
|
|
|
|
|
|
|
+curl -fsSL https://gogs.av2x.dev/av2x/yaar/raw/master/setup.sh | sh
|
|
|
|
|
+```
|
|
|
|
|
|
|
|
-# Or via scp from any machine:
|
|
|
|
|
-scp yaar.tar.gz root@<container-ip>:/root/
|
|
|
|
|
|
|
+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'
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### Step 3 — Run setup inside the container
|
|
|
|
|
|
|
+`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
|
|
```sh
|
|
|
-pct enter <CTID> # enter the container, or ssh into it
|
|
|
|
|
-tar xzf /root/yaar.tar.gz -C /root/
|
|
|
|
|
-sh /root/yaar/setup.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
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-`setup.sh` detects if it's accidentally run on the Proxmox host and exits
|
|
|
|
|
-immediately — it will not proceed unless it's running inside a container.
|
|
|
|
|
|
|
+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 script installs:
|
|
|
|
|
-- Python 3, pip, venv
|
|
|
|
|
-- ffmpeg
|
|
|
|
|
-- yt-dlp (latest), Flask, Gunicorn
|
|
|
|
|
-- OpenRC init script (`/etc/init.d/yaar`)
|
|
|
|
|
-- Weekly yt-dlp auto-update cron
|
|
|
|
|
|
|
+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.
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -93,108 +110,143 @@ rc-service yaar restart
|
|
|
rc-service yaar status
|
|
rc-service yaar status
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-Logs:
|
|
|
|
|
-```sh
|
|
|
|
|
-tail -f /opt/yaar/logs/yaar.log # Application log
|
|
|
|
|
-tail -f /opt/yaar/logs/gunicorn.log # Web server log
|
|
|
|
|
-```
|
|
|
|
|
-
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## Jellyfin integration
|
|
## Jellyfin integration
|
|
|
|
|
|
|
|
-Point Jellyfin at the three library roots:
|
|
|
|
|
|
|
+Point Jellyfin at the library roots inside your mounted media directory:
|
|
|
|
|
|
|
|
| Library type | Path |
|
|
| Library type | Path |
|
|
|
|---|---|
|
|
|---|---|
|
|
|
-| Movies | `/media/yaar/Movies` |
|
|
|
|
|
-| Shows | `/media/yaar/Shows` |
|
|
|
|
|
-| Other (YouTube) | `/media/yaar/YouTube` |
|
|
|
|
|
|
|
+| Movies | `/nas/jellyfin/Movies` |
|
|
|
|
|
+| Shows | `/nas/jellyfin/Shows` |
|
|
|
|
|
+| Music | `/nas/jellyfin/Music` |
|
|
|
|
|
|
|
|
For the **Shows** library, enable NFO metadata reading so episode details from
|
|
For the **Shows** library, enable NFO metadata reading so episode details from
|
|
|
-yt-dlp populate correctly. For YouTube videos, the library type "Other Videos"
|
|
|
|
|
-or "Shows" both work — choose based on preference.
|
|
|
|
|
|
|
+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
|
|
## Format choices
|
|
|
|
|
|
|
|
|
|
+**Video (series / movie):**
|
|
|
|
|
+
|
|
|
| Setting | Value | Why |
|
|
| Setting | Value | Why |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
|
| Video codec | H.264 (avc1) | Universal Jellyfin direct-play, no transcoding |
|
|
| Video codec | H.264 (avc1) | Universal Jellyfin direct-play, no transcoding |
|
|
|
| Audio codec | AAC (mp4a) | Universal, no transcoding on any client |
|
|
| Audio codec | AAC (mp4a) | Universal, no transcoding on any client |
|
|
|
| Container | MKV | Supports metadata, chapters, thumbnails |
|
|
| Container | MKV | Supports metadata, chapters, thumbnails |
|
|
|
-| Quality | Best available H.264 | yt-dlp format selector: `bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]` |
|
|
|
|
|
|
|
+| 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.
|
|
|
|
|
|
|
|
-If a video is only available as VP9/AV1 (e.g. high-res YouTube uploads), yt-dlp
|
|
|
|
|
-falls back to the best available VP9 + audio, then remuxes to MKV. The web UI
|
|
|
|
|
-shows whatever was actually used.
|
|
|
|
|
|
|
+**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 structure
|
|
|
|
|
|
|
+## File & folder layout
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
/opt/yaar/
|
|
/opt/yaar/
|
|
|
web/
|
|
web/
|
|
|
- app.py ← Flask application
|
|
|
|
|
- templates/
|
|
|
|
|
- index.html ← Web UI
|
|
|
|
|
- venv/ ← Python virtualenv
|
|
|
|
|
|
|
+ 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/
|
|
logs/
|
|
|
- yaar.log
|
|
|
|
|
- gunicorn.log
|
|
|
|
|
- access.log
|
|
|
|
|
- jobs/ ← Persisted job JSON files
|
|
|
|
|
-
|
|
|
|
|
-/media/yaar/
|
|
|
|
|
|
|
+ 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/
|
|
Movies/
|
|
|
- Fantastic Voyage (1966)/
|
|
|
|
|
- Fantastic Voyage (1966).mkv
|
|
|
|
|
- Fantastic Voyage (1966).nfo
|
|
|
|
|
|
|
+ Fantastic Voyage (1966).mkv
|
|
|
|
|
+ Fantastic Voyage (1966).nfo
|
|
|
Shows/
|
|
Shows/
|
|
|
Kurzgesagt/
|
|
Kurzgesagt/
|
|
|
Season 01/
|
|
Season 01/
|
|
|
S01E01 - The Egg.mkv
|
|
S01E01 - The Egg.mkv
|
|
|
S01E01 - The Egg.nfo
|
|
S01E01 - The Egg.nfo
|
|
|
- YouTube/
|
|
|
|
|
- Veritasium/
|
|
|
|
|
- Does Planet 9 Exist.mkv
|
|
|
|
|
|
|
+ Music/
|
|
|
|
|
+ Nujabes/
|
|
|
|
|
+ Modal Soul/
|
|
|
|
|
+ 03 - Luv(sic) Part 3.m4a
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
## Configuration
|
|
## Configuration
|
|
|
|
|
|
|
|
-Environment variables (set in `/etc/yaar.env`):
|
|
|
|
|
|
|
+Environment variables (in `/etc/yaar.env`):
|
|
|
|
|
|
|
|
| Variable | Default | Description |
|
|
| Variable | Default | Description |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
|
-| `YAAR_BASE` | `/opt/yaar` | App, logs, jobs directory |
|
|
|
|
|
-| `YAAR_MEDIA` | `/media/yaar` | Root media output directory |
|
|
|
|
|
|
|
+| `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 |
|
|
| `YAAR_PORT` | `7474` | Web UI port |
|
|
|
|
|
|
|
|
|
|
+After changing any of these, restart: `rc-service yaar restart`.
|
|
|
|
|
+
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## Updating yt-dlp manually
|
|
|
|
|
|
|
+## Logs
|
|
|
|
|
|
|
|
```sh
|
|
```sh
|
|
|
-/opt/yaar/venv/bin/pip install --upgrade yt-dlp
|
|
|
|
|
-rc-service yaar restart
|
|
|
|
|
|
|
+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
|
|
## Troubleshooting
|
|
|
|
|
|
|
|
-**Download fails with "Sign in to confirm your age"**
|
|
|
|
|
-YouTube sometimes requires cookies for age-restricted content. Export cookies from a logged-in browser and pass them to yt-dlp via `--cookies` in `build_ydl_opts()` in `app.py`.
|
|
|
|
|
|
|
+**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.
|
|
|
|
|
|
|
|
-**H.264 format not available / falls back to VP9**
|
|
|
|
|
-Some YouTube videos are only available in VP9. YAAR will still download and remux, but Jellyfin may transcode on lower-powered clients. This is a YouTube limitation, not a YAAR bug.
|
|
|
|
|
|
|
+**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`.
|
|
|
|
|
|
|
|
-**Container can't write to media directory**
|
|
|
|
|
-Ensure the bind mount path exists on the host and the container has write permission. For unprivileged containers, the UID/GID mapping may need adjustment: `pct set <CT_ID> --mp0 /host/path,mp=/media/yaar,uid=0,gid=0`.
|
|
|
|
|
|
|
+**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**
|
|
**Port already in use**
|
|
|
-Edit `YAAR_PORT` in `/etc/yaar.env` and restart the service.
|
|
|
|
|
|
|
+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.
|