diff --git a/Bildschirmfoto 2026-04-12 um 20.43.33.png b/Bildschirmfoto 2026-04-12 um 20.43.33.png deleted file mode 100644 index a770fac..0000000 Binary files a/Bildschirmfoto 2026-04-12 um 20.43.33.png and /dev/null differ diff --git a/Bildschirmfoto 2026-04-12 um 20.45.16.png b/Bildschirmfoto 2026-04-12 um 20.45.16.png deleted file mode 100644 index 5d20187..0000000 Binary files a/Bildschirmfoto 2026-04-12 um 20.45.16.png and /dev/null differ diff --git a/Bildschirmfoto 2026-04-12 um 21.03.46.png b/Bildschirmfoto 2026-04-12 um 21.03.46.png deleted file mode 100644 index 098f1b9..0000000 Binary files a/Bildschirmfoto 2026-04-12 um 21.03.46.png and /dev/null differ diff --git a/Bildschirmfoto 2026-04-12 um 21.11.19.png b/Bildschirmfoto 2026-04-12 um 21.11.19.png deleted file mode 100644 index a8143aa..0000000 Binary files a/Bildschirmfoto 2026-04-12 um 21.11.19.png and /dev/null differ diff --git a/backend/package-lock.json b/backend/package-lock.json index 5c846ef..02c8d2b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.13.1-6", + "version": "0.13.1-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.13.1-6", + "version": "0.13.1-7", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 48d88d9..1dfac8e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.13.1-6", + "version": "0.13.1-7", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/services/historyService.js b/backend/src/services/historyService.js index bc91b5a..052ec17 100644 --- a/backend/src/services/historyService.js +++ b/backend/src/services/historyService.js @@ -2356,6 +2356,14 @@ function normalizeJobIdValue(value) { return Math.trunc(parsed); } +function isSeriesContainerRow(row) { + return String(row?.job_kind || '').trim().toLowerCase() === 'dvd_series_container'; +} + +function isSeriesChildRow(row) { + return String(row?.job_kind || '').trim().toLowerCase() === 'dvd_series_child'; +} + function normalizeArchiveTarget(value) { const raw = String(value || '').trim().toLowerCase(); if (raw === 'raw') { @@ -4771,6 +4779,11 @@ class HistoryService { if (!entry.isDirectory() || isHiddenDirectoryName(entry.name)) { continue; } + if (/^incomplete_/i.test(String(entry.name || '').trim())) { + // Incomplete RAW folders represent unfinished rips and are intentionally + // not re-importable from /database. + continue; + } const rawPath = path.join(rawDir, entry.name); const normalizedPath = normalizeComparablePath(rawPath); @@ -5765,6 +5778,12 @@ class HistoryService { const pending = [normalizedJobId]; const visited = new Set(); + const parentRow = normalizeJobIdValue(primary?.parent_job_id) !== null + ? byId.get(normalizeJobIdValue(primary?.parent_job_id)) + : null; + const isPrimarySeriesChild = isSeriesChildRow(primary) + || (parentRow && isSeriesContainerRow(parentRow)); + const isPrimarySeriesContainer = isSeriesContainerRow(primary); const enqueue = (value) => { const id = normalizeJobIdValue(value); if (!id || visited.has(id)) { @@ -5785,8 +5804,10 @@ class HistoryService { continue; } - enqueue(row.parent_job_id); - enqueue(parseSourceJobIdFromPlan(row.encode_plan_json)); + if (!(isPrimarySeriesChild || isPrimarySeriesContainer)) { + enqueue(row.parent_job_id); + enqueue(parseSourceJobIdFromPlan(row.encode_plan_json)); + } for (const childId of (childrenByParent.get(currentId) || [])) { enqueue(childId); @@ -5795,7 +5816,7 @@ class HistoryService { enqueue(childId); } - if (includeLogLinks) { + if (includeLogLinks && !(isPrimarySeriesChild || isPrimarySeriesContainer)) { try { const processLog = await this.readProcessLogLines(currentId, { includeAll: true }); const linkedJobIds = parseRetryLinkedJobIdsFromLogLines(processLog.lines); @@ -5808,6 +5829,25 @@ class HistoryService { } } + if (isPrimarySeriesChild && parentRow && isSeriesContainerRow(parentRow)) { + const parentId = normalizeJobIdValue(parentRow?.id); + if (parentId) { + const remainingChildren = rows.filter((row) => { + const rowId = normalizeJobIdValue(row?.id); + if (!rowId) { + return false; + } + if (normalizeJobIdValue(row?.parent_job_id) !== parentId) { + return false; + } + return !visited.has(rowId); + }); + if (remainingChildren.length === 0) { + visited.add(parentId); + } + } + } + return Array.from(visited) .map((id) => byId.get(id)) .filter(Boolean) @@ -6849,16 +6889,23 @@ class HistoryService { const includeRelated = Boolean(options?.includeRelated); if (!includeRelated) { - const existing = await this.getJobById(jobId); + const resolved = await this.getJobByIdOrReplacement(jobId); + const existing = resolved?.job || null; + const normalizedJobId = normalizeJobIdValue(resolved?.resolvedJobId || jobId); if (!existing) { const error = new Error('Job nicht gefunden.'); error.statusCode = 404; throw error; } + if (!normalizedJobId) { + const error = new Error('Ungültige Job-ID.'); + error.statusCode = 400; + throw error; + } let fileSummary = null; if (fileTarget !== 'none') { - const preview = await this.getJobDeletePreview(jobId, { includeRelated: false }); + const preview = await this.getJobDeletePreview(normalizedJobId, { includeRelated: false }); fileSummary = this._deletePathsFromPreview(preview, fileTarget, { selectedMoviePaths: options?.selectedMoviePaths }); @@ -6869,7 +6916,7 @@ class HistoryService { 'SELECT state, active_job_id FROM pipeline_state WHERE id = 1' ); - const isActivePipelineJob = Number(pipelineRow?.active_job_id || 0) === Number(jobId); + const isActivePipelineJob = Number(pipelineRow?.active_job_id || 0) === Number(normalizedJobId); const runningStates = new Set(['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']); if (isActivePipelineJob && runningStates.has(String(pipelineRow?.state || ''))) { @@ -6904,23 +6951,23 @@ class HistoryService { updated_at = CURRENT_TIMESTAMP WHERE id = 1 AND active_job_id = ? `, - [jobId] + [normalizedJobId] ); } - await db.run('DELETE FROM jobs WHERE id = ?', [jobId]); + await db.run('DELETE FROM jobs WHERE id = ?', [normalizedJobId]); await db.exec('COMMIT'); } catch (error) { await db.exec('ROLLBACK'); throw error; } - await this.closeProcessLog(jobId); - this._deleteProcessLogFile(jobId); - thumbnailService.deleteThumbnail(jobId); + await this.closeProcessLog(normalizedJobId); + this._deleteProcessLogFile(normalizedJobId); + thumbnailService.deleteThumbnail(normalizedJobId); logger.warn('job:deleted', { - jobId, + jobId: normalizedJobId, fileTarget, includeRelated: false, pipelineStateReset: isActivePipelineJob, @@ -6934,15 +6981,16 @@ class HistoryService { return { deleted: true, - jobId, + jobId: normalizedJobId, fileTarget, includeRelated: false, - deletedJobIds: [Number(jobId)], + deletedJobIds: [Number(normalizedJobId)], fileSummary }; } - const normalizedJobId = normalizeJobIdValue(jobId); + const resolved = await this.getJobByIdOrReplacement(jobId); + const normalizedJobId = normalizeJobIdValue(resolved?.resolvedJobId || jobId); const preview = await this.getJobDeletePreview(normalizedJobId, { includeRelated: true }); const deleteJobIds = Array.isArray(preview?.relatedJobs) ? preview.relatedJobs diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index b850f52..5b110a8 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -20984,7 +20984,7 @@ class PipelineService extends EventEmitter { const shouldAutoRecoverEncode = normalizedStage === 'ENCODING' && hasConfirmedPlan - && (!isCancelled || resolvedMediaProfile === 'converter'); + && resolvedMediaProfile !== 'audiobook'; if (shouldAutoRecoverEncode) { try { const recoveryReasonLabel = isCancelled ? 'Abbruch' : 'Fehler'; @@ -22782,7 +22782,13 @@ class PipelineService extends EventEmitter { ); newRawPath = nextRaw.rawDir; } else { - const rawBaseDir = path.dirname(currentRawPath); + const rawStorage = resolveSeriesAwareRawStorage( + settings || {}, + mediaProfile, + selectedMetadata, + mkInfo?.analyzeContext || null + ); + const rawBaseDir = String(rawStorage?.rawBaseDir || '').trim() || path.dirname(currentRawPath); const newMetadataBase = buildRawMetadataBase({ title: job.title || job.detected_title || null, year: job.year || null, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9147818..eabc805 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.13.1-6", + "version": "0.13.1-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.13.1-6", + "version": "0.13.1-7", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 5d82deb..d80fa41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.13.1-6", + "version": "0.13.1-7", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/ConverterJobCard.jsx b/frontend/src/components/ConverterJobCard.jsx index f298ffc..fa45460 100644 --- a/frontend/src/components/ConverterJobCard.jsx +++ b/frontend/src/components/ConverterJobCard.jsx @@ -940,9 +940,9 @@ function InlineConfig({ job, plan, onStarted, onDeleted, onCancelled, onInputsCh {isVideo && (