1.0.0-rc4 rc4
This commit is contained in:
@@ -6377,6 +6377,133 @@ function buildSelectedHandBrakeReviewTitles(scanJson, selectedHandBrakeTitleIds
|
||||
return titles;
|
||||
}
|
||||
|
||||
function collectAlternativeFeatureReviewCandidates({
|
||||
playlistCandidates = [],
|
||||
handBrakePlaylistScan = null,
|
||||
playlistAnalysis = null,
|
||||
selectedPlaylistId = null,
|
||||
selectedHandBrakeTitleId = null,
|
||||
selectedTitleInfo = null,
|
||||
makeMkvSubtitleTracks = []
|
||||
}) {
|
||||
const normalizedSelectedPlaylistId = normalizePlaylistId(selectedPlaylistId);
|
||||
const normalizedSelectedHandBrakeTitleId = normalizeReviewTitleId(selectedHandBrakeTitleId);
|
||||
const normalizedCache = normalizeHandBrakePlaylistScanCache(handBrakePlaylistScan);
|
||||
const baselineDurationSeconds = Number(selectedTitleInfo?.durationSeconds || 0) || 0;
|
||||
const baselineSizeBytes = Number(selectedTitleInfo?.sizeBytes || 0) || 0;
|
||||
const candidateRows = Array.isArray(playlistCandidates) ? playlistCandidates : [];
|
||||
const candidateMap = new Map();
|
||||
|
||||
for (const row of candidateRows) {
|
||||
const playlistId = normalizePlaylistId(row?.playlistId || row?.playlistFile || null);
|
||||
if (!playlistId || candidateMap.has(playlistId)) {
|
||||
continue;
|
||||
}
|
||||
candidateMap.set(playlistId, row);
|
||||
}
|
||||
|
||||
const resolved = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
const pushCandidate = (playlistIdRaw, titleInfoRaw, sourceRow = null) => {
|
||||
const playlistId = normalizePlaylistId(playlistIdRaw || titleInfoRaw?.playlistId || null);
|
||||
const titleInfo = enrichTitleInfoWithForcedSubtitleAvailability(
|
||||
titleInfoRaw,
|
||||
makeMkvSubtitleTracks
|
||||
);
|
||||
const handBrakeTitleId = normalizeReviewTitleId(
|
||||
titleInfo?.handBrakeTitleId
|
||||
|| sourceRow?.handBrakeTitleId
|
||||
|| null
|
||||
);
|
||||
if (!playlistId || !titleInfo || !handBrakeTitleId) {
|
||||
return;
|
||||
}
|
||||
const durationSeconds = Number(titleInfo?.durationSeconds || sourceRow?.durationSeconds || 0) || 0;
|
||||
const sizeBytes = Number(titleInfo?.sizeBytes || sourceRow?.sizeBytes || 0) || 0;
|
||||
const isSelectedDefault = (
|
||||
(normalizedSelectedPlaylistId && playlistId === normalizedSelectedPlaylistId)
|
||||
|| (normalizedSelectedHandBrakeTitleId && handBrakeTitleId === normalizedSelectedHandBrakeTitleId)
|
||||
);
|
||||
if (!isSelectedDefault && durationSeconds < baselineDurationSeconds) {
|
||||
return;
|
||||
}
|
||||
const dedupeKey = `${playlistId}:${handBrakeTitleId}`;
|
||||
if (seenKeys.has(dedupeKey)) {
|
||||
return;
|
||||
}
|
||||
seenKeys.add(dedupeKey);
|
||||
resolved.push({
|
||||
playlistId,
|
||||
handBrakeTitleId,
|
||||
titleInfo,
|
||||
sourceRow,
|
||||
durationSeconds,
|
||||
sizeBytes,
|
||||
playlistInfo: resolvePlaylistInfoFromAnalysis(playlistAnalysis, playlistId),
|
||||
isSelectedDefault
|
||||
});
|
||||
};
|
||||
|
||||
if (normalizedSelectedPlaylistId && selectedTitleInfo) {
|
||||
pushCandidate(
|
||||
normalizedSelectedPlaylistId,
|
||||
selectedTitleInfo,
|
||||
candidateMap.get(normalizedSelectedPlaylistId) || null
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedCache?.byPlaylist && typeof normalizedCache.byPlaylist === 'object') {
|
||||
for (const [playlistId, cacheEntry] of Object.entries(normalizedCache.byPlaylist)) {
|
||||
pushCandidate(
|
||||
playlistId,
|
||||
cacheEntry?.titleInfo || null,
|
||||
candidateMap.get(playlistId) || null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of candidateRows) {
|
||||
const playlistId = normalizePlaylistId(row?.playlistId || row?.playlistFile || null);
|
||||
if (!playlistId) {
|
||||
continue;
|
||||
}
|
||||
const cacheEntry = normalizedCache?.byPlaylist?.[playlistId] || null;
|
||||
pushCandidate(
|
||||
playlistId,
|
||||
cacheEntry?.titleInfo || null,
|
||||
row
|
||||
);
|
||||
}
|
||||
|
||||
const byPriority = resolved
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
if (left.isSelectedDefault !== right.isSelectedDefault) {
|
||||
return left.isSelectedDefault ? -1 : 1;
|
||||
}
|
||||
return left.durationSeconds - right.durationSeconds
|
||||
|| left.sizeBytes - right.sizeBytes
|
||||
|| String(left.playlistId || '').localeCompare(String(right.playlistId || ''))
|
||||
|| left.handBrakeTitleId - right.handBrakeTitleId;
|
||||
});
|
||||
|
||||
if (byPriority.length === 0 && normalizedSelectedPlaylistId && selectedTitleInfo) {
|
||||
return [{
|
||||
playlistId: normalizedSelectedPlaylistId,
|
||||
handBrakeTitleId: normalizedSelectedHandBrakeTitleId || null,
|
||||
titleInfo: enrichTitleInfoWithForcedSubtitleAvailability(selectedTitleInfo, makeMkvSubtitleTracks),
|
||||
sourceRow: candidateMap.get(normalizedSelectedPlaylistId) || null,
|
||||
durationSeconds: baselineDurationSeconds,
|
||||
sizeBytes: baselineSizeBytes,
|
||||
playlistInfo: resolvePlaylistInfoFromAnalysis(playlistAnalysis, normalizedSelectedPlaylistId),
|
||||
isSelectedDefault: true
|
||||
}];
|
||||
}
|
||||
|
||||
return byPriority;
|
||||
}
|
||||
|
||||
function pickTitleIdForTrackReview(playlistAnalysis, selectedTitleId = null) {
|
||||
const explicit = normalizeNonNegativeInteger(selectedTitleId);
|
||||
if (explicit !== null) {
|
||||
@@ -8116,6 +8243,23 @@ function applyEncodeTitleSelectionToPlan(encodePlan, selectedEncodeTitleId, sele
|
||||
selectedTitleIds: normalizedTitleIds,
|
||||
encodeInputTitleId: effectivePrimaryTitleId,
|
||||
encodeInputPath: selectedTitle?.filePath || null,
|
||||
handBrakeTitleId: normalizeReviewTitleId(
|
||||
selectedTitle?.handBrakeTitleId
|
||||
?? selectedTitle?.titleIndex
|
||||
?? encodePlan?.handBrakeTitleId
|
||||
?? null
|
||||
),
|
||||
selectedPlaylistId: normalizePlaylistId(
|
||||
selectedTitle?.playlistId
|
||||
|| selectedTitle?.playlistFile
|
||||
|| encodePlan?.selectedPlaylistId
|
||||
|| null
|
||||
),
|
||||
selectedMakemkvTitleId: normalizeNonNegativeInteger(
|
||||
selectedTitle?.makemkvTitleId
|
||||
?? encodePlan?.selectedMakemkvTitleId
|
||||
?? null
|
||||
),
|
||||
titleSelectionRequired: false
|
||||
},
|
||||
selectedTitle,
|
||||
@@ -9348,6 +9492,9 @@ function resolvePrefillEncodeTitleId(reviewPlan, previousPlan) {
|
||||
if (reviewTitles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (Boolean(reviewPlan?.alternativeTitleSelection?.enabled)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousSelectedTitle = findSelectedTitleInPlan(previousPlan);
|
||||
if (!previousSelectedTitle) {
|
||||
@@ -11072,6 +11219,63 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (stage === 'RIPPING' || stage === 'MEDIAINFO_CHECK') {
|
||||
let recoveryValidation;
|
||||
try {
|
||||
recoveryValidation = await this.validateStartupEncodeRecoveryReadiness(row);
|
||||
} catch (validationError) {
|
||||
logger.warn('startup:recover-stale-review:validation-failed', {
|
||||
jobId,
|
||||
stage,
|
||||
error: errorToMeta(validationError)
|
||||
});
|
||||
recoveryValidation = {
|
||||
ok: false,
|
||||
code: 'validation_failed',
|
||||
message: `Server-Neustart: Rip-Validierung fehlgeschlagen (${validationError?.message || 'unknown'}). Bitte Rip vom Laufwerk neu starten.`
|
||||
};
|
||||
}
|
||||
|
||||
if (recoveryValidation?.ok) {
|
||||
const reviewRecoveryMessage = stage === 'MEDIAINFO_CHECK'
|
||||
? 'Server-Neustart während Titel-/Spurprüfung erkannt. Rip ist bereits fertig; Review kann aus dem RAW-Backup neu gestartet werden.'
|
||||
: 'Server-Neustart nach abgeschlossenem Rip erkannt. Review kann aus dem RAW-Backup neu gestartet werden.';
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'ERROR',
|
||||
last_state: 'MEDIAINFO_CHECK',
|
||||
end_time: nowIso(),
|
||||
error_message: reviewRecoveryMessage,
|
||||
raw_path: recoveryValidation?.resolvedRawPath || row?.raw_path || null,
|
||||
rip_successful: 1
|
||||
});
|
||||
try {
|
||||
await historyService.appendLog(jobId, 'SYSTEM', reviewRecoveryMessage);
|
||||
} catch (_error) {
|
||||
// keep recovery path even if log append fails
|
||||
}
|
||||
markedError += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const recoveryMessage = String(recoveryValidation?.message || '').trim()
|
||||
|| `${message} Rip ist unvollständig. Bitte Rip vom Laufwerk neu starten.`;
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'ERROR',
|
||||
last_state: 'RIPPING',
|
||||
end_time: nowIso(),
|
||||
error_message: recoveryMessage,
|
||||
rip_successful: 0
|
||||
});
|
||||
try {
|
||||
await historyService.appendLog(jobId, 'SYSTEM', recoveryMessage);
|
||||
} catch (_error) {
|
||||
// keep recovery path even if log append fails
|
||||
}
|
||||
markedError += 1;
|
||||
blockedIncompleteRip += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'ERROR',
|
||||
last_state: stage,
|
||||
@@ -18502,14 +18706,25 @@ class PipelineService extends EventEmitter {
|
||||
&& !selectedPlaylistId
|
||||
&& playlistCandidates.length > 0
|
||||
);
|
||||
if (shouldPrepareHandBrakeDecisionData) {
|
||||
const shouldPrepareAlternativeFeatureReviewData = Boolean(
|
||||
selectedPlaylistId
|
||||
&& !isSeriesBackupReview
|
||||
&& playlistCandidates.length > 1
|
||||
);
|
||||
const shouldPrepareExpandedHandBrakePlaylistData = (
|
||||
shouldPrepareHandBrakeDecisionData
|
||||
|| shouldPrepareAlternativeFeatureReviewData
|
||||
);
|
||||
if (shouldPrepareExpandedHandBrakePlaylistData) {
|
||||
const hasCompleteCache = hasCachedHandBrakeDataForPlaylistCandidates(handBrakePlaylistScan, playlistCandidates);
|
||||
if (!hasCompleteCache) {
|
||||
await this.updateProgress(
|
||||
'MEDIAINFO_CHECK',
|
||||
25,
|
||||
null,
|
||||
'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet',
|
||||
shouldPrepareAlternativeFeatureReviewData
|
||||
? 'HandBrake Trackdaten für alternative Filmfassungen werden vorbereitet'
|
||||
: 'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet',
|
||||
jobId
|
||||
);
|
||||
try {
|
||||
@@ -18540,13 +18755,17 @@ class PipelineService extends EventEmitter {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
`HandBrake Playlist-Trackdaten vorbereitet: ${Object.keys(preparedCache.byPlaylist || {}).length} Kandidaten aus --scan -t 0 analysiert.`
|
||||
shouldPrepareAlternativeFeatureReviewData
|
||||
? `HandBrake Trackdaten für alternative Filmfassungen vorbereitet: ${Object.keys(preparedCache.byPlaylist || {}).length} Kandidaten aus --scan -t 0 analysiert.`
|
||||
: `HandBrake Playlist-Trackdaten vorbereitet: ${Object.keys(preparedCache.byPlaylist || {}).length} Kandidaten aus --scan -t 0 analysiert.`
|
||||
);
|
||||
} else {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
'HandBrake Playlist-Trackdaten konnten aus --scan -t 0 nicht auf Kandidaten abgebildet werden.'
|
||||
shouldPrepareAlternativeFeatureReviewData
|
||||
? 'HandBrake Trackdaten für alternative Filmfassungen konnten aus --scan -t 0 nicht auf Kandidaten abgebildet werden.'
|
||||
: 'HandBrake Playlist-Trackdaten konnten aus --scan -t 0 nicht auf Kandidaten abgebildet werden.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -18562,7 +18781,9 @@ class PipelineService extends EventEmitter {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
`HandBrake Voranalyse für Playlist-Auswahl fehlgeschlagen: ${error.message}`
|
||||
shouldPrepareAlternativeFeatureReviewData
|
||||
? `HandBrake Vorbereitung für alternative Filmfassungen fehlgeschlagen: ${error.message}`
|
||||
: `HandBrake Voranalyse für Playlist-Auswahl fehlgeschlagen: ${error.message}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
@@ -18572,7 +18793,9 @@ class PipelineService extends EventEmitter {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
'HandBrake Playlist-Trackdaten aus Cache übernommen (kein erneuter --scan -t 0).'
|
||||
shouldPrepareAlternativeFeatureReviewData
|
||||
? 'HandBrake Trackdaten für alternative Filmfassungen aus Cache übernommen (kein erneuter --scan -t 0).'
|
||||
: 'HandBrake Playlist-Trackdaten aus Cache übernommen (kein erneuter --scan -t 0).'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19455,188 +19678,243 @@ class PipelineService extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
const syntheticFilePath = path.join(
|
||||
rawPath,
|
||||
reviewTitleSource === 'handbrake' && Number.isFinite(Number(resolvedHandBrakeTitleId)) && Number(resolvedHandBrakeTitleId) > 0
|
||||
? `handbrake_t${String(Math.trunc(Number(resolvedHandBrakeTitleId))).padStart(2, '0')}.mkv`
|
||||
: `makemkv_t${String(selectedTitleForReview).padStart(2, '0')}.mkv`
|
||||
const alternativeReviewCandidates = collectAlternativeFeatureReviewCandidates({
|
||||
playlistCandidates,
|
||||
handBrakePlaylistScan,
|
||||
playlistAnalysis,
|
||||
selectedPlaylistId: resolvedPlaylistId,
|
||||
selectedHandBrakeTitleId: resolvedHandBrakeTitleId,
|
||||
selectedTitleInfo: reviewTitleInfo,
|
||||
makeMkvSubtitleTracks: makeMkvSubtitleTracksForSelection
|
||||
});
|
||||
const reviewCandidateRows = alternativeReviewCandidates.length > 0
|
||||
? alternativeReviewCandidates
|
||||
: [{
|
||||
playlistId: resolvedPlaylistId,
|
||||
handBrakeTitleId: resolvedHandBrakeTitleId,
|
||||
titleInfo: reviewTitleInfo,
|
||||
sourceRow: null,
|
||||
durationSeconds: Number(reviewTitleInfo?.durationSeconds || 0) || 0,
|
||||
sizeBytes: Number(reviewTitleInfo?.sizeBytes || 0) || 0,
|
||||
playlistInfo: resolvePlaylistInfoFromAnalysis(playlistAnalysis, resolvedPlaylistId),
|
||||
isSelectedDefault: true
|
||||
}];
|
||||
|
||||
const buildSyntheticCandidatePath = (handBrakeTitleId, fallbackTitleId) => (
|
||||
path.join(
|
||||
rawPath,
|
||||
reviewTitleSource === 'handbrake' && Number.isFinite(Number(handBrakeTitleId)) && Number(handBrakeTitleId) > 0
|
||||
? `handbrake_t${String(Math.trunc(Number(handBrakeTitleId))).padStart(2, '0')}.mkv`
|
||||
: `makemkv_t${String(fallbackTitleId).padStart(2, '0')}.mkv`
|
||||
)
|
||||
);
|
||||
const syntheticMediaInfoByPath = {
|
||||
[syntheticFilePath]: buildSyntheticMediaInfoFromMakeMkvTitle(reviewTitleInfo)
|
||||
};
|
||||
|
||||
const reviewCandidatesWithPaths = reviewCandidateRows.map((candidate, index) => {
|
||||
const fallbackTitleId = Number(selectedTitleForReview || index + 1) || (index + 1);
|
||||
return {
|
||||
...candidate,
|
||||
syntheticFilePath: buildSyntheticCandidatePath(candidate.handBrakeTitleId, fallbackTitleId)
|
||||
};
|
||||
});
|
||||
|
||||
const syntheticMediaInfoByPath = Object.fromEntries(
|
||||
reviewCandidatesWithPaths.map((candidate) => [
|
||||
candidate.syntheticFilePath,
|
||||
buildSyntheticMediaInfoFromMakeMkvTitle(candidate.titleInfo)
|
||||
])
|
||||
);
|
||||
const allowAlternativeTitleSelection = reviewCandidatesWithPaths.length > 1;
|
||||
let review = buildMediainfoReview({
|
||||
mediaFiles: [{
|
||||
path: syntheticFilePath,
|
||||
size: Number(reviewTitleInfo?.sizeBytes || 0)
|
||||
}],
|
||||
mediaFiles: reviewCandidatesWithPaths.map((candidate) => ({
|
||||
path: candidate.syntheticFilePath,
|
||||
size: Number(candidate?.titleInfo?.sizeBytes || 0)
|
||||
})),
|
||||
mediaInfoByPath: syntheticMediaInfoByPath,
|
||||
settings,
|
||||
presetProfile,
|
||||
playlistAnalysis,
|
||||
preferredEncodeTitleId: selectedTitleForReview,
|
||||
selectedPlaylistId: resolvedPlaylistId || reviewTitleInfo?.playlistId || null,
|
||||
selectedMakemkvTitleId: selectedTitleForReview
|
||||
preferredEncodeTitleId: allowAlternativeTitleSelection ? null : selectedTitleForReview,
|
||||
selectedPlaylistId: allowAlternativeTitleSelection
|
||||
? null
|
||||
: (resolvedPlaylistId || reviewTitleInfo?.playlistId || null),
|
||||
selectedMakemkvTitleId: allowAlternativeTitleSelection ? null : selectedTitleForReview
|
||||
});
|
||||
review = remapReviewTrackIdsToSourceIds(review);
|
||||
|
||||
const resolvedPlaylistInfo = resolvePlaylistInfoFromAnalysis(playlistAnalysis, resolvedPlaylistId);
|
||||
const minLengthMinutesForReview = Number(review?.minLengthMinutes ?? settings?.makemkv_min_length_minutes ?? 0);
|
||||
const minLengthSecondsForReview = Math.max(0, Math.round(minLengthMinutesForReview * 60));
|
||||
const subtitleTrackMetaBySourceId = new Map(
|
||||
(Array.isArray(reviewTitleInfo?.subtitleTracks) ? reviewTitleInfo.subtitleTracks : [])
|
||||
.map((track) => {
|
||||
const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
|
||||
return sourceTrackId ? [sourceTrackId, track] : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
const candidateMetaBySyntheticPath = new Map(
|
||||
reviewCandidatesWithPaths.map((candidate) => [candidate.syntheticFilePath, candidate])
|
||||
);
|
||||
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) => {
|
||||
const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
|
||||
const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null;
|
||||
const selectedByRule = Boolean(sourceMeta?.selectedByRule ?? track?.selectedByRule);
|
||||
const duplicate = Boolean(sourceMeta?.duplicate ?? track?.duplicate);
|
||||
const subtitleType = String(
|
||||
sourceMeta?.subtitleType
|
||||
|| track?.subtitleType
|
||||
|| (sourceMeta?.forcedTrack ? 'forced' : (track?.forcedTrack ? 'forced' : 'full'))
|
||||
).trim().toLowerCase() === 'forced'
|
||||
? 'forced'
|
||||
: 'full';
|
||||
const rawConfidence = String(sourceMeta?.sourceConfidence || track?.sourceConfidence || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const confidenceSource = String(sourceMeta?.confidenceSource || track?.confidenceSource || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
let sourceConfidence = null;
|
||||
if (subtitleType === 'forced') {
|
||||
if (rawConfidence === 'high' || rawConfidence === 'medium' || rawConfidence === 'low') {
|
||||
sourceConfidence = rawConfidence;
|
||||
} else if (confidenceSource === 'title') {
|
||||
sourceConfidence = 'medium';
|
||||
} else {
|
||||
sourceConfidence = 'low';
|
||||
}
|
||||
}
|
||||
const subtitlePreviewBurnIn = Boolean(sourceMeta?.subtitlePreviewBurnIn ?? track?.subtitlePreviewBurnIn);
|
||||
const subtitlePreviewForced = Boolean(
|
||||
sourceMeta?.subtitlePreviewForced
|
||||
?? track?.subtitlePreviewForced
|
||||
?? (selectedByRule && subtitleType === 'forced')
|
||||
);
|
||||
const subtitlePreviewForcedOnly = Boolean(
|
||||
sourceMeta?.subtitlePreviewForcedOnly
|
||||
?? track?.subtitlePreviewForcedOnly
|
||||
);
|
||||
const isForcedOnly = Boolean(
|
||||
sourceMeta?.isForcedOnly
|
||||
?? track?.isForcedOnly
|
||||
?? sourceMeta?.forcedOnly
|
||||
?? track?.forcedOnly
|
||||
?? subtitlePreviewForcedOnly
|
||||
?? subtitleType === 'forced'
|
||||
);
|
||||
const fullHasForced = !isForcedOnly && Boolean(
|
||||
sourceMeta?.fullHasForced
|
||||
?? track?.fullHasForced
|
||||
?? sourceMeta?.subtitleFullHasForced
|
||||
?? track?.subtitleFullHasForced
|
||||
);
|
||||
const subtitlePreviewDefaultTrack = Boolean(
|
||||
sourceMeta?.subtitlePreviewDefaultTrack
|
||||
?? track?.subtitlePreviewDefaultTrack
|
||||
?? sourceMeta?.defaultFlag
|
||||
?? track?.defaultFlag
|
||||
);
|
||||
const subtitlePreviewFlags = Array.isArray(sourceMeta?.subtitlePreviewFlags)
|
||||
? sourceMeta.subtitlePreviewFlags
|
||||
: (Array.isArray(track?.subtitlePreviewFlags) ? track.subtitlePreviewFlags : []);
|
||||
const subtitlePreviewSummary = String(
|
||||
sourceMeta?.subtitlePreviewSummary
|
||||
|| track?.subtitlePreviewSummary
|
||||
|| (selectedByRule
|
||||
? 'Übernehmen'
|
||||
: (duplicate ? 'Nicht übernommen (Duplikat)' : 'Nicht übernommen'))
|
||||
).trim() || 'Nicht übernommen';
|
||||
const selectedForEncode = selectedByRule;
|
||||
return {
|
||||
...track,
|
||||
id: sourceTrackId || track?.id || null,
|
||||
sourceTrackId: sourceTrackId || track?.sourceTrackId || track?.id || null,
|
||||
language: sourceMeta?.language || track?.language || 'und',
|
||||
languageLabel: sourceMeta?.languageLabel || track?.languageLabel || track?.language || 'und',
|
||||
title: sourceMeta?.title ?? track?.title ?? null,
|
||||
format: sourceMeta?.format || track?.format || null,
|
||||
defaultFlag: Boolean(sourceMeta?.defaultFlag ?? track?.defaultFlag),
|
||||
eventCount: Number.isFinite(Number(sourceMeta?.eventCount))
|
||||
? Math.trunc(Number(sourceMeta.eventCount))
|
||||
: (Number.isFinite(Number(track?.eventCount)) ? Math.trunc(Number(track.eventCount)) : null),
|
||||
streamSizeBytes: Number.isFinite(Number(sourceMeta?.streamSizeBytes))
|
||||
? Math.trunc(Number(sourceMeta.streamSizeBytes))
|
||||
: (Number.isFinite(Number(track?.streamSizeBytes)) ? Math.trunc(Number(track.streamSizeBytes)) : null),
|
||||
forcedTrack: Boolean(sourceMeta?.forcedTrack ?? subtitlePreviewForced),
|
||||
forcedAvailable: Boolean(sourceMeta?.forcedAvailable),
|
||||
forcedSourceTrackIds: normalizeTrackIdList(sourceMeta?.forcedSourceTrackIds || []),
|
||||
isForcedOnly,
|
||||
fullHasForced,
|
||||
subtitleType,
|
||||
sourceConfidence,
|
||||
confidenceSource,
|
||||
duplicate,
|
||||
selected: selectedByRule,
|
||||
selectedByRule,
|
||||
selectedForEncode,
|
||||
subtitlePreviewSummary,
|
||||
subtitlePreviewFlags,
|
||||
subtitlePreviewBurnIn,
|
||||
subtitlePreviewForced,
|
||||
subtitlePreviewForcedOnly,
|
||||
subtitlePreviewDefaultTrack,
|
||||
burnIn: selectedForEncode ? subtitlePreviewBurnIn : false,
|
||||
forced: selectedForEncode ? subtitlePreviewForced : false,
|
||||
forcedOnly: selectedForEncode ? subtitlePreviewForcedOnly : false,
|
||||
defaultTrack: selectedForEncode ? subtitlePreviewDefaultTrack : false,
|
||||
flags: selectedForEncode ? subtitlePreviewFlags : [],
|
||||
subtitleActionSummary: selectedForEncode ? subtitlePreviewSummary : 'Nicht übernommen'
|
||||
};
|
||||
});
|
||||
|
||||
const buildNormalizedReviewTitle = (title) => {
|
||||
const candidateMeta = candidateMetaBySyntheticPath.get(String(title?.filePath || '').trim()) || null;
|
||||
const candidateTitleInfo = candidateMeta?.titleInfo || reviewTitleInfo;
|
||||
const candidatePlaylistInfo = candidateMeta?.playlistInfo || resolvePlaylistInfoFromAnalysis(playlistAnalysis, resolvedPlaylistId);
|
||||
const candidateHandBrakeTitleId = normalizeReviewTitleId(
|
||||
candidateMeta?.handBrakeTitleId
|
||||
?? resolvedHandBrakeTitleId
|
||||
?? title?.handBrakeTitleId
|
||||
?? title?.titleIndex
|
||||
?? title?.id
|
||||
?? selectedTitleForReview
|
||||
);
|
||||
const durationSeconds = Number(candidateTitleInfo?.durationSeconds || title?.durationSeconds || 0);
|
||||
const eligibleForEncode = durationSeconds >= minLengthSecondsForReview;
|
||||
const subtitleTrackMetaBySourceId = new Map(
|
||||
(Array.isArray(candidateTitleInfo?.subtitleTracks) ? candidateTitleInfo.subtitleTracks : [])
|
||||
.map((track) => {
|
||||
const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
|
||||
return sourceTrackId ? [sourceTrackId, track] : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => {
|
||||
const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
|
||||
const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null;
|
||||
const selectedByRule = Boolean(sourceMeta?.selectedByRule ?? track?.selectedByRule);
|
||||
const duplicate = Boolean(sourceMeta?.duplicate ?? track?.duplicate);
|
||||
const subtitleType = String(
|
||||
sourceMeta?.subtitleType
|
||||
|| track?.subtitleType
|
||||
|| (sourceMeta?.forcedTrack ? 'forced' : (track?.forcedTrack ? 'forced' : 'full'))
|
||||
).trim().toLowerCase() === 'forced'
|
||||
? 'forced'
|
||||
: 'full';
|
||||
const rawConfidence = String(sourceMeta?.sourceConfidence || track?.sourceConfidence || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const confidenceSource = String(sourceMeta?.confidenceSource || track?.confidenceSource || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
let sourceConfidence = null;
|
||||
if (subtitleType === 'forced') {
|
||||
if (rawConfidence === 'high' || rawConfidence === 'medium' || rawConfidence === 'low') {
|
||||
sourceConfidence = rawConfidence;
|
||||
} else if (confidenceSource === 'title') {
|
||||
sourceConfidence = 'medium';
|
||||
} else {
|
||||
sourceConfidence = 'low';
|
||||
}
|
||||
}
|
||||
const subtitlePreviewBurnIn = Boolean(sourceMeta?.subtitlePreviewBurnIn ?? track?.subtitlePreviewBurnIn);
|
||||
const subtitlePreviewForced = Boolean(
|
||||
sourceMeta?.subtitlePreviewForced
|
||||
?? track?.subtitlePreviewForced
|
||||
?? (selectedByRule && subtitleType === 'forced')
|
||||
);
|
||||
const subtitlePreviewForcedOnly = Boolean(
|
||||
sourceMeta?.subtitlePreviewForcedOnly
|
||||
?? track?.subtitlePreviewForcedOnly
|
||||
);
|
||||
const isForcedOnly = Boolean(
|
||||
sourceMeta?.isForcedOnly
|
||||
?? track?.isForcedOnly
|
||||
?? sourceMeta?.forcedOnly
|
||||
?? track?.forcedOnly
|
||||
?? subtitlePreviewForcedOnly
|
||||
?? subtitleType === 'forced'
|
||||
);
|
||||
const fullHasForced = !isForcedOnly && Boolean(
|
||||
sourceMeta?.fullHasForced
|
||||
?? track?.fullHasForced
|
||||
?? sourceMeta?.subtitleFullHasForced
|
||||
?? track?.subtitleFullHasForced
|
||||
);
|
||||
const subtitlePreviewDefaultTrack = Boolean(
|
||||
sourceMeta?.subtitlePreviewDefaultTrack
|
||||
?? track?.subtitlePreviewDefaultTrack
|
||||
?? sourceMeta?.defaultFlag
|
||||
?? track?.defaultFlag
|
||||
);
|
||||
const subtitlePreviewFlags = Array.isArray(sourceMeta?.subtitlePreviewFlags)
|
||||
? sourceMeta.subtitlePreviewFlags
|
||||
: (Array.isArray(track?.subtitlePreviewFlags) ? track.subtitlePreviewFlags : []);
|
||||
const subtitlePreviewSummary = String(
|
||||
sourceMeta?.subtitlePreviewSummary
|
||||
|| track?.subtitlePreviewSummary
|
||||
|| (selectedByRule
|
||||
? 'Übernehmen'
|
||||
: (duplicate ? 'Nicht übernommen (Duplikat)' : 'Nicht übernommen'))
|
||||
).trim() || 'Nicht übernommen';
|
||||
const selectedForEncode = selectedByRule;
|
||||
return {
|
||||
...title,
|
||||
id: normalizedHandBrakeTitleId || title?.id || null,
|
||||
handBrakeTitleId: normalizedHandBrakeTitleId || null,
|
||||
filePath: rawPath,
|
||||
fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
|
||||
durationSeconds,
|
||||
durationMinutes: Number(((durationSeconds / 60)).toFixed(2)),
|
||||
selectedByMinLength: eligibleForEncode,
|
||||
eligibleForEncode,
|
||||
sizeBytes: Number(reviewTitleInfo?.sizeBytes || title?.sizeBytes || 0),
|
||||
playlistId: resolvedPlaylistInfo.playlistId || title?.playlistId || null,
|
||||
playlistFile: resolvedPlaylistInfo.playlistFile || title?.playlistFile || null,
|
||||
playlistRecommended: Boolean(resolvedPlaylistInfo.recommended || title?.playlistRecommended),
|
||||
playlistEvaluationLabel: resolvedPlaylistInfo.evaluationLabel || title?.playlistEvaluationLabel || null,
|
||||
playlistSegmentCommand: resolvedPlaylistInfo.segmentCommand || title?.playlistSegmentCommand || null,
|
||||
playlistSegmentFiles: Array.isArray(resolvedPlaylistInfo.segmentFiles) && resolvedPlaylistInfo.segmentFiles.length > 0
|
||||
? resolvedPlaylistInfo.segmentFiles
|
||||
: (Array.isArray(title?.playlistSegmentFiles) ? title.playlistSegmentFiles : []),
|
||||
subtitleTracks
|
||||
...track,
|
||||
id: sourceTrackId || track?.id || null,
|
||||
sourceTrackId: sourceTrackId || track?.sourceTrackId || track?.id || null,
|
||||
language: sourceMeta?.language || track?.language || 'und',
|
||||
languageLabel: sourceMeta?.languageLabel || track?.languageLabel || track?.language || 'und',
|
||||
title: sourceMeta?.title ?? track?.title ?? null,
|
||||
format: sourceMeta?.format || track?.format || null,
|
||||
defaultFlag: Boolean(sourceMeta?.defaultFlag ?? track?.defaultFlag),
|
||||
eventCount: Number.isFinite(Number(sourceMeta?.eventCount))
|
||||
? Math.trunc(Number(sourceMeta.eventCount))
|
||||
: (Number.isFinite(Number(track?.eventCount)) ? Math.trunc(Number(track.eventCount)) : null),
|
||||
streamSizeBytes: Number.isFinite(Number(sourceMeta?.streamSizeBytes))
|
||||
? Math.trunc(Number(sourceMeta.streamSizeBytes))
|
||||
: (Number.isFinite(Number(track?.streamSizeBytes)) ? Math.trunc(Number(track.streamSizeBytes)) : null),
|
||||
forcedTrack: Boolean(sourceMeta?.forcedTrack ?? subtitlePreviewForced),
|
||||
forcedAvailable: Boolean(sourceMeta?.forcedAvailable),
|
||||
forcedSourceTrackIds: normalizeTrackIdList(sourceMeta?.forcedSourceTrackIds || []),
|
||||
isForcedOnly,
|
||||
fullHasForced,
|
||||
subtitleType,
|
||||
sourceConfidence,
|
||||
confidenceSource,
|
||||
duplicate,
|
||||
selected: selectedByRule,
|
||||
selectedByRule,
|
||||
selectedForEncode,
|
||||
subtitlePreviewSummary,
|
||||
subtitlePreviewFlags,
|
||||
subtitlePreviewBurnIn,
|
||||
subtitlePreviewForced,
|
||||
subtitlePreviewForcedOnly,
|
||||
subtitlePreviewDefaultTrack,
|
||||
burnIn: selectedForEncode ? subtitlePreviewBurnIn : false,
|
||||
forced: selectedForEncode ? subtitlePreviewForced : false,
|
||||
forcedOnly: selectedForEncode ? subtitlePreviewForcedOnly : false,
|
||||
defaultTrack: selectedForEncode ? subtitlePreviewDefaultTrack : false,
|
||||
flags: selectedForEncode ? subtitlePreviewFlags : [],
|
||||
subtitleActionSummary: selectedForEncode ? subtitlePreviewSummary : 'Nicht übernommen'
|
||||
};
|
||||
});
|
||||
|
||||
const encodeInputTitleId = Number(normalizedTitles[0]?.id || review.encodeInputTitleId || null) || null;
|
||||
return {
|
||||
...title,
|
||||
id: candidateHandBrakeTitleId || title?.id || null,
|
||||
handBrakeTitleId: candidateHandBrakeTitleId || null,
|
||||
filePath: rawPath,
|
||||
fileName: candidateTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
|
||||
durationSeconds,
|
||||
durationMinutes: Number((durationSeconds / 60).toFixed(2)),
|
||||
selectedByMinLength: eligibleForEncode,
|
||||
eligibleForEncode,
|
||||
sizeBytes: Number(candidateTitleInfo?.sizeBytes || title?.sizeBytes || 0),
|
||||
playlistId: candidatePlaylistInfo.playlistId || title?.playlistId || null,
|
||||
playlistFile: candidatePlaylistInfo.playlistFile || title?.playlistFile || null,
|
||||
playlistAliases: Array.isArray(candidatePlaylistInfo.playlistAliases) ? candidatePlaylistInfo.playlistAliases : [],
|
||||
playlistRecommended: Boolean(candidatePlaylistInfo.recommended || title?.playlistRecommended),
|
||||
playlistEvaluationLabel: candidatePlaylistInfo.evaluationLabel || title?.playlistEvaluationLabel || null,
|
||||
playlistSegmentCommand: candidatePlaylistInfo.segmentCommand || title?.playlistSegmentCommand || null,
|
||||
playlistSegmentFiles: Array.isArray(candidatePlaylistInfo.segmentFiles) && candidatePlaylistInfo.segmentFiles.length > 0
|
||||
? candidatePlaylistInfo.segmentFiles
|
||||
: (Array.isArray(title?.playlistSegmentFiles) ? title.playlistSegmentFiles : []),
|
||||
subtitleTracks
|
||||
};
|
||||
};
|
||||
|
||||
const normalizedTitles = (Array.isArray(review.titles) ? review.titles : [])
|
||||
.map((title) => buildNormalizedReviewTitle(title))
|
||||
.filter((title) => normalizeReviewTitleId(title?.id));
|
||||
|
||||
const encodeInputTitleId = normalizeReviewTitleId(
|
||||
normalizedTitles.find((title) => Boolean(title?.playlistId && normalizePlaylistId(title.playlistId) === normalizePlaylistId(resolvedPlaylistId)))?.id
|
||||
|| normalizedTitles.find((title) => normalizeReviewTitleId(title?.handBrakeTitleId) === normalizeReviewTitleId(resolvedHandBrakeTitleId))?.id
|
||||
|| normalizedTitles[0]?.id
|
||||
|| review?.encodeInputTitleId
|
||||
|| null
|
||||
);
|
||||
review = {
|
||||
...review,
|
||||
mode,
|
||||
@@ -19644,8 +19922,8 @@ class PipelineService extends EventEmitter {
|
||||
sourceJobId: options.sourceJobId || null,
|
||||
reviewConfirmed: false,
|
||||
partial: false,
|
||||
processedFiles: 1,
|
||||
totalFiles: 1,
|
||||
processedFiles: normalizedTitles.length,
|
||||
totalFiles: normalizedTitles.length,
|
||||
handBrakeTitleId: resolvedHandBrakeTitleId || null,
|
||||
selectedPlaylistId: resolvedPlaylistId || null,
|
||||
selectedMakemkvTitleId: selectedTitleForReview,
|
||||
@@ -19654,12 +19932,26 @@ class PipelineService extends EventEmitter {
|
||||
selectedTitleIds: encodeInputTitleId ? [encodeInputTitleId] : [],
|
||||
encodeInputTitleId,
|
||||
encodeInputPath: rawPath,
|
||||
alternativeTitleSelection: normalizedTitles.length > 1
|
||||
? {
|
||||
enabled: true,
|
||||
reason: 'same_or_longer_titles',
|
||||
matchedTitleId: encodeInputTitleId || null,
|
||||
matchedPlaylistId: normalizePlaylistId(resolvedPlaylistId) || null,
|
||||
matchedDurationSeconds: Number(reviewTitleInfo?.durationSeconds || 0) || 0,
|
||||
optionCount: normalizedTitles.length
|
||||
}
|
||||
: null,
|
||||
notes: [
|
||||
...(Array.isArray(review.notes) ? review.notes : []),
|
||||
'MakeMKV Full-Analyse wurde einmal für Playlist-/Titel-Mapping verwendet.',
|
||||
`HandBrake Track-Analyse aktiv: ${toPlaylistFile(resolvedPlaylistId)} -> -t ${resolvedHandBrakeTitleId} (aus --scan -t 0).`
|
||||
`HandBrake Track-Analyse aktiv: ${toPlaylistFile(resolvedPlaylistId)} -> -t ${resolvedHandBrakeTitleId} (aus --scan -t 0).`,
|
||||
...(normalizedTitles.length > 1
|
||||
? ['Weitere gleichlange oder längere Titel wurden gefunden. In der Review kann zwischen alternativen Schnittfassungen/Playlist-Varianten umgeschaltet werden.']
|
||||
: [])
|
||||
]
|
||||
};
|
||||
review = applyEncodeTitleSelectionToPlan(review, encodeInputTitleId, encodeInputTitleId ? [encodeInputTitleId] : []).plan;
|
||||
|
||||
const reviewPrefillResult = applyPreviousSelectionDefaultsToReviewPlan(
|
||||
review,
|
||||
@@ -25796,6 +26088,14 @@ class PipelineService extends EventEmitter {
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
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;
|
||||
let handBrakeTitleId = null;
|
||||
let directoryInput = false;
|
||||
try {
|
||||
@@ -25807,7 +26107,12 @@ class PipelineService extends EventEmitter {
|
||||
handBrakeTitleId = null;
|
||||
}
|
||||
if (directoryInput) {
|
||||
const reviewMappedTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId);
|
||||
const reviewMappedTitleId = normalizeReviewTitleId(
|
||||
primaryPlanTitle?.handBrakeTitleId
|
||||
?? primaryPlanTitle?.titleIndex
|
||||
?? encodePlan?.handBrakeTitleId
|
||||
?? null
|
||||
);
|
||||
if (reviewMappedTitleId) {
|
||||
handBrakeTitleId = reviewMappedTitleId;
|
||||
await historyService.appendLog(
|
||||
@@ -25967,14 +26272,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user