From ea096cb33c563f73dfc17dd575e696108c9d806f Mon Sep 17 00:00:00 2001 From: mboehmlaender Date: Mon, 16 Mar 2026 14:30:54 +0000 Subject: [PATCH] 0.10.2-14 DVD title Select --- backend/package-lock.json | 4 +- backend/package.json | 2 +- backend/src/services/pipelineService.js | 107 +++++++++++++++++------- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- package-lock.json | 4 +- package.json | 2 +- 7 files changed, 84 insertions(+), 41 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 7db5678..f37dbb4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.10.2-13", + "version": "0.10.2-14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.10.2-13", + "version": "0.10.2-14", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 3cd60ac..ffbb27b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.10.2-13", + "version": "0.10.2-14", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 5c37c2f..ca7e4e5 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -1449,6 +1449,28 @@ function pickScanTitleList(scanJson) { 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) { const playlistId = normalizePlaylistId(playlistIdRaw); 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); 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) { selected = parsedTitles .slice() @@ -8193,6 +8223,7 @@ class PipelineService extends EventEmitter { || rawMedia.source === 'single_extensionless'; let dvdHandBrakeScanJson = null; let dvdHandBrakeScanInputPath = null; + let dvdSelectedTitleInfo = null; // set when HandBrake scan succeeds; used to skip re-selection if (isDvdSource && mediaFiles.length > 0) { // 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) { const titleInfo = parseHandBrakeSelectedTitleInfo(dvdHandBrakeScanJson); if (titleInfo) { + dvdSelectedTitleInfo = titleInfo; const syntheticMediaInfo = buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo); mediaInfoByPath[dvdHandBrakeScanInputPath] = syntheticMediaInfo; effectiveMediaFiles = [{ path: dvdHandBrakeScanInputPath, size: 0 }]; @@ -8415,39 +8447,50 @@ class PipelineService extends EventEmitter { 'SYSTEM', `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 { - // Reuse the HandBrake scan result from the DVD analysis step when available, - // otherwise run a fresh scan for title selection. - let dvdTitleScanJson = dvdHandBrakeScanJson || null; - if (!dvdTitleScanJson) { - 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 - }); - dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n')); - } + // No pre-scanned result available — run a fresh HandBrake scan for title selection. + let dvdTitleScanJson = null; + 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 + }); + dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n')); if (dvdTitleScanJson) { const allTitles = parseHandBrakeTitleList(dvdTitleScanJson); const minLengthMinutes = Number(settings?.makemkv_min_length_minutes || 0); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9618053..d451cea 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.10.2-13", + "version": "0.10.2-14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.10.2-13", + "version": "0.10.2-14", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 69bbaa1..cba0cf8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.10.2-13", + "version": "0.10.2-14", "private": true, "type": "module", "scripts": { diff --git a/package-lock.json b/package-lock.json index ada0866..adb164d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster", - "version": "0.10.2-13", + "version": "0.10.2-14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster", - "version": "0.10.2-13", + "version": "0.10.2-14", "devDependencies": { "concurrently": "^9.1.2" } diff --git a/package.json b/package.json index 5798de5..d776a6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ripster", "private": true, - "version": "0.10.2-13", + "version": "0.10.2-14", "scripts": { "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev:backend": "npm run dev --prefix backend",