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>
Subscribe to the whole library or a single channel in any podcast/media
app.
- src/feed.rs: pure RSS 2.0 + iTunes-namespace renderer with absolute
enclosure URLs, MIME-by-extension, HH:MM:SS durations, and a
chrono-free RFC-2822 date (civil-date algorithm). Unit-tested
(mime / escape / duration / date / render).
- web.rs: GET /feed.xml (whole library, newest first, capped at 300) and
GET /feed/:platform/:handle (one channel); /api/feed-info returns the
token for the UI. Enclosure + thumbnail URLs are absolute (derived from
Host + X-Forwarded-Proto) and carry the feed token.
- Auth: a stable read-only `feed_token` (persisted setting) lets a
tokenized `?token=` GET reach /feed*, /files, and /music-files even when
a password is set — podcast clients can't do the cookie login. Scoped to
reads of feeds + media; never /api mutations.
- Web UI: a "📡 Podcast feed" section in Settings (library URL + Copy) and
a "📡 Feed URL" button in each channel's options dialog.
Integration tests cover the served RSS (enclosure path, MIME, pubDate),
the channel feed + 404, and the full token gate (401 without token once a
password is set, 200 with it, /files reachable with it). 127 tests pass.
Desktop URL display follows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 🔍 library search now matches spoken words, not just titles /
channels / descriptions.
- database.rs: add a `transcript` column to the `video_search` FTS5 index.
Fresh DBs get it directly; existing ones are migrated (FTS5 has no ADD
COLUMN, so the table is recreated and search_meta cleared to force a
one-time reindex). `sync_search_index` reads each video's first subtitle
(mtime-gated like the description), flattens it via the shared `vtt`
parser (`transcript_text`, capped at 256 KB), and indexes it. Search
snippets now use column -1 so the excerpt comes from whichever column
matched (description or transcript).
- library.rs: `build_search_entries` passes the first subtitle path.
- Both UIs: search copy updated to mention transcripts.
Tests: a unit test (a word only in the .vtt is found), a migration test
(seed an old 5-column index + stale meta, open, confirm the recreated
index indexes transcript text), and the search integration test now seeds
a subtitle and asserts a spoken-only word hits. Also bumped the test
server's startup budget so the ffmpeg-heavy dedup test can't starve a
sibling server into a flaky timeout. 120 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the fingerprint engine into the web Maintenance view.
- web.rs: a DedupState job (one at a time) on its own thread. POST
/api/maintenance/dedup/scan snapshots the library, mtime-gates against
stored fingerprints, fingerprints the new/changed videos in parallel
(progress counter), prunes vanished entries, groups by visual
similarity, and builds review rows (title/channel/size/files + a
recommended-keep = largest copy). GET /api/maintenance/dedup/status
polls progress + results. Deletion reuses /api/maintenance/remove.
- index.html: a "Similar content (perceptual)" section in the Maintenance
modal — Scan button, live progress bar, grouped results with
checkboxes (recommended-keep pre-unchecked), and a re-scan that drops
deleted copies. Poller self-cancels when the modal closes.
Integration test (ffmpeg-gated): generates orig + CRF-38 downscaled
re-encode + an unrelated clip, runs the real scan end-to-end, and asserts
the first two group while the third stays out. 118 tests pass.
Desktop Maintenance UI next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new FTS5 index (`video_search`) over every video's title, channel, and
description — searchable across the whole library, not just the loaded
grid. `search_meta` tracks each video's mtime so a routine rescan only
re-reads a description sidecar when the video actually changed; vanished
videos are evicted. The index is refreshed after every scan in both
front-ends via the shared `library::build_search_entries`.
- database.rs: schema + `sync_search_index` (mtime-gated, one txn) +
`search_videos` (ranked, with a highlighted snippet) + a safe prefix
MATCH builder. Unit-tested (index/search/prefix/AND/evict/garbage).
- web.rs: `GET /api/search?q=&limit=`; index synced after the initial
scan, rescan, and maintenance-remove.
- Web UI: a 🔍 header button + `f` shortcut open a debounced search modal
with ranked results and highlighted description snippets; clicking a
result jumps to it.
- Desktop (egui): a 🔎 Search button opens a floating results window
querying the same index; clicking a result plays the video. Closes the
desktop/web parity gap for search.
Integration test seeds a real video + description, rescans, and asserts
title / prefix / description-only matches hit and unrelated misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment viewer (web UI): the flat tree gains search (highlight + keep
ancestor context), sort (threaded / top / newest / oldest), per-thread
collapse/expand with collapse-all/expand-all, an OP badge for the
uploader, and a "new since last visit" highlight + count (localStorage
per-video timestamp). API now returns each comment's `timestamp` and
`author_is_uploader`.
SponsorBlock: was a hardcoded `--sponsorblock-mark all`. Now a real
setting (off / mark / remove) with a per-channel override, threaded
through the full five touchpoints — config `[backup].sponsorblock_mode`
(default "mark", preserving prior behavior), DownloadOptions override,
a downloader resolver (apply_sponsorblock), and both UIs (egui Settings
+ channel dialog; web Settings modal + channel dialog + SettingsPayload).
Integration tests extended: settings round-trip asserts the sponsorblock
default + persistence; channel-options round-trip asserts the override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unit tests cover parsers/resolvers but not whether the axum handlers,
SQLite layer, and config persistence actually wire together. New
tests/api.rs spawns the compiled binary in --web mode against a scratch
library dir and exercises the real HTTP API the way a browser does —
no network / yt-dlp needed (every endpoint here is local state).
Coverage: index + /api/library served; ETag conditional GET → 304;
settings round-trip (POST → GET reflects → config.toml on disk updated);
folder CRUD + the self-parent cycle guard (→ 400); notes upsert/delete;
channel-options store + is_empty() clear; backup/db returns a real
SQLite file.
HTTP via curl (transparently handles the server's gzip + chunked
encoding; already a CI dependency) with a graceful skip if curl is
absent. Each test gets its own server, port (bind :0 to grab a free
one), and tempdir, so they run in parallel and clean up on drop.
Added .forgejo/workflows/test.yml so `cargo test` (unit + integration)
runs on every push/PR — the release workflow only fired on tags. 105
tests pass (98 unit + 7 integration).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>