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
+8 -1
View File
@@ -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,
+221 -13
View File
@@ -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);
+94 -4
View File
@@ -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)
};
}