apply codex code review: fix useEffect refetch loops, dead routes, subtitle job_type leftovers
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s

All ack'd as real bugs:

frontend
- AudioDetailPage / SubtitleDetailPage / PathsPage / ScanPage /
  SubtitleListPage / ExecutePage: load() was a fresh function reference
  every render, so 'useEffect(() => load(), [load])' refetched on every
  render. Wrap each in useCallback with the right deps ([id], [filter],
  or []).
- SetupPage: langsLoaded was useState; setting it inside load() retriggered
  the same effect → infinite loop. Switch to useRef. Also wrap saveJellyfin/
  Radarr/Sonarr in async fns so they return Promise<void> (matches the
  consumer signatures, fixes the latent TS error).
- DashboardPage: redirect target /setup doesn't exist; the route is
  /settings.
- ExecutePage: <>...</> fragment with two <tr> children had keys on the
  rows but not on the fragment → React reconciliation warning. Use
  <Fragment key>. jobTypeLabel + badge variant still branched on the
  removed 'subtitle' job_type — relabel to 'Audio Transcode' / 'Audio
  Remux' and use 'manual'/'noop' variants.

server
- review.ts + scan.ts: parseLanguageList helper catches JSON errors and
  enforces array-of-strings shape with a fallback. A corrupted config
  row would otherwise throw mid-scan.
This commit is contained in:
2026-04-13 12:01:57 +02:00
parent cafb3852a1
commit 1aafcb4972
10 changed files with 107 additions and 68 deletions

View File

@@ -11,7 +11,21 @@ const app = new Hono();
// ─── Helpers ──────────────────────────────────────────────────────────────────
function getSubtitleLanguages(): string[] {
return JSON.parse(getConfig("subtitle_languages") ?? '["eng","deu","spa"]');
return parseLanguageList(getConfig("subtitle_languages"), ["eng", "deu", "spa"]);
}
function getAudioLanguages(): string[] {
return parseLanguageList(getConfig("audio_languages"), []);
}
function parseLanguageList(raw: string | null, fallback: string[]): string[] {
if (!raw) return fallback;
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : fallback;
} catch {
return fallback;
}
}
function countsByFilter(db: ReturnType<typeof getDb>): Record<string, number> {
@@ -114,7 +128,7 @@ function reanalyze(db: ReturnType<typeof getDb>, itemId: number, preservedTitles
.prepare("SELECT * FROM media_streams WHERE item_id = ? ORDER BY stream_index")
.all(itemId) as MediaStream[];
const subtitleLanguages = getSubtitleLanguages();
const audioLanguages: string[] = JSON.parse(getConfig("audio_languages") ?? "[]");
const audioLanguages = getAudioLanguages();
const analysis = analyzeItem(
{ original_language: item.original_language, needs_review: item.needs_review, container: item.container },
streams,
@@ -186,7 +200,7 @@ function recomputePlanAfterToggle(db: ReturnType<typeof getDb>, itemId: number):
}[];
const origLang = item.original_language ? normalizeLanguage(item.original_language) : null;
const audioLanguages: string[] = JSON.parse(getConfig("audio_languages") ?? "[]");
const audioLanguages = getAudioLanguages();
// Re-assign target_index based on current actions
const decWithIdx = decisions.map((d) => ({