Files
ytplayer/plans/patches/009-media-cache.diff

121 lines
5.2 KiB
Diff

--- a/server/media-cache.js
+++ b/server/media-cache.js
@@ -24,6 +24,7 @@
statSync, statfsSync, unlinkSync,
} from 'node:fs';
import { join } from 'node:path';
+import { sha256File } from './hash.js';
export const HIGH = 0; // explicit save / Broken re-download
export const LOW = 1; // auto-cache after a play
@@ -167,6 +168,9 @@
freeBytes = () => { const s = statfsSync(dir); return s.bavail * s.bsize; },
now = () => Date.now(),
log = console,
+ hashFile = sha256File, // server-computed SHA-256 = the file's P2P content id
+ onReady = null, // ({ id, gen, path, sha256, size, … }) after a validated copy lands
+ backfillDelayMs = 30_000, // hash pre-existing copies this long after init()
} = {}) {
const tmpDir = join(dir, '.tmp');
const fileFor = (id, gen, kind = 'mp4') => join(dir, `${id}.${gen}.${kind}`);
@@ -236,6 +240,20 @@
}
}
+ // Tell the P2P layer a validated file with a server-computed hash exists.
+ // Never throws and never delays the caller.
+ function notifyReady(id, gen, sha256, probe, metaJson) {
+ if (!onReady || !sha256) return;
+ let meta = {};
+ try { meta = JSON.parse(metaJson || '{}'); } catch { /* keep {} */ }
+ Promise.resolve()
+ .then(() => onReady({
+ id, gen, path: fileFor(id, gen), sha256, size: probe.size, height: probe.height,
+ vcodec: probe.vcodec, acodec: probe.acodec, duration: probe.duration, meta,
+ }))
+ .catch((e) => log.warn?.(`[media] onReady ${id} failed: ${e.message}`));
+ }
+
async function setStatus(job, status) {
job.status = status;
await db.upsertMedia(job.id, { status, updated_at: now() });
@@ -308,6 +326,7 @@
'-map', '0:a:0', '-c', 'copy', '-movflags', '+faststart', '-f', 'mp4', m4a]);
if (ex.code !== 0 || sizeOf(m4a) < 1024) throw new Error('audio sidecar failed: ' + tail(ex.stderr));
+ const sha256 = await hashFile(norm);
const prev = await db.getMedia(id);
const gen = ((prev && prev.gen) || 0) + 1;
renameSync(norm, fileFor(id, gen));
@@ -318,13 +337,14 @@
size: probe.size + sizeOf(fileFor(id, gen, 'm4a')),
height: probe.height, vcodec: probe.vcodec, acodec: probe.acodec, duration: probe.duration,
optimized: 0, meta: JSON.stringify(metaFromInfo(info, probe.duration)),
- attempts: 0, error: null, retry_at: 0, updated_at: t, last_access: t,
+ attempts: 0, error: null, retry_at: 0, updated_at: t, last_access: t, sha256,
};
await db.upsertMedia(id, fields);
removeFiles(id, gen);
job.status = 'ready';
log.info?.(`[media] cached ${id} ${probe.height}p ${fmtMB(fields.size)}`);
enqueueOptimize(id);
+ notifyReady(id, gen, sha256, probe, fields.meta);
return { ...(prev || {}), video_id: id, ...fields };
} catch (err) {
job.status = 'failed';
@@ -427,6 +447,7 @@
log.info?.(`[media] kept original ${id}: ${probe.vcodec} ${fmtMB(probe.size)} vs ${fmtMB(oldSize)}`);
return;
}
+ const sha256 = await hashFile(out);
const gen = row.gen + 1;
renameSync(out, fileFor(id, gen));
const oldAudio = fileFor(id, row.gen, 'm4a');
@@ -434,8 +455,9 @@
try { linkSync(oldAudio, newAudio); } catch { copyFileSync(oldAudio, newAudio); }
await db.upsertMedia(id, {
gen, size: probe.size + sizeOf(newAudio), height: probe.height, vcodec: probe.vcodec,
- duration: probe.duration, optimized: OPT_REV, updated_at: now(),
+ duration: probe.duration, optimized: OPT_REV, updated_at: now(), sha256,
});
+ notifyReady(id, gen, sha256, probe, cur.meta);
removeFiles(id, gen, OLD_GEN_GRACE_MS);
log.info?.(`[media] optimized ${id}: ${fmtMB(oldSize)} → ${fmtMB(probe.size)} (${probe.vcodec})`);
} catch (err) {
@@ -622,7 +644,34 @@
}
const s = await db.mediaStats();
log.info?.(`[media] ${s.count} cached (${fmtMB(s.bytes)}), ${resumed} job(s) resumed, dir ${dir}`);
+ if (backfillDelayMs >= 0) {
+ const t = setTimeout(() => backfillHashes().catch(() => {}), backfillDelayMs);
+ t.unref?.();
+ }
+ }
+
+ // Copies cached before hashing existed get their sha256 in the background,
+ // one at a time, so their content ids can be admitted too.
+ async function backfillHashes() {
+ let n = 0;
+ for (const r of await db.listMedia()) {
+ if (r.status !== 'ready' || r.sha256) continue;
+ const p = fileFor(r.video_id, r.gen);
+ if (!existsSync(p)) continue;
+ try {
+ const sha256 = await hashFile(p);
+ const cur = await db.getMedia(r.video_id);
+ if (!cur || cur.status !== 'ready' || cur.gen !== r.gen) continue;
+ await db.upsertMedia(r.video_id, { sha256 });
+ notifyReady(r.video_id, r.gen, sha256,
+ { size: sizeOf(p), height: r.height, vcodec: r.vcodec, acodec: r.acodec, duration: r.duration }, r.meta);
+ n++;
+ } catch (e) {
+ log.warn?.(`[media] hash backfill ${r.video_id}: ${e.message}`);
+ }
+ }
+ return n;
}
- return { init, ensureCached, redownload, verify, getReady, filePath, touch, status, stats, isMediaId };
+ return { init, ensureCached, redownload, verify, getReady, filePath, touch, status, stats, isMediaId, backfillHashes };
}