0.12.0-4 DVD Plugin
This commit is contained in:
@@ -1094,20 +1094,20 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
{
|
||||
key: 'use_plugin_architecture_dvd_review',
|
||||
label: 'Review / Mediainfo-Prüfung (DVD)',
|
||||
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
|
||||
defaultValue: 'false',
|
||||
description: 'Review-/Track-Analyse läuft über das DVD-Plugin.',
|
||||
defaultValue: 'true',
|
||||
orderIndex: 10013,
|
||||
dependsOn: 'use_plugin_architecture_dvd',
|
||||
validationJson: '{"readonly":true}'
|
||||
validationJson: '{}'
|
||||
},
|
||||
{
|
||||
key: 'use_plugin_architecture_dvd_encode',
|
||||
label: 'Encoding (DVD)',
|
||||
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
|
||||
defaultValue: 'false',
|
||||
description: 'Encoding läuft über das DVD-Plugin.',
|
||||
defaultValue: 'true',
|
||||
orderIndex: 10014,
|
||||
dependsOn: 'use_plugin_architecture_dvd',
|
||||
validationJson: '{"readonly":true}'
|
||||
validationJson: '{}'
|
||||
},
|
||||
// ── Audio-CD ─────────────────────────────────────────────────────────────
|
||||
{
|
||||
|
||||
@@ -147,12 +147,30 @@ class VideoDiscPlugin extends SourcePlugin {
|
||||
});
|
||||
|
||||
const scanLines = [];
|
||||
const runInfo = await _runCommandCaptured({
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
onStdoutLine: (line) => scanLines.push(line),
|
||||
context: { jobId: job?.id, source: `${this.id}Plugin.review` }
|
||||
});
|
||||
const runCommandFn = typeof ctx.extra?.runCommand === 'function' ? ctx.extra.runCommand : null;
|
||||
const reviewStage = String(ctx.extra?.reviewStage || 'MEDIAINFO_CHECK').trim().toUpperCase() || 'MEDIAINFO_CHECK';
|
||||
const reviewSource = String(ctx.extra?.reviewSource || 'HANDBRAKE_SCAN').trim().toUpperCase() || 'HANDBRAKE_SCAN';
|
||||
const reviewSilent = Boolean(ctx.extra?.reviewSilent);
|
||||
let runInfo;
|
||||
if (runCommandFn) {
|
||||
runInfo = await runCommandFn({
|
||||
jobId: job?.id,
|
||||
stage: reviewStage,
|
||||
source: reviewSource,
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
collectLines: scanLines,
|
||||
collectStderrLines: false,
|
||||
silent: reviewSilent
|
||||
});
|
||||
} else {
|
||||
runInfo = await _runCommandCaptured({
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
onStdoutLine: (line) => scanLines.push(line),
|
||||
context: { jobId: job?.id, source: `${this.id}Plugin.review` }
|
||||
});
|
||||
}
|
||||
|
||||
ctx.logger.info(`${this.id}:review:scan-done`, {
|
||||
jobId: job?.id,
|
||||
@@ -315,16 +333,35 @@ class VideoDiscPlugin extends SourcePlugin {
|
||||
|
||||
ctx.emitProgress(0, `${this.name}: Encoding läuft …`);
|
||||
|
||||
const runInfo = await _spawnAndWait({
|
||||
cmd: handBrakeConfig.cmd,
|
||||
args: handBrakeConfig.args,
|
||||
jobId: job?.id,
|
||||
progressParser: parseHandBrakeProgress,
|
||||
onProgress: (pct, statusText) => ctx.emitProgress(Math.round(pct), statusText || `${this.name}: Encoding ${pct}%`),
|
||||
isCancelled,
|
||||
onProcessHandle,
|
||||
context: { jobId: job?.id, source: `${this.id}Plugin.encode`, stage: 'ENCODING' }
|
||||
});
|
||||
const runCommandFn = typeof ctx.extra?.runCommand === 'function' ? ctx.extra.runCommand : null;
|
||||
const encodeSource = String(ctx.extra?.encodeSource || 'HANDBRAKE').trim().toUpperCase() || 'HANDBRAKE';
|
||||
const encodeStage = String(ctx.extra?.encodeStage || 'ENCODING').trim().toUpperCase() || 'ENCODING';
|
||||
const encodeParser = typeof ctx.extra?.progressParser === 'function'
|
||||
? ctx.extra.progressParser
|
||||
: parseHandBrakeProgress;
|
||||
|
||||
let runInfo;
|
||||
if (runCommandFn) {
|
||||
runInfo = await runCommandFn({
|
||||
jobId: job?.id,
|
||||
stage: encodeStage,
|
||||
source: encodeSource,
|
||||
cmd: handBrakeConfig.cmd,
|
||||
args: handBrakeConfig.args,
|
||||
parser: encodeParser
|
||||
});
|
||||
} else {
|
||||
runInfo = await _spawnAndWait({
|
||||
cmd: handBrakeConfig.cmd,
|
||||
args: handBrakeConfig.args,
|
||||
jobId: job?.id,
|
||||
progressParser: parseHandBrakeProgress,
|
||||
onProgress: (pct, statusText) => ctx.emitProgress(Math.round(pct), statusText || `${this.name}: Encoding ${pct}%`),
|
||||
isCancelled,
|
||||
onProcessHandle,
|
||||
context: { jobId: job?.id, source: `${this.id}Plugin.encode`, stage: 'ENCODING' }
|
||||
});
|
||||
}
|
||||
|
||||
ctx.logger.info(`${this.id}:encode:done`, {
|
||||
jobId: job?.id,
|
||||
|
||||
@@ -4296,6 +4296,42 @@ class PipelineService extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
async runPluginReviewScan(plugin, job, {
|
||||
jobId,
|
||||
rawPath = null,
|
||||
deviceInfo = null,
|
||||
reviewMode = 'rip',
|
||||
source = 'HANDBRAKE_SCAN',
|
||||
silent = false
|
||||
} = {}) {
|
||||
if (!plugin || typeof plugin.review !== 'function') {
|
||||
const error = new Error('Plugin-Review nicht verfügbar.');
|
||||
error.statusCode = 500;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const pluginCtx = await this.buildPluginContext(plugin.id, {
|
||||
jobId,
|
||||
rawJobDir: rawPath || null,
|
||||
deviceInfo: deviceInfo || null,
|
||||
reviewMode: String(reviewMode || 'rip').trim().toLowerCase() || 'rip',
|
||||
runCommand: this.runCommand.bind(this),
|
||||
reviewSource: String(source || 'HANDBRAKE_SCAN').trim().toUpperCase() || 'HANDBRAKE_SCAN',
|
||||
reviewStage: 'MEDIAINFO_CHECK',
|
||||
reviewSilent: Boolean(silent)
|
||||
});
|
||||
|
||||
const pluginResult = await plugin.review(job, pluginCtx);
|
||||
return {
|
||||
scanLines: Array.isArray(pluginResult?.scanLines) ? pluginResult.scanLines : [],
|
||||
runInfo: pluginResult?.runInfo && typeof pluginResult.runInfo === 'object'
|
||||
? pluginResult.runInfo
|
||||
: null,
|
||||
sourceArg: String(pluginResult?.sourceArg || '').trim() || null,
|
||||
pluginExecution: this.sanitizePluginExecutionState(pluginCtx.getPluginExecution())
|
||||
};
|
||||
}
|
||||
|
||||
isRipSuccessful(job = null) {
|
||||
if (Number(job?.rip_successful || 0) === 1) {
|
||||
return true;
|
||||
@@ -7247,6 +7283,13 @@ class PipelineService extends EventEmitter {
|
||||
makemkvInfo: mkInfo
|
||||
});
|
||||
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
||||
const reviewPlugin = await this.resolveAnalyzePlugin({ mediaProfile }, 'review').catch(() => null);
|
||||
const reviewPluginEnabled = Boolean(
|
||||
reviewPlugin
|
||||
&& reviewPlugin.id === mediaProfile
|
||||
&& typeof reviewPlugin.review === 'function'
|
||||
);
|
||||
let reviewPluginExecution = null;
|
||||
const analyzeContext = mkInfo?.analyzeContext || {};
|
||||
const playlistAnalysis = analyzeContext.playlistAnalysis || this.snapshot.context?.playlistAnalysis || null;
|
||||
const selectedPlaylistId = normalizePlaylistId(
|
||||
@@ -7295,24 +7338,44 @@ class PipelineService extends EventEmitter {
|
||||
});
|
||||
|
||||
const lines = [];
|
||||
const scanConfig = await settingsService.buildHandBrakeScanConfig(deviceInfo, { mediaProfile });
|
||||
logger.info('disc-track-review:command', {
|
||||
jobId,
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
sourceArg: scanConfig.sourceArg,
|
||||
selectedTitleId: selectedMakemkvTitleId
|
||||
});
|
||||
let runInfo = null;
|
||||
let sourceArg = null;
|
||||
if (reviewPluginEnabled) {
|
||||
const pluginScan = await this.runPluginReviewScan(reviewPlugin, job, {
|
||||
jobId,
|
||||
deviceInfo,
|
||||
reviewMode: 'pre_rip',
|
||||
source: 'HANDBRAKE_SCAN',
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
lines.push(...pluginScan.scanLines);
|
||||
runInfo = pluginScan.runInfo || null;
|
||||
sourceArg = pluginScan.sourceArg || null;
|
||||
reviewPluginExecution = this.mergePluginExecutionState(
|
||||
reviewPluginExecution,
|
||||
pluginScan.pluginExecution
|
||||
);
|
||||
} else {
|
||||
const scanConfig = await settingsService.buildHandBrakeScanConfig(deviceInfo, { mediaProfile });
|
||||
logger.info('disc-track-review:command', {
|
||||
jobId,
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
sourceArg: scanConfig.sourceArg,
|
||||
selectedTitleId: selectedMakemkvTitleId
|
||||
});
|
||||
|
||||
const runInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN',
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
collectLines: lines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
runInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN',
|
||||
cmd: scanConfig.cmd,
|
||||
args: scanConfig.args,
|
||||
collectLines: lines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
sourceArg = scanConfig.sourceArg;
|
||||
}
|
||||
|
||||
const parsed = parseMediainfoJsonOutput(lines.join('\n'));
|
||||
if (!parsed) {
|
||||
@@ -7328,7 +7391,7 @@ class PipelineService extends EventEmitter {
|
||||
selectedPlaylistId,
|
||||
selectedMakemkvTitleId,
|
||||
mediaProfile,
|
||||
sourceArg: scanConfig.sourceArg
|
||||
sourceArg
|
||||
});
|
||||
|
||||
if (!Array.isArray(review.titles) || review.titles.length === 0) {
|
||||
@@ -7337,15 +7400,16 @@ class PipelineService extends EventEmitter {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const persistedMediainfoInfo = this.withPluginExecutionMeta({
|
||||
generatedAt: nowIso(),
|
||||
source: 'disc_scan',
|
||||
runInfo
|
||||
}, reviewPluginExecution);
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'READY_TO_ENCODE',
|
||||
last_state: 'READY_TO_ENCODE',
|
||||
error_message: null,
|
||||
mediainfo_info_json: JSON.stringify({
|
||||
generatedAt: nowIso(),
|
||||
source: 'disc_scan',
|
||||
runInfo
|
||||
}),
|
||||
mediainfo_info_json: JSON.stringify(persistedMediainfoInfo),
|
||||
encode_plan_json: JSON.stringify(review),
|
||||
encode_input_path: review.encodeInputPath || null,
|
||||
encode_review_confirmed: 0
|
||||
@@ -7373,7 +7437,8 @@ class PipelineService extends EventEmitter {
|
||||
mode: 'pre_rip',
|
||||
mediaProfile,
|
||||
mediaInfoReview: review,
|
||||
selectedMetadata
|
||||
selectedMetadata,
|
||||
...(reviewPluginExecution ? { pluginExecution: this.mergePluginExecutionState(mkInfo?.pluginExecution, reviewPluginExecution) } : {})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7467,6 +7532,13 @@ class PipelineService extends EventEmitter {
|
||||
makemkvInfo: mkInfo
|
||||
});
|
||||
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
||||
const reviewPlugin = await this.resolveAnalyzePlugin({ mediaProfile }, 'review').catch(() => null);
|
||||
const reviewPluginEnabled = Boolean(
|
||||
reviewPlugin
|
||||
&& reviewPlugin.id === mediaProfile
|
||||
&& typeof reviewPlugin.review === 'function'
|
||||
);
|
||||
let reviewPluginExecution = null;
|
||||
const analyzeContext = mkInfo?.analyzeContext || {};
|
||||
let playlistAnalysis = forceFreshAnalyze
|
||||
? null
|
||||
@@ -7627,24 +7699,39 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
try {
|
||||
const resolveScanLines = [];
|
||||
const resolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(rawPath, { mediaProfile });
|
||||
logger.info('backup-track-review:handbrake-predecision-command', {
|
||||
jobId,
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
sourceArg: resolveScanConfig.sourceArg,
|
||||
candidatePlaylists: playlistCandidates.map((item) => item.playlistFile || item.playlistId)
|
||||
});
|
||||
if (reviewPluginEnabled) {
|
||||
const pluginScan = await this.runPluginReviewScan(reviewPlugin, job, {
|
||||
jobId,
|
||||
rawPath,
|
||||
reviewMode: 'rip',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
resolveScanLines.push(...pluginScan.scanLines);
|
||||
reviewPluginExecution = this.mergePluginExecutionState(
|
||||
reviewPluginExecution,
|
||||
pluginScan.pluginExecution
|
||||
);
|
||||
} else {
|
||||
const resolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(rawPath, { mediaProfile });
|
||||
logger.info('backup-track-review:handbrake-predecision-command', {
|
||||
jobId,
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
sourceArg: resolveScanConfig.sourceArg,
|
||||
candidatePlaylists: playlistCandidates.map((item) => item.playlistFile || item.playlistId)
|
||||
});
|
||||
|
||||
await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
collectLines: resolveScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
collectLines: resolveScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
}
|
||||
|
||||
const resolveScanJson = parseMediainfoJsonOutput(resolveScanLines.join('\n'));
|
||||
if (resolveScanJson) {
|
||||
@@ -7666,6 +7753,11 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (reviewPluginEnabled) {
|
||||
const error = new Error('HandBrake Playlist-Trackdaten konnten nicht geparst werden (Plugin-Review aktiv, kein Fallback).');
|
||||
error.runInfo = null;
|
||||
throw error;
|
||||
}
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
@@ -7682,6 +7774,9 @@ class PipelineService extends EventEmitter {
|
||||
'SYSTEM',
|
||||
`HandBrake Voranalyse für Playlist-Auswahl fehlgeschlagen: ${error.message}`
|
||||
);
|
||||
if (reviewPluginEnabled) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
playlistAnalysis = enrichPlaylistAnalysisWithHandBrakeCache(playlistAnalysis, handBrakePlaylistScan);
|
||||
@@ -7694,7 +7789,7 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMakemkvInfo = {
|
||||
const updatedMakemkvInfoBase = {
|
||||
...mkInfo,
|
||||
analyzeContext: {
|
||||
...(mkInfo?.analyzeContext || {}),
|
||||
@@ -7720,6 +7815,10 @@ class PipelineService extends EventEmitter {
|
||||
error: null
|
||||
}
|
||||
};
|
||||
const buildUpdatedMakemkvInfo = () => this.withPluginExecutionMeta(updatedMakemkvInfoBase, reviewPluginExecution);
|
||||
const getMergedReviewPluginExecution = () => this.sanitizePluginExecutionState(
|
||||
buildUpdatedMakemkvInfo()?.pluginExecution
|
||||
);
|
||||
|
||||
if (playlistDecisionRequired && !selectedPlaylistId) {
|
||||
const evaluated = Array.isArray(playlistAnalysis?.evaluatedCandidates)
|
||||
@@ -7727,17 +7826,18 @@ class PipelineService extends EventEmitter {
|
||||
: [];
|
||||
const recommendationFile = toPlaylistFile(playlistAnalysis?.recommendation?.playlistId);
|
||||
const decisionContext = describePlaylistManualDecision(playlistAnalysis);
|
||||
const waitingMediainfoInfo = this.withPluginExecutionMeta({
|
||||
generatedAt: nowIso(),
|
||||
source: 'makemkv_backup_robot',
|
||||
runInfo: makeMkvAnalyzeRunInfo
|
||||
}, reviewPluginExecution);
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'WAITING_FOR_USER_DECISION',
|
||||
last_state: 'WAITING_FOR_USER_DECISION',
|
||||
error_message: null,
|
||||
makemkv_info_json: JSON.stringify(updatedMakemkvInfo),
|
||||
mediainfo_info_json: JSON.stringify({
|
||||
generatedAt: nowIso(),
|
||||
source: 'makemkv_backup_robot',
|
||||
runInfo: makeMkvAnalyzeRunInfo
|
||||
}),
|
||||
makemkv_info_json: JSON.stringify(buildUpdatedMakemkvInfo()),
|
||||
mediainfo_info_json: JSON.stringify(waitingMediainfoInfo),
|
||||
encode_plan_json: null,
|
||||
encode_input_path: null,
|
||||
encode_review_confirmed: 0
|
||||
@@ -7764,6 +7864,7 @@ class PipelineService extends EventEmitter {
|
||||
`Status=awaiting_playlist_selection${recommendationFile ? ` | Empfehlung=${recommendationFile}` : ''}`
|
||||
);
|
||||
|
||||
const mergedPluginExecutionForWaiting = getMergedReviewPluginExecution();
|
||||
await this.setState('WAITING_FOR_USER_DECISION', {
|
||||
activeJobId: jobId,
|
||||
progress: 0,
|
||||
@@ -7787,7 +7888,8 @@ class PipelineService extends EventEmitter {
|
||||
selectedTitleId: null,
|
||||
waitingForManualPlaylistSelection: true,
|
||||
manualDecisionState: 'awaiting_playlist_selection',
|
||||
mediaInfoReview: null
|
||||
mediaInfoReview: null,
|
||||
...(mergedPluginExecutionForWaiting ? { pluginExecution: mergedPluginExecutionForWaiting } : {})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7849,9 +7951,9 @@ class PipelineService extends EventEmitter {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (updatedMakemkvInfo && updatedMakemkvInfo.analyzeContext) {
|
||||
updatedMakemkvInfo.analyzeContext.selectedTitleId = selectedTitleForReview;
|
||||
updatedMakemkvInfo.analyzeContext.selectedPlaylist = resolvedPlaylistId;
|
||||
if (updatedMakemkvInfoBase && updatedMakemkvInfoBase.analyzeContext) {
|
||||
updatedMakemkvInfoBase.analyzeContext.selectedTitleId = selectedTitleForReview;
|
||||
updatedMakemkvInfoBase.analyzeContext.selectedPlaylist = resolvedPlaylistId;
|
||||
}
|
||||
|
||||
const cachedHandBrakePlaylistEntry = getCachedHandBrakePlaylistEntry(handBrakePlaylistScan, resolvedPlaylistId);
|
||||
@@ -7922,26 +8024,42 @@ class PipelineService extends EventEmitter {
|
||||
} else {
|
||||
try {
|
||||
const resolveScanLines = [];
|
||||
const resolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(rawPath, { mediaProfile });
|
||||
logger.info('backup-track-review:handbrake-resolve-command', {
|
||||
jobId,
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
sourceArg: resolveScanConfig.sourceArg,
|
||||
selectedPlaylistId: resolvedPlaylistId,
|
||||
selectedMakemkvTitleId: selectedTitleForReview
|
||||
});
|
||||
if (reviewPluginEnabled) {
|
||||
const pluginScan = await this.runPluginReviewScan(reviewPlugin, job, {
|
||||
jobId,
|
||||
rawPath,
|
||||
reviewMode: 'rip',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
resolveScanLines.push(...pluginScan.scanLines);
|
||||
handBrakeResolveRunInfo = pluginScan.runInfo || null;
|
||||
reviewPluginExecution = this.mergePluginExecutionState(
|
||||
reviewPluginExecution,
|
||||
pluginScan.pluginExecution
|
||||
);
|
||||
} else {
|
||||
const resolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(rawPath, { mediaProfile });
|
||||
logger.info('backup-track-review:handbrake-resolve-command', {
|
||||
jobId,
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
sourceArg: resolveScanConfig.sourceArg,
|
||||
selectedPlaylistId: resolvedPlaylistId,
|
||||
selectedMakemkvTitleId: selectedTitleForReview
|
||||
});
|
||||
|
||||
handBrakeResolveRunInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
collectLines: resolveScanLines,
|
||||
collectStderrLines: false,
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
handBrakeResolveRunInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_PLAYLIST_MAP',
|
||||
cmd: resolveScanConfig.cmd,
|
||||
args: resolveScanConfig.args,
|
||||
collectLines: resolveScanLines,
|
||||
collectStderrLines: false,
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
}
|
||||
|
||||
const resolveScanJson = parseMediainfoJsonOutput(resolveScanLines.join('\n'));
|
||||
if (!resolveScanJson) {
|
||||
@@ -8029,8 +8147,8 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedMakemkvInfo && updatedMakemkvInfo.analyzeContext) {
|
||||
updatedMakemkvInfo.analyzeContext.handBrakePlaylistScan = handBrakePlaylistScan || null;
|
||||
if (updatedMakemkvInfoBase && updatedMakemkvInfoBase.analyzeContext) {
|
||||
updatedMakemkvInfoBase.analyzeContext.handBrakePlaylistScan = handBrakePlaylistScan || null;
|
||||
}
|
||||
playlistCandidates = buildPlaylistCandidates(playlistAnalysis);
|
||||
|
||||
@@ -8168,20 +8286,22 @@ class PipelineService extends EventEmitter {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const persistedMediainfoInfo = this.withPluginExecutionMeta({
|
||||
generatedAt: nowIso(),
|
||||
source: 'raw_backup_handbrake_playlist_scan',
|
||||
makemkvAnalyzeRunInfo: makeMkvAnalyzeRunInfo,
|
||||
makemkvTitleAnalyzeRunInfo: null,
|
||||
handbrakePlaylistResolveRunInfo: handBrakeResolveRunInfo,
|
||||
handbrakeTitleRunInfo: handBrakeTitleRunInfo,
|
||||
handbrakeTitleId: resolvedHandBrakeTitleId || null
|
||||
}, reviewPluginExecution);
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'READY_TO_ENCODE',
|
||||
last_state: 'READY_TO_ENCODE',
|
||||
error_message: null,
|
||||
makemkv_info_json: JSON.stringify(updatedMakemkvInfo),
|
||||
mediainfo_info_json: JSON.stringify({
|
||||
generatedAt: nowIso(),
|
||||
source: 'raw_backup_handbrake_playlist_scan',
|
||||
makemkvAnalyzeRunInfo: makeMkvAnalyzeRunInfo,
|
||||
makemkvTitleAnalyzeRunInfo: null,
|
||||
handbrakePlaylistResolveRunInfo: handBrakeResolveRunInfo,
|
||||
handbrakeTitleRunInfo: handBrakeTitleRunInfo,
|
||||
handbrakeTitleId: resolvedHandBrakeTitleId || null
|
||||
}),
|
||||
makemkv_info_json: JSON.stringify(buildUpdatedMakemkvInfo()),
|
||||
mediainfo_info_json: JSON.stringify(persistedMediainfoInfo),
|
||||
encode_plan_json: JSON.stringify(review),
|
||||
encode_input_path: review.encodeInputPath || null,
|
||||
encode_review_confirmed: 0
|
||||
@@ -8222,6 +8342,7 @@ class PipelineService extends EventEmitter {
|
||||
|
||||
const hasEncodableTitle = Boolean(review.encodeInputPath && review.encodeInputTitleId);
|
||||
if (this.isPrimaryJob(jobId)) {
|
||||
const mergedPluginExecutionForReady = getMergedReviewPluginExecution();
|
||||
await this.setState('READY_TO_ENCODE', {
|
||||
activeJobId: jobId,
|
||||
progress: 0,
|
||||
@@ -8247,7 +8368,8 @@ class PipelineService extends EventEmitter {
|
||||
playlistDecisionRequired,
|
||||
playlistCandidates,
|
||||
selectedPlaylist: resolvedPlaylistId || null,
|
||||
selectedTitleId: selectedTitleForReview
|
||||
selectedTitleId: selectedTitleForReview,
|
||||
...(mergedPluginExecutionForReady ? { pluginExecution: mergedPluginExecutionForReady } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9744,6 +9866,13 @@ class PipelineService extends EventEmitter {
|
||||
rawPath
|
||||
});
|
||||
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
||||
const reviewPlugin = await this.resolveAnalyzePlugin({ mediaProfile }, 'review').catch(() => null);
|
||||
const reviewPluginEnabled = Boolean(
|
||||
reviewPlugin
|
||||
&& reviewPlugin.id === mediaProfile
|
||||
&& typeof reviewPlugin.review === 'function'
|
||||
);
|
||||
let reviewPluginExecution = null;
|
||||
const analyzeContext = mkInfo?.analyzeContext || {};
|
||||
const selectedPlaylistId = normalizePlaylistId(
|
||||
analyzeContext.selectedPlaylist
|
||||
@@ -9875,26 +10004,43 @@ class PipelineService extends EventEmitter {
|
||||
await this.updateProgress('MEDIAINFO_CHECK', 10, null, 'HandBrake DVD-Scan läuft', jobId);
|
||||
|
||||
const dvdScanLines = [];
|
||||
const dvdScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdHandBrakeScanInputPath, {
|
||||
mediaProfile,
|
||||
settingsMap: settings
|
||||
});
|
||||
logger.info('mediainfo:review:dvd-handbrake-scan:command', {
|
||||
jobId,
|
||||
dvdHandBrakeScanInputPath,
|
||||
cmd: dvdScanConfig.cmd,
|
||||
args: dvdScanConfig.args
|
||||
});
|
||||
let dvdScanRunInfo = null;
|
||||
if (reviewPluginEnabled) {
|
||||
const pluginScan = await this.runPluginReviewScan(reviewPlugin, job, {
|
||||
jobId,
|
||||
rawPath: dvdHandBrakeScanInputPath,
|
||||
reviewMode: 'rip',
|
||||
source: 'HANDBRAKE_SCAN_DVD',
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
dvdScanLines.push(...pluginScan.scanLines);
|
||||
dvdScanRunInfo = pluginScan.runInfo || null;
|
||||
reviewPluginExecution = this.mergePluginExecutionState(
|
||||
reviewPluginExecution,
|
||||
pluginScan.pluginExecution
|
||||
);
|
||||
} else {
|
||||
const dvdScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdHandBrakeScanInputPath, {
|
||||
mediaProfile,
|
||||
settingsMap: settings
|
||||
});
|
||||
logger.info('mediainfo:review:dvd-handbrake-scan:command', {
|
||||
jobId,
|
||||
dvdHandBrakeScanInputPath,
|
||||
cmd: dvdScanConfig.cmd,
|
||||
args: dvdScanConfig.args
|
||||
});
|
||||
|
||||
const dvdScanRunInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_DVD',
|
||||
cmd: dvdScanConfig.cmd,
|
||||
args: dvdScanConfig.args,
|
||||
collectLines: dvdScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
dvdScanRunInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_DVD',
|
||||
cmd: dvdScanConfig.cmd,
|
||||
args: dvdScanConfig.args,
|
||||
collectLines: dvdScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
}
|
||||
|
||||
dvdHandBrakeScanJson = parseMediainfoJsonOutput(dvdScanLines.join('\n'));
|
||||
if (dvdHandBrakeScanJson) {
|
||||
@@ -9911,6 +10057,10 @@ class PipelineService extends EventEmitter {
|
||||
'SYSTEM',
|
||||
`HandBrake DVD-Scan: Audio=${audioCount}, Subtitle=${subtitleCount}, Dauer=${formatDurationClock(titleInfo.durationSeconds)}`
|
||||
);
|
||||
} else if (reviewPluginEnabled) {
|
||||
const error = new Error('HandBrake DVD-Scan: Kein Titel erkannt (Plugin-Review aktiv, kein Fallback).');
|
||||
error.runInfo = dvdScanRunInfo;
|
||||
throw error;
|
||||
} else {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
@@ -9919,6 +10069,10 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
dvdHandBrakeScanJson = null;
|
||||
}
|
||||
} else if (reviewPluginEnabled) {
|
||||
const error = new Error('HandBrake DVD-Scan: Ausgabe nicht lesbar (Plugin-Review aktiv, kein Fallback).');
|
||||
error.runInfo = dvdScanRunInfo;
|
||||
throw error;
|
||||
} else {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
@@ -9946,7 +10100,8 @@ class PipelineService extends EventEmitter {
|
||||
mediaProfile,
|
||||
sourceJobId: options.sourceJobId || null,
|
||||
mediaInfoReview: partialReview,
|
||||
selectedMetadata
|
||||
selectedMetadata,
|
||||
...(reviewPluginExecution ? { pluginExecution: this.mergePluginExecutionState(mkInfo?.pluginExecution, reviewPluginExecution) } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10106,25 +10261,42 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
await this.updateProgress('MEDIAINFO_CHECK', 100, null, 'HandBrake DVD Titel-Scan läuft', jobId);
|
||||
const dvdTitleScanLines = [];
|
||||
const dvdTitleScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdScanInputPath, {
|
||||
mediaProfile,
|
||||
settingsMap: settings
|
||||
});
|
||||
logger.info('mediainfo:review:dvd-title-scan:command', {
|
||||
jobId,
|
||||
cmd: dvdTitleScanConfig.cmd,
|
||||
args: dvdTitleScanConfig.args,
|
||||
dvdScanInputPath
|
||||
});
|
||||
await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_DVD_TITLES',
|
||||
cmd: dvdTitleScanConfig.cmd,
|
||||
args: dvdTitleScanConfig.args,
|
||||
collectLines: dvdTitleScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
let dvdTitleScanRunInfo = null;
|
||||
if (reviewPluginEnabled) {
|
||||
const pluginScan = await this.runPluginReviewScan(reviewPlugin, job, {
|
||||
jobId,
|
||||
rawPath: dvdScanInputPath,
|
||||
reviewMode: 'rip',
|
||||
source: 'HANDBRAKE_SCAN_DVD_TITLES',
|
||||
silent: !this.isPrimaryJob(jobId)
|
||||
});
|
||||
dvdTitleScanLines.push(...pluginScan.scanLines);
|
||||
dvdTitleScanRunInfo = pluginScan.runInfo || null;
|
||||
reviewPluginExecution = this.mergePluginExecutionState(
|
||||
reviewPluginExecution,
|
||||
pluginScan.pluginExecution
|
||||
);
|
||||
} else {
|
||||
const dvdTitleScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdScanInputPath, {
|
||||
mediaProfile,
|
||||
settingsMap: settings
|
||||
});
|
||||
logger.info('mediainfo:review:dvd-title-scan:command', {
|
||||
jobId,
|
||||
cmd: dvdTitleScanConfig.cmd,
|
||||
args: dvdTitleScanConfig.args,
|
||||
dvdScanInputPath
|
||||
});
|
||||
dvdTitleScanRunInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'MEDIAINFO_CHECK',
|
||||
source: 'HANDBRAKE_SCAN_DVD_TITLES',
|
||||
cmd: dvdTitleScanConfig.cmd,
|
||||
args: dvdTitleScanConfig.args,
|
||||
collectLines: dvdTitleScanLines,
|
||||
collectStderrLines: false
|
||||
});
|
||||
}
|
||||
dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n'));
|
||||
if (dvdTitleScanJson) {
|
||||
const allTitles = parseHandBrakeTitleList(dvdTitleScanJson);
|
||||
@@ -10163,14 +10335,15 @@ class PipelineService extends EventEmitter {
|
||||
handBrakeDvdInputPath: dvdScanInputPath,
|
||||
encodeInputPath: null
|
||||
};
|
||||
const waitingMediainfoInfo = this.withPluginExecutionMeta({
|
||||
generatedAt: nowIso(),
|
||||
files: mediaInfoRuns
|
||||
}, reviewPluginExecution);
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'WAITING_FOR_USER_DECISION',
|
||||
last_state: 'WAITING_FOR_USER_DECISION',
|
||||
error_message: null,
|
||||
mediainfo_info_json: JSON.stringify({
|
||||
generatedAt: nowIso(),
|
||||
files: mediaInfoRuns
|
||||
}),
|
||||
mediainfo_info_json: JSON.stringify(waitingMediainfoInfo),
|
||||
encode_plan_json: JSON.stringify(enrichedReview),
|
||||
encode_input_path: null,
|
||||
encode_review_confirmed: 0
|
||||
@@ -10197,7 +10370,8 @@ class PipelineService extends EventEmitter {
|
||||
mediaProfile,
|
||||
sourceJobId: options.sourceJobId || null,
|
||||
mediaInfoReview: enrichedReview,
|
||||
selectedMetadata
|
||||
selectedMetadata,
|
||||
...(reviewPluginExecution ? { pluginExecution: this.mergePluginExecutionState(mkInfo?.pluginExecution, reviewPluginExecution) } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10214,6 +10388,10 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
enrichedReview = { ...enrichedReview, encodeInputPath: dvdScanInputPath };
|
||||
}
|
||||
} else if (reviewPluginEnabled) {
|
||||
const error = new Error('HandBrake DVD Titel-Scan: Ausgabe nicht lesbar (Plugin-Review aktiv, kein Fallback).');
|
||||
error.runInfo = dvdTitleScanRunInfo;
|
||||
throw error;
|
||||
} else {
|
||||
logger.warn('mediainfo:review:dvd-title-scan:parse-failed', { jobId });
|
||||
await historyService.appendLog(
|
||||
@@ -10235,14 +10413,16 @@ class PipelineService extends EventEmitter {
|
||||
];
|
||||
}
|
||||
|
||||
const persistedMediainfoInfo = this.withPluginExecutionMeta({
|
||||
generatedAt: nowIso(),
|
||||
files: mediaInfoRuns
|
||||
}, reviewPluginExecution);
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'READY_TO_ENCODE',
|
||||
last_state: 'READY_TO_ENCODE',
|
||||
error_message: null,
|
||||
mediainfo_info_json: JSON.stringify({
|
||||
generatedAt: nowIso(),
|
||||
files: mediaInfoRuns
|
||||
}),
|
||||
mediainfo_info_json: JSON.stringify(persistedMediainfoInfo),
|
||||
encode_plan_json: JSON.stringify(enrichedReview),
|
||||
encode_input_path: enrichedReview.encodeInputPath || null,
|
||||
encode_review_confirmed: 0
|
||||
@@ -10284,7 +10464,8 @@ class PipelineService extends EventEmitter {
|
||||
mediaProfile,
|
||||
sourceJobId: options.sourceJobId || null,
|
||||
mediaInfoReview: enrichedReview,
|
||||
selectedMetadata
|
||||
selectedMetadata,
|
||||
...(reviewPluginExecution ? { pluginExecution: this.mergePluginExecutionState(mkInfo?.pluginExecution, reviewPluginExecution) } : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10745,6 +10926,13 @@ class PipelineService extends EventEmitter {
|
||||
rawPath: job.raw_path
|
||||
});
|
||||
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
||||
const encodePlugin = await this.resolveAnalyzePlugin({ mediaProfile }, 'encode').catch(() => null);
|
||||
const encodePluginEnabled = Boolean(
|
||||
encodePlugin
|
||||
&& encodePlugin.id === mediaProfile
|
||||
&& typeof encodePlugin.encode === 'function'
|
||||
);
|
||||
let encodePluginExecution = null;
|
||||
const resolvedRawPath = this.resolveCurrentRawPathForSettings(settings, mediaProfile, job.raw_path);
|
||||
const activeRawPath = resolvedRawPath || String(job.raw_path || '').trim() || null;
|
||||
if (activeRawPath && normalizeComparablePath(activeRawPath) !== normalizeComparablePath(job.raw_path)) {
|
||||
@@ -11052,13 +11240,11 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
}
|
||||
}
|
||||
const handBrakeConfig = await settingsService.buildHandBrakeConfig(inputPath, incompleteOutputPath, {
|
||||
trackSelection,
|
||||
titleId: handBrakeTitleId,
|
||||
mediaProfile,
|
||||
settingsMap: settings,
|
||||
userPreset: encodePlan?.userPreset || null
|
||||
});
|
||||
const effectiveEncodePlan = {
|
||||
...(encodePlan && typeof encodePlan === 'object' ? encodePlan : {}),
|
||||
trackSelection: trackSelection || null,
|
||||
handBrakeTitleId: handBrakeTitleId || null
|
||||
};
|
||||
if (trackSelection) {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
@@ -11073,7 +11259,6 @@ class PipelineService extends EventEmitter {
|
||||
`HandBrake Titel-Selektion aktiv: -t ${handBrakeTitleId}`
|
||||
);
|
||||
}
|
||||
logger.info('encoding:command', { jobId, cmd: handBrakeConfig.cmd, args: handBrakeConfig.args });
|
||||
const handBrakeProgressParser = encodeScriptProgressTracker.hasScriptSteps
|
||||
? (line) => {
|
||||
const parsed = parseHandBrakeProgress(line);
|
||||
@@ -11086,14 +11271,46 @@ class PipelineService extends EventEmitter {
|
||||
};
|
||||
}
|
||||
: parseHandBrakeProgress;
|
||||
const handbrakeInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'ENCODING',
|
||||
source: 'HANDBRAKE',
|
||||
cmd: handBrakeConfig.cmd,
|
||||
args: handBrakeConfig.args,
|
||||
parser: handBrakeProgressParser
|
||||
});
|
||||
let handbrakeInfo = null;
|
||||
if (encodePluginEnabled) {
|
||||
const encodeCtx = await this.buildPluginContext(encodePlugin.id, {
|
||||
jobId,
|
||||
inputPath,
|
||||
outputPath: incompleteOutputPath,
|
||||
encodePlan: effectiveEncodePlan,
|
||||
runCommand: this.runCommand.bind(this),
|
||||
progressParser: handBrakeProgressParser,
|
||||
encodeSource: 'HANDBRAKE',
|
||||
encodeStage: 'ENCODING'
|
||||
});
|
||||
handbrakeInfo = await encodePlugin.encode(job, encodeCtx);
|
||||
encodePluginExecution = this.mergePluginExecutionState(
|
||||
encodePluginExecution,
|
||||
this.sanitizePluginExecutionState(encodeCtx.getPluginExecution())
|
||||
);
|
||||
logger.info('plugin:encode:used', {
|
||||
jobId,
|
||||
pluginId: encodePlugin.id,
|
||||
mediaProfile
|
||||
});
|
||||
} else {
|
||||
const handBrakeConfig = await settingsService.buildHandBrakeConfig(inputPath, incompleteOutputPath, {
|
||||
trackSelection,
|
||||
titleId: handBrakeTitleId,
|
||||
mediaProfile,
|
||||
settingsMap: settings,
|
||||
userPreset: encodePlan?.userPreset || null
|
||||
});
|
||||
logger.info('encoding:command', { jobId, cmd: handBrakeConfig.cmd, args: handBrakeConfig.args });
|
||||
handbrakeInfo = await this.runCommand({
|
||||
jobId,
|
||||
stage: 'ENCODING',
|
||||
source: 'HANDBRAKE',
|
||||
cmd: handBrakeConfig.cmd,
|
||||
args: handBrakeConfig.args,
|
||||
parser: handBrakeProgressParser
|
||||
});
|
||||
}
|
||||
const outputFinalization = finalizeOutputPathForCompletedEncode(
|
||||
incompleteOutputPath,
|
||||
preferredFinalOutputPath
|
||||
@@ -11192,11 +11409,11 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
const handbrakeInfoWithPostScripts = {
|
||||
const handbrakeInfoWithPostScripts = this.withPluginExecutionMeta({
|
||||
...handbrakeInfo,
|
||||
preEncodeScripts: preEncodeScriptsSummary,
|
||||
postEncodeScripts: postEncodeScriptsSummary
|
||||
};
|
||||
}, encodePluginExecution);
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
handbrake_info_json: JSON.stringify(handbrakeInfoWithPostScripts),
|
||||
@@ -11511,12 +11728,11 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
|
||||
const mkInfoBeforeRip = this.safeParseJson(job.makemkv_info_json);
|
||||
// Nach dem Plugin-Rip hat jobProgress die aktuelle pluginExecution (analyze + rip).
|
||||
// Nach Legacy-Rip nehmen wir die analyze-Phase aus der DB.
|
||||
const postRipPluginExecution = this.sanitizePluginExecutionState(
|
||||
// Merge bestehende Analyze-Marker aus der DB mit den Live-Markern aus dem Rip-Lauf,
|
||||
// damit analyze nicht verloren geht, wenn jobProgress nur die neueste Rip-Phase enthält.
|
||||
const postRipPluginExecution = this.mergePluginExecutionState(
|
||||
mkInfoBeforeRip?.pluginExecution,
|
||||
this.jobProgress.get(Number(jobId))?.context?.pluginExecution
|
||||
?? mkInfoBeforeRip?.pluginExecution
|
||||
?? null
|
||||
);
|
||||
await historyService.updateJob(jobId, {
|
||||
makemkv_info_json: JSON.stringify(this.withAnalyzeContextMediaProfile({
|
||||
|
||||
Reference in New Issue
Block a user