From b10b68582d4014e3f2808634c29db8807c2c41bd Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Jun 2026 10:08:16 +1000 Subject: [PATCH] feat(api): capture routes YouTube/Vimeo URLs to ingest.video POST /api/capture with a youtube.com / youtu.be / vimeo.com URL enqueues ingest.video (Python worker) instead of ingest.url (Node worker). Detection by URL hostname; idempotency_key + response shape unchanged. Co-Authored-By: Claude Opus 4.7 --- lib/api/routes/capture.js | 10 +++++++++- tests/api/capture.test.js | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/api/routes/capture.js b/lib/api/routes/capture.js index 2ac6796..fa1abc5 100644 --- a/lib/api/routes/capture.js +++ b/lib/api/routes/capture.js @@ -29,6 +29,13 @@ function key(space_id, url) { return crypto.createHash('sha256').update(space_id + '\x00' + url).digest('hex'); } +const VIDEO_HOST_RE = /(^|\.)(youtube\.com|youtu\.be|vimeo\.com)$/i; + +function isVideoUrl(url) { + try { return VIDEO_HOST_RE.test(new URL(url).hostname); } + catch { return false; } +} + export const router = Router(); router.post('/', @@ -46,7 +53,8 @@ router.post('/', job_id: null, idempotency_key: idem, ref_id: existing.id }); } - const job_id = await queue.enqueue('ingest.url', { space_id, url }); + const job_name = isVideoUrl(url) ? 'ingest.video' : 'ingest.url'; + const job_id = await queue.enqueue(job_name, { space_id, url }); res.status(202).json({ job_id, idempotency_key: idem }); }) ); diff --git a/tests/api/capture.test.js b/tests/api/capture.test.js index a38ecb0..89b131d 100644 --- a/tests/api/capture.test.js +++ b/tests/api/capture.test.js @@ -69,4 +69,25 @@ describe('capture api', () => { .send({ space_id: sp.id, url: 'https://example.com/a' }); expect(res.status).toBe(401); }); + + it('POST /api/capture with YouTube URL enqueues ingest.video', async () => { + const res = await request(app).post('/api/capture').set(ownerHeaders) + .send({ space_id: sp.id, url: 'https://youtu.be/abc' }); + expect(res.status).toBe(202); + expect(res.body.job_id).toBeTruthy(); + const { default: jobsRepo } = { default: await import('../../lib/db/repos/jobs.js') }; + const rows = await jobsRepo.list({ name: 'ingest.video' }); + expect(rows.find(r => r.id === res.body.job_id)).toBeTruthy(); + const urlRows = await jobsRepo.list({ name: 'ingest.url' }); + expect(urlRows.find(r => r.id === res.body.job_id)).toBeFalsy(); + }); + + it('POST /api/capture with vimeo URL enqueues ingest.video', async () => { + const res = await request(app).post('/api/capture').set(ownerHeaders) + .send({ space_id: sp.id, url: 'https://vimeo.com/123' }); + expect(res.status).toBe(202); + const { default: jobsRepo } = { default: await import('../../lib/db/repos/jobs.js') }; + const rows = await jobsRepo.list({ name: 'ingest.video' }); + expect(rows.find(r => r.id === res.body.job_id)).toBeTruthy(); + }); });