Archilyzer · Source

archilyzer

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

commit 48343182098bc1eb3570f35680e698e5dc9060dc
parent f9fd2272b3840c5a0e7e7a4771c980ae09bf625d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 29 Sep 2026 20:51:35 -0400

common: the hold's words move to lib/channelMediaHold.ts — which statuses hold, why (no path), and the ways out a refusal names — so the index build can share them; the stats build's messages are unchanged

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

Diffstat:
Mcommon/controller/buildStats.ts | 34+++++++++-------------------------
Acommon/lib/channelMediaHold.ts | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 70 insertions(+), 25 deletions(-)

diff --git a/common/controller/buildStats.ts b/common/controller/buildStats.ts @@ -63,12 +63,14 @@ import { import type { VideoStatus } from "../lib/stats"; import type { ChannelConfig } from "../lib/channelConfig"; import { readChannelConfigFile } from "./channels"; +import { inspectChannelMedia } from "../lib/channelMedia"; import { - inspectChannelMedia, - type ChannelMediaStatus, -} from "../lib/channelMedia"; + HELD_WAYS_OUT, + describeHeld, + heldReason, + isMediaHeld, +} from "../lib/channelMediaHold"; import { getSettings } from "../lib/settings"; -import { locationLabelOfDataDir } from "../lib/storageLocations"; import type { Paths } from "../lib/paths"; import { listSites, siteStatsDir } from "../lib/site"; import { @@ -248,16 +250,6 @@ async function resolveAcquisitionDates( return { downloadedDate, transcribedDate }; } -// Why a channel is held, without the paths inspectChannelMedia's `detail` -// carries (/storage shows those). -const HELD_REASON: Record<ChannelMediaStatus, string> = { - unreachable: "its media is not reachable (drive not mounted?)", - "in-transition": "a move of its media is in progress or was interrupted", - inconsistent: "its data link and its config disagree", - ok: "reachable", - "in-place": "reachable", -}; - // `held` maps each channel whose media is not reachable to why, in words with // no path in them: it is not scanned, and the caller keeps its cached stats // (see the file header). @@ -292,12 +284,8 @@ async function scanSource( // An unmounted drive is not an empty channel (lib/channelMedia.ts): the // readdir below would fail and every one of its stats would be removed. const media = await inspectChannelMedia({ channelsDir }, ch.name, cfg); - if (media.status !== "ok" && media.status !== "in-place") { - const label = locationLabelOfDataDir(cfg.dataDir ?? media.target, locations); - held.set( - ch.name, - `${HELD_REASON[media.status]}${label ? `, on location "${label}"` : ""}`, - ); + if (isMediaHeld(media.status)) { + held.set(ch.name, heldReason(media, cfg.dataDir, locations)); continue; } const dataDir = path.join(channelDir, "data"); @@ -444,11 +432,7 @@ export async function buildStats({ await root.close(); throw new Error( `The stats cache must be rebuilt (stats schema ${storedSchema ?? "<none>"} -> ${STATS_SCHEMA_VERSION}), ` + - `but ${held.size} channel(s) cannot be read: ` + - [...held].map(([slug, why]) => `${slug} (${why})`).join("; ") + - `. For each: mount its media and run this again; or repair or re-point its location on /storage; ` + - `or finish or clear its move (the channel's Storage panel); or, if it is gone for good, ` + - `delete the channel or set excludeFromBuild in its config.`, + `but ${held.size} channel(s) cannot be read: ${describeHeld(held)}. ${HELD_WAYS_OUT}`, ); } log( diff --git a/common/lib/channelMediaHold.ts b/common/lib/channelMediaHold.ts @@ -0,0 +1,61 @@ +import type { + ChannelMediaLocation, + ChannelMediaStatus, +} from "./channelMedia"; +import { + locationLabelOfDataDir, + type StorageLocation, +} from "./storageLocations"; + +// THE HOLD, in the words both pool-wide builds use. +// +// The index build (controller/buildIndex.ts) and the stats build +// (controller/buildStats.ts) each walk every channel's `data/`. A channel whose +// media cannot be read — a relocated `data/` on an unmounted drive, a move in +// progress, a link and a config that disagree — is HELD by both: not rescanned, +// and what the last build knew of it kept, rather than read as a channel with +// no videos and removed (lib/channelMedia.ts says why that reading is the +// dangerous one). This module is only the shared vocabulary: which statuses +// hold, why, in words with no path in them (/storage shows the paths), and the +// ways out a refusal names. Each build decides for itself what "kept" means. +// +// Pure: no I/O. The caller asks inspectChannelMedia and passes the answer in. + +// "ok" and "in-place" are read; every other status holds, including any a later +// inspectChannelMedia adds. +export function isMediaHeld(status: ChannelMediaStatus): boolean { + return status !== "ok" && status !== "in-place"; +} + +// Why a channel is held, without the paths inspectChannelMedia's `detail` +// carries. +export const HELD_REASON: Record<ChannelMediaStatus, string> = { + unreachable: "its media is not reachable (drive not mounted?)", + "in-transition": "a move of its media is in progress or was interrupted", + inconsistent: "its data link and its config disagree", + ok: "reachable", + "in-place": "reachable", +}; + +// The reason, and the storage location's label when the channel's media is on +// one. `dataDir` is the channel config's; the inspector's target stands in when +// the config names none (a move in flight). +export function heldReason( + media: Pick<ChannelMediaLocation, "status" | "target">, + dataDir: string | undefined, + locations: StorageLocation[], +): string { + const label = locationLabelOfDataDir(dataDir ?? media.target, locations); + return `${HELD_REASON[media.status]}${label ? `, on location "${label}"` : ""}`; +} + +// A held channel named in a refusal, as `slug (why)`, joined. +export function describeHeld(held: Map<string, string>): string { + return [...held].map(([slug, why]) => `${slug} (${why})`).join("; "); +} + +// What a refusal tells the operator to do, mounting first. +export const HELD_WAYS_OUT = + `For each: mount its media and run this again; or repair or re-point its location on /storage; ` + + `or finish or clear its move (the channel's Storage panel); or, if it is gone for good, ` + + `delete the channel or set excludeFromBuild in its config.`;