openvibers/openvibe.media Commits

Opening the database no longer fails a clip being cut: getDb() ran the restart recovery ('processing' clips → failed) every time any process opened media.db, so a backfill or operator script run on the host while the service cut a clip failed that clip under it. The recovery stays where the server boots (vod/clip-jobs.start(), which also queues the re-cut); test/db-open-keeps-clips.test.js opens the database from a second process and checks the clip is untouched

OP OpenVibers committed 23 Sept 2026 · 6c2d384b8a · 2 files, +42 −18


Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex 9b0daf0..ee66808 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -32,27 +32,13 @@ function getDb() {
ensureObjectTriggers();
normalizeThumbnailUrls();
seedSettings();
-- recoverInterruptedClips();
++ // Clips a restart left 'processing' are marked failed (and re-cut) by vod/clip-jobs.start() when
++ // the SERVER boots, not here: every script that opens this database (backfills, reconcilers,
++ // operator tools) comes through getDb(), and running it while the service is cutting a clip
++ // used to fail that clip under it.
return database;
}
--/**
-- * A clip is marked 'processing' while ffmpeg cuts it. If the service restarts mid-cut
-- * that ffmpeg dies, and nothing ever moves the row on — it stays 'processing' forever,
-- * which the player renders as a permanent "the server is cutting your clip".
-- *
-- * Mark them failed on boot instead: failed is honest, visible, and re-cuttable, whereas
-- * 'processing' is a lie that no longer has a process behind it.
-- */
--function recoverInterruptedClips() {
-- try {
-- const r = database.prepare(
-- "UPDATE clips SET status = 'failed' WHERE status = 'processing'"
-- ).run();
-- if (r.changes) console.log(`[DB] Recovered ${r.changes} clip(s) left mid-cut by a restart → failed (re-cuttable)`);
-- } catch { /* non-fatal */ }
--}
--
// One-time normalization: migrated rows stored old-stack thumbnail URLs
// (/api/thumbnails/<name> or absolute variants). Canonical form is /t/<name>
// — the legacy redirect route covers stragglers, but direct URLs cache better.
ddiff --git a/test/db-open-keeps-clips.test.js b/test/db-open-keeps-clips.test.js
nnew file mode 100644
iindex 0000000..8b65389
---- /dev/null
++++ b/test/db-open-keeps-clips.test.js
@@@ -0,0 +1,38 @@
++'use strict';
++
++// Opening the database (what every script and the service do through getDb()) must not touch a
++// clip that is being cut: only the server's own boot (vod/clip-jobs.start()) recovers clips a
++// restart left 'processing'. A backfill run on the host used to fail the clip being cut under it.
++
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++
++const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-dbopen-'));
++const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
++process.env.DB_PATH = path.join(tmp, 'media.db');
++for (const [k, n] of [['VOD_PATH', 'vods'], ['CLIPS_PATH', 'clips'], ['FILES_PATH', 'files'], ['THUMBNAILS_PATH', 'thumbnails'], ['PASTES_PATH', 'pastes'], ['OBJECTS_PATH', 'objects']]) process.env[k] = dir(n);
++process.env.MEDIA_PUBLIC_URL = 'https://media.test';
++console.log = () => {};
++
++const Database = require('better-sqlite3');
++const db = require('../server/db/database');
++db.getDb();
++db.run("INSERT INTO clips (id, app_id, vod_id, title, status) VALUES (41, 'live', 1, 'being cut', 'processing')");
++
++// A second process opening the same file (a script on the host) goes through getDb() too.
++const other = require('child_process').spawnSync(process.execPath, ['-e', `
++ process.env.DB_PATH = ${JSON.stringify(process.env.DB_PATH)};
++ ${['VOD_PATH', 'CLIPS_PATH', 'FILES_PATH', 'THUMBNAILS_PATH', 'PASTES_PATH', 'OBJECTS_PATH', 'MEDIA_PUBLIC_URL'].map(k => `process.env.${k} = ${JSON.stringify(process.env[k])};`).join('\n')}
++ console.log = () => {};
++ require(${JSON.stringify(path.join(__dirname, '..', 'server', 'db', 'database'))}).getDb();
++`], { encoding: 'utf8' });
++assert.strictEqual(other.status, 0, other.stderr);
++
++const raw = new Database(process.env.DB_PATH, { readonly: true });
++assert.strictEqual(raw.prepare('SELECT status FROM clips WHERE id = 41').get().status, 'processing', 'opening the database leaves a clip being cut alone');
++raw.close();
++
++fs.rmSync(tmp, { recursive: true, force: true });
++process.stdout.write('db open keeps clips: all checks passed\n');