final dev
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
@@ -32,6 +32,23 @@ function StatusBadge({ status }) {
|
||||
return <span className={`cron-status cron-status--${info.cls}`}>{info.label}</span>;
|
||||
}
|
||||
|
||||
function normalizeActiveCronRuns(rawPayload) {
|
||||
const payload = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
|
||||
const active = Array.isArray(payload.active) ? payload.active : [];
|
||||
return active
|
||||
.map((item) => (item && typeof item === 'object' ? item : null))
|
||||
.filter(Boolean)
|
||||
.filter((item) => String(item.type || '').trim().toLowerCase() === 'cron')
|
||||
.map((item) => ({
|
||||
id: Number(item.id),
|
||||
cronJobId: Number(item.cronJobId || 0),
|
||||
currentStep: String(item.currentStep || '').trim() || null,
|
||||
currentScriptName: String(item.currentScriptName || '').trim() || null,
|
||||
startedAt: item.startedAt || null
|
||||
}))
|
||||
.filter((item) => Number.isFinite(item.cronJobId) && item.cronJobId > 0);
|
||||
}
|
||||
|
||||
const EMPTY_FORM = {
|
||||
name: '',
|
||||
cronExpression: '',
|
||||
@@ -50,6 +67,7 @@ export default function CronJobsTab({ onWsMessage }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scripts, setScripts] = useState([]);
|
||||
const [chains, setChains] = useState([]);
|
||||
const [activeCronRuns, setActiveCronRuns] = useState([]);
|
||||
|
||||
// Editor-Dialog
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
@@ -76,14 +94,18 @@ export default function CronJobsTab({ onWsMessage }) {
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [cronResp, scriptsResp, chainsResp] = await Promise.allSettled([
|
||||
const [cronResp, scriptsResp, chainsResp, runtimeResp] = await Promise.allSettled([
|
||||
api.getCronJobs(),
|
||||
api.getScripts(),
|
||||
api.getScriptChains()
|
||||
api.getScriptChains(),
|
||||
api.getRuntimeActivities()
|
||||
]);
|
||||
if (cronResp.status === 'fulfilled') setJobs(cronResp.value?.jobs || []);
|
||||
if (scriptsResp.status === 'fulfilled') setScripts(scriptsResp.value?.scripts || []);
|
||||
if (chainsResp.status === 'fulfilled') setChains(chainsResp.value?.chains || []);
|
||||
if (runtimeResp.status === 'fulfilled') {
|
||||
setActiveCronRuns(normalizeActiveCronRuns(runtimeResp.value));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -93,6 +115,36 @@ export default function CronJobsTab({ onWsMessage }) {
|
||||
loadAll();
|
||||
}, [loadAll]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const refreshRuntime = async () => {
|
||||
try {
|
||||
const response = await api.getRuntimeActivities();
|
||||
if (!cancelled) {
|
||||
setActiveCronRuns(normalizeActiveCronRuns(response));
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore polling errors
|
||||
}
|
||||
};
|
||||
const interval = setInterval(refreshRuntime, 2500);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const activeCronRunByJobId = useMemo(() => {
|
||||
const map = new Map();
|
||||
for (const item of activeCronRuns) {
|
||||
if (!item?.cronJobId) {
|
||||
continue;
|
||||
}
|
||||
map.set(item.cronJobId, item);
|
||||
}
|
||||
return map;
|
||||
}, [activeCronRuns]);
|
||||
|
||||
// WebSocket: Cronjob-Updates empfangen
|
||||
useEffect(() => {
|
||||
if (!onWsMessage) return;
|
||||
@@ -279,6 +331,7 @@ export default function CronJobsTab({ onWsMessage }) {
|
||||
<div className="cron-list">
|
||||
{jobs.map((job) => {
|
||||
const isBusy = busyId === job.id;
|
||||
const activeCronRun = activeCronRunByJobId.get(Number(job.id)) || null;
|
||||
return (
|
||||
<div key={job.id} className={`cron-item${job.enabled ? '' : ' cron-item--disabled'}`}>
|
||||
<div className="cron-item-header">
|
||||
@@ -305,6 +358,17 @@ export default function CronJobsTab({ onWsMessage }) {
|
||||
<span className="cron-meta-label">Nächster Lauf:</span>
|
||||
<span className="cron-meta-value">{formatDateTime(job.nextRunAt)}</span>
|
||||
</span>
|
||||
{activeCronRun ? (
|
||||
<span className="cron-meta-entry">
|
||||
<span className="cron-meta-label">Aktuell:</span>
|
||||
<span className="cron-meta-value">
|
||||
<StatusBadge status="running" />
|
||||
{activeCronRun.currentScriptName
|
||||
? `Skript: ${activeCronRun.currentScriptName}`
|
||||
: (activeCronRun.currentStep || 'Ausführung läuft')}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="cron-item-toggles">
|
||||
|
||||
@@ -54,6 +54,127 @@ function ScriptSummarySection({ title, summary }) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeIdList(values) {
|
||||
const list = Array.isArray(values) ? values : [];
|
||||
const seen = new Set();
|
||||
const output = [];
|
||||
for (const value of list) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
continue;
|
||||
}
|
||||
const id = Math.trunc(parsed);
|
||||
const key = String(id);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
output.push(id);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
const raw = String(value ?? '');
|
||||
if (raw.length === 0) {
|
||||
return "''";
|
||||
}
|
||||
if (/^[A-Za-z0-9_./:=,+-]+$/.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
return `'${raw.replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
||||
const cmd = String(handbrakeInfo?.cmd || '').trim();
|
||||
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
|
||||
if (!cmd) {
|
||||
return null;
|
||||
}
|
||||
return `${cmd} ${args.map((arg) => shellQuote(arg)).join(' ')}`.trim();
|
||||
}
|
||||
|
||||
function buildConfiguredScriptAndChainSelection(job) {
|
||||
const plan = job?.encodePlan && typeof job.encodePlan === 'object' ? job.encodePlan : {};
|
||||
const handbrakeInfo = job?.handbrakeInfo && typeof job.handbrakeInfo === 'object' ? job.handbrakeInfo : {};
|
||||
const scriptNameById = new Map();
|
||||
const chainNameById = new Map();
|
||||
|
||||
const addScriptHint = (idValue, nameValue) => {
|
||||
const id = normalizeIdList([idValue])[0] || null;
|
||||
const name = String(nameValue || '').trim();
|
||||
if (!id || !name || scriptNameById.has(id)) {
|
||||
return;
|
||||
}
|
||||
scriptNameById.set(id, name);
|
||||
};
|
||||
|
||||
const addChainHint = (idValue, nameValue) => {
|
||||
const id = normalizeIdList([idValue])[0] || null;
|
||||
const name = String(nameValue || '').trim();
|
||||
if (!id || !name || chainNameById.has(id)) {
|
||||
return;
|
||||
}
|
||||
chainNameById.set(id, name);
|
||||
};
|
||||
|
||||
const addScriptHintsFromList = (list) => {
|
||||
for (const item of (Array.isArray(list) ? list : [])) {
|
||||
addScriptHint(item?.id ?? item?.scriptId, item?.name ?? item?.scriptName);
|
||||
}
|
||||
};
|
||||
|
||||
const addChainHintsFromList = (list) => {
|
||||
for (const item of (Array.isArray(list) ? list : [])) {
|
||||
addChainHint(item?.id ?? item?.chainId, item?.name ?? item?.chainName);
|
||||
}
|
||||
};
|
||||
|
||||
addScriptHintsFromList(plan?.preEncodeScripts);
|
||||
addScriptHintsFromList(plan?.postEncodeScripts);
|
||||
addChainHintsFromList(plan?.preEncodeChains);
|
||||
addChainHintsFromList(plan?.postEncodeChains);
|
||||
|
||||
const scriptSummaries = [handbrakeInfo?.preEncodeScripts, handbrakeInfo?.postEncodeScripts];
|
||||
for (const summary of scriptSummaries) {
|
||||
const results = Array.isArray(summary?.results) ? summary.results : [];
|
||||
for (const result of results) {
|
||||
addScriptHint(result?.scriptId, result?.scriptName);
|
||||
addChainHint(result?.chainId, result?.chainName);
|
||||
}
|
||||
}
|
||||
|
||||
const preScriptIds = normalizeIdList([
|
||||
...(Array.isArray(plan?.preEncodeScriptIds) ? plan.preEncodeScriptIds : []),
|
||||
...(Array.isArray(plan?.preEncodeScripts) ? plan.preEncodeScripts.map((item) => item?.id ?? item?.scriptId) : [])
|
||||
]);
|
||||
const postScriptIds = normalizeIdList([
|
||||
...(Array.isArray(plan?.postEncodeScriptIds) ? plan.postEncodeScriptIds : []),
|
||||
...(Array.isArray(plan?.postEncodeScripts) ? plan.postEncodeScripts.map((item) => item?.id ?? item?.scriptId) : [])
|
||||
]);
|
||||
const preChainIds = normalizeIdList([
|
||||
...(Array.isArray(plan?.preEncodeChainIds) ? plan.preEncodeChainIds : []),
|
||||
...(Array.isArray(plan?.preEncodeChains) ? plan.preEncodeChains.map((item) => item?.id ?? item?.chainId) : [])
|
||||
]);
|
||||
const postChainIds = normalizeIdList([
|
||||
...(Array.isArray(plan?.postEncodeChainIds) ? plan.postEncodeChainIds : []),
|
||||
...(Array.isArray(plan?.postEncodeChains) ? plan.postEncodeChains.map((item) => item?.id ?? item?.chainId) : [])
|
||||
]);
|
||||
|
||||
return {
|
||||
preScriptIds,
|
||||
postScriptIds,
|
||||
preChainIds,
|
||||
postChainIds,
|
||||
preScripts: preScriptIds.map((id) => scriptNameById.get(id) || `Skript #${id}`),
|
||||
postScripts: postScriptIds.map((id) => scriptNameById.get(id) || `Skript #${id}`),
|
||||
preChains: preChainIds.map((id) => chainNameById.get(id) || `Kette #${id}`),
|
||||
postChains: postChainIds.map((id) => chainNameById.get(id) || `Kette #${id}`),
|
||||
scriptCatalog: Array.from(scriptNameById.entries()).map(([id, name]) => ({ id, name })),
|
||||
chainCatalog: Array.from(chainNameById.entries()).map(([id, name]) => ({ id, name }))
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMediaType(job) {
|
||||
const candidates = [
|
||||
job?.mediaType,
|
||||
@@ -205,6 +326,25 @@ export default function JobDetailDialog({
|
||||
: (mediaType === 'dvd' ? 'DVD' : 'Sonstiges Medium');
|
||||
const statusMeta = statusBadgeMeta(job?.status, queueLocked);
|
||||
const omdbInfo = job?.omdbInfo && typeof job.omdbInfo === 'object' ? job.omdbInfo : {};
|
||||
const configuredSelection = buildConfiguredScriptAndChainSelection(job);
|
||||
const hasConfiguredSelection = configuredSelection.preScriptIds.length > 0
|
||||
|| configuredSelection.postScriptIds.length > 0
|
||||
|| configuredSelection.preChainIds.length > 0
|
||||
|| configuredSelection.postChainIds.length > 0;
|
||||
const reviewPreEncodeItems = [
|
||||
...configuredSelection.preScriptIds.map((id) => ({ type: 'script', id })),
|
||||
...configuredSelection.preChainIds.map((id) => ({ type: 'chain', id }))
|
||||
];
|
||||
const reviewPostEncodeItems = [
|
||||
...configuredSelection.postScriptIds.map((id) => ({ type: 'script', id })),
|
||||
...configuredSelection.postChainIds.map((id) => ({ type: 'chain', id }))
|
||||
];
|
||||
const encodePlanUserPreset = job?.encodePlan?.userPreset && typeof job.encodePlan.userPreset === 'object'
|
||||
? job.encodePlan.userPreset
|
||||
: null;
|
||||
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
||||
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -335,6 +475,42 @@ export default function JobDetailDialog({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{hasConfiguredSelection || encodePlanUserPreset ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Hinterlegte Encode-Auswahl</h4>
|
||||
<div className="job-configured-selection-grid">
|
||||
<div>
|
||||
<strong>Pre-Encode Skripte:</strong> {configuredSelection.preScripts.length > 0 ? configuredSelection.preScripts.join(' | ') : '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Pre-Encode Ketten:</strong> {configuredSelection.preChains.length > 0 ? configuredSelection.preChains.join(' | ') : '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Post-Encode Skripte:</strong> {configuredSelection.postScripts.length > 0 ? configuredSelection.postScripts.join(' | ') : '-'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Post-Encode Ketten:</strong> {configuredSelection.postChains.length > 0 ? configuredSelection.postChains.join(' | ') : '-'}
|
||||
</div>
|
||||
<div className="job-meta-col-span-2">
|
||||
<strong>User-Preset:</strong>{' '}
|
||||
{encodePlanUserPreset
|
||||
? `${encodePlanUserPreset.name || '-'} | Preset=${encodePlanUserPreset.handbrakePreset || '-'} | ExtraArgs=${encodePlanUserPreset.extraArgs || '-'}`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{executedHandBrakeCommand ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Ausgeführter Encode-Befehl</h4>
|
||||
<div className="handbrake-command-preview">
|
||||
<small><strong>HandBrakeCLI (tatsächlich gestartet):</strong></small>
|
||||
<pre>{executedHandBrakeCommand}</pre>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{(job.handbrakeInfo?.preEncodeScripts?.configured > 0 || job.handbrakeInfo?.postEncodeScripts?.configured > 0) ? (
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Skripte</h4>
|
||||
@@ -356,7 +532,18 @@ export default function JobDetailDialog({
|
||||
{job.encodePlan ? (
|
||||
<>
|
||||
<h4>Mediainfo-Prüfung (Auswertung)</h4>
|
||||
<MediaInfoReviewPanel review={job.encodePlan} />
|
||||
<MediaInfoReviewPanel
|
||||
review={job.encodePlan}
|
||||
commandOutputPath={job.output_path || null}
|
||||
availableScripts={configuredSelection.scriptCatalog}
|
||||
availableChains={configuredSelection.chainCatalog}
|
||||
preEncodeItems={reviewPreEncodeItems}
|
||||
postEncodeItems={reviewPostEncodeItems}
|
||||
userPresets={reviewUserPresets}
|
||||
selectedUserPresetId={Number.isFinite(encodePlanUserPresetId) && encodePlanUserPresetId > 0
|
||||
? Math.trunc(encodePlanUserPresetId)
|
||||
: null}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -684,6 +684,14 @@ function normalizeScriptId(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeChainId(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeScriptIdList(values) {
|
||||
const list = Array.isArray(values) ? values : [];
|
||||
const seen = new Set();
|
||||
@@ -762,8 +770,8 @@ export default function MediaInfoReviewPanel({
|
||||
.filter((item) => item.id !== null && item.name.length > 0);
|
||||
const scriptById = new Map(scriptCatalog.map((item) => [item.id, item]));
|
||||
const chainCatalog = (Array.isArray(availableChains) ? availableChains : [])
|
||||
.map((item) => ({ id: Number(item?.id), name: String(item?.name || '').trim() }))
|
||||
.filter((item) => Number.isFinite(item.id) && item.id > 0 && item.name.length > 0);
|
||||
.map((item) => ({ id: normalizeChainId(item?.id), name: String(item?.name || '').trim() }))
|
||||
.filter((item) => item.id !== null && item.name.length > 0);
|
||||
const chainById = new Map(chainCatalog.map((item) => [item.id, item]));
|
||||
|
||||
const makeHandleDrop = (items, onReorder) => (event, targetIndex) => {
|
||||
@@ -884,13 +892,29 @@ export default function MediaInfoReviewPanel({
|
||||
? (scriptObj?.name || `Skript #${item.id}`)
|
||||
: (chainObj?.name || `Kette #${item.id}`);
|
||||
const usedScriptIds = new Set(
|
||||
preEncodeItems.filter((it, i) => it.type === 'script' && i !== rowIndex).map((it) => String(normalizeScriptId(it.id)))
|
||||
preEncodeItems
|
||||
.filter((it, i) => it.type === 'script' && i !== rowIndex)
|
||||
.map((it) => normalizeScriptId(it.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const usedChainIds = new Set(
|
||||
preEncodeItems
|
||||
.filter((it, i) => it.type === 'chain' && i !== rowIndex)
|
||||
.map((it) => normalizeChainId(it.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const scriptOptions = scriptCatalog.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
disabled: usedScriptIds.has(String(s.id))
|
||||
}));
|
||||
const chainOptions = chainCatalog.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
disabled: usedChainIds.has(String(c.id))
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
key={`pre-item-${rowIndex}-${item.type}-${item.id}`}
|
||||
@@ -926,7 +950,15 @@ export default function MediaInfoReviewPanel({
|
||||
className="full-width"
|
||||
/>
|
||||
) : (
|
||||
<span className="post-script-chain-name">{name}</span>
|
||||
<Dropdown
|
||||
value={normalizeChainId(item.id)}
|
||||
options={chainOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePreEncodeItem?.(rowIndex, 'chain', event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
)}
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePreEncodeItem?.(rowIndex)} />
|
||||
</>
|
||||
@@ -980,13 +1012,29 @@ export default function MediaInfoReviewPanel({
|
||||
? (scriptObj?.name || `Skript #${item.id}`)
|
||||
: (chainObj?.name || `Kette #${item.id}`);
|
||||
const usedScriptIds = new Set(
|
||||
postEncodeItems.filter((it, i) => it.type === 'script' && i !== rowIndex).map((it) => String(normalizeScriptId(it.id)))
|
||||
postEncodeItems
|
||||
.filter((it, i) => it.type === 'script' && i !== rowIndex)
|
||||
.map((it) => normalizeScriptId(it.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const usedChainIds = new Set(
|
||||
postEncodeItems
|
||||
.filter((it, i) => it.type === 'chain' && i !== rowIndex)
|
||||
.map((it) => normalizeChainId(it.id))
|
||||
.filter((id) => id !== null)
|
||||
.map((id) => String(id))
|
||||
);
|
||||
const scriptOptions = scriptCatalog.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
disabled: usedScriptIds.has(String(s.id))
|
||||
}));
|
||||
const chainOptions = chainCatalog.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
disabled: usedChainIds.has(String(c.id))
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
key={`post-item-${rowIndex}-${item.type}-${item.id}`}
|
||||
@@ -1022,7 +1070,15 @@ export default function MediaInfoReviewPanel({
|
||||
className="full-width"
|
||||
/>
|
||||
) : (
|
||||
<span className="post-script-chain-name">{name}</span>
|
||||
<Dropdown
|
||||
value={normalizeChainId(item.id)}
|
||||
options={chainOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
optionDisabled="disabled"
|
||||
onChange={(event) => onChangePostEncodeItem?.(rowIndex, 'chain', event.value)}
|
||||
className="full-width"
|
||||
/>
|
||||
)}
|
||||
<Button icon="pi pi-times" severity="danger" outlined onClick={() => onRemovePostEncodeItem?.(rowIndex)} />
|
||||
</>
|
||||
|
||||
@@ -98,6 +98,46 @@ function normalizeChainId(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeMediaProfile(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (['bluray', 'blu-ray', 'blu_ray', 'bd', 'bdmv', 'bdrom', 'bd-rom', 'bd-r', 'bd-re'].includes(raw)) {
|
||||
return 'bluray';
|
||||
}
|
||||
if (['dvd', 'disc', 'dvdvideo', 'dvd-video', 'dvdrom', 'dvd-rom', 'video_ts', 'iso9660'].includes(raw)) {
|
||||
return 'dvd';
|
||||
}
|
||||
if (['other', 'sonstiges', 'unknown'].includes(raw)) {
|
||||
return 'other';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePipelineMediaProfile(pipeline, mediaInfoReview) {
|
||||
const context = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : {};
|
||||
const device = context?.device && typeof context.device === 'object' ? context.device : {};
|
||||
const review = mediaInfoReview && typeof mediaInfoReview === 'object' ? mediaInfoReview : {};
|
||||
const candidates = [
|
||||
context?.mediaProfile,
|
||||
context?.media_profile,
|
||||
review?.mediaProfile,
|
||||
review?.media_profile,
|
||||
device?.mediaProfile,
|
||||
device?.media_profile,
|
||||
device?.profile,
|
||||
device?.type
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizeMediaProfile(candidate);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isBurnedSubtitleTrack(track) {
|
||||
const flags = Array.isArray(track?.subtitlePreviewFlags)
|
||||
? track.subtitlePreviewFlags
|
||||
@@ -115,6 +155,7 @@ function isBurnedSubtitleTrack(track) {
|
||||
function buildDefaultTrackSelection(review) {
|
||||
const titles = Array.isArray(review?.titles) ? review.titles : [];
|
||||
const selection = {};
|
||||
const reviewEncodeInputTitleId = normalizeTitleId(review?.encodeInputTitleId);
|
||||
|
||||
for (const title of titles) {
|
||||
const titleId = normalizeTitleId(title?.id);
|
||||
@@ -122,15 +163,27 @@ function buildDefaultTrackSelection(review) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const audioTracks = Array.isArray(title?.audioTracks) ? title.audioTracks : [];
|
||||
const subtitleTracks = Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [];
|
||||
const isEncodeInputTitle = Boolean(
|
||||
title?.selectedForEncode
|
||||
|| title?.encodeInput
|
||||
|| (reviewEncodeInputTitleId && reviewEncodeInputTitleId === titleId)
|
||||
);
|
||||
const audioSelectionSource = isEncodeInputTitle
|
||||
? audioTracks.filter((track) => Boolean(track?.selectedForEncode))
|
||||
: audioTracks.filter((track) => Boolean(track?.selectedByRule));
|
||||
const subtitleSelectionSource = isEncodeInputTitle
|
||||
? subtitleTracks.filter((track) => Boolean(track?.selectedForEncode))
|
||||
: subtitleTracks.filter((track) => Boolean(track?.selectedByRule));
|
||||
|
||||
selection[titleId] = {
|
||||
audioTrackIds: normalizeTrackIdList(
|
||||
(Array.isArray(title?.audioTracks) ? title.audioTracks : [])
|
||||
.filter((track) => Boolean(track?.selectedByRule))
|
||||
.map((track) => track?.id)
|
||||
audioSelectionSource.map((track) => track?.id)
|
||||
),
|
||||
subtitleTrackIds: normalizeTrackIdList(
|
||||
(Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [])
|
||||
.filter((track) => Boolean(track?.selectedByRule) && !isBurnedSubtitleTrack(track))
|
||||
subtitleSelectionSource
|
||||
.filter((track) => !isBurnedSubtitleTrack(track))
|
||||
.map((track) => track?.id)
|
||||
)
|
||||
};
|
||||
@@ -235,10 +288,9 @@ export default function PipelineStatusCard({
|
||||
const mediaInfoReview = pipeline?.context?.mediaInfoReview || null;
|
||||
const playlistAnalysis = pipeline?.context?.playlistAnalysis || null;
|
||||
const encodeInputPath = pipeline?.context?.inputPath || mediaInfoReview?.encodeInputPath || null;
|
||||
const reviewConfirmed = Boolean(pipeline?.context?.reviewConfirmed || mediaInfoReview?.reviewConfirmed);
|
||||
const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
|
||||
const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
|
||||
const jobMediaProfile = String(pipeline?.context?.mediaProfile || '').trim().toLowerCase() || null;
|
||||
const jobMediaProfile = resolvePipelineMediaProfile(pipeline, mediaInfoReview);
|
||||
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
|
||||
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
|
||||
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
||||
@@ -319,8 +371,16 @@ export default function PipelineStatusCard({
|
||||
...normalizeScriptIdList(mediaInfoReview?.postEncodeScriptIds || []).map((id) => ({ type: 'script', id })),
|
||||
...normChain(mediaInfoReview?.postEncodeChainIds).map((id) => ({ type: 'chain', id }))
|
||||
]);
|
||||
setSelectedUserPresetId(null);
|
||||
}, [mediaInfoReview?.encodeInputTitleId, mediaInfoReview?.generatedAt, retryJobId]);
|
||||
const userPresetId = Number(mediaInfoReview?.userPreset?.id);
|
||||
setSelectedUserPresetId(Number.isFinite(userPresetId) && userPresetId > 0 ? Math.trunc(userPresetId) : null);
|
||||
}, [
|
||||
mediaInfoReview?.encodeInputTitleId,
|
||||
mediaInfoReview?.generatedAt,
|
||||
mediaInfoReview?.reviewConfirmedAt,
|
||||
mediaInfoReview?.prefilledFromPreviousRunAt,
|
||||
mediaInfoReview?.userPreset?.id,
|
||||
retryJobId
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTitleId = normalizeTitleId(selectedEncodeTitleId);
|
||||
@@ -348,10 +408,11 @@ export default function PipelineStatusCard({
|
||||
|
||||
// Filter user presets by job media profile ('all' presets always shown)
|
||||
const filteredUserPresets = (Array.isArray(userPresets) ? userPresets : []).filter((p) => {
|
||||
const presetMediaType = normalizeMediaProfile(p?.mediaType) || 'all';
|
||||
if (!jobMediaProfile) {
|
||||
return true;
|
||||
}
|
||||
return p.mediaType === 'all' || p.mediaType === jobMediaProfile;
|
||||
return presetMediaType === 'all' || presetMediaType === jobMediaProfile;
|
||||
});
|
||||
const canStartReadyJob = isPreRipReview
|
||||
? Boolean(retryJobId)
|
||||
@@ -592,12 +653,6 @@ export default function PipelineStatusCard({
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
onClick={async () => {
|
||||
const requiresAutoConfirm = !reviewConfirmed;
|
||||
if (!requiresAutoConfirm) {
|
||||
await onStart(retryJobId);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
encodeTitleId,
|
||||
selectedTrackSelection,
|
||||
|
||||
Reference in New Issue
Block a user