mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Ingest-time thumbnail generation is best-effort by contract, so a row that misses it stays bare forever: video uploads on a host without ffmpeg (a SYSTEM dependency nothing surfaced), or content from before thumbnails existed. Operators read that as "thumbnails don't work". Two additions. A [MEDIA] startup diagnostic (async probe, cached) states loudly whether ffmpeg/ffprobe were found, mirroring the [EMAIL] block. And a once-per-boot sweep re-derives metadata for local image/video rows with no thumbnail — serial, paced, delayed past boot, unref'd. The sweep's row UPDATE re-checks that thumbnail_path is still empty so it never clobbers a thumbnail written concurrently by the replace flow, removes its just-written file when the row vanished mid-derive, salvages probed dims/duration even when the thumbnail itself failed, and stops after 25 failures per boot so a library of undecodable clips can't turn every restart into subprocess churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0131RYmVh8ePEhparD3mXBhU
33 lines
1.1 KiB
JavaScript
33 lines
1.1 KiB
JavaScript
'use strict';
|
|
|
|
// Availability probe for the external media binaries (ffmpeg/ffprobe) that video
|
|
// thumbnail + duration extraction depends on. They are SYSTEM dependencies, not npm
|
|
// ones, so a deployment can easily lack them — and content-ingest's best-effort
|
|
// contract means every video then uploads fine but silently gets no thumbnail and
|
|
// no duration. Probed once and cached: the answer can't change without an operator
|
|
// installing packages, which comes with a restart anyway.
|
|
//
|
|
// Async on purpose: the first caller is server.js right after listen, and a hung
|
|
// binary (NFS-mounted PATH shim, broken wrapper) must degrade to a late log line,
|
|
// not block request serving on a freshly-bound port.
|
|
|
|
const { execFile } = require('child_process');
|
|
|
|
let cached = null;
|
|
|
|
function probeTool(bin) {
|
|
return new Promise((resolve) => {
|
|
execFile(bin, ['-version'], { timeout: 5000 }, (err) => resolve(!err));
|
|
});
|
|
}
|
|
|
|
function mediaToolStatus() {
|
|
if (!cached) {
|
|
cached = Promise.all([probeTool('ffmpeg'), probeTool('ffprobe')])
|
|
.then(([ffmpeg, ffprobe]) => ({ ffmpeg, ffprobe }));
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
module.exports = { mediaToolStatus };
|