diff --git a/server/api/review.ts b/server/api/review.ts index 83257df..e53f289 100644 --- a/server/api/review.ts +++ b/server/api/review.ts @@ -379,4 +379,126 @@ app.post('/:id/rescan', async (c) => { return c.json(detail); }); +// ─── Pipeline: approve up to here ──────────────────────────────────────────── + +app.post('/approve-up-to/:id', (c) => { + const targetId = Number(c.req.param('id')); + const db = getDb(); + + const target = db.prepare('SELECT id FROM review_plans WHERE id = ?').get(targetId) as { id: number } | undefined; + if (!target) return c.json({ error: 'Plan not found' }, 404); + + // Get all pending plans sorted by confidence (high first), then name + const pendingPlans = db.prepare(` + SELECT rp.id + FROM review_plans rp + JOIN media_items mi ON mi.id = rp.item_id + WHERE rp.status = 'pending' AND rp.is_noop = 0 + ORDER BY + CASE rp.confidence WHEN 'high' THEN 0 ELSE 1 END, + COALESCE(mi.series_name, mi.name), + mi.season_number, + mi.episode_number, + mi.name + `).all() as { id: number }[]; + + // Find the target and approve everything up to and including it + const toApprove: number[] = []; + for (const plan of pendingPlans) { + toApprove.push(plan.id); + if (plan.id === targetId) break; + } + + // Batch approve and create jobs + for (const planId of toApprove) { + db.prepare("UPDATE review_plans SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?").run(planId); + const planRow = db.prepare('SELECT item_id, job_type FROM review_plans WHERE id = ?').get(planId) as { item_id: number; job_type: string }; + const detail = loadItemDetail(db, planRow.item_id); + if (detail.item && detail.command) { + db.prepare("INSERT INTO jobs (item_id, command, job_type, status) VALUES (?, ?, ?, 'pending')") + .run(planRow.item_id, detail.command, planRow.job_type); + } + } + + return c.json({ approved: toApprove.length }); +}); + +// ─── Pipeline: series language ─────────────────────────────────────────────── + +app.patch('/series/:seriesKey/language', async (c) => { + const seriesKey = decodeURIComponent(c.req.param('seriesKey')); + const { language } = await c.req.json<{ language: string }>(); + const db = getDb(); + + const items = db.prepare( + 'SELECT id FROM media_items WHERE series_jellyfin_id = ? OR (series_jellyfin_id IS NULL AND series_name = ?)' + ).all(seriesKey, seriesKey) as { id: number }[]; + + const normalizedLang = language ? normalizeLanguage(language) : null; + for (const item of items) { + db.prepare("UPDATE media_items SET original_language = ?, orig_lang_source = 'manual', needs_review = 0 WHERE id = ?") + .run(normalizedLang, item.id); + } + + // Re-analyze all episodes + for (const item of items) { + reanalyze(db, item.id); + } + + return c.json({ updated: items.length }); +}); + +// ─── Pipeline: summary ─────────────────────────────────────────────────────── + +app.get('/pipeline', (c) => { + const db = getDb(); + + const review = db.prepare(` + SELECT rp.*, mi.name, mi.series_name, mi.series_jellyfin_id, + mi.season_number, mi.episode_number, mi.type, mi.container, + mi.original_language, mi.orig_lang_source, mi.file_path + FROM review_plans rp + JOIN media_items mi ON mi.id = rp.item_id + WHERE rp.status = 'pending' AND rp.is_noop = 0 + ORDER BY + CASE rp.confidence WHEN 'high' THEN 0 ELSE 1 END, + COALESCE(mi.series_name, mi.name), + mi.season_number, mi.episode_number + `).all(); + + const queued = db.prepare(` + SELECT j.*, mi.name, mi.series_name, mi.type, + rp.job_type, rp.apple_compat + FROM jobs j + JOIN media_items mi ON mi.id = j.item_id + JOIN review_plans rp ON rp.item_id = j.item_id + WHERE j.status = 'pending' + ORDER BY j.created_at + `).all(); + + const processing = db.prepare(` + SELECT j.*, mi.name, mi.series_name, mi.type, + rp.job_type, rp.apple_compat + FROM jobs j + JOIN media_items mi ON mi.id = j.item_id + JOIN review_plans rp ON rp.item_id = j.item_id + WHERE j.status = 'running' + `).all(); + + const done = db.prepare(` + SELECT j.*, mi.name, mi.series_name, mi.type, + rp.job_type, rp.apple_compat + FROM jobs j + JOIN media_items mi ON mi.id = j.item_id + JOIN review_plans rp ON rp.item_id = j.item_id + WHERE j.status IN ('done', 'error') + ORDER BY j.completed_at DESC + LIMIT 50 + `).all(); + + const noops = db.prepare('SELECT COUNT(*) as count FROM review_plans WHERE is_noop = 1').get() as { count: number }; + + return c.json({ review, queued, processing, done, noopCount: noops.count }); +}); + export default app;