# Release 21 — playable archives: local media for held videos, WebTorrent playback on the sites Written 2026-10-09. Pilot: the TISM (The Incredible Salt Mine) v1 torrent archive → `the-incredible-salt-mine` (a Jeralyzer member channel). Two outcomes: 1. **Agents can clip TISM** — every held TISM video gets its media on disk, through the editor, so `fetch_clip` / `fetch-windows` / `reports prepare` / report-to-video cut it locally. 2. **Readers can play TISM** — the site plays the video in the transcript page (click-to-seek works) from a torrent. Nothing is hosted on Cloudflare: the bytes come from peers, and a home seeder behind a VPN is the **peer of last resort** — it seeds a torrent only while no other seeder has it. ## Rulings (operator, 2026-10-09) - **Pilot on `jeralyzer-private` first**; public Jeralyzer is a later, separate ruling. - **No web seed, no R2.** A **home seeder only**, behind a VPN, with an app layer that seeds a torrent only when no other seeder exists (peer of last resort). Accepted consequence: a torrent with no other seeder plays only while the home seeder is up. - **ASR for the 3 new videos only** (`pnpm ops transcribe-one`); the other 161 keep their YouTube captions. - *Open:* the attribution line for the archived copy on the video page (the archive's README names its maker and sources). Records state facts; the wording is the operator's. ## What is there (measured 2026-10-09) | | | |---|---| | Archive | `/v1-torrent/`: `Uploads/YouTube.zip` (36.5 GB, **stored**, 183 folders), `SoundCloud.7z` (2 mp3: TISM001/002), `3rd-party-Odysee.7z` (2 mp4, solid LZMA2), `Images.7z`, `Official-page-archives.7z`, `README-v1.html` | | YouTube.zip folders | 183: **169 with media** (36.4 GB, **222.7 h**), 10 `[LOST]` (source.txt only), 4 others without media; 33 carry an archive.org `youtube-_archive.torrent`; each folder has yt-dlp `.info.json`/`.description`/thumbnail, or `description.txt` + `source.txt` | | Codecs (ffprobe, in place) | 124 mp4 h264/aac with the **moov at the back**, 21 mp4 h264/aac faststart, 9 webm vp9/opus, 11 mkv vp9/aac, 2 mkv h264/opus, 1 mp4 av1/aac, 1 mkv av1/opus. Heights: 109×360p, 35×720p, 21×1080p, 4×144p | | Corpus | `the-incredible-salt-mine` (handling youtube, the channel is deleted): 171 videos, each `metadata.info.json` + cues (161 YouTube VTT, 10 `transcript.json`), **no media**. All 171 ids are in the archive. 3 archive videos with media are **not held**: `6ijPdSuZdws`, `VcFA_LOnQoc`, `xLgf_MGQzXk` | | Disk | the archive drive has 849 GB free; the saved-video store already lives on it (`archilyzer-media/saved-videos`) | ## Gaps in the code (verified) - **No local-file ingest.** Every import takes a URL or an archive.org identifier (`import-one`, `import-archive-org`); `sourceKind` is `video | social`. Only the downloader attaches a container to a held record (`persistSourceVideo`, `common/lib/savedVideo-server.ts:102`). - **A clip window never reads a saved container.** `fetchWindowAction` (`editor/app/channels/[slug]/videos/[id]/videoActions.ts`, ~1080) asks `findContainingClipWindow`, then the network; only `full: true` returns the saved container (~1259), and only `evidenceClip-server.ts` (reports prepare, report-to-video) cuts from it. For a deleted channel, a window request fails every time today. - **The sites already play files and seek**: every player behind `PlayerProvider.tsx` exposes `seekTo`, and cue rows call it (`TranscriptModal.tsx` `CueRow`), so click-to-seek needs nothing new. - **No torrent code beyond archive.org ingest** (aria2c, `archiveOrgTorrent*.ts`). No WebTorrent anywhere. - **The export site ships a root-scope service worker** (`export/service-worker/site-sw.js`). WebTorrent 2's browser streaming needs a service worker (`client.createServer({ controller })`), so the two must be one worker. The site sends no CSP, so wss trackers and blob media are not blocked. ## Slices ### D1 — attach local media to held videos `[unit]` (clippability; nothing public) - `common/controller/attachMedia.ts` + job kind `attach-media` (drainable, `needsMedia`), ops action `attach-media {slug, source, items?:[{id, path}], match?, createRecords?, dryRun?}`, CLI `archilyzer media attach`. - **Sources:** a directory, or a **zip read in place** (a stored entry is copied straight out, no temp dir; a deflated or 7z entry is extracted to a scratch dir on the target disk first). The id comes from the folder's trailing `(<11-char id>)`, else the media file's `-.` suffix, else an explicit item. `dryRun` lists matched, unmatched, already attached, not held and `[LOST]`. - **Writes** through `persistSourceVideo` (the one writer): the container into the saved-video store as `source-media.`, `keepReason: "pin"`, a new `origin: {kind: "local-archive", archive, entry, sha256, attachedAt}` (which archive, which entry, its hash). Refuses over an existing pointer unless `replace`; never modifies the archive. Guards: `assertSavedVideosStoreWritable`, the media-reachable guard. - **Not held** (the 3): `createRecords: true` writes `metadata.info.json` from the folder's yt-dlp `.info.json` (else from `description.txt`/`source.txt`, the title from the folder name), then `pnpm ops transcribe-one` for each. - `[LOST]` folders are listed, never attached. ### D2 — clip windows cut from a saved container `[unit]` + `[spec ops-api]` - In `fetchWindowAction`, after the clip-window cache and BEFORE the network: when the video has a saved container covering `[from − pad, to + pad]`, cut the window locally (evidenceClip's ffmpeg path), write it into `data//clips/` like a fetched window with provenance `{source: "saved-video"}`, and answer `cached: true`. `fetch-windows` batches then use no network for such videos. Helps every persisted video, not only TISM. - `mcp/src/instructions.ts`: a video held locally is cut without a fetch. - Starts after release 19's A5 merges (A5 edits the same fetch-window code: dedupe + queue key). ### D3 — streamable copies + torrents `[unit]` - `common/lib/playableMedia.ts` picks a **lossless** remux (`-c copy`) per container: h264/vp9/av1 + aac/opus → mp4 `+faststart`; vp9/av1 + opus in webm stays webm. 169 copies, ~36 GB, on the archive drive under `archilyzer-media/playable///.`, keyed by the source's sha256 (a re-run is a no-op). - One **single-file torrent per video** (`create-torrent`, from the WebTorrent ecosystem): piece length scaled to the size (256 KiB–1 MiB), no `urlList`, `name` = `.`, no `comment`/`createdBy` naming a person or a private URL. The announce list is not in the info dict, so the infohash depends only on the bytes. The pilot announces only to a **self-hosted tracker**, so nothing public learns the infohashes. Going public later means adding public WSS trackers (browsers) and UDP trackers (desktop clients) to the magnet: same infohash, no re-hash. - Job kind `prepare-playable` (heavy: runs in release 19 B1's `pnpm heavy` slot once that lands). ### D4 — publish the torrents `[unit]` - `site.json` gains `playable: { channels: string[], trackers: string[] }` (schema + SITE.md regenerated): the channels whose records carry a torrent, and the announce URLs that site's magnets name (the pilot: the self-hosted tracker only). - The index stage writes `.export-index/sites//playable.json` (id → infohash, magnet, mime, bytes). The site's `inputSig` includes it, so adding a torrent makes the build stale. No media bytes leave the machine. - Compose: records of a playable channel gain `playable: {magnet, infoHash, mime, bytes}` in the published JSON; each `.torrent` ships in the bundle (`/t/.torrent`, ~170 small files, within the 15,000 cap). `corpus.json` / `llms.txt` name the field. ### D4b — the seeder: peer of last resort, behind a VPN `[unit]` + a manual network test - `archilyzer seed`, a long-running node service: WebTorrent 2 in node, with WebRTC via `node-datachannel` so it serves browsers, and TCP/uTP for desktop clients. It seeds the playable manifests of the sites named in settings (`seeder: { sites, maxUploadKiBps, maxConnections, pollSeconds, standbyAfterSeconds }`, SETTINGS.md regenerated). - **Last resort**, per torrent: the seeder polls the trackers' scrape (`complete`, minus itself) and its own peer view. - Other seeders present for `standbyAfterSeconds` → **standby**: stop announcing, close peers, keep the data. - No other seeder (`complete` = 0) while a leecher announces, or a connected leecher with no other source → resume. - Both edges have hysteresis, so it does not flap, and the log names each transition and why. - Known cost: if the last other seeder leaves mid-stream, the viewer stalls for up to `pollSeconds` until the seeder resumes. - **Behind a VPN, with no way around it:** the seeder runs in its own network namespace or container whose only route is the VPN tunnel (a docker compose profile `seeder` with a WireGuard sidecar). Kill switch: tunnel down means no network, never the home connection. ICE gathers candidates only from the tunnel interface, so WebRTC never offers the home address (the classic WebRTC leak). `archilyzer doctor` reports the seeder's egress IP against the host's and refuses to call the seeder healthy if they match. - **The risk to measure first:** whether the VPN's NAT lets browser peers connect directly (UDP hole punching). If it does not, the options are a VPN with port forwarding or a TURN relay. That is a ruling, not a default. - Manual network test (recorded): a browser on another network plays a torrent through the seeder, and the seeder's candidates and the tracker's view of its address show only the VPN exit. ### D5 — the player `[unit]` + `[spec]` (export e2e) - `common/components/TorrentPlayer.tsx`: WebTorrent 2 (npm, bundled, dynamically imported only on a playable record), `client.add(magnet)` → streamed into a native `