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