0.16.1-6 Detection Fix

This commit is contained in:
2026-05-02 19:31:01 +00:00
parent 6fbc5395a1
commit 024a6305e2
10 changed files with 342 additions and 35 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.16.1-5", "version": "0.16.1-6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.16.1-5", "version": "0.16.1-6",
"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.16.1-5", "version": "0.16.1-6",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+8 -1
View File
@@ -1049,7 +1049,14 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed =
const metadataKind = String(selectedMetadata?.metadataKind || '').trim().toLowerCase(); const metadataKind = String(selectedMetadata?.metadataKind || '').trim().toLowerCase();
const normalizedJobKind = String(job?.job_kind || '').trim().toLowerCase(); const normalizedJobKind = String(job?.job_kind || '').trim().toLowerCase();
const hasSeriesJobKind = normalizedJobKind === 'dvd_series_child' || normalizedJobKind === 'dvd_series_container'; const hasSeriesJobKind = normalizedJobKind === 'dvd_series_child' || normalizedJobKind === 'dvd_series_container';
const hasSeriesParent = normalizeJobIdValue(job?.parent_job_id) !== null; const hasParentJob = normalizeJobIdValue(job?.parent_job_id) !== null;
const isMultipartJobKind = (
normalizedJobKind === 'multipart_movie_container'
|| normalizedJobKind === 'multipart_movie_child'
|| normalizedJobKind === 'multipart_movie_merge'
);
// Parent-ID alone is not a reliable series signal: multipart movie jobs also use parent_job_id.
const hasSeriesParent = hasParentJob && !isMultipartJobKind;
const hasSeriesRawPathHint = isLikelySeriesRawPath(job?.raw_path, settings || {}); const hasSeriesRawPathHint = isLikelySeriesRawPath(job?.raw_path, settings || {});
const hasSeriesMetadataHint = hasSeriesMetadataSignals( const hasSeriesMetadataHint = hasSeriesMetadataSignals(
selectedMetadata, selectedMetadata,
+221 -13
View File
@@ -4023,12 +4023,45 @@ function pickScanMainFeatureTitleId(scanJson) {
return null; return null;
} }
function normalizePlaylistAliasIds(values, excludePlaylistId = null) {
const exclude = normalizePlaylistId(excludePlaylistId);
const rows = Array.isArray(values) ? values : [];
const seen = new Set();
const aliases = [];
for (const value of rows) {
const playlistId = normalizePlaylistId(value);
if (!playlistId || playlistId === exclude || seen.has(playlistId)) {
continue;
}
seen.add(playlistId);
aliases.push(playlistId);
}
return aliases;
}
function getPlaylistAliasesForPlaylist(playlistAnalysis, playlistIdRaw) {
const playlistId = normalizePlaylistId(playlistIdRaw);
if (!playlistId || !playlistAnalysis || typeof playlistAnalysis !== 'object') {
return [];
}
const aliasMap = playlistAnalysis?.playlistAliasMap
&& typeof playlistAnalysis.playlistAliasMap === 'object'
? playlistAnalysis.playlistAliasMap
: null;
if (!aliasMap) {
return [];
}
return normalizePlaylistAliasIds(aliasMap[playlistId], playlistId);
}
function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) { function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) {
const playlistId = normalizePlaylistId(playlistIdRaw); const playlistId = normalizePlaylistId(playlistIdRaw);
const playlistAliases = getPlaylistAliasesForPlaylist(playlistAnalysis, playlistId);
if (!playlistId || !playlistAnalysis) { if (!playlistId || !playlistAnalysis) {
return { return {
playlistId: playlistId || null, playlistId: playlistId || null,
playlistFile: playlistId ? `${playlistId}.mpls` : null, playlistFile: playlistId ? `${playlistId}.mpls` : null,
playlistAliases,
recommended: false, recommended: false,
evaluationLabel: null, evaluationLabel: null,
segmentCommand: playlistId ? `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts` : null, segmentCommand: playlistId ? `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts` : null,
@@ -4050,6 +4083,7 @@ function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) {
return { return {
playlistId, playlistId,
playlistFile: `${playlistId}.mpls`, playlistFile: `${playlistId}.mpls`,
playlistAliases,
recommended, recommended,
evaluationLabel: evaluated?.evaluationLabel || (recommended ? 'wahrscheinlich korrekt (Heuristik)' : null), evaluationLabel: evaluated?.evaluationLabel || (recommended ? 'wahrscheinlich korrekt (Heuristik)' : null),
segmentCommand: segmentEntry?.segmentCommand || `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts`, segmentCommand: segmentEntry?.segmentCommand || `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts`,
@@ -4322,8 +4356,13 @@ function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw, options = {
? Math.trunc(durationToleranceRaw) ? Math.trunc(durationToleranceRaw)
: 5; : 5;
const playlistAliases = normalizePlaylistAliasIds(options?.playlistAliases, playlistId);
const candidatePlaylistIds = [playlistId, ...playlistAliases];
const candidatePlaylistSet = new Set(candidatePlaylistIds);
const rows = buildHandBrakeScanTitleRows(scanJson); const rows = buildHandBrakeScanTitleRows(scanJson);
const matches = rows.filter((item) => item.playlist === playlistId); const directMatches = rows.filter((item) => item.playlist === playlistId);
const matches = rows.filter((item) => candidatePlaylistSet.has(item.playlist));
const scoreForExpected = (row) => { const scoreForExpected = (row) => {
const durationDelta = expectedDurationSeconds !== null const durationDelta = expectedDurationSeconds !== null
@@ -4352,15 +4391,36 @@ function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw, options = {
if (matches.length > 0) { if (matches.length > 0) {
if (expectedDurationSeconds !== null || expectedSizeBytes !== null) { if (expectedDurationSeconds !== null || expectedSizeBytes !== null) {
const scored = matches.map(scoreForExpected).sort(sortByExpectedScore); const scored = matches.map(scoreForExpected).sort(sortByExpectedScore);
const scoredDirect = directMatches.map(scoreForExpected).sort(sortByExpectedScore);
if (expectedDurationSeconds !== null) { if (expectedDurationSeconds !== null) {
const withinTolerance = scored.filter((item) => item.durationDelta <= durationToleranceSeconds); const directWithinTolerance = scoredDirect.filter((item) => item.durationDelta <= durationToleranceSeconds);
if (withinTolerance.length > 0) { if (directWithinTolerance.length > 0) {
return withinTolerance[0].row.handBrakeTitleId; return directWithinTolerance[0].row.handBrakeTitleId;
} }
const aliasWithinTolerance = scored.filter((item) =>
item.durationDelta <= durationToleranceSeconds
&& normalizePlaylistId(item?.row?.playlist) !== playlistId
);
if (aliasWithinTolerance.length > 0) {
return aliasWithinTolerance[0].row.handBrakeTitleId;
}
}
if (scoredDirect.length > 0) {
return scoredDirect[0].row.handBrakeTitleId;
} }
return scored[0].row.handBrakeTitleId; return scored[0].row.handBrakeTitleId;
} }
const best = matches.sort((a, b) => const directBest = directMatches
.slice()
.sort((a, b) =>
b.durationSeconds - a.durationSeconds
|| b.sizeBytes - a.sizeBytes
|| a.handBrakeTitleId - b.handBrakeTitleId
)[0];
if (directBest) {
return directBest.handBrakeTitleId;
}
const best = matches.slice().sort((a, b) =>
b.durationSeconds - a.durationSeconds b.durationSeconds - a.durationSeconds
|| b.sizeBytes - a.sizeBytes || b.sizeBytes - a.sizeBytes
|| a.handBrakeTitleId - b.handBrakeTitleId || a.handBrakeTitleId - b.handBrakeTitleId
@@ -4416,8 +4476,12 @@ function isHandBrakePlaylistCacheEntryCompatible(entry, playlistIdRaw, options =
return false; return false;
} }
const allowedPlaylistIds = new Set([
playlistId,
...normalizePlaylistAliasIds(options?.playlistAliases, playlistId)
]);
const cachedPlaylistId = normalizePlaylistId(titleInfo?.playlistId || null); const cachedPlaylistId = normalizePlaylistId(titleInfo?.playlistId || null);
if (cachedPlaylistId && cachedPlaylistId !== playlistId) { if (cachedPlaylistId && !allowedPlaylistIds.has(cachedPlaylistId)) {
return false; return false;
} }
@@ -6001,6 +6065,7 @@ function buildDiscScanReview({
eligibleForEncode: title.selectedByMinLength, eligibleForEncode: title.selectedByMinLength,
playlistId: title.playlistMatch?.playlistId || null, playlistId: title.playlistMatch?.playlistId || null,
playlistFile: title.playlistMatch?.playlistFile || null, playlistFile: title.playlistMatch?.playlistFile || null,
playlistAliases: normalizePlaylistAliasIds(title.playlistMatch?.playlistAliases, title.playlistMatch?.playlistId),
playlistRecommended: Boolean(title.playlistMatch?.recommended), playlistRecommended: Boolean(title.playlistMatch?.recommended),
playlistEvaluationLabel: title.playlistMatch?.evaluationLabel || null, playlistEvaluationLabel: title.playlistMatch?.evaluationLabel || null,
playlistSegmentCommand: title.playlistMatch?.segmentCommand || null, playlistSegmentCommand: title.playlistMatch?.segmentCommand || null,
@@ -6529,6 +6594,9 @@ function buildPlaylistCandidates(playlistAnalysis) {
const segmentMap = playlistAnalysis?.playlistSegments && typeof playlistAnalysis.playlistSegments === 'object' const segmentMap = playlistAnalysis?.playlistSegments && typeof playlistAnalysis.playlistSegments === 'object'
? playlistAnalysis.playlistSegments ? playlistAnalysis.playlistSegments
: {}; : {};
const aliasMap = playlistAnalysis?.playlistAliasMap && typeof playlistAnalysis.playlistAliasMap === 'object'
? playlistAnalysis.playlistAliasMap
: {};
return rawList return rawList
.map((playlistId) => normalizePlaylistId(playlistId)) .map((playlistId) => normalizePlaylistId(playlistId))
@@ -6536,6 +6604,12 @@ function buildPlaylistCandidates(playlistAnalysis) {
.map((playlistId) => { .map((playlistId) => {
const source = sourceRows.find((item) => normalizePlaylistId(item?.playlistId || item?.playlistFile) === playlistId) || null; const source = sourceRows.find((item) => normalizePlaylistId(item?.playlistId || item?.playlistFile) === playlistId) || null;
const segmentEntry = segmentMap[playlistId] || segmentMap[`${playlistId}.mpls`] || null; const segmentEntry = segmentMap[playlistId] || segmentMap[`${playlistId}.mpls`] || null;
const playlistAliases = normalizePlaylistAliasIds(
Array.isArray(source?.playlistAliases) && source.playlistAliases.length > 0
? source.playlistAliases
: aliasMap[playlistId],
playlistId
);
const score = Number(source?.score); const score = Number(source?.score);
const sequenceCoherence = Number(source?.structuralMetrics?.sequenceCoherence); const sequenceCoherence = Number(source?.structuralMetrics?.sequenceCoherence);
const titleId = Number(source?.titleId ?? source?.id); const titleId = Number(source?.titleId ?? source?.id);
@@ -6584,6 +6658,7 @@ function buildPlaylistCandidates(playlistAnalysis) {
return { return {
playlistId, playlistId,
playlistFile: toPlaylistFile(playlistId), playlistFile: toPlaylistFile(playlistId),
playlistAliases,
titleId: Number.isFinite(titleId) ? Math.trunc(titleId) : null, titleId: Number.isFinite(titleId) ? Math.trunc(titleId) : null,
durationSeconds, durationSeconds,
durationLabel: durationLabel || null, durationLabel: durationLabel || null,
@@ -6747,7 +6822,8 @@ function buildHandBrakePlaylistScanCache(scanJson, playlistCandidates = [], rawP
candidateMetaByPlaylist.set(playlistId, { candidateMetaByPlaylist.set(playlistId, {
expectedMakemkvTitleId: normalizeNonNegativeInteger(row?.titleId), expectedMakemkvTitleId: normalizeNonNegativeInteger(row?.titleId),
expectedDurationSeconds: Number(row?.durationSeconds || 0) || null, expectedDurationSeconds: Number(row?.durationSeconds || 0) || null,
expectedSizeBytes: Number(row?.sizeBytes || 0) || null expectedSizeBytes: Number(row?.sizeBytes || 0) || null,
playlistAliases: normalizePlaylistAliasIds(row?.playlistAliases, playlistId)
}); });
} }
@@ -11238,7 +11314,10 @@ class PipelineService extends EventEmitter {
const queueIds = new Set(normalizedIds.map((value) => String(value))); const queueIds = new Set(normalizedIds.map((value) => String(value)));
const previousQueueLength = this.queueEntries.length; const previousQueueLength = this.queueEntries.length;
this.queueEntries = this.queueEntries.filter((entry) => !queueIds.has(String(Number(entry?.jobId || 0)))); this.queueEntries = this.queueEntries.filter((entry) => !queueIds.has(String(Number(entry?.jobId || 0))));
const removedQueueEntries = previousQueueLength - this.queueEntries.length; let removedQueueEntries = Math.max(0, previousQueueLength - this.queueEntries.length);
for (const deletedJobId of normalizedIds) {
removedQueueEntries += this.removeDetachedQueueAutomationEntriesForJob(deletedJobId);
}
for (const jobId of normalizedIds) { for (const jobId of normalizedIds) {
this.cancelRequestedByJob.add(jobId); this.cancelRequestedByJob.add(jobId);
@@ -14484,6 +14563,41 @@ class PipelineService extends EventEmitter {
return this.queueEntries.findIndex((entry) => Number(entry?.jobId) === Number(jobId)); return this.queueEntries.findIndex((entry) => Number(entry?.jobId) === Number(jobId));
} }
removeDetachedQueueAutomationEntriesForJob(jobId, options = {}) {
const normalizedJobId = this.normalizeQueueJobId(jobId);
if (!normalizedJobId) {
return 0;
}
const allowedPhases = Array.isArray(options?.phases)
? new Set(
options.phases
.map((value) => String(value || '').trim().toLowerCase())
.filter((value) => value === 'pre_encode' || value === 'post_encode')
)
: null;
const hasPhaseFilter = Boolean(allowedPhases && allowedPhases.size > 0);
const queueLengthBefore = this.queueEntries.length;
this.queueEntries = this.queueEntries.filter((entry) => {
const entryType = String(entry?.type || '').trim().toLowerCase();
if (entryType !== 'script' && entryType !== 'chain') {
return true;
}
const sourceJobId = this.normalizeQueueJobId(entry?.sourceJobId);
if (sourceJobId !== normalizedJobId) {
return true;
}
if (!hasPhaseFilter) {
return false;
}
const sourcePhase = String(entry?.sourcePhase || '').trim().toLowerCase();
return !allowedPhases.has(sourcePhase);
});
return Math.max(0, queueLengthBefore - this.queueEntries.length);
}
normalizeQueueChainIdList(rawList) { normalizeQueueChainIdList(rawList) {
const list = Array.isArray(rawList) ? rawList : []; const list = Array.isArray(rawList) ? rawList : [];
const seen = new Set(); const seen = new Set();
@@ -15607,6 +15721,15 @@ class PipelineService extends EventEmitter {
'SYSTEM', 'SYSTEM',
`Queue-Start fehlgeschlagen (${QUEUE_ACTION_LABELS[entry.action] || entry.action}): ${error.message}` `Queue-Start fehlgeschlagen (${QUEUE_ACTION_LABELS[entry.action] || entry.action}): ${error.message}`
); );
const removedDetachedEntries = this.removeDetachedQueueAutomationEntriesForJob(entry.jobId);
if (removedDetachedEntries > 0) {
await historyService.appendLog(
entry.jobId,
'SYSTEM',
`Queue-Cleanup: ${removedDetachedEntries} verknüpfte Skript/Ketten-Eintrag${removedDetachedEntries === 1 ? '' : 'e'} entfernt.`
);
await this.emitQueueChanged();
}
} }
} }
} }
@@ -18655,13 +18778,21 @@ class PipelineService extends EventEmitter {
const cachedHandBrakePlaylistEntry = getCachedHandBrakePlaylistEntry(handBrakePlaylistScan, resolvedPlaylistId); const cachedHandBrakePlaylistEntry = getCachedHandBrakePlaylistEntry(handBrakePlaylistScan, resolvedPlaylistId);
const expectedDurationForCache = Number(selectedTitleFromAnalysis?.durationSeconds || 0) || null; const expectedDurationForCache = Number(selectedTitleFromAnalysis?.durationSeconds || 0) || null;
const expectedSizeForCache = Number(selectedTitleFromAnalysis?.sizeBytes || 0) || null; const expectedSizeForCache = Number(selectedTitleFromAnalysis?.sizeBytes || 0) || null;
const playlistAliasesForResolve = normalizePlaylistAliasIds(
[
...(Array.isArray(selectedTitleFromAnalysis?.playlistAliases) ? selectedTitleFromAnalysis.playlistAliases : []),
...getPlaylistAliasesForPlaylist(playlistAnalysis, resolvedPlaylistId)
],
resolvedPlaylistId
);
const hasCachedHandBrakeEntry = Boolean( const hasCachedHandBrakeEntry = Boolean(
isHandBrakePlaylistCacheEntryCompatible( isHandBrakePlaylistCacheEntryCompatible(
cachedHandBrakePlaylistEntry, cachedHandBrakePlaylistEntry,
resolvedPlaylistId, resolvedPlaylistId,
{ {
expectedDurationSeconds: expectedDurationForCache, expectedDurationSeconds: expectedDurationForCache,
expectedSizeBytes: expectedSizeForCache expectedSizeBytes: expectedSizeForCache,
playlistAliases: playlistAliasesForResolve
} }
) )
); );
@@ -18744,7 +18875,8 @@ class PipelineService extends EventEmitter {
resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId, { resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId, {
expectedMakemkvTitleId: selectedTitleForReview, expectedMakemkvTitleId: selectedTitleForReview,
expectedDurationSeconds: expectedDurationForCache, expectedDurationSeconds: expectedDurationForCache,
expectedSizeBytes: expectedSizeForCache expectedSizeBytes: expectedSizeForCache,
playlistAliases: playlistAliasesForResolve
}); });
if (!resolvedHandBrakeTitleId) { if (!resolvedHandBrakeTitleId) {
const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson); const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson);
@@ -18776,7 +18908,8 @@ class PipelineService extends EventEmitter {
titleInfo: reviewTitleInfo titleInfo: reviewTitleInfo
}, resolvedPlaylistId, { }, resolvedPlaylistId, {
expectedDurationSeconds: expectedDurationForCache, expectedDurationSeconds: expectedDurationForCache,
expectedSizeBytes: expectedSizeForCache expectedSizeBytes: expectedSizeForCache,
playlistAliases: playlistAliasesForResolve
})) { })) {
const error = new Error( const error = new Error(
`HandBrake Titel-Mapping inkonsistent für ${toPlaylistFile(resolvedPlaylistId)} (-t ${resolvedHandBrakeTitleId}).` `HandBrake Titel-Mapping inkonsistent für ${toPlaylistFile(resolvedPlaylistId)} (-t ${resolvedHandBrakeTitleId}).`
@@ -18880,6 +19013,13 @@ class PipelineService extends EventEmitter {
const normalizedTitles = (Array.isArray(review.titles) ? review.titles : []) const normalizedTitles = (Array.isArray(review.titles) ? review.titles : [])
.slice(0, 1) .slice(0, 1)
.map((title) => { .map((title) => {
const normalizedHandBrakeTitleId = normalizeReviewTitleId(
resolvedHandBrakeTitleId
?? title?.handBrakeTitleId
?? title?.titleIndex
?? title?.id
?? selectedTitleForReview
);
const durationSeconds = Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0); const durationSeconds = Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0);
const eligibleForEncode = durationSeconds >= minLengthSecondsForReview; const eligibleForEncode = durationSeconds >= minLengthSecondsForReview;
const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => { const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => {
@@ -18995,6 +19135,8 @@ class PipelineService extends EventEmitter {
return { return {
...title, ...title,
id: normalizedHandBrakeTitleId || title?.id || null,
handBrakeTitleId: normalizedHandBrakeTitleId || null,
filePath: rawPath, filePath: rawPath,
fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`, fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
durationSeconds, durationSeconds,
@@ -25141,6 +25283,25 @@ class PipelineService extends EventEmitter {
); );
const expectedDurationSecondsForResolve = Number(selectedEncodeTitle?.durationSeconds || 0) || null; const expectedDurationSecondsForResolve = Number(selectedEncodeTitle?.durationSeconds || 0) || null;
const expectedSizeBytesForResolve = Number(selectedEncodeTitle?.sizeBytes || 0) || null; const expectedSizeBytesForResolve = Number(selectedEncodeTitle?.sizeBytes || 0) || null;
const encodePlanPlaylistAnalysis = encodePlan?.playlistAnalysis && typeof encodePlan.playlistAnalysis === 'object'
? encodePlan.playlistAnalysis
: null;
const selectedPlaylistAliasesForResolve = selectedPlaylistId
? normalizePlaylistAliasIds(
[
...(Array.isArray(selectedEncodeTitle?.playlistAliases) ? selectedEncodeTitle.playlistAliases : []),
...getPlaylistAliasesForPlaylist(
encodePlanPlaylistAnalysis
|| playlistDecision?.playlistAnalysis
|| this.snapshot.context?.playlistAnalysis
|| this.safeParseJson(job?.makemkv_info_json)?.analyzeContext?.playlistAnalysis
|| null,
selectedPlaylistId
)
],
selectedPlaylistId
)
: [];
if (!handBrakeTitleId && selectedPlaylistId) { if (!handBrakeTitleId && selectedPlaylistId) {
const titleResolveScanLines = []; const titleResolveScanLines = [];
const titleResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, { const titleResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, {
@@ -25172,7 +25333,8 @@ class PipelineService extends EventEmitter {
handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(titleResolveParsed, selectedPlaylistId, { handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(titleResolveParsed, selectedPlaylistId, {
expectedMakemkvTitleId: expectedMakemkvTitleIdForResolve, expectedMakemkvTitleId: expectedMakemkvTitleIdForResolve,
expectedDurationSeconds: expectedDurationSecondsForResolve, expectedDurationSeconds: expectedDurationSecondsForResolve,
expectedSizeBytes: expectedSizeBytesForResolve expectedSizeBytes: expectedSizeBytesForResolve,
playlistAliases: selectedPlaylistAliasesForResolve
}); });
if (!handBrakeTitleId) { if (!handBrakeTitleId) {
const knownPlaylists = listAvailableHandBrakePlaylists(titleResolveParsed); const knownPlaylists = listAvailableHandBrakePlaylists(titleResolveParsed);
@@ -25235,6 +25397,29 @@ class PipelineService extends EventEmitter {
} }
} }
} }
if (!handBrakeTitleId) {
const primaryPlanTitleId = normalizeReviewTitleId(encodePlan?.encodeInputTitleId);
const primaryPlanTitle = Array.isArray(encodePlan?.titles)
? (
encodePlan.titles.find((title) => normalizeReviewTitleId(title?.id) === primaryPlanTitleId)
|| encodePlan.titles.find((title) => Boolean(title?.selectedForEncode || title?.encodeInput))
|| null
)
: null;
const primaryTitleMappedHandBrakeId = normalizeReviewTitleId(
primaryPlanTitle?.handBrakeTitleId
?? primaryPlanTitle?.titleIndex
?? null
);
if (primaryTitleMappedHandBrakeId) {
handBrakeTitleId = primaryTitleMappedHandBrakeId;
await historyService.appendLog(
jobId,
'SYSTEM',
`HandBrake Titel-ID aus Primär-Titel übernommen: -t ${handBrakeTitleId}`
);
}
}
if (!handBrakeTitleId) { if (!handBrakeTitleId) {
const fallbackTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId); const fallbackTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId);
if (fallbackTitleId) { if (fallbackTitleId) {
@@ -27403,7 +27588,9 @@ class PipelineService extends EventEmitter {
const queueEntriesBefore = this.queueEntries.length; const queueEntriesBefore = this.queueEntries.length;
this.queueEntries = this.queueEntries.filter((entry) => { this.queueEntries = this.queueEntries.filter((entry) => {
const action = String(entry?.action || '').trim().toUpperCase(); const action = String(entry?.action || '').trim().toUpperCase();
const entryType = String(entry?.type || 'job').trim().toLowerCase();
const entryJobId = Number(entry?.jobId); const entryJobId = Number(entry?.jobId);
const entrySourceJobId = this.normalizeQueueJobId(entry?.sourceJobId);
const isLegacyParentEntry = ( const isLegacyParentEntry = (
entryJobId === Number(normalizedParentJobId) entryJobId === Number(normalizedParentJobId)
&& action === QUEUE_ACTIONS.START_SERIES_EPISODE && action === QUEUE_ACTIONS.START_SERIES_EPISODE
@@ -27414,7 +27601,15 @@ class PipelineService extends EventEmitter {
&& entryJobId > 0 && entryJobId > 0
&& seriesChildJobIdSet.has(entryJobId) && seriesChildJobIdSet.has(entryJobId)
); );
return !(isLegacyParentEntry || isSeriesChildJobEntry); const isDetachedSeriesAutomationEntry = (
(entryType === 'script' || entryType === 'chain')
&& entrySourceJobId
&& (
Number(entrySourceJobId) === Number(normalizedParentJobId)
|| seriesChildJobIdSet.has(Number(entrySourceJobId))
)
);
return !(isLegacyParentEntry || isSeriesChildJobEntry || isDetachedSeriesAutomationEntry);
}); });
const removedQueueEntries = Math.max(0, queueEntriesBefore - this.queueEntries.length); const removedQueueEntries = Math.max(0, queueEntriesBefore - this.queueEntries.length);
@@ -27598,10 +27793,14 @@ class PipelineService extends EventEmitter {
const queuedIndex = this.findQueueEntryIndexByJobId(normalizedJobId); const queuedIndex = this.findQueueEntryIndexByJobId(normalizedJobId);
if (queuedIndex >= 0) { if (queuedIndex >= 0) {
const [removed] = this.queueEntries.splice(queuedIndex, 1); const [removed] = this.queueEntries.splice(queuedIndex, 1);
const removedDetachedEntries = this.removeDetachedQueueAutomationEntriesForJob(normalizedJobId);
await historyService.appendLog( await historyService.appendLog(
normalizedJobId, normalizedJobId,
'USER_ACTION', 'USER_ACTION',
`Aus Queue entfernt: ${QUEUE_ACTION_LABELS[removed?.action] || removed?.action || 'Aktion'}` `Aus Queue entfernt: ${QUEUE_ACTION_LABELS[removed?.action] || removed?.action || 'Aktion'}`
+ (removedDetachedEntries > 0
? ` (inkl. ${removedDetachedEntries} verknüpften Skript/Ketten-Eintrag${removedDetachedEntries === 1 ? '' : 'en'}).`
: '')
); );
await this.emitQueueChanged(); await this.emitQueueChanged();
return { return {
@@ -28058,6 +28257,15 @@ class PipelineService extends EventEmitter {
const job = await historyService.getJobById(jobId); const job = await historyService.getJobById(jobId);
const title = job?.title || job?.detected_title || `Job #${jobId}`; const title = job?.title || job?.detected_title || `Job #${jobId}`;
const finalState = isCancelled ? 'CANCELLED' : 'ERROR'; const finalState = isCancelled ? 'CANCELLED' : 'ERROR';
const removedDetachedQueueEntries = this.removeDetachedQueueAutomationEntriesForJob(jobId);
if (removedDetachedQueueEntries > 0) {
await historyService.appendLog(
jobId,
'SYSTEM',
`Queue-Cleanup: ${removedDetachedQueueEntries} verknüpfte Skript/Ketten-Eintrag${removedDetachedQueueEntries === 1 ? '' : 'e'} entfernt.`
);
await this.emitQueueChanged();
}
logger[isCancelled ? 'warn' : 'error']('job:failed', { jobId, stage, error: errorToMeta(error) }); logger[isCancelled ? 'warn' : 'error']('job:failed', { jobId, stage, error: errorToMeta(error) });
const makemkvInfo = this.safeParseJson(job?.makemkv_info_json); const makemkvInfo = this.safeParseJson(job?.makemkv_info_json);
const encodePlan = this.safeParseJson(job?.encode_plan_json); const encodePlan = this.safeParseJson(job?.encode_plan_json);
+94 -4
View File
@@ -135,6 +135,83 @@ function extractPlaylistMapping(line) {
return null; return null;
} }
function extractPlaylistEqualityMapping(line) {
const raw = String(line || '');
if (!raw) {
return null;
}
// Robot output often ends with explicit args:
// ..., "00100.mpls","00091.mpls"
if (/^MSG:3309,/i.test(raw)) {
const quoted = [];
const regex = /"([^"]*)"/g;
let match = regex.exec(raw);
while (match) {
quoted.push(String(match[1] || '').trim());
match = regex.exec(raw);
}
if (quoted.length >= 2) {
const aliasId = normalizePlaylistId(quoted[quoted.length - 2]);
const canonicalId = normalizePlaylistId(quoted[quoted.length - 1]);
if (aliasId && canonicalId && aliasId !== canonicalId) {
return {
aliasId,
canonicalId
};
}
}
}
// Text fallback:
// "Title 00100.mpls is equal to title 00091.mpls and was skipped"
const textMatch = raw.match(/title\s+(\d{5}\.mpls)\s+is equal to title\s+(\d{5}\.mpls)/i);
if (textMatch) {
const aliasId = normalizePlaylistId(textMatch[1]);
const canonicalId = normalizePlaylistId(textMatch[2]);
if (aliasId && canonicalId && aliasId !== canonicalId) {
return {
aliasId,
canonicalId
};
}
}
return null;
}
function buildPlaylistAliasMap(lines) {
const aliasMap = new Map();
for (const line of lines || []) {
const mapping = extractPlaylistEqualityMapping(line);
if (!mapping) {
continue;
}
const canonicalId = normalizePlaylistId(mapping.canonicalId);
const aliasId = normalizePlaylistId(mapping.aliasId);
if (!canonicalId || !aliasId || canonicalId === aliasId) {
continue;
}
if (!aliasMap.has(canonicalId)) {
aliasMap.set(canonicalId, new Set());
}
aliasMap.get(canonicalId).add(aliasId);
}
const normalized = {};
for (const [canonicalId, aliasSet] of aliasMap.entries()) {
const aliases = Array.from(aliasSet)
.map((value) => normalizePlaylistId(value))
.filter(Boolean)
.filter((value) => value !== canonicalId)
.sort();
if (aliases.length > 0) {
normalized[canonicalId] = aliases;
}
}
return normalized;
}
function parseAnalyzeTitles(lines) { function parseAnalyzeTitles(lines) {
const titleMap = new Map(); const titleMap = new Map();
@@ -696,6 +773,18 @@ function extractPlaylistMismatchWarnings(titles) {
function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) { function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) {
const parsedTitles = parseAnalyzeTitles(lines); const parsedTitles = parseAnalyzeTitles(lines);
const playlistAliasMap = buildPlaylistAliasMap(lines);
const parsedTitlesWithAliases = parsedTitles.map((item) => {
const playlistId = normalizePlaylistId(item?.playlistId);
const aliasesRaw = playlistId && Array.isArray(playlistAliasMap?.[playlistId])
? playlistAliasMap[playlistId]
: [];
const playlistAliases = uniqueOrdered(aliasesRaw.map((value) => normalizePlaylistId(value)).filter(Boolean));
return {
...item,
playlistAliases
};
});
const reportedTitleCount = parseReportedTitleCount(lines); const reportedTitleCount = parseReportedTitleCount(lines);
const minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60)); const minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60));
const durationSimilaritySeconds = Math.max( const durationSimilaritySeconds = Math.max(
@@ -703,7 +792,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS)) Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS))
); );
const candidatesRaw = parsedTitles const candidatesRaw = parsedTitlesWithAliases
.filter((item) => Number(item.durationSeconds || 0) >= minSeconds) .filter((item) => Number(item.durationSeconds || 0) >= minSeconds)
.sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId); .sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId);
const candidates = suppressRawMirrorCandidates(candidatesRaw) const candidates = suppressRawMirrorCandidates(candidatesRaw)
@@ -724,7 +813,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
const recommendation = evaluatedCandidates[0] || null; const recommendation = evaluatedCandidates[0] || null;
const candidatePlaylists = manualDecisionRequired ? candidatePlaylistsAll : []; const candidatePlaylists = manualDecisionRequired ? candidatePlaylistsAll : [];
const playlistSegments = buildPlaylistSegmentMap(decisionPool); const playlistSegments = buildPlaylistSegmentMap(decisionPool);
const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitles); const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitlesWithAliases);
return { return {
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
@@ -732,7 +821,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
minLengthMinutes: Number(minLengthMinutes || 0), minLengthMinutes: Number(minLengthMinutes || 0),
minLengthSeconds: minSeconds, minLengthSeconds: minSeconds,
durationSimilaritySeconds, durationSimilaritySeconds,
titles: parsedTitles, titles: parsedTitlesWithAliases,
candidates, candidates,
duplicateDurationGroups: similarityGroups, duplicateDurationGroups: similarityGroups,
obfuscationDetected, obfuscationDetected,
@@ -743,6 +832,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
candidatePlaylists, candidatePlaylists,
candidatePlaylistFiles: candidatePlaylists.map((item) => `${item}.mpls`), candidatePlaylistFiles: candidatePlaylists.map((item) => `${item}.mpls`),
playlistToTitleId, playlistToTitleId,
playlistAliasMap,
recommendation: recommendation recommendation: recommendation
? { ? {
titleId: recommendation.titleId, titleId: recommendation.titleId,
@@ -765,7 +855,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
...(reportedTitleCount !== null && reportedTitleCount !== parsedTitles.length ...(reportedTitleCount !== null && reportedTitleCount !== parsedTitles.length
? [`Titel-Anzahl abweichend: TCOUNT=${reportedTitleCount}, geparst=${parsedTitles.length}`] ? [`Titel-Anzahl abweichend: TCOUNT=${reportedTitleCount}, geparst=${parsedTitles.length}`]
: []), : []),
...extractPlaylistMismatchWarnings(parsedTitles) ...extractPlaylistMismatchWarnings(parsedTitlesWithAliases)
].slice(0, 60) ].slice(0, 60)
}; };
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.16.1-5", "version": "0.16.1-6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.16.1-5", "version": "0.16.1-6",
"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.16.1-5", "version": "0.16.1-6",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -2527,6 +2527,7 @@ export default function MediaInfoReviewPanel({
<p>Keine Titel analysiert.</p> <p>Keine Titel analysiert.</p>
) : displayTitles.map((title) => { ) : displayTitles.map((title) => {
const normalizedTitleId = normalizeTitleId(title.id); const normalizedTitleId = normalizeTitleId(title.id);
const displayTitleId = normalizeTitleId(title?.handBrakeTitleId) || normalizedTitleId;
const titleChecked = allowTitleSelection const titleChecked = allowTitleSelection
? (selectedTitleIdSet.size > 0 ? (selectedTitleIdSet.size > 0
? selectedTitleIdSet.has(String(normalizedTitleId)) ? selectedTitleIdSet.has(String(normalizedTitleId))
@@ -2634,7 +2635,7 @@ export default function MediaInfoReviewPanel({
disabled={!allowTitleSelection} disabled={!allowTitleSelection}
/> />
<span> <span>
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} #{displayTitleId ?? '-'} | {title.fileName} | {formatDuration(title.durationMinutes)}
{audioCount > 0 ? ` | Audio: ${audioCount}` : ''} {audioCount > 0 ? ` | Audio: ${audioCount}` : ''}
{subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''} {subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''}
{title.encodeInput ? ' | Encode-Input' : ''} {title.encodeInput ? ' | Encode-Input' : ''}
@@ -2692,7 +2693,7 @@ export default function MediaInfoReviewPanel({
<summary>Tonspuren und Untertitel</summary> <summary>Tonspuren und Untertitel</summary>
<div className="media-track-grid"> <div className="media-track-grid">
<TrackList <TrackList
title={`Tonspuren (Titel #${title.id})`} title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
tracks={title.audioTracks || []} tracks={title.audioTracks || []}
type="audio" type="audio"
allowSelection={allowTrackSelectionForTitle} allowSelection={allowTrackSelectionForTitle}
@@ -2706,7 +2707,7 @@ export default function MediaInfoReviewPanel({
}} }}
/> />
<TrackList <TrackList
title={`Subtitles (Titel #${title.id})`} title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
tracks={allowTrackSelectionForTitle tracks={allowTrackSelectionForTitle
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track)) ? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
: subtitleTracks} : subtitleTracks}
@@ -2727,7 +2728,7 @@ export default function MediaInfoReviewPanel({
) : (!compactTitleSelection ? ( ) : (!compactTitleSelection ? (
<div className="media-track-grid"> <div className="media-track-grid">
<TrackList <TrackList
title={`Tonspuren (Titel #${title.id})`} title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
tracks={title.audioTracks || []} tracks={title.audioTracks || []}
type="audio" type="audio"
allowSelection={allowTrackSelectionForTitle} allowSelection={allowTrackSelectionForTitle}
@@ -2741,7 +2742,7 @@ export default function MediaInfoReviewPanel({
}} }}
/> />
<TrackList <TrackList
title={`Subtitles (Titel #${title.id})`} title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
tracks={allowTrackSelectionForTitle tracks={allowTrackSelectionForTitle
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track)) ? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
: subtitleTracks} : subtitleTracks}
@@ -2817,6 +2818,7 @@ export default function MediaInfoReviewPanel({
<small>Keine Titel in der Referenz-Disc gefunden.</small> <small>Keine Titel in der Referenz-Disc gefunden.</small>
) : comparisonTitles.map((title) => { ) : comparisonTitles.map((title) => {
const normalizedTitleId = normalizeTitleId(title?.id); const normalizedTitleId = normalizeTitleId(title?.id);
const displayTitleId = normalizeTitleId(title?.handBrakeTitleId) || normalizedTitleId;
const titleChecked = normalizedTitleId !== null const titleChecked = normalizedTitleId !== null
&& comparisonSelectedTitleSet.has(String(normalizedTitleId)); && comparisonSelectedTitleSet.has(String(normalizedTitleId));
const titleIsPrimary = normalizedTitleId !== null const titleIsPrimary = normalizedTitleId !== null
@@ -2839,7 +2841,7 @@ export default function MediaInfoReviewPanel({
disabled disabled
/> />
<span> <span>
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} #{displayTitleId ?? '-'} | {title.fileName} | {formatDuration(title.durationMinutes)}
{audioCount > 0 ? ` | Audio: ${audioCount}` : ''} {audioCount > 0 ? ` | Audio: ${audioCount}` : ''}
{subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''} {subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''}
{title.encodeInput ? ' | Encode-Input' : ''} {title.encodeInput ? ' | Encode-Input' : ''}
@@ -2872,13 +2874,13 @@ export default function MediaInfoReviewPanel({
<div className="media-track-grid"> <div className="media-track-grid">
<TrackList <TrackList
title={`Tonspuren (Titel #${title.id})`} title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
tracks={title.audioTracks || []} tracks={title.audioTracks || []}
type="audio" type="audio"
allowSelection={false} allowSelection={false}
/> />
<TrackList <TrackList
title={`Subtitles (Titel #${title.id})`} title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
tracks={subtitleTracks} tracks={subtitleTracks}
type="subtitle" type="subtitle"
allowSelection={false} allowSelection={false}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.16.1-5", "version": "0.16.1-6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.16.1-5", "version": "0.16.1-6",
"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.16.1-5", "version": "0.16.1-6",
"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",