fix: 05_Finalize_Move - atomare Sidecar-Erzeugung, 0-Byte-Pruefung, nur erfolgreich extrahierte Textspuren entfernen, Remux-Verifikation
This commit is contained in:
parent
6551dae21f
commit
ecabc8f996
1 changed files with 131 additions and 24 deletions
|
|
@ -26,8 +26,7 @@ function Script(SourceRoot, DestRoot)
|
||||||
|
|
||||||
Flow.Execute({ command: '/bin/mkdir', argumentList: ['-p', destDir] });
|
Flow.Execute({ command: '/bin/mkdir', argumentList: ['-p', destDir] });
|
||||||
|
|
||||||
// Wenn Ziel schon existiert: explizit loggen und ueberschreiben (typisch bei Re-Encode
|
// Wenn Ziel schon existiert: explizit loggen und ueberschreiben
|
||||||
// gleichen Films aus altem Run). Kein -n, sonst geht der frische Encode beim Cleanup verloren.
|
|
||||||
let testExist = Flow.Execute({ command: '/usr/bin/test', argumentList: ['-f', destAbs] });
|
let testExist = Flow.Execute({ command: '/usr/bin/test', argumentList: ['-f', destAbs] });
|
||||||
if (testExist && testExist.exitCode === 0) {
|
if (testExist && testExist.exitCode === 0) {
|
||||||
let oldSize = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', destAbs] });
|
let oldSize = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', destAbs] });
|
||||||
|
|
@ -50,7 +49,7 @@ function Script(SourceRoot, DestRoot)
|
||||||
Logger.ILog('Deleted original: ' + origPath);
|
Logger.ILog('Deleted original: ' + origPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
// =======================================================================
|
||||||
// UNTERTITEL-HANDLING (greift fuer ALLE Zweige: AV1, HEVC, reiner Copy)
|
// UNTERTITEL-HANDLING (greift fuer ALLE Zweige: AV1, HEVC, reiner Copy)
|
||||||
// Ziel: Emby-Auto-Burn-Transcode wegen Untertiteln verhindern.
|
// Ziel: Emby-Auto-Burn-Transcode wegen Untertiteln verhindern.
|
||||||
// - Text-Subs (srt/ass): als externe .srt-Sidecar rausziehen UND aus MKV entfernen.
|
// - Text-Subs (srt/ass): als externe .srt-Sidecar rausziehen UND aus MKV entfernen.
|
||||||
|
|
@ -60,7 +59,14 @@ function Script(SourceRoot, DestRoot)
|
||||||
// - Forced-Textspur -> eigene .forced.srt.
|
// - Forced-Textspur -> eigene .forced.srt.
|
||||||
// Remux (Container-only, -c copy) laeuft NUR wenn noetig (Text-Sub vorhanden oder
|
// Remux (Container-only, -c copy) laeuft NUR wenn noetig (Text-Sub vorhanden oder
|
||||||
// ein Bild-Sub traegt default/forced). Sonst wird das Video nicht angefasst.
|
// ein Bild-Sub traegt default/forced). Sonst wird das Video nicht angefasst.
|
||||||
// ═════════════════════════════════════════════════════════════════════════
|
//
|
||||||
|
// SICHERHEITS-FIX 17.07.2026:
|
||||||
|
// - Sidecar wird atomar erzeugt (.tmp-GUID, dann rename bei Erfolg + 0-Byte-Pruefung)
|
||||||
|
// - NUR erfolgreich exportierte Textspuren werden beim Remux entfernt
|
||||||
|
// - Fehlgeschlagene Textspuren bleiben eingebettet
|
||||||
|
// - Remux-Ergebnis wird per ffprobe verifiziert vor Ersetzen
|
||||||
|
// - Alle Temp-Dateien werden in Fehlerpfaden sicher aufgeraeumt
|
||||||
|
// =======================================================================
|
||||||
try {
|
try {
|
||||||
handleSubtitles(destAbs);
|
handleSubtitles(destAbs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -69,7 +75,7 @@ function Script(SourceRoot, DestRoot)
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
// ── Helfer ───────────────────────────────────────────────────────────────
|
// -- Helfer --------------------------------------------------------------
|
||||||
function handleSubtitles(file) {
|
function handleSubtitles(file) {
|
||||||
var ffprobe = Flow.GetToolPath('ffprobe') || Variables['ffprobe'] || 'ffprobe';
|
var ffprobe = Flow.GetToolPath('ffprobe') || Variables['ffprobe'] || 'ffprobe';
|
||||||
var ffmpeg = Flow.GetToolPath('ffmpeg') || Variables['ffmpeg'] || 'ffmpeg';
|
var ffmpeg = Flow.GetToolPath('ffmpeg') || Variables['ffmpeg'] || 'ffmpeg';
|
||||||
|
|
@ -91,7 +97,7 @@ function Script(SourceRoot, DestRoot)
|
||||||
var disp = s.disposition || {};
|
var disp = s.disposition || {};
|
||||||
var tags = s.tags || {};
|
var tags = s.tags || {};
|
||||||
subs.push({
|
subs.push({
|
||||||
sIdx: sidx, // relativer -map 0:s:N Index
|
sIdx: sidx,
|
||||||
codec: codec,
|
codec: codec,
|
||||||
isText: textCodecs.indexOf(codec) !== -1,
|
isText: textCodecs.indexOf(codec) !== -1,
|
||||||
lang: ('' + (tags.language || 'und')).toLowerCase(),
|
lang: ('' + (tags.language || 'und')).toLowerCase(),
|
||||||
|
|
@ -106,45 +112,102 @@ function Script(SourceRoot, DestRoot)
|
||||||
var textSubs = subs.filter(function (s) { return s.isText; });
|
var textSubs = subs.filter(function (s) { return s.isText; });
|
||||||
var imgSubs = subs.filter(function (s) { return !s.isText; });
|
var imgSubs = subs.filter(function (s) { return !s.isText; });
|
||||||
|
|
||||||
// 1) Text-Subs als .srt-Sidecar extrahieren (jeweils eigener ffmpeg-Call, Main unangetastet)
|
// 1) Text-Subs als .srt-Sidecar extrahieren
|
||||||
var base = file.replace(/\.[^./]+$/, ''); // Pfad ohne Extension
|
// ATOMAR: erst .tmp-GUID, dann rename bei Erfolg + Groessenpruefung
|
||||||
var seen = {}; // lang+forced -> Zaehler (Mehrfachspuren)
|
// Nur erfolgreiche werden spaeter aus dem MKV entfernt
|
||||||
|
var base = file.replace(/\.[^./]+$/, '');
|
||||||
|
var seen = {};
|
||||||
|
var successfullyExtracted = []; // sIdx-Werte der erfolgreich extrahierten Textspuren
|
||||||
|
|
||||||
textSubs.forEach(function (s) {
|
textSubs.forEach(function (s) {
|
||||||
var key = s.lang + (s.forced ? '.forced' : '');
|
var key = s.lang + (s.forced ? '.forced' : '');
|
||||||
var n = seen[key] || 0; seen[key] = n + 1;
|
var n = seen[key] || 0; seen[key] = n + 1;
|
||||||
var suffix = '.' + s.lang + (s.forced ? '.forced' : '') + (n > 0 ? '.' + n : '');
|
var suffix = '.' + s.lang + (s.forced ? '.forced' : '') + (n > 0 ? '.' + n : '');
|
||||||
var sidecar = base + suffix + '.srt';
|
var sidecar = base + suffix + '.srt';
|
||||||
|
var tmpSidecar = Flow.TempPath + '/' + Flow.NewGuid() + '.srt';
|
||||||
|
|
||||||
var ex = Flow.Execute({
|
var ex = Flow.Execute({
|
||||||
command: ffmpeg,
|
command: ffmpeg,
|
||||||
argumentList: ['-hide_banner', '-y', '-i', file,
|
argumentList: ['-hide_banner', '-y', '-i', file,
|
||||||
'-map', '0:s:' + s.sIdx, '-c:s', 'srt', sidecar]
|
'-map', '0:s:' + s.sIdx, '-c:s', 'srt', tmpSidecar]
|
||||||
});
|
});
|
||||||
if (ex && ex.exitCode === 0) Logger.ILog('Sub: Sidecar extrahiert -> ' + sidecar);
|
|
||||||
else Logger.WLog('Sub: Sidecar-Extraktion fehlgeschlagen (0:s:' + s.sIdx + ', ' + s.codec + ')');
|
// Pruefung: exit code 0, Datei existiert, Groesse > 0
|
||||||
|
var ok = false;
|
||||||
|
if (ex && ex.exitCode === 0) {
|
||||||
|
var stat = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', tmpSidecar] });
|
||||||
|
var sizeStr = (stat && stat.standardOutput) ? stat.standardOutput.trim() : '0';
|
||||||
|
var size = parseInt(sizeStr, 10) || 0;
|
||||||
|
if (size > 0) {
|
||||||
|
// Atomar umbenennen
|
||||||
|
var rn = Flow.Execute({ command: '/bin/mv', argumentList: ['-f', tmpSidecar, sidecar] });
|
||||||
|
if (rn && rn.exitCode === 0) {
|
||||||
|
ok = true;
|
||||||
|
Logger.ILog('Sub: Sidecar extrahiert -> ' + sidecar + ' (' + size + ' bytes)');
|
||||||
|
successfullyExtracted.push(s.sIdx);
|
||||||
|
} else {
|
||||||
|
Logger.WLog('Sub: Rename fehlgeschlagen fuer 0:s:' + s.sIdx + ' - Temp bleibt, Spur bleibt eingebettet');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmpSidecar] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.WLog('Sub: Sidecar 0 bytes (0:s:' + s.sIdx + ', ' + s.codec + ') - Spur bleibt eingebettet');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmpSidecar] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.WLog('Sub: Extraktion fehlgeschlagen (0:s:' + s.sIdx + ', ' + s.codec + ', exit=' + (ex ? ex.exitCode : '?') + ') - Spur bleibt eingebettet');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmpSidecar] });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2) Remux nur wenn noetig: Text-Subs raus ODER ein Bild-Sub traegt default/forced.
|
// 2) Remux nur wenn noetig:
|
||||||
|
// a) mindestens eine Textspur erfolgreich extrahiert -> diese aus dem MKV entfernen
|
||||||
|
// b) ein Bild-Sub traegt default/forced -> Disposition neutralisieren
|
||||||
|
// Fehlgeschlagene Textspuren bleiben IMMER eingebettet
|
||||||
var badImgDisp = imgSubs.some(function (s) { return s.isDefault || s.forced; });
|
var badImgDisp = imgSubs.some(function (s) { return s.isDefault || s.forced; });
|
||||||
if (textSubs.length === 0 && !badImgDisp) {
|
var needRemux = (successfullyExtracted.length > 0) || badImgDisp;
|
||||||
Logger.ILog('Sub: nur Bild-Subs ohne default/forced -> kein Remux noetig.');
|
|
||||||
|
if (!needRemux) {
|
||||||
|
Logger.ILog('Sub: kein Remux noetig (keine erfolgreichen Text-Extraktionen, keine Bild-Sub-Flags).');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map fuer zu entfernende Textspuren (nur erfolgreich extrahierte)
|
||||||
|
var removeTextIdx = successfullyExtracted;
|
||||||
|
|
||||||
|
// Build ffmpeg args: Video + Audio + Attachments 1:1, Bild-Subs behalten,
|
||||||
|
// fehlgeschlagene Text-Subs behalten, erfolgreiche Text-Subs weglassen
|
||||||
var tmp = Flow.TempPath + '/' + Flow.NewGuid() + '.mkv';
|
var tmp = Flow.TempPath + '/' + Flow.NewGuid() + '.mkv';
|
||||||
// Video + Audio + Attachments 1:1; Bild-Subs behalten (mit neutralisierter Disposition),
|
|
||||||
// Text-Subs weglassen.
|
|
||||||
var args = ['-hide_banner', '-y', '-i', file, '-map', '0:v?', '-map', '0:a?', '-map', '0:t?'];
|
var args = ['-hide_banner', '-y', '-i', file, '-map', '0:v?', '-map', '0:a?', '-map', '0:t?'];
|
||||||
|
|
||||||
|
// Bild-Subs behalten
|
||||||
var outSubPos = 0;
|
var outSubPos = 0;
|
||||||
imgSubs.forEach(function (s) { args.push('-map', '0:s:' + s.sIdx); });
|
var keepSubIdxs = [];
|
||||||
|
|
||||||
|
// Erfolgreich extrahierte Textspuren werden NICHT gemappt (also entfernt)
|
||||||
|
// Alle anderen (fehlgeschlagene Text + alle Bild) werden behalten
|
||||||
|
subs.forEach(function (s) {
|
||||||
|
if (removeTextIdx.indexOf(s.sIdx) !== -1) return; // skip = entfernen
|
||||||
|
keepSubIdxs.push(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
keepSubIdxs.forEach(function (s) {
|
||||||
|
args.push('-map', '0:s:' + s.sIdx);
|
||||||
|
});
|
||||||
|
|
||||||
args.push('-c', 'copy');
|
args.push('-c', 'copy');
|
||||||
// alle behaltenen Bild-Subs: default + forced weg (Emby brennt sonst automatisch ein)
|
|
||||||
imgSubs.forEach(function () {
|
// Bild-Sub Disposition neutralisieren
|
||||||
args.push('-disposition:s:' + outSubPos, '0');
|
keepSubIdxs.forEach(function (s) {
|
||||||
|
if (!s.isText) {
|
||||||
|
args.push('-disposition:s:' + outSubPos, '0');
|
||||||
|
}
|
||||||
outSubPos++;
|
outSubPos++;
|
||||||
});
|
});
|
||||||
|
|
||||||
args.push('-dn', '-max_muxing_queue_size', '9999', tmp);
|
args.push('-dn', '-max_muxing_queue_size', '9999', tmp);
|
||||||
|
|
||||||
Logger.ILog('Sub: Remux (Text raus: ' + textSubs.length + ', Bild behalten: ' + imgSubs.length +
|
Logger.ILog('Sub: Remux (Text raus: ' + removeTextIdx.length + ', Text behalten: ' +
|
||||||
|
(textSubs.length - removeTextIdx.length) + ', Bild behalten: ' + imgSubs.length +
|
||||||
', Flags neutralisiert). FFmpeg: ' + args.join(' '));
|
', Flags neutralisiert). FFmpeg: ' + args.join(' '));
|
||||||
var rm = Flow.Execute({ command: ffmpeg, argumentList: args });
|
var rm = Flow.Execute({ command: ffmpeg, argumentList: args });
|
||||||
if (!rm || rm.exitCode !== 0) {
|
if (!rm || rm.exitCode !== 0) {
|
||||||
|
|
@ -153,12 +216,56 @@ function Script(SourceRoot, DestRoot)
|
||||||
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verifikation: remuxte Datei mit ffprobe pruefen
|
||||||
|
var verify = Flow.Execute({
|
||||||
|
command: ffprobe,
|
||||||
|
argumentList: ['-v', 'quiet', '-print_format', 'json', '-show_streams', tmp]
|
||||||
|
});
|
||||||
|
if (!verify || verify.exitCode !== 0) {
|
||||||
|
Logger.WLog('Sub: Verifikation fehlgeschlagen (ffprobe) - Original bleibt.');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var vj;
|
||||||
|
try { vj = JSON.parse(verify.standardOutput || verify.output || ''); } catch (e) {
|
||||||
|
Logger.WLog('Sub: Verifikation JSON-Fehler - Original bleibt.');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pruefe: mindestens 1 Video-Stream und gleiche Audio-Trackzahl
|
||||||
|
var vVideo = (vj.streams || []).filter(function (s) { return s.codec_type === 'video'; });
|
||||||
|
var vAudio = (vj.streams || []).filter(function (s) { return s.codec_type === 'audio'; });
|
||||||
|
var oAudio = (j.streams || []).filter(function (s) { return s.codec_type === 'audio'; });
|
||||||
|
|
||||||
|
if (vVideo.length < 1) {
|
||||||
|
Logger.WLog('Sub: Verifikation - kein Video-Stream im Remux - Original bleibt.');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (vAudio.length !== oAudio.length) {
|
||||||
|
Logger.WLog('Sub: Verifikung - Audio-Trackzahl geaendert (' + oAudio.length + ' -> ' + vAudio.length + ') - Original bleibt.');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dateigroesse pruefen (nicht 0)
|
||||||
|
var sizeCheck = Flow.Execute({ command: '/usr/bin/stat', argumentList: ['-c', '%s', tmp] });
|
||||||
|
var tmpSize = parseInt((sizeCheck && sizeCheck.standardOutput) ? sizeCheck.standardOutput.trim() : '0', 10) || 0;
|
||||||
|
if (tmpSize < 1024) {
|
||||||
|
Logger.WLog('Sub: Verifikation - Remux-Datei zu klein (' + tmpSize + ' bytes) - Original bleibt.');
|
||||||
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erfolgreich: atomar ersetzen
|
||||||
var mv2 = Flow.Execute({ command: '/bin/mv', argumentList: ['-f', tmp, file] });
|
var mv2 = Flow.Execute({ command: '/bin/mv', argumentList: ['-f', tmp, file] });
|
||||||
if (!mv2 || mv2.exitCode !== 0) {
|
if (!mv2 || mv2.exitCode !== 0) {
|
||||||
Logger.WLog('Sub: Ersetzen der Datei fehlgeschlagen - Original bleibt.');
|
Logger.WLog('Sub: Ersetzen der Datei fehlgeschlagen - Original bleibt.');
|
||||||
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', tmp] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger.ILog('Sub: MKV neu gemuxt -> ' + file);
|
Logger.ILog('Sub: MKV neu gemuxt -> ' + file + ' (' + tmpSize + ' bytes, video=' + vVideo.length + ', audio=' + vAudio.length + ')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue