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
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:
@@ -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",
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user