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

@@ -75,6 +75,9 @@ mise exec bun -- bun run build # build frontend to dist/
mise exec bun -- bun start # production: Hono serves dist/ + API on :3000
```
## Forward-looking rules
- [Schema changes need migrations going forward](feedback_schema_migrations.md) — resurrect the try/catch ALTER TABLE pattern in `server/db/index.ts` whenever touching table columns
## Workflow rules
- **Always bump version** in `package.json` before committing/pushing. CalVer with dot-suffix per global AGENTS.md (`YYYY.MM.DD.N`). `.gitea/workflows/build.yml` tags a Docker image with this version, so the `+N` form breaks CI with `invalid reference format`.

View File

@@ -1,6 +1,6 @@
{
"name": "netfelix-audio-fix",
"version": "2026.04.13.6",
"version": "2026.04.13.7",
"scripts": {
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
"dev:client": "vite",

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

View File

@@ -342,8 +342,6 @@ export function SettingsPage() {
const [data, setData] = useState<SettingsData | null>(settingsCache);
const [loading, setLoading] = useState(settingsCache === null);
const [clearStatus, setClearStatus] = useState("");
const [subLangs, setSubLangs] = useState<string[]>([]);
const [subSaved, setSubSaved] = useState("");
const [audLangs, setAudLangs] = useState<string[]>([]);
const [audSaved, setAudSaved] = useState("");
const langsLoadedRef = useRef(false);
@@ -356,7 +354,6 @@ export function SettingsPage() {
settingsCache = d;
setData(d);
if (!langsLoadedRef.current) {
setSubLangs(JSON.parse(d.config.subtitle_languages ?? '["eng","deu","spa"]'));
setAudLangs(JSON.parse(d.config.audio_languages ?? "[]"));
langsLoadedRef.current = true;
}
@@ -379,11 +376,6 @@ export function SettingsPage() {
const saveSonarr = (url: string, apiKey: string) =>
api.post<SaveResult>("/api/settings/sonarr", { url, api_key: apiKey });
const saveSubtitleLangs = async () => {
await api.post("/api/settings/subtitle-languages", { langs: subLangs });
setSubSaved("Saved.");
setTimeout(() => setSubSaved(""), 2000);
};
const saveAudioLangs = async () => {
await api.post("/api/settings/audio-languages", { langs: audLangs });
setAudSaved("Saved.");
@@ -481,20 +473,6 @@ export function SettingsPage() {
</div>
</SectionCard>
{/* Subtitle languages */}
<SectionCard
title="Subtitle Languages"
subtitle="Subtitle tracks in these languages are extracted to sidecar files. Order determines priority. All subtitles are removed from the container during processing."
>
<SortableLanguageList langs={subLangs} onChange={setSubLangs} disabled={locked.has("subtitle_languages")} />
<div className="flex items-center gap-2 mt-3">
<Button onClick={saveSubtitleLangs} disabled={locked.has("subtitle_languages")}>
Save
</Button>
{subSaved && <span className="text-green-700 text-sm">{subSaved}</span>}
</div>
</SectionCard>
{/* Danger zone */}
<div className="border border-red-400 rounded-lg p-4 mb-4">
<div className="font-semibold text-sm text-red-700 mb-1">Danger Zone</div>