Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit bb156c29beaa3bd7e0c048dc82447bd6d047e788
parent 4458168e0e07afa34db8fa9e4bd4d722e6978cc2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 21:10:17 -0400

common, editor: the export build tiers nothing; a shard save sweeps nothing; a video delete is refused while its media is unreachable (review L4, L6, N9, N10)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mcommon/controller/archiveLiveChat.ts | 2++
Mcommon/controller/channelSnapshot.ts | 9++++++---
Mcommon/controller/normalizeLiveChat.ts | 5++++-
Mcommon/ytdlp/runYtdlp.ts | 5++++-
Meditor/app/channels/[slug]/videos/[id]/videoActions.ts | 31+++++++++++++++++++++++++++++--
5 files changed, 45 insertions(+), 7 deletions(-)

diff --git a/common/controller/archiveLiveChat.ts b/common/controller/archiveLiveChat.ts @@ -161,6 +161,8 @@ async function stageChannel( videoDir, channelSlug: ch.slug, configName: ch.config.name, + // A build moves no media (release 17): the jobs tier, this reads. + tier: false, }); } catch (err) { normalizeFailed++; diff --git a/common/controller/channelSnapshot.ts b/common/controller/channelSnapshot.ts @@ -366,9 +366,12 @@ export type ChannelSnapshot = { // snapshot is still written — its text is readable — and the bytes are // unknown, never 0. totalMediaBytes?: number; - // THE TEXT TIER's bytes: everything else under `data/<id>/` — transcripts, - // cues, metadata, sidecars, thumbnails, a persisted source container not yet - // moved to the store, scratch — but not `clips/`. On the corpus disk always. + // EVERYTHING ELSE UNDER `data/<id>/` BUT `clips/` — the bytes this channel + // holds on the corpus disk outside the tier and the clip cache: the text + // (transcripts, cues, metadata, sidecars, thumbnails) AND whatever is not + // tierable — a persisted source container not yet moved to the store, a + // partial download, scratch. Named for its bulk; on a channel with a few + // containers or partials it is more than the text alone. // Absent on a snapshot written before release 17: "unknown", never 0. totalTextBytes?: number; // The bytes held by `data/<id>/clips/` — the clip windows umtool and the diff --git a/common/controller/normalizeLiveChat.ts b/common/controller/normalizeLiveChat.ts @@ -27,6 +27,9 @@ export type NormalizeLiveChatOptions = { configName?: string; log?: (msg: string) => void; force?: boolean; + // Tier the raw replay after writing the cues (default true). The export + // build's archive pass passes false: a build reads, it does not move media. + tier?: boolean; }; export type NormalizeLiveChatOutcome = @@ -119,7 +122,7 @@ export async function normalizeLiveChat( // THE MEDIA TIER'S HOOK (release 17): the raw replay is read once, here, and // every other reader uses the cues just written — so it moves into // channels/<slug>/media now (a relative link stays). Never throws. - await tierMediaFile(opts.videoDir, LIVE_CHAT_FILENAME, { + if (opts.tier !== false) await tierMediaFile(opts.videoDir, LIVE_CHAT_FILENAME, { onLog: opts.log ? (line) => opts.log?.(line.trimEnd()) : undefined, }); return { status: "wrote", cuesPath }; diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts @@ -183,7 +183,10 @@ export async function runYtdlp(opts: RunYtdlpOpts): Promise<void> { if (!opts.channelConfig.url) { throw new Error("Channel has no `url` configured"); } - if (!MEDIA_WRITING_MODES.has(opts.mode)) return runYtdlpMode(opts); + // A shard save computes a slice and writes it; it downloads nothing. + if (!MEDIA_WRITING_MODES.has(opts.mode) || opts.saveShardOnly) { + return runYtdlpMode(opts); + } // THE MEDIA TIER'S HOOK FOR A BATCH (release 17): after the child returns — // done, failed or cancelled, whatever it finished — every video dir this run // touched has its media moved into channels/<slug>/media when the channel diff --git a/editor/app/channels/[slug]/videos/[id]/videoActions.ts b/editor/app/channels/[slug]/videos/[id]/videoActions.ts @@ -36,7 +36,11 @@ import { type KeepVideosField, type KeepVideosResult, } from "yt-dlp-transcript-common/controller/keepVideosMatching"; -import { ChannelMediaUnreachableError } from "yt-dlp-transcript-common/lib/channelMedia"; +import { + ChannelMediaUnreachableError, + inspectChannelMedia, +} from "yt-dlp-transcript-common/lib/channelMedia"; +import { onDrive } from "yt-dlp-transcript-common/lib/storageHealth"; import { setExcludedFromTruncatedCheck } from "yt-dlp-transcript-common/lib/excludeTruncatedCheck-server"; import { pruneFailedTranscriptions } from "yt-dlp-transcript-common/controller/failedTranscriptions"; import { transcodeAudio } from "yt-dlp-transcript-common/controller/transcode"; @@ -573,7 +577,30 @@ export async function deleteOneVideoDir( } // The video's media tier first (release 17): every tiered link's bytes and // its `media/<id>/`, which the recursive rm of the text dir cannot reach. - await removeVideoDirMedia(resolved); + // Only while the channel's media is reachable, and through the watchdog: on + // an unmounted drive the bytes would be orphaned, on a stalled one the rm + // would block. Refused before anything is touched. + const config = await readChannelConfig(getPaths(), slug); + const media = await inspectChannelMedia(getPaths(), slug, config, { fresh: true }); + if (media.status !== "ok" && media.status !== "in-place") { + return { + ok: false, + error: + `Refusing to delete ${videoId}: its media is not reachable — ` + + `${media.detail ?? media.status}. Nothing has been touched.`, + }; + } + const drive = config?.mediaDir?.trim(); + try { + await (drive + ? onDrive(drive, () => removeVideoDirMedia(resolved)) + : removeVideoDirMedia(resolved)); + } catch (err) { + return { + ok: false, + error: `Refusing to delete ${videoId}: ${(err as Error).message}. The text was not touched.`, + }; + } await rm(resolved, { recursive: true, force: true }); return { ok: true }; }