remove subtitle streams from container after extraction, remove job list limit, fix audio detail display
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m9s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 09:17:39 +01:00
parent a393dd280e
commit 588a3d8f1f
4 changed files with 43 additions and 10 deletions

View File

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

View File

@@ -82,7 +82,7 @@ app.get('/', (c) => {
FROM jobs j
LEFT JOIN media_items mi ON mi.id = j.item_id
LEFT JOIN nodes n ON n.id = j.node_id
ORDER BY j.created_at DESC LIMIT 200
ORDER BY j.created_at DESC
`).all() as (Job & { name: string; type: string; series_name: string | null; season_number: number | null; episode_number: number | null; file_path: string; node_name: string | null; host: string | null; port: number | null; username: string | null; private_key: string | null; ffmpeg_path: string | null; work_dir: string | null; node_status: string | null; })[];
const jobs = jobRows.map((r) => ({

View File

@@ -321,9 +321,12 @@ export function buildMkvConvertCommand(
}
/**
* Build a command that ONLY extracts subtitles to sidecar files
* without modifying the container. Useful when the item is otherwise
* a noop but the user wants sidecar subtitle files.
* Build a command that extracts subtitles to sidecar files AND
* remuxes the container without subtitle streams (single ffmpeg pass).
*
* ffmpeg supports multiple outputs: first we extract each subtitle
* track to its own sidecar file, then the final output copies all
* video + audio streams into a temp file without subtitles.
*/
export function buildExtractOnlyCommand(
item: MediaItem,
@@ -332,7 +335,26 @@ export function buildExtractOnlyCommand(
const basePath = item.file_path.replace(/\.[^.]+$/, '');
const extractionOutputs = buildExtractionOutputs(streams, basePath);
if (extractionOutputs.length === 0) return null;
return ['ffmpeg', '-y', '-i', shellQuote(item.file_path), ...extractionOutputs].join(' ');
const inputPath = item.file_path;
const ext = inputPath.match(/\.([^.]+)$/)?.[1] ?? 'mkv';
const tmpPath = inputPath.replace(/\.[^.]+$/, `.tmp.${ext}`);
// Single ffmpeg pass: extract sidecar files + remux without subtitles
const parts: string[] = [
'ffmpeg', '-y',
'-i', shellQuote(inputPath),
// Subtitle extraction outputs (each to its own file)
...extractionOutputs,
// Final output: copy all video + audio, no subtitles
'-map 0:v', '-map 0:a',
'-c copy',
shellQuote(tmpPath),
'&&',
'mv', shellQuote(tmpPath), shellQuote(inputPath),
];
return parts.join(' ');
}
/** Safely quote a path for shell usage. */

View File

@@ -41,7 +41,8 @@ const STREAM_SECTIONS = [
const TYPE_ORDER: Record<string, number> = { Video: 0, Audio: 1, Subtitle: 2, Data: 3, EmbeddedImage: 4 };
function computeOutIdx(streams: MediaStream[], decisions: StreamDecision[]): Map<number, number> {
/** Compute per-type output indices for kept streams (e.g. a:0, a:1). */
function computeOutIdx(streams: MediaStream[], decisions: StreamDecision[]): Map<number, string> {
const mappedKept = streams
.filter((s) => ['Video', 'Audio'].includes(s.type))
.filter((s) => {
@@ -56,8 +57,14 @@ function computeOutIdx(streams: MediaStream[], decisions: StreamDecision[]): Map
const db = decisions.find((d) => d.stream_id === b.id);
return (da?.target_index ?? 0) - (db?.target_index ?? 0);
});
const m = new Map<number, number>();
mappedKept.forEach((s, i) => m.set(s.id, i));
const m = new Map<number, string>();
const typeCounts: Record<string, number> = {};
for (const s of mappedKept) {
const prefix = s.type === 'Video' ? 'v' : 'a';
const idx = typeCounts[s.type] ?? 0;
m.set(s.id, `${prefix}:${idx}`);
typeCounts[s.type] = idx + 1;
}
return m;
}
@@ -115,7 +122,11 @@ function StreamTable({ data, onUpdate }: StreamTableProps) {
</td>
<td className="py-1.5 px-2 border-b border-gray-100 font-mono text-xs">{s.codec ?? '—'}</td>
<td className="py-1.5 px-2 border-b border-gray-100">
{lang} {s.language ? <span className="text-gray-400 font-mono text-xs">({s.language})</span> : null}
{isAudio ? (
<>{lang} {s.language ? <span className="text-gray-400 font-mono text-xs">({s.language})</span> : null}</>
) : (
<span className="text-gray-400"></span>
)}
</td>
<td className="py-1.5 px-2 border-b border-gray-100">
{isEditable ? (