Skip to content

Commit 0eee68c

Browse files
Merge pull request #3077 from CapSoftware/feat/loom-importer
feat: dedicated Loom importer that copies videos and defers processing to first view
2 parents cb0e0bc + 3e8cd63 commit 0eee68c

25 files changed

Lines changed: 3315 additions & 30 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2+
import { mkdtemp, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { getJob } from "../../lib/job-manager";
6+
import {
7+
createLoomImporter,
8+
loadImporterSettings,
9+
} from "../../loom-importer/app";
10+
11+
describe("Loom importer API", () => {
12+
let root: string;
13+
let slowSource: ReturnType<typeof Bun.serve>;
14+
let release: () => void = () => {};
15+
const released = new Promise<void>((resolve) => {
16+
release = resolve;
17+
});
18+
19+
beforeAll(async () => {
20+
process.env.MEDIA_SERVER_WEBHOOK_SECRET = "importer-secret";
21+
root = await mkdtemp(join(tmpdir(), "loom-importer-app-"));
22+
slowSource = Bun.serve({
23+
port: 0,
24+
hostname: "127.0.0.1",
25+
async fetch() {
26+
await released;
27+
return new Response("#EXTM3U\n#EXT-X-ENDLIST");
28+
},
29+
});
30+
});
31+
32+
afterAll(async () => {
33+
release();
34+
slowSource?.stop(true);
35+
if (root) await rm(root, { recursive: true, force: true });
36+
});
37+
38+
function importer() {
39+
const config = loadImporterSettings({
40+
LOOM_IMPORTER_WORK_DIR: root,
41+
LOOM_IMPORTER_ALLOWED_HOSTS: "127.0.0.1",
42+
LOOM_IMPORTER_MAX_JOBS: "1",
43+
LOOM_IMPORTER_MAX_QUEUED: "1",
44+
LOOM_IMPORTER_MIN_FREE_GB: "1",
45+
});
46+
config.settings.minFreeBytes = 1;
47+
return createLoomImporter(config);
48+
}
49+
50+
const body = (videoId: string) =>
51+
JSON.stringify({
52+
videoId,
53+
userId: "user",
54+
videoUrl: `http://127.0.0.1:${slowSource.port}/master.m3u8`,
55+
outputPresignedUrl: "http://127.0.0.1:9/result.mp4",
56+
});
57+
58+
const post = (
59+
app: ReturnType<typeof importer>["app"],
60+
payload: string,
61+
secret = "importer-secret",
62+
) =>
63+
app.request("/loom-importer/video/import", {
64+
method: "POST",
65+
headers: {
66+
"Content-Type": "application/json",
67+
"x-media-server-secret": secret,
68+
},
69+
body: payload,
70+
});
71+
72+
test("requires the media server secret", async () => {
73+
const { app } = importer();
74+
expect((await post(app, body("a"), "wrong")).status).toBe(401);
75+
expect((await app.request("/loom-importer/video/status")).status).toBe(401);
76+
});
77+
78+
test("keeps waiting imports out of the shared job store and reports them as queued", async () => {
79+
const { app, stats } = importer();
80+
const first = (await (await post(app, body("first"))).json()) as {
81+
jobId: string;
82+
};
83+
const second = (await (await post(app, body("second"))).json()) as {
84+
jobId: string;
85+
};
86+
expect(stats()).toEqual({ running: 1, queued: 1 });
87+
expect(getJob(first.jobId)?.abortController).toBeDefined();
88+
expect(getJob(second.jobId)).toBeUndefined();
89+
const status = await app.request(
90+
`/loom-importer/video/process/${second.jobId}/status`,
91+
{ headers: { "x-media-server-secret": "importer-secret" } },
92+
);
93+
expect(await status.json()).toMatchObject({
94+
jobId: second.jobId,
95+
videoId: "second",
96+
phase: "queued",
97+
});
98+
const full = await post(app, body("third"));
99+
expect(full.status).toBe(503);
100+
expect(await full.json()).toMatchObject({ code: "SERVER_BUSY" });
101+
release();
102+
const deadline = Date.now() + 10_000;
103+
while (stats().running + stats().queued > 0 && Date.now() < deadline)
104+
await Bun.sleep(50);
105+
expect(getJob(first.jobId)?.phase).toBe("error");
106+
expect(getJob(second.jobId)?.phase).toBe("error");
107+
});
108+
109+
test("rejects malformed jobs", async () => {
110+
const { app } = importer();
111+
const response = await post(app, JSON.stringify({ videoId: "x" }));
112+
expect(response.status).toBe(400);
113+
});
114+
});

0 commit comments

Comments
 (0)