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:
parent
404362bfb0
commit
3f611849af
72 changed files with 1513 additions and 293 deletions
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
- name: Add Windows cross-compile target
|
- name: Add Windows cross-compile target
|
||||||
# package.sh's `all` only builds the Windows zip when this 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
|
# 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
|
run: rustup target add x86_64-pc-windows-gnu
|
||||||
|
|
||||||
- name: Cache cargo registry + build
|
- name: Cache cargo registry + build
|
||||||
|
|
@ -87,7 +87,7 @@ jobs:
|
||||||
- `.deb` — Debian / Ubuntu / Mint
|
- `.deb` — Debian / Ubuntu / Mint
|
||||||
- `.rpm` — Fedora / RHEL / openSUSE
|
- `.rpm` — Fedora / RHEL / openSUSE
|
||||||
- `.AppImage` — any Linux (chmod +x and run)
|
- `.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
|
Runtime requirements: yt-dlp, ffmpeg, mpv on PATH (plus
|
||||||
xdg-utils on Linux). The Windows zip's README lists the same.
|
xdg-utils on Linux). The Windows zip's README lists the same.
|
||||||
|
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
# makepkg build artifacts (finished .pkg.tar.zst is NOT ignored)
|
# makepkg build artifacts (finished .pkg.tar.zst is NOT ignored)
|
||||||
/pkg/
|
/pkg/
|
||||||
/src/yt-offline/
|
/src/catacomb/
|
||||||
/yt-offline/
|
/catacomb/
|
||||||
|
|
||||||
# Rendered mdBook output (source lives in docs/src/)
|
# Rendered mdBook output (source lives in docs/src/)
|
||||||
/docs/book/
|
/docs/book/
|
||||||
|
|
@ -27,4 +27,4 @@ cookies.txt
|
||||||
config.toml
|
config.toml
|
||||||
|
|
||||||
# Runtime database (watched status, positions, password hash) — never commit
|
# Runtime database (watched status, positions, password hash) — never commit
|
||||||
yt-offline.db
|
catacomb.db
|
||||||
|
|
|
||||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
|
|
@ -5,25 +5,25 @@
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Debug executable 'yt-offline'",
|
"name": "Debug executable 'catacomb'",
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"run",
|
"run",
|
||||||
"--bin=yt-offline"
|
"--bin=catacomb"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"args": []
|
"args": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug unit tests in executable 'yt-offline'",
|
"name": "Debug unit tests in executable 'catacomb'",
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
"--bin=yt-offline"
|
"--bin=catacomb"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
AGENTS.md
27
AGENTS.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
|
||||||
|
|
||||||
## What this is
|
## 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/
|
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
|
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
|
[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 # 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 <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
|
cargo test --release real_ffmpeg -- --ignored # opt-in: real-ffmpeg fingerprint accuracy/speed check
|
||||||
./target/release/yt-offline # desktop GUI mode (default)
|
./target/release/catacomb # desktop GUI mode (default)
|
||||||
./target/release/yt-offline --web 8080 # headless web server on a port
|
./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)
|
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
|
`bump_library_version()` (the ETag) so `/api/library` stays consistent without a
|
||||||
rescan.
|
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
|
Three `(path|video_id, mtime)`-keyed caches let expensive work happen once and be
|
||||||
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
|
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
|
||||||
`search_meta` + the `video_search` FTS5 index (full-text title/channel/
|
`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
|
### Bundled toolchain & anti-bot
|
||||||
|
|
||||||
`ytdlp_bin.rs` manages an optional self-contained venv at
|
`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
|
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
|
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
|
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
|
## Conventions
|
||||||
|
|
||||||
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
|
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
|
||||||
specific), or `yt-offline.db` (contains the Argon2 password hash). All
|
specific), or `catacomb.db` (plain SQLite app state, including the Argon2
|
||||||
gitignored.
|
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 the absolute cookies path out of any log line surfaced to the UI/API
|
||||||
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
|
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
|
||||||
- `app.rs` and `web.rs` are large (~3–4k lines) because each owns a full UI; new
|
- `app.rs` and `web.rs` are large (~3–4k lines) because each owns a full UI; new
|
||||||
desktop code goes in `app.rs`, web handlers in `web.rs`, shared logic in the
|
desktop code goes in `app.rs`, web handlers in `web.rs`, shared logic in the
|
||||||
focused modules (`downloader`, `database`, `library`, `platform`, `fingerprint`,
|
focused modules (`downloader`, `database`, `library`, `platform`, `fingerprint`,
|
||||||
`vtt`, …).
|
`vtt`, …).
|
||||||
- Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only/no-GTK by
|
- Tray (`ksni`) and the `rfd` xdg-portal dialog backend are Linux-only/no-GTK
|
||||||
design; keep that posture (it's why packaging avoids a GTK dep). Windows/macOS
|
by design (it's why packaging avoids a GTK dep). They're target-gated in
|
||||||
are not yet first-class — the tray would need a per-OS backend.
|
`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.
|
||||||
|
|
|
||||||
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## What this is
|
## 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/
|
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
|
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
|
[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 # 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 <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
|
cargo test --release real_ffmpeg -- --ignored # opt-in: real-ffmpeg fingerprint accuracy/speed check
|
||||||
./target/release/yt-offline # desktop GUI mode (default)
|
./target/release/catacomb # desktop GUI mode (default)
|
||||||
./target/release/yt-offline --web 8080 # headless web server on a port
|
./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)
|
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
|
`bump_library_version()` (the ETag) so `/api/library` stays consistent without a
|
||||||
rescan.
|
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
|
Three `(path|video_id, mtime)`-keyed caches let expensive work happen once and be
|
||||||
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
|
skipped on unchanged files: `info_cache` (parsed `info.json` fields),
|
||||||
`search_meta` + the `video_search` FTS5 index (full-text title/channel/
|
`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
|
### Bundled toolchain & anti-bot
|
||||||
|
|
||||||
`ytdlp_bin.rs` manages an optional self-contained venv at
|
`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
|
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
|
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
|
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
|
## Conventions
|
||||||
|
|
||||||
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
|
- **Never commit** `cookies.txt` (live session creds), `config.toml` (user-
|
||||||
specific), or `yt-offline.db` (contains the Argon2 password hash). All
|
specific), or `catacomb.db` (plain SQLite app state, including the Argon2
|
||||||
gitignored.
|
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 the absolute cookies path out of any log line surfaced to the UI/API
|
||||||
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
|
(`redact_sensitive` in `downloader.rs`) — it leaks `$HOME`.
|
||||||
- `app.rs` and `web.rs` are large (~3–4k lines) because each owns a full UI; new
|
- `app.rs` and `web.rs` are large (~3–4k 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
|
cross-compiles — `cargo check --target x86_64-pc-windows-gnu` is green — so
|
||||||
keep new platform-specific code behind `cfg(unix)`/`cfg(windows)`/
|
keep new platform-specific code behind `cfg(unix)`/`cfg(windows)`/
|
||||||
`cfg(target_os = …)` with a fallback, the way `disk_space`, `plex`, the mpv
|
`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*
|
IPC, and the `chmod` guards already do. Windows now ships as a cross-compiled
|
||||||
(no tray backend, no link/CI yet), but don't reintroduce an unconditional
|
zip from CI; macOS has a local osxcross packaging path but is not in CI and
|
||||||
Linux-only dep.
|
still needs signing/notarization + a real tray backend. Don't reintroduce an
|
||||||
|
unconditional Linux-only dep.
|
||||||
|
|
|
||||||
54
Cargo.lock
generated
54
Cargo.lock
generated
|
|
@ -727,6 +727,33 @@ dependencies = [
|
||||||
"wayland-client",
|
"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]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.62"
|
version = "1.2.62"
|
||||||
|
|
@ -5408,33 +5435,6 @@ dependencies = [
|
||||||
"synstructure",
|
"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]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
|
|
|
||||||
20
Cargo.toml
20
Cargo.toml
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "yt-offline"
|
name = "catacomb"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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."
|
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"
|
depends = "libc6, libxcb1, yt-dlp, ffmpeg, mpv, xdg-utils"
|
||||||
recommends = "libnotify4"
|
recommends = "libnotify4"
|
||||||
assets = [
|
assets = [
|
||||||
["target/release/yt-offline", "usr/bin/", "755"],
|
["target/release/catacomb", "usr/bin/", "755"],
|
||||||
["youtube-backup.desktop", "usr/share/applications/yt-offline.desktop", "644"],
|
["catacomb.desktop", "usr/share/applications/catacomb.desktop", "644"],
|
||||||
["icon.png", "usr/share/pixmaps/yt-offline.png", "644"],
|
["icon.png", "usr/share/pixmaps/catacomb.png", "644"],
|
||||||
["README.md", "usr/share/doc/yt-offline/README.md", "644"],
|
["README.md", "usr/share/doc/catacomb/README.md", "644"],
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── Fedora / RHEL / openSUSE package (cargo-generate-rpm) ─────────────────────
|
# ── 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.
|
# Fedora; ffmpeg lives in RPM Fusion but we still declare the dep.
|
||||||
requires = { yt-dlp = "*", ffmpeg = "*", mpv = "*", "xdg-utils" = "*" }
|
requires = { yt-dlp = "*", ffmpeg = "*", mpv = "*", "xdg-utils" = "*" }
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/yt-offline", dest = "/usr/bin/yt-offline", mode = "755" },
|
{ source = "target/release/catacomb", dest = "/usr/bin/catacomb", mode = "755" },
|
||||||
{ source = "youtube-backup.desktop", dest = "/usr/share/applications/yt-offline.desktop", mode = "644" },
|
{ source = "catacomb.desktop", dest = "/usr/share/applications/catacomb.desktop", mode = "644" },
|
||||||
{ source = "icon.png", dest = "/usr/share/pixmaps/yt-offline.png", mode = "644" },
|
{ source = "icon.png", dest = "/usr/share/pixmaps/catacomb.png", mode = "644" },
|
||||||
{ source = "README.md", dest = "/usr/share/doc/yt-offline/README.md", mode = "644" },
|
{ source = "README.md", dest = "/usr/share/doc/catacomb/README.md", mode = "644" },
|
||||||
{ source = "LICENSE", dest = "/usr/share/licenses/yt-offline/LICENSE", mode = "644" },
|
{ source = "LICENSE", dest = "/usr/share/licenses/catacomb/LICENSE", mode = "644" },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
163
HANDOFF.md
Normal file
163
HANDOFF.md
Normal 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.5–2×), 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 `<` `>` 0–9). 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).
|
||||||
13
PKGBUILD
13
PKGBUILD
|
|
@ -1,5 +1,5 @@
|
||||||
# Maintainer: InannaBeloved <anassaeneroi@pm.me>
|
# Maintainer: InannaBeloved <anassaeneroi@pm.me>
|
||||||
pkgname=yt-offline
|
pkgname=catacomb
|
||||||
pkgver=0.1.0
|
pkgver=0.1.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Self-hosted archive for YouTube, TikTok, Twitch, Vimeo, Bandcamp, SoundCloud, Odysee and more. Desktop GUI + web UI."
|
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
|
# previously broke the rusqlite bundled-sqlite link; thin LTO in Cargo
|
||||||
# does the cross-crate inlining we actually want.
|
# does the cross-crate inlining we actually want.
|
||||||
options=('!lto')
|
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')
|
sha256sums=('SKIP')
|
||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
|
|
@ -44,9 +47,9 @@ build() {
|
||||||
package() {
|
package() {
|
||||||
cd "$pkgname"
|
cd "$pkgname"
|
||||||
|
|
||||||
install -Dm755 target/release/yt-offline "$pkgdir/usr/bin/yt-offline"
|
install -Dm755 target/release/catacomb "$pkgdir/usr/bin/catacomb"
|
||||||
install -Dm644 youtube-backup.desktop "$pkgdir/usr/share/applications/yt-offline.desktop"
|
install -Dm644 catacomb.desktop "$pkgdir/usr/share/applications/catacomb.desktop"
|
||||||
install -Dm644 icon.png "$pkgdir/usr/share/pixmaps/yt-offline.png"
|
install -Dm644 icon.png "$pkgdir/usr/share/pixmaps/catacomb.png"
|
||||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
README.md
50
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# yt-offline
|
# Catacomb
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A self-hosted media archive for YouTube and friends. Pastes any URL, routes it
|
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
|
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.
|
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.
|
first run, the anti-bot stack, troubleshooting, and architecture.
|
||||||
|
|
||||||
## What it backs up
|
## What it backs up
|
||||||
|
|
@ -65,10 +65,11 @@ exact URL to refresh from.
|
||||||
- **Desktop GUI** launches your configured player (mpv by default; mpv gets
|
- **Desktop GUI** launches your configured player (mpv by default; mpv gets
|
||||||
resume-position tracking via JSON-IPC). A floating **transcript window**
|
resume-position tracking via JSON-IPC). A floating **transcript window**
|
||||||
lets you search the subtitles and click any line to seek the running mpv.
|
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
|
- **Web UI** plays videos inline in any browser with one custom player for
|
||||||
**chapters** panel, and a searchable **transcript** pane — click any line
|
direct files and transcoded streams: speed control, caption toggle,
|
||||||
to seek, and the current line highlights as the video plays. Optional
|
keyboard seeking, PiP, fullscreen, saved volume/speed, a **chapters**
|
||||||
on-the-fly **ffmpeg transcoding** for browsers that can't decode MKV.
|
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
|
- **Comment viewer** (web) — for videos archived with comments: threaded
|
||||||
with collapse/expand, in-comment search, sort (top / newest / oldest), an
|
with collapse/expand, in-comment search, sort (top / newest / oldest), an
|
||||||
uploader badge, and a "new since last visit" highlight.
|
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
|
- **9-class error classifier** — every failure gets a one-line suggested fix
|
||||||
(rate-limited, members-only, geoblocked, bad cookies, codec missing, disk
|
(rate-limited, members-only, geoblocked, bad cookies, codec missing, disk
|
||||||
full, …) shown in both UIs.
|
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
|
next to the database, so a GUI launched without a terminal still leaves a
|
||||||
trace.
|
trace.
|
||||||
- **Library backup & restore** — download a snapshot of the SQLite database;
|
- **Library backup & restore** — download a snapshot of the SQLite database;
|
||||||
restore does a schema-validated, idempotent merge (watched / positions /
|
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
|
- Poison-recovering locks keep one handler's panic from taking down the
|
||||||
long-running web server.
|
long-running web server.
|
||||||
|
|
||||||
|
|
@ -127,8 +129,8 @@ exact URL to refresh from.
|
||||||
- **Bind interface picker** — localhost only (default), Tailscale, LAN, or
|
- **Bind interface picker** — localhost only (default), Tailscale, LAN, or
|
||||||
all interfaces.
|
all interfaces.
|
||||||
- **Password-gated UI** when enabled — Argon2 hashed, 256-bit session
|
- **Password-gated UI** when enabled — Argon2 hashed, 256-bit session
|
||||||
tokens, 30-day TTL with lazy pruning, per-IP rate-limit on `/api/login`
|
tokens persisted across restarts, 30-day TTL with lazy pruning, per-IP
|
||||||
(5 failures → 60s lockout).
|
rate-limit on `/api/login` (5 failures → 60s lockout).
|
||||||
- **Security headers:** Content-Security-Policy, X-Frame-Options DENY,
|
- **Security headers:** Content-Security-Policy, X-Frame-Options DENY,
|
||||||
X-Content-Type-Options nosniff, Referrer-Policy no-referrer.
|
X-Content-Type-Options nosniff, Referrer-Policy no-referrer.
|
||||||
- **`Secure` cookie flag** when `X-Forwarded-Proto: https` is present
|
- **`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
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://codeberg.org/anassaeneroi/yt-offline
|
git clone https://codeberg.org/anassaeneroi/catacomb
|
||||||
cd yt-offline
|
cd catacomb
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Desktop GUI
|
# Desktop GUI
|
||||||
./target/release/yt-offline
|
./target/release/catacomb
|
||||||
|
|
||||||
# Or web server only (headless)
|
# 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
|
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**.
|
1. Open **Settings**.
|
||||||
2. Under **yt-dlp binary**, choose **Bundled** and click **Install**.
|
2. Under **yt-dlp binary**, choose **Bundled** and click **Install**.
|
||||||
3. The installer creates `~/.local/share/yt-offline/venv/` with
|
3. The installer creates `~/.local/share/catacomb/venv/` with
|
||||||
`yt-dlp[default]` + `curl_cffi`, plus `~/.local/share/yt-offline/bin/deno`.
|
`yt-dlp[default]` + `curl_cffi`, plus `~/.local/share/catacomb/bin/deno`.
|
||||||
Progress streams into a regular job entry.
|
Progress streams into a regular job entry.
|
||||||
|
|
||||||
Updates are the same button. Switching to **System** uses whatever `yt-dlp`
|
Updates are the same button. Switching to **System** uses whatever `yt-dlp`
|
||||||
|
|
@ -197,8 +199,8 @@ is on PATH instead.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S --needed rust mpv # python3 + python-pip already present
|
sudo pacman -S --needed rust mpv # python3 + python-pip already present
|
||||||
git clone https://codeberg.org/anassaeneroi/yt-offline
|
git clone https://codeberg.org/anassaeneroi/catacomb
|
||||||
cd yt-offline
|
cd catacomb
|
||||||
makepkg -si # or: cargo build --release
|
makepkg -si # or: cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -236,8 +238,8 @@ Tools with the **Desktop development with C++** workload, then:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
winget install mpv.net python.python.3.12
|
winget install mpv.net python.python.3.12
|
||||||
git clone https://codeberg.org/anassaeneroi/yt-offline
|
git clone https://codeberg.org/anassaeneroi/catacomb
|
||||||
cd yt-offline
|
cd catacomb
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -276,7 +278,7 @@ interval_hours = 24
|
||||||
port = 8080
|
port = 8080
|
||||||
bind = "127.0.0.1" # 127.0.0.1 | 0.0.0.0 | a Tailscale/LAN address
|
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)
|
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]
|
[plex]
|
||||||
library_path = "/path/to/plex/TV/youtube" # leave unset to disable
|
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/
|
├── music/
|
||||||
│ └── <artist>/
|
│ └── <artist>/
|
||||||
│ └── Track Title [xyz].opus
|
│ └── Track Title [xyz].opus
|
||||||
└── yt-offline.db ← watched + positions + password hash
|
└── catacomb.db ← plain SQLite app state (watched, positions, password hash, sessions, caches)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
@ -360,7 +362,7 @@ switch to **Bundled** in Settings.
|
||||||
Sessions cleared after the password was changed. Refresh the page.
|
Sessions cleared after the password was changed. Refresh the page.
|
||||||
|
|
||||||
**Videos play but seeking is approximate**
|
**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=`
|
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
|
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
|
exact byte position. Turn transcoding off if your browser can play the
|
||||||
|
|
|
||||||
90
ROADMAP.md
90
ROADMAP.md
|
|
@ -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.
|
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
|
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
|
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
|
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-
|
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
|
### 2.1 Integration test coverage — DONE
|
||||||
|
|
||||||
98 unit tests cover parsers/helpers/resolvers; `tests/api.rs` adds 7
|
128 passing unit tests cover parsers/helpers/resolvers, plus one ignored
|
||||||
end-to-end tests that spawn the **real** `--web` binary against a scratch
|
real-ffmpeg fingerprint check for opt-in local validation. `tests/api.rs`
|
||||||
tempdir and drive the HTTP API with curl (index/library serving, ETag
|
adds 11 end-to-end tests that spawn the **real** `--web` binary against a
|
||||||
304, settings round-trip + persistence, folders CRUD + cycle guard, notes
|
scratch tempdir and drive the HTTP API with curl (index/library serving,
|
||||||
round-trip, channel-options round-trip + clear, DB backup). Each test
|
ETag 304, settings round-trip + persistence, folders CRUD + cycle guard,
|
||||||
gets its own server/port/tempdir, so they run in parallel — `cargo test`
|
notes round-trip, channel-options round-trip + clear, DB backup, full-text
|
||||||
runs the lot. (A `.forgejo/workflows/test.yml` CI definition exists, but
|
search, podcast/feed-token auth, RSS enclosures, and perceptual dedup).
|
||||||
Codeberg executes Woodpecker, not Forgejo Actions, so it's inert there
|
Each test gets its own server/port/tempdir, so they run in parallel —
|
||||||
without a self-hosted runner; tests run locally.) Stretch left: a
|
`cargo test --release` runs the lot. (A `.forgejo/workflows/test.yml` CI
|
||||||
recorded-fixture corpus for the download pipeline and a headless web-UI
|
definition exists, but Codeberg executes Woodpecker, not Forgejo Actions,
|
||||||
test.
|
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
|
### 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
|
**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
|
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
|
warnings). `scripts/package.sh win` (and `all`, when the cross toolchain
|
||||||
is present) cross-compiles and bundles it into
|
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
|
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
|
`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
|
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
|
**macOS has a packaging path, pending the toolchain.** `scripts/package.sh
|
||||||
mac` cross-compiles an Apple target (`MAC_ARCH=arm64` default, or
|
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
|
(`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
|
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
|
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 —
|
current line) and a floating desktop window that seeks mpv over IPC —
|
||||||
backed by a shared `vtt` WebVTT/SRT parser.
|
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.
|
### 4.1 Living-room mode
|
||||||
- AI summarisation of videos (transcript → bullet points).
|
|
||||||
- Multi-user accounts with per-user watched/positions (currently single-user).
|
A remote-friendly web UI for couch/TV use: large focus states, D-pad
|
||||||
- Integration with Plex / Jellyfin / Kodi as a *source plugin* rather than a
|
navigation, queue/playlist controls, "continue watching" as the default
|
||||||
symlink generator.
|
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)
|
## 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
|
`start=` offset and the web player uses custom controls for live ffmpeg
|
||||||
streams, so MKV/H.264 transcode mode can scrub, resume, jump chapters,
|
streams, so MKV/H.264 transcode mode can scrub, resume, jump chapters,
|
||||||
and sync transcript highlights even though the pipe has no byte ranges.
|
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
|
- **Federation / multi-host** (3.5) — read-only peer libraries with
|
||||||
tokenized direct media URLs, so videos stream from the peer rather than
|
tokenized direct media URLs, so videos stream from the peer rather than
|
||||||
proxying through this instance.
|
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
|
search + transcript tooling (3.9), and the **Windows** half of 3.1
|
||||||
(cross-compiled .zip + release CI). Still open: a **macOS** binary
|
(cross-compiled .zip + release CI). Still open: a **macOS** binary
|
||||||
(3.1; needs a Mac runner), Android (3.2), and plugin hooks (3.8).
|
(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
|
Items inside a phase are loosely ordered by user-visible impact, not strict
|
||||||
prerequisite. With Windows banked, the highest-leverage remaining moves are
|
prerequisite. With Windows banked, the highest-leverage remaining moves are
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Security Audit — yt-offline
|
# Security Audit — catacomb
|
||||||
|
|
||||||
**Date:** 2026-05-23 (re-audit after security hardening commit `5999673`)
|
**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.)
|
**Earlier audit:** 2026-05-17 (commit pre-dating session auth, CSP, SHA-256 verify, etc.)
|
||||||
**Scope:** Rust codebase + embedded web UI + dependencies
|
**Scope:** Rust codebase + embedded web UI + dependencies
|
||||||
**Threat model:** Self-hosted personal archiving tool. Primary deployment is
|
**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 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 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).
|
- 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
|
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`).
|
- Argon2-hashed password in the `settings` table (never in `config.toml`).
|
||||||
- 256-bit `rand::thread_rng()` (CSPRNG) session tokens, hex-encoded.
|
- 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).
|
- `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.
|
- 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.
|
- `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.
|
**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.)
|
- `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
|
### ⚠️ 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) |
|
| Optional HTTP password auth | ✅ added (Argon2 + sessions + rate limit) |
|
||||||
| Filter yt-dlp stderr | ⚠️ still echoed (accepted) |
|
| Filter yt-dlp stderr | ⚠️ still echoed (accepted) |
|
||||||
| chmod 600 on cookies.txt | ⚠️ documented; not automated |
|
| 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 |
|
| Session token TTL + GC | ✅ added |
|
||||||
| `Secure` cookie flag for HTTPS | ✅ added (gated on `X-Forwarded-Proto`) |
|
| `Secure` cookie flag for HTTPS | ✅ added (gated on `X-Forwarded-Proto`) |
|
||||||
| Login rate limiting | ✅ added (5 / 60 s per IP) |
|
| 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
|
7. **TLS terminator in-process.** Optional `rustls` integration so the
|
||||||
app can serve HTTPS without a reverse proxy. Currently considered
|
app can serve HTTPS without a reverse proxy. Currently considered
|
||||||
out of scope.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=YT Offline
|
Name=Catacomb
|
||||||
GenericName=Video Archiver
|
GenericName=Video Archiver
|
||||||
Comment=Self-hosted archive for YouTube, TikTok, Twitch, and more
|
Comment=Self-hosted archive for YouTube, TikTok, Twitch, and more
|
||||||
Exec=yt-offline
|
Exec=catacomb
|
||||||
Icon=yt-offline
|
Icon=catacomb
|
||||||
Categories=AudioVideo;Video;Network;
|
Categories=AudioVideo;Video;Network;
|
||||||
Keywords=youtube;yt-dlp;archive;backup;download;
|
Keywords=youtube;yt-dlp;archive;backup;download;
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Packaging yt-offline
|
# Packaging Catacomb
|
||||||
|
|
||||||
This document covers building distributable packages. The one-liner:
|
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
|
```sh
|
||||||
scripts/package.sh deb
|
scripts/package.sh deb
|
||||||
sudo apt install ./dist/yt-offline_*_amd64.deb
|
sudo apt install ./dist/catacomb_*_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
### .rpm (Fedora / RHEL / openSUSE)
|
### .rpm (Fedora / RHEL / openSUSE)
|
||||||
|
|
@ -34,7 +34,7 @@ already-built (and release-profile-stripped) binary.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/package.sh rpm
|
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/);
|
Note: `ffmpeg` on Fedora lives in [RPM Fusion](https://rpmfusion.org/);
|
||||||
|
|
@ -50,8 +50,8 @@ would balloon the image and tangle the licensing.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/package.sh appimage
|
scripts/package.sh appimage
|
||||||
chmod +x dist/yt-offline-*-x86_64.AppImage
|
chmod +x dist/catacomb-*-x86_64.AppImage
|
||||||
./dist/yt-offline-*-x86_64.AppImage
|
./dist/catacomb-*-x86_64.AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arch Linux
|
## Arch Linux
|
||||||
|
|
@ -82,11 +82,11 @@ sudo apt install mingw-w64 zip # or your distro's equivalent
|
||||||
scripts/package.sh win
|
scripts/package.sh win
|
||||||
```
|
```
|
||||||
|
|
||||||
This produces `dist/yt-offline-<ver>-x86_64-windows.zip` containing
|
This produces `dist/catacomb-<ver>-x86_64-windows.zip` containing
|
||||||
`yt-offline.exe`, `LICENSE.txt`, and a `README.txt` listing the runtime
|
`catacomb.exe`, `LICENSE.txt`, and a `README.txt` listing the runtime
|
||||||
PATH deps (yt-dlp / ffmpeg / mpv). Release builds link as a GUI-subsystem
|
PATH deps (yt-dlp / ffmpeg / mpv). Release builds link as a GUI-subsystem
|
||||||
app (no console window); the binary reattaches to the launching terminal
|
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
|
--web 8080` from PowerShell prints its logs, while a double-click stays
|
||||||
windowless.
|
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
|
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
|
(`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
|
`.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
|
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.
|
are expected on PATH as on the other platforms.
|
||||||
|
|
||||||
A `.dmg` and codesigning/notarization (and a `tray-icon`-based macOS tray)
|
A `.dmg` and codesigning/notarization (and a `tray-icon`-based macOS tray)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[book]
|
[book]
|
||||||
title = "yt-offline"
|
title = "Catacomb"
|
||||||
description = "Self-hosted archive for YouTube, TikTok, Twitch and more. Setup, usage, anti-bot, troubleshooting, and architecture."
|
description = "Self-hosted archive for YouTube, TikTok, Twitch and more. Setup, usage, anti-bot, troubleshooting, and architecture."
|
||||||
authors = ["InannaBeloved"]
|
authors = ["InannaBeloved"]
|
||||||
language = "en"
|
language = "en"
|
||||||
|
|
@ -8,5 +8,5 @@ src = "src"
|
||||||
[output.html]
|
[output.html]
|
||||||
default-theme = "navy"
|
default-theme = "navy"
|
||||||
preferred-dark-theme = "navy"
|
preferred-dark-theme = "navy"
|
||||||
git-repository-url = "https://codeberg.org/anassaeneroi/yt-offline"
|
git-repository-url = "https://codeberg.org/anassaeneroi/catacomb"
|
||||||
edit-url-template = "https://codeberg.org/anassaeneroi/yt-offline/_edit/main/docs/{path}"
|
edit-url-template = "https://codeberg.org/anassaeneroi/catacomb/_edit/main/docs/{path}"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[Introduction](./introduction.md)
|
[Introduction](./introduction.md)
|
||||||
|
|
||||||
# Using yt-offline
|
# Using catacomb
|
||||||
|
|
||||||
- [Installation](./installation.md)
|
- [Installation](./installation.md)
|
||||||
- [First run & configuration](./first-run.md)
|
- [First run & configuration](./first-run.md)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Staying ahead of YouTube's bot detection
|
# Staying ahead of YouTube's bot detection
|
||||||
|
|
||||||
YouTube increasingly fingerprints and rate-limits automated clients.
|
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."
|
difference between "everything downloads" and "constant captchas."
|
||||||
|
|
||||||
In rough order of impact:
|
In rough order of impact:
|
||||||
|
|
@ -10,13 +10,13 @@ In rough order of impact:
|
||||||
|
|
||||||
This is the single biggest factor. **Anonymous requests get captcha-
|
This is the single biggest factor. **Anonymous requests get captcha-
|
||||||
walled the hardest.** Provide a `cookies.txt` exported from a browser
|
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.
|
browser profile directly.
|
||||||
|
|
||||||
A *valid* logged-in jar contains the auth cookies `SID`, `SAPISID`,
|
A *valid* logged-in jar contains the auth cookies `SID`, `SAPISID`,
|
||||||
`__Secure-1PSID`, `__Secure-3PSID`, `LOGIN_INFO`, etc. A jar with only
|
`__Secure-1PSID`, `__Secure-3PSID`, `LOGIN_INFO`, etc. A jar with only
|
||||||
`VISITOR_INFO1_LIVE`, `PREF`, `YSC` is **anonymous** — it's not signed in
|
`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.
|
Settings → Cookies panel warns you when your jar is anonymous or expired.
|
||||||
|
|
||||||
Two ways to supply cookies:
|
Two ways to supply cookies:
|
||||||
|
|
@ -37,7 +37,7 @@ Two ways to supply cookies:
|
||||||
subdirectory itself). The advantage: cookies are read fresh from the
|
subdirectory itself). The advantage: cookies are read fresh from the
|
||||||
live session each download, so they never go stale.
|
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
|
> `cookies.txt` unprompted, and redacts the cookie path out of any log
|
||||||
> line shown in the UI.
|
> 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
|
yt-dlp's `--impersonate` makes requests carry a real browser's TLS
|
||||||
fingerprint (via `curl_cffi`), so the connection doesn't *look* like a
|
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.
|
picks an impersonation target per platform.
|
||||||
|
|
||||||
If impersonation silently does nothing, it's almost always a
|
If impersonation silently does nothing, it's almost always a
|
||||||
|
|
@ -58,7 +58,7 @@ is present). See
|
||||||
## 3. POT tokens (Proof-of-Origin)
|
## 3. POT tokens (Proof-of-Origin)
|
||||||
|
|
||||||
YouTube increasingly binds a per-video **Proof-of-Origin token** to
|
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
|
[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.
|
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
|
> **Version-skew footgun:** the yt-dlp plugin must come from the *same
|
||||||
> release* as the bgutil-pot server binary — **not** the PyPI package,
|
> release* as the bgutil-pot server binary — **not** the PyPI package,
|
||||||
> which versions independently and silently produces no tokens on a
|
> 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.
|
> version-matched plugin zip from the server's release.
|
||||||
|
|
||||||
## 4. Player-client selection
|
## 4. Player-client selection
|
||||||
|
|
||||||
YouTube cracks down on different internal "player clients" over time —
|
YouTube cracks down on different internal "player clients" over time —
|
||||||
the `web` client is currently the most captcha-prone, while `tv` and
|
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
|
pick good defaults. If a specific channel keeps hitting captchas, set a
|
||||||
client override (global or per-channel):
|
client override (global or per-channel):
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ tv,mweb
|
||||||
|
|
||||||
## 5. Throttling
|
## 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
|
a small jittered pause between videos (a fixed cadence looks robotic; a
|
||||||
random one looks human) and, after any rate-limit hit, triples the
|
random one looks human) and, after any rate-limit hit, triples the
|
||||||
sleeps for the rest of the batch before recovering.
|
sleeps for the rest of the batch before recovering.
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ fallback.
|
||||||
## Anti-bot subsystems
|
## Anti-bot subsystems
|
||||||
|
|
||||||
`ytdlp_bin.rs` manages the optional self-contained venv at
|
`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
|
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
|
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
|
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
|
and drives the HTTP API with curl — genuine end-to-end coverage of the
|
||||||
axum + SQLite + config stack.
|
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
|
exists, but Codeberg runs Woodpecker rather than Forgejo Actions, so it
|
||||||
doesn't execute there without a self-hosted runner — run the suite
|
doesn't execute there without a self-hosted runner — run the suite
|
||||||
locally.)
|
locally.)
|
||||||
|
|
@ -103,8 +103,9 @@ locally.)
|
||||||
## Platform support
|
## Platform support
|
||||||
|
|
||||||
Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only / no-GTK
|
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
|
by design — that's why packaging avoids a GTK dependency. Those Linux-only
|
||||||
aren't first-class yet: the tray needs a per-OS backend before a clean
|
pieces are target-gated: Windows builds use native file dialogs and a no-op
|
||||||
cross-build. The rest (eframe/wgpu, axum, rusqlite-bundled) already
|
tray stub, and the repo now ships a cross-compiled Windows zip. macOS has a
|
||||||
compiles cross-platform, and `ytdlp_bin` already has `cfg!(windows)`
|
local osxcross packaging path, but still needs on-hardware validation,
|
||||||
branches.
|
codesigning/notarization, and a real tray backend before it is a normal
|
||||||
|
release artifact.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
## Starting a download
|
## Starting a download
|
||||||
|
|
||||||
Paste any supported URL into the download bar (desktop) or the ⬇
|
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
|
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.
|
the whole thing; a single-video URL pulls just that one.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## The config file
|
## 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
|
directory you launch it from), not a fixed path. The same goes for
|
||||||
`cookies.txt`. Everything in `config.toml` is also editable in Settings;
|
`cookies.txt`. Everything in `config.toml` is also editable in Settings;
|
||||||
edits there are written back to the file.
|
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
|
music/ ← audio-only "Music mode" downloads, by artist
|
||||||
archive.txt ← yt-dlp's global download archive
|
archive.txt ← yt-dlp's global download archive
|
||||||
cookies.txt ← optional, if you set one
|
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
|
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:
|
In **Settings → yt-dlp binary** you choose:
|
||||||
|
|
||||||
- **System** — uses whatever `yt-dlp` is on your `PATH`.
|
- **System** — uses whatever `yt-dlp` is on your `PATH`.
|
||||||
- **Bundled** — click **Install** and yt-offline builds a self-contained
|
- **Bundled** — click **Install** and catacomb builds a self-contained
|
||||||
venv at `~/.local/share/yt-offline/`: nightly `yt-dlp[default]` +
|
venv at `~/.local/share/catacomb/`: nightly `yt-dlp[default]` +
|
||||||
`curl_cffi` (TLS impersonation) + a bundled `deno` (player-JS). The
|
`curl_cffi` (TLS impersonation) + a bundled `deno` (player-JS). The
|
||||||
same button updates it later.
|
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
|
## The two front-ends
|
||||||
|
|
||||||
- `yt-offline` — desktop GUI (eframe/egui).
|
- `catacomb` — desktop GUI (eframe/egui).
|
||||||
- `yt-offline --web [PORT]` — headless web server. Bind to `127.0.0.1`
|
- `catacomb --web [PORT]` — headless web server. Bind to `127.0.0.1`
|
||||||
(default) for localhost-only, a Tailscale address for your tailnet, or
|
(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
|
`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
|
beyond localhost — the UI and all `/api` routes are then gated behind an
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Installation
|
# 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.
|
build from source, or grab the AppImage.
|
||||||
|
|
||||||
## Runtime dependencies
|
## Runtime dependencies
|
||||||
|
|
@ -8,7 +8,7 @@ build from source, or grab the AppImage.
|
||||||
Whichever way you install, these are invoked as subprocesses at runtime:
|
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-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
|
- **ffmpeg** — muxing, format conversion, on-the-fly transcode for the
|
||||||
web player.
|
web player.
|
||||||
- **mpv** — the default desktop player (any player taking a file path
|
- **mpv** — the default desktop player (any player taking a file path
|
||||||
|
|
@ -21,14 +21,14 @@ Releases attach `.deb`, `.rpm`, and `.AppImage` artifacts.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Debian / Ubuntu / Mint
|
# Debian / Ubuntu / Mint
|
||||||
sudo apt install ./yt-offline_*_amd64.deb
|
sudo apt install ./catacomb_*_amd64.deb
|
||||||
|
|
||||||
# Fedora / RHEL / openSUSE (ffmpeg via RPM Fusion)
|
# 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
|
# Any Linux — AppImage
|
||||||
chmod +x yt-offline-*-x86_64.AppImage
|
chmod +x catacomb-*-x86_64.AppImage
|
||||||
./yt-offline-*-x86_64.AppImage
|
./catacomb-*-x86_64.AppImage
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arch / CachyOS / Manjaro
|
## 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
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
source "$HOME/.cargo/env"
|
source "$HOME/.cargo/env"
|
||||||
|
|
||||||
git clone https://codeberg.org/anassaeneroi/yt-offline
|
git clone https://codeberg.org/anassaeneroi/catacomb
|
||||||
cd yt-offline
|
cd catacomb
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/yt-offline # desktop GUI
|
./target/release/catacomb # desktop GUI
|
||||||
./target/release/yt-offline --web 8080 # headless web server
|
./target/release/catacomb --web 8080 # headless web server
|
||||||
```
|
```
|
||||||
|
|
||||||
`python3-venv` is only needed for the bundled-yt-dlp install path; skip
|
`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
|
## Windows / macOS
|
||||||
|
|
||||||
Not first-class yet. The Linux-only system tray (`ksni`) and file dialog
|
Windows is available as a release zip when built through
|
||||||
(`rfd` xdg-portal) need per-OS backends before a clean cross-build; the
|
`scripts/package.sh win` or tag-release CI. It includes `catacomb.exe`,
|
||||||
rest of the stack already compiles. See the
|
the license, and a short runtime-dependency README; install `yt-dlp`,
|
||||||
[architecture notes](./architecture.md#platform-support).
|
`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).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# yt-offline
|
# Catacomb
|
||||||
|
|
||||||
A self-hosted media archive for YouTube and friends. Paste any URL, it
|
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
|
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
|
## Why it exists
|
||||||
|
|
||||||
Tartube is the mature open-source yt-dlp GUI and the benchmark in this
|
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
|
doesn't have: a real web UI reachable from any device, a single-binary
|
||||||
distribution with a bundled toolchain, a modern security model
|
distribution with a bundled toolchain, a modern security model
|
||||||
(password-gated UI, Argon2, rate-limited login), a built-in anti-bot
|
(password-gated UI, Argon2, rate-limited login), a built-in anti-bot
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,20 @@
|
||||||
Build distributable Linux packages with one script:
|
Build distributable Linux packages with one script:
|
||||||
|
|
||||||
```sh
|
```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 deb # just the .deb
|
||||||
scripts/package.sh rpm
|
scripts/package.sh rpm
|
||||||
scripts/package.sh appimage
|
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,
|
It builds the release binary once and reuses it for every format,
|
||||||
installing `cargo-deb` / `cargo-generate-rpm` on demand and downloading
|
installing `cargo-deb` / `cargo-generate-rpm` on demand and downloading
|
||||||
`appimagetool` to `dist/tools/` on first AppImage build. Per-format
|
`appimagetool` to `dist/tools/` on first AppImage build. Windows is included
|
||||||
failures are isolated and summarized at the end. Output (gitignored)
|
when the mingw target/toolchain is present; macOS is local-only because
|
||||||
lands in `dist/`.
|
osxcross needs an Apple SDK. Per-format failures are isolated and summarized
|
||||||
|
at the end. Output (gitignored) lands in `dist/`.
|
||||||
|
|
||||||
## Formats
|
## 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
|
then, build packages locally with `scripts/package.sh` and publish docs
|
||||||
with `scripts/publish-docs.sh`.
|
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.
|
has the per-distro install commands and the Windows/macOS status in full.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Troubleshooting
|
# 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
|
a one-line suggested fix next to the failed job. This page expands on the
|
||||||
most common ones, plus a few non-download issues.
|
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.
|
2. **Switch to bundled (nightly) yt-dlp** if you're on system stable.
|
||||||
3. **Enable the POT token provider.**
|
3. **Enable the POT token provider.**
|
||||||
4. **Try a player-client override** of `tv,mweb` for that channel.
|
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.
|
rate-limits after a cooldown.
|
||||||
|
|
||||||
## Impersonate targets show "(unavailable)"
|
## 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
|
**Cause:** the yt-dlp plugin came from PyPI (Brainicism's package, which
|
||||||
versions independently) instead of the jim60105 Rust server's release.
|
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
|
the version-matched plugin zip from the same release as the server
|
||||||
binary. Don't `pip install bgutil-ytdlp-pot-provider` yourself.
|
binary. Don't `pip install bgutil-ytdlp-pot-provider` yourself.
|
||||||
|
|
||||||
|
|
@ -68,14 +68,14 @@ see [Anti-bot](./anti-bot.md).
|
||||||
|
|
||||||
## Downloads stall forever
|
## 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
|
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
|
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.
|
issue with that source; check the job log in the Downloads panel.
|
||||||
|
|
||||||
## Disk fills up / downloads fail with ENOSPC
|
## 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
|
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
|
clear "disk full" failure rather than a half-written file. Free space and
|
||||||
retry.
|
retry.
|
||||||
|
|
@ -103,13 +103,13 @@ broken Vulkan stack, the OS can create the window while wgpu never
|
||||||
presents a frame. Try the OpenGL renderer:
|
presents a frame. Try the OpenGL renderer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
YT_OFFLINE_RENDERER=glow yt-offline
|
YT_OFFLINE_RENDERER=glow catacomb
|
||||||
```
|
```
|
||||||
|
|
||||||
or:
|
or:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yt-offline --renderer glow
|
catacomb --renderer glow
|
||||||
```
|
```
|
||||||
|
|
||||||
If that works, update/reinstall your Vulkan driver later and switch back
|
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 /
|
- **The job log** — every download/transcode job keeps its full yt-dlp /
|
||||||
ffmpeg output in the Downloads panel (expand the job).
|
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
|
any thread (UI, web worker, download) is appended here with a
|
||||||
timestamp, so it survives a GUI launched without a terminal.
|
timestamp, so it survives a GUI launched without a terminal.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
A structured analysis of [Tartube v2.5.231](https://github.com/axcore/tartube)
|
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
|
(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.
|
"What we can do" note about whether to adopt, adapt, or skip.
|
||||||
|
|
||||||
Tartube codebase: ~146,000 lines of Python 3 across 19 modules under
|
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
|
**What we can do:** A handful of these tabs map to features we
|
||||||
literally don't have (FFmpeg presets, comments, profiles, scheduling
|
literally don't have (FFmpeg presets, comments, profiles, scheduling
|
||||||
calendar). They're roadmap items, not config surface. The actual
|
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
|
- backup dir, max concurrent, bundled-or-system yt-dlp
|
||||||
- player command, browser (for `--cookies-from-browser`)
|
- player command, browser (for `--cookies-from-browser`)
|
||||||
|
|
@ -568,7 +568,7 @@ For the record (to keep momentum honest):
|
||||||
|
|
||||||
## 12. Specification for "Tartube parity" milestone
|
## 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.
|
the following are all true.
|
||||||
|
|
||||||
- [x] **Per-target download options** with cascade resolution and a
|
- [x] **Per-target download options** with cascade resolution and a
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build distributable packages for yt-offline.
|
# Build distributable packages for catacomb.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/package.sh [deb|rpm|appimage|win|mac|all]
|
# scripts/package.sh [deb|rpm|appimage|win|mac|all]
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
# macOS notes: cross-compiling for Apple targets needs osxcross
|
# macOS notes: cross-compiling for Apple targets needs osxcross
|
||||||
# (https://github.com/tpoechtrager/osxcross) built with a macOS SDK, and its
|
# (https://github.com/tpoechtrager/osxcross) built with a macOS SDK, and its
|
||||||
# wrapper compilers (oa64-clang / o64-clang) on PATH. Set MAC_ARCH=arm64
|
# 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
|
# 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.
|
# via right-click → Open (or `xattr -dr com.apple.quarantine`) the first time.
|
||||||
# See docs/PACKAGING.md.
|
# See docs/PACKAGING.md.
|
||||||
|
|
@ -39,7 +39,7 @@ mkdir -p "$DIST" "$TOOLS"
|
||||||
|
|
||||||
# Pull version + name straight from Cargo.toml so packages stay in sync.
|
# Pull version + name straight from Cargo.toml so packages stay in sync.
|
||||||
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
|
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)"
|
||||||
PKG="yt-offline"
|
PKG="catacomb"
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
say() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
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"
|
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 -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.
|
# 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"
|
install -Dm644 icon.png "$appdir/usr/share/icons/hicolor/256x256/apps/$PKG.png"
|
||||||
cp icon.png "$appdir/$PKG.png"
|
cp icon.png "$appdir/$PKG.png"
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ build_appimage() {
|
||||||
cat > "$appdir/AppRun" <<'APPRUN'
|
cat > "$appdir/AppRun" <<'APPRUN'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
HERE="$(dirname "$(readlink -f "${0}")")"
|
HERE="$(dirname "$(readlink -f "${0}")")"
|
||||||
exec "$HERE/usr/bin/yt-offline" "$@"
|
exec "$HERE/usr/bin/catacomb" "$@"
|
||||||
APPRUN
|
APPRUN
|
||||||
chmod +x "$appdir/AppRun"
|
chmod +x "$appdir/AppRun"
|
||||||
|
|
||||||
|
|
@ -185,14 +185,14 @@ build_win() {
|
||||||
install -m755 "$exe" "$staging/$PKG.exe"
|
install -m755 "$exe" "$staging/$PKG.exe"
|
||||||
install -m644 LICENSE "$staging/LICENSE.txt"
|
install -m644 LICENSE "$staging/LICENSE.txt"
|
||||||
cat > "$staging/README.txt" <<README
|
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
|
To run the headless web server, open PowerShell or Command Prompt in this
|
||||||
folder and run:
|
folder and run:
|
||||||
|
|
||||||
.\\yt-offline.exe --web 8080
|
.\\catacomb.exe --web 8080
|
||||||
|
|
||||||
then browse to http://localhost:8080. (Launched from a terminal the server
|
then browse to http://localhost:8080. (Launched from a terminal the server
|
||||||
prints its logs to that terminal; double-clicked it runs windowless.)
|
prints its logs to that terminal; double-clicked it runs windowless.)
|
||||||
|
|
@ -231,7 +231,7 @@ win_available() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── macOS .app .zip (cross-compiled via osxcross) ────────────────────────────
|
# ── 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
|
# 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
|
# 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
|
# 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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleName</key> <string>yt-offline</string>
|
<key>CFBundleName</key> <string>Catacomb</string>
|
||||||
<key>CFBundleDisplayName</key> <string>yt-offline</string>
|
<key>CFBundleDisplayName</key> <string>Catacomb</string>
|
||||||
<key>CFBundleIdentifier</key> <string>org.codeberg.yt-offline</string>
|
<key>CFBundleIdentifier</key> <string>org.codeberg.catacomb</string>
|
||||||
<key>CFBundleVersion</key> <string>${VERSION}</string>
|
<key>CFBundleVersion</key> <string>${VERSION}</string>
|
||||||
<key>CFBundleShortVersionString</key> <string>${VERSION}</string>
|
<key>CFBundleShortVersionString</key> <string>${VERSION}</string>
|
||||||
<key>CFBundleExecutable</key> <string>${PKG}</string>
|
<key>CFBundleExecutable</key> <string>${PKG}</string>
|
||||||
|
|
|
||||||
26
src/app.rs
26
src/app.rs
|
|
@ -392,7 +392,7 @@ impl App {
|
||||||
// Open the DB first so the library scanner can use info_cache to
|
// Open the DB first so the library scanner can use info_cache to
|
||||||
// skip re-parsing unchanged info.json sidecars. Cold start still
|
// skip re-parsing unchanged info.json sidecars. Cold start still
|
||||||
// pays full cost; every subsequent launch is faster.
|
// 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)
|
let db = Database::open(&db_path)
|
||||||
.unwrap_or_else(|_| Database::open_in_memory().expect("in-memory db failed"));
|
.unwrap_or_else(|_| Database::open_in_memory().expect("in-memory db failed"));
|
||||||
// Defer the (potentially multi-minute, disk-bound) library scan +
|
// Defer the (potentially multi-minute, disk-bound) library scan +
|
||||||
|
|
@ -409,7 +409,7 @@ impl App {
|
||||||
let channels_root = channels_root.clone();
|
let channels_root = channels_root.clone();
|
||||||
let ctx = cc.egui_ctx.clone();
|
let ctx = cc.egui_ctx.clone();
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("yt-offline-libscan".into())
|
.name("catacomb-libscan".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let mut library = library::scan_channels_with_cache(&channels_root, Some(&db));
|
let mut library = library::scan_channels_with_cache(&channels_root, Some(&db));
|
||||||
// Hydrate per-channel download options + folder assignments
|
// Hydrate per-channel download options + folder assignments
|
||||||
|
|
@ -493,7 +493,7 @@ impl App {
|
||||||
let tx = thumb_result_tx.clone();
|
let tx = thumb_result_tx.clone();
|
||||||
let ctx = cc.egui_ctx.clone();
|
let ctx = cc.egui_ctx.clone();
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name(format!("yt-offline-thumb-{worker_id}"))
|
.name(format!("catacomb-thumb-{worker_id}"))
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
// Hold the lock only for the recv itself; release
|
// 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();
|
self.status = "Play this video first, then click a line to seek".into();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let sock = format!("/tmp/yt-offline-{id}.sock");
|
let sock = format!("/tmp/catacomb-{id}.sock");
|
||||||
match UnixStream::connect(&sock) {
|
match UnixStream::connect(&sock) {
|
||||||
Ok(mut s) => {
|
Ok(mut s) => {
|
||||||
let cmd = format!("{{\"command\":[\"seek\",{secs},\"absolute\"]}}\n");
|
let cmd = format!("{{\"command\":[\"seek\",{secs},\"absolute\"]}}\n");
|
||||||
|
|
@ -1216,7 +1216,7 @@ impl App {
|
||||||
let use_mpv_ipc = exe == "mpv" || exe == "mpv.exe";
|
let use_mpv_ipc = exe == "mpv" || exe == "mpv.exe";
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[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);
|
let mut child_cmd = Command::new(&cmd);
|
||||||
|
|
||||||
|
|
@ -1415,7 +1415,7 @@ impl App {
|
||||||
format!("Download failed: {label}")
|
format!("Download failed: {label}")
|
||||||
};
|
};
|
||||||
let _ = notify_rust::Notification::new()
|
let _ = notify_rust::Notification::new()
|
||||||
.summary("yt-offline")
|
.summary("Catacomb")
|
||||||
.body(&summary)
|
.body(&summary)
|
||||||
.timeout(notify_rust::Timeout::Milliseconds(4000))
|
.timeout(notify_rust::Timeout::Milliseconds(4000))
|
||||||
.show();
|
.show();
|
||||||
|
|
@ -1430,7 +1430,7 @@ impl App {
|
||||||
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
|
||||||
ui.add_space(2.0);
|
ui.add_space(2.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.heading("yt-offline");
|
ui.heading("Catacomb");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label("🔍");
|
ui.label("🔍");
|
||||||
ui.add(
|
ui.add(
|
||||||
|
|
@ -2481,7 +2481,7 @@ impl App {
|
||||||
ui.heading("🌐 Remote libraries");
|
ui.heading("🌐 Remote libraries");
|
||||||
});
|
});
|
||||||
ui.label(egui::RichText::new(
|
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());
|
.weak().small());
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
|
@ -3234,7 +3234,7 @@ impl App {
|
||||||
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, false, "System")
|
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, false, "System")
|
||||||
.on_hover_text("Use whatever yt-dlp is on PATH.");
|
.on_hover_text("Use whatever yt-dlp is on PATH.");
|
||||||
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, true, "Bundled")
|
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 installed = crate::ytdlp_bin::bundled_installed();
|
||||||
let btn_label = if installed { "Update" } else { "Install" };
|
let btn_label = if installed { "Update" } else { "Install" };
|
||||||
if ui.button(btn_label)
|
if ui.button(btn_label)
|
||||||
|
|
@ -3559,7 +3559,7 @@ impl App {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(
|
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 \
|
waiting / channel-options / folders). Restore by copying it back \
|
||||||
into your channels directory before launch.",
|
into your channels directory before launch.",
|
||||||
)
|
)
|
||||||
|
|
@ -3572,7 +3572,7 @@ impl App {
|
||||||
// cookies file-picker pattern.
|
// cookies file-picker pattern.
|
||||||
let tx = self.backup_save_tx.clone();
|
let tx = self.backup_save_tx.clone();
|
||||||
let default_name = format!(
|
let default_name = format!(
|
||||||
"yt-offline-{}.db",
|
"catacomb-{}.db",
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs()).unwrap_or(0),
|
.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()),
|
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() {
|
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) {
|
match std::fs::copy(&src, &dest) {
|
||||||
Ok(bytes) => self.status = format!(
|
Ok(bytes) => self.status = format!(
|
||||||
"Backup saved ({} → {})",
|
"Backup saved ({} → {})",
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ pub struct Config {
|
||||||
pub subtitles: SubtitlesSection,
|
pub subtitles: SubtitlesSection,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub convert: ConvertSection,
|
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
|
/// read-only from this one (see [`crate::remote`]). Each is a
|
||||||
/// `[[remote]]` table in config.toml.
|
/// `[[remote]]` table in config.toml.
|
||||||
#[serde(default, rename = "remote")]
|
#[serde(default, rename = "remote")]
|
||||||
pub remotes: Vec<RemoteSection>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct RemoteSection {
|
pub struct RemoteSection {
|
||||||
/// Display name in the UI's remote switcher.
|
/// 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).
|
/// automatically when a slot opens. Set to 0 for no limit (not recommended).
|
||||||
#[serde(default = "default_max_concurrent")]
|
#[serde(default = "default_max_concurrent")]
|
||||||
pub max_concurrent: usize,
|
pub max_concurrent: usize,
|
||||||
/// If true, use the bundled yt-dlp + deno binaries managed by yt-offline
|
/// If true, use the bundled yt-dlp + deno binaries managed by catacomb
|
||||||
/// (installed under `~/.local/share/yt-offline/bin/`). If false, use the
|
/// (installed under `~/.local/share/catacomb/bin/`). If false, use the
|
||||||
/// `yt-dlp` found on the system PATH.
|
/// `yt-dlp` found on the system PATH.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub use_bundled_ytdlp: bool,
|
pub use_bundled_ytdlp: bool,
|
||||||
|
|
|
||||||
12
src/crash.rs
12
src/crash.rs
|
|
@ -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
|
//! 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.
|
//! from a `.desktop` file or the IDE), so panics today vanish entirely.
|
||||||
//! This module installs a `panic::set_hook` early in `main` that appends
|
//! 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.
|
//! panic from any thread.
|
||||||
//!
|
//!
|
||||||
//! The default panic handler still runs after ours so dev runs keep
|
//! 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.
|
/// dozen panics is what a bug-report attacher actually wants.
|
||||||
const LOG_SIZE_CAP_BYTES: u64 = 256 * 1024;
|
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.
|
/// while preserving the default stderr behavior.
|
||||||
///
|
///
|
||||||
/// `dir` is the same directory the SQLite database lives in (typically
|
/// `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
|
/// Safe to call once at process startup. Calling it more than once
|
||||||
/// replaces the previous hook (which is fine: the new dir wins).
|
/// replaces the previous hook (which is fine: the new dir wins).
|
||||||
pub fn install(dir: &Path) {
|
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,
|
// 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
|
// continue. The DB-open path further down the chain will surface a
|
||||||
// permission / missing-dir error in a more legible way.
|
// 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
|
// (`std::thread::spawn` so the panic doesn't unwind the test
|
||||||
// runner), and assert the file contents afterward.
|
// runner), and assert the file contents afterward.
|
||||||
let mut tmp = std::env::temp_dir();
|
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);
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
std::fs::create_dir_all(&tmp).unwrap();
|
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
|
// Install the hook scoped to this test. We swap it back at the
|
||||||
// end so other tests run on the default hook.
|
// end so other tests run on the default hook.
|
||||||
|
|
|
||||||
|
|
@ -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
|
//! a small `r2d2`-managed pool of connections rather than a single shared
|
||||||
//! `Connection` — that way concurrent read queries from different axum
|
//! `Connection` — that way concurrent read queries from different axum
|
||||||
//! handlers don't serialize on a mutex, and write queries still take their
|
//! 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 |
|
//! | `watched` | `video_id` (PK), `watched_at` | Records videos the user has marked watched |
|
||||||
//! | `positions` | `video_id` (PK), `position_secs`, `updated_at` | Stores resume positions |
|
//! | `positions` | `video_id` (PK), `position_secs`, `updated_at` | Stores resume positions |
|
||||||
//! | `settings` | `key` (PK), `value` | Persistent app settings (password hash, etc.) |
|
//! | `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 r2d2_sqlite::SqliteConnectionManager;
|
||||||
use rusqlite::{Connection, Result};
|
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
|
/// Designed for "Import library backup…" — the user uploads a snapshot
|
||||||
/// produced by `GET /api/backup/db`, and we merge its rows in without
|
/// 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),
|
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISMATCH),
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"backup is missing required table `{table}` — \
|
"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};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
static N: AtomicU64 = AtomicU64::new(0);
|
static N: AtomicU64 = AtomicU64::new(0);
|
||||||
let id = N.fetch_add(1, Ordering::Relaxed);
|
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);
|
let _ = std::fs::remove_dir_all(&p);
|
||||||
std::fs::create_dir_all(&p).unwrap();
|
std::fs::create_dir_all(&p).unwrap();
|
||||||
ScratchDir(p)
|
ScratchDir(p)
|
||||||
|
|
@ -1478,7 +1480,7 @@ mod restore_tests {
|
||||||
let live = Database::open(&dir.join("live.db")).unwrap();
|
let live = Database::open(&dir.join("live.db")).unwrap();
|
||||||
|
|
||||||
// Create a SQLite file with a completely different schema.
|
// 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();
|
let conn = Connection::open(&bad).unwrap();
|
||||||
conn.execute("CREATE TABLE foo (x INT)", []).unwrap();
|
conn.execute("CREATE TABLE foo (x INT)", []).unwrap();
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
|
||||||
|
|
@ -1467,7 +1467,7 @@ impl Downloader {
|
||||||
|
|
||||||
impl Drop for Downloader {
|
impl Drop for Downloader {
|
||||||
/// Tear down the bgutil-pot child if we spawned one. Without this
|
/// 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
|
/// still bound to port 4416, blocking the next launch from
|
||||||
/// re-spawning.
|
/// re-spawning.
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
|
|
@ -1594,7 +1594,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn redact_sensitive_strips_cookie_path() {
|
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 input = format!("Unable to load cookies from {abs}");
|
||||||
let out = redact_sensitive(&input, abs);
|
let out = redact_sensitive(&input, abs);
|
||||||
assert_eq!(out, "Unable to load cookies from cookies.txt");
|
assert_eq!(out, "Unable to load cookies from cookies.txt");
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn renders_valid_rss() {
|
fn renders_valid_rss() {
|
||||||
let feed = Feed {
|
let feed = Feed {
|
||||||
title: "yt-offline — Demo & co".into(),
|
title: "catacomb — Demo & co".into(),
|
||||||
description: "archive".into(),
|
description: "archive".into(),
|
||||||
link: "http://host/".into(),
|
link: "http://host/".into(),
|
||||||
items: vec![FeedItem {
|
items: vec![FeedItem {
|
||||||
|
|
@ -232,7 +232,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
let xml = render(&feed);
|
let xml = render(&feed);
|
||||||
assert!(xml.starts_with("<?xml"));
|
assert!(xml.starts_with("<?xml"));
|
||||||
assert!(xml.contains("<title>yt-offline — Demo & co</title>"));
|
assert!(xml.contains("<title>catacomb — Demo & co</title>"));
|
||||||
assert!(xml.contains("<title>Episode <1></title>"));
|
assert!(xml.contains("<title>Episode <1></title>"));
|
||||||
assert!(xml.contains(r#"<enclosure url="http://host/files/channels/Demo/a.mkv" length="12345" type="video/x-matroska"/>"#));
|
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>"));
|
assert!(xml.contains("<itunes:duration>1:05</itunes:duration>"));
|
||||||
|
|
|
||||||
48
src/main.rs
48
src/main.rs
|
|
@ -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
|
//! # Usage
|
||||||
//!
|
//!
|
||||||
//! * **GUI mode** (default): `yt-offline`
|
//! * **GUI mode** (default): `catacomb`
|
||||||
//! * **Web mode**: `yt-offline --web [PORT]` — starts a headless HTTP server
|
//! * **Web mode**: `catacomb --web [PORT]` — starts a headless HTTP server
|
||||||
//! on the configured port (default 8080).
|
//! on the configured port (default 8080).
|
||||||
//!
|
//!
|
||||||
//! Configuration is read from `config.toml` in the current working directory.
|
//! 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() {
|
if requested.is_some() {
|
||||||
renderer_from_name(requested)
|
renderer_from_name(requested)
|
||||||
} else {
|
} 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())
|
renderer_from_name(env_renderer.as_deref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +90,33 @@ fn attach_windows_console() {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
fn attach_windows_console() {}
|
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<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
attach_windows_console();
|
attach_windows_console();
|
||||||
|
|
||||||
|
|
@ -104,6 +131,11 @@ fn main() -> eframe::Result<()> {
|
||||||
let mut cfg = config::Config::load(&cfg_path)
|
let mut cfg = config::Config::load(&cfg_path)
|
||||||
.unwrap_or_else(|_| config::Config::default_with_dir(cwd.join("channels")));
|
.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
|
// Install the persistent panic logger. Logs go alongside the SQLite
|
||||||
// database; the parent of channels_root is the same "library root"
|
// database; the parent of channels_root is the same "library root"
|
||||||
// the desktop UI shows.
|
// the desktop UI shows.
|
||||||
|
|
@ -137,16 +169,16 @@ fn main() -> eframe::Result<()> {
|
||||||
viewport: eframe::egui::ViewportBuilder::default()
|
viewport: eframe::egui::ViewportBuilder::default()
|
||||||
.with_inner_size([1280.0, 820.0])
|
.with_inner_size([1280.0, 820.0])
|
||||||
.with_min_inner_size([800.0, 500.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
|
// Default to wgpu (Vulkan): the glow/OpenGL path crashes on some
|
||||||
// NVIDIA + Wayland maximizes (Glutin EGL_BAD_ALLOC). Keep a launch
|
// NVIDIA + Wayland maximizes (Glutin EGL_BAD_ALLOC). Keep a launch
|
||||||
// escape hatch for systems where Vulkan/wgpu opens a blank window:
|
// 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),
|
renderer: renderer_from_args(&args),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"yt-offline",
|
"Catacomb",
|
||||||
native_options,
|
native_options,
|
||||||
Box::new(|cc| Ok(Box::new(app::App::new(cc, tray)))),
|
Box::new(|cc| Ok(Box::new(app::App::new(cc, tray)))),
|
||||||
)
|
)
|
||||||
|
|
@ -182,7 +214,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn renderer_arg_overrides_env_parser_path() {
|
fn renderer_arg_overrides_env_parser_path() {
|
||||||
let args = vec![
|
let args = vec![
|
||||||
"yt-offline".to_string(),
|
"Catacomb".to_string(),
|
||||||
"--renderer".to_string(),
|
"--renderer".to_string(),
|
||||||
"gl".to_string(),
|
"gl".to_string(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_within_accepts_inside_path() {
|
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 _ = fs::create_dir_all(&tmp);
|
||||||
let inside = tmp.join("foo.txt");
|
let inside = tmp.join("foo.txt");
|
||||||
let _ = fs::write(&inside, "x");
|
let _ = fs::write(&inside, "x");
|
||||||
|
|
@ -253,7 +253,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_within_rejects_outside_path() {
|
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);
|
let _ = fs::create_dir_all(&tmp);
|
||||||
// The target's parent canonicalises to /tmp, which doesn't start with 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");
|
let outside = std::env::temp_dir().join("not-our-dir-xyz.txt");
|
||||||
|
|
@ -264,7 +264,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn is_within_handles_missing_target() {
|
fn is_within_handles_missing_target() {
|
||||||
// Target doesn't exist; parent dir does and is inside root.
|
// 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 _ = fs::create_dir_all(&tmp);
|
||||||
let ghost = tmp.join("does-not-exist.txt");
|
let ghost = tmp.join("does-not-exist.txt");
|
||||||
assert!(is_within(&tmp, &ghost));
|
assert!(is_within(&tmp, &ghost));
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// All platforms (including YouTube) are nested as subdirectories of the
|
||||||
/// configured `channels_root`. So a config with
|
/// configured `channels_root`. So a config with
|
||||||
/// `directory = "/mnt/library/yt-offline"` puts YouTube channels at
|
/// `directory = "/mnt/library/catacomb"` puts YouTube channels at
|
||||||
/// `/mnt/library/yt-offline/channels/<creator>/` and Bandcamp artists at
|
/// `/mnt/library/catacomb/channels/<creator>/` and Bandcamp artists at
|
||||||
/// `/mnt/library/yt-offline/bandcamp/<artist>/`. This keeps everything a
|
/// `/mnt/library/catacomb/bandcamp/<artist>/`. This keeps everything a
|
||||||
/// user might want to archive under one tidy umbrella directory.
|
/// user might want to archive under one tidy umbrella directory.
|
||||||
///
|
///
|
||||||
/// The function's name predates the layout change — `channels_root` is
|
/// The function's name predates the layout change — `channels_root` is
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
//! Lives next to the bundled deno + yt-dlp:
|
//! Lives next to the bundled deno + yt-dlp:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! ~/.local/share/yt-offline/
|
//! ~/.local/share/catacomb/
|
||||||
//! bin/
|
//! bin/
|
||||||
//! bgutil-pot ← the Rust HTTP server binary
|
//! bgutil-pot ← the Rust HTTP server binary
|
||||||
//! venv/ ← reused — plugin zip unpacked into site-packages
|
//! 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.
|
/// Path to the bgutil-pot binary inside the bundled bin dir.
|
||||||
///
|
///
|
||||||
/// Co-locates with the bundled `deno` so a single bundled-dir cleanup
|
/// 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.
|
/// both.
|
||||||
pub fn bin_path() -> PathBuf {
|
pub fn bin_path() -> PathBuf {
|
||||||
let mut p = crate::ytdlp_bin::bundled_dir();
|
let mut p = crate::ytdlp_bin::bundled_dir();
|
||||||
|
|
|
||||||
|
|
@ -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:
|
//! A [`RemoteClient`] talks to another instance over its existing web API:
|
||||||
//! it fetches `/api/library`, logging in first if the peer has a password
|
//! 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()
|
let client = reqwest::blocking::Client::builder()
|
||||||
.cookie_store(true)
|
.cookie_store(true)
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.user_agent("yt-offline-federation")
|
.user_agent("catacomb-federation")
|
||||||
.build()
|
.build()
|
||||||
.unwrap_or_else(|_| reqwest::blocking::Client::new());
|
.unwrap_or_else(|_| reqwest::blocking::Client::new());
|
||||||
RemoteClient {
|
RemoteClient {
|
||||||
|
|
|
||||||
10
src/tray.rs
10
src/tray.rs
|
|
@ -62,14 +62,14 @@ struct TrayModel {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
impl ksni::Tray for TrayModel {
|
impl ksni::Tray for TrayModel {
|
||||||
fn id(&self) -> String {
|
fn id(&self) -> String {
|
||||||
"yt-offline".into()
|
"Catacomb".into()
|
||||||
}
|
}
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
"yt-offline".into()
|
"Catacomb".into()
|
||||||
}
|
}
|
||||||
fn tool_tip(&self) -> ksni::ToolTip {
|
fn tool_tip(&self) -> ksni::ToolTip {
|
||||||
ksni::ToolTip {
|
ksni::ToolTip {
|
||||||
title: "yt-offline".into(),
|
title: "Catacomb".into(),
|
||||||
description: "Self-hosted archive for YouTube + friends".into(),
|
description: "Self-hosted archive for YouTube + friends".into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ impl ksni::Tray for TrayModel {
|
||||||
use ksni::menu::StandardItem;
|
use ksni::menu::StandardItem;
|
||||||
vec![
|
vec![
|
||||||
StandardItem {
|
StandardItem {
|
||||||
label: "Show yt-offline".into(),
|
label: "Show Catacomb".into(),
|
||||||
activate: Box::new(|m: &mut Self| {
|
activate: Box::new(|m: &mut Self| {
|
||||||
let _ = m.tx.send(TrayEvent::Show);
|
let _ = m.tx.send(TrayEvent::Show);
|
||||||
}),
|
}),
|
||||||
|
|
@ -153,7 +153,7 @@ pub fn start(icon_png_bytes: &[u8]) -> Option<TrayHandle> {
|
||||||
};
|
};
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("yt-offline-tray".into())
|
.name("catacomb-tray".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
|
|
||||||
20
src/web.rs
20
src/web.rs
|
|
@ -401,7 +401,7 @@ struct SettingsPayload {
|
||||||
/// Maximum simultaneous yt-dlp processes. 0 = unlimited. Ignored if 0 on POST.
|
/// Maximum simultaneous yt-dlp processes. 0 = unlimited. Ignored if 0 on POST.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
max_concurrent: usize,
|
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.
|
/// instead of the system PATH yt-dlp.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
use_bundled_ytdlp: bool,
|
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())?;
|
std::fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||||
// cookies.txt carries live session credentials — tighten the mode so it
|
// cookies.txt carries live session credentials — tighten the mode so it
|
||||||
// isn't world-readable on multi-user systems. Best-effort, like the
|
// 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)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
@ -2406,7 +2406,7 @@ async fn get_feed_all(State(state): State<Arc<WebState>>, headers: HeaderMap) ->
|
||||||
.collect();
|
.collect();
|
||||||
let items = build_feed_items(all, root, &base, &state.feed_token, FEED_LIMIT);
|
let items = build_feed_items(all, root, &base, &state.feed_token, FEED_LIMIT);
|
||||||
let feed = crate::feed::Feed {
|
let feed = crate::feed::Feed {
|
||||||
title: "yt-offline — Library".into(),
|
title: "Catacomb — Library".into(),
|
||||||
description: "Your archived videos as a podcast feed.".into(),
|
description: "Your archived videos as a podcast feed.".into(),
|
||||||
link: format!("{base}/"),
|
link: format!("{base}/"),
|
||||||
items,
|
items,
|
||||||
|
|
@ -2430,7 +2430,7 @@ async fn get_feed_channel(
|
||||||
.chain(ch.playlists.iter().flat_map(|p| p.videos.iter())).collect();
|
.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 items = build_feed_items(vids, root, &base, &state.feed_token, FEED_LIMIT);
|
||||||
let feed = crate::feed::Feed {
|
let feed = crate::feed::Feed {
|
||||||
title: format!("yt-offline — {}", ch.name),
|
title: format!("Catacomb — {}", ch.name),
|
||||||
description: format!("Archived videos from {}.", ch.name),
|
description: format!("Archived videos from {}.", ch.name),
|
||||||
link: format!("{base}/"),
|
link: format!("{base}/"),
|
||||||
items,
|
items,
|
||||||
|
|
@ -2696,7 +2696,7 @@ async fn delete_folder(
|
||||||
/// session credentials that shouldn't fly over the wire unprompted, and
|
/// session credentials that shouldn't fly over the wire unprompted, and
|
||||||
/// keeping the endpoint to one file means no extra deps for tar/zip.
|
/// keeping the endpoint to one file means no extra deps for tar/zip.
|
||||||
async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
|
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 {
|
let Ok(bytes) = std::fs::read(&db_path) else {
|
||||||
return (StatusCode::NOT_FOUND, "no db file on disk (running in-memory?)").into_response();
|
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)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.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()),
|
(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
|
/// rows into the live DB. Mirrors `GET /api/backup/db` in the opposite
|
||||||
/// direction.
|
/// 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
|
/// `get_backup_db` produces. We write it to a sibling temp file, hand it
|
||||||
/// to [`Database::restore_from_backup`] for the actual merge, then
|
/// to [`Database::restore_from_backup`] for the actual merge, then
|
||||||
/// refresh the in-memory watched / positions / flags caches so the next
|
/// 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
|
// 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
|
// the live DB — keeps ATTACH happy and avoids cross-device rename
|
||||||
// issues if we ever extend this to atomic-replace semantics.
|
// 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) {
|
if let Err(e) = std::fs::write(&tmp_path, &body) {
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR,
|
return (StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
format!("write tmp file: {e}")).into_response();
|
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 dir = crate::platform::platform_root(&channels_root, p);
|
||||||
let _ = std::fs::create_dir_all(&dir);
|
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()
|
let config_path = std::env::current_dir()
|
||||||
.unwrap_or_else(|_| PathBuf::from("."))
|
.unwrap_or_else(|_| PathBuf::from("."))
|
||||||
.join("config.toml");
|
.join("config.toml");
|
||||||
|
|
@ -3374,7 +3374,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
||||||
return;
|
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
|
// 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
|
// the background so a cold-cache scan of a large library doesn't delay
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<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
|
<!-- Embedded UI fonts (base64 below, offline-safe — no CDN): Instrument Serif
|
||||||
by Rodrigo Fuenzalida and Hanken Grotesk by Alfredo Marco Pradil, both
|
by Rodrigo Fuenzalida and Hanken Grotesk by Alfredo Marco Pradil, both
|
||||||
under the SIL Open Font License 1.1. -->
|
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-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-body{display:flex;gap:12px;overflow:hidden;flex:1;min-height:0}
|
||||||
.modal video{flex:1;min-width:0;max-height:75vh;background:#000}
|
.modal video{flex:1;min-width:0;max-height:75vh;background:#000}
|
||||||
/* Custom player (used for non-seekable transcode streams): the live ffmpeg
|
/* Custom player: direct files and non-seekable transcode streams now share
|
||||||
pipe has no byte ranges, so we drive a scrubber off the known duration and
|
the same chrome. The ffmpeg pipe has no byte ranges, so transcode scrubbing
|
||||||
re-request the stream at an offset on seek. */
|
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{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}
|
.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}
|
.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>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
||||||
<h1>yt-offline</h1>
|
<h1>Catacomb</h1>
|
||||||
<input type="search" id="search" placeholder="Filter…" oninput="saveUiState();renderGrid()">
|
<input type="search" id="search" placeholder="Filter…" oninput="saveUiState();renderGrid()">
|
||||||
<select id="sort" onchange="saveUiState();renderGrid()">
|
<select id="sort" onchange="saveUiState();renderGrid()">
|
||||||
<optgroup label="Download date">
|
<optgroup label="Download date">
|
||||||
|
|
@ -598,7 +598,7 @@ function closeSidebar(){document.getElementById('sidebar').classList.remove('ope
|
||||||
/* ── API ────────────────────────────────────────────────────────── */
|
/* ── API ────────────────────────────────────────────────────────── */
|
||||||
async function api(path,opts){
|
async function api(path,opts){
|
||||||
const r=await fetch(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"
|
// the cached SPA would otherwise just throw cryptic "authentication required"
|
||||||
// toasts on every action. Reload instead so the server serves the login page.
|
// toasts on every action. Reload instead so the server serves the login page.
|
||||||
if(r.status===401){
|
if(r.status===401){
|
||||||
|
|
@ -1632,8 +1632,8 @@ function shufflePlay(){
|
||||||
}
|
}
|
||||||
playVideo(pick.id);
|
playVideo(pick.id);
|
||||||
}
|
}
|
||||||
// Player state for the custom transcode controls. For direct /files/ videos
|
// Player state for the custom controls. For direct /files/ videos `isTranscode`
|
||||||
// `isTranscode` is false and native <video controls> handle everything.
|
// is false and seeking sets currentTime; transcode seeks by reloading at start=.
|
||||||
let player={isTranscode:false,base:'',seekBase:0,duration:0};
|
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));}
|
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
|
// 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>
|
<label for="cf-ytdlp-bundled">Use bundled yt-dlp + deno</label>
|
||||||
<input type="checkbox" id="cf-ytdlp-bundled" ${cur.use_bundled_ytdlp?'checked':''}>
|
<input type="checkbox" id="cf-ytdlp-bundled" ${cur.use_bundled_ytdlp?'checked':''}>
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
<span style="font-size:11px;color:var(--muted)">${cur.bundled_ytdlp_installed?'✓ installed':'not installed'}</span>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<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>
|
<style>
|
||||||
/* Standalone page (the SPA's embedded fonts aren't available here), so the
|
/* 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
|
display face is a system serif stack — editorial without extra bytes. The
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="box">
|
<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="tag">your private archive</div>
|
||||||
<div class="field"><input type="password" id="pwd" placeholder="Password" autofocus onkeydown="if(event.key==='Enter')login()"></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>
|
<button onclick="login()">Sign in</button>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
//! app invokes its own yt-dlp instead of whatever's on PATH. To get the
|
//! app invokes its own yt-dlp instead of whatever's on PATH. To get the
|
||||||
//! full feature set — most importantly `curl_cffi`-backed `--impersonate`
|
//! full feature set — most importantly `curl_cffi`-backed `--impersonate`
|
||||||
//! support — we install yt-dlp into a self-contained Python virtualenv
|
//! support — we install yt-dlp into a self-contained Python virtualenv
|
||||||
//! under `~/.local/share/yt-offline/venv/`. A bundled `deno` lives at
|
//! under `~/.local/share/catacomb/venv/`. A bundled `deno` lives at
|
||||||
//! `~/.local/share/yt-offline/bin/deno` so yt-dlp can evaluate the
|
//! `~/.local/share/catacomb/bin/deno` so yt-dlp can evaluate the
|
||||||
//! JavaScript signature-deciphering code YouTube serves with the player.
|
//! JavaScript signature-deciphering code YouTube serves with the player.
|
||||||
//!
|
//!
|
||||||
//! Layout:
|
//! Layout:
|
||||||
//! ```text
|
//! ```text
|
||||||
//! ~/.local/share/yt-offline/
|
//! ~/.local/share/catacomb/
|
||||||
//! bin/ ← prepended to PATH so yt-dlp finds deno
|
//! bin/ ← prepended to PATH so yt-dlp finds deno
|
||||||
//! deno
|
//! deno
|
||||||
//! venv/
|
//! venv/
|
||||||
|
|
@ -30,7 +30,7 @@ pub fn bundled_root() -> PathBuf {
|
||||||
let home = std::env::var_os("HOME")
|
let home = std::env::var_os("HOME")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|| 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
|
/// Directory holding non-Python binaries (currently just `deno`). Also the
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
|
|
||||||
/// Absolute path to the binary under test (set by Cargo for integration
|
/// Absolute path to the binary under test (set by Cargo for integration
|
||||||
/// tests of a crate that builds a binary).
|
/// 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
|
/// True if `curl` is usable; the tests no-op otherwise so a machine
|
||||||
/// without curl doesn't show spurious failures.
|
/// without curl doesn't show spurious failures.
|
||||||
|
|
@ -32,7 +32,7 @@ fn have_ffmpeg() -> bool {
|
||||||
.status().map(|s| s.success()).unwrap_or(false)
|
.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.
|
/// its dir removed on drop.
|
||||||
struct Server {
|
struct Server {
|
||||||
child: Child,
|
child: Child,
|
||||||
|
|
@ -59,7 +59,7 @@ impl Server {
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("spawn yt-offline --web");
|
.expect("spawn catacomb --web");
|
||||||
let s = Server { child, port, dir };
|
let s = Server { child, port, dir };
|
||||||
s.wait_ready();
|
s.wait_ready();
|
||||||
s
|
s
|
||||||
|
|
@ -176,7 +176,7 @@ fn index_and_library_served() {
|
||||||
|
|
||||||
let (code, body) = s.get("/");
|
let (code, body) = s.get("/");
|
||||||
assert_eq!(code, 200, "GET / should serve the SPA");
|
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");
|
let (code, body) = s.get("/api/library");
|
||||||
assert_eq!(code, 200);
|
assert_eq!(code, 200);
|
||||||
|
|
|
||||||
0
yt-offline.db
Normal file
0
yt-offline.db
Normal file
2
yt-offline/FETCH_HEAD
Normal file
2
yt-offline/FETCH_HEAD
Normal 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
1
yt-offline/HEAD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ref: refs/heads/main
|
||||||
9
yt-offline/config
Normal file
9
yt-offline/config
Normal 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
1
yt-offline/description
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
15
yt-offline/hooks/applypatch-msg.sample
Executable file
15
yt-offline/hooks/applypatch-msg.sample
Executable 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+"$@"}
|
||||||
|
:
|
||||||
74
yt-offline/hooks/commit-msg.sample
Executable file
74
yt-offline/hooks/commit-msg.sample
Executable 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
|
||||||
168
yt-offline/hooks/fsmonitor-watchman.sample
Executable file
168
yt-offline/hooks/fsmonitor-watchman.sample
Executable 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;
|
||||||
|
}
|
||||||
8
yt-offline/hooks/post-update.sample
Executable file
8
yt-offline/hooks/post-update.sample
Executable 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
|
||||||
14
yt-offline/hooks/pre-applypatch.sample
Executable file
14
yt-offline/hooks/pre-applypatch.sample
Executable 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+"$@"}
|
||||||
|
:
|
||||||
49
yt-offline/hooks/pre-commit.sample
Executable file
49
yt-offline/hooks/pre-commit.sample
Executable 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 --
|
||||||
13
yt-offline/hooks/pre-merge-commit.sample
Executable file
13
yt-offline/hooks/pre-merge-commit.sample
Executable 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"
|
||||||
|
:
|
||||||
53
yt-offline/hooks/pre-push.sample
Executable file
53
yt-offline/hooks/pre-push.sample
Executable 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
|
||||||
169
yt-offline/hooks/pre-rebase.sample
Executable file
169
yt-offline/hooks/pre-rebase.sample
Executable 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
|
||||||
24
yt-offline/hooks/pre-receive.sample
Executable file
24
yt-offline/hooks/pre-receive.sample
Executable 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
|
||||||
42
yt-offline/hooks/prepare-commit-msg.sample
Executable file
42
yt-offline/hooks/prepare-commit-msg.sample
Executable 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
|
||||||
78
yt-offline/hooks/push-to-checkout.sample
Executable file
78
yt-offline/hooks/push-to-checkout.sample
Executable 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
|
||||||
77
yt-offline/hooks/sendemail-validate.sample
Executable file
77
yt-offline/hooks/sendemail-validate.sample
Executable 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
128
yt-offline/hooks/update.sample
Executable 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
|
||||||
1
yt-offline/info/attributes
Normal file
1
yt-offline/info/attributes
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* -export-subst -export-ignore
|
||||||
6
yt-offline/info/exclude
Normal file
6
yt-offline/info/exclude
Normal 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]
|
||||||
|
# *~
|
||||||
BIN
yt-offline/objects/17/c149c21a91e639919901ef7504bbc3df87ef33
Normal file
BIN
yt-offline/objects/17/c149c21a91e639919901ef7504bbc3df87ef33
Normal file
Binary file not shown.
BIN
yt-offline/objects/20/3949ac943638b07ac5c9b79042cb08510ea5e1
Normal file
BIN
yt-offline/objects/20/3949ac943638b07ac5c9b79042cb08510ea5e1
Normal file
Binary file not shown.
BIN
yt-offline/objects/ad/c0d2b547569fc86596ea4f6bfa1dc8149bab42
Normal file
BIN
yt-offline/objects/ad/c0d2b547569fc86596ea4f6bfa1dc8149bab42
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
yt-offline/packed-refs
Normal file
3
yt-offline/packed-refs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
8b4796b518296287bc3748413981ee65c8d328b2 refs/heads/main
|
||||||
|
f3c136a0183cdd64a7adc71c4d4dcea24e3fefcf refs/heads/master
|
||||||
1
yt-offline/refs/heads/main
Normal file
1
yt-offline/refs/heads/main
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
17c149c21a91e639919901ef7504bbc3df87ef33
|
||||||
Loading…
Add table
Add a link
Reference in a new issue