# yt-offline ![yt-offline icon](icon.png) A self-hosted media archive for YouTube and friends. Pastes any URL, routes it to the right folder by source, tracks what you've watched, and lets you play everything back from a desktop GUI or browser โ€” even when you're offline or the source video has been taken down. Built on [yt-dlp](https://github.com/yt-dlp/yt-dlp); written in Rust. ๐Ÿ“– **Documentation:** โ€” setup, first run, the anti-bot stack, troubleshooting, and architecture. ## What it backs up | Platform | Channels | Playlists | Single videos | | ---------------------------- | ----------- | ----------- | ------------- | | YouTube | โœ… | โœ… | โœ… | | TikTok | โœ… | โ€” | โœ… | | Twitch (VODs + clips) | โœ… | โ€” | โœ… | | Vimeo | โœ… | โœ… | โœ… | | Bandcamp | โœ… (artist) | โœ… (albums) | โœ… (tracks) | | SoundCloud | โœ… | โœ… (sets) | โœ… | | Odysee | โœ… | โ€” | โœ… | | Anything else yt-dlp accepts | `other/` | `other/` | `other/` | Each source lands in its own sibling folder under your backup directory (`channels/` for YouTube, `tiktok/`, `twitch/`, etc.). A `.source-url` sidecar is dropped in every creator folder so re-checks always know the exact URL to refresh from. ## Features ### Library - **Multi-platform sidebar** grouped by source, with platform icons, plus **folders/groups** that nest channels to any depth, and **smart folders** for per-video state flags (favourite / bookmark / watch-later / archive). - **Full-text search** across the whole library โ€” titles, channels, descriptions, **and subtitle transcripts** (SQLite FTS5, with highlighted snippets), alongside a quick title/id filter box for narrowing the current view. - **Filter chips** (watched, date, size, has-subtitles, has-chapters) that AND together, with saveable **named presets** (web). - **Sort by:** Newest / Oldest (upload date), Largest / Smallest, Longest / Shortest, Title. - **Watched tracking** + **resume positions** persisted in SQLite, plus a **Continue watching** view and **shuffle play** of a random unwatched video. - **Subtitles** (auto + manual, SRT and WebVTT), **chapters**, and **configurable SponsorBlock** (off / chapter-mark / cut) โ€” global and per-channel. - **Per-channel & per-video notes** โ€” searchable annotations. - **Bulk tagging** โ€” multi-select to set state flags across many videos at once. - **Statistics** โ€” totals, top channels by size/count, weekly downloads histogram, upload-year histogram. - **Maintenance** โ€” finds exact duplicate video IDs, **perceptual "similar content"** duplicates (the same video re-uploaded / mirrored / re-encoded under a *different* ID, detected by comparing sampled frames), and missing sidecars (thumbnail / info.json / description), each with one-click cleanup or repair. ### Playback - **Desktop GUI** launches your configured player (mpv by default; mpv gets resume-position tracking via JSON-IPC). A floating **transcript window** lets you search the subtitles and click any line to seek the running mpv. - **Web UI** plays videos inline in any browser, with subtitle tracks, a **chapters** panel, and a searchable **transcript** pane โ€” click any line to seek, and the current line highlights as the video plays. Optional on-the-fly **ffmpeg transcoding** for browsers that can't decode MKV. - **Comment viewer** (web) โ€” for videos archived with comments: threaded with collapse/expand, in-comment search, sort (top / newest / oldest), an uploader badge, and a "new since last visit" highlight. ### Downloads - **Bundled yt-dlp + curl_cffi + deno** โ€” one click in Settings sets up a self-contained Python venv with `yt-dlp[default]` plus `curl_cffi` (browser-impersonation for bot-detection bypass) and `deno` for player-JS evaluation. No system installation required. - Or use **system yt-dlp** โ€” toggleable in Settings. - **Concurrent-download limit** (default 3) with a visible queue and live WebSocket progress. - **Quality picker:** Best / 1080p / 720p / 480p / 360p, or **Music mode** for audio-only extraction into `music//`. - **Per-channel overrides** โ€” quality, subtitles, SponsorBlock mode, format conversion, YouTube player-clients, rate/size/date filters, and raw extra yt-dlp args, applied automatically on scheduled re-checks. - **Format conversion** โ€” optional post-download ffmpeg pass: remux to MP4, re-encode to H.264/AAC at a chosen CRF, or extract audio (global or per-channel, with a keep-original toggle). - **Anti-bot stack** โ€” browser TLS impersonation via `curl_cffi`, an optional **POT (Proof-of-Origin) token provider** (bundled `bgutil-pot`), configurable **player clients** to route around captcha-walled clients, and a cookie-freshness / anonymous-jar warning. - **Auto-retry + adaptive throttle** โ€” transient rate-limit/network failures are re-queued after a cooldown, and the batch slows itself after a hit (on top of `--retries 30 --fragment-retries 30 --retry-sleep linear=1:30:2`). - **Scheduler** โ€” periodically re-check every channel for new uploads. - **Cookies** โ€” paste / file-pick a Netscape `cookies.txt`, or fall back to `--cookies-from-browser` for whatever browser you pick. ### Reliability - **Hang watchdog** โ€” a yt-dlp/ffmpeg job that goes silent for 5 minutes is killed and re-queued, so a stalled process can't wedge the queue. - **Disk-full preflight** โ€” refuses to start a download when the target filesystem is nearly full, surfacing a clear error instead of a half-written file. - **9-class error classifier** โ€” every failure gets a one-line suggested fix (rate-limited, members-only, geoblocked, bad cookies, codec missing, disk full, โ€ฆ) shown in both UIs. - **Crash log** โ€” any thread panic is appended to `yt-offline.crash.log` next to the database, so a GUI launched without a terminal still leaves a trace. - **Library backup & restore** โ€” download a snapshot of the SQLite database; restore does a schema-validated, idempotent merge (watched / positions / flags / folders / notes). - Poison-recovering locks keep one handler's panic from taking down the long-running web server. ### Web server - Single binary, no Node / Python / Docker required (Python only for the optional bundled yt-dlp venv). - **Bind interface picker** โ€” localhost only (default), Tailscale, LAN, or all interfaces. - **Password-gated UI** when enabled โ€” Argon2 hashed, 256-bit session tokens, 30-day TTL with lazy pruning, per-IP rate-limit on `/api/login` (5 failures โ†’ 60s lockout). - **Security headers:** Content-Security-Policy, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy no-referrer. - **`Secure` cookie flag** when `X-Forwarded-Proto: https` is present (i.e. behind a reverse proxy doing TLS). - 4 MiB request body limit on every route. ### Plex integration - One click generates a symlink tree your Plex "TV Shows" library can ingest directly: `//Season 2024/ - S2024E001 - title.mkv`. - Writes `.nfo` sidecars per episode (title, season, episode, aired date, runtime, plot) plus a show-level `tvshow.nfo`, so Plex's "Personal Media (TV Shows)" agent picks up YouTube metadata correctly. - Symlinks the thumbnail as `-thumb.jpg` so Plex shows episode art. - Non-YouTube creators get ` - ` show folders to avoid collisions across sources. ### Themes Ten themes shared across desktop and web โ€” Dark, Light, Dracula, Trans, plus three Emo/Scene flavours (Nocturnal, Coffin, Scene Queen). ### Misc - **Desktop UI zoom** โ€” global egui scale (whole UI, not just cards) via a Settings slider or `Ctrl +`/`-`/`0`, persisted. - **System tray** (Linux StatusNotifierItem) with opt-in minimize-to-tray. - **Keyboard shortcuts** in the web UI (`/` filter, `f` full-text search, `r` rescan, `d` downloads, `?` help). - Native notifications when downloads finish (notify-rust / zbus, no GTK build dep). - `/api/stats`, `/api/ytdlp/update`, `/api/scheduler/run` โ€” every UI button has an equivalent JSON endpoint. ## Quick start ```bash git clone https://codeberg.org/anassaeneroi/yt-offline cd yt-offline cargo build --release # Desktop GUI ./target/release/yt-offline # Or web server only (headless) ./target/release/yt-offline --web 8080 ``` On first run a `config.toml` is created in the working directory. Settings can also be changed from inside either UI โ€” no manual editing required. ### Bundled yt-dlp setup 1. Open **Settings**. 2. Under **yt-dlp binary**, choose **Bundled** and click **Install**. 3. The installer creates `~/.local/share/yt-offline/venv/` with `yt-dlp[default]` + `curl_cffi`, plus `~/.local/share/yt-offline/bin/deno`. Progress streams into a regular job entry. Updates are the same button. Switching to **System** uses whatever `yt-dlp` is on PATH instead. ## Building ### Arch / CachyOS / Manjaro ```bash sudo pacman -S --needed rust mpv # python3 + python-pip already present git clone https://codeberg.org/anassaeneroi/yt-offline cd yt-offline makepkg -si # or: cargo build --release ``` A `PKGBUILD` is included. ### Debian / Ubuntu ```bash sudo apt install \ build-essential pkg-config curl git python3-venv \ libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ libxkbcommon-dev libssl-dev curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" cargo build --release ``` `python3-venv` is required for the bundled yt-dlp install path (skipable if you only ever use system yt-dlp). ### macOS ```bash xcode-select --install brew install mpv curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" cargo build --release ``` ### Windows Install [Rust](https://rustup.rs/) (MSVC toolchain), Visual Studio Build Tools with the **Desktop development with C++** workload, then: ```powershell winget install mpv.net python.python.3.12 git clone https://codeberg.org/anassaeneroi/yt-offline cd yt-offline cargo build --release ``` ## Configuration `config.toml` lives next to the binary (or in the working directory). All fields are also editable in Settings. ```toml [backup] directory = "/path/to/library" # library root; every platform nests under it (channels/, tiktok/, โ€ฆ) max_concurrent = 3 # parallel yt-dlp processes use_bundled_ytdlp = false # true = use the venv set up by the Install button use_pot_provider = false # run bgutil-pot for YouTube Proof-of-Origin tokens sponsorblock_mode = "mark" # off | mark (chapter markers) | remove (cut segments) youtube_player_clients = "" # e.g. "tv,mweb" to route around captcha-walled clients [player] command = "mpv" # any executable that takes a file path as last arg browser = "firefox" # used by --cookies-from-browser when no cookies.txt is set [ui] theme = "dark" # dark | light | dracula | trans | emo-nocturnal | emo-coffin | emo-scene-queen ui_scale = 1.0 # global zoom for the desktop UI # Subtitles ([subtitles]: download / auto / embed / convert-format / langs) # and post-download format conversion ([convert]: mode / crf / preset / # audio_format / keep_original) also have their own sections โ€” easiest to set # from Settings. Both support per-channel overrides. [scheduler] enabled = false interval_hours = 24 [web] port = 8080 bind = "127.0.0.1" # 127.0.0.1 | 0.0.0.0 | a Tailscale/LAN address transcode = false # MKV โ†’ MP4 on the fly for browsers (needs ffmpeg) source_url = "https://codeberg.org/anassaeneroi/yt-offline" # AGPL ยง13 footer link [plex] library_path = "/path/to/plex/TV/youtube" # leave unset to disable ``` ## Usage ### Desktop GUI 1. Click **โฌ‡ Downloads**, paste any URL โ€” channel, video, playlist, or profile from any supported platform. The dialog shows the detected source and target folder before you confirm. 2. Choose quality (or **๐ŸŽต Music** for audio-only). 3. Pick **Fast mode** to stop at the first already-archived video, or leave it off for a full back-fill. The sidebar groups channels by platform. Right-click any channel for **Check for new videos** and **Open folder**. The download bar shows the active job count plus what's queued. Other top-bar buttons: - **โŸณ Rescan** โ€” re-read the library directory. - **๐Ÿ“Š Stats** โ€” totals, top channels, weekly download histogram. - **๐Ÿ”Ž Search** โ€” full-text across titles, descriptions, and transcripts. - **๐Ÿฉบ Maintenance** โ€” resolve duplicate IDs, perceptual "similar content" duplicates, and missing sidecars. - **โš™ Settings** โ€” everything in `config.toml`, plus cookies, password, bundled-yt-dlp install/update, and starting the web server. ### Web UI Start the server (`--web` CLI flag or **Start** button in Settings โ†’ **Web server**), open `http://localhost:8080` (or your configured bind address). The UI mirrors the desktop one and adds inline video playback with a searchable transcript pane, a threaded comment viewer, and full-text library search (๐Ÿ” in the header, or press `f`). If a download password is set, you'll be redirected to a login page; the session cookie is HttpOnly + SameSite=Strict, valid 30 days. ## Library layout ```text /path/to/library/ โ”œโ”€โ”€ channels/ โ† YouTube โ”‚ โ”œโ”€โ”€ @creator-name/ โ”‚ โ”‚ โ”œโ”€โ”€ .source-url โ† original URL, for re-checks โ”‚ โ”‚ โ”œโ”€โ”€ archive.txt โ† yt-dlp's downloaded-ID record โ”‚ โ”‚ โ”œโ”€โ”€ Video Title [abc123].mkv โ”‚ โ”‚ โ”œโ”€โ”€ Video Title [abc123].webp โ”‚ โ”‚ โ”œโ”€โ”€ Video Title [abc123].info.json โ”‚ โ”‚ โ”œโ”€โ”€ Video Title [abc123].description โ”‚ โ”‚ โ””โ”€โ”€ Video Title [abc123].en.vtt โ”‚ โ””โ”€โ”€ @another/ โ”œโ”€โ”€ tiktok/ โ”œโ”€โ”€ twitch/ โ”œโ”€โ”€ vimeo/ โ”œโ”€โ”€ bandcamp/ โ”œโ”€โ”€ soundcloud/ โ”œโ”€โ”€ odysee/ โ”œโ”€โ”€ other/ โ”œโ”€โ”€ music/ โ”‚ โ””โ”€โ”€ / โ”‚ โ””โ”€โ”€ Track Title [xyz].opus โ””โ”€โ”€ yt-offline.db โ† watched + positions + password hash ``` ## Troubleshooting **"Impersonate target ... is not available"** You're on bundled mode but the venv install hasn't completed. Open Settings โ†’ yt-dlp binary โ†’ click **Install** and watch the job for the "impersonation targets available" line. **`yt-dlp` not found (system mode)** Install yt-dlp via your package manager or `pip install yt-dlp`, or switch to **Bundled** in Settings. **Web UI says "authentication required" but no password is set** Sessions cleared after the password was changed. Refresh the page. **Videos play but seeking is approximate** That's usually transcoding mode (`web.transcode = true`). yt-offline can scrub those live ffmpeg streams by re-requesting the stream at a `start=` offset, but ffmpeg's fast seek lands on a nearby keyframe rather than an exact byte position. Turn transcoding off if your browser can play the original file directly and you want native browser seeking. **Connection-reset / "Recv failure" errors on download** The defaults already retry 30ร— with linear backoff. If they're persistent, your IP is being rate-limited โ€” paste fresh cookies from a browser session into Settings โ†’ Cookies. **Build fails on Debian/Ubuntu** Make sure all packages from the build dependencies block are installed, plus `python3-venv` if you want the bundled yt-dlp install path. ## Security See [SECURITY_AUDIT.md](SECURITY_AUDIT.md) โ€” the codebase has been audited end-to-end, covering command/SQL injection, path traversal, XSS, session management, rate-limiting, supply-chain (bundled binary SHA verification), and dependency status. ## License [AGPL-3.0-or-later](LICENSE). When you run this software as a network service, the AGPL requires that you offer network users access to the Corresponding Source โ€” the running version, with any modifications. Set `web.source_url` in config to a public URL where the source is available; the web UI surfaces it in the footer and `GET /api/settings`.