0.10.2-10 DVD Title Scan

This commit is contained in:
2026-03-16 11:05:38 +00:00
parent 38a82706ce
commit 901b8a0b61
14 changed files with 482 additions and 99 deletions
+5 -3
View File
@@ -197,7 +197,7 @@ router.post(
router.post(
'/select-metadata',
asyncHandler(async (req, res) => {
const { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist } = req.body;
const { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist, selectedHandBrakeTitleId } = req.body;
if (!jobId) {
const error = new Error('jobId fehlt.');
@@ -213,7 +213,8 @@ router.post(
imdbId,
poster,
fromOmdb,
selectedPlaylist
selectedPlaylist,
selectedHandBrakeTitleId
});
const job = await pipelineService.selectMetadata({
@@ -223,7 +224,8 @@ router.post(
imdbId,
poster,
fromOmdb,
selectedPlaylist
selectedPlaylist,
selectedHandBrakeTitleId
});
res.json({ job });
+252 -4
View File
@@ -6959,9 +6959,9 @@ class PipelineService extends EventEmitter {
return this.runMediainfoReviewForJob(jobId, rawPath, options);
}
async selectMetadata({ jobId, title, year, imdbId, poster, fromOmdb = null, selectedPlaylist = null }) {
async selectMetadata({ jobId, title, year, imdbId, poster, fromOmdb = null, selectedPlaylist = null, selectedHandBrakeTitleId = null }) {
this.ensureNotBusy('selectMetadata', jobId);
logger.info('metadata:selected', { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist });
logger.info('metadata:selected', { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist, selectedHandBrakeTitleId });
const job = await historyService.getJobById(jobId);
if (!job) {
@@ -6973,6 +6973,7 @@ class PipelineService extends EventEmitter {
const mediaProfile = this.resolveMediaProfileForJob(job, { makemkvInfo: mkInfo });
const normalizedSelectedPlaylist = normalizePlaylistId(selectedPlaylist);
const normalizedSelectedHandBrakeTitleId = normalizeReviewTitleId(selectedHandBrakeTitleId);
const waitingForPlaylistSelection = (
job.status === 'WAITING_FOR_USER_DECISION'
|| job.last_state === 'WAITING_FOR_USER_DECISION'
@@ -6984,6 +6985,52 @@ class PipelineService extends EventEmitter {
|| poster !== undefined
|| (fromOmdb !== null && fromOmdb !== undefined)
);
if (normalizedSelectedHandBrakeTitleId && waitingForPlaylistSelection && job.raw_path && !hasExplicitMetadataPayload && !normalizedSelectedPlaylist) {
const currentMkInfo = mkInfo;
const currentAnalyzeContext = currentMkInfo?.analyzeContext || {};
const updatedMkInfo = this.withAnalyzeContextMediaProfile({
...currentMkInfo,
analyzeContext: {
...currentAnalyzeContext,
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId
}
}, mediaProfile);
await historyService.updateJob(jobId, {
status: 'MEDIAINFO_CHECK',
last_state: 'MEDIAINFO_CHECK',
error_message: null,
makemkv_info_json: JSON.stringify(updatedMkInfo),
mediainfo_info_json: null,
encode_plan_json: null,
encode_input_path: null,
encode_review_confirmed: 0
});
await historyService.appendLog(
jobId,
'USER_ACTION',
`DVD Titel-Auswahl gesetzt: -t ${normalizedSelectedHandBrakeTitleId}.`
);
try {
await this.runMediainfoReviewForJob(jobId, job.raw_path, {
mode: 'rip',
mediaProfile,
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId
});
} catch (error) {
logger.error('metadata:handbrake-title-selection:review-failed', {
jobId,
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId,
error: errorToMeta(error)
});
await this.failJob(jobId, 'MEDIAINFO_CHECK', error);
throw error;
}
return historyService.getJobById(jobId);
}
if (normalizedSelectedPlaylist && waitingForPlaylistSelection && job.raw_path && !hasExplicitMetadataPayload) {
const currentMkInfo = mkInfo;
const currentAnalyzeContext = currentMkInfo?.analyzeContext || {};
@@ -8222,9 +8269,156 @@ class PipelineService extends EventEmitter {
: null
);
enrichedReview = reviewPrefillResult.plan;
// HandBrake title scan for DVD structures (VIDEO_TS directories / ISO images)
const needsHandBrakeTitleScan = rawMedia.source === 'dvd'
|| rawMedia.source === 'dvd_image'
|| rawMedia.source === 'single_extensionless';
if (needsHandBrakeTitleScan) {
const dvdScanInputPath = rawMedia.source === 'dvd' ? rawPath : mediaFiles[0].path;
const preSelectedHandBrakeTitleId = normalizeReviewTitleId(options?.selectedHandBrakeTitleId);
if (preSelectedHandBrakeTitleId) {
enrichedReview = {
...enrichedReview,
handBrakeTitleId: preSelectedHandBrakeTitleId,
encodeInputPath: dvdScanInputPath
};
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake Titel-Auswahl (manuell) übernommen: -t ${preSelectedHandBrakeTitleId}`
);
} else {
await historyService.appendLog(
jobId,
'SYSTEM',
'HandBrake Titel-Scan für DVD-Struktur wird gestartet...'
);
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
});
const dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n'));
if (dvdTitleScanJson) {
const allTitles = parseHandBrakeTitleList(dvdTitleScanJson);
const minLengthMinutes = Number(settings?.makemkv_min_length_minutes || 0);
const minLengthSeconds = Math.max(0, Math.round(minLengthMinutes * 60));
const titleCandidates = allTitles.filter((t) => t.durationSeconds >= minLengthSeconds);
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake DVD Titel-Scan: ${allTitles.length} gesamt, ${titleCandidates.length}${minLengthMinutes} min.`
);
if (titleCandidates.length === 1) {
enrichedReview = {
...enrichedReview,
handBrakeTitleId: titleCandidates[0].handBrakeTitleId,
encodeInputPath: dvdScanInputPath
};
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake DVD Titel automatisch gewählt: -t ${titleCandidates[0].handBrakeTitleId}`
);
} else if (titleCandidates.length > 1) {
const candidatesData = titleCandidates.map((t) => ({
handBrakeTitleId: t.handBrakeTitleId,
durationSeconds: t.durationSeconds,
durationMinutes: Number((t.durationSeconds / 60).toFixed(1)),
audioTrackCount: t.audioTrackCount,
subtitleTrackCount: t.subtitleTrackCount,
sizeBytes: t.sizeBytes || 0
}));
enrichedReview = {
...enrichedReview,
handBrakeTitleDecisionRequired: true,
handBrakeTitleCandidates: candidatesData,
handBrakeDvdInputPath: dvdScanInputPath,
encodeInputPath: null
};
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
}),
encode_plan_json: JSON.stringify(enrichedReview),
encode_input_path: null,
encode_review_confirmed: 0
});
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake DVD Titelauswahl erforderlich: ${titleCandidates.length} Kandidaten. Nutzer muss Titel wählen.`
);
if (this.isPrimaryJob(jobId)) {
await this.setState('WAITING_FOR_USER_DECISION', {
activeJobId: jobId,
progress: 0,
eta: null,
statusText: `DVD Titelauswahl: ${titleCandidates.length} Kandidaten gefunden`,
context: {
jobId,
rawPath,
handBrakeTitleDecisionRequired: true,
handBrakeTitleCandidates: candidatesData,
handBrakeDvdInputPath: dvdScanInputPath,
reviewConfirmed: false,
mode: options.mode || 'rip',
mediaProfile,
sourceJobId: options.sourceJobId || null,
mediaInfoReview: enrichedReview,
selectedMetadata
}
});
}
void this.notifyPushover('metadata_ready', {
title: 'Ripster - DVD Titelauswahl',
message: `Job #${jobId}: ${titleCandidates.length} DVD-Titel gefunden, manuelle Auswahl erforderlich`
});
return enrichedReview;
} else {
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake DVD Titel-Scan: Kein Titel ≥ ${minLengthMinutes} min. Ohne Titel-ID fortgefahren.`
);
enrichedReview = { ...enrichedReview, encodeInputPath: dvdScanInputPath };
}
} else {
logger.warn('mediainfo:review:dvd-title-scan:parse-failed', { jobId });
await historyService.appendLog(
jobId,
'SYSTEM',
'HandBrake DVD Titel-Scan: Ausgabe nicht lesbar. Ohne Titel-ID fortgefahren.'
);
enrichedReview = { ...enrichedReview, encodeInputPath: dvdScanInputPath };
}
}
}
const hasEncodableTitle = Boolean(enrichedReview.encodeInputPath);
const titleSelectionRequired = Boolean(enrichedReview.titleSelectionRequired);
if (!hasEncodableTitle && !titleSelectionRequired) {
if (!hasEncodableTitle && !titleSelectionRequired && !enrichedReview.handBrakeTitleDecisionRequired) {
enrichedReview.notes = [
...(Array.isArray(enrichedReview.notes) ? enrichedReview.notes : []),
'Kein Titel erfüllt aktuell MIN_LENGTH_MINUTES. Bitte Konfiguration prüfen.'
@@ -8991,7 +9185,61 @@ class PipelineService extends EventEmitter {
`HandBrake Titel-Mapping: ${selectedPlaylistId}.mpls -> -t ${handBrakeTitleId}`
);
} else if (!handBrakeTitleId) {
handBrakeTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId ?? encodePlan?.encodeInputTitleId);
const dvdResolveScanLines = [];
const dvdResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, {
mediaProfile,
settingsMap: settings
});
await historyService.appendLog(
jobId,
'SYSTEM',
'HandBrake Titel-Scan für Verzeichnis ohne Playlist wird gestartet...'
);
logger.info('encoding:dvd-title-resolve-scan:command', {
jobId,
cmd: dvdResolveScanConfig.cmd,
args: dvdResolveScanConfig.args
});
const dvdResolveRunInfo = await this.runCommand({
jobId,
stage: 'ENCODING',
source: 'HANDBRAKE_SCAN_TITLE_RESOLVE',
cmd: dvdResolveScanConfig.cmd,
args: dvdResolveScanConfig.args,
collectLines: dvdResolveScanLines,
collectStderrLines: false
});
const dvdResolveParsed = parseMediainfoJsonOutput(dvdResolveScanLines.join('\n'));
if (dvdResolveParsed) {
const dvdTitleInfo = parseHandBrakeSelectedTitleInfo(dvdResolveParsed, {
handBrakeTitleId: null,
playlistId: null
});
if (dvdTitleInfo?.handBrakeTitleId) {
handBrakeTitleId = dvdTitleInfo.handBrakeTitleId;
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake Titel-Scan: längster Titel gefunden -> -t ${handBrakeTitleId}`
);
}
} else {
logger.warn('encoding:dvd-title-resolve-scan:parse-failed', {
jobId,
runInfo: dvdResolveRunInfo
});
}
}
}
if (!handBrakeTitleId) {
const fallbackTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId);
if (fallbackTitleId) {
handBrakeTitleId = fallbackTitleId;
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake Titel-ID aus Encode-Plan übernommen: -t ${handBrakeTitleId}`
);
}
}
const handBrakeConfig = await settingsService.buildHandBrakeConfig(inputPath, incompleteOutputPath, {