docs: rename yt-offline → Catacomb across README, ROADMAP, guides, packaging

Prose + commands updated to the new name: brand reads "Catacomb", while
binary/path/package references are the lowercase `catacomb` (commands, deb/rpm
names, ~/.local/share/catacomb, catacomb.db, catacomb.desktop). Codeberg repo
URLs left pointing at the existing repo. Also folds in the pending doc edits
from earlier in the session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-19 03:34:18 -07:00
parent 404362bfb0
commit 3f611849af
72 changed files with 1513 additions and 293 deletions

View file

@ -42,7 +42,7 @@ jobs:
- name: Add Windows cross-compile target
# package.sh's `all` only builds the Windows zip when this target +
# mingw + zip are all present (win_available), so this step is what
# opts the release build into producing yt-offline-*-x86_64-windows.zip.
# opts the release build into producing catacomb-*-x86_64-windows.zip.
run: rustup target add x86_64-pc-windows-gnu
- name: Cache cargo registry + build
@ -87,7 +87,7 @@ jobs:
- `.deb` — Debian / Ubuntu / Mint
- `.rpm` — Fedora / RHEL / openSUSE
- `.AppImage` — any Linux (chmod +x and run)
- `-x86_64-windows.zip` — Windows 10/11 (unzip, run yt-offline.exe)
- `-x86_64-windows.zip` — Windows 10/11 (unzip, run catacomb.exe)
Runtime requirements: yt-dlp, ffmpeg, mpv on PATH (plus
xdg-utils on Linux). The Windows zip's README lists the same.

6
.gitignore vendored
View file

@ -9,8 +9,8 @@
# makepkg build artifacts (finished .pkg.tar.zst is NOT ignored)
/pkg/
/src/yt-offline/
/yt-offline/
/src/catacomb/
/catacomb/
# Rendered mdBook output (source lives in docs/src/)
/docs/book/
@ -27,4 +27,4 @@ cookies.txt
config.toml
# Runtime database (watched status, positions, password hash) — never commit
yt-offline.db
catacomb.db

8
.vscode/launch.json vendored
View file

@ -5,25 +5,25 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug executable 'yt-offline'",
"name": "Debug executable 'catacomb'",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"run",
"--bin=yt-offline"
"--bin=catacomb"
]
},
"args": []
},
{
"name": "Debug unit tests in executable 'yt-offline'",
"name": "Debug unit tests in executable 'catacomb'",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"test",
"--bin=yt-offline"
"--bin=catacomb"
]
}
}

View file

@ -4,7 +4,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
## What this is
`yt-offline` — a single Rust binary that is **both** a desktop GUI (eframe/egui)
`catacomb` — a single Rust binary that is **both** a desktop GUI (eframe/egui)
and a headless web server (axum), wrapping `yt-dlp` to archive YouTube/TikTok/
Twitch/etc. AGPL-3.0. North-star goal and feature-parity tracking live in
[ROADMAP.md](ROADMAP.md); a structured analysis of the Tartube benchmark is in
@ -17,8 +17,8 @@ cargo build --release # the binary (release profile is opt-level=3 + t
cargo test --release # unit tests + tests/api.rs integration tests (no network)
cargo test --release <name> # single test by substring, e.g. `cargo test --release subs_disabled`
cargo test --release real_ffmpeg -- --ignored # opt-in: real-ffmpeg fingerprint accuracy/speed check
./target/release/yt-offline # desktop GUI mode (default)
./target/release/yt-offline --web 8080 # headless web server on a port
./target/release/catacomb # desktop GUI mode (default)
./target/release/catacomb --web 8080 # headless web server on a port
scripts/package.sh [deb|rpm|appimage|all] # build distro packages → dist/ (see docs/PACKAGING.md)
```
@ -103,6 +103,12 @@ in memory and mutating endpoints mirror DB writes onto those caches +
`bump_library_version()` (the ETag) so `/api/library` stays consistent without a
rescan.
Web login sessions are runtime-owned but restart-persistent: `web.rs` keeps the
live `HashMap<String, u64>` and mirrors tokens into the `sessions` table, then
rehydrates non-expired rows at startup. Logout and password changes clear those
rows. The DB is plain SQLite, not encrypted, so `/api/backup/db` exports a
sensitive file; at-rest DB encryption is deliberately deferred to Phase 4.
Three `(path|video_id, mtime)`-keyed caches let expensive work happen once and be
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
`search_meta` + the `video_search` FTS5 index (full-text title/channel/
@ -123,7 +129,7 @@ whole SPA — a `cargo build` won't catch it since the HTML is just a string).
### Bundled toolchain & anti-bot
`ytdlp_bin.rs` manages an optional self-contained venv at
`~/.local/share/yt-offline/` (nightly `yt-dlp[default]` via `--pre` + `curl_cffi`
`~/.local/share/catacomb/` (nightly `yt-dlp[default]` via `--pre` + `curl_cffi`
for TLS impersonation + bundled `deno`). `pot_provider.rs` runs `bgutil-pot` (a
loopback HTTP server) for YouTube Proof-of-Origin tokens; **its yt-dlp plugin
must come from the same release as the server binary, not PyPI** (version skew
@ -154,14 +160,17 @@ both the transcript indexer and the transcript viewers.
## Conventions
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
specific), or `yt-offline.db` (contains the Argon2 password hash). All
gitignored.
specific), or `catacomb.db` (plain SQLite app state, including the Argon2
password hash and restart-persistent session tokens). All gitignored.
- Redact the absolute cookies path out of any log line surfaced to the UI/API
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
- `app.rs` and `web.rs` are large (~34k lines) because each owns a full UI; new
desktop code goes in `app.rs`, web handlers in `web.rs`, shared logic in the
focused modules (`downloader`, `database`, `library`, `platform`, `fingerprint`,
`vtt`, …).
- Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only/no-GTK by
design; keep that posture (it's why packaging avoids a GTK dep). Windows/macOS
are not yet first-class — the tray would need a per-OS backend.
- Tray (`ksni`) and the `rfd` xdg-portal dialog backend are Linux-only/no-GTK
by design (it's why packaging avoids a GTK dep). They're target-gated in
`Cargo.toml`; off Linux, `rfd` uses its native backend and `tray::start` is a
no-op. Windows now ships as a cross-compiled zip; macOS has a local osxcross
packaging path but is not in CI and still needs signing/notarization + a real
tray backend.

View file

@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What this is
`yt-offline` — a single Rust binary that is **both** a desktop GUI (eframe/egui)
`catacomb` — a single Rust binary that is **both** a desktop GUI (eframe/egui)
and a headless web server (axum), wrapping `yt-dlp` to archive YouTube/TikTok/
Twitch/etc. AGPL-3.0. North-star goal and feature-parity tracking live in
[ROADMAP.md](ROADMAP.md); a structured analysis of the Tartube benchmark is in
@ -17,8 +17,8 @@ cargo build --release # the binary (release profile is opt-level=3 + t
cargo test --release # unit tests + tests/api.rs integration tests (no network)
cargo test --release <name> # single test by substring, e.g. `cargo test --release subs_disabled`
cargo test --release real_ffmpeg -- --ignored # opt-in: real-ffmpeg fingerprint accuracy/speed check
./target/release/yt-offline # desktop GUI mode (default)
./target/release/yt-offline --web 8080 # headless web server on a port
./target/release/catacomb # desktop GUI mode (default)
./target/release/catacomb --web 8080 # headless web server on a port
scripts/package.sh [deb|rpm|appimage|all] # build distro packages → dist/ (see docs/PACKAGING.md)
```
@ -103,6 +103,12 @@ in memory and mutating endpoints mirror DB writes onto those caches +
`bump_library_version()` (the ETag) so `/api/library` stays consistent without a
rescan.
Web login sessions are runtime-owned but restart-persistent: `web.rs` keeps the
live `HashMap<String, u64>` and mirrors tokens into the `sessions` table, then
rehydrates non-expired rows at startup. Logout and password changes clear those
rows. The DB is plain SQLite, not encrypted, so `/api/backup/db` exports a
sensitive file; at-rest DB encryption is deliberately deferred to Phase 4.
Three `(path|video_id, mtime)`-keyed caches let expensive work happen once and be
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
`search_meta` + the `video_search` FTS5 index (full-text title/channel/
@ -123,7 +129,7 @@ whole SPA — a `cargo build` won't catch it since the HTML is just a string).
### Bundled toolchain & anti-bot
`ytdlp_bin.rs` manages an optional self-contained venv at
`~/.local/share/yt-offline/` (nightly `yt-dlp[default]` via `--pre` + `curl_cffi`
`~/.local/share/catacomb/` (nightly `yt-dlp[default]` via `--pre` + `curl_cffi`
for TLS impersonation + bundled `deno`). `pot_provider.rs` runs `bgutil-pot` (a
loopback HTTP server) for YouTube Proof-of-Origin tokens; **its yt-dlp plugin
must come from the same release as the server binary, not PyPI** (version skew
@ -154,8 +160,8 @@ both the transcript indexer and the transcript viewers.
## Conventions
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
specific), or `yt-offline.db` (contains the Argon2 password hash). All
gitignored.
specific), or `catacomb.db` (plain SQLite app state, including the Argon2
password hash and restart-persistent session tokens). All gitignored.
- Redact the absolute cookies path out of any log line surfaced to the UI/API
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
- `app.rs` and `web.rs` are large (~34k lines) because each owns a full UI; new
@ -172,6 +178,7 @@ both the transcript indexer and the transcript viewers.
cross-compiles — `cargo check --target x86_64-pc-windows-gnu` is green — so
keep new platform-specific code behind `cfg(unix)`/`cfg(windows)`/
`cfg(target_os = …)` with a fallback, the way `disk_space`, `plex`, the mpv
IPC, and the `chmod` guards already do. Windows/macOS still aren't *shipped*
(no tray backend, no link/CI yet), but don't reintroduce an unconditional
Linux-only dep.
IPC, and the `chmod` guards already do. Windows now ships as a cross-compiled
zip from CI; macOS has a local osxcross packaging path but is not in CI and
still needs signing/notarization + a real tray backend. Don't reintroduce an
unconditional Linux-only dep.

54
Cargo.lock generated
View file

@ -727,6 +727,33 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "catacomb"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"eframe",
"image",
"ksni",
"libc",
"notify-rust",
"r2d2",
"r2d2_sqlite",
"rand 0.8.6",
"reqwest",
"rfd",
"rusqlite",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tokio-util",
"toml",
"tower-http 0.5.2",
"windows-sys 0.59.0",
]
[[package]]
name = "cc"
version = "1.2.62"
@ -5408,33 +5435,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "yt-offline"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"eframe",
"image",
"ksni",
"libc",
"notify-rust",
"r2d2",
"r2d2_sqlite",
"rand 0.8.6",
"reqwest",
"rfd",
"rusqlite",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tokio-util",
"toml",
"tower-http 0.5.2",
"windows-sys 0.59.0",
]
[[package]]
name = "zbus"
version = "4.4.0"

View file

@ -1,5 +1,5 @@
[package]
name = "yt-offline"
name = "catacomb"
version = "0.1.0"
edition = "2021"
description = "Self-hosted archive for YouTube, TikTok, Twitch, Vimeo, Bandcamp, SoundCloud, Odysee and more. Desktop GUI + web UI, bundled yt-dlp with curl_cffi impersonation, Plex export, SQLite-backed resume tracking. AGPL-3.0."
@ -97,10 +97,10 @@ priority = "optional"
depends = "libc6, libxcb1, yt-dlp, ffmpeg, mpv, xdg-utils"
recommends = "libnotify4"
assets = [
["target/release/yt-offline", "usr/bin/", "755"],
["youtube-backup.desktop", "usr/share/applications/yt-offline.desktop", "644"],
["icon.png", "usr/share/pixmaps/yt-offline.png", "644"],
["README.md", "usr/share/doc/yt-offline/README.md", "644"],
["target/release/catacomb", "usr/bin/", "755"],
["catacomb.desktop", "usr/share/applications/catacomb.desktop", "644"],
["icon.png", "usr/share/pixmaps/catacomb.png", "644"],
["README.md", "usr/share/doc/catacomb/README.md", "644"],
]
# ── Fedora / RHEL / openSUSE package (cargo-generate-rpm) ─────────────────────
@ -113,9 +113,9 @@ license = "AGPLv3"
# Fedora; ffmpeg lives in RPM Fusion but we still declare the dep.
requires = { yt-dlp = "*", ffmpeg = "*", mpv = "*", "xdg-utils" = "*" }
assets = [
{ source = "target/release/yt-offline", dest = "/usr/bin/yt-offline", mode = "755" },
{ source = "youtube-backup.desktop", dest = "/usr/share/applications/yt-offline.desktop", mode = "644" },
{ source = "icon.png", dest = "/usr/share/pixmaps/yt-offline.png", mode = "644" },
{ source = "README.md", dest = "/usr/share/doc/yt-offline/README.md", mode = "644" },
{ source = "LICENSE", dest = "/usr/share/licenses/yt-offline/LICENSE", mode = "644" },
{ source = "target/release/catacomb", dest = "/usr/bin/catacomb", mode = "755" },
{ source = "catacomb.desktop", dest = "/usr/share/applications/catacomb.desktop", mode = "644" },
{ source = "icon.png", dest = "/usr/share/pixmaps/catacomb.png", mode = "644" },
{ source = "README.md", dest = "/usr/share/doc/catacomb/README.md", mode = "644" },
{ source = "LICENSE", dest = "/usr/share/licenses/catacomb/LICENSE", mode = "644" },
]

163
HANDOFF.md Normal file
View file

@ -0,0 +1,163 @@
# Session handoff — Catacomb
> Working notes for continuing this work in another session/agent (e.g. Codex).
> Written 2026-06-19. Delete or update freely; this is not a tracked spec.
## TL;DR — where things are
- Project: `catacomb` — one Rust binary that is **both** an egui desktop GUI
and an axum web server wrapping `yt-dlp`. See [CLAUDE.md](CLAUDE.md) for the
authoritative architecture; [ROADMAP.md](ROADMAP.md) for the plan.
- Recent work has been almost entirely on the **web UI** (`src/web_ui/index.html`,
a single `include_str!`-baked SPA) plus a few `web.rs`/`database.rs` changes.
- Branch: `main`. Last commit: `2f95e7f` (unified cinematic video player).
- A dev web server is usually running on **:8081** against the user's real
library. It keeps getting reaped between turns — just restart it (see below).
## How to build / run / test
```bash
cargo build --release # ~1.5 min (opt-level=3 + thin LTO)
cargo test --release # 128 unit + 11 integration (tests/api.rs); no network
./target/release/catacomb --web 8081 # headless web server (what the user uses)
./target/release/catacomb # desktop GUI (default)
```
### CRITICAL gotchas (learned this session)
1. **The web SPA is one big embedded file**`src/web_ui/index.html`, baked in
at compile time via `include_str!`. **Editing it requires a `cargo build`** to
take effect. A JS syntax error there will NOT be caught by `cargo build` (the
HTML is just a string), so after every edit:
```bash
awk '/<script>/{f=1;next}/<\/script>/{f=0}f' src/web_ui/index.html > /tmp/spa.js && node --check /tmp/spa.js
```
2. **DB location**: the server opens the DB at `channels_root.join("catacomb.db")`
(`web.rs` ~L3127), i.e. **`<backup.directory>/catacomb.db`**. For this user
that's `/mnt/InannaBeloved/youtube-backup/catacomb.db` (~64 MB, real data —
do NOT clobber). `config.toml` is read from the **process CWD**. A stray
0-byte `catacomb.db` may sit in the repo dir; it is unused — ignore it.
3. **Offline-first**: this is a self-hosted, possibly-no-internet archiver.
**Never load fonts/CSS/JS from a CDN.** UI fonts are embedded as base64 woff2
(SIL OFL) directly in the `<style>` block. Keep it that way.
4. **All 10 themes** are driven by 7 CSS variables (`--bg --panel --card --accent
--text --muted --border`) set per `.theme-*` class. Any new UI must use those
vars (and the design-layer tokens below) so every theme inherits it.
5. **Background server reaping**: in the sandbox, `run_in_background` servers get
killed between turns and shell `&`/`nohup` is unreliable. Just relaunch
`./target/release/catacomb --web 8081` each time you need it up. `pkill -f`
first to avoid a port clash.
6. **Verifying UI visually without a browser driver** (no puppeteer/playwright):
extract the relevant CSS/JS from `index.html` into a `/tmp/*.html` harness with
a mock payload and screenshot with headless chromium:
```bash
chromium --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
--window-size=1100,1300 --virtual-time-budget=3000 \
--screenshot=/tmp/out.png "file:///tmp/harness.html"
```
Note: `--virtual-time-budget` can capture entry animations mid-flight; add a
"settle" override (disable transitions, force final state) for a clean shot.
## Settings flow (the easy thing to get wrong)
Adding a setting touches **five** places (grep an existing one like
`dedup_enabled` or `sponsorblock_mode` end-to-end first):
1. `config.rs` — field + `Default` + `default_with_dir()`.
2. `download_options.rs``Option<…>` per-channel override (None = use global).
3. `downloader.rs` — resolver merging global+override + a `pub` field on `Downloader`.
4. Both UIs: `app.rs` (egui Settings + channel-options dialog) AND
`web_ui/index.html` (Settings modal + channel dialog) AND `web.rs`
`SettingsPayload` (GET reads config, POST writes config + pushes to live `Downloader`).
5. Seed the `Downloader` field at construction AND on settings-save, in BOTH
`app.rs` and `web.rs`.
## What this session shipped (committed)
Newest first (all on `main`):
- `2f95e7f` **Unified cinematic video player.** Replaced native `<video controls>`
(direct `/files/` playback) with ONE custom player for both direct + transcode.
Custom scrubber (played/buffered/chapter-ticks/hover tooltip, drag-to-seek via
pointer events, commits on release), playback-speed popover (0.52×), persistent
volume/speed/captions (localStorage `plPrefs`), CC toggle, PiP, fullscreen,
auto-hiding controls + center play/pause flash, expanded keyboard
(space/k j/l ←→ ↑↓ m c p f `<` `>` 09). All seeking routes through the
existing `playerSeek`/`effTime` so the transcode reload-at-offset path is intact.
CSS prefix `.pl-*`; JS around `playVideo`/`updateVctrl`/`plBindScrubber`.
- `816da05` **Persist login sessions in SQLite.** New `sessions(token, issued_at)`
table; in-memory map switched `Instant``u64` unix secs; insert on login, delete
on logout, clear on password change, rehydrate at startup via `load_sessions()`.
Fixes "restart logs everyone out". (`database.rs` + `web.rs`.)
- `dd48e89` **api() 401 → reload to login** instead of a cryptic "error" toast.
- `615c088` **Maintenance modal → "diagnostics console"** (CSS prefix `.mx-*`):
pulsing health verdict + status tiles + instrument-panel sections. Presentation
only; all handler ids/classes (`dup-chk` `sim-chk` `at-chk` `dedup-area`
`autotag-area`, polling) untouched.
- `6f61821` **Stats modal → "observatory" dashboard** (CSS prefix `.sx-*`):
count-up metric cards, self-drawing SVG area chart (Catmull-Rom), growing
histogram, ranked leaderboard with Size/Count toggle.
- `c8cb700` **"Cinematheque" reskin**: embedded Instrument Serif + Hanken Grotesk
(base64 OFL), accent aurora + film-grain atmosphere, cinematic card hover,
recording-dot wordmark. Design-layer CSS appended after the functional rules so
it overrides by source order; everything flows through the theme variables.
- `983864f` **macOS osxcross packaging path** (`scripts/package.sh mac``.app`
zip; local-only, not in CI — needs osxcross + SDK).
- `207013e` **Windows shippable**: cross-compiled `.zip` via mingw + console
reattach fix (`attach_windows_console` in `main.rs`) + Windows build in the
Forgejo release CI.
- `8b25787` **Library sorting**: download-date sort (file mtime) + grouped sort
options, both UIs.
- earlier: `015d037` scan leaves one core free; `5ffdb17` download-modal
diff-aware repaint + Retry-all; `9ed6293` download cancel/retry/queue + dedup
off-switch.
### Design-layer conventions (web UI)
- CSS is appended near the end of the `<style>` block, AFTER the functional
rules, so equal-specificity overrides win by source order. Prefixes:
`.sx-*` = stats observatory, `.mx-*` = maintenance console, `.pl-*` = player.
- Reveal animations gate behind a class added post-layout (e.g. `.sx-go`) so they
play once per open, not on every re-render. All honor
`@media (prefers-reduced-motion: reduce)` (a global rule at the end of `<style>`).
- Fonts: `--font-display` (Instrument Serif), `--font-body` (Hanken Grotesk).
Extra tokens: `--radius`, `--radius-sm`, `--ring`, `--glow`, `--shadow`, `--hair`.
## Uncommitted changes (REVIEW + COMMIT before continuing)
`git status` shows modified-but-uncommitted (mostly docs, made by the user/linter
— do NOT revert, just review and commit):
- Docs/prose: `AGENTS.md`, `CLAUDE.md`, `README.md`, `ROADMAP.md`,
`SECURITY_AUDIT.md`, `docs/src/{architecture,first-run,installation,packaging}.md`
- Small code touches: `src/database.rs` (~4 lines — schema doc/comment),
`src/web_ui/index.html` (6/6 — minor).
Run `git diff` to confirm these are intended, then commit them. They look like
documentation catch-up for the session's features.
## Suggested next steps (pick up here)
Highest-leverage, all buildable + verifiable locally:
1. **Login page reskin**`LOGIN_HTML` in `web.rs` is a plain form and is the one
surface that still clashes with the cinematheque aesthetic. Small, high impact.
2. **Search overlay** (`openSearch` in the SPA) — could become a command-palette
experience to match the new players.
3. Commit the pending doc changes (above).
Roadmap "surpass" items still open (see [ROADMAP.md](ROADMAP.md) §3):
- **3.1 macOS binary** — osxcross scaffolding done (`scripts/package.sh mac`);
needs the toolchain + SDK installed, then verify. Windows already ships.
- **3.2 Android client** (big), **3.8 plugin/scripting hooks** (architectural).
- Phase 4 blue-sky: AI transcript summarisation (FTS transcript index already
exists), TV-mode layout, multi-user accounts.
- Federation follow-up (3.5): in-UI "add remote" editor (peers are config-only).
## Watch-outs / open questions
- The video player's seek/speed/captions are verified by code-path review +
static chrome screenshot, NOT live playback (no headless video). Worth a real
click-through in the app.
- Session persistence: the user's *current* browser cookie predates the
persistence commit, so they must log in once more; logins after that survive
restarts. (They may have a download password set or not — it has toggled during
testing; that's user-driven, not a bug.)
- Don't commit `cookies.txt`, `config.toml`, or `catacomb.db` (all gitignored;
contain creds / the Argon2 password hash).

View file

@ -1,5 +1,5 @@
# Maintainer: InannaBeloved <anassaeneroi@pm.me>
pkgname=yt-offline
pkgname=catacomb
pkgver=0.1.0
pkgrel=1
pkgdesc="Self-hosted archive for YouTube, TikTok, Twitch, Vimeo, Bandcamp, SoundCloud, Odysee and more. Desktop GUI + web UI."
@ -23,7 +23,10 @@ makedepends=('rust' 'cargo')
# previously broke the rusqlite bundled-sqlite link; thin LTO in Cargo
# does the cross-crate inlining we actually want.
options=('!lto')
source=("git+https://codeberg.org/anassaeneroi/yt-offline.git#branch=main")
# Force the checkout dir to $pkgname (catacomb); the Codeberg repo is still
# named yt-offline, so without the `$pkgname::` prefix the clone dir wouldn't
# match the `cd "$pkgname"` below.
source=("$pkgname::git+https://codeberg.org/anassaeneroi/yt-offline.git#branch=main")
sha256sums=('SKIP')
pkgver() {
@ -44,9 +47,9 @@ build() {
package() {
cd "$pkgname"
install -Dm755 target/release/yt-offline "$pkgdir/usr/bin/yt-offline"
install -Dm644 youtube-backup.desktop "$pkgdir/usr/share/applications/yt-offline.desktop"
install -Dm644 icon.png "$pkgdir/usr/share/pixmaps/yt-offline.png"
install -Dm755 target/release/catacomb "$pkgdir/usr/bin/catacomb"
install -Dm644 catacomb.desktop "$pkgdir/usr/share/applications/catacomb.desktop"
install -Dm644 icon.png "$pkgdir/usr/share/pixmaps/catacomb.png"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
}

View file

@ -1,6 +1,6 @@
# yt-offline
# Catacomb
![yt-offline icon](icon.png)
![catacomb 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
@ -9,7 +9,7 @@ the source video has been taken down.
Built on [yt-dlp](https://github.com/yt-dlp/yt-dlp); written in Rust.
📖 **Documentation:** <https://anassaeneroi.codeberg.page/yt-offline/> — setup,
📖 **Documentation:** <https://anassaeneroi.codeberg.page/catacomb/> — setup,
first run, the anti-bot stack, troubleshooting, and architecture.
## What it backs up
@ -65,10 +65,11 @@ exact URL to refresh from.
- **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.
- **Web UI** plays videos inline in any browser with one custom player for
direct files and transcoded streams: speed control, caption toggle,
keyboard seeking, PiP, fullscreen, saved volume/speed, a **chapters**
panel, and a searchable **transcript** pane. Optional on-the-fly
**ffmpeg transcoding** covers 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.
@ -111,12 +112,13 @@ exact URL to refresh from.
- **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`
- **Crash log** — any thread panic is appended to `catacomb.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).
flags / folders / notes). The DB is plain SQLite, so treat backups as
sensitive.
- Poison-recovering locks keep one handler's panic from taking down the
long-running web server.
@ -127,8 +129,8 @@ exact URL to refresh from.
- **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).
tokens persisted across restarts, 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
@ -166,15 +168,15 @@ plus three Emo/Scene flavours (Nocturnal, Coffin, Scene Queen).
## Quick start
```bash
git clone https://codeberg.org/anassaeneroi/yt-offline
cd yt-offline
git clone https://codeberg.org/anassaeneroi/catacomb
cd catacomb
cargo build --release
# Desktop GUI
./target/release/yt-offline
./target/release/catacomb
# Or web server only (headless)
./target/release/yt-offline --web 8080
./target/release/catacomb --web 8080
```
On first run a `config.toml` is created in the working directory. Settings
@ -184,8 +186,8 @@ can also be changed from inside either UI — no manual editing required.
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`.
3. The installer creates `~/.local/share/catacomb/venv/` with
`yt-dlp[default]` + `curl_cffi`, plus `~/.local/share/catacomb/bin/deno`.
Progress streams into a regular job entry.
Updates are the same button. Switching to **System** uses whatever `yt-dlp`
@ -197,8 +199,8 @@ is on PATH instead.
```bash
sudo pacman -S --needed rust mpv # python3 + python-pip already present
git clone https://codeberg.org/anassaeneroi/yt-offline
cd yt-offline
git clone https://codeberg.org/anassaeneroi/catacomb
cd catacomb
makepkg -si # or: cargo build --release
```
@ -236,8 +238,8 @@ 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
git clone https://codeberg.org/anassaeneroi/catacomb
cd catacomb
cargo build --release
```
@ -276,7 +278,7 @@ interval_hours = 24
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
source_url = "https://codeberg.org/anassaeneroi/catacomb" # AGPL §13 footer link
[plex]
library_path = "/path/to/plex/TV/youtube" # leave unset to disable
@ -342,7 +344,7 @@ the session cookie is HttpOnly + SameSite=Strict, valid 30 days.
├── music/
│ └── <artist>/
│ └── Track Title [xyz].opus
└── yt-offline.db ← watched + positions + password hash
└── catacomb.db ← plain SQLite app state (watched, positions, password hash, sessions, caches)
```
## Troubleshooting
@ -360,7 +362,7 @@ switch to **Bundled** in Settings.
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
That's usually transcoding mode (`web.transcode = true`). catacomb 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

View file

@ -9,7 +9,7 @@ configuration surface lives at [`docs/tartube-spec.md`](docs/tartube-spec.md);
it's what the Phase 1 parity work traced back to.
Tartube is the mature open-source yt-dlp GUI and the obvious benchmark for a
project in this space. yt-offline has architectural advantages Tartube can't
project in this space. catacomb has architectural advantages Tartube can't
catch up on quickly (Rust + axum + a real web UI + bundled toolchain + a
modern security model). **As of 2026-06 we're at feature parity** — every
Tartube subsystem is matched or led; the remaining gap is years-of-edge-
@ -78,17 +78,19 @@ Every Phase 2 item is now done — 2.1 / 2.2 shipped alongside 2.3 / 2.4 /
### 2.1 Integration test coverage — DONE
98 unit tests cover parsers/helpers/resolvers; `tests/api.rs` adds 7
end-to-end tests that spawn the **real** `--web` binary against a scratch
tempdir and drive the HTTP API with curl (index/library serving, ETag
304, settings round-trip + persistence, folders CRUD + cycle guard, notes
round-trip, channel-options round-trip + clear, DB backup). Each test
gets its own server/port/tempdir, so they run in parallel — `cargo test`
runs the lot. (A `.forgejo/workflows/test.yml` CI definition exists, but
Codeberg executes Woodpecker, not Forgejo Actions, so it's inert there
without a self-hosted runner; tests run locally.) Stretch left: a
recorded-fixture corpus for the download pipeline and a headless web-UI
test.
128 passing unit tests cover parsers/helpers/resolvers, plus one ignored
real-ffmpeg fingerprint check for opt-in local validation. `tests/api.rs`
adds 11 end-to-end tests that spawn the **real** `--web` binary against a
scratch tempdir and drive the HTTP API with curl (index/library serving,
ETag 304, settings round-trip + persistence, folders CRUD + cycle guard,
notes round-trip, channel-options round-trip + clear, DB backup, full-text
search, podcast/feed-token auth, RSS enclosures, and perceptual dedup).
Each test gets its own server/port/tempdir, so they run in parallel —
`cargo test --release` runs the lot. (A `.forgejo/workflows/test.yml` CI
definition exists, but Codeberg executes Woodpecker, not Forgejo Actions,
so it's inert there without a self-hosted runner; tests run locally.)
Stretch left: a recorded-fixture corpus for the download pipeline,
a restart-persistent-session regression test, and a headless web-UI test.
### 2.2 Documentation site — DONE
@ -130,10 +132,10 @@ Once we're at parity, we push past Tartube on its own ground.
**Windows is now a shippable artifact.** `cargo build --release --target
x86_64-pc-windows-gnu` doesn't just check — it *links* a working 22 MB
`yt-offline.exe` via mingw-w64 (only the upstream egui `f32: From<f64>`
`catacomb.exe` via mingw-w64 (only the upstream egui `f32: From<f64>`
warnings). `scripts/package.sh win` (and `all`, when the cross toolchain
is present) cross-compiles and bundles it into
`yt-offline-<ver>-x86_64-windows.zip` (exe + LICENSE + a README listing
`catacomb-<ver>-x86_64-windows.zip` (exe + LICENSE + a README listing
the yt-dlp/ffmpeg/mpv PATH deps). The Forgejo release workflow installs
`mingw-w64` + `zip` + the rust target so a tag push produces the Windows
zip alongside the Linux packages — all from the one Linux container, no
@ -156,7 +158,7 @@ Linux; `disk_space` (`statvfs`), `plex` (symlinks, with a Windows
**macOS has a packaging path, pending the toolchain.** `scripts/package.sh
mac` cross-compiles an Apple target (`MAC_ARCH=arm64` default, or
`x86_64`) via osxcross, assembles an unsigned `yt-offline.app`
`x86_64`) via osxcross, assembles an unsigned `catacomb.app`
(`Info.plist` + Mach-O + best-effort `.icns`), and zips it. The crypto
stack is `ring`, which builds against the osxcross SDK fine. It's gated on
the operator having osxcross built with a macOS SDK on PATH (the SDK comes
@ -245,15 +247,47 @@ surfaced as a 🔍 search in both UIs. Plus a searchable, click-to-seek
current line) and a floating desktop window that seeks mpv over IPC —
backed by a shared `vtt` WebVTT/SRT parser.
## Phase 4 — Stretch / blue-sky
## Phase 4 — Long-Horizon Bets
Probably never, or much later.
Phase 4 is where bigger strategic bets live after Phase 3's "surpass"
work is banked. These are not required for the current product to be
healthy; they either change the deployment model, add a new consumption
surface, or raise the security/privacy bar enough to deserve their own
design pass.
- A web UI built around a "TV mode" remote-friendly layout.
- AI summarisation of videos (transcript → bullet points).
- Multi-user accounts with per-user watched/positions (currently single-user).
- Integration with Plex / Jellyfin / Kodi as a *source plugin* rather than a
symlink generator.
### 4.1 Living-room mode
A remote-friendly web UI for couch/TV use: large focus states, D-pad
navigation, queue/playlist controls, "continue watching" as the default
view, and no dense maintenance/settings chrome.
### 4.2 Local intelligence over the archive
Transcript-derived summaries, topic extraction, "find the clip where..."
search, and maybe offline embedding indexes. This should stay optional and
local-first; no cloud dependency should become required for normal archive
use.
### 4.3 Multi-user library state
Per-user watched state, resume positions, flags, notes, and maybe
permissions. This is a schema/auth redesign, not a bolt-on setting, because
the app is currently intentionally single-user.
### 4.4 At-rest privacy hardening
Encrypted `catacomb.db` or encrypted backups. This likely means SQLCipher
or an export/import backup format, plus key management, migration from
plaintext DBs, and packaging validation across Linux/Windows/macOS. Until
that design exists, the DB remains plain SQLite protected by filesystem
permissions.
### 4.5 Media-server integration as a real source
Plex / Jellyfin / Kodi integration as a plugin/source surface rather than
only a symlink/NFO generator. The goal would be first-class browsing and
playback integration without making the archive layout depend on one media
server's conventions.
## Recently shipped (highlights)
@ -263,6 +297,13 @@ Roughly reverse-chronological. Items that closed out a roadmap line.
`start=` offset and the web player uses custom controls for live ffmpeg
streams, so MKV/H.264 transcode mode can scrub, resume, jump chapters,
and sync transcript highlights even though the pipe has no byte ranges.
- **Unified web video player** — direct `/files/` playback and live
`/api/transcode/` streams now share one custom player with speed control,
caption toggle, keyboard seeking, PiP, fullscreen, saved volume/speed, and
chapter ticks.
- **Restart-safe web sessions** — login tokens are mirrored into SQLite and
rehydrated at startup, so a server restart or upgrade no longer logs out
active browsers; password changes and logout still clear sessions.
- **Federation / multi-host** (3.5) — read-only peer libraries with
tokenized direct media URLs, so videos stream from the peer rather than
proxying through this instance.
@ -345,7 +386,10 @@ Roughly reverse-chronological. Items that closed out a roadmap line.
search + transcript tooling (3.9), and the **Windows** half of 3.1
(cross-compiled .zip + release CI). Still open: a **macOS** binary
(3.1; needs a Mac runner), Android (3.2), and plugin hooks (3.8).
- **Phase 4** items might be valuable, but commit to nothing.
- **Phase 4** is reserved for bigger future bets: living-room mode, local
archive intelligence, multi-user state, at-rest privacy hardening, and
deeper media-server integration. Treat these as design projects, not
quick backlog items.
Items inside a phase are loosely ordered by user-visible impact, not strict
prerequisite. With Windows banked, the highest-leverage remaining moves are

View file

@ -1,6 +1,7 @@
# Security Audit — yt-offline
# Security Audit — catacomb
**Date:** 2026-05-23 (re-audit after security hardening commit `5999673`)
**Docs refreshed:** 2026-06-18 (persisted sessions / DB backup caveat)
**Earlier audit:** 2026-05-17 (commit pre-dating session auth, CSP, SHA-256 verify, etc.)
**Scope:** Rust codebase + embedded web UI + dependencies
**Threat model:** Self-hosted personal archiving tool. Primary deployment is
@ -49,7 +50,7 @@ interpretation. Sites audited:
- yt-dlp downloads: `src/downloader.rs:start`, `start_music`, `repair`
- yt-dlp preview: `src/web.rs:get_preview` (now goes through `apply_cookie_flags` and respects bundled-binary setting)
- yt-dlp bundled install: `src/ytdlp_bin.rs:install_command` runs `bash -c` with a static script whose only interpolated values are paths under `~/.local/share/yt-offline/` and constant GitHub URLs — no user input flows in.
- yt-dlp bundled install: `src/ytdlp_bin.rs:install_command` runs `bash -c` with a static script whose only interpolated values are paths under `~/.local/share/catacomb/` and constant GitHub URLs — no user input flows in.
- ffmpeg transcoder: `src/web.rs:get_transcode` (kill_on_drop set; child terminates when stream is dropped).
Path interpolation in the install script uses single quotes around
@ -84,7 +85,11 @@ the `settings` table was added.
- Argon2-hashed password in the `settings` table (never in `config.toml`).
- 256-bit `rand::thread_rng()` (CSPRNG) session tokens, hex-encoded.
- `Mutex<HashMap<String, Instant>>` of issued tokens, lazily pruned past 30-day TTL — no unbounded growth.
- Runtime `Mutex<HashMap<String, u64>>` of issued tokens, lazily pruned past
30-day TTL — no unbounded growth.
- Session tokens are mirrored to the SQLite `sessions` table and rehydrated
at startup, so server restarts/upgrades do not log users out. Logout and
password changes delete the corresponding persisted sessions.
- `Set-Cookie` includes `HttpOnly`, `SameSite=Strict`, `Path=/`, `Max-Age=2592000`. `Secure` is added when `X-Forwarded-Proto: https` is present (so reverse-proxied deployments get it; plain-HTTP LAN does not, which is correct since browsers would otherwise refuse to send the cookie).
- All routes layered behind `auth_middleware` when a password is configured. `/api/login` and unauthenticated `GET /` (serves login page) are the only exceptions.
- `password_required` is cached in an `AtomicBool` so the auth middleware doesn't hit SQLite on every static-file fetch.
@ -125,9 +130,9 @@ unencrypted over the network. Defenses:
**Mitigation if you need stronger guarantees:** put a reverse proxy with TLS in front (Caddy, nginx, Traefik) and set the `X-Forwarded-Proto` header. The app will then issue cookies with the `Secure` attribute automatically.
### ⚠️ `cookies.txt` and `yt-offline.db` file permissions
### ⚠️ `cookies.txt` and `catacomb.db` file permissions
- `yt-offline.db`: now `chmod 0600` at open time on Unix (`src/database.rs`). Contains the Argon2 password hash and resume positions. Other local users on the same machine cannot read it.
- `catacomb.db`: now `chmod 0600` at open time on Unix (`src/database.rs`). It is **plain SQLite**, not encrypted. It contains the Argon2 password hash, watched/resume state, notes/options/flags, derived caches, and restart-persistent web session tokens. Other local users on the same machine cannot read it under normal Unix permissions, but copied DB files and downloaded DB backups remain sensitive.
- `cookies.txt`: not auto-chmodded because the user pastes / file-picks it in, and our `write_cookies()` uses `std::fs::write` which respects the user's umask. **Recommendation in README:** `chmod 600 cookies.txt` after import. (Tracking issue for future automation.)
### ⚠️ Job log echoes yt-dlp stderr verbatim
@ -164,7 +169,7 @@ root, at which point they own the data anyway. Accepted.
| Optional HTTP password auth | ✅ added (Argon2 + sessions + rate limit) |
| Filter yt-dlp stderr | ⚠️ still echoed (accepted) |
| chmod 600 on cookies.txt | ⚠️ documented; not automated |
| chmod 600 on yt-offline.db | ✅ added (`Database::open`) |
| chmod 600 on catacomb.db | ✅ added (`Database::open`) |
| Session token TTL + GC | ✅ added |
| `Secure` cookie flag for HTTPS | ✅ added (gated on `X-Forwarded-Proto`) |
| Login rate limiting | ✅ added (5 / 60 s per IP) |
@ -194,6 +199,13 @@ These are nice-to-haves; none are blocking the current deployment.
7. **TLS terminator in-process.** Optional `rustls` integration so the
app can serve HTTPS without a reverse proxy. Currently considered
out of scope.
8. **Hash persisted session tokens or sanitize DB backups.** The current
`sessions` table stores bearer tokens so restart-safe sessions work; a
copied DB backup can therefore carry live web sessions until logout,
password change, or TTL expiry.
9. **Optional at-rest DB encryption.** Tracked as a Phase 4 design item,
not an active hardening requirement. It needs key-management, migration,
backup/restore, and packaging decisions.
---

View file

@ -1,10 +1,10 @@
[Desktop Entry]
Type=Application
Name=YT Offline
Name=Catacomb
GenericName=Video Archiver
Comment=Self-hosted archive for YouTube, TikTok, Twitch, and more
Exec=yt-offline
Icon=yt-offline
Exec=catacomb
Icon=catacomb
Categories=AudioVideo;Video;Network;
Keywords=youtube;yt-dlp;archive;backup;download;
StartupNotify=true

View file

@ -1,4 +1,4 @@
# Packaging yt-offline
# Packaging Catacomb
This document covers building distributable packages. The one-liner:
@ -23,7 +23,7 @@ on demand. Runtime deps declared: `yt-dlp`, `ffmpeg`, `mpv`, `xdg-utils`,
```sh
scripts/package.sh deb
sudo apt install ./dist/yt-offline_*_amd64.deb
sudo apt install ./dist/catacomb_*_amd64.deb
```
### .rpm (Fedora / RHEL / openSUSE)
@ -34,7 +34,7 @@ already-built (and release-profile-stripped) binary.
```sh
scripts/package.sh rpm
sudo dnf install ./dist/yt-offline-*.x86_64.rpm
sudo dnf install ./dist/catacomb-*.x86_64.rpm
```
Note: `ffmpeg` on Fedora lives in [RPM Fusion](https://rpmfusion.org/);
@ -50,8 +50,8 @@ would balloon the image and tangle the licensing.
```sh
scripts/package.sh appimage
chmod +x dist/yt-offline-*-x86_64.AppImage
./dist/yt-offline-*-x86_64.AppImage
chmod +x dist/catacomb-*-x86_64.AppImage
./dist/catacomb-*-x86_64.AppImage
```
## Arch Linux
@ -82,11 +82,11 @@ sudo apt install mingw-w64 zip # or your distro's equivalent
scripts/package.sh win
```
This produces `dist/yt-offline-<ver>-x86_64-windows.zip` containing
`yt-offline.exe`, `LICENSE.txt`, and a `README.txt` listing the runtime
This produces `dist/catacomb-<ver>-x86_64-windows.zip` containing
`catacomb.exe`, `LICENSE.txt`, and a `README.txt` listing the runtime
PATH deps (yt-dlp / ffmpeg / mpv). Release builds link as a GUI-subsystem
app (no console window); the binary reattaches to the launching terminal
at runtime (`attach_windows_console` in `main.rs`) so `yt-offline.exe
at runtime (`attach_windows_console` in `main.rs`) so `catacomb.exe
--web 8080` from PowerShell prints its logs, while a double-click stays
windowless.
@ -110,12 +110,12 @@ rustup target add aarch64-apple-darwin # or x86_64-apple-darwin
MAC_ARCH=arm64 scripts/package.sh mac # arm64 (default) | x86_64
```
This cross-compiles, assembles an unsigned `yt-offline.app`
This cross-compiles, assembles an unsigned `catacomb.app`
(`Info.plist` + the Mach-O binary; an `.icns` icon too if `png2icns` is
available), and zips it to `dist/yt-offline-<ver>-<arch>-macos.zip`. The
available), and zips it to `dist/catacomb-<ver>-<arch>-macos.zip`. The
`.app` is **not codesigned or notarized**, so on the target Mac it must be
opened the first time via right-click → Open, or cleared with `xattr -dr
com.apple.quarantine yt-offline.app`. Runtime deps (yt-dlp / ffmpeg / mpv)
com.apple.quarantine catacomb.app`. Runtime deps (yt-dlp / ffmpeg / mpv)
are expected on PATH as on the other platforms.
A `.dmg` and codesigning/notarization (and a `tray-icon`-based macOS tray)

View file

@ -1,5 +1,5 @@
[book]
title = "yt-offline"
title = "Catacomb"
description = "Self-hosted archive for YouTube, TikTok, Twitch and more. Setup, usage, anti-bot, troubleshooting, and architecture."
authors = ["InannaBeloved"]
language = "en"
@ -8,5 +8,5 @@ src = "src"
[output.html]
default-theme = "navy"
preferred-dark-theme = "navy"
git-repository-url = "https://codeberg.org/anassaeneroi/yt-offline"
edit-url-template = "https://codeberg.org/anassaeneroi/yt-offline/_edit/main/docs/{path}"
git-repository-url = "https://codeberg.org/anassaeneroi/catacomb"
edit-url-template = "https://codeberg.org/anassaeneroi/catacomb/_edit/main/docs/{path}"

View file

@ -2,7 +2,7 @@
[Introduction](./introduction.md)
# Using yt-offline
# Using catacomb
- [Installation](./installation.md)
- [First run & configuration](./first-run.md)

View file

@ -1,7 +1,7 @@
# Staying ahead of YouTube's bot detection
YouTube increasingly fingerprints and rate-limits automated clients.
yt-offline ships a layered defense; understanding the layers makes the
catacomb ships a layered defense; understanding the layers makes the
difference between "everything downloads" and "constant captchas."
In rough order of impact:
@ -10,13 +10,13 @@ In rough order of impact:
This is the single biggest factor. **Anonymous requests get captcha-
walled the hardest.** Provide a `cookies.txt` exported from a browser
where you are **signed in to YouTube**, or point yt-offline at the
where you are **signed in to YouTube**, or point catacomb at the
browser profile directly.
A *valid* logged-in jar contains the auth cookies `SID`, `SAPISID`,
`__Secure-1PSID`, `__Secure-3PSID`, `LOGIN_INFO`, etc. A jar with only
`VISITOR_INFO1_LIVE`, `PREF`, `YSC` is **anonymous** — it's not signed in
and actually makes detection *worse* than no cookies. yt-offline's
and actually makes detection *worse* than no cookies. catacomb's
Settings → Cookies panel warns you when your jar is anonymous or expired.
Two ways to supply cookies:
@ -37,7 +37,7 @@ Two ways to supply cookies:
subdirectory itself). The advantage: cookies are read fresh from the
live session each download, so they never go stale.
> Cookies are session credentials — yt-offline never commits or transmits
> Cookies are session credentials — catacomb never commits or transmits
> `cookies.txt` unprompted, and redacts the cookie path out of any log
> line shown in the UI.
@ -45,7 +45,7 @@ Two ways to supply cookies:
yt-dlp's `--impersonate` makes requests carry a real browser's TLS
fingerprint (via `curl_cffi`), so the connection doesn't *look* like a
script. The bundled install sets this up automatically and yt-offline
script. The bundled install sets this up automatically and catacomb
picks an impersonation target per platform.
If impersonation silently does nothing, it's almost always a
@ -58,7 +58,7 @@ is present). See
## 3. POT tokens (Proof-of-Origin)
YouTube increasingly binds a per-video **Proof-of-Origin token** to
playback; without one, format URLs come back empty. yt-offline can run
playback; without one, format URLs come back empty. catacomb can run
[bgutil-pot](https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs), a
loopback HTTP server that mints these tokens, and point yt-dlp at it.
@ -68,14 +68,14 @@ yt-dlp; the matching plugin installs into its venv) and click **Install**.
> **Version-skew footgun:** the yt-dlp plugin must come from the *same
> release* as the bgutil-pot server binary — **not** the PyPI package,
> which versions independently and silently produces no tokens on a
> mismatch. yt-offline's installer handles this by unpacking the
> mismatch. catacomb's installer handles this by unpacking the
> version-matched plugin zip from the server's release.
## 4. Player-client selection
YouTube cracks down on different internal "player clients" over time —
the `web` client is currently the most captcha-prone, while `tv` and
`mweb` are the least. yt-offline no longer forces `web`; it lets yt-dlp
`mweb` are the least. catacomb no longer forces `web`; it lets yt-dlp
pick good defaults. If a specific channel keeps hitting captchas, set a
client override (global or per-channel):
@ -85,7 +85,7 @@ tv,mweb
## 5. Throttling
A burst of ~30 rapid requests is a classic trip-wire. yt-offline inserts
A burst of ~30 rapid requests is a classic trip-wire. catacomb inserts
a small jittered pause between videos (a fixed cadence looks robotic; a
random one looks human) and, after any rate-limit hit, triples the
sleeps for the rest of the batch before recovering.

View file

@ -80,7 +80,7 @@ fallback.
## Anti-bot subsystems
`ytdlp_bin.rs` manages the optional self-contained venv at
`~/.local/share/yt-offline/` (nightly `yt-dlp[default]` + `curl_cffi` +
`~/.local/share/catacomb/` (nightly `yt-dlp[default]` + `curl_cffi` +
bundled `deno`). `pot_provider.rs` runs `bgutil-pot` for Proof-of-Origin
tokens — its yt-dlp plugin must come from the same release as the server
binary. `error_class.rs` pattern-matches yt-dlp stderr into actionable
@ -95,7 +95,7 @@ wall is RateLimited, not NotFound).
and drives the HTTP API with curl — genuine end-to-end coverage of the
axum + SQLite + config stack.
`cargo test` runs both. (A `.forgejo/workflows/test.yml` CI definition
`cargo test --release` runs both. (A `.forgejo/workflows/test.yml` CI definition
exists, but Codeberg runs Woodpecker rather than Forgejo Actions, so it
doesn't execute there without a self-hosted runner — run the suite
locally.)
@ -103,8 +103,9 @@ locally.)
## Platform support
Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only / no-GTK
by design — that's why packaging avoids a GTK dependency. Windows/macOS
aren't first-class yet: the tray needs a per-OS backend before a clean
cross-build. The rest (eframe/wgpu, axum, rusqlite-bundled) already
compiles cross-platform, and `ytdlp_bin` already has `cfg!(windows)`
branches.
by design — that's why packaging avoids a GTK dependency. Those Linux-only
pieces are target-gated: Windows builds use native file dialogs and a no-op
tray stub, and the repo now ships a cross-compiled Windows zip. macOS has a
local osxcross packaging path, but still needs on-hardware validation,
codesigning/notarization, and a real tray backend before it is a normal
release artifact.

View file

@ -3,7 +3,7 @@
## Starting a download
Paste any supported URL into the download bar (desktop) or the ⬇
Downloads modal (web). yt-offline classifies the URL by platform, routes
Downloads modal (web). catacomb classifies the URL by platform, routes
it to the right folder, and starts yt-dlp. A channel/playlist URL pulls
the whole thing; a single-video URL pulls just that one.

View file

@ -2,7 +2,7 @@
## The config file
yt-offline reads `config.toml` **from its working directory** (the
catacomb reads `config.toml` **from its working directory** (the
directory you launch it from), not a fixed path. The same goes for
`cookies.txt`. Everything in `config.toml` is also editable in Settings;
edits there are written back to the file.
@ -61,7 +61,7 @@ Everything nests under the one `backup.directory`:
music/ ← audio-only "Music mode" downloads, by artist
archive.txt ← yt-dlp's global download archive
cookies.txt ← optional, if you set one
yt-offline.db ← watched/positions/flags/folders/notes/cache + password hash
catacomb.db ← plain SQLite app state (watched/positions/flags/notes, password hash, sessions, caches)
```
Each creator folder gets a hidden `.source-url` sidecar so re-checks
@ -72,8 +72,8 @@ always know the exact URL to refresh from.
In **Settings → yt-dlp binary** you choose:
- **System** — uses whatever `yt-dlp` is on your `PATH`.
- **Bundled** — click **Install** and yt-offline builds a self-contained
venv at `~/.local/share/yt-offline/`: nightly `yt-dlp[default]` +
- **Bundled** — click **Install** and catacomb builds a self-contained
venv at `~/.local/share/catacomb/`: nightly `yt-dlp[default]` +
`curl_cffi` (TLS impersonation) + a bundled `deno` (player-JS). The
same button updates it later.
@ -83,8 +83,8 @@ also required for the [POT token provider](./anti-bot.md#3-pot-tokens-proof-of-o
## The two front-ends
- `yt-offline` — desktop GUI (eframe/egui).
- `yt-offline --web [PORT]` — headless web server. Bind to `127.0.0.1`
- `catacomb` — desktop GUI (eframe/egui).
- `catacomb --web [PORT]` — headless web server. Bind to `127.0.0.1`
(default) for localhost-only, a Tailscale address for your tailnet, or
`0.0.0.0` for the LAN. **Set a password** (Settings) before exposing it
beyond localhost — the UI and all `/api` routes are then gated behind an

View file

@ -1,6 +1,6 @@
# Installation
yt-offline is a single Rust binary. You can install a prebuilt package,
Catacomb is a single Rust binary. You can install a prebuilt package,
build from source, or grab the AppImage.
## Runtime dependencies
@ -8,7 +8,7 @@ build from source, or grab the AppImage.
Whichever way you install, these are invoked as subprocesses at runtime:
- **yt-dlp** — the download engine. You can use the system one *or* let
yt-offline manage a bundled copy (see [First run](./first-run.md)).
catacomb manage a bundled copy (see [First run](./first-run.md)).
- **ffmpeg** — muxing, format conversion, on-the-fly transcode for the
web player.
- **mpv** — the default desktop player (any player taking a file path
@ -21,14 +21,14 @@ Releases attach `.deb`, `.rpm`, and `.AppImage` artifacts.
```sh
# Debian / Ubuntu / Mint
sudo apt install ./yt-offline_*_amd64.deb
sudo apt install ./catacomb_*_amd64.deb
# Fedora / RHEL / openSUSE (ffmpeg via RPM Fusion)
sudo dnf install ./yt-offline-*.x86_64.rpm
sudo dnf install ./catacomb-*.x86_64.rpm
# Any Linux — AppImage
chmod +x yt-offline-*-x86_64.AppImage
./yt-offline-*-x86_64.AppImage
chmod +x catacomb-*-x86_64.AppImage
./catacomb-*-x86_64.AppImage
```
## Arch / CachyOS / Manjaro
@ -55,11 +55,11 @@ sudo apt install build-essential pkg-config curl git python3-venv \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
git clone https://codeberg.org/anassaeneroi/yt-offline
cd yt-offline
git clone https://codeberg.org/anassaeneroi/catacomb
cd catacomb
cargo build --release
./target/release/yt-offline # desktop GUI
./target/release/yt-offline --web 8080 # headless web server
./target/release/catacomb # desktop GUI
./target/release/catacomb --web 8080 # headless web server
```
`python3-venv` is only needed for the bundled-yt-dlp install path; skip
@ -67,7 +67,13 @@ it if you'll always use system yt-dlp.
## Windows / macOS
Not first-class yet. The Linux-only system tray (`ksni`) and file dialog
(`rfd` xdg-portal) need per-OS backends before a clean cross-build; the
rest of the stack already compiles. See the
[architecture notes](./architecture.md#platform-support).
Windows is available as a release zip when built through
`scripts/package.sh win` or tag-release CI. It includes `catacomb.exe`,
the license, and a short runtime-dependency README; install `yt-dlp`,
`ffmpeg`, and `mpv` / `mpv.net` on PATH.
macOS has a local osxcross packaging path (`scripts/package.sh mac`) that
assembles an unsigned `.app` zip when you provide an Apple SDK locally.
It is not built in public CI because the SDK cannot be redistributed, and
codesigning/notarization remain follow-ups. See
[packaging](./packaging.md) and the [architecture notes](./architecture.md#platform-support).

View file

@ -1,4 +1,4 @@
# yt-offline
# Catacomb
A self-hosted media archive for YouTube and friends. Paste any URL, it
routes the download to the right folder by source, tracks what you've
@ -24,7 +24,7 @@ single binary that is **both** a desktop app and a headless web server.
## Why it exists
Tartube is the mature open-source yt-dlp GUI and the benchmark in this
space. yt-offline matches its feature set while adding things Tartube
space. catacomb matches its feature set while adding things Tartube
doesn't have: a real web UI reachable from any device, a single-binary
distribution with a bundled toolchain, a modern security model
(password-gated UI, Argon2, rate-limited login), a built-in anti-bot

View file

@ -3,17 +3,20 @@
Build distributable Linux packages with one script:
```sh
scripts/package.sh all # .deb + .rpm + .AppImage → dist/
scripts/package.sh all # Linux packages + Windows zip when toolchains exist
scripts/package.sh deb # just the .deb
scripts/package.sh rpm
scripts/package.sh appimage
scripts/package.sh win # Windows .zip via mingw-w64
scripts/package.sh mac # unsigned macOS .app .zip via local osxcross SDK
```
It builds the release binary once and reuses it for every format,
installing `cargo-deb` / `cargo-generate-rpm` on demand and downloading
`appimagetool` to `dist/tools/` on first AppImage build. Per-format
failures are isolated and summarized at the end. Output (gitignored)
lands in `dist/`.
`appimagetool` to `dist/tools/` on first AppImage build. Windows is included
when the mingw target/toolchain is present; macOS is local-only because
osxcross needs an Apple SDK. Per-format failures are isolated and summarized
at the end. Output (gitignored) lands in `dist/`.
## Formats
@ -37,5 +40,5 @@ Actions — so they don't run there without a self-hosted runner. Until
then, build packages locally with `scripts/package.sh` and publish docs
with `scripts/publish-docs.sh`.
The repo's [`docs/PACKAGING.md`](https://codeberg.org/anassaeneroi/yt-offline/src/branch/main/docs/PACKAGING.md)
The repo's [`docs/PACKAGING.md`](https://codeberg.org/anassaeneroi/catacomb/src/branch/main/docs/PACKAGING.md)
has the per-distro install commands and the Windows/macOS status in full.

View file

@ -1,6 +1,6 @@
# Troubleshooting
yt-offline classifies failed downloads into one of nine classes and shows
catacomb classifies failed downloads into one of nine classes and shows
a one-line suggested fix next to the failed job. This page expands on the
most common ones, plus a few non-download issues.
@ -15,7 +15,7 @@ wall. In order of effectiveness:
2. **Switch to bundled (nightly) yt-dlp** if you're on system stable.
3. **Enable the POT token provider.**
4. **Try a player-client override** of `tv,mweb` for that channel.
5. If it's a one-off, just wait — yt-offline auto-retries transient
5. If it's a one-off, just wait — catacomb auto-retries transient
rate-limits after a cooldown.
## Impersonate targets show "(unavailable)"
@ -40,7 +40,7 @@ major versions are mismatched"* warning.
**Cause:** the yt-dlp plugin came from PyPI (Brainicism's package, which
versions independently) instead of the jim60105 Rust server's release.
**Fix:** re-run the POT **Install/Update** button — yt-offline installs
**Fix:** re-run the POT **Install/Update** button — catacomb installs
the version-matched plugin zip from the same release as the server
binary. Don't `pip install bgutil-ytdlp-pot-provider` yourself.
@ -68,14 +68,14 @@ see [Anti-bot](./anti-bot.md).
## Downloads stall forever
A job sits running with no progress. yt-offline's **hang watchdog**
A job sits running with no progress. catacomb's **hang watchdog**
auto-kills any job silent for 5 minutes and re-queues it, so this should
self-heal. If it recurs on a specific URL, it's usually a server-side
issue with that source; check the job log in the Downloads panel.
## Disk fills up / downloads fail with ENOSPC
yt-offline runs a **disk-full preflight** and refuses to start a download
catacomb runs a **disk-full preflight** and refuses to start a download
when the target filesystem has less than ~500 MB free, surfacing it as a
clear "disk full" failure rather than a half-written file. Free space and
retry.
@ -103,13 +103,13 @@ broken Vulkan stack, the OS can create the window while wgpu never
presents a frame. Try the OpenGL renderer:
```bash
YT_OFFLINE_RENDERER=glow yt-offline
YT_OFFLINE_RENDERER=glow catacomb
```
or:
```bash
yt-offline --renderer glow
catacomb --renderer glow
```
If that works, update/reinstall your Vulkan driver later and switch back
@ -129,6 +129,6 @@ UI until restarted.
- **The job log** — every download/transcode job keeps its full yt-dlp /
ffmpeg output in the Downloads panel (expand the job).
- **`yt-offline.crash.log`** — next to your `yt-offline.db`. A panic in
- **`catacomb.crash.log`** — next to your `catacomb.db`. A panic in
any thread (UI, web worker, download) is appended here with a
timestamp, so it survives a GUI launched without a terminal.

View file

@ -2,7 +2,7 @@
A structured analysis of [Tartube v2.5.231](https://github.com/axcore/tartube)
(2026-05-24), our project's primary feature benchmark. Written to give
yt-offline a concrete target to work toward — every section ends with a
catacomb a concrete target to work toward — every section ends with a
"What we can do" note about whether to adopt, adapt, or skip.
Tartube codebase: ~146,000 lines of Python 3 across 19 modules under
@ -452,7 +452,7 @@ The 791 boolean flags in `mainapp.py` cover all of these plus their
**What we can do:** A handful of these tabs map to features we
literally don't have (FFmpeg presets, comments, profiles, scheduling
calendar). They're roadmap items, not config surface. The actual
*configuration surface* of yt-offline is currently:
*configuration surface* of Catacomb is currently:
- backup dir, max concurrent, bundled-or-system yt-dlp
- player command, browser (for `--cookies-from-browser`)
@ -568,7 +568,7 @@ For the record (to keep momentum honest):
## 12. Specification for "Tartube parity" milestone
Working definition: a yt-offline release is "at Tartube parity" when
Working definition: a catacomb release is "at Tartube parity" when
the following are all true.
- [x] **Per-target download options** with cascade resolution and a

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build distributable packages for yt-offline.
# Build distributable packages for catacomb.
#
# Usage:
# scripts/package.sh [deb|rpm|appimage|win|mac|all]
@ -23,7 +23,7 @@
# macOS notes: cross-compiling for Apple targets needs osxcross
# (https://github.com/tpoechtrager/osxcross) built with a macOS SDK, and its
# wrapper compilers (oa64-clang / o64-clang) on PATH. Set MAC_ARCH=arm64
# (default) or x86_64. The result is an unsigned yt-offline.app inside a .zip;
# (default) or x86_64. The result is an unsigned catacomb.app inside a .zip;
# it is *not* codesigned or notarized, so on the target Mac it must be opened
# via right-click → Open (or `xattr -dr com.apple.quarantine`) the first time.
# See docs/PACKAGING.md.
@ -39,7 +39,7 @@ mkdir -p "$DIST" "$TOOLS"
# Pull version + name straight from Cargo.toml so packages stay in sync.
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
PKG="yt-offline"
PKG="catacomb"
ARCH="$(uname -m)"
say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
@ -124,9 +124,9 @@ build_appimage() {
mkdir -p "$appdir/usr/bin" "$appdir/usr/share/applications" "$appdir/usr/share/icons/hicolor/256x256/apps"
install -Dm755 "target/release/$PKG" "$appdir/usr/bin/$PKG"
install -Dm644 youtube-backup.desktop "$appdir/usr/share/applications/$PKG.desktop"
install -Dm644 catacomb.desktop "$appdir/usr/share/applications/$PKG.desktop"
# AppImage wants the desktop file + icon at the AppDir root too.
cp youtube-backup.desktop "$appdir/$PKG.desktop"
cp catacomb.desktop "$appdir/$PKG.desktop"
install -Dm644 icon.png "$appdir/usr/share/icons/hicolor/256x256/apps/$PKG.png"
cp icon.png "$appdir/$PKG.png"
@ -134,7 +134,7 @@ build_appimage() {
cat > "$appdir/AppRun" <<'APPRUN'
#!/usr/bin/env bash
HERE="$(dirname "$(readlink -f "${0}")")"
exec "$HERE/usr/bin/yt-offline" "$@"
exec "$HERE/usr/bin/catacomb" "$@"
APPRUN
chmod +x "$appdir/AppRun"
@ -185,14 +185,14 @@ build_win() {
install -m755 "$exe" "$staging/$PKG.exe"
install -m644 LICENSE "$staging/LICENSE.txt"
cat > "$staging/README.txt" <<README
yt-offline ${VERSION} — Windows build
Catacomb ${VERSION} — Windows build
Run the GUI by double-clicking yt-offline.exe.
Run the GUI by double-clicking catacomb.exe.
To run the headless web server, open PowerShell or Command Prompt in this
folder and run:
.\\yt-offline.exe --web 8080
.\\catacomb.exe --web 8080
then browse to http://localhost:8080. (Launched from a terminal the server
prints its logs to that terminal; double-clicked it runs windowless.)
@ -231,7 +231,7 @@ win_available() {
}
# ── macOS .app .zip (cross-compiled via osxcross) ────────────────────────────
# Builds an unsigned yt-offline.app for one Apple arch (MAC_ARCH=arm64 default,
# Builds an unsigned catacomb.app for one Apple arch (MAC_ARCH=arm64 default,
# or x86_64) and zips it. Requires osxcross's wrapper compilers on PATH; the
# crypto stack is `ring`, which cross-builds against osxcross's SDK fine. Like
# the other formats we don't bundle yt-dlp/ffmpeg/mpv — the .app's README and
@ -322,9 +322,9 @@ build_mac() {
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>yt-offline</string>
<key>CFBundleDisplayName</key> <string>yt-offline</string>
<key>CFBundleIdentifier</key> <string>org.codeberg.yt-offline</string>
<key>CFBundleName</key> <string>Catacomb</string>
<key>CFBundleDisplayName</key> <string>Catacomb</string>
<key>CFBundleIdentifier</key> <string>org.codeberg.catacomb</string>
<key>CFBundleVersion</key> <string>${VERSION}</string>
<key>CFBundleShortVersionString</key> <string>${VERSION}</string>
<key>CFBundleExecutable</key> <string>${PKG}</string>

View file

@ -392,7 +392,7 @@ impl App {
// Open the DB first so the library scanner can use info_cache to
// skip re-parsing unchanged info.json sidecars. Cold start still
// pays full cost; every subsequent launch is faster.
let db_path = channels_root.join("yt-offline.db");
let db_path = channels_root.join("catacomb.db");
let db = Database::open(&db_path)
.unwrap_or_else(|_| Database::open_in_memory().expect("in-memory db failed"));
// Defer the (potentially multi-minute, disk-bound) library scan +
@ -409,7 +409,7 @@ impl App {
let channels_root = channels_root.clone();
let ctx = cc.egui_ctx.clone();
std::thread::Builder::new()
.name("yt-offline-libscan".into())
.name("catacomb-libscan".into())
.spawn(move || {
let mut library = library::scan_channels_with_cache(&channels_root, Some(&db));
// Hydrate per-channel download options + folder assignments
@ -493,7 +493,7 @@ impl App {
let tx = thumb_result_tx.clone();
let ctx = cc.egui_ctx.clone();
std::thread::Builder::new()
.name(format!("yt-offline-thumb-{worker_id}"))
.name(format!("catacomb-thumb-{worker_id}"))
.spawn(move || {
loop {
// Hold the lock only for the recv itself; release
@ -1185,7 +1185,7 @@ impl App {
self.status = "Play this video first, then click a line to seek".into();
return;
}
let sock = format!("/tmp/yt-offline-{id}.sock");
let sock = format!("/tmp/catacomb-{id}.sock");
match UnixStream::connect(&sock) {
Ok(mut s) => {
let cmd = format!("{{\"command\":[\"seek\",{secs},\"absolute\"]}}\n");
@ -1216,7 +1216,7 @@ impl App {
let use_mpv_ipc = exe == "mpv" || exe == "mpv.exe";
#[cfg(unix)]
let sock_path = format!("/tmp/yt-offline-{video_id}.sock");
let sock_path = format!("/tmp/catacomb-{video_id}.sock");
let mut child_cmd = Command::new(&cmd);
@ -1415,7 +1415,7 @@ impl App {
format!("Download failed: {label}")
};
let _ = notify_rust::Notification::new()
.summary("yt-offline")
.summary("Catacomb")
.body(&summary)
.timeout(notify_rust::Timeout::Milliseconds(4000))
.show();
@ -1430,7 +1430,7 @@ impl App {
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.heading("yt-offline");
ui.heading("Catacomb");
ui.separator();
ui.label("🔍");
ui.add(
@ -2481,7 +2481,7 @@ impl App {
ui.heading("🌐 Remote libraries");
});
ui.label(egui::RichText::new(
"Browse another yt-offline instance read-only. Playback streams from the peer.")
"Browse another Catacomb instance read-only. Playback streams from the peer.")
.weak().small());
ui.separator();
ui.horizontal_wrapped(|ui| {
@ -3234,7 +3234,7 @@ impl App {
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, false, "System")
.on_hover_text("Use whatever yt-dlp is on PATH.");
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, true, "Bundled")
.on_hover_text("Use the yt-dlp + deno installed under ~/.local/share/yt-offline/bin/.");
.on_hover_text("Use the yt-dlp + deno installed under ~/.local/share/catacomb/bin/.");
let installed = crate::ytdlp_bin::bundled_installed();
let btn_label = if installed { "Update" } else { "Install" };
if ui.button(btn_label)
@ -3559,7 +3559,7 @@ impl App {
ui.add_space(4.0);
ui.label(
egui::RichText::new(
"Saves a copy of yt-offline.db (watched / favourites / bookmarks / \
"Saves a copy of catacomb.db (watched / favourites / bookmarks / \
waiting / channel-options / folders). Restore by copying it back \
into your channels directory before launch.",
)
@ -3572,7 +3572,7 @@ impl App {
// cookies file-picker pattern.
let tx = self.backup_save_tx.clone();
let default_name = format!(
"yt-offline-{}.db",
"catacomb-{}.db",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs()).unwrap_or(0),
@ -4517,9 +4517,9 @@ impl eframe::App for App {
Err(e) => self.status = format!("Could not read {}: {e}", path.display()),
}
}
// User picked a backup destination — copy yt-offline.db there.
// User picked a backup destination — copy catacomb.db there.
while let Ok(dest) = self.backup_save_rx.try_recv() {
let src = self.channels_root.join("yt-offline.db");
let src = self.channels_root.join("catacomb.db");
match std::fs::copy(&src, &dest) {
Ok(bytes) => self.status = format!(
"Backup saved ({} → {})",

View file

@ -24,14 +24,14 @@ pub struct Config {
pub subtitles: SubtitlesSection,
#[serde(default)]
pub convert: ConvertSection,
/// Federation: other yt-offline instances whose libraries can be browsed
/// Federation: other catacomb instances whose libraries can be browsed
/// read-only from this one (see [`crate::remote`]). Each is a
/// `[[remote]]` table in config.toml.
#[serde(default, rename = "remote")]
pub remotes: Vec<RemoteSection>,
}
/// One `[[remote]]` entry — a peer yt-offline instance to browse read-only.
/// One `[[remote]]` entry — a peer catacomb instance to browse read-only.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RemoteSection {
/// Display name in the UI's remote switcher.
@ -122,8 +122,8 @@ pub struct BackupSection {
/// automatically when a slot opens. Set to 0 for no limit (not recommended).
#[serde(default = "default_max_concurrent")]
pub max_concurrent: usize,
/// If true, use the bundled yt-dlp + deno binaries managed by yt-offline
/// (installed under `~/.local/share/yt-offline/bin/`). If false, use the
/// If true, use the bundled yt-dlp + deno binaries managed by catacomb
/// (installed under `~/.local/share/catacomb/bin/`). If false, use the
/// `yt-dlp` found on the system PATH.
#[serde(default)]
pub use_bundled_ytdlp: bool,

View file

@ -1,9 +1,9 @@
//! Persistent panic logging so a crashed yt-offline leaves a paper trail.
//! Persistent panic logging so a crashed catacomb leaves a paper trail.
//!
//! GUI users don't see stderr (the binary is launched without a terminal
//! from a `.desktop` file or the IDE), so panics today vanish entirely.
//! This module installs a `panic::set_hook` early in `main` that appends
//! a structured entry to `<channels_root>/yt-offline.crash.log` for every
//! a structured entry to `<channels_root>/catacomb.crash.log` for every
//! panic from any thread.
//!
//! The default panic handler still runs after ours so dev runs keep
@ -36,7 +36,7 @@ use std::path::{Path, PathBuf};
/// dozen panics is what a bug-report attacher actually wants.
const LOG_SIZE_CAP_BYTES: u64 = 256 * 1024;
/// Install a global panic hook that logs to `<dir>/yt-offline.crash.log`
/// Install a global panic hook that logs to `<dir>/catacomb.crash.log`
/// while preserving the default stderr behavior.
///
/// `dir` is the same directory the SQLite database lives in (typically
@ -47,7 +47,7 @@ const LOG_SIZE_CAP_BYTES: u64 = 256 * 1024;
/// Safe to call once at process startup. Calling it more than once
/// replaces the previous hook (which is fine: the new dir wins).
pub fn install(dir: &Path) {
let path = dir.join("yt-offline.crash.log");
let path = dir.join("catacomb.crash.log");
// Don't fail startup if the parent dir doesn't exist yet — log it,
// continue. The DB-open path further down the chain will surface a
// permission / missing-dir error in a more legible way.
@ -194,10 +194,10 @@ mod tests {
// (`std::thread::spawn` so the panic doesn't unwind the test
// runner), and assert the file contents afterward.
let mut tmp = std::env::temp_dir();
tmp.push(format!("yt-offline-crash-test-{}", std::process::id()));
tmp.push(format!("catacomb-crash-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).unwrap();
let log = tmp.join("yt-offline.crash.log");
let log = tmp.join("catacomb.crash.log");
// Install the hook scoped to this test. We swap it back at the
// end so other tests run on the default hook.

View file

@ -1,6 +1,7 @@
//! Persistent storage for watched status, playback positions, and settings.
//! Persistent storage for watched status, playback positions, settings, and
//! restart-persistent web sessions.
//!
//! Backed by a bundled SQLite database (`yt-offline.db`). Access goes through
//! Backed by a bundled SQLite database (`catacomb.db`). Access goes through
//! a small `r2d2`-managed pool of connections rather than a single shared
//! `Connection` — that way concurrent read queries from different axum
//! handlers don't serialize on a mutex, and write queries still take their
@ -13,6 +14,7 @@
//! | `watched` | `video_id` (PK), `watched_at` | Records videos the user has marked watched |
//! | `positions` | `video_id` (PK), `position_secs`, `updated_at` | Stores resume positions |
//! | `settings` | `key` (PK), `value` | Persistent app settings (password hash, etc.) |
//! | `sessions` | `token` (PK), `issued_at` | Restart-persistent web login sessions |
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{Connection, Result};
@ -790,7 +792,7 @@ impl Database {
);
}
/// Idempotently merge another yt-offline database into this one.
/// Idempotently merge another catacomb database into this one.
///
/// Designed for "Import library backup…" — the user uploads a snapshot
/// produced by `GET /api/backup/db`, and we merge its rows in without
@ -858,7 +860,7 @@ impl Database {
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISMATCH),
Some(format!(
"backup is missing required table `{table}` — \
not a yt-offline snapshot, or from an incompatible version"
not a catacomb snapshot, or from an incompatible version"
)),
));
}
@ -1392,7 +1394,7 @@ mod restore_tests {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let id = N.fetch_add(1, Ordering::Relaxed);
p.push(format!("yt-offline-test-{}-{}-{}", std::process::id(), id, name));
p.push(format!("catacomb-test-{}-{}-{}", std::process::id(), id, name));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
ScratchDir(p)
@ -1478,7 +1480,7 @@ mod restore_tests {
let live = Database::open(&dir.join("live.db")).unwrap();
// Create a SQLite file with a completely different schema.
let bad = dir.join("not-yt-offline.db");
let bad = dir.join("not-catacomb.db");
let conn = Connection::open(&bad).unwrap();
conn.execute("CREATE TABLE foo (x INT)", []).unwrap();
drop(conn);

View file

@ -1467,7 +1467,7 @@ impl Downloader {
impl Drop for Downloader {
/// Tear down the bgutil-pot child if we spawned one. Without this
/// the server keeps running after yt-offline exits — orphaned and
/// the server keeps running after catacomb exits — orphaned and
/// still bound to port 4416, blocking the next launch from
/// re-spawning.
fn drop(&mut self) {
@ -1594,7 +1594,7 @@ mod tests {
#[test]
fn redact_sensitive_strips_cookie_path() {
let abs = "/home/luna/.config/yt-offline/cookies.txt";
let abs = "/home/luna/.config/catacomb/cookies.txt";
let input = format!("Unable to load cookies from {abs}");
let out = redact_sensitive(&input, abs);
assert_eq!(out, "Unable to load cookies from cookies.txt");

View file

@ -215,7 +215,7 @@ mod tests {
#[test]
fn renders_valid_rss() {
let feed = Feed {
title: "yt-offline — Demo & co".into(),
title: "catacomb — Demo & co".into(),
description: "archive".into(),
link: "http://host/".into(),
items: vec![FeedItem {
@ -232,7 +232,7 @@ mod tests {
};
let xml = render(&feed);
assert!(xml.starts_with("<?xml"));
assert!(xml.contains("<title>yt-offline — Demo &amp; co</title>"));
assert!(xml.contains("<title>catacomb — Demo &amp; co</title>"));
assert!(xml.contains("<title>Episode &lt;1&gt;</title>"));
assert!(xml.contains(r#"<enclosure url="http://host/files/channels/Demo/a.mkv" length="12345" type="video/x-matroska"/>"#));
assert!(xml.contains("<itunes:duration>1:05</itunes:duration>"));

View file

@ -1,9 +1,9 @@
//! yt-offline — desktop and web app for archiving YouTube content with yt-dlp.
//! catacomb — desktop and web app for archiving YouTube content with yt-dlp.
//!
//! # Usage
//!
//! * **GUI mode** (default): `yt-offline`
//! * **Web mode**: `yt-offline --web [PORT]` — starts a headless HTTP server
//! * **GUI mode** (default): `catacomb`
//! * **Web mode**: `catacomb --web [PORT]` — starts a headless HTTP server
//! on the configured port (default 8080).
//!
//! Configuration is read from `config.toml` in the current working directory.
@ -60,7 +60,7 @@ fn renderer_from_args(args: &[String]) -> eframe::Renderer {
if requested.is_some() {
renderer_from_name(requested)
} else {
let env_renderer = std::env::var("YT_OFFLINE_RENDERER").ok();
let env_renderer = std::env::var("CATACOMB_RENDERER").ok();
renderer_from_name(env_renderer.as_deref())
}
}
@ -90,6 +90,33 @@ fn attach_windows_console() {
#[cfg(not(windows))]
fn attach_windows_console() {}
/// Adopt the pre-rename `yt-offline` data paths under the new `catacomb` names
/// so the rename doesn't strand an existing library or bundled toolchain.
/// Best-effort: only renames when the new path is absent and the old one still
/// exists, so it's a no-op on fresh installs and after the first migrated run.
fn migrate_legacy_paths(channels_root: &std::path::Path) {
fn adopt(old: std::path::PathBuf, new: std::path::PathBuf) {
if !new.exists() && old.exists() {
match std::fs::rename(&old, &new) {
Ok(()) => eprintln!("migrated {}{}", old.display(), new.display()),
Err(e) => eprintln!("warning: could not migrate {}{}: {e}", old.display(), new.display()),
}
}
}
// SQLite DB plus its WAL/SHM sidecars (if any), in the library root.
for suffix in ["", "-wal", "-shm"] {
adopt(
channels_root.join(format!("yt-offline.db{suffix}")),
channels_root.join(format!("catacomb.db{suffix}")),
);
}
// Bundled venv + binaries under the XDG data dir.
if let Some(home) = std::env::var_os("HOME") {
let share = std::path::PathBuf::from(home).join(".local").join("share");
adopt(share.join("yt-offline"), share.join("catacomb"));
}
}
fn main() -> eframe::Result<()> {
attach_windows_console();
@ -104,6 +131,11 @@ fn main() -> eframe::Result<()> {
let mut cfg = config::Config::load(&cfg_path)
.unwrap_or_else(|_| config::Config::default_with_dir(cwd.join("channels")));
// One-time adoption of the pre-rename `yt-offline` data paths so existing
// installs keep their library + bundled toolchain after the rename to
// Catacomb. Runs before anything opens the DB or the venv.
migrate_legacy_paths(&cfg.backup.directory);
// Install the persistent panic logger. Logs go alongside the SQLite
// database; the parent of channels_root is the same "library root"
// the desktop UI shows.
@ -137,16 +169,16 @@ fn main() -> eframe::Result<()> {
viewport: eframe::egui::ViewportBuilder::default()
.with_inner_size([1280.0, 820.0])
.with_min_inner_size([800.0, 500.0])
.with_title("yt-offline"),
.with_title("Catacomb"),
// Default to wgpu (Vulkan): the glow/OpenGL path crashes on some
// NVIDIA + Wayland maximizes (Glutin EGL_BAD_ALLOC). Keep a launch
// escape hatch for systems where Vulkan/wgpu opens a blank window:
// `YT_OFFLINE_RENDERER=glow yt-offline` or `yt-offline --renderer glow`.
// `CATACOMB_RENDERER=glow catacomb` or `catacomb --renderer glow`.
renderer: renderer_from_args(&args),
..Default::default()
};
eframe::run_native(
"yt-offline",
"Catacomb",
native_options,
Box::new(|cc| Ok(Box::new(app::App::new(cc, tray)))),
)
@ -182,7 +214,7 @@ mod tests {
#[test]
fn renderer_arg_overrides_env_parser_path() {
let args = vec![
"yt-offline".to_string(),
"Catacomb".to_string(),
"--renderer".to_string(),
"gl".to_string(),
];

View file

@ -242,7 +242,7 @@ mod tests {
#[test]
fn is_within_accepts_inside_path() {
let tmp = std::env::temp_dir().join("yt-offline-iw-test-1");
let tmp = std::env::temp_dir().join("catacomb-iw-test-1");
let _ = fs::create_dir_all(&tmp);
let inside = tmp.join("foo.txt");
let _ = fs::write(&inside, "x");
@ -253,7 +253,7 @@ mod tests {
#[test]
fn is_within_rejects_outside_path() {
let tmp = std::env::temp_dir().join("yt-offline-iw-test-2");
let tmp = std::env::temp_dir().join("catacomb-iw-test-2");
let _ = fs::create_dir_all(&tmp);
// The target's parent canonicalises to /tmp, which doesn't start with tmp.
let outside = std::env::temp_dir().join("not-our-dir-xyz.txt");
@ -264,7 +264,7 @@ mod tests {
#[test]
fn is_within_handles_missing_target() {
// Target doesn't exist; parent dir does and is inside root.
let tmp = std::env::temp_dir().join("yt-offline-iw-test-3");
let tmp = std::env::temp_dir().join("catacomb-iw-test-3");
let _ = fs::create_dir_all(&tmp);
let ghost = tmp.join("does-not-exist.txt");
assert!(is_within(&tmp, &ghost));

View file

@ -355,9 +355,9 @@ fn extract_after<'a>(url: &'a str, marker: &str) -> Option<&'a str> {
///
/// All platforms (including YouTube) are nested as subdirectories of the
/// configured `channels_root`. So a config with
/// `directory = "/mnt/library/yt-offline"` puts YouTube channels at
/// `/mnt/library/yt-offline/channels/<creator>/` and Bandcamp artists at
/// `/mnt/library/yt-offline/bandcamp/<artist>/`. This keeps everything a
/// `directory = "/mnt/library/catacomb"` puts YouTube channels at
/// `/mnt/library/catacomb/channels/<creator>/` and Bandcamp artists at
/// `/mnt/library/catacomb/bandcamp/<artist>/`. This keeps everything a
/// user might want to archive under one tidy umbrella directory.
///
/// The function's name predates the layout change — `channels_root` is

View file

@ -24,7 +24,7 @@
//! Lives next to the bundled deno + yt-dlp:
//!
//! ```text
//! ~/.local/share/yt-offline/
//! ~/.local/share/catacomb/
//! bin/
//! bgutil-pot ← the Rust HTTP server binary
//! venv/ ← reused — plugin zip unpacked into site-packages
@ -65,7 +65,7 @@ pub fn server_url() -> String {
/// Path to the bgutil-pot binary inside the bundled bin dir.
///
/// Co-locates with the bundled `deno` so a single bundled-dir cleanup
/// (currently just `rm -rf ~/.local/share/yt-offline/bin`) removes
/// (currently just `rm -rf ~/.local/share/catacomb/bin`) removes
/// both.
pub fn bin_path() -> PathBuf {
let mut p = crate::ytdlp_bin::bundled_dir();

View file

@ -1,4 +1,4 @@
//! Federation — read-only browsing of a *peer* yt-offline instance's library.
//! Federation — read-only browsing of a *peer* catacomb instance's library.
//!
//! A [`RemoteClient`] talks to another instance over its existing web API:
//! it fetches `/api/library`, logging in first if the peer has a password
@ -61,7 +61,7 @@ impl RemoteClient {
let client = reqwest::blocking::Client::builder()
.cookie_store(true)
.timeout(Duration::from_secs(30))
.user_agent("yt-offline-federation")
.user_agent("catacomb-federation")
.build()
.unwrap_or_else(|_| reqwest::blocking::Client::new());
RemoteClient {

View file

@ -62,14 +62,14 @@ struct TrayModel {
#[cfg(target_os = "linux")]
impl ksni::Tray for TrayModel {
fn id(&self) -> String {
"yt-offline".into()
"Catacomb".into()
}
fn title(&self) -> String {
"yt-offline".into()
"Catacomb".into()
}
fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: "yt-offline".into(),
title: "Catacomb".into(),
description: "Self-hosted archive for YouTube + friends".into(),
..Default::default()
}
@ -92,7 +92,7 @@ impl ksni::Tray for TrayModel {
use ksni::menu::StandardItem;
vec![
StandardItem {
label: "Show yt-offline".into(),
label: "Show Catacomb".into(),
activate: Box::new(|m: &mut Self| {
let _ = m.tx.send(TrayEvent::Show);
}),
@ -153,7 +153,7 @@ pub fn start(icon_png_bytes: &[u8]) -> Option<TrayHandle> {
};
std::thread::Builder::new()
.name("yt-offline-tray".into())
.name("catacomb-tray".into())
.spawn(move || {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()

View file

@ -401,7 +401,7 @@ struct SettingsPayload {
/// Maximum simultaneous yt-dlp processes. 0 = unlimited. Ignored if 0 on POST.
#[serde(default)]
max_concurrent: usize,
/// If true, invoke the bundled yt-dlp under `~/.local/share/yt-offline/bin/`
/// If true, invoke the bundled yt-dlp under `~/.local/share/catacomb/bin/`
/// instead of the system PATH yt-dlp.
#[serde(default)]
use_bundled_ytdlp: bool,
@ -806,7 +806,7 @@ pub fn write_cookies(text: &str) -> Result<usize, String> {
std::fs::write(&path, &content).map_err(|e| e.to_string())?;
// cookies.txt carries live session credentials — tighten the mode so it
// isn't world-readable on multi-user systems. Best-effort, like the
// similar guard on yt-offline.db.
// similar guard on catacomb.db.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
@ -2406,7 +2406,7 @@ async fn get_feed_all(State(state): State<Arc<WebState>>, headers: HeaderMap) ->
.collect();
let items = build_feed_items(all, root, &base, &state.feed_token, FEED_LIMIT);
let feed = crate::feed::Feed {
title: "yt-offline — Library".into(),
title: "Catacomb — Library".into(),
description: "Your archived videos as a podcast feed.".into(),
link: format!("{base}/"),
items,
@ -2430,7 +2430,7 @@ async fn get_feed_channel(
.chain(ch.playlists.iter().flat_map(|p| p.videos.iter())).collect();
let items = build_feed_items(vids, root, &base, &state.feed_token, FEED_LIMIT);
let feed = crate::feed::Feed {
title: format!("yt-offline — {}", ch.name),
title: format!("Catacomb — {}", ch.name),
description: format!("Archived videos from {}.", ch.name),
link: format!("{base}/"),
items,
@ -2696,7 +2696,7 @@ async fn delete_folder(
/// session credentials that shouldn't fly over the wire unprompted, and
/// keeping the endpoint to one file means no extra deps for tar/zip.
async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
let db_path = state.channels_root.join("yt-offline.db");
let db_path = state.channels_root.join("catacomb.db");
let Ok(bytes) = std::fs::read(&db_path) else {
return (StatusCode::NOT_FOUND, "no db file on disk (running in-memory?)").into_response();
};
@ -2704,7 +2704,7 @@ async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let filename = format!("yt-offline-{now_secs}.db");
let filename = format!("catacomb-{now_secs}.db");
(
[
(header::CONTENT_TYPE, "application/x-sqlite3".to_string()),
@ -2719,7 +2719,7 @@ async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
/// rows into the live DB. Mirrors `GET /api/backup/db` in the opposite
/// direction.
///
/// The body is the raw `yt-offline.db` bytes — same shape as what
/// The body is the raw `catacomb.db` bytes — same shape as what
/// `get_backup_db` produces. We write it to a sibling temp file, hand it
/// to [`Database::restore_from_backup`] for the actual merge, then
/// refresh the in-memory watched / positions / flags caches so the next
@ -2750,7 +2750,7 @@ async fn post_restore_db(
// Write to a sibling tmp file so it lives on the same filesystem as
// the live DB — keeps ATTACH happy and avoids cross-device rename
// issues if we ever extend this to atomic-replace semantics.
let tmp_path = state.channels_root.join(".yt-offline.restore.tmp");
let tmp_path = state.channels_root.join(".catacomb.restore.tmp");
if let Err(e) = std::fs::write(&tmp_path, &body) {
return (StatusCode::INTERNAL_SERVER_ERROR,
format!("write tmp file: {e}")).into_response();
@ -3124,7 +3124,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
let dir = crate::platform::platform_root(&channels_root, p);
let _ = std::fs::create_dir_all(&dir);
}
let db_path = channels_root.join("yt-offline.db");
let db_path = channels_root.join("catacomb.db");
let config_path = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("config.toml");
@ -3374,7 +3374,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
return;
}
};
println!("yt-offline web UI: http://localhost:{port}");
println!("Catacomb web UI: http://localhost:{port}");
// Now that we're accepting connections, run the initial library scan in
// the background so a cold-cache scan of a large library doesn't delay

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>yt-offline</title>
<title>Catacomb</title>
<!-- Embedded UI fonts (base64 below, offline-safe — no CDN): Instrument Serif
by Rodrigo Fuenzalida and Hanken Grotesk by Alfredo Marco Pradil, both
under the SIL Open Font License 1.1. -->
@ -87,9 +87,9 @@
.modal-hdr h2{flex:1;font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.modal-body{display:flex;gap:12px;overflow:hidden;flex:1;min-height:0}
.modal video{flex:1;min-width:0;max-height:75vh;background:#000}
/* Custom player (used for non-seekable transcode streams): the live ffmpeg
pipe has no byte ranges, so we drive a scrubber off the known duration and
re-request the stream at an offset on seek. */
/* Custom player: direct files and non-seekable transcode streams now share
the same chrome. The ffmpeg pipe has no byte ranges, so transcode scrubbing
re-requests the stream at an offset while direct files set currentTime. */
.vid-wrap{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.vid-wrap video{flex:1;min-height:0;max-height:72vh;width:100%;background:#000}
.vctrl{display:flex;align-items:center;gap:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:6px 8px;flex-wrap:wrap}
@ -499,7 +499,7 @@
<body>
<header>
<button id="menu-btn" onclick="toggleSidebar()"></button>
<h1>yt-offline</h1>
<h1>Catacomb</h1>
<input type="search" id="search" placeholder="Filter…" oninput="saveUiState();renderGrid()">
<select id="sort" onchange="saveUiState();renderGrid()">
<optgroup label="Download date">
@ -598,7 +598,7 @@ function closeSidebar(){document.getElementById('sidebar').classList.remove('ope
/* ── API ────────────────────────────────────────────────────────── */
async function api(path,opts){
const r=await fetch(path,opts);
// Session gone (expired, or the server restarted — sessions are in-memory):
// Session gone (expired, cleared by password change/logout, or otherwise absent):
// the cached SPA would otherwise just throw cryptic "authentication required"
// toasts on every action. Reload instead so the server serves the login page.
if(r.status===401){
@ -1632,8 +1632,8 @@ function shufflePlay(){
}
playVideo(pick.id);
}
// Player state for the custom transcode controls. For direct /files/ videos
// `isTranscode` is false and native <video controls> handle everything.
// Player state for the custom controls. For direct /files/ videos `isTranscode`
// is false and seeking sets currentTime; transcode seeks by reloading at start=.
let player={isTranscode:false,base:'',seekBase:0,duration:0};
function transcodeUrlAt(t){const u=player.base;const sep=u.includes('?')?'&':'?';return safeUrl(u+sep+'start='+Math.max(0,t).toFixed(2));}
// Effective playback position: a transcode element's currentTime restarts at 0
@ -2289,7 +2289,7 @@ async function openSettings(){
<label for="cf-ytdlp-bundled">Use bundled yt-dlp + deno</label>
<input type="checkbox" id="cf-ytdlp-bundled" ${cur.use_bundled_ytdlp?'checked':''}>
</div>
<div class="settings-hint" style="margin-bottom:6px">Bundled binaries live in ~/.local/share/yt-offline/bin/ and include a JS runtime (deno) needed for YouTube signature deciphering. When off, the yt-dlp on your PATH is used.</div>
<div class="settings-hint" style="margin-bottom:6px">Bundled binaries live in ~/.local/share/catacomb/bin/ and include a JS runtime (deno) needed for YouTube signature deciphering. When off, the yt-dlp on your PATH is used.</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px">
<button onclick="updateYtdlp(this)">${cur.bundled_ytdlp_installed?'⟳ Update bundled':'⤓ Install bundled'}</button>
<span style="font-size:11px;color:var(--muted)">${cur.bundled_ytdlp_installed?'✓ installed':'not installed'}</span>

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>yt-offline — Sign in</title>
<title>Catacomb — Sign in</title>
<style>
/* Standalone page (the SPA's embedded fonts aren't available here), so the
display face is a system serif stack — editorial without extra bytes. The
@ -47,7 +47,7 @@
</head>
<body>
<div class="box">
<div class="brand"><span class="dot"></span><h1>yt-offline</h1></div>
<div class="brand"><span class="dot"></span><h1>Catacomb</h1></div>
<div class="tag">your private archive</div>
<div class="field"><input type="password" id="pwd" placeholder="Password" autofocus onkeydown="if(event.key==='Enter')login()"></div>
<button onclick="login()">Sign in</button>

View file

@ -4,13 +4,13 @@
//! app invokes its own yt-dlp instead of whatever's on PATH. To get the
//! full feature set — most importantly `curl_cffi`-backed `--impersonate`
//! support — we install yt-dlp into a self-contained Python virtualenv
//! under `~/.local/share/yt-offline/venv/`. A bundled `deno` lives at
//! `~/.local/share/yt-offline/bin/deno` so yt-dlp can evaluate the
//! under `~/.local/share/catacomb/venv/`. A bundled `deno` lives at
//! `~/.local/share/catacomb/bin/deno` so yt-dlp can evaluate the
//! JavaScript signature-deciphering code YouTube serves with the player.
//!
//! Layout:
//! ```text
//! ~/.local/share/yt-offline/
//! ~/.local/share/catacomb/
//! bin/ ← prepended to PATH so yt-dlp finds deno
//! deno
//! venv/
@ -30,7 +30,7 @@ pub fn bundled_root() -> PathBuf {
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".local").join("share").join("yt-offline")
home.join(".local").join("share").join("catacomb")
}
/// Directory holding non-Python binaries (currently just `deno`). Also the

View file

@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicU16, Ordering};
/// Absolute path to the binary under test (set by Cargo for integration
/// tests of a crate that builds a binary).
const BIN: &str = env!("CARGO_BIN_EXE_yt-offline");
const BIN: &str = env!("CARGO_BIN_EXE_catacomb");
/// True if `curl` is usable; the tests no-op otherwise so a machine
/// without curl doesn't show spurious failures.
@ -32,7 +32,7 @@ fn have_ffmpeg() -> bool {
.status().map(|s| s.success()).unwrap_or(false)
}
/// A running `yt-offline --web` child against a scratch dir. Killed and
/// A running `catacomb --web` child against a scratch dir. Killed and
/// its dir removed on drop.
struct Server {
child: Child,
@ -59,7 +59,7 @@ impl Server {
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn yt-offline --web");
.expect("spawn catacomb --web");
let s = Server { child, port, dir };
s.wait_ready();
s
@ -176,7 +176,7 @@ fn index_and_library_served() {
let (code, body) = s.get("/");
assert_eq!(code, 200, "GET / should serve the SPA");
assert!(body.contains("yt-offline"), "index should mention yt-offline");
assert!(body.contains("Catacomb"), "index should mention Catacomb");
let (code, body) = s.get("/api/library");
assert_eq!(code, 200);

0
yt-offline.db Normal file
View file

2
yt-offline/FETCH_HEAD Normal file
View file

@ -0,0 +1,2 @@
17c149c21a91e639919901ef7504bbc3df87ef33 not-for-merge branch 'main' of https://codeberg.org/anassaeneroi/yt-offline
f3c136a0183cdd64a7adc71c4d4dcea24e3fefcf not-for-merge branch 'master' of https://codeberg.org/anassaeneroi/yt-offline

1
yt-offline/HEAD Normal file
View file

@ -0,0 +1 @@
ref: refs/heads/main

9
yt-offline/config Normal file
View file

@ -0,0 +1,9 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://codeberg.org/anassaeneroi/yt-offline.git
tagOpt = --no-tags
fetch = +refs/*:refs/*
mirror = true

1
yt-offline/description Normal file
View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View file

@ -0,0 +1,15 @@
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit. The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:

View file

@ -0,0 +1,74 @@
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".
# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
# This example catches duplicate Signed-off-by lines and messages that
# would confuse 'git am'.
ret=0
test "" = "$(grep '^Signed-off-by: ' "$1" |
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
echo >&2 Duplicate Signed-off-by lines.
ret=1
}
comment_re="$(
{
git config --get-regexp "^core\.comment(char|string)\$" ||
echo '#'
} | sed -n -e '
${
s/^[^ ]* //
s|[][*./\]|\\&|g
s/^auto$/[#;@!$%^&|:]/
p
}'
)"
scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$"
comment_line="^${comment_re}.*"
blank_line='^[ ]*$'
# Disallow lines starting with "diff -" or "Index: " in the body of the
# message. Stop looking if we see a scissors line.
line="$(sed -n -e "
# Skip comments and blank lines at the start of the file.
/${scissors_line}/q
/${comment_line}/d
/${blank_line}/d
# The first paragraph will become the subject header so
# does not need to be checked.
: subject
n
/${scissors_line}/q
/${blank_line}/!b subject
# Check the body of the message for problematic
# prefixes.
: body
n
/${scissors_line}/q
/${comment_line}/b body
/^diff -/{p;q;}
/^Index: /{p;q;}
b body
" "$1")"
if test -n "$line"
then
echo >&2 "Message contains a diff that will confuse 'git am'."
echo >&2 "To fix this indent the diff."
ret=1
fi
exit $ret

View file

@ -0,0 +1,168 @@
#!/usr/bin/perl
use strict;
use warnings;
use IPC::Open2;
# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 2) and last update token
# formatted as a string and outputs to stdout a new update token and
# all files that have been modified since the update token. Paths must
# be relative to the root of the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $last_update_token) = @ARGV;
# Uncomment for debugging
# print STDERR "$0 $version $last_update_token\n";
# Check the hook interface version
if ($version ne 2) {
die "Unsupported query-fsmonitor hook version '$version'.\n" .
"Falling back to scanning...\n";
}
my $git_work_tree = get_working_dir();
my $json_pkg;
eval {
require JSON::XS;
$json_pkg = "JSON::XS";
1;
} or do {
require JSON::PP;
$json_pkg = "JSON::PP";
};
launch_watchman();
sub launch_watchman {
my $o = watchman_query();
if (is_work_tree_watched($o)) {
output_result($o->{clock}, @{$o->{files}});
}
}
sub output_result {
my ($clockid, @files) = @_;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# binmode $fh, ":utf8";
# print $fh "$clockid\n@files\n";
# close $fh;
binmode STDOUT, ":utf8";
print $clockid;
print "\0";
local $, = "\0";
print @files;
}
sub watchman_clock {
my $response = qx/watchman clock "$git_work_tree"/;
die "Failed to get clock id on '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
return $json_pkg->new->utf8->decode($response);
}
sub watchman_query {
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
or die "open2() failed: $!\n" .
"Falling back to scanning...\n";
# In the query expression below we're asking for names of files that
# changed since $last_update_token but not from the .git folder.
#
# To accomplish this, we're using the "since" generator to use the
# recency index to select candidate nodes and "fields" to limit the
# output to file names only. Then we're using the "expression" term to
# further constrain the results.
my $last_update_line = "";
if (substr($last_update_token, 0, 1) eq "c") {
$last_update_token = "\"$last_update_token\"";
$last_update_line = qq[\n"since": $last_update_token,];
}
my $query = <<" END";
["query", "$git_work_tree", {$last_update_line
"fields": ["name"],
"expression": ["not", ["dirname", ".git"]]
}]
END
# Uncomment for debugging the watchman query
# open (my $fh, ">", ".git/watchman-query.json");
# print $fh $query;
# close $fh;
print CHLD_IN $query;
close CHLD_IN;
my $response = do {local $/; <CHLD_OUT>};
# Uncomment for debugging the watch response
# open ($fh, ">", ".git/watchman-response.json");
# print $fh $response;
# close $fh;
die "Watchman: command returned no output.\n" .
"Falling back to scanning...\n" if $response eq "";
die "Watchman: command returned invalid output: $response\n" .
"Falling back to scanning...\n" unless $response =~ /^\{/;
return $json_pkg->new->utf8->decode($response);
}
sub is_work_tree_watched {
my ($output) = @_;
my $error = $output->{error};
if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
my $response = qx/watchman watch "$git_work_tree"/;
die "Failed to make watchman watch '$git_work_tree'.\n" .
"Falling back to scanning...\n" if $? != 0;
$output = $json_pkg->new->utf8->decode($response);
$error = $output->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
# Uncomment for debugging watchman output
# open (my $fh, ">", ".git/watchman-output.out");
# close $fh;
# Watchman will always return all files on the first query so
# return the fast "everything is dirty" flag to git and do the
# Watchman query just to get it over with now so we won't pay
# the cost in git to look up each individual file.
my $o = watchman_clock();
$error = $o->{error};
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
output_result($o->{clock}, ("/"));
return 0;
}
die "Watchman: $error.\n" .
"Falling back to scanning...\n" if $error;
return 1;
}
sub get_working_dir {
my $working_dir;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
$working_dir = Win32::GetCwd();
$working_dir =~ tr/\\/\//;
} else {
require Cwd;
$working_dir = Cwd::cwd();
}
return $working_dir;
}

View file

@ -0,0 +1,8 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

View file

@ -0,0 +1,14 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:

View file

@ -0,0 +1,49 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --type=bool hooks.allownonascii)
# Redirect output to stderr.
exec 1>&2
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
cat <<\EOF
Error: Attempt to add a non-ASCII file name.
This can cause problems if you want to work with people on other platforms.
To be portable it is advisable to rename the file.
If you know what you are doing you can disable this check using:
git config hooks.allownonascii true
EOF
exit 1
fi
# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

View file

@ -0,0 +1,13 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".
. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
exec "$GIT_DIR/hooks/pre-commit"
:

View file

@ -0,0 +1,53 @@
#!/bin/sh
# An example hook script to verify what is about to be pushed. Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed. If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
# <local ref> <local oid> <remote ref> <remote oid>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).
remote="$1"
url="$2"
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
while read local_ref local_oid remote_ref remote_oid
do
if test "$local_oid" = "$zero"
then
# Handle delete
:
else
if test "$remote_oid" = "$zero"
then
# New branch, examine all commits
range="$local_oid"
else
# Update to existing branch, examine new commits
range="$remote_oid..$local_oid"
fi
# Check for WIP commit
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
if test -n "$commit"
then
echo >&2 "Found WIP commit in $local_ref, not pushing"
exit 1
fi
fi
done
exit 0

View file

@ -0,0 +1,169 @@
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.
publish=next
basebranch="$1"
if test "$#" = 2
then
topic="refs/heads/$2"
else
topic=`git symbolic-ref HEAD` ||
exit 0 ;# we do not interrupt rebasing detached HEAD
fi
case "$topic" in
refs/heads/??/*)
;;
*)
exit 0 ;# we do not interrupt others.
;;
esac
# Now we are dealing with a topic branch being rebased
# on top of master. Is it OK to rebase it?
# Does the topic really exist?
git show-ref -q "$topic" || {
echo >&2 "No such branch $topic"
exit 1
}
# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
echo >&2 "$topic is fully merged to master; better remove it."
exit 1 ;# we could allow it, but there is no point.
fi
# Is topic ever merged to next? If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
not_in_topic=`git rev-list "^$topic" master`
if test -z "$not_in_topic"
then
echo >&2 "$topic is already up to date with master"
exit 1 ;# we could allow it, but there is no point.
else
exit 0
fi
else
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
/usr/bin/perl -e '
my $topic = $ARGV[0];
my $msg = "* $topic has commits already merged to public branch:\n";
my (%not_in_next) = map {
/^([0-9a-f]+) /;
($1 => 1);
} split(/\n/, $ARGV[1]);
for my $elem (map {
/^([0-9a-f]+) (.*)$/;
[$1 => $2];
} split(/\n/, $ARGV[2])) {
if (!exists $not_in_next{$elem->[0]}) {
if ($msg) {
print STDERR $msg;
undef $msg;
}
print STDERR " $elem->[1]\n";
}
}
' "$topic" "$not_in_next" "$not_in_master"
exit 1
fi
<<\DOC_END
This sample hook safeguards topic branches that have been
published from being rewound.
The workflow assumed here is:
* Once a topic branch forks from "master", "master" is never
merged into it again (either directly or indirectly).
* Once a topic branch is fully cooked and merged into "master",
it is deleted. If you need to build on top of it to correct
earlier mistakes, a new topic branch is created by forking at
the tip of the "master". This is not strictly necessary, but
it makes it easier to keep your history simple.
* Whenever you need to test or publish your changes to topic
branches, merge them into "next" branch.
The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.
With this workflow, you would want to know:
(1) ... if a topic branch has ever been merged to "next". Young
topic branches can have stupid mistakes you would rather
clean up before publishing, and things that have not been
merged into other branches can be easily rebased without
affecting other people. But once it is published, you would
not want to rewind it.
(2) ... if a topic branch has been fully merged to "master".
Then you can delete it. More importantly, you should not
build on top of it -- other people may already want to
change things related to the topic as patches against your
"master", so if you need further changes, it is better to
fork the topic (perhaps with the same name) afresh from the
tip of "master".
Let's look at this example:
o---o---o---o---o---o---o---o---o---o "next"
/ / / /
/ a---a---b A / /
/ / / /
/ / c---c---c---c B /
/ / / \ /
/ / / b---b C \ /
/ / / / \ /
---o---o---o---o---o---o---o---o---o---o---o "master"
A, B and C are topic branches.
* A has one fix since it was merged up to "next".
* B has finished. It has been fully merged up to "master" and "next",
and is ready to be deleted.
* C has not merged to "next" at all.
We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.
To compute (1):
git rev-list ^master ^topic next
git rev-list ^master next
if these match, topic has not merged in next at all.
To compute (2):
git rev-list master..topic
if this is empty, it is fully merged to "master".
DOC_END

View file

@ -0,0 +1,24 @@
#!/bin/sh
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".
if test -n "$GIT_PUSH_OPTION_COUNT"
then
i=0
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
do
eval "value=\$GIT_PUSH_OPTION_$i"
case "$value" in
echoback=*)
echo "echo from the pre-receive-hook: ${value#*=}" >&2
;;
reject)
exit 1
esac
i=$((i + 1))
done
fi

View file

@ -0,0 +1,42 @@
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source. The hook's purpose is to edit the commit
# message file. If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".
# This hook includes three examples. The first one removes the
# "# Please enter the commit message..." help message.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output. It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited. This is rarely a good idea.
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
# case "$COMMIT_SOURCE,$SHA1" in
# ,|template,)
# /usr/bin/perl -i.bak -pe '
# print "\n" . `git diff --cached --name-status -r`
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
# *) ;;
# esac
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
# if test -z "$COMMIT_SOURCE"
# then
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
# fi

View file

@ -0,0 +1,78 @@
#!/bin/sh
# An example hook script to update a checked-out tree on a git push.
#
# This hook is invoked by git-receive-pack(1) when it reacts to git
# push and updates reference(s) in its repository, and when the push
# tries to update the branch that is currently checked out and the
# receive.denyCurrentBranch configuration variable is set to
# updateInstead.
#
# By default, such a push is refused if the working tree and the index
# of the remote repository has any difference from the currently
# checked out commit; when both the working tree and the index match
# the current commit, they are updated to match the newly pushed tip
# of the branch. This hook is to be used to override the default
# behaviour; however the code below reimplements the default behaviour
# as a starting point for convenient modification.
#
# The hook receives the commit with which the tip of the current
# branch is going to be updated:
commit=$1
# It can exit with a non-zero status to refuse the push (when it does
# so, it must not modify the index or the working tree).
die () {
echo >&2 "$*"
exit 1
}
# Or it can make any necessary changes to the working tree and to the
# index to bring them to the desired state when the tip of the current
# branch is updated to the new commit, and exit with a zero status.
#
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
# in order to emulate git fetch that is run in the reverse direction
# with git push, as the two-tree form of git read-tree -u -m is
# essentially the same as git switch or git checkout that switches
# branches while keeping the local changes in the working tree that do
# not interfere with the difference between the branches.
# The below is a more-or-less exact translation to shell of the C code
# for the default behaviour for git's push-to-checkout hook defined in
# the push_to_deploy() function in builtin/receive-pack.c.
#
# Note that the hook will be executed from the repository directory,
# not from the working tree, so if you want to perform operations on
# the working tree, you will have to adapt your code accordingly, e.g.
# by adding "cd .." or using relative paths.
if ! git update-index -q --ignore-submodules --refresh
then
die "Up-to-date check failed"
fi
if ! git diff-files --quiet --ignore-submodules --
then
die "Working directory has unstaged changes"
fi
# This is a rough translation of:
#
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
if git cat-file -e HEAD 2>/dev/null
then
head=HEAD
else
head=$(git hash-object -t tree --stdin </dev/null)
fi
if ! git diff-index --quiet --cached --ignore-submodules $head --
then
die "Working directory has staged changes"
fi
if ! git read-tree -u -m "$commit"
then
die "Could not update working tree to new HEAD"
fi

View file

@ -0,0 +1,77 @@
#!/bin/sh
# An example hook script to validate a patch (and/or patch series) before
# sending it via email.
#
# The hook should exit with non-zero status after issuing an appropriate
# message if it wants to prevent the email(s) from being sent.
#
# To enable this hook, rename this file to "sendemail-validate".
#
# By default, it will only check that the patch(es) can be applied on top of
# the default upstream branch without conflicts in a secondary worktree. After
# validation (successful or not) of the last patch of a series, the worktree
# will be deleted.
#
# The following config variables can be set to change the default remote and
# remote ref that are used to apply the patches against:
#
# sendemail.validateRemote (default: origin)
# sendemail.validateRemoteRef (default: HEAD)
#
# Replace the TODO placeholders with appropriate checks according to your
# needs.
validate_cover_letter () {
file="$1"
# TODO: Replace with appropriate checks (e.g. spell checking).
true
}
validate_patch () {
file="$1"
# Ensure that the patch applies without conflicts.
git am -3 "$file" || return
# TODO: Replace with appropriate checks for this patch
# (e.g. checkpatch.pl).
true
}
validate_series () {
# TODO: Replace with appropriate checks for the whole series
# (e.g. quick build, coding style checks, etc.).
true
}
# main -------------------------------------------------------------------------
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
then
remote=$(git config --default origin --get sendemail.validateRemote) &&
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
git config --replace-all sendemail.validateWorktree "$worktree"
else
worktree=$(git config --get sendemail.validateWorktree)
fi || {
echo "sendemail-validate: error: failed to prepare worktree" >&2
exit 1
}
unset GIT_DIR GIT_WORK_TREE
cd "$worktree" &&
if grep -q "^diff --git " "$1"
then
validate_patch "$1"
else
validate_cover_letter "$1"
fi &&
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
then
git config --unset-all sendemail.validateWorktree &&
trap 'git worktree remove -ff "$worktree"' EXIT &&
validate_series
fi

128
yt-offline/hooks/update.sample Executable file
View file

@ -0,0 +1,128 @@
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"
# --- Safety check
if [ -z "$GIT_DIR" ]; then
echo "Don't run this script from the command line." >&2
echo " (if you want, you could supply GIT_DIR then run" >&2
echo " $0 <ref> <oldrev> <newrev>)" >&2
exit 1
fi
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
exit 1
fi
# --- Config
allowunannotated=$(git config --type=bool hooks.allowunannotated)
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
echo "*** Project description file hasn't been set" >&2
exit 1
;;
esac
# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
if [ "$newrev" = "$zero" ]; then
newrev_type=delete
else
newrev_type=$(git cat-file -t $newrev)
fi
case "$refname","$newrev_type" in
refs/tags/*,commit)
# un-annotated tag
short_refname=${refname##refs/tags/}
if [ "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
refs/tags/*,delete)
# delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,tag)
# annotated tag
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
then
echo "*** Tag '$refname' already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
fi
;;
refs/heads/*,commit)
# branch
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
# tracking branch
;;
refs/remotes/*,delete)
# delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
# Anything else (is there anything else?)
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
exit 1
;;
esac
# --- Finished
exit 0

View file

@ -0,0 +1 @@
* -export-subst -export-ignore

6
yt-offline/info/exclude Normal file
View file

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

3
yt-offline/packed-refs Normal file
View file

@ -0,0 +1,3 @@
# pack-refs with: peeled fully-peeled sorted
8b4796b518296287bc3748413981ee65c8d328b2 refs/heads/main
f3c136a0183cdd64a7adc71c4d4dcea24e3fefcf refs/heads/master

View file

@ -0,0 +1 @@
17c149c21a91e639919901ef7504bbc3df87ef33