0.16.1-6 Detection Fix
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -1049,7 +1049,14 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed =
|
||||
const metadataKind = String(selectedMetadata?.metadataKind || '').trim().toLowerCase();
|
||||
const normalizedJobKind = String(job?.job_kind || '').trim().toLowerCase();
|
||||
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 hasSeriesMetadataHint = hasSeriesMetadataSignals(
|
||||
selectedMetadata,
|
||||
|
||||
@@ -4023,12 +4023,45 @@ function pickScanMainFeatureTitleId(scanJson) {
|
||||
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) {
|
||||
const playlistId = normalizePlaylistId(playlistIdRaw);
|
||||
const playlistAliases = getPlaylistAliasesForPlaylist(playlistAnalysis, playlistId);
|
||||
if (!playlistId || !playlistAnalysis) {
|
||||
return {
|
||||
playlistId: playlistId || null,
|
||||
playlistFile: playlistId ? `${playlistId}.mpls` : null,
|
||||
playlistAliases,
|
||||
recommended: false,
|
||||
evaluationLabel: null,
|
||||
segmentCommand: playlistId ? `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts` : null,
|
||||
@@ -4050,6 +4083,7 @@ function resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistIdRaw) {
|
||||
return {
|
||||
playlistId,
|
||||
playlistFile: `${playlistId}.mpls`,
|
||||
playlistAliases,
|
||||
recommended,
|
||||
evaluationLabel: evaluated?.evaluationLabel || (recommended ? 'wahrscheinlich korrekt (Heuristik)' : null),
|
||||
segmentCommand: segmentEntry?.segmentCommand || `strings BDMV/PLAYLIST/${playlistId}.mpls | grep m2ts`,
|
||||
@@ -4322,8 +4356,13 @@ function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw, options = {
|
||||
? Math.trunc(durationToleranceRaw)
|
||||
: 5;
|
||||
|
||||
const playlistAliases = normalizePlaylistAliasIds(options?.playlistAliases, playlistId);
|
||||
const candidatePlaylistIds = [playlistId, ...playlistAliases];
|
||||
const candidatePlaylistSet = new Set(candidatePlaylistIds);
|
||||
|
||||
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 durationDelta = expectedDurationSeconds !== null
|
||||
@@ -4352,15 +4391,36 @@ function resolveHandBrakeTitleIdForPlaylist(scanJson, playlistIdRaw, options = {
|
||||
if (matches.length > 0) {
|
||||
if (expectedDurationSeconds !== null || expectedSizeBytes !== null) {
|
||||
const scored = matches.map(scoreForExpected).sort(sortByExpectedScore);
|
||||
const scoredDirect = directMatches.map(scoreForExpected).sort(sortByExpectedScore);
|
||||
if (expectedDurationSeconds !== null) {
|
||||
const withinTolerance = scored.filter((item) => item.durationDelta <= durationToleranceSeconds);
|
||||
if (withinTolerance.length > 0) {
|
||||
return withinTolerance[0].row.handBrakeTitleId;
|
||||
const directWithinTolerance = scoredDirect.filter((item) => item.durationDelta <= durationToleranceSeconds);
|
||||
if (directWithinTolerance.length > 0) {
|
||||
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;
|
||||
}
|
||||
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.sizeBytes - a.sizeBytes
|
||||
|| a.handBrakeTitleId - b.handBrakeTitleId
|
||||
@@ -4416,8 +4476,12 @@ function isHandBrakePlaylistCacheEntryCompatible(entry, playlistIdRaw, options =
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedPlaylistIds = new Set([
|
||||
playlistId,
|
||||
...normalizePlaylistAliasIds(options?.playlistAliases, playlistId)
|
||||
]);
|
||||
const cachedPlaylistId = normalizePlaylistId(titleInfo?.playlistId || null);
|
||||
if (cachedPlaylistId && cachedPlaylistId !== playlistId) {
|
||||
if (cachedPlaylistId && !allowedPlaylistIds.has(cachedPlaylistId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6001,6 +6065,7 @@ function buildDiscScanReview({
|
||||
eligibleForEncode: title.selectedByMinLength,
|
||||
playlistId: title.playlistMatch?.playlistId || null,
|
||||
playlistFile: title.playlistMatch?.playlistFile || null,
|
||||
playlistAliases: normalizePlaylistAliasIds(title.playlistMatch?.playlistAliases, title.playlistMatch?.playlistId),
|
||||
playlistRecommended: Boolean(title.playlistMatch?.recommended),
|
||||
playlistEvaluationLabel: title.playlistMatch?.evaluationLabel || null,
|
||||
playlistSegmentCommand: title.playlistMatch?.segmentCommand || null,
|
||||
@@ -6529,6 +6594,9 @@ function buildPlaylistCandidates(playlistAnalysis) {
|
||||
const segmentMap = playlistAnalysis?.playlistSegments && typeof playlistAnalysis.playlistSegments === 'object'
|
||||
? playlistAnalysis.playlistSegments
|
||||
: {};
|
||||
const aliasMap = playlistAnalysis?.playlistAliasMap && typeof playlistAnalysis.playlistAliasMap === 'object'
|
||||
? playlistAnalysis.playlistAliasMap
|
||||
: {};
|
||||
|
||||
return rawList
|
||||
.map((playlistId) => normalizePlaylistId(playlistId))
|
||||
@@ -6536,6 +6604,12 @@ function buildPlaylistCandidates(playlistAnalysis) {
|
||||
.map((playlistId) => {
|
||||
const source = sourceRows.find((item) => normalizePlaylistId(item?.playlistId || item?.playlistFile) === playlistId) || 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 sequenceCoherence = Number(source?.structuralMetrics?.sequenceCoherence);
|
||||
const titleId = Number(source?.titleId ?? source?.id);
|
||||
@@ -6584,6 +6658,7 @@ function buildPlaylistCandidates(playlistAnalysis) {
|
||||
return {
|
||||
playlistId,
|
||||
playlistFile: toPlaylistFile(playlistId),
|
||||
playlistAliases,
|
||||
titleId: Number.isFinite(titleId) ? Math.trunc(titleId) : null,
|
||||
durationSeconds,
|
||||
durationLabel: durationLabel || null,
|
||||
@@ -6747,7 +6822,8 @@ function buildHandBrakePlaylistScanCache(scanJson, playlistCandidates = [], rawP
|
||||
candidateMetaByPlaylist.set(playlistId, {
|
||||
expectedMakemkvTitleId: normalizeNonNegativeInteger(row?.titleId),
|
||||
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 previousQueueLength = this.queueEntries.length;
|
||||
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) {
|
||||
this.cancelRequestedByJob.add(jobId);
|
||||
@@ -14484,6 +14563,41 @@ class PipelineService extends EventEmitter {
|
||||
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) {
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
const seen = new Set();
|
||||
@@ -15607,6 +15721,15 @@ class PipelineService extends EventEmitter {
|
||||
'SYSTEM',
|
||||
`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 expectedDurationForCache = Number(selectedTitleFromAnalysis?.durationSeconds || 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(
|
||||
isHandBrakePlaylistCacheEntryCompatible(
|
||||
cachedHandBrakePlaylistEntry,
|
||||
resolvedPlaylistId,
|
||||
{
|
||||
expectedDurationSeconds: expectedDurationForCache,
|
||||
expectedSizeBytes: expectedSizeForCache
|
||||
expectedSizeBytes: expectedSizeForCache,
|
||||
playlistAliases: playlistAliasesForResolve
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -18744,7 +18875,8 @@ class PipelineService extends EventEmitter {
|
||||
resolvedHandBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(resolveScanJson, resolvedPlaylistId, {
|
||||
expectedMakemkvTitleId: selectedTitleForReview,
|
||||
expectedDurationSeconds: expectedDurationForCache,
|
||||
expectedSizeBytes: expectedSizeForCache
|
||||
expectedSizeBytes: expectedSizeForCache,
|
||||
playlistAliases: playlistAliasesForResolve
|
||||
});
|
||||
if (!resolvedHandBrakeTitleId) {
|
||||
const knownPlaylists = listAvailableHandBrakePlaylists(resolveScanJson);
|
||||
@@ -18776,7 +18908,8 @@ class PipelineService extends EventEmitter {
|
||||
titleInfo: reviewTitleInfo
|
||||
}, resolvedPlaylistId, {
|
||||
expectedDurationSeconds: expectedDurationForCache,
|
||||
expectedSizeBytes: expectedSizeForCache
|
||||
expectedSizeBytes: expectedSizeForCache,
|
||||
playlistAliases: playlistAliasesForResolve
|
||||
})) {
|
||||
const error = new Error(
|
||||
`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 : [])
|
||||
.slice(0, 1)
|
||||
.map((title) => {
|
||||
const normalizedHandBrakeTitleId = normalizeReviewTitleId(
|
||||
resolvedHandBrakeTitleId
|
||||
?? title?.handBrakeTitleId
|
||||
?? title?.titleIndex
|
||||
?? title?.id
|
||||
?? selectedTitleForReview
|
||||
);
|
||||
const durationSeconds = Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0);
|
||||
const eligibleForEncode = durationSeconds >= minLengthSecondsForReview;
|
||||
const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => {
|
||||
@@ -18995,6 +19135,8 @@ class PipelineService extends EventEmitter {
|
||||
|
||||
return {
|
||||
...title,
|
||||
id: normalizedHandBrakeTitleId || title?.id || null,
|
||||
handBrakeTitleId: normalizedHandBrakeTitleId || null,
|
||||
filePath: rawPath,
|
||||
fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
|
||||
durationSeconds,
|
||||
@@ -25141,6 +25283,25 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
const expectedDurationSecondsForResolve = Number(selectedEncodeTitle?.durationSeconds || 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) {
|
||||
const titleResolveScanLines = [];
|
||||
const titleResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, {
|
||||
@@ -25172,7 +25333,8 @@ class PipelineService extends EventEmitter {
|
||||
handBrakeTitleId = resolveHandBrakeTitleIdForPlaylist(titleResolveParsed, selectedPlaylistId, {
|
||||
expectedMakemkvTitleId: expectedMakemkvTitleIdForResolve,
|
||||
expectedDurationSeconds: expectedDurationSecondsForResolve,
|
||||
expectedSizeBytes: expectedSizeBytesForResolve
|
||||
expectedSizeBytes: expectedSizeBytesForResolve,
|
||||
playlistAliases: selectedPlaylistAliasesForResolve
|
||||
});
|
||||
if (!handBrakeTitleId) {
|
||||
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) {
|
||||
const fallbackTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId);
|
||||
if (fallbackTitleId) {
|
||||
@@ -27403,7 +27588,9 @@ class PipelineService extends EventEmitter {
|
||||
const queueEntriesBefore = this.queueEntries.length;
|
||||
this.queueEntries = this.queueEntries.filter((entry) => {
|
||||
const action = String(entry?.action || '').trim().toUpperCase();
|
||||
const entryType = String(entry?.type || 'job').trim().toLowerCase();
|
||||
const entryJobId = Number(entry?.jobId);
|
||||
const entrySourceJobId = this.normalizeQueueJobId(entry?.sourceJobId);
|
||||
const isLegacyParentEntry = (
|
||||
entryJobId === Number(normalizedParentJobId)
|
||||
&& action === QUEUE_ACTIONS.START_SERIES_EPISODE
|
||||
@@ -27414,7 +27601,15 @@ class PipelineService extends EventEmitter {
|
||||
&& entryJobId > 0
|
||||
&& 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);
|
||||
|
||||
@@ -27598,10 +27793,14 @@ class PipelineService extends EventEmitter {
|
||||
const queuedIndex = this.findQueueEntryIndexByJobId(normalizedJobId);
|
||||
if (queuedIndex >= 0) {
|
||||
const [removed] = this.queueEntries.splice(queuedIndex, 1);
|
||||
const removedDetachedEntries = this.removeDetachedQueueAutomationEntriesForJob(normalizedJobId);
|
||||
await historyService.appendLog(
|
||||
normalizedJobId,
|
||||
'USER_ACTION',
|
||||
`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();
|
||||
return {
|
||||
@@ -28058,6 +28257,15 @@ class PipelineService extends EventEmitter {
|
||||
const job = await historyService.getJobById(jobId);
|
||||
const title = job?.title || job?.detected_title || `Job #${jobId}`;
|
||||
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) });
|
||||
const makemkvInfo = this.safeParseJson(job?.makemkv_info_json);
|
||||
const encodePlan = this.safeParseJson(job?.encode_plan_json);
|
||||
|
||||
@@ -135,6 +135,83 @@ function extractPlaylistMapping(line) {
|
||||
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) {
|
||||
const titleMap = new Map();
|
||||
|
||||
@@ -696,6 +773,18 @@ function extractPlaylistMismatchWarnings(titles) {
|
||||
|
||||
function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {}) {
|
||||
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 minSeconds = Math.max(0, Math.round(Number(minLengthMinutes || 0) * 60));
|
||||
const durationSimilaritySeconds = Math.max(
|
||||
@@ -703,7 +792,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
||||
Math.round(Number(options.durationSimilaritySeconds || DEFAULT_DURATION_SIMILARITY_SECONDS))
|
||||
);
|
||||
|
||||
const candidatesRaw = parsedTitles
|
||||
const candidatesRaw = parsedTitlesWithAliases
|
||||
.filter((item) => Number(item.durationSeconds || 0) >= minSeconds)
|
||||
.sort((a, b) => b.durationSeconds - a.durationSeconds || b.sizeBytes - a.sizeBytes || a.titleId - b.titleId);
|
||||
const candidates = suppressRawMirrorCandidates(candidatesRaw)
|
||||
@@ -724,7 +813,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
||||
const recommendation = evaluatedCandidates[0] || null;
|
||||
const candidatePlaylists = manualDecisionRequired ? candidatePlaylistsAll : [];
|
||||
const playlistSegments = buildPlaylistSegmentMap(decisionPool);
|
||||
const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitles);
|
||||
const playlistToTitleId = buildPlaylistToTitleIdMap(parsedTitlesWithAliases);
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
@@ -732,7 +821,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
||||
minLengthMinutes: Number(minLengthMinutes || 0),
|
||||
minLengthSeconds: minSeconds,
|
||||
durationSimilaritySeconds,
|
||||
titles: parsedTitles,
|
||||
titles: parsedTitlesWithAliases,
|
||||
candidates,
|
||||
duplicateDurationGroups: similarityGroups,
|
||||
obfuscationDetected,
|
||||
@@ -743,6 +832,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
||||
candidatePlaylists,
|
||||
candidatePlaylistFiles: candidatePlaylists.map((item) => `${item}.mpls`),
|
||||
playlistToTitleId,
|
||||
playlistAliasMap,
|
||||
recommendation: recommendation
|
||||
? {
|
||||
titleId: recommendation.titleId,
|
||||
@@ -765,7 +855,7 @@ function analyzePlaylistObfuscation(lines, minLengthMinutes = 60, options = {})
|
||||
...(reportedTitleCount !== null && reportedTitleCount !== parsedTitles.length
|
||||
? [`Titel-Anzahl abweichend: TCOUNT=${reportedTitleCount}, geparst=${parsedTitles.length}`]
|
||||
: []),
|
||||
...extractPlaylistMismatchWarnings(parsedTitles)
|
||||
...extractPlaylistMismatchWarnings(parsedTitlesWithAliases)
|
||||
].slice(0, 60)
|
||||
};
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -2527,6 +2527,7 @@ export default function MediaInfoReviewPanel({
|
||||
<p>Keine Titel analysiert.</p>
|
||||
) : displayTitles.map((title) => {
|
||||
const normalizedTitleId = normalizeTitleId(title.id);
|
||||
const displayTitleId = normalizeTitleId(title?.handBrakeTitleId) || normalizedTitleId;
|
||||
const titleChecked = allowTitleSelection
|
||||
? (selectedTitleIdSet.size > 0
|
||||
? selectedTitleIdSet.has(String(normalizedTitleId))
|
||||
@@ -2634,7 +2635,7 @@ export default function MediaInfoReviewPanel({
|
||||
disabled={!allowTitleSelection}
|
||||
/>
|
||||
<span>
|
||||
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)}
|
||||
#{displayTitleId ?? '-'} | {title.fileName} | {formatDuration(title.durationMinutes)}
|
||||
{audioCount > 0 ? ` | Audio: ${audioCount}` : ''}
|
||||
{subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''}
|
||||
{title.encodeInput ? ' | Encode-Input' : ''}
|
||||
@@ -2692,7 +2693,7 @@ export default function MediaInfoReviewPanel({
|
||||
<summary>Tonspuren und Untertitel</summary>
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
@@ -2706,7 +2707,7 @@ export default function MediaInfoReviewPanel({
|
||||
}}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={allowTrackSelectionForTitle
|
||||
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
: subtitleTracks}
|
||||
@@ -2727,7 +2728,7 @@ export default function MediaInfoReviewPanel({
|
||||
) : (!compactTitleSelection ? (
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={allowTrackSelectionForTitle}
|
||||
@@ -2741,7 +2742,7 @@ export default function MediaInfoReviewPanel({
|
||||
}}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={allowTrackSelectionForTitle
|
||||
? subtitleTracks.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
: subtitleTracks}
|
||||
@@ -2817,6 +2818,7 @@ export default function MediaInfoReviewPanel({
|
||||
<small>Keine Titel in der Referenz-Disc gefunden.</small>
|
||||
) : comparisonTitles.map((title) => {
|
||||
const normalizedTitleId = normalizeTitleId(title?.id);
|
||||
const displayTitleId = normalizeTitleId(title?.handBrakeTitleId) || normalizedTitleId;
|
||||
const titleChecked = normalizedTitleId !== null
|
||||
&& comparisonSelectedTitleSet.has(String(normalizedTitleId));
|
||||
const titleIsPrimary = normalizedTitleId !== null
|
||||
@@ -2839,7 +2841,7 @@ export default function MediaInfoReviewPanel({
|
||||
disabled
|
||||
/>
|
||||
<span>
|
||||
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)}
|
||||
#{displayTitleId ?? '-'} | {title.fileName} | {formatDuration(title.durationMinutes)}
|
||||
{audioCount > 0 ? ` | Audio: ${audioCount}` : ''}
|
||||
{subtitleCount > 0 ? ` | Untertitel: ${subtitleCount}` : ''}
|
||||
{title.encodeInput ? ' | Encode-Input' : ''}
|
||||
@@ -2872,13 +2874,13 @@ export default function MediaInfoReviewPanel({
|
||||
|
||||
<div className="media-track-grid">
|
||||
<TrackList
|
||||
title={`Tonspuren (Titel #${title.id})`}
|
||||
title={`Tonspuren (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={title.audioTracks || []}
|
||||
type="audio"
|
||||
allowSelection={false}
|
||||
/>
|
||||
<TrackList
|
||||
title={`Subtitles (Titel #${title.id})`}
|
||||
title={`Subtitles (Titel #${displayTitleId ?? '-'})`}
|
||||
tracks={subtitleTracks}
|
||||
type="subtitle"
|
||||
allowSelection={false}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.16.1-5",
|
||||
"version": "0.16.1-6",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user