1// The processor registry. Each processor has a stable string `name` and an
2// integer `version`; bumping the version makes every applicable file re-run
3// that processor on the next sweep. This replaces the old scheme of hashing
4// `run.toString()` into a bitfield (rest in peace).
5export interface Processor {
6 /** stable identifier, stored in the `processors` table. never rename. */
7 name: string;
8 /** bump to re-run this processor on all applicable files */
9 version: number;
10 /** human-readable label for progress display */
11 title: string;
12 /** approximate cores used while running, for the scheduler */
13 cores?: number;
14 /** false when a required tool (ffmpeg, ...) is missing on this host */
15 enable?: boolean;
16 /** if set, only these extensions apply; otherwise all but `exclude` */
17 include?: Set<string>;
18 /** extensions that do not apply (only when `include` is unset) */
19 exclude?: Set<string>;
20 /** processor names that must complete before this one runs */
21 depends?: string[];
22 run(args: ProcessorRunArgs): Promise<void>;
23}
24
25export interface ProcessorRunArgs {
26 path: Path;
27 stat: fs.Stats;
28 mediaFile: MediaFile;
29 node: progress.Node;
30 /** aborts when the file is deleted mid-run; pass to subprocesses */
31 signal: AbortSignal;
32}
33
34export const processors: readonly Processor[] = [
35 ...mediaProcessors,
36 ...textProcessors,
37 ...compressProcessors,
38];
39
40// integrity checks, evaluated once at import
41{
42 const names = new Set<string>();
43 for (const p of processors) {
44 if (names.has(p.name)) throw new Error(`duplicate processor: ${p.name}`);
45 names.add(p.name);
46 ASSERT(Number.isInteger(p.version) && p.version >= 1, p.name);
47 for (const depend of p.depends ?? []) {
48 if (depend === p.name) throw new Error(`${p.name} depends on itself`);
49 if (!processors.some((o) => o.name === depend)) {
50 throw new Error(`${p.name} depends on unknown '${depend}'`);
51 }
52 }
53 }
54}
55
56/**
57 * which processors apply to a file extension. extension must come from
58 * a media file's `extensionNonEmpty`, lowercased. deterministic across hosts:
59 * `enable` and the disable list do not affect applicability, only execution,
60 * so `media_files.pending` means the same thing everywhere.
61 */
62export function applicableFor(ext: string): Processor[] {
63 return processors.filter((p) => p.include ? p.include.has(ext) : !p.exclude?.has(ext));
64}
65
66const disabledList = new Set(
67 (process.env.CLOVER_PROCESSORS_DISABLE ?? "")
68 .split(",")
69 .map((x) => x.trim())
70 .filter(Boolean),
71);
72for (const name of disabledList) {
73 if (!processors.some((p) => p.name === name)) {
74 console.warn(`CLOVER_PROCESSORS_DISABLE: unknown processor '${name}'`);
75 }
76}
77
78/** false when the host cannot or should not execute this processor */
79export function canExecute(p: Processor): boolean {
80 return p.enable !== false && !disabledList.has(p.name);
81}
82
83export function byName(name: string): Processor | null {
84 return processors.find((p) => p.name === name) ?? null;
85}
86
87import type { MediaFile } from "#src/file-viewer/models/media-files.ts";
88import type * as progress from "@clo/lib/progress";
89import type * as fs from "node:fs";
90
91import type { Path } from "#sitegen/path";
92
93import { ASSERT } from "@clo/lib/assert";
94import { processors as compressProcessors } from "./processors/compress.ts";
95import { processors as mediaProcessors } from "./processors/media.ts";
96import { processors as textProcessors } from "./processors/text.ts";