0.12.0-4 DVD Plugin

This commit is contained in:
2026-03-21 19:12:58 +00:00
parent e99cdf1895
commit c0350644b9
11 changed files with 446 additions and 192 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-backend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-backend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"dependencies": {
"archiver": "^7.0.1",
"cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-backend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"private": true,
"type": "commonjs",
"scripts": {
+6 -6
View File
@@ -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 ─────────────────────────────────────────────────────────────
{
+39 -2
View File
@@ -147,12 +147,30 @@ class VideoDiscPlugin extends SourcePlugin {
});
const scanLines = [];
const runInfo = await _runCommandCaptured({
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,7 +333,25 @@ class VideoDiscPlugin extends SourcePlugin {
ctx.emitProgress(0, `${this.name}: Encoding läuft …`);
const runInfo = await _spawnAndWait({
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,
@@ -325,6 +361,7 @@ class VideoDiscPlugin extends SourcePlugin {
onProcessHandle,
context: { jobId: job?.id, source: `${this.id}Plugin.encode`, stage: 'ENCODING' }
});
}
ctx.logger.info(`${this.id}:encode:done`, {
jobId: job?.id,
+274 -58
View File
@@ -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,6 +7338,24 @@ class PipelineService extends EventEmitter {
});
const lines = [];
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,
@@ -7304,7 +7365,7 @@ class PipelineService extends EventEmitter {
selectedTitleId: selectedMakemkvTitleId
});
const runInfo = await this.runCommand({
runInfo = await this.runCommand({
jobId,
stage: 'MEDIAINFO_CHECK',
source: 'HANDBRAKE_SCAN',
@@ -7313,6 +7374,8 @@ class PipelineService extends EventEmitter {
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,6 +7699,20 @@ class PipelineService extends EventEmitter {
);
try {
const resolveScanLines = [];
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,
@@ -7645,6 +7731,7 @@ class PipelineService extends EventEmitter {
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,6 +8024,21 @@ class PipelineService extends EventEmitter {
} else {
try {
const resolveScanLines = [];
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,
@@ -7942,6 +8059,7 @@ class PipelineService extends EventEmitter {
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,12 +8286,7 @@ class PipelineService extends EventEmitter {
throw error;
}
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({
const persistedMediainfoInfo = this.withPluginExecutionMeta({
generatedAt: nowIso(),
source: 'raw_backup_handbrake_playlist_scan',
makemkvAnalyzeRunInfo: makeMkvAnalyzeRunInfo,
@@ -8181,7 +8294,14 @@ class PipelineService extends EventEmitter {
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(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,6 +10004,22 @@ class PipelineService extends EventEmitter {
await this.updateProgress('MEDIAINFO_CHECK', 10, null, 'HandBrake DVD-Scan läuft', jobId);
const dvdScanLines = [];
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
@@ -9886,7 +10031,7 @@ class PipelineService extends EventEmitter {
args: dvdScanConfig.args
});
const dvdScanRunInfo = await this.runCommand({
dvdScanRunInfo = await this.runCommand({
jobId,
stage: 'MEDIAINFO_CHECK',
source: 'HANDBRAKE_SCAN_DVD',
@@ -9895,6 +10040,7 @@ class PipelineService extends EventEmitter {
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,6 +10261,22 @@ class PipelineService extends EventEmitter {
);
await this.updateProgress('MEDIAINFO_CHECK', 100, null, 'HandBrake DVD Titel-Scan läuft', jobId);
const dvdTitleScanLines = [];
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
@@ -10116,7 +10287,7 @@ class PipelineService extends EventEmitter {
args: dvdTitleScanConfig.args,
dvdScanInputPath
});
await this.runCommand({
dvdTitleScanRunInfo = await this.runCommand({
jobId,
stage: 'MEDIAINFO_CHECK',
source: 'HANDBRAKE_SCAN_DVD_TITLES',
@@ -10125,6 +10296,7 @@ class PipelineService extends EventEmitter {
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,7 +11271,38 @@ class PipelineService extends EventEmitter {
};
}
: parseHandBrakeProgress;
const handbrakeInfo = await this.runCommand({
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',
@@ -11094,6 +11310,7 @@ class PipelineService extends EventEmitter {
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({
+4 -4
View File
@@ -515,12 +515,12 @@ VALUES ('use_plugin_architecture_dvd_rip', 'Erweitert', 'Rip (DVD)', 'boolean',
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('use_plugin_architecture_dvd_rip', 'true');
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index, depends_on)
VALUES ('use_plugin_architecture_dvd_review', 'Erweitert', 'Review / Mediainfo-Prüfung (DVD)', 'boolean', 1, 'Noch nicht implementiert — kommt in einem späteren Sprint.', 'false', '[]', '{"readonly":true}', 10013, 'use_plugin_architecture_dvd');
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('use_plugin_architecture_dvd_review', 'false');
VALUES ('use_plugin_architecture_dvd_review', 'Erweitert', 'Review / Mediainfo-Prüfung (DVD)', 'boolean', 1, 'Review-/Track-Analyse läuft über das DVD-Plugin.', 'true', '[]', '{}', 10013, 'use_plugin_architecture_dvd');
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('use_plugin_architecture_dvd_review', 'true');
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index, depends_on)
VALUES ('use_plugin_architecture_dvd_encode', 'Erweitert', 'Encoding (DVD)', 'boolean', 1, 'Noch nicht implementiert — kommt in einem späteren Sprint.', 'false', '[]', '{"readonly":true}', 10014, 'use_plugin_architecture_dvd');
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('use_plugin_architecture_dvd_encode', 'false');
VALUES ('use_plugin_architecture_dvd_encode', 'Erweitert', 'Encoding (DVD)', 'boolean', 1, 'Encoding läuft über das DVD-Plugin.', 'true', '[]', '{}', 10014, 'use_plugin_architecture_dvd');
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('use_plugin_architecture_dvd_encode', 'true');
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index, depends_on)
VALUES ('use_plugin_architecture_cd', 'Erweitert', 'Audio-CD Plugin', 'boolean', 1, 'Aktiviert das Audio-CD Plugin. Deaktiviert = Legacy-Pfad für alle CD-Phasen.', 'true', '[]', '{}', 10020, 'use_plugin_architecture');
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
"version": "0.12.0-3",
"version": "0.12.0-4",
"private": true,
"type": "module",
"scripts": {
+2 -1
View File
@@ -111,11 +111,12 @@ export function normalizePluginExecutionState(rawState) {
export function resolveJobPluginExecution(job = null, liveContext = null, options = {}) {
const liveExecution = normalizePluginExecutionState(liveContext?.pluginExecution);
const makemkvExecution = normalizePluginExecutionState(job?.makemkvInfo?.pluginExecution);
const mediainfoExecution = normalizePluginExecutionState(job?.mediainfoInfo?.pluginExecution);
const handbrakeExecution = normalizePluginExecutionState(job?.handbrakeInfo?.pluginExecution);
const jobExecution = normalizePluginExecutionState(job?.pluginExecution);
const expectedStage = inferExpectedPluginStage(options);
const candidates = [liveExecution, makemkvExecution, handbrakeExecution, jobExecution].filter(Boolean);
const candidates = [liveExecution, handbrakeExecution, mediainfoExecution, makemkvExecution, jobExecution].filter(Boolean);
if (candidates.length === 0) {
return null;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster",
"version": "0.12.0-3",
"version": "0.12.0-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster",
"version": "0.12.0-3",
"version": "0.12.0-4",
"devDependencies": {
"concurrently": "^9.1.2"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ripster",
"private": true,
"version": "0.12.0-3",
"version": "0.12.0-4",
"scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend",