Two-task TDD plan: (1) serde-derive the library structs + library_snapshot
table with save/load + unit tests; (2) desktop wiring to seed self.library at
launch and write the snapshot after every scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design for Step 2: persist the scanned Vec<Channel> as a JSON blob keyed by
library root, seed self.library from it at launch for instant render, then
background-rescan and swap. Desktop-only; blob over normalized tables (YAGNI).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop GUI's startup library scan runs on a background thread; on a
cold filesystem cache a large library on encrypted/compressed storage takes
~60s to walk + stat, during which the content area rendered an empty video
list. That looked like a broken/empty library and prompted a needless Rescan
(which only appeared to help because the OS cache was warm by then).
Render a centered spinner + "Scanning library…" while the initial scan is
in flight (library_load_rx still open and library empty). The scan/drain
pipeline is unchanged; this is display-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At the ~1000px window minimum, the toolbar's right-aligned sort group
ran out of row width and overflowed leftward past the panel edge; egui
then advances the vertical cursor from the overflowed rect, so every
row below (view toggle, the whole list) shifted left and was clipped
under the sidebar.
The sort chips now right-align only when a font-metrics measurement
says they fit, and otherwise drop to their own wrapped row. The top
bar likewise wraps instead of pushing nav buttons off-screen, with the
right-aligned status label width-guarded the same way (truncating
fallback).
Verified via live GUI screenshots at 1000x700 and 1400x800; the wide
layout is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render List/Card/Grid through ScrollArea::show_rows so only the visible
row range is laid out per frame. Fixed-size cells (allocate_ui_with_layout
+ set_min_size) enforce a uniform lattice; titles truncate to one line with
the full title on hover. List separators are painted hlines inside the
cell; Grid is a manual horizontal run of fixed cells per lattice row with
cols computed from available width before show_rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executes docs/superpowers/specs/2026-07-07-runtime-performance-pass-design.md:
- database.rs: every pooled connection now runs WAL + synchronous=NORMAL
+ busy_timeout=5000 + foreign_keys=ON via with_init (in-memory pools
get the latter two). Fixes the SQLITE_BUSY failure mode under the
parallel scanner and makes FK cascades work on every connection, not
just whichever one last ran the old one-shot pragma in delete_folder.
- database.rs: prepare_cached for the per-video info_cache queries; new
info_cache_put_many bulk upsert (single transaction) replaces the
per-row autocommitted info_cache_put, with a round-trip unit test.
- library.rs: enrich_with_cache collects cache misses and flushes them
once per channel; title sort uses sort_by_cached_key.
- app.rs: Title/ChannelAsc sorts use sort_by_cached_key instead of
allocating two lowercased strings per comparison.
- Cargo.toml: panic=abort in release (crash-hook still fires; Cargo
forces unwind for test harnesses). PACKAGING.md documents the local
RUSTFLAGS target-cpu=native opt-in; repo stays portable.
- .gitignore: catacomb.db-wal / catacomb.db-shm sidecars.
Verified: 146 tests pass (integration suite exercises the pragmas
end-to-end), x86_64-pc-windows-gnu check green, live smoke shows
journal_mode=wal + batched cache writes landing correct rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mobile fixes (all in the embedded SPA):
- Move the mobile media queries to the bottom of the stylesheet — the
cinematheque design layer was appended after them, so its base rules
(header padding, video heights) silently overrode the phone layout by
source order. A comment now pins the ordering invariant.
- The design layer's uiDrop entry animation (fill-mode:both) permanently
overrode the sidebar's translateX(-100%), leaving the drawer stuck open
over the content on phones; the mobile block now disables it.
- Lower the single-column breakpoint 380px -> 350px: it was catching
360/375px phones and blowing every card up to full width.
- viewport-fit=cover so the env(safe-area-inset-*) paddings actually
resolve on iPhones; notch-safe padding on header/content/modals.
- 16px inputs on small screens (kills iOS Safari's focus auto-zoom),
same for the login page's password field.
- Mobile rules for the design-layer components that had none: full-height
command-palette sheet, clamped stats numerals, coarse-pointer touch
targets for the custom player (taller scrub track, always-visible
thumb, no volume slider).
- CSP font-src now allows data: — the embedded base64 woff2 fonts were
being blocked and silently fell back to system faces.
PWA:
- manifest.webmanifest, minimal service worker, and a new Catacomb arch
icon set (SVG source + rendered PNGs incl. maskable + apple-touch),
all include_str!/include_bytes!-embedded like the HTML.
- sw.js only intercepts GET navigations to "/" (network-first, cached
offline fallback) and the static assets; /api, /ws, /files,
/music-files, /feed are never touched, and it's served no-store so
binary upgrades keep propagating.
- Routes + auth_middleware allowlist so the browser can fetch the
statics pre-login; theme-color meta tracks the active theme's panel.
- tests/api.rs covers the new endpoints incl. the ungated-when-password
invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn android/ into a real Gradle app (Kotlin + Jetpack Compose + Material3)
that bundles an on-device yt-dlp engine and reuses the Phase-4 Rust JNI core.
- Bundled engine: youtubedl-android (Python + yt-dlp + ffmpeg + aria2c) via
Engine.kt; initialises off the main thread on first launch. Requires legacy
jniLib packaging so the bundled .zip.so payloads extract to disk.
- Intuitive navigation: bottom NavigationBar — Download / Files / Settings.
- Settings section: dedicated screen with a live theme picker, default-quality
chips, engine info, and a Rust-core demo.
- All 19 desktop themes ported from src/theme.rs into Theme.kt as Material3
colour schemes, shown as tappable swatches; selection persisted (Prefs.kt).
- Download screen: live platform detection through the Rust core as you type,
plus an improved animated determinate progress indicator (%/ETA/status/
success-error) and a scrollable log.
Toolchain pinned for reproducibility: Gradle 8.9 wrapper, AGP 8.5.2, Kotlin
1.9.24, Compose BOM 2024.06.00, compileSdk 34, minSdk 24, ABIs arm64-v8a +
x86_64. build-apk.sh runs Gradle on a JDK <= 21 (system JDK may break R8) and
builds the Rust libs first.
Verified on an Android 14 x86_64 emulator: installs, launches, engine reports
"yt-dlp ready", Rust .so loads, theme switching re-themes live and persists
across restart, all screens render. (A real YouTube download still depends on
the anti-bot/POT question — Phases 2-3, device-only.)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add android/demo — a minimal, dependency-free app (plain Android Views,
no Gradle/AGP/AndroidX/Compose) that calls the Phase-4 JNI core on-device
and displays the results. build-apk.sh drives the SDK build-tools directly
(aapt2 -> javac -> d8 -> zip -> zipalign -> apksigner), compiling against
android.jar alone so it needs no network and tolerates a very new system
JDK by routing the SDK tools through a JDK <= 21.
The app has three buttons -> platformFromUrl/platformDirName, classifyError,
and vttParse, each calling libcatacomb_core.so and showing the JSON.
Verified on an Android 14 (API 34) x86_64 emulator: installs, launches,
loads the .so, and returns correct JSON on-device
({"dir_name":"channels","display_name":"YouTube","icon":"..."} for a
YouTube URL). Output APK (out/, git-ignored): catacomb-spike-debug.apk,
v2+v3 signed, minSdk 24, native code for arm64-v8a + x86_64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement the shared-Rust-core leg of the Android Stage-1 prototype
(docs/superpowers/specs/2026-06-27-android-stage1-prototype-plan.md).
android/rust/catacomb_core is a cdylib that reuses the pure desktop
modules (vtt, error_class, platform) verbatim via #[path] includes — no
logic fork — and exposes them to Kotlin over JNI as String-in/String-out
entry points:
RustCore.vttParse(vtt) -> JSON [{start,text},...]
RustCore.classifyError(log) -> JSON {class,label,hint}
RustCore.platformFromUrl(url) -> JSON {dir_name,display_name,icon}
RustCore.platformDirName(url) -> folder name
Built with panic=abort + per-entry catch_unwind so a Rust panic can never
unwind into the JVM. build.sh locates the NDK portably and cross-compiles
to arm64-v8a + x86_64, depositing the .so into app/src/main/jniLibs/.
Verified: cargo test (38 pass), llvm-readelf confirms all 4 JNI symbols
exported in the arm64 .so, and an end-to-end host-JVM round-trip
(System.load + call each native method) returns correct JSON.
Phases 2/3/5 (WebView-BotGuard anti-bot) remain device-only and untouched.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All four questions answered: Q1-3 RISKY (credible paths), Q4 PROVEN. WebView-
BotGuard POT (YTDLnis-proven) collapses the JS-runtime problem; HLahwani/yt-
dlp-android solves Q1+Q2+Q3 together. One open question remains: does WebView-
POT recover anti-bot WITHOUT curl_cffi (broken on Android)? The Stage-1
prototype tests exactly that. Fallback: client-to-server app against existing web
API. Update ROADMAP 3.2 with pointer to findings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real build-proof run locally: vtt.rs cross-compiles to aarch64 + x86_64 Android
.so (NDK r26d, exported C-ABI symbol). cargo-ndk + uniffi recommended; ~5 days
to reuse 5-6 pure modules. On-device run-proof blocked by sandbox emulator
reaping, downgraded to symbol-export check per plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pivotal gate does NOT block: POT is required on mobile, but WebView-based
BotGuard token generation (proven in YTDLnis) is the durable path and collapses
the JS-runtime + POT problems into one, killing the Deno/Node sidecar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
External JS runtime now a hard yt-dlp dependency (Python jsinterp deprecated
for YouTube 2025.11.12); QuickJS-ng via NDK or HLahwani/yt-dlp-android is the
path. Also fixes a markdown line starting with '#'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six tasks (Q1–Q4 research + emulator repros, plus scaffold and go/no-go
synthesis) against the approved spec. Research-spike shape: investigate →
reproduce-where-cheap → record evidence → verdict, not TDD.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First sub-project of the Android client: a research spike to decide go/no-go
on a standalone on-device download engine (yt-dlp + JS runtime + POT) before
building any UI. Four research questions, "reproduce where cheap" on the
emulator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the old search modal with a keyboard-driven command palette: blurred
backdrop, centered input, results grouped by channel with highlighted FTS
snippets (char(2)/char(3) → <mark>), ↑↓ navigation, Enter to open, Esc to
close, recent searches + quick actions (Downloads/Stats/Health/Shortcuts) in
localStorage. Debounced queries to /api/search with a sequence guard so stale
responses don't clobber newer ones. Upload date is looked up client-side from
the loaded library (SearchHit has no date field). Opened by the 🔍 header
button and the `f` hotkey.
Verified: node --check passes, release builds clean, and a headless harness
screenshot confirms grouped results, highlighted matches, and selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Codeberg repo was renamed yt-offline → catacomb. Update repository/homepage
(Cargo.toml), url + git source (PKGBUILD), and the Windows-zip README Source
line (package.sh) to the new path. The PKGBUILD `$pkgname::` checkout-dir prefix
is no longer needed now that the repo basename matches pkgname.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Full product rename with a one-time data migration so existing installs keep
their library + bundled toolchain:
- Crate/binary: `yt-offline` → `catacomb` (Cargo.toml, deb/rpm assets, PKGBUILD,
package.sh, the desktop file → catacomb.desktop, launch.json, release CI,
tests/api.rs CARGO_BIN_EXE_catacomb). Codeberg repo URLs left as-is (real
addresses); PKGBUILD pins the checkout dir via `$pkgname::`.
- Data paths: DB `yt-offline.db` → `catacomb.db`, venv `~/.local/share/yt-offline`
→ `~/.local/share/catacomb`. `migrate_legacy_paths()` in main.rs adopts the old
names on first run (renames DB + WAL/SHM sidecars + the venv dir; best-effort,
no-op once migrated). Crash log + restore-temp + IPC socket names follow suit.
- Display: window/app title, tray, web page titles, feed titles, login + SPA
wordmarks → "Catacomb". Env override `YT_OFFLINE_RENDERER` → `CATACOMB_RENDERER`.
Verified: builds clean as target/release/catacomb, 128 unit + 11 integration
tests pass, and a scratch-dir run confirms the DB (+wal) and venv are migrated
with bytes preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The login page was the last surface still on the old flat style. Give it the
same identity as the rest of the UI: serif wordmark with the glowing crimson
"recording" dot, an italic "your private archive" tagline, accent aurora +
film-grain atmosphere on a gradient card, a themed focus ring and accent
button, and a soft entrance. Login JS is unchanged; honours reduced-motion.
Standalone page, so the display font is a system serif stack (no CDN, no
duplicating the SPA's embedded woff2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the browser's native <video controls> (used for direct /files/
playback) with one custom player for both direct and transcode streams, so
the chrome matches the reskin and gains features it never had. All seeking
still routes through playerSeek/effTime, so the transcode reload-at-offset
path is unchanged.
- Custom scrubber: accent played fill + buffered fill + chapter tick markers
+ a hover time tooltip; click/drag to seek (pointer events, works in both
modes — commits the seek on release so transcode only reloads once).
- Playback speed popover (0.5–2×) and persistent volume + speed + captions
across videos (localStorage).
- Captions (CC) toggle for overlaid <track> subtitles (off by default; the
searchable 📄 transcript pane is unchanged), PiP, fullscreen.
- Auto-hiding controls + cursor after idle while playing; a center play/pause
flash; gradient scrim.
- Expanded keyboard: space/k, j/l ±10, ←/→ ±5, ↑/↓ volume, m, c, p, f,
< > speed, 0–9 seek-to-percent — now active for direct videos too (they
previously relied on native controls).
Verified: node --check passes, release builds clean, live 8081 serves it, and
a headless screenshot of the control chrome (extracted CSS) renders correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>