add pipeline API: approve-up-to, series language, pipeline summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user