fix subtitle summary 404 by moving /summary route before /:id catch-all, bump version
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m8s
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m8s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "netfelix-audio-fix",
|
||||
"version": "2026.03.05.7",
|
||||
"version": "2026.03.05.8",
|
||||
"scripts": {
|
||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||
"dev:client": "vite",
|
||||
|
||||
@@ -154,6 +154,93 @@ app.get('/series/:seriesKey/episodes', (c) => {
|
||||
return c.json({ seasons });
|
||||
});
|
||||
|
||||
// ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CategoryRow { language: string | null; is_forced: number; is_hearing_impaired: number; cnt: number }
|
||||
|
||||
function variantOf(row: { is_forced: number; is_hearing_impaired: number }): 'forced' | 'cc' | 'standard' {
|
||||
if (row.is_forced) return 'forced';
|
||||
if (row.is_hearing_impaired) return 'cc';
|
||||
return 'standard';
|
||||
}
|
||||
|
||||
function catKey(lang: string | null, variant: string) { return `${lang ?? '__null__'}|${variant}`; }
|
||||
|
||||
app.get('/summary', (c) => {
|
||||
const db = getDb();
|
||||
|
||||
// Embedded count — items with subtitle streams where subs_extracted = 0
|
||||
const embeddedCount = (db.prepare(`
|
||||
SELECT COUNT(DISTINCT mi.id) as n FROM media_items mi
|
||||
JOIN media_streams ms ON ms.item_id = mi.id AND ms.type = 'Subtitle'
|
||||
LEFT JOIN review_plans rp ON rp.item_id = mi.id
|
||||
WHERE COALESCE(rp.subs_extracted, 0) = 0
|
||||
`).get() as { n: number }).n;
|
||||
|
||||
// Stream counts by (language, variant)
|
||||
const streamRows = db.prepare(`
|
||||
SELECT language, is_forced, is_hearing_impaired, COUNT(*) as cnt
|
||||
FROM media_streams WHERE type = 'Subtitle'
|
||||
GROUP BY language, is_forced, is_hearing_impaired
|
||||
`).all() as CategoryRow[];
|
||||
|
||||
// File counts by (language, variant)
|
||||
const fileRows = db.prepare(`
|
||||
SELECT language, is_forced, is_hearing_impaired, COUNT(*) as cnt
|
||||
FROM subtitle_files
|
||||
GROUP BY language, is_forced, is_hearing_impaired
|
||||
`).all() as CategoryRow[];
|
||||
|
||||
// Merge into categories
|
||||
const catMap = new Map<string, { language: string | null; variant: string; streamCount: number; fileCount: number }>();
|
||||
for (const r of streamRows) {
|
||||
const v = variantOf(r);
|
||||
const k = catKey(r.language, v);
|
||||
catMap.set(k, { language: r.language, variant: v, streamCount: r.cnt, fileCount: 0 });
|
||||
}
|
||||
for (const r of fileRows) {
|
||||
const v = variantOf(r);
|
||||
const k = catKey(r.language, v);
|
||||
const existing = catMap.get(k);
|
||||
if (existing) { existing.fileCount = r.cnt; }
|
||||
else { catMap.set(k, { language: r.language, variant: v, streamCount: 0, fileCount: r.cnt }); }
|
||||
}
|
||||
const categories = Array.from(catMap.values()).sort((a, b) => {
|
||||
const la = a.language ?? 'zzz';
|
||||
const lb = b.language ?? 'zzz';
|
||||
if (la !== lb) return la.localeCompare(lb);
|
||||
return a.variant.localeCompare(b.variant);
|
||||
});
|
||||
|
||||
// Title grouping
|
||||
const titleRows = db.prepare(`
|
||||
SELECT language, title, COUNT(*) as cnt
|
||||
FROM media_streams WHERE type = 'Subtitle'
|
||||
GROUP BY language, title
|
||||
ORDER BY language, cnt DESC
|
||||
`).all() as { language: string | null; title: string | null; cnt: number }[];
|
||||
|
||||
// Determine canonical title per language (most common)
|
||||
const canonicalByLang = new Map<string | null, string | null>();
|
||||
for (const r of titleRows) {
|
||||
if (!canonicalByLang.has(r.language)) canonicalByLang.set(r.language, r.title);
|
||||
}
|
||||
|
||||
const titles = titleRows.map((r) => ({
|
||||
language: r.language,
|
||||
title: r.title,
|
||||
count: r.cnt,
|
||||
isCanonical: canonicalByLang.get(r.language) === r.title,
|
||||
}));
|
||||
|
||||
// Keep languages from config
|
||||
const raw = getConfig('subtitle_languages');
|
||||
let keepLanguages: string[] = [];
|
||||
try { keepLanguages = JSON.parse(raw ?? '[]'); } catch { /* empty */ }
|
||||
|
||||
return c.json({ embeddedCount, categories, titles, keepLanguages });
|
||||
});
|
||||
|
||||
// ─── Detail ──────────────────────────────────────────────────────────────────
|
||||
|
||||
app.get('/:id', (c) => {
|
||||
@@ -295,93 +382,6 @@ app.post('/:id/rescan', async (c) => {
|
||||
return c.json(detail);
|
||||
});
|
||||
|
||||
// ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CategoryRow { language: string | null; is_forced: number; is_hearing_impaired: number; cnt: number }
|
||||
|
||||
function variantOf(row: { is_forced: number; is_hearing_impaired: number }): 'forced' | 'cc' | 'standard' {
|
||||
if (row.is_forced) return 'forced';
|
||||
if (row.is_hearing_impaired) return 'cc';
|
||||
return 'standard';
|
||||
}
|
||||
|
||||
function catKey(lang: string | null, variant: string) { return `${lang ?? '__null__'}|${variant}`; }
|
||||
|
||||
app.get('/summary', (c) => {
|
||||
const db = getDb();
|
||||
|
||||
// Embedded count — items with subtitle streams where subs_extracted = 0
|
||||
const embeddedCount = (db.prepare(`
|
||||
SELECT COUNT(DISTINCT mi.id) as n FROM media_items mi
|
||||
JOIN media_streams ms ON ms.item_id = mi.id AND ms.type = 'Subtitle'
|
||||
LEFT JOIN review_plans rp ON rp.item_id = mi.id
|
||||
WHERE COALESCE(rp.subs_extracted, 0) = 0
|
||||
`).get() as { n: number }).n;
|
||||
|
||||
// Stream counts by (language, variant)
|
||||
const streamRows = db.prepare(`
|
||||
SELECT language, is_forced, is_hearing_impaired, COUNT(*) as cnt
|
||||
FROM media_streams WHERE type = 'Subtitle'
|
||||
GROUP BY language, is_forced, is_hearing_impaired
|
||||
`).all() as CategoryRow[];
|
||||
|
||||
// File counts by (language, variant)
|
||||
const fileRows = db.prepare(`
|
||||
SELECT language, is_forced, is_hearing_impaired, COUNT(*) as cnt
|
||||
FROM subtitle_files
|
||||
GROUP BY language, is_forced, is_hearing_impaired
|
||||
`).all() as CategoryRow[];
|
||||
|
||||
// Merge into categories
|
||||
const catMap = new Map<string, { language: string | null; variant: string; streamCount: number; fileCount: number }>();
|
||||
for (const r of streamRows) {
|
||||
const v = variantOf(r);
|
||||
const k = catKey(r.language, v);
|
||||
catMap.set(k, { language: r.language, variant: v, streamCount: r.cnt, fileCount: 0 });
|
||||
}
|
||||
for (const r of fileRows) {
|
||||
const v = variantOf(r);
|
||||
const k = catKey(r.language, v);
|
||||
const existing = catMap.get(k);
|
||||
if (existing) { existing.fileCount = r.cnt; }
|
||||
else { catMap.set(k, { language: r.language, variant: v, streamCount: 0, fileCount: r.cnt }); }
|
||||
}
|
||||
const categories = Array.from(catMap.values()).sort((a, b) => {
|
||||
const la = a.language ?? 'zzz';
|
||||
const lb = b.language ?? 'zzz';
|
||||
if (la !== lb) return la.localeCompare(lb);
|
||||
return a.variant.localeCompare(b.variant);
|
||||
});
|
||||
|
||||
// Title grouping
|
||||
const titleRows = db.prepare(`
|
||||
SELECT language, title, COUNT(*) as cnt
|
||||
FROM media_streams WHERE type = 'Subtitle'
|
||||
GROUP BY language, title
|
||||
ORDER BY language, cnt DESC
|
||||
`).all() as { language: string | null; title: string | null; cnt: number }[];
|
||||
|
||||
// Determine canonical title per language (most common)
|
||||
const canonicalByLang = new Map<string | null, string | null>();
|
||||
for (const r of titleRows) {
|
||||
if (!canonicalByLang.has(r.language)) canonicalByLang.set(r.language, r.title);
|
||||
}
|
||||
|
||||
const titles = titleRows.map((r) => ({
|
||||
language: r.language,
|
||||
title: r.title,
|
||||
count: r.cnt,
|
||||
isCanonical: canonicalByLang.get(r.language) === r.title,
|
||||
}));
|
||||
|
||||
// Keep languages from config
|
||||
const raw = getConfig('subtitle_languages');
|
||||
let keepLanguages: string[] = [];
|
||||
try { keepLanguages = JSON.parse(raw ?? '[]'); } catch { /* empty */ }
|
||||
|
||||
return c.json({ embeddedCount, categories, titles, keepLanguages });
|
||||
});
|
||||
|
||||
// ─── Batch delete subtitle files ─────────────────────────────────────────────
|
||||
|
||||
app.post('/batch-delete', async (c) => {
|
||||
|
||||
Reference in New Issue
Block a user