@mieweb/pulsevault 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ // Make every already-uploaded video artifact in a local-storage workspace
3
+ // web-playable, in place. New uploads are handled by the `onUploadComplete`
4
+ // web-ready hook (see examples/fastify-demo/server.mjs); this script is the
5
+ // one-time backfill for artifacts that landed before the hook existed.
6
+ //
7
+ // For each ready `kind: "video"` sidecar in `<workspaceDir>/.pulsevault/`:
8
+ // - moov atom at the end of the file → lossless faststart remux (sub-second)
9
+ // - non-H.264 video codec (e.g. HEVC) → one-time libx264 transcode
10
+ // - already web-ready / not MP4 bytes → untouched
11
+ //
12
+ // Rewrites are atomic (tmp file + rename), so interrupting the script never
13
+ // corrupts an artifact — rerun it and it picks up where it left off (already
14
+ // fixed files report `none` and are skipped for free).
15
+ //
16
+ // Usage:
17
+ // node scripts/web-ready-migrate.mjs <workspaceDir> [--dry-run] [--no-transcode]
18
+ //
19
+ // Requires ffmpeg + ffprobe on PATH (apt install ffmpeg / brew install ffmpeg).
20
+
21
+ import fs from "node:fs/promises";
22
+ import path from "node:path";
23
+ import process from "node:process";
24
+ import { ensureWebReady, scanMoovPosition } from "../dist/app.js";
25
+
26
+ const args = process.argv.slice(2);
27
+ const dryRun = args.includes("--dry-run");
28
+ const transcode = !args.includes("--no-transcode");
29
+ const workspaceDir = args.find((a) => !a.startsWith("--"));
30
+
31
+ if (!workspaceDir) {
32
+ console.error(
33
+ "Usage: node scripts/web-ready-migrate.mjs <workspaceDir> [--dry-run] [--no-transcode]",
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ const root = path.resolve(workspaceDir);
39
+ const sidecarDir = path.join(root, ".pulsevault");
40
+
41
+ let sidecarFiles;
42
+ try {
43
+ sidecarFiles = (await fs.readdir(sidecarDir)).filter((f) => f.endsWith(".json"));
44
+ } catch {
45
+ console.error(`No .pulsevault directory in ${root} — is this a local-storage workspace?`);
46
+ process.exit(1);
47
+ }
48
+
49
+ const counts = { none: 0, remuxed: 0, transcoded: 0, skipped: 0, missing: 0 };
50
+ let processed = 0;
51
+
52
+ for (const sidecarFile of sidecarFiles) {
53
+ let sidecar;
54
+ try {
55
+ sidecar = JSON.parse(await fs.readFile(path.join(sidecarDir, sidecarFile), "utf8"));
56
+ } catch {
57
+ continue; // unreadable sidecar — not this script's problem
58
+ }
59
+ // Old sidecars predate `kind` and are videos by definition. Mirror the
60
+ // storage adapter's status semantics (src/storage/local.ts): anything that
61
+ // isn't explicitly "uploading" is ready — legacy sidecars predate `status`
62
+ // too, and those pre-hook artifacts are exactly what this script exists for.
63
+ const kind = sidecar.kind ?? "video";
64
+ const status = sidecar.status === "uploading" ? "uploading" : "ready";
65
+ if (kind !== "video" || status !== "ready") continue;
66
+
67
+ const artifactId = path.basename(sidecarFile, ".json");
68
+ const filePath = path.join(root, kind, `${artifactId}${sidecar.ext ?? ".mp4"}`);
69
+ processed += 1;
70
+
71
+ try {
72
+ await fs.access(filePath);
73
+ } catch {
74
+ counts.missing += 1;
75
+ console.warn(`missing bytes ${artifactId}`);
76
+ continue;
77
+ }
78
+
79
+ if (dryRun) {
80
+ // Report what WOULD happen: the moov scan is free; the codec check is
81
+ // ffprobe-cheap but we keep dry-run dependency-free and byte-only.
82
+ const moov = await scanMoovPosition(filePath);
83
+ console.log(`would inspect ${artifactId} (moov: ${moov})`);
84
+ continue;
85
+ }
86
+
87
+ const result = await ensureWebReady(filePath, { transcode, logger: console });
88
+ counts[result.action] += 1;
89
+ if (result.action !== "none") {
90
+ console.log(`${result.action.padEnd(11)} ${artifactId} (${result.reason})`);
91
+ }
92
+ }
93
+
94
+ if (dryRun) {
95
+ console.log(`\nDry run: inspected ${processed} ready video artifact(s); nothing was modified.`);
96
+ } else {
97
+ console.log(
98
+ `\nDone: ${processed} ready video artifact(s) — ` +
99
+ `${counts.remuxed} remuxed, ${counts.transcoded} transcoded, ` +
100
+ `${counts.none} already web-ready, ${counts.skipped} skipped, ${counts.missing} missing.`,
101
+ );
102
+ }