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
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
|
||||
// skip re-parsing unchanged info.json sidecars. Cold start still
|
||||
// pays full cost; every subsequent launch is faster.
|
||||
let db_path = channels_root.join("yt-offline.db");
|
||||
let db_path = channels_root.join("catacomb.db");
|
||||
let db = Database::open(&db_path)
|
||||
.unwrap_or_else(|_| Database::open_in_memory().expect("in-memory db failed"));
|
||||
// Defer the (potentially multi-minute, disk-bound) library scan +
|
||||
|
|
@ -409,7 +409,7 @@ impl App {
|
|||
let channels_root = channels_root.clone();
|
||||
let ctx = cc.egui_ctx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("yt-offline-libscan".into())
|
||||
.name("catacomb-libscan".into())
|
||||
.spawn(move || {
|
||||
let mut library = library::scan_channels_with_cache(&channels_root, Some(&db));
|
||||
// Hydrate per-channel download options + folder assignments
|
||||
|
|
@ -493,7 +493,7 @@ impl App {
|
|||
let tx = thumb_result_tx.clone();
|
||||
let ctx = cc.egui_ctx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("yt-offline-thumb-{worker_id}"))
|
||||
.name(format!("catacomb-thumb-{worker_id}"))
|
||||
.spawn(move || {
|
||||
loop {
|
||||
// Hold the lock only for the recv itself; release
|
||||
|
|
@ -1185,7 +1185,7 @@ impl App {
|
|||
self.status = "Play this video first, then click a line to seek".into();
|
||||
return;
|
||||
}
|
||||
let sock = format!("/tmp/yt-offline-{id}.sock");
|
||||
let sock = format!("/tmp/catacomb-{id}.sock");
|
||||
match UnixStream::connect(&sock) {
|
||||
Ok(mut s) => {
|
||||
let cmd = format!("{{\"command\":[\"seek\",{secs},\"absolute\"]}}\n");
|
||||
|
|
@ -1216,7 +1216,7 @@ impl App {
|
|||
let use_mpv_ipc = exe == "mpv" || exe == "mpv.exe";
|
||||
|
||||
#[cfg(unix)]
|
||||
let sock_path = format!("/tmp/yt-offline-{video_id}.sock");
|
||||
let sock_path = format!("/tmp/catacomb-{video_id}.sock");
|
||||
|
||||
let mut child_cmd = Command::new(&cmd);
|
||||
|
||||
|
|
@ -1415,7 +1415,7 @@ impl App {
|
|||
format!("Download failed: {label}")
|
||||
};
|
||||
let _ = notify_rust::Notification::new()
|
||||
.summary("yt-offline")
|
||||
.summary("Catacomb")
|
||||
.body(&summary)
|
||||
.timeout(notify_rust::Timeout::Milliseconds(4000))
|
||||
.show();
|
||||
|
|
@ -1430,7 +1430,7 @@ impl App {
|
|||
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("yt-offline");
|
||||
ui.heading("Catacomb");
|
||||
ui.separator();
|
||||
ui.label("🔍");
|
||||
ui.add(
|
||||
|
|
@ -2481,7 +2481,7 @@ impl App {
|
|||
ui.heading("🌐 Remote libraries");
|
||||
});
|
||||
ui.label(egui::RichText::new(
|
||||
"Browse another yt-offline instance read-only. Playback streams from the peer.")
|
||||
"Browse another Catacomb instance read-only. Playback streams from the peer.")
|
||||
.weak().small());
|
||||
ui.separator();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
|
|
@ -3234,7 +3234,7 @@ impl App {
|
|||
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, false, "System")
|
||||
.on_hover_text("Use whatever yt-dlp is on PATH.");
|
||||
ui.radio_value(&mut self.config.backup.use_bundled_ytdlp, true, "Bundled")
|
||||
.on_hover_text("Use the yt-dlp + deno installed under ~/.local/share/yt-offline/bin/.");
|
||||
.on_hover_text("Use the yt-dlp + deno installed under ~/.local/share/catacomb/bin/.");
|
||||
let installed = crate::ytdlp_bin::bundled_installed();
|
||||
let btn_label = if installed { "Update" } else { "Install" };
|
||||
if ui.button(btn_label)
|
||||
|
|
@ -3559,7 +3559,7 @@ impl App {
|
|||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Saves a copy of yt-offline.db (watched / favourites / bookmarks / \
|
||||
"Saves a copy of catacomb.db (watched / favourites / bookmarks / \
|
||||
waiting / channel-options / folders). Restore by copying it back \
|
||||
into your channels directory before launch.",
|
||||
)
|
||||
|
|
@ -3572,7 +3572,7 @@ impl App {
|
|||
// cookies file-picker pattern.
|
||||
let tx = self.backup_save_tx.clone();
|
||||
let default_name = format!(
|
||||
"yt-offline-{}.db",
|
||||
"catacomb-{}.db",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs()).unwrap_or(0),
|
||||
|
|
@ -4517,9 +4517,9 @@ impl eframe::App for App {
|
|||
Err(e) => self.status = format!("Could not read {}: {e}", path.display()),
|
||||
}
|
||||
}
|
||||
// User picked a backup destination — copy yt-offline.db there.
|
||||
// User picked a backup destination — copy catacomb.db there.
|
||||
while let Ok(dest) = self.backup_save_rx.try_recv() {
|
||||
let src = self.channels_root.join("yt-offline.db");
|
||||
let src = self.channels_root.join("catacomb.db");
|
||||
match std::fs::copy(&src, &dest) {
|
||||
Ok(bytes) => self.status = format!(
|
||||
"Backup saved ({} → {})",
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ pub struct Config {
|
|||
pub subtitles: SubtitlesSection,
|
||||
#[serde(default)]
|
||||
pub convert: ConvertSection,
|
||||
/// Federation: other yt-offline instances whose libraries can be browsed
|
||||
/// Federation: other catacomb instances whose libraries can be browsed
|
||||
/// read-only from this one (see [`crate::remote`]). Each is a
|
||||
/// `[[remote]]` table in config.toml.
|
||||
#[serde(default, rename = "remote")]
|
||||
pub remotes: Vec<RemoteSection>,
|
||||
}
|
||||
|
||||
/// One `[[remote]]` entry — a peer yt-offline instance to browse read-only.
|
||||
/// One `[[remote]]` entry — a peer catacomb instance to browse read-only.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RemoteSection {
|
||||
/// Display name in the UI's remote switcher.
|
||||
|
|
@ -122,8 +122,8 @@ pub struct BackupSection {
|
|||
/// automatically when a slot opens. Set to 0 for no limit (not recommended).
|
||||
#[serde(default = "default_max_concurrent")]
|
||||
pub max_concurrent: usize,
|
||||
/// If true, use the bundled yt-dlp + deno binaries managed by yt-offline
|
||||
/// (installed under `~/.local/share/yt-offline/bin/`). If false, use the
|
||||
/// If true, use the bundled yt-dlp + deno binaries managed by catacomb
|
||||
/// (installed under `~/.local/share/catacomb/bin/`). If false, use the
|
||||
/// `yt-dlp` found on the system PATH.
|
||||
#[serde(default)]
|
||||
pub use_bundled_ytdlp: bool,
|
||||
|
|
|
|||
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
|
||||
//! from a `.desktop` file or the IDE), so panics today vanish entirely.
|
||||
//! This module installs a `panic::set_hook` early in `main` that appends
|
||||
//! a structured entry to `<channels_root>/yt-offline.crash.log` for every
|
||||
//! a structured entry to `<channels_root>/catacomb.crash.log` for every
|
||||
//! panic from any thread.
|
||||
//!
|
||||
//! The default panic handler still runs after ours so dev runs keep
|
||||
|
|
@ -36,7 +36,7 @@ use std::path::{Path, PathBuf};
|
|||
/// dozen panics is what a bug-report attacher actually wants.
|
||||
const LOG_SIZE_CAP_BYTES: u64 = 256 * 1024;
|
||||
|
||||
/// Install a global panic hook that logs to `<dir>/yt-offline.crash.log`
|
||||
/// Install a global panic hook that logs to `<dir>/catacomb.crash.log`
|
||||
/// while preserving the default stderr behavior.
|
||||
///
|
||||
/// `dir` is the same directory the SQLite database lives in (typically
|
||||
|
|
@ -47,7 +47,7 @@ const LOG_SIZE_CAP_BYTES: u64 = 256 * 1024;
|
|||
/// Safe to call once at process startup. Calling it more than once
|
||||
/// replaces the previous hook (which is fine: the new dir wins).
|
||||
pub fn install(dir: &Path) {
|
||||
let path = dir.join("yt-offline.crash.log");
|
||||
let path = dir.join("catacomb.crash.log");
|
||||
// Don't fail startup if the parent dir doesn't exist yet — log it,
|
||||
// continue. The DB-open path further down the chain will surface a
|
||||
// permission / missing-dir error in a more legible way.
|
||||
|
|
@ -194,10 +194,10 @@ mod tests {
|
|||
// (`std::thread::spawn` so the panic doesn't unwind the test
|
||||
// runner), and assert the file contents afterward.
|
||||
let mut tmp = std::env::temp_dir();
|
||||
tmp.push(format!("yt-offline-crash-test-{}", std::process::id()));
|
||||
tmp.push(format!("catacomb-crash-test-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let log = tmp.join("yt-offline.crash.log");
|
||||
let log = tmp.join("catacomb.crash.log");
|
||||
|
||||
// Install the hook scoped to this test. We swap it back at the
|
||||
// end so other tests run on the default hook.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Persistent storage for watched status, playback positions, and settings.
|
||||
//! Persistent storage for watched status, playback positions, settings, and
|
||||
//! restart-persistent web sessions.
|
||||
//!
|
||||
//! Backed by a bundled SQLite database (`yt-offline.db`). Access goes through
|
||||
//! Backed by a bundled SQLite database (`catacomb.db`). Access goes through
|
||||
//! a small `r2d2`-managed pool of connections rather than a single shared
|
||||
//! `Connection` — that way concurrent read queries from different axum
|
||||
//! handlers don't serialize on a mutex, and write queries still take their
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
//! | `watched` | `video_id` (PK), `watched_at` | Records videos the user has marked watched |
|
||||
//! | `positions` | `video_id` (PK), `position_secs`, `updated_at` | Stores resume positions |
|
||||
//! | `settings` | `key` (PK), `value` | Persistent app settings (password hash, etc.) |
|
||||
//! | `sessions` | `token` (PK), `issued_at` | Restart-persistent web login sessions |
|
||||
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::{Connection, Result};
|
||||
|
|
@ -790,7 +792,7 @@ impl Database {
|
|||
);
|
||||
}
|
||||
|
||||
/// Idempotently merge another yt-offline database into this one.
|
||||
/// Idempotently merge another catacomb database into this one.
|
||||
///
|
||||
/// Designed for "Import library backup…" — the user uploads a snapshot
|
||||
/// produced by `GET /api/backup/db`, and we merge its rows in without
|
||||
|
|
@ -858,7 +860,7 @@ impl Database {
|
|||
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISMATCH),
|
||||
Some(format!(
|
||||
"backup is missing required table `{table}` — \
|
||||
not a yt-offline snapshot, or from an incompatible version"
|
||||
not a catacomb snapshot, or from an incompatible version"
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
|
@ -1392,7 +1394,7 @@ mod restore_tests {
|
|||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static N: AtomicU64 = AtomicU64::new(0);
|
||||
let id = N.fetch_add(1, Ordering::Relaxed);
|
||||
p.push(format!("yt-offline-test-{}-{}-{}", std::process::id(), id, name));
|
||||
p.push(format!("catacomb-test-{}-{}-{}", std::process::id(), id, name));
|
||||
let _ = std::fs::remove_dir_all(&p);
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
ScratchDir(p)
|
||||
|
|
@ -1478,7 +1480,7 @@ mod restore_tests {
|
|||
let live = Database::open(&dir.join("live.db")).unwrap();
|
||||
|
||||
// Create a SQLite file with a completely different schema.
|
||||
let bad = dir.join("not-yt-offline.db");
|
||||
let bad = dir.join("not-catacomb.db");
|
||||
let conn = Connection::open(&bad).unwrap();
|
||||
conn.execute("CREATE TABLE foo (x INT)", []).unwrap();
|
||||
drop(conn);
|
||||
|
|
|
|||
|
|
@ -1467,7 +1467,7 @@ impl Downloader {
|
|||
|
||||
impl Drop for Downloader {
|
||||
/// Tear down the bgutil-pot child if we spawned one. Without this
|
||||
/// the server keeps running after yt-offline exits — orphaned and
|
||||
/// the server keeps running after catacomb exits — orphaned and
|
||||
/// still bound to port 4416, blocking the next launch from
|
||||
/// re-spawning.
|
||||
fn drop(&mut self) {
|
||||
|
|
@ -1594,7 +1594,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn redact_sensitive_strips_cookie_path() {
|
||||
let abs = "/home/luna/.config/yt-offline/cookies.txt";
|
||||
let abs = "/home/luna/.config/catacomb/cookies.txt";
|
||||
let input = format!("Unable to load cookies from {abs}");
|
||||
let out = redact_sensitive(&input, abs);
|
||||
assert_eq!(out, "Unable to load cookies from cookies.txt");
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ mod tests {
|
|||
#[test]
|
||||
fn renders_valid_rss() {
|
||||
let feed = Feed {
|
||||
title: "yt-offline — Demo & co".into(),
|
||||
title: "catacomb — Demo & co".into(),
|
||||
description: "archive".into(),
|
||||
link: "http://host/".into(),
|
||||
items: vec![FeedItem {
|
||||
|
|
@ -232,7 +232,7 @@ mod tests {
|
|||
};
|
||||
let xml = render(&feed);
|
||||
assert!(xml.starts_with("<?xml"));
|
||||
assert!(xml.contains("<title>yt-offline — Demo & co</title>"));
|
||||
assert!(xml.contains("<title>catacomb — Demo & co</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("<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
|
||||
//!
|
||||
//! * **GUI mode** (default): `yt-offline`
|
||||
//! * **Web mode**: `yt-offline --web [PORT]` — starts a headless HTTP server
|
||||
//! * **GUI mode** (default): `catacomb`
|
||||
//! * **Web mode**: `catacomb --web [PORT]` — starts a headless HTTP server
|
||||
//! on the configured port (default 8080).
|
||||
//!
|
||||
//! Configuration is read from `config.toml` in the current working directory.
|
||||
|
|
@ -60,7 +60,7 @@ fn renderer_from_args(args: &[String]) -> eframe::Renderer {
|
|||
if requested.is_some() {
|
||||
renderer_from_name(requested)
|
||||
} else {
|
||||
let env_renderer = std::env::var("YT_OFFLINE_RENDERER").ok();
|
||||
let env_renderer = std::env::var("CATACOMB_RENDERER").ok();
|
||||
renderer_from_name(env_renderer.as_deref())
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,33 @@ fn attach_windows_console() {
|
|||
#[cfg(not(windows))]
|
||||
fn attach_windows_console() {}
|
||||
|
||||
/// Adopt the pre-rename `yt-offline` data paths under the new `catacomb` names
|
||||
/// so the rename doesn't strand an existing library or bundled toolchain.
|
||||
/// Best-effort: only renames when the new path is absent and the old one still
|
||||
/// exists, so it's a no-op on fresh installs and after the first migrated run.
|
||||
fn migrate_legacy_paths(channels_root: &std::path::Path) {
|
||||
fn adopt(old: std::path::PathBuf, new: std::path::PathBuf) {
|
||||
if !new.exists() && old.exists() {
|
||||
match std::fs::rename(&old, &new) {
|
||||
Ok(()) => eprintln!("migrated {} → {}", old.display(), new.display()),
|
||||
Err(e) => eprintln!("warning: could not migrate {} → {}: {e}", old.display(), new.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
// SQLite DB plus its WAL/SHM sidecars (if any), in the library root.
|
||||
for suffix in ["", "-wal", "-shm"] {
|
||||
adopt(
|
||||
channels_root.join(format!("yt-offline.db{suffix}")),
|
||||
channels_root.join(format!("catacomb.db{suffix}")),
|
||||
);
|
||||
}
|
||||
// Bundled venv + binaries under the XDG data dir.
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
let share = std::path::PathBuf::from(home).join(".local").join("share");
|
||||
adopt(share.join("yt-offline"), share.join("catacomb"));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
attach_windows_console();
|
||||
|
||||
|
|
@ -104,6 +131,11 @@ fn main() -> eframe::Result<()> {
|
|||
let mut cfg = config::Config::load(&cfg_path)
|
||||
.unwrap_or_else(|_| config::Config::default_with_dir(cwd.join("channels")));
|
||||
|
||||
// One-time adoption of the pre-rename `yt-offline` data paths so existing
|
||||
// installs keep their library + bundled toolchain after the rename to
|
||||
// Catacomb. Runs before anything opens the DB or the venv.
|
||||
migrate_legacy_paths(&cfg.backup.directory);
|
||||
|
||||
// Install the persistent panic logger. Logs go alongside the SQLite
|
||||
// database; the parent of channels_root is the same "library root"
|
||||
// the desktop UI shows.
|
||||
|
|
@ -137,16 +169,16 @@ fn main() -> eframe::Result<()> {
|
|||
viewport: eframe::egui::ViewportBuilder::default()
|
||||
.with_inner_size([1280.0, 820.0])
|
||||
.with_min_inner_size([800.0, 500.0])
|
||||
.with_title("yt-offline"),
|
||||
.with_title("Catacomb"),
|
||||
// Default to wgpu (Vulkan): the glow/OpenGL path crashes on some
|
||||
// NVIDIA + Wayland maximizes (Glutin EGL_BAD_ALLOC). Keep a launch
|
||||
// escape hatch for systems where Vulkan/wgpu opens a blank window:
|
||||
// `YT_OFFLINE_RENDERER=glow yt-offline` or `yt-offline --renderer glow`.
|
||||
// `CATACOMB_RENDERER=glow catacomb` or `catacomb --renderer glow`.
|
||||
renderer: renderer_from_args(&args),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"yt-offline",
|
||||
"Catacomb",
|
||||
native_options,
|
||||
Box::new(|cc| Ok(Box::new(app::App::new(cc, tray)))),
|
||||
)
|
||||
|
|
@ -182,7 +214,7 @@ mod tests {
|
|||
#[test]
|
||||
fn renderer_arg_overrides_env_parser_path() {
|
||||
let args = vec![
|
||||
"yt-offline".to_string(),
|
||||
"Catacomb".to_string(),
|
||||
"--renderer".to_string(),
|
||||
"gl".to_string(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn is_within_accepts_inside_path() {
|
||||
let tmp = std::env::temp_dir().join("yt-offline-iw-test-1");
|
||||
let tmp = std::env::temp_dir().join("catacomb-iw-test-1");
|
||||
let _ = fs::create_dir_all(&tmp);
|
||||
let inside = tmp.join("foo.txt");
|
||||
let _ = fs::write(&inside, "x");
|
||||
|
|
@ -253,7 +253,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn is_within_rejects_outside_path() {
|
||||
let tmp = std::env::temp_dir().join("yt-offline-iw-test-2");
|
||||
let tmp = std::env::temp_dir().join("catacomb-iw-test-2");
|
||||
let _ = fs::create_dir_all(&tmp);
|
||||
// The target's parent canonicalises to /tmp, which doesn't start with tmp.
|
||||
let outside = std::env::temp_dir().join("not-our-dir-xyz.txt");
|
||||
|
|
@ -264,7 +264,7 @@ mod tests {
|
|||
#[test]
|
||||
fn is_within_handles_missing_target() {
|
||||
// Target doesn't exist; parent dir does and is inside root.
|
||||
let tmp = std::env::temp_dir().join("yt-offline-iw-test-3");
|
||||
let tmp = std::env::temp_dir().join("catacomb-iw-test-3");
|
||||
let _ = fs::create_dir_all(&tmp);
|
||||
let ghost = tmp.join("does-not-exist.txt");
|
||||
assert!(is_within(&tmp, &ghost));
|
||||
|
|
|
|||
|
|
@ -355,9 +355,9 @@ fn extract_after<'a>(url: &'a str, marker: &str) -> Option<&'a str> {
|
|||
///
|
||||
/// All platforms (including YouTube) are nested as subdirectories of the
|
||||
/// configured `channels_root`. So a config with
|
||||
/// `directory = "/mnt/library/yt-offline"` puts YouTube channels at
|
||||
/// `/mnt/library/yt-offline/channels/<creator>/` and Bandcamp artists at
|
||||
/// `/mnt/library/yt-offline/bandcamp/<artist>/`. This keeps everything a
|
||||
/// `directory = "/mnt/library/catacomb"` puts YouTube channels at
|
||||
/// `/mnt/library/catacomb/channels/<creator>/` and Bandcamp artists at
|
||||
/// `/mnt/library/catacomb/bandcamp/<artist>/`. This keeps everything a
|
||||
/// user might want to archive under one tidy umbrella directory.
|
||||
///
|
||||
/// The function's name predates the layout change — `channels_root` is
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
//! Lives next to the bundled deno + yt-dlp:
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.local/share/yt-offline/
|
||||
//! ~/.local/share/catacomb/
|
||||
//! bin/
|
||||
//! bgutil-pot ← the Rust HTTP server binary
|
||||
//! venv/ ← reused — plugin zip unpacked into site-packages
|
||||
|
|
@ -65,7 +65,7 @@ pub fn server_url() -> String {
|
|||
/// Path to the bgutil-pot binary inside the bundled bin dir.
|
||||
///
|
||||
/// Co-locates with the bundled `deno` so a single bundled-dir cleanup
|
||||
/// (currently just `rm -rf ~/.local/share/yt-offline/bin`) removes
|
||||
/// (currently just `rm -rf ~/.local/share/catacomb/bin`) removes
|
||||
/// both.
|
||||
pub fn bin_path() -> PathBuf {
|
||||
let mut p = crate::ytdlp_bin::bundled_dir();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Federation — read-only browsing of a *peer* yt-offline instance's library.
|
||||
//! Federation — read-only browsing of a *peer* catacomb instance's library.
|
||||
//!
|
||||
//! A [`RemoteClient`] talks to another instance over its existing web API:
|
||||
//! it fetches `/api/library`, logging in first if the peer has a password
|
||||
|
|
@ -61,7 +61,7 @@ impl RemoteClient {
|
|||
let client = reqwest::blocking::Client::builder()
|
||||
.cookie_store(true)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.user_agent("yt-offline-federation")
|
||||
.user_agent("catacomb-federation")
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::blocking::Client::new());
|
||||
RemoteClient {
|
||||
|
|
|
|||
10
src/tray.rs
10
src/tray.rs
|
|
@ -62,14 +62,14 @@ struct TrayModel {
|
|||
#[cfg(target_os = "linux")]
|
||||
impl ksni::Tray for TrayModel {
|
||||
fn id(&self) -> String {
|
||||
"yt-offline".into()
|
||||
"Catacomb".into()
|
||||
}
|
||||
fn title(&self) -> String {
|
||||
"yt-offline".into()
|
||||
"Catacomb".into()
|
||||
}
|
||||
fn tool_tip(&self) -> ksni::ToolTip {
|
||||
ksni::ToolTip {
|
||||
title: "yt-offline".into(),
|
||||
title: "Catacomb".into(),
|
||||
description: "Self-hosted archive for YouTube + friends".into(),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ impl ksni::Tray for TrayModel {
|
|||
use ksni::menu::StandardItem;
|
||||
vec![
|
||||
StandardItem {
|
||||
label: "Show yt-offline".into(),
|
||||
label: "Show Catacomb".into(),
|
||||
activate: Box::new(|m: &mut Self| {
|
||||
let _ = m.tx.send(TrayEvent::Show);
|
||||
}),
|
||||
|
|
@ -153,7 +153,7 @@ pub fn start(icon_png_bytes: &[u8]) -> Option<TrayHandle> {
|
|||
};
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("yt-offline-tray".into())
|
||||
.name("catacomb-tray".into())
|
||||
.spawn(move || {
|
||||
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
|
|
|
|||
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.
|
||||
#[serde(default)]
|
||||
max_concurrent: usize,
|
||||
/// If true, invoke the bundled yt-dlp under `~/.local/share/yt-offline/bin/`
|
||||
/// If true, invoke the bundled yt-dlp under `~/.local/share/catacomb/bin/`
|
||||
/// instead of the system PATH yt-dlp.
|
||||
#[serde(default)]
|
||||
use_bundled_ytdlp: bool,
|
||||
|
|
@ -806,7 +806,7 @@ pub fn write_cookies(text: &str) -> Result<usize, String> {
|
|||
std::fs::write(&path, &content).map_err(|e| e.to_string())?;
|
||||
// cookies.txt carries live session credentials — tighten the mode so it
|
||||
// isn't world-readable on multi-user systems. Best-effort, like the
|
||||
// similar guard on yt-offline.db.
|
||||
// similar guard on catacomb.db.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
|
@ -2406,7 +2406,7 @@ async fn get_feed_all(State(state): State<Arc<WebState>>, headers: HeaderMap) ->
|
|||
.collect();
|
||||
let items = build_feed_items(all, root, &base, &state.feed_token, FEED_LIMIT);
|
||||
let feed = crate::feed::Feed {
|
||||
title: "yt-offline — Library".into(),
|
||||
title: "Catacomb — Library".into(),
|
||||
description: "Your archived videos as a podcast feed.".into(),
|
||||
link: format!("{base}/"),
|
||||
items,
|
||||
|
|
@ -2430,7 +2430,7 @@ async fn get_feed_channel(
|
|||
.chain(ch.playlists.iter().flat_map(|p| p.videos.iter())).collect();
|
||||
let items = build_feed_items(vids, root, &base, &state.feed_token, FEED_LIMIT);
|
||||
let feed = crate::feed::Feed {
|
||||
title: format!("yt-offline — {}", ch.name),
|
||||
title: format!("Catacomb — {}", ch.name),
|
||||
description: format!("Archived videos from {}.", ch.name),
|
||||
link: format!("{base}/"),
|
||||
items,
|
||||
|
|
@ -2696,7 +2696,7 @@ async fn delete_folder(
|
|||
/// session credentials that shouldn't fly over the wire unprompted, and
|
||||
/// keeping the endpoint to one file means no extra deps for tar/zip.
|
||||
async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
|
||||
let db_path = state.channels_root.join("yt-offline.db");
|
||||
let db_path = state.channels_root.join("catacomb.db");
|
||||
let Ok(bytes) = std::fs::read(&db_path) else {
|
||||
return (StatusCode::NOT_FOUND, "no db file on disk (running in-memory?)").into_response();
|
||||
};
|
||||
|
|
@ -2704,7 +2704,7 @@ async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
|
|||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let filename = format!("yt-offline-{now_secs}.db");
|
||||
let filename = format!("catacomb-{now_secs}.db");
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/x-sqlite3".to_string()),
|
||||
|
|
@ -2719,7 +2719,7 @@ async fn get_backup_db(State(state): State<Arc<WebState>>) -> Response {
|
|||
/// rows into the live DB. Mirrors `GET /api/backup/db` in the opposite
|
||||
/// direction.
|
||||
///
|
||||
/// The body is the raw `yt-offline.db` bytes — same shape as what
|
||||
/// The body is the raw `catacomb.db` bytes — same shape as what
|
||||
/// `get_backup_db` produces. We write it to a sibling temp file, hand it
|
||||
/// to [`Database::restore_from_backup`] for the actual merge, then
|
||||
/// refresh the in-memory watched / positions / flags caches so the next
|
||||
|
|
@ -2750,7 +2750,7 @@ async fn post_restore_db(
|
|||
// Write to a sibling tmp file so it lives on the same filesystem as
|
||||
// the live DB — keeps ATTACH happy and avoids cross-device rename
|
||||
// issues if we ever extend this to atomic-replace semantics.
|
||||
let tmp_path = state.channels_root.join(".yt-offline.restore.tmp");
|
||||
let tmp_path = state.channels_root.join(".catacomb.restore.tmp");
|
||||
if let Err(e) = std::fs::write(&tmp_path, &body) {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("write tmp file: {e}")).into_response();
|
||||
|
|
@ -3124,7 +3124,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
let dir = crate::platform::platform_root(&channels_root, p);
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
}
|
||||
let db_path = channels_root.join("yt-offline.db");
|
||||
let db_path = channels_root.join("catacomb.db");
|
||||
let config_path = std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("config.toml");
|
||||
|
|
@ -3374,7 +3374,7 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
return;
|
||||
}
|
||||
};
|
||||
println!("yt-offline web UI: http://localhost:{port}");
|
||||
println!("Catacomb web UI: http://localhost:{port}");
|
||||
|
||||
// Now that we're accepting connections, run the initial library scan in
|
||||
// the background so a cold-cache scan of a large library doesn't delay
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>yt-offline</title>
|
||||
<title>Catacomb</title>
|
||||
<!-- Embedded UI fonts (base64 below, offline-safe — no CDN): Instrument Serif
|
||||
by Rodrigo Fuenzalida and Hanken Grotesk by Alfredo Marco Pradil, both
|
||||
under the SIL Open Font License 1.1. -->
|
||||
|
|
@ -87,9 +87,9 @@
|
|||
.modal-hdr h2{flex:1;font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.modal-body{display:flex;gap:12px;overflow:hidden;flex:1;min-height:0}
|
||||
.modal video{flex:1;min-width:0;max-height:75vh;background:#000}
|
||||
/* Custom player (used for non-seekable transcode streams): the live ffmpeg
|
||||
pipe has no byte ranges, so we drive a scrubber off the known duration and
|
||||
re-request the stream at an offset on seek. */
|
||||
/* Custom player: direct files and non-seekable transcode streams now share
|
||||
the same chrome. The ffmpeg pipe has no byte ranges, so transcode scrubbing
|
||||
re-requests the stream at an offset while direct files set currentTime. */
|
||||
.vid-wrap{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||
.vid-wrap video{flex:1;min-height:0;max-height:72vh;width:100%;background:#000}
|
||||
.vctrl{display:flex;align-items:center;gap:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:6px 8px;flex-wrap:wrap}
|
||||
|
|
@ -499,7 +499,7 @@
|
|||
<body>
|
||||
<header>
|
||||
<button id="menu-btn" onclick="toggleSidebar()">☰</button>
|
||||
<h1>yt-offline</h1>
|
||||
<h1>Catacomb</h1>
|
||||
<input type="search" id="search" placeholder="Filter…" oninput="saveUiState();renderGrid()">
|
||||
<select id="sort" onchange="saveUiState();renderGrid()">
|
||||
<optgroup label="Download date">
|
||||
|
|
@ -598,7 +598,7 @@ function closeSidebar(){document.getElementById('sidebar').classList.remove('ope
|
|||
/* ── API ────────────────────────────────────────────────────────── */
|
||||
async function api(path,opts){
|
||||
const r=await fetch(path,opts);
|
||||
// Session gone (expired, or the server restarted — sessions are in-memory):
|
||||
// Session gone (expired, cleared by password change/logout, or otherwise absent):
|
||||
// the cached SPA would otherwise just throw cryptic "authentication required"
|
||||
// toasts on every action. Reload instead so the server serves the login page.
|
||||
if(r.status===401){
|
||||
|
|
@ -1632,8 +1632,8 @@ function shufflePlay(){
|
|||
}
|
||||
playVideo(pick.id);
|
||||
}
|
||||
// Player state for the custom transcode controls. For direct /files/ videos
|
||||
// `isTranscode` is false and native <video controls> handle everything.
|
||||
// Player state for the custom controls. For direct /files/ videos `isTranscode`
|
||||
// is false and seeking sets currentTime; transcode seeks by reloading at start=.
|
||||
let player={isTranscode:false,base:'',seekBase:0,duration:0};
|
||||
function transcodeUrlAt(t){const u=player.base;const sep=u.includes('?')?'&':'?';return safeUrl(u+sep+'start='+Math.max(0,t).toFixed(2));}
|
||||
// Effective playback position: a transcode element's currentTime restarts at 0
|
||||
|
|
@ -2289,7 +2289,7 @@ async function openSettings(){
|
|||
<label for="cf-ytdlp-bundled">Use bundled yt-dlp + deno</label>
|
||||
<input type="checkbox" id="cf-ytdlp-bundled" ${cur.use_bundled_ytdlp?'checked':''}>
|
||||
</div>
|
||||
<div class="settings-hint" style="margin-bottom:6px">Bundled binaries live in ~/.local/share/yt-offline/bin/ and include a JS runtime (deno) needed for YouTube signature deciphering. When off, the yt-dlp on your PATH is used.</div>
|
||||
<div class="settings-hint" style="margin-bottom:6px">Bundled binaries live in ~/.local/share/catacomb/bin/ and include a JS runtime (deno) needed for YouTube signature deciphering. When off, the yt-dlp on your PATH is used.</div>
|
||||
<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px">
|
||||
<button onclick="updateYtdlp(this)">${cur.bundled_ytdlp_installed?'⟳ Update bundled':'⤓ Install bundled'}</button>
|
||||
<span style="font-size:11px;color:var(--muted)">${cur.bundled_ytdlp_installed?'✓ installed':'not installed'}</span>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>yt-offline — Sign in</title>
|
||||
<title>Catacomb — Sign in</title>
|
||||
<style>
|
||||
/* Standalone page (the SPA's embedded fonts aren't available here), so the
|
||||
display face is a system serif stack — editorial without extra bytes. The
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="brand"><span class="dot"></span><h1>yt-offline</h1></div>
|
||||
<div class="brand"><span class="dot"></span><h1>Catacomb</h1></div>
|
||||
<div class="tag">your private archive</div>
|
||||
<div class="field"><input type="password" id="pwd" placeholder="Password" autofocus onkeydown="if(event.key==='Enter')login()"></div>
|
||||
<button onclick="login()">Sign in</button>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
//! app invokes its own yt-dlp instead of whatever's on PATH. To get the
|
||||
//! full feature set — most importantly `curl_cffi`-backed `--impersonate`
|
||||
//! support — we install yt-dlp into a self-contained Python virtualenv
|
||||
//! under `~/.local/share/yt-offline/venv/`. A bundled `deno` lives at
|
||||
//! `~/.local/share/yt-offline/bin/deno` so yt-dlp can evaluate the
|
||||
//! under `~/.local/share/catacomb/venv/`. A bundled `deno` lives at
|
||||
//! `~/.local/share/catacomb/bin/deno` so yt-dlp can evaluate the
|
||||
//! JavaScript signature-deciphering code YouTube serves with the player.
|
||||
//!
|
||||
//! Layout:
|
||||
//! ```text
|
||||
//! ~/.local/share/yt-offline/
|
||||
//! ~/.local/share/catacomb/
|
||||
//! bin/ ← prepended to PATH so yt-dlp finds deno
|
||||
//! deno
|
||||
//! venv/
|
||||
|
|
@ -30,7 +30,7 @@ pub fn bundled_root() -> PathBuf {
|
|||
let home = std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
home.join(".local").join("share").join("yt-offline")
|
||||
home.join(".local").join("share").join("catacomb")
|
||||
}
|
||||
|
||||
/// Directory holding non-Python binaries (currently just `deno`). Also the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue