diff --git a/backend/package-lock.json b/backend/package-lock.json index d27da25..7db5678 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.10.2-12", + "version": "0.10.2-13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.10.2-12", + "version": "0.10.2-13", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 8e5240d..3cd60ac 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.10.2-12", + "version": "0.10.2-13", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index 5514061..5c37c2f 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -8154,8 +8154,10 @@ class PipelineService extends EventEmitter { const mediaInfoByPath = {}; const mediaInfoRuns = []; + let effectiveMediaFiles = mediaFiles; + const buildReviewSnapshot = (processedCount) => { - const processedFiles = mediaFiles + const processedFiles = effectiveMediaFiles .slice(0, processedCount) .filter((item) => Boolean(mediaInfoByPath[item.path])); @@ -8178,73 +8180,95 @@ class PipelineService extends EventEmitter { mediaProfile, sourceJobId: options.sourceJobId || null, reviewConfirmed: false, - partial: processedFiles.length < mediaFiles.length, + partial: processedFiles.length < effectiveMediaFiles.length, processedFiles: processedFiles.length, - totalFiles: mediaFiles.length + totalFiles: effectiveMediaFiles.length }; }; - for (let i = 0; i < mediaFiles.length; i += 1) { - const file = mediaFiles[i]; - const percent = Number((((i + 1) / mediaFiles.length) * 100).toFixed(2)); - await this.updateProgress('MEDIAINFO_CHECK', percent, null, `Mediainfo ${i + 1}/${mediaFiles.length}: ${path.basename(file.path)}`, jobId); + // For DVD sources, use HandBrake scan directly instead of mediainfo (mediainfo + // is unreliable on VOB files and often returns incomplete track information). + const isDvdSource = rawMedia.source === 'dvd' + || rawMedia.source === 'dvd_image' + || rawMedia.source === 'single_extensionless'; + let dvdHandBrakeScanJson = null; + let dvdHandBrakeScanInputPath = null; - const result = await this.runMediainfoForFile(jobId, file.path, { + if (isDvdSource && mediaFiles.length > 0) { + // For 'dvd': scan the folder that contains VIDEO_TS (parent of the VIDEO_TS dir). + // For image/extensionless: scan the file directly. + dvdHandBrakeScanInputPath = rawMedia.source === 'dvd' + ? path.dirname(path.dirname(mediaFiles[0].path)) + : mediaFiles[0].path; + + await historyService.appendLog( + jobId, + 'SYSTEM', + `DVD-Analyse via HandBrake Scan: ${path.basename(dvdHandBrakeScanInputPath)}` + ); + await this.updateProgress('MEDIAINFO_CHECK', 10, null, 'HandBrake DVD-Scan läuft', jobId); + + const dvdScanLines = []; + const dvdScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdHandBrakeScanInputPath, { mediaProfile, settingsMap: settings }); - let parsedMediaInfo = result.parsed; - let fallbackRunInfo = null; - if (shouldRunDvdTrackFallback(parsedMediaInfo, mediaProfile, file.path)) { - try { - const fallback = await this.runDvdTrackFallbackForFile(jobId, file.path, { - mediaProfile, - settingsMap: settings - }); - if (fallback?.parsedMediaInfo) { - parsedMediaInfo = fallback.parsedMediaInfo; - fallbackRunInfo = fallback.runInfo || null; - const audioCount = Array.isArray(fallback?.titleInfo?.audioTracks) - ? fallback.titleInfo.audioTracks.length - : 0; - const subtitleCount = Array.isArray(fallback?.titleInfo?.subtitleTracks) - ? fallback.titleInfo.subtitleTracks.length - : 0; - await historyService.appendLog( - jobId, - 'SYSTEM', - `DVD Track-Fallback aktiv (${path.basename(file.path)}): Audio=${audioCount}, Subtitle=${subtitleCount}.` - ); - } else { - await historyService.appendLog( - jobId, - 'SYSTEM', - `DVD Track-Fallback ohne Ergebnis (${path.basename(file.path)}).` - ); - } - } catch (error) { - logger.warn('mediainfo:track-fallback:failed', { - jobId, - inputPath: file.path, - error: errorToMeta(error) - }); - } - } - - mediaInfoByPath[file.path] = parsedMediaInfo; - mediaInfoRuns.push({ - filePath: file.path, - runInfo: result.runInfo, - fallbackRunInfo + logger.info('mediainfo:review:dvd-handbrake-scan:command', { + jobId, + dvdHandBrakeScanInputPath, + cmd: dvdScanConfig.cmd, + args: dvdScanConfig.args }); - const partialReview = buildReviewSnapshot(i + 1); + const 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) { + const titleInfo = parseHandBrakeSelectedTitleInfo(dvdHandBrakeScanJson); + if (titleInfo) { + const syntheticMediaInfo = buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo); + mediaInfoByPath[dvdHandBrakeScanInputPath] = syntheticMediaInfo; + effectiveMediaFiles = [{ path: dvdHandBrakeScanInputPath, size: 0 }]; + const audioCount = Array.isArray(titleInfo.audioTracks) ? titleInfo.audioTracks.length : 0; + const subtitleCount = Array.isArray(titleInfo.subtitleTracks) ? titleInfo.subtitleTracks.length : 0; + await historyService.appendLog( + jobId, + 'SYSTEM', + `HandBrake DVD-Scan: Audio=${audioCount}, Subtitle=${subtitleCount}, Dauer=${formatDurationClock(titleInfo.durationSeconds)}` + ); + } else { + await historyService.appendLog( + jobId, + 'SYSTEM', + 'HandBrake DVD-Scan: Kein Titel erkannt, falle auf MediaInfo zurück.' + ); + dvdHandBrakeScanJson = null; + } + } else { + await historyService.appendLog( + jobId, + 'SYSTEM', + 'HandBrake DVD-Scan: Ausgabe nicht lesbar, falle auf MediaInfo zurück.' + ); + } + + mediaInfoRuns.push({ filePath: dvdHandBrakeScanInputPath, runInfo: dvdScanRunInfo, fallbackRunInfo: null }); + + const partialReview = buildReviewSnapshot(1); if (this.isPrimaryJob(jobId)) { await this.setState('MEDIAINFO_CHECK', { activeJobId: jobId, - progress: percent, + progress: 50, eta: null, - statusText: `Mediainfo ${i + 1}/${mediaFiles.length} analysiert: ${path.basename(file.path)}`, + statusText: `HandBrake DVD-Scan abgeschlossen: ${path.basename(dvdHandBrakeScanInputPath)}`, context: { jobId, rawPath, @@ -8261,8 +8285,88 @@ class PipelineService extends EventEmitter { } } + // For non-DVD sources (or when HandBrake scan failed), run mediainfo on each file. + if (!isDvdSource || !dvdHandBrakeScanJson) { + for (let i = 0; i < mediaFiles.length; i += 1) { + const file = mediaFiles[i]; + const percent = Number((((i + 1) / mediaFiles.length) * 100).toFixed(2)); + await this.updateProgress('MEDIAINFO_CHECK', percent, null, `Mediainfo ${i + 1}/${mediaFiles.length}: ${path.basename(file.path)}`, jobId); + + const result = await this.runMediainfoForFile(jobId, file.path, { + mediaProfile, + settingsMap: settings + }); + let parsedMediaInfo = result.parsed; + let fallbackRunInfo = null; + if (shouldRunDvdTrackFallback(parsedMediaInfo, mediaProfile, file.path)) { + try { + const fallback = await this.runDvdTrackFallbackForFile(jobId, file.path, { + mediaProfile, + settingsMap: settings + }); + if (fallback?.parsedMediaInfo) { + parsedMediaInfo = fallback.parsedMediaInfo; + fallbackRunInfo = fallback.runInfo || null; + const audioCount = Array.isArray(fallback?.titleInfo?.audioTracks) + ? fallback.titleInfo.audioTracks.length + : 0; + const subtitleCount = Array.isArray(fallback?.titleInfo?.subtitleTracks) + ? fallback.titleInfo.subtitleTracks.length + : 0; + await historyService.appendLog( + jobId, + 'SYSTEM', + `DVD Track-Fallback aktiv (${path.basename(file.path)}): Audio=${audioCount}, Subtitle=${subtitleCount}.` + ); + } else { + await historyService.appendLog( + jobId, + 'SYSTEM', + `DVD Track-Fallback ohne Ergebnis (${path.basename(file.path)}).` + ); + } + } catch (error) { + logger.warn('mediainfo:track-fallback:failed', { + jobId, + inputPath: file.path, + error: errorToMeta(error) + }); + } + } + + mediaInfoByPath[file.path] = parsedMediaInfo; + mediaInfoRuns.push({ + filePath: file.path, + runInfo: result.runInfo, + fallbackRunInfo + }); + + const partialReview = buildReviewSnapshot(i + 1); + if (this.isPrimaryJob(jobId)) { + await this.setState('MEDIAINFO_CHECK', { + activeJobId: jobId, + progress: percent, + eta: null, + statusText: `Mediainfo ${i + 1}/${mediaFiles.length} analysiert: ${path.basename(file.path)}`, + context: { + jobId, + rawPath, + inputPath: partialReview?.encodeInputPath || null, + hasEncodableTitle: Boolean(partialReview?.encodeInputPath), + reviewConfirmed: false, + mode: options.mode || 'rip', + mediaProfile, + sourceJobId: options.sourceJobId || null, + mediaInfoReview: partialReview, + selectedMetadata + } + }); + } + } + } + const review = buildMediainfoReview({ - mediaFiles, + mediaFiles: effectiveMediaFiles, mediaInfoByPath, settings, presetProfile, @@ -8279,8 +8383,8 @@ class PipelineService extends EventEmitter { sourceJobId: options.sourceJobId || null, reviewConfirmed: false, partial: false, - processedFiles: mediaFiles.length, - totalFiles: mediaFiles.length + processedFiles: effectiveMediaFiles.length, + totalFiles: effectiveMediaFiles.length }; const reviewPrefillResult = applyPreviousSelectionDefaultsToReviewPlan( enrichedReview, @@ -8295,7 +8399,10 @@ class PipelineService extends EventEmitter { || rawMedia.source === 'dvd_image' || rawMedia.source === 'single_extensionless'; if (needsHandBrakeTitleScan) { - const dvdScanInputPath = rawMedia.source === 'dvd' ? rawPath : mediaFiles[0].path; + // Use the same path that was used for the DVD HandBrake scan above, or fall back + // to the existing logic for cases where the scan was not yet performed. + const dvdScanInputPath = dvdHandBrakeScanInputPath + || (rawMedia.source === 'dvd' ? rawPath : mediaFiles[0].path); const preSelectedHandBrakeTitleId = normalizeReviewTitleId(options?.selectedHandBrakeTitleId); if (preSelectedHandBrakeTitleId) { enrichedReview = { @@ -8309,33 +8416,38 @@ class PipelineService extends EventEmitter { `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')); + // 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')); + } 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 f3b2855..9618053 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.10.2-12", + "version": "0.10.2-13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.10.2-12", + "version": "0.10.2-13", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 9aa1302..69bbaa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.10.2-12", + "version": "0.10.2-13", "private": true, "type": "module", "scripts": { diff --git a/package-lock.json b/package-lock.json index ba48471..ada0866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster", - "version": "0.10.2-12", + "version": "0.10.2-13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster", - "version": "0.10.2-12", + "version": "0.10.2-13", "devDependencies": { "concurrently": "^9.1.2" } diff --git a/package.json b/package.json index bbcdac6..5798de5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ripster", "private": true, - "version": "0.10.2-12", + "version": "0.10.2-13", "scripts": { "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev:backend": "npm run dev --prefix backend",