drop the subtitle-languages setting, it never influenced extraction
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s

analyzer removes every subtitle unconditionally (see case 'Subtitle' in
decideAction) and the pipeline extracts all of them to sidecars — the config
was purely informational and only subtitles.ts echoed it back as
'keepLanguages' for a subtitle-manager ui that doesn't exist yet. we'll
revive language preferences inside that manager when it ships.

removes: the settings card + ui state, POST /api/settings/subtitle-languages,
the config default, the SUBTITLE_LANGUAGES env mapping, AnalyzerConfig's
subtitleLanguages field, RescanConfig's subtitleLanguages field, every
caller site (scan.ts / execute.ts / review.ts), and the keepLanguages
surface in subtitles.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 15:26:48 +02:00
parent a3fde7c441
commit 6d8a8fa6d6
13 changed files with 9 additions and 72 deletions

View File

@@ -71,7 +71,6 @@ async function refreshItemFromJellyfin(itemId: number): Promise<void> {
db,
fresh,
{
subtitleLanguages: parseLanguageList(cfg.subtitle_languages, ["eng", "deu", "spa"]),
audioLanguages: parseLanguageList(cfg.audio_languages, []),
radarr: radarrEnabled ? radarrCfg : null,
sonarr: sonarrEnabled ? sonarrCfg : null,

View File

@@ -10,10 +10,6 @@ const app = new Hono();
// ─── Helpers ──────────────────────────────────────────────────────────────────
function getSubtitleLanguages(): string[] {
return parseLanguageList(getConfig("subtitle_languages"), ["eng", "deu", "spa"]);
}
function getAudioLanguages(): string[] {
return parseLanguageList(getConfig("audio_languages"), []);
}
@@ -127,12 +123,11 @@ function reanalyze(db: ReturnType<typeof getDb>, itemId: number, preservedTitles
const streams = db
.prepare("SELECT * FROM media_streams WHERE item_id = ? ORDER BY stream_index")
.all(itemId) as MediaStream[];
const subtitleLanguages = getSubtitleLanguages();
const audioLanguages = getAudioLanguages();
const analysis = analyzeItem(
{ original_language: item.original_language, needs_review: item.needs_review, container: item.container },
streams,
{ subtitleLanguages, audioLanguages },
{ audioLanguages },
);
db

View File

@@ -150,7 +150,6 @@ async function runScan(limit: number | null = null): Promise<void> {
const cfg = getAllConfig();
const jellyfinCfg = { url: cfg.jellyfin_url, apiKey: cfg.jellyfin_api_key, userId: cfg.jellyfin_user_id };
const subtitleLanguages = parseLanguageList(cfg.subtitle_languages ?? null, ["eng", "deu", "spa"]);
const audioLanguages = parseLanguageList(cfg.audio_languages ?? null, []);
const radarrCfg = { url: cfg.radarr_url, apiKey: cfg.radarr_api_key };
const sonarrCfg = { url: cfg.sonarr_url, apiKey: cfg.sonarr_api_key };
@@ -183,7 +182,6 @@ async function runScan(limit: number | null = null): Promise<void> {
let total = 0;
const rescanCfg = {
subtitleLanguages,
audioLanguages,
radarr: radarrEnabled ? radarrCfg : null,
sonarr: sonarrEnabled ? sonarrCfg : null,

View File

@@ -83,14 +83,6 @@ app.post("/sonarr", async (c) => {
return c.json({ ok: result.ok, saved: true, testError: result.ok ? undefined : result.error });
});
app.post("/subtitle-languages", async (c) => {
const body = await c.req.json<{ langs: string[] }>();
if (body.langs?.length > 0) {
setConfig("subtitle_languages", JSON.stringify(body.langs));
}
return c.json({ ok: true });
});
app.post("/audio-languages", async (c) => {
const body = await c.req.json<{ langs: string[] }>();
setConfig("audio_languages", JSON.stringify(body.langs ?? []));

View File

@@ -1,7 +1,7 @@
import { unlinkSync } from "node:fs";
import { dirname, resolve as resolvePath, sep } from "node:path";
import { Hono } from "hono";
import { getAllConfig, getConfig, getDb } from "../db/index";
import { getAllConfig, getDb } from "../db/index";
import { error as logError } from "../lib/log";
import { parseId } from "../lib/validate";
import { getItem, mapStream, normalizeLanguage, refreshItem } from "../services/jellyfin";
@@ -288,16 +288,7 @@ app.get("/summary", (c) => {
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 });
return c.json({ embeddedCount, categories, titles });
});
// ─── Detail ──────────────────────────────────────────────────────────────────

View File

@@ -21,7 +21,6 @@ const ENV_MAP: Record<string, string> = {
sonarr_url: "SONARR_URL",
sonarr_api_key: "SONARR_API_KEY",
sonarr_enabled: "SONARR_ENABLED",
subtitle_languages: "SUBTITLE_LANGUAGES",
audio_languages: "AUDIO_LANGUAGES",
};
@@ -32,8 +31,7 @@ function envValue(key: string): string | null {
const val = process.env[envKey];
if (!val) return null;
if (key.endsWith("_enabled")) return val === "1" || val.toLowerCase() === "true" ? "1" : "0";
if (key === "subtitle_languages" || key === "audio_languages")
return JSON.stringify(val.split(",").map((s) => s.trim()));
if (key === "audio_languages") return JSON.stringify(val.split(",").map((s) => s.trim()));
if (key.endsWith("_url")) return val.replace(/\/$/, "");
return val;
}

View File

@@ -133,7 +133,6 @@ export const DEFAULT_CONFIG: Record<string, string> = {
sonarr_url: "",
sonarr_api_key: "",
sonarr_enabled: "0",
subtitle_languages: JSON.stringify(["eng", "deu", "spa"]),
audio_languages: "[]",
scan_running: "0",

View File

@@ -35,7 +35,6 @@ describe("analyzeItem — audio keep rules", () => {
stream({ id: 4, type: "Audio", stream_index: 3, codec: "aac", language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: ["deu"],
});
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
@@ -49,7 +48,6 @@ describe("analyzeItem — audio keep rules", () => {
stream({ id: 3, type: "Audio", stream_index: 2, language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: null, needs_review: 1 }, streams, {
subtitleLanguages: [],
audioLanguages: ["deu"],
});
expect(result.decisions.every((d) => d.action === "keep")).toBe(true);
@@ -62,7 +60,6 @@ describe("analyzeItem — audio keep rules", () => {
stream({ id: 2, type: "Audio", stream_index: 1, language: null }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
const byId = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
@@ -72,7 +69,6 @@ describe("analyzeItem — audio keep rules", () => {
test("normalizes language codes (ger → deu)", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, language: "ger" })];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "deu" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.decisions[0].action).toBe("keep");
@@ -87,7 +83,6 @@ describe("analyzeItem — audio ordering", () => {
stream({ id: 12, type: "Audio", stream_index: 2, codec: "aac", language: "spa" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: ["deu", "spa"],
});
const byId = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.target_index]));
@@ -103,7 +98,6 @@ describe("analyzeItem — audio ordering", () => {
stream({ id: 3, type: "Audio", stream_index: 2, language: "eng" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: ["deu"],
});
expect(result.is_noop).toBe(false);
@@ -116,7 +110,6 @@ describe("analyzeItem — audio ordering", () => {
stream({ id: 3, type: "Audio", stream_index: 2, codec: "aac", language: "deu" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: ["deu"],
});
expect(result.is_noop).toBe(true);
@@ -128,7 +121,6 @@ describe("analyzeItem — audio ordering", () => {
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
@@ -142,7 +134,6 @@ describe("analyzeItem — subtitles & is_noop", () => {
stream({ id: 2, type: "Subtitle", stream_index: 1, codec: "subrip", language: "eng" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: ["eng"],
audioLanguages: [],
});
const subDec = result.decisions.find((d) => d.stream_id === 2);
@@ -156,7 +147,6 @@ describe("analyzeItem — subtitles & is_noop", () => {
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.is_noop).toBe(true);
@@ -168,7 +158,6 @@ describe("analyzeItem — subtitles & is_noop", () => {
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 0 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
@@ -180,7 +169,6 @@ describe("analyzeItem — subtitles & is_noop", () => {
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "en", is_default: 1 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
@@ -191,7 +179,6 @@ describe("analyzeItem — transcode targets", () => {
test("DTS on mp4 → transcode to eac3", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "dts", language: "eng" })];
const result = analyzeItem({ original_language: "eng", needs_review: 0, container: "mp4" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.decisions[0].transcode_codec).toBe("eac3");
@@ -202,7 +189,6 @@ describe("analyzeItem — transcode targets", () => {
test("AAC passes through without transcode", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng" })];
const result = analyzeItem({ original_language: "eng", needs_review: 0, container: "mp4" }, streams, {
subtitleLanguages: [],
audioLanguages: [],
});
expect(result.decisions[0].transcode_codec).toBe(null);

View File

@@ -3,7 +3,6 @@ import { computeAppleCompat, transcodeTarget } from "./apple-compat";
import { normalizeLanguage } from "./jellyfin";
export interface AnalyzerConfig {
subtitleLanguages: string[];
audioLanguages: string[]; // additional languages to keep (after OG)
}

View File

@@ -6,7 +6,6 @@ import { type RadarrLibrary, getOriginalLanguage as radarrLang } from "./radarr"
import { type SonarrLibrary, getOriginalLanguage as sonarrLang } from "./sonarr";
export interface RescanConfig {
subtitleLanguages: string[];
audioLanguages: string[];
radarr: { url: string; apiKey: string } | null;
sonarr: { url: string; apiKey: string } | null;
@@ -217,7 +216,7 @@ export async function upsertJellyfinItem(
const analysis = analyzeItem(
{ original_language: origLang, needs_review: needsReview, container: jellyfinItem.Container ?? null },
streams,
{ subtitleLanguages: cfg.subtitleLanguages, audioLanguages: cfg.audioLanguages },
{ audioLanguages: cfg.audioLanguages },
);
db