0.12.0-1 Plugins und diverses
This commit is contained in:
@@ -91,6 +91,21 @@ function normalizePositiveInteger(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeCdText(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFC')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isPlaceholderCdTrackTitle(value) {
|
||||
const normalized = normalizeCdText(value).toLowerCase();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
return /^track\s*\d+$/i.test(normalized);
|
||||
}
|
||||
|
||||
function formatDurationSeconds(totalSeconds) {
|
||||
const parsed = Number(totalSeconds);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
@@ -352,7 +367,11 @@ function resolveCdDetails(job) {
|
||||
selectedMetadata?.mbId
|
||||
|| selectedMetadata?.musicBrainzId
|
||||
|| selectedMetadata?.musicbrainzId
|
||||
|| selectedMetadata?.musicbrainz_id
|
||||
|| selectedMetadata?.music_brainz_id
|
||||
|| selectedMetadata?.musicbrainz
|
||||
|| selectedMetadata?.mbid
|
||||
|| job?.imdb_id
|
||||
|| ''
|
||||
).trim() || null;
|
||||
|
||||
@@ -568,11 +587,13 @@ export default function JobDetailDialog({
|
||||
onResumeReady,
|
||||
onRestartEncode,
|
||||
onRestartReview,
|
||||
onRestartCdReview,
|
||||
onReencode,
|
||||
onRetry,
|
||||
onDeleteFiles,
|
||||
onDeleteEntry,
|
||||
onDownloadArchive,
|
||||
onDownloadOutputFolder,
|
||||
onRemoveFromQueue,
|
||||
isQueued = false,
|
||||
omdbAssignBusy = false,
|
||||
@@ -580,7 +601,8 @@ export default function JobDetailDialog({
|
||||
actionBusy = false,
|
||||
reencodeBusy = false,
|
||||
deleteEntryBusy = false,
|
||||
downloadBusyTarget = null
|
||||
downloadBusyTarget = null,
|
||||
downloadFolderBusyPath = null
|
||||
}) {
|
||||
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
|
||||
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(job?.status);
|
||||
@@ -588,8 +610,76 @@ export default function JobDetailDialog({
|
||||
const mediaType = resolveMediaType(job);
|
||||
const isCd = mediaType === 'cd';
|
||||
const isAudiobook = mediaType === 'audiobook';
|
||||
const canReencode = !!(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true && !running
|
||||
&& (isCd || mkDone));
|
||||
const cdRawPathCandidates = [
|
||||
job?.raw_path,
|
||||
job?.makemkvInfo?.rawPath,
|
||||
job?.makemkvInfo?.importContext?.requestedRawPath,
|
||||
job?.makemkvInfo?.importContext?.originalRawPath
|
||||
]
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean);
|
||||
const hasCdRawInputCandidate = Boolean(job?.rawStatus?.exists) || cdRawPathCandidates.length > 0;
|
||||
const hasReencodeRawInput = isCd
|
||||
? hasCdRawInputCandidate
|
||||
: Boolean(job?.rawStatus?.exists && job?.rawStatus?.isEmpty !== true);
|
||||
const canReencode = !!(hasReencodeRawInput && !running && (isCd || mkDone));
|
||||
// For CDs: direct re-encode requires track data from the last run
|
||||
const cdEncodePlan = isCd && job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : null;
|
||||
const cdSelectedMeta = isCd && job?.makemkvInfo?.selectedMetadata && typeof job.makemkvInfo.selectedMetadata === 'object'
|
||||
? job.makemkvInfo.selectedMetadata
|
||||
: {};
|
||||
const cdPlanTracks = isCd && Array.isArray(cdEncodePlan?.tracks)
|
||||
? cdEncodePlan.tracks
|
||||
: [];
|
||||
const cdSelectedTrackPositions = isCd
|
||||
? (
|
||||
Array.isArray(cdEncodePlan?.selectedTracks) && cdEncodePlan.selectedTracks.length > 0
|
||||
? cdEncodePlan.selectedTracks
|
||||
: cdPlanTracks.filter((track) => track?.selected !== false).map((track) => track?.position)
|
||||
)
|
||||
.map((value) => normalizePositiveInteger(value))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const cdTrackByPosition = new Map(
|
||||
cdPlanTracks
|
||||
.map((track) => {
|
||||
const position = normalizePositiveInteger(track?.position);
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
return [position, track];
|
||||
})
|
||||
.filter(Boolean)
|
||||
);
|
||||
const cdHasCoreMetadata = Boolean(
|
||||
normalizeCdText(cdSelectedMeta?.title || cdSelectedMeta?.album || job?.title || job?.detected_title)
|
||||
&& normalizeCdText(cdSelectedMeta?.artist)
|
||||
);
|
||||
const cdHasMeaningfulTrackTitles = cdSelectedTrackPositions.some((position) => {
|
||||
const track = cdTrackByPosition.get(position);
|
||||
const title = normalizeCdText(track?.title);
|
||||
return Boolean(title) && !isPlaceholderCdTrackTitle(title);
|
||||
});
|
||||
const cdHasPriorRunEvidence = Boolean(
|
||||
cdEncodePlan?.directReencodeReady
|
||||
|| (Array.isArray(job?.handbrakeInfo?.tracks) && job.handbrakeInfo.tracks.length > 0)
|
||||
|| String(job?.handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS'
|
||||
);
|
||||
const cdHasLastRunData = Boolean(
|
||||
cdEncodePlan
|
||||
&& Array.isArray(cdEncodePlan.tracks)
|
||||
&& cdEncodePlan.tracks.length > 0
|
||||
&& cdEncodePlan.format
|
||||
);
|
||||
const canCdDirectEncode = Boolean(
|
||||
canReencode
|
||||
&& cdHasLastRunData
|
||||
&& cdHasCoreMetadata
|
||||
&& cdHasMeaningfulTrackTitles
|
||||
&& cdHasPriorRunEvidence
|
||||
);
|
||||
const canCdStartReview = !!(hasCdRawInputCandidate && !running
|
||||
&& isCd && typeof onRestartCdReview === 'function');
|
||||
const canResumeReady = Boolean(
|
||||
(String(job?.status || '').trim().toUpperCase() === 'READY_TO_ENCODE' || String(job?.last_state || '').trim().toUpperCase() === 'READY_TO_ENCODE')
|
||||
&& !running
|
||||
@@ -634,7 +724,9 @@ export default function JobDetailDialog({
|
||||
const mediaTypeAlt = mediaTypeLabel;
|
||||
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
|
||||
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
|
||||
const pluginExecution = resolveJobPluginExecution(job, null);
|
||||
const pluginExecution = resolveJobPluginExecution(job, null, {
|
||||
currentStatus: job?.status
|
||||
});
|
||||
const configuredSelection = buildConfiguredScriptAndChainSelection(job);
|
||||
const hasConfiguredSelection = configuredSelection.preScriptIds.length > 0
|
||||
|| configuredSelection.postScriptIds.length > 0
|
||||
@@ -647,6 +739,8 @@ export default function JobDetailDialog({
|
||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
|
||||
const outputFolders = Array.isArray(job?.outputFolders) ? job.outputFolders : [];
|
||||
const hasAnyOutputFolder = outputFolders.length > 0 || Boolean(job?.outputStatus?.exists);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -867,13 +961,34 @@ export default function JobDetailDialog({
|
||||
downloadDisabled={!canDownloadRaw}
|
||||
downloadLoading={downloadBusyTarget === 'raw'}
|
||||
/>
|
||||
<PathField
|
||||
label="Output:"
|
||||
value={job.output_path}
|
||||
onDownload={canDownloadOutput ? () => onDownloadArchive?.(job, 'output') : null}
|
||||
downloadDisabled={!canDownloadOutput}
|
||||
downloadLoading={downloadBusyTarget === 'output'}
|
||||
/>
|
||||
{outputFolders.length > 0 ? (
|
||||
outputFolders.map((folder, idx) => {
|
||||
const folderPath = String(folder?.output_path || '').trim();
|
||||
if (!folderPath) {
|
||||
return null;
|
||||
}
|
||||
const label = outputFolders.length > 1 ? `Output ${idx + 1}:` : 'Output:';
|
||||
const canDownloadFolder = Boolean(typeof onDownloadOutputFolder === 'function');
|
||||
return (
|
||||
<PathField
|
||||
key={folder.id || folderPath}
|
||||
label={label}
|
||||
value={folderPath}
|
||||
onDownload={canDownloadFolder ? () => onDownloadOutputFolder?.(job, folderPath) : null}
|
||||
downloadDisabled={!canDownloadFolder}
|
||||
downloadLoading={downloadFolderBusyPath === folderPath}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<PathField
|
||||
label="Output:"
|
||||
value={job.output_path}
|
||||
onDownload={canDownloadOutput ? () => onDownloadArchive?.(job, 'output') : null}
|
||||
downloadDisabled={!canDownloadOutput}
|
||||
downloadLoading={downloadBusyTarget === 'output'}
|
||||
/>
|
||||
)}
|
||||
{job.error_message ? (
|
||||
<div><strong>Fehler:</strong> {job.error_message}</div>
|
||||
) : null}
|
||||
@@ -1003,13 +1118,7 @@ export default function JobDetailDialog({
|
||||
{tracksRaw.map((track, i) => {
|
||||
const pos = Number(track.position ?? (i + 1));
|
||||
const label = String(track.title || track.name || `Track ${pos}`).trim();
|
||||
const artist = String(track.artist || '').trim();
|
||||
const durationSec = Number(track.durationMs || 0) > 0
|
||||
? Number(track.durationMs) / 1000
|
||||
: Number(track.durationSec || 0);
|
||||
const durLabel = durationSec > 0
|
||||
? `${Math.floor(durationSec / 60)}:${String(Math.floor(durationSec % 60)).padStart(2, '0')} min`
|
||||
: '-';
|
||||
const artist = String(track.artist || '').trim() || String(job?.makemkvInfo?.selectedMetadata?.artist || '').trim();
|
||||
const selected = selectedSet ? selectedSet.has(pos) : track.selected !== false;
|
||||
const encodeResult = encodeResultMap.get(pos);
|
||||
const outcome = !selected
|
||||
@@ -1018,8 +1127,8 @@ export default function JobDetailDialog({
|
||||
? (encodeResult.success ? 'success' : 'error')
|
||||
: (job?.ripSuccessful != null ? (job.ripSuccessful ? 'success' : 'error') : null);
|
||||
return (
|
||||
<div key={pos} className="track-item">
|
||||
<span>#{pos} | {label}{artist ? ` | ${artist}` : ''} | {durLabel}</span>
|
||||
<div key={pos} className="track-item track-item-inline-status">
|
||||
<span className="track-item-main">#{pos} {artist ? `${artist} - ` : ''}{label}</span>
|
||||
<SelectionStateNote selected={selected} outcome={outcome} />
|
||||
</div>
|
||||
);
|
||||
@@ -1070,8 +1179,8 @@ export default function JobDetailDialog({
|
||||
: stepStatus === 'ERROR' ? 'tone-no'
|
||||
: '';
|
||||
return (
|
||||
<div key={id} className="track-item">
|
||||
<span>#{id} | {label} | {durLabel}</span>
|
||||
<div key={id} className={`track-item${isSplitAudiobook ? ' track-item-inline-status' : ''}`}>
|
||||
<span className={isSplitAudiobook ? 'track-item-main' : undefined}>#{id} | {label} | {durLabel}</span>
|
||||
{isSplitAudiobook ? (
|
||||
<SelectionStateNote selected outcome={outcome} />
|
||||
) : (
|
||||
@@ -1164,25 +1273,40 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onReencode?.(job)}
|
||||
loading={reencodeBusy}
|
||||
disabled={!canReencode || typeof onReencode !== 'function'}
|
||||
disabled={!canCdDirectEncode || typeof onReencode !== 'function'}
|
||||
/>
|
||||
<span className="action-desc">Encodiert die vorhandenen WAV-Rohdaten erneut — ohne die CD neu zu lesen. Nützlich wenn sich Format oder Metadaten geändert haben.</span>
|
||||
<span className="action-desc">
|
||||
{canCdDirectEncode
|
||||
? 'Encodiert die vorhandenen WAV-Rohdaten mit den zuletzt bestätigten Einstellungen erneut — ohne die CD neu zu lesen.'
|
||||
: 'Direktes Encoding ist gesperrt, solange kein bestätigter Vorlauf mit vollständigen Metadaten vorhanden ist. Bitte zuerst "Vorprüfung starten".'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Vorprüfung starten"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onRestartCdReview?.(job)}
|
||||
loading={actionBusy}
|
||||
disabled={!canCdStartReview}
|
||||
/>
|
||||
<span className="action-desc">Öffnet den Vorprüfungs-Workflow: MusicBrainz-Suche, Trackauswahl, Ausgabeeinstellungen — dann Encode aus vorhandenen WAV-Daten starten.</span>
|
||||
</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="MusicBrainz neu zuweisen"
|
||||
icon="pi pi-tag"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onAssignCdMetadata?.(job)}
|
||||
loading={cdMetadataAssignBusy}
|
||||
disabled={running || typeof onAssignCdMetadata !== 'function'}
|
||||
/>
|
||||
<span className="action-desc">Öffnet die MusicBrainz-Suche, um Album-Metadaten (Titel, Interpret, Tracks) neu zuzuweisen.</span>
|
||||
</div>
|
||||
{typeof onAssignCdMetadata === 'function' ? (
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="MusicBrainz neu zuweisen"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onAssignCdMetadata?.(job)}
|
||||
loading={cdMetadataAssignBusy}
|
||||
disabled={running}
|
||||
/>
|
||||
<span className="action-desc">Öffnet die MusicBrainz-Suche, um Album-Metadaten (Titel, Interpret, Tracks) neu zuzuweisen.</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="actions-group">
|
||||
@@ -1294,7 +1418,7 @@ export default function JobDetailDialog({
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(job, 'movie')}
|
||||
loading={actionBusy}
|
||||
disabled={!job.outputStatus?.exists || typeof onDeleteFiles !== 'function'}
|
||||
disabled={!hasAnyOutputFolder || typeof onDeleteFiles !== 'function'}
|
||||
/>
|
||||
<span className="action-desc">Löscht {isCd ? 'die fertig gerippten Audiodateien' : isAudiobook ? 'die fertig konvertierte Audiobook-Ausgabe' : 'die fertig encodierte Filmdatei'}.</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user