0.10.2-14 DVD title Select

This commit is contained in:
2026-03-16 14:30:54 +00:00
parent 2cbbbd9a86
commit ea096cb33c
7 changed files with 84 additions and 41 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-13", "version": "0.10.2-14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-13", "version": "0.10.2-14",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-13", "version": "0.10.2-14",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+75 -32
View File
@@ -1449,6 +1449,28 @@ function pickScanTitleList(scanJson) {
return alt || []; return alt || [];
} }
// Returns the HandBrake-selected main feature title ID (the "1 valid title" HandBrake reports).
// Present in the JSON as MainFeature at the top level or inside the Scan node.
function pickScanMainFeatureTitleId(scanJson) {
if (!scanJson || typeof scanJson !== 'object') {
return null;
}
const direct = scanJson.MainFeature ?? scanJson.mainFeature ?? null;
if (direct != null) {
const id = Number(direct);
return Number.isFinite(id) && id > 0 ? id : null;
}
const scanNode = scanJson.Scan && typeof scanJson.Scan === 'object' ? scanJson.Scan : null;
if (scanNode) {
const nested = scanNode.MainFeature ?? scanNode.mainFeature ?? null;
if (nested != null) {
const id = Number(nested);
return Number.isFinite(id) && id > 0 ? id : null;
}
}
return null;
}
function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) { function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) {
const playlistId = normalizePlaylistId(playlistIdRaw); const playlistId = normalizePlaylistId(playlistIdRaw);
if (!playlistId || !playlistAnalysis) { if (!playlistId || !playlistAnalysis) {
@@ -2094,6 +2116,14 @@ function parseHandBrakeSelectedTitleInfo(scanJson, options = {}) {
.sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.handBrakeTitleId - b.handBrakeTitleId); .sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.handBrakeTitleId - b.handBrakeTitleId);
selected = playlistMatches[0] || null; selected = playlistMatches[0] || null;
} }
// Use HandBrake's own MainFeature designation before falling back to heuristics.
// This is exactly the "1 valid title" HandBrake reports in text output.
if (!selected) {
const mainFeatureId = pickScanMainFeatureTitleId(scanJson);
if (mainFeatureId) {
selected = parsedTitles.find((title) => title.handBrakeTitleId === mainFeatureId) || null;
}
}
if (!selected) { if (!selected) {
selected = parsedTitles selected = parsedTitles
.slice() .slice()
@@ -8193,6 +8223,7 @@ class PipelineService extends EventEmitter {
|| rawMedia.source === 'single_extensionless'; || rawMedia.source === 'single_extensionless';
let dvdHandBrakeScanJson = null; let dvdHandBrakeScanJson = null;
let dvdHandBrakeScanInputPath = null; let dvdHandBrakeScanInputPath = null;
let dvdSelectedTitleInfo = null; // set when HandBrake scan succeeds; used to skip re-selection
if (isDvdSource && mediaFiles.length > 0) { if (isDvdSource && mediaFiles.length > 0) {
// For 'dvd': scan the folder that contains VIDEO_TS (parent of the VIDEO_TS dir). // For 'dvd': scan the folder that contains VIDEO_TS (parent of the VIDEO_TS dir).
@@ -8234,6 +8265,7 @@ class PipelineService extends EventEmitter {
if (dvdHandBrakeScanJson) { if (dvdHandBrakeScanJson) {
const titleInfo = parseHandBrakeSelectedTitleInfo(dvdHandBrakeScanJson); const titleInfo = parseHandBrakeSelectedTitleInfo(dvdHandBrakeScanJson);
if (titleInfo) { if (titleInfo) {
dvdSelectedTitleInfo = titleInfo;
const syntheticMediaInfo = buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo); const syntheticMediaInfo = buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo);
mediaInfoByPath[dvdHandBrakeScanInputPath] = syntheticMediaInfo; mediaInfoByPath[dvdHandBrakeScanInputPath] = syntheticMediaInfo;
effectiveMediaFiles = [{ path: dvdHandBrakeScanInputPath, size: 0 }]; effectiveMediaFiles = [{ path: dvdHandBrakeScanInputPath, size: 0 }];
@@ -8415,39 +8447,50 @@ class PipelineService extends EventEmitter {
'SYSTEM', 'SYSTEM',
`HandBrake Titel-Auswahl (manuell) übernommen: -t ${preSelectedHandBrakeTitleId}` `HandBrake Titel-Auswahl (manuell) übernommen: -t ${preSelectedHandBrakeTitleId}`
); );
} else if (dvdSelectedTitleInfo) {
// HandBrake already identified the valid title during the DVD analysis scan above.
// Use it directly — no need to re-parse all titles from TitleList, which would
// include short/invalid titles and incorrectly trigger manual title selection.
enrichedReview = {
...enrichedReview,
handBrakeTitleId: dvdSelectedTitleInfo.handBrakeTitleId,
encodeInputPath: dvdScanInputPath
};
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake DVD Titel automatisch gewählt (aus Scan): -t ${dvdSelectedTitleInfo.handBrakeTitleId}`
);
} else { } else {
// Reuse the HandBrake scan result from the DVD analysis step when available, // No pre-scanned result available — run a fresh HandBrake scan for title selection.
// otherwise run a fresh scan for title selection. let dvdTitleScanJson = null;
let dvdTitleScanJson = dvdHandBrakeScanJson || null; await historyService.appendLog(
if (!dvdTitleScanJson) { jobId,
await historyService.appendLog( 'SYSTEM',
jobId, 'HandBrake Titel-Scan für DVD-Struktur wird gestartet...'
'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 = [];
await this.updateProgress('MEDIAINFO_CHECK', 100, null, 'HandBrake DVD Titel-Scan läuft', jobId); const dvdTitleScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdScanInputPath, {
const dvdTitleScanLines = []; mediaProfile,
const dvdTitleScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdScanInputPath, { settingsMap: settings
mediaProfile, });
settingsMap: settings logger.info('mediainfo:review:dvd-title-scan:command', {
}); jobId,
logger.info('mediainfo:review:dvd-title-scan:command', { cmd: dvdTitleScanConfig.cmd,
jobId, args: dvdTitleScanConfig.args,
cmd: dvdTitleScanConfig.cmd, dvdScanInputPath
args: dvdTitleScanConfig.args, });
dvdScanInputPath await this.runCommand({
}); jobId,
await this.runCommand({ stage: 'MEDIAINFO_CHECK',
jobId, source: 'HANDBRAKE_SCAN_DVD_TITLES',
stage: 'MEDIAINFO_CHECK', cmd: dvdTitleScanConfig.cmd,
source: 'HANDBRAKE_SCAN_DVD_TITLES', args: dvdTitleScanConfig.args,
cmd: dvdTitleScanConfig.cmd, collectLines: dvdTitleScanLines,
args: dvdTitleScanConfig.args, collectStderrLines: false
collectLines: dvdTitleScanLines, });
collectStderrLines: false dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n'));
});
dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n'));
}
if (dvdTitleScanJson) { if (dvdTitleScanJson) {
const allTitles = parseHandBrakeTitleList(dvdTitleScanJson); const allTitles = parseHandBrakeTitleList(dvdTitleScanJson);
const minLengthMinutes = Number(settings?.makemkv_min_length_minutes || 0); const minLengthMinutes = Number(settings?.makemkv_min_length_minutes || 0);
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-13", "version": "0.10.2-14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-13", "version": "0.10.2-14",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-13", "version": "0.10.2-14",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.10.2-13", "version": "0.10.2-14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.10.2-13", "version": "0.10.2-14",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.10.2-13", "version": "0.10.2-14",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend", "dev:backend": "npm run dev --prefix backend",