Bugfix and Docs

This commit is contained in:
2026-03-10 13:12:57 +00:00
parent 3516ff8486
commit ac4d77dddf
75 changed files with 3511 additions and 5142 deletions

View File

@@ -65,6 +65,23 @@ function isBurnedSubtitleTrack(track) {
);
}
function isForcedOnlySubtitleTrack(track) {
const summary = `${track?.title || ''} ${track?.description || ''} ${track?.languageLabel || ''}`.toLowerCase();
return Boolean(
track?.forcedTrack
|| /forced only/.test(summary)
|| /nur erzwungen/.test(summary)
|| /\berzwungen\b/.test(summary)
);
}
function hasForcedSubtitleAvailable(track) {
const sourceTrackIds = normalizeTrackIdList(
Array.isArray(track?.forcedSourceTrackIds) ? track.forcedSourceTrackIds : []
);
return Boolean(track?.forcedAvailable || sourceTrackIds.length > 0);
}
function splitArgs(input) {
if (!input || typeof input !== 'string') {
return [];
@@ -601,6 +618,8 @@ function TrackList({
const displayAudioTitle = audioChannelLabel(track.channels);
const audioVariant = type === 'audio' ? extractAudioVariant(displayHint) : '';
const disabled = !allowSelection || (type === 'subtitle' && burned);
const forcedOnlyTrack = type === 'subtitle' ? isForcedOnlySubtitleTrack(track) : false;
const forcedAvailable = type === 'subtitle' ? hasForcedSubtitleAvailable(track) : false;
let displayText = `#${track.id} | ${displayLanguage} | ${displayCodec}`;
if (type === 'audio') {
@@ -616,6 +635,10 @@ function TrackList({
}
if (type === 'subtitle' && burned) {
displayText += ' | burned';
} else if (type === 'subtitle' && forcedOnlyTrack) {
displayText += ' | forced-only';
} else if (type === 'subtitle' && forcedAvailable) {
displayText += ' | forced verfügbar';
}
return (
@@ -1074,7 +1097,6 @@ export default function MediaInfoReviewPanel({
allowTrackSelection
&& allowTitleSelection
&& titleChecked
&& titleEligible
);
return (
@@ -1090,7 +1112,7 @@ export default function MediaInfoReviewPanel({
onSelectEncodeTitle(normalizeTitleId(title.id));
}}
readOnly={!allowTitleSelection}
disabled={!allowTitleSelection || !titleEligible}
disabled={!allowTitleSelection}
/>
<span>
#{title.id} | {title.fileName} | {formatDuration(title.durationMinutes)} | {formatBytes(title.sizeBytes)}

View File

@@ -24,6 +24,18 @@ function normalizePlaylistId(value) {
return match ? String(match[1]).padStart(5, '0') : null;
}
function formatDurationClock(seconds) {
const total = Number(seconds || 0);
if (!Number.isFinite(total) || total <= 0) {
return null;
}
const rounded = Math.max(0, Math.trunc(total));
const h = Math.floor(rounded / 3600);
const m = Math.floor((rounded % 3600) / 60);
const s = rounded % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
function normalizeTrackId(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -376,10 +388,18 @@ export default function PipelineStatusCard({
item?.structuralMetrics?.sequenceCoherence ?? item?.sequenceCoherence
);
const handBrakeTitleId = Number(item?.handBrakeTitleId);
const durationSecondsRaw = Number(item?.durationSeconds ?? item?.duration ?? 0);
const durationSeconds = Number.isFinite(durationSecondsRaw) && durationSecondsRaw > 0
? Math.trunc(durationSecondsRaw)
: 0;
const durationLabelRaw = String(item?.durationLabel || '').trim();
const durationLabel = durationLabelRaw || formatDurationClock(durationSeconds);
return {
playlistId,
playlistFile,
titleId: Number.isFinite(Number(item?.titleId)) ? Number(item.titleId) : null,
durationSeconds,
durationLabel: durationLabel || null,
score: Number.isFinite(score) ? score : null,
evaluationLabel: item?.evaluationLabel || null,
segmentCommand: item?.segmentCommand
@@ -688,6 +708,7 @@ export default function PipelineStatusCard({
<span>
{row.playlistFile}
{row.titleId !== null ? ` | Titel #${row.titleId}` : ''}
{row.durationLabel ? ` | Dauer ${row.durationLabel}` : ''}
{row.score !== null ? ` | Score ${row.score}` : ''}
{row.recommended ? ' | empfohlen' : ''}
</span>
@@ -757,7 +778,7 @@ export default function PipelineStatusCard({
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
<div className="mediainfo-review-block">
<h3>Titel-/Spurprüfung</h3>
{state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked ? (
{state === 'READY_TO_ENCODE' && !queueLocked ? (
<small>
{isPreRipReview
? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
@@ -770,9 +791,9 @@ export default function PipelineStatusCard({
presetDisplayValue={presetDisplayValue}
commandOutputPath={commandOutputPath}
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
allowTitleSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
allowTitleSelection={state === 'READY_TO_ENCODE' && !queueLocked}
onSelectEncodeTitle={(titleId) => setSelectedEncodeTitleId(normalizeTitleId(titleId))}
allowTrackSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
allowTrackSelection={state === 'READY_TO_ENCODE' && !queueLocked}
trackSelectionByTitle={trackSelectionByTitle}
onTrackSelectionChange={(titleId, trackType, trackId, checked) => {
const normalizedTitleId = normalizeTitleId(titleId);
@@ -808,7 +829,7 @@ export default function PipelineStatusCard({
userPresets={filteredUserPresets}
selectedUserPresetId={selectedUserPresetId}
onUserPresetChange={(presetId) => setSelectedUserPresetId(presetId)}
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !reviewConfirmed && !queueLocked}
allowEncodeItemSelection={state === 'READY_TO_ENCODE' && !queueLocked}
onAddPreEncodeItem={(itemType) => {
setPreEncodeItems((prev) => {
const current = Array.isArray(prev) ? prev : [];