pre release

This commit is contained in:
2026-03-10 20:47:56 +00:00
parent 5703a8d00a
commit b93f5da8d7
4 changed files with 213 additions and 68 deletions

View File

@@ -99,6 +99,9 @@ router.post(
const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null; const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null; const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds; const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
const selectedPreEncodeScriptIds = req.body?.selectedPreEncodeScriptIds;
const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate); const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null; const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
logger.info('post:confirm-encode', { logger.info('post:confirm-encode', {
@@ -110,12 +113,24 @@ router.post(
selectedUserPresetId, selectedUserPresetId,
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds) selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
? selectedPostEncodeScriptIds.length ? selectedPostEncodeScriptIds.length
: 0,
selectedPreEncodeScriptIdsCount: Array.isArray(selectedPreEncodeScriptIds)
? selectedPreEncodeScriptIds.length
: 0,
selectedPostEncodeChainIdsCount: Array.isArray(selectedPostEncodeChainIds)
? selectedPostEncodeChainIds.length
: 0,
selectedPreEncodeChainIdsCount: Array.isArray(selectedPreEncodeChainIds)
? selectedPreEncodeChainIds.length
: 0 : 0
}); });
const job = await pipelineService.confirmEncodeReview(jobId, { const job = await pipelineService.confirmEncodeReview(jobId, {
selectedEncodeTitleId, selectedEncodeTitleId,
selectedTrackSelection, selectedTrackSelection,
selectedPostEncodeScriptIds, selectedPostEncodeScriptIds,
selectedPreEncodeScriptIds,
selectedPostEncodeChainIds,
selectedPreEncodeChainIds,
skipPipelineStateUpdate, skipPipelineStateUpdate,
selectedUserPresetId selectedUserPresetId
}); });

View File

@@ -98,8 +98,12 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
} }
if (fstype.includes('udf')) { if (fstype.includes('udf')) {
// UDF is used by both DVDs (UDF 1.x) and Blu-rays (UDF 2.x).
// Drive model alone (hasBlurayModelMarker) is not reliable: a BD-ROM drive
// with a DVD inside would incorrectly be detected as Blu-ray.
// Return null so UDF version detection via blkid can decide.
if (hasBlurayModelMarker) { if (hasBlurayModelMarker) {
return 'bluray'; return null;
} }
if (hasDvdModelMarker) { if (hasDvdModelMarker) {
return 'dvd'; return 'dvd';
@@ -108,9 +112,8 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
} }
if (fstype.includes('iso9660') || fstype.includes('cdfs')) { if (fstype.includes('iso9660') || fstype.includes('cdfs')) {
if (hasBlurayModelMarker) { // iso9660/cdfs is never used by Blu-ray discs (they use UDF 2.x).
return 'bluray'; // Ignore hasBlurayModelMarker it only reflects drive capability.
}
if (hasCdOnlyModelMarker) { if (hasCdOnlyModelMarker) {
return 'other'; return 'other';
} }
@@ -120,6 +123,30 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
return null; return null;
} }
function inferMediaProfileFromUdevProperties(properties = {}) {
const flags = Object.entries(properties).reduce((acc, [key, rawValue]) => {
const normalizedKey = String(key || '').trim().toUpperCase();
if (!normalizedKey) {
return acc;
}
acc[normalizedKey] = String(rawValue || '').trim();
return acc;
}, {});
const hasFlag = (prefix) => Object.entries(flags).some(([key, value]) => key.startsWith(prefix) && value === '1');
if (hasFlag('ID_CDROM_MEDIA_BD')) {
return 'bluray';
}
if (hasFlag('ID_CDROM_MEDIA_DVD')) {
return 'dvd';
}
if (hasFlag('ID_CDROM_MEDIA_CD')) {
return 'other';
}
return null;
}
class DiskDetectionService extends EventEmitter { class DiskDetectionService extends EventEmitter {
constructor() { constructor() {
super(); super();
@@ -496,31 +523,60 @@ class DiskDetectionService extends EventEmitter {
} }
} }
async inferMediaProfileFromUdev(devicePath) {
const normalizedPath = String(devicePath || '').trim();
if (!normalizedPath) {
return null;
}
try {
const { stdout } = await execFileAsync('udevadm', ['info', '--query=property', '--name', normalizedPath]);
const properties = {};
for (const line of String(stdout || '').split(/\r?\n/)) {
const idx = line.indexOf('=');
if (idx <= 0) {
continue;
}
const key = String(line.slice(0, idx)).trim();
const value = String(line.slice(idx + 1)).trim();
if (!key) {
continue;
}
properties[key] = value;
}
const inferred = inferMediaProfileFromUdevProperties(properties);
if (inferred) {
logger.debug('udev:media-profile', { devicePath: normalizedPath, inferred });
}
return inferred;
} catch (error) {
logger.debug('udev:media-profile:failed', {
devicePath: normalizedPath,
error: errorToMeta(error)
});
return null;
}
}
async inferMediaProfile(devicePath, hints = {}) { async inferMediaProfile(devicePath, hints = {}) {
const explicit = normalizeMediaProfile(hints?.mediaProfile); const explicit = normalizeMediaProfile(hints?.mediaProfile);
if (isSpecificMediaProfile(explicit)) { if (isSpecificMediaProfile(explicit)) {
return explicit; return explicit;
} }
// Only pass disc-specific fields NOT hints?.model (drive model).
// Drive model (e.g. "BD-ROM") reflects drive capability, not disc type.
// A BD-ROM drive with a DVD would otherwise be detected as Blu-ray here.
const hinted = inferMediaProfileFromTextParts([ const hinted = inferMediaProfileFromTextParts([
hints?.discLabel, hints?.discLabel,
hints?.label, hints?.label,
hints?.fstype, hints?.fstype,
hints?.model
]); ]);
if (hinted) { if (hinted) {
return hinted; return hinted;
} }
const hintFstype = String(hints?.fstype || '').trim().toLowerCase();
const byFsTypeHint = inferMediaProfileFromFsTypeAndModel(hints?.fstype, hints?.model);
// UDF is used for both Blu-ray (UDF 2.x) and DVD (UDF 1.x). Without a clear model
// marker identifying it as Blu-ray, a 'dvd' result from UDF is ambiguous. Skip the
// early return and fall through to the blkid check which uses the UDF version number.
if (byFsTypeHint && !(hintFstype.includes('udf') && byFsTypeHint !== 'bluray')) {
return byFsTypeHint;
}
const mountpoint = String(hints?.mountpoint || '').trim(); const mountpoint = String(hints?.mountpoint || '').trim();
if (mountpoint) { if (mountpoint) {
try { try {
@@ -539,8 +595,25 @@ class DiskDetectionService extends EventEmitter {
} }
} }
const byUdev = await this.inferMediaProfileFromUdev(devicePath);
if (byUdev) {
return byUdev;
}
const hintFstype = String(hints?.fstype || '').trim().toLowerCase();
const byFsTypeHint = inferMediaProfileFromFsTypeAndModel(hints?.fstype, hints?.model);
const udfHintFallback = hintFstype.includes('udf')
? inferMediaProfileFromFsTypeAndModel(hints?.fstype, null)
: null;
// UDF is used for both Blu-ray (UDF 2.x) and DVD (UDF 1.x). Without a clear model
// marker identifying it as Blu-ray, a 'dvd' result from UDF is ambiguous. Skip the
// early return and fall through to the blkid check which uses the UDF version number.
if (byFsTypeHint && !(hintFstype.includes('udf') && byFsTypeHint !== 'bluray')) {
return byFsTypeHint;
}
try { try {
const { stdout } = await execFileAsync('blkid', ['-o', 'export', devicePath]); const { stdout } = await execFileAsync('blkid', ['-p', '-o', 'export', devicePath]);
const payload = {}; const payload = {};
for (const line of String(stdout || '').split(/\r?\n/)) { for (const line of String(stdout || '').split(/\r?\n/)) {
const idx = line.indexOf('='); const idx = line.indexOf('=');
@@ -555,28 +628,40 @@ class DiskDetectionService extends EventEmitter {
payload[key] = value; payload[key] = value;
} }
// APPLICATION_ID contains disc-specific strings (e.g. "BDAV"/"BDMV" for Blu-ray,
// "DVD_VIDEO" for DVD). Drive model is excluded see reasoning above.
const byBlkidMarker = inferMediaProfileFromTextParts([ const byBlkidMarker = inferMediaProfileFromTextParts([
payload.LABEL, payload.LABEL,
payload.TYPE, payload.TYPE,
payload.VERSION, payload.VERSION,
payload.APPLICATION_ID, payload.APPLICATION_ID,
hints?.model
]); ]);
if (byBlkidMarker) { if (byBlkidMarker) {
return byBlkidMarker; return byBlkidMarker;
} }
const type = String(payload.TYPE || '').trim().toLowerCase(); const type = String(payload.TYPE || '').trim().toLowerCase();
// For UDF, VERSION is the most reliable discriminator: 1.x → DVD, 2.x → Blu-ray.
// This check must run independently of inferMediaProfileFromFsTypeAndModel so it
// is not skipped when the drive model returns null (BD-ROM drive with DVD inside).
if (type.includes('udf')) {
const version = Number.parseFloat(String(payload.VERSION || '').replace(',', '.'));
if (Number.isFinite(version)) {
return version >= 2 ? 'bluray' : 'dvd';
}
}
const byBlkidFsType = inferMediaProfileFromFsTypeAndModel(type, hints?.model); const byBlkidFsType = inferMediaProfileFromFsTypeAndModel(type, hints?.model);
if (byBlkidFsType) { if (byBlkidFsType) {
if (type.includes('udf')) {
const version = Number.parseFloat(String(payload.VERSION || '').replace(',', '.'));
if (Number.isFinite(version)) {
return version >= 2 ? 'bluray' : 'dvd';
}
}
return byBlkidFsType; return byBlkidFsType;
} }
// Last resort for drives that only expose TYPE=udf without VERSION/APPLICATION_ID:
// prefer DVD over "other" so DVDs in BD-capable drives do not fall back to Misc.
const byBlkidFsTypeWithoutModel = inferMediaProfileFromFsTypeAndModel(type, null);
if (byBlkidFsTypeWithoutModel) {
return byBlkidFsTypeWithoutModel;
}
} catch (error) { } catch (error) {
logger.debug('infer-media-profile:blkid-failed', { logger.debug('infer-media-profile:blkid-failed', {
devicePath, devicePath,
@@ -584,7 +669,11 @@ class DiskDetectionService extends EventEmitter {
}); });
} }
return explicit === 'other' ? 'other' : null; if (udfHintFallback) {
return udfHintFallback;
}
return 'other';
} }
guessDiscIndex(name) { guessDiscIndex(name) {

View File

@@ -128,8 +128,12 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
} }
if (fstype.includes('udf')) { if (fstype.includes('udf')) {
// UDF is used by both DVDs (UDF 1.02) and Blu-rays (UDF 2.5/2.6).
// Drive model alone (hasBlurayModelMarker) is not reliable: a BD-ROM drive
// with a DVD inside would incorrectly be detected as Blu-ray.
// Return null so the mountpoint BDMV/VIDEO_TS check can decide.
if (hasBlurayModelMarker) { if (hasBlurayModelMarker) {
return 'bluray'; return null;
} }
if (hasDvdModelMarker) { if (hasDvdModelMarker) {
return 'dvd'; return 'dvd';
@@ -138,9 +142,8 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
} }
if (fstype.includes('iso9660') || fstype.includes('cdfs')) { if (fstype.includes('iso9660') || fstype.includes('cdfs')) {
if (hasBlurayModelMarker) { // iso9660/cdfs is never used by Blu-ray discs (they use UDF 2.5/2.6).
return 'bluray'; // Ignore hasBlurayModelMarker here it only reflects drive capability.
}
if (hasCdOnlyModelMarker) { if (hasCdOnlyModelMarker) {
return 'other'; return 'other';
} }
@@ -266,20 +269,22 @@ function inferMediaProfileFromDeviceInfo(deviceInfo = null) {
return explicit; return explicit;
} }
const markerText = [ // Only use disc-specific fields for keyword detection, NOT device.model.
// The drive model describes drive capability (e.g. "BD-ROM"), not disc type.
// A BD-ROM drive with a DVD inserted would otherwise be misdetected as Blu-ray.
const discMarkerText = [
device.discLabel, device.discLabel,
device.label, device.label,
device.fstype, device.fstype,
device.model
] ]
.map((value) => String(value || '').trim().toLowerCase()) .map((value) => String(value || '').trim().toLowerCase())
.filter(Boolean) .filter(Boolean)
.join(' '); .join(' ');
if (/(^|[\s_-])bdmv($|[\s_-])|blu[\s-]?ray|bd-rom|bd-r|bd-re/.test(markerText)) { if (/(^|[\s_-])bdmv($|[\s_-])|blu[\s-]?ray|bd-rom|bd-r|bd-re/.test(discMarkerText)) {
return 'bluray'; return 'bluray';
} }
if (/(^|[\s_-])video_ts($|[\s_-])|dvd/.test(markerText)) { if (/(^|[\s_-])video_ts($|[\s_-])|dvd/.test(discMarkerText)) {
return 'dvd'; return 'dvd';
} }
@@ -1166,10 +1171,14 @@ function parseMakeMkvDurationSeconds(rawValue) {
} }
function buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo) { function buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo) {
const durationSeconds = Math.max(0, Math.trunc(Number(titleInfo?.durationSeconds || 0)));
const tracks = []; const tracks = [];
tracks.push({ tracks.push({
'@type': 'General', '@type': 'General',
Duration: String(Number(titleInfo?.durationSeconds || 0)) // MediaInfo reports numeric Duration as milliseconds. Keep this format so
// parseDurationSeconds() does not misinterpret long titles.
Duration: String(durationSeconds * 1000),
Duration_String3: formatDurationClock(durationSeconds) || null
}); });
const audioTracks = Array.isArray(titleInfo?.audioTracks) ? titleInfo.audioTracks : []; const audioTracks = Array.isArray(titleInfo?.audioTracks) ? titleInfo.audioTracks : [];
@@ -3903,10 +3912,17 @@ class PipelineService extends EventEmitter {
eta: patch.eta ?? null, eta: patch.eta ?? null,
statusText: patch.statusText ?? null statusText: patch.statusText ?? null
}); });
} else if (patch.activeJobId === null && previousActiveJobId != null) { } else if (patch.activeJobId === null) {
// Job slot cleared remove the finished job's live entry so it falls // Job slot cleared remove the finished job's live entry so it falls
// back to DB data in the frontend. // back to DB data in the frontend.
this.jobProgress.delete(Number(previousActiveJobId)); // Use patch.finishingJobId when provided (parallel-safe); fall back to
// previousActiveJobId only when no parallel job has overwritten the slot.
const finishingJobId = patch.finishingJobId != null
? Number(patch.finishingJobId)
: (previousActiveJobId != null ? Number(previousActiveJobId) : null);
if (finishingJobId != null) {
this.jobProgress.delete(finishingJobId);
}
} }
logger.info('state:changed', { logger.info('state:changed', {
from: previous, from: previous,
@@ -4917,7 +4933,8 @@ class PipelineService extends EventEmitter {
'MEDIAINFO_CHECK', 'MEDIAINFO_CHECK',
25, 25,
null, null,
'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet' 'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet',
jobId
); );
try { try {
const resolveScanLines = []; const resolveScanLines = [];
@@ -5169,16 +5186,15 @@ class PipelineService extends EventEmitter {
); );
} }
if (this.isPrimaryJob(jobId)) { await this.updateProgress(
await this.updateProgress( 'MEDIAINFO_CHECK',
'MEDIAINFO_CHECK', 30,
30, null,
null, hasCachedHandBrakeEntry
hasCachedHandBrakeEntry ? `HandBrake Trackdaten aus Cache (${toPlaylistFile(resolvedPlaylistId) || resolvedPlaylistId})`
? `HandBrake Trackdaten aus Cache (${toPlaylistFile(resolvedPlaylistId) || resolvedPlaylistId})` : `HandBrake Titel-/Spurscan läuft (${toPlaylistFile(resolvedPlaylistId) || resolvedPlaylistId})`,
: `HandBrake Titel-/Spurscan läuft (${toPlaylistFile(resolvedPlaylistId) || resolvedPlaylistId})` jobId
); );
}
let handBrakeResolveRunInfo = null; let handBrakeResolveRunInfo = null;
let handBrakeTitleRunInfo = null; let handBrakeTitleRunInfo = null;
@@ -5371,6 +5387,8 @@ class PipelineService extends EventEmitter {
review = remapReviewTrackIdsToSourceIds(review); review = remapReviewTrackIdsToSourceIds(review);
const resolvedPlaylistInfo = resolvePlaylistInfoFromAnalysis(playlistAnalysis, resolvedPlaylistId); 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( const subtitleTrackMetaBySourceId = new Map(
(Array.isArray(reviewTitleInfo?.subtitleTracks) ? reviewTitleInfo.subtitleTracks : []) (Array.isArray(reviewTitleInfo?.subtitleTracks) ? reviewTitleInfo.subtitleTracks : [])
.map((track) => { .map((track) => {
@@ -5382,6 +5400,8 @@ class PipelineService extends EventEmitter {
const normalizedTitles = (Array.isArray(review.titles) ? review.titles : []) const normalizedTitles = (Array.isArray(review.titles) ? review.titles : [])
.slice(0, 1) .slice(0, 1)
.map((title) => { .map((title) => {
const durationSeconds = Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0);
const eligibleForEncode = durationSeconds >= minLengthSecondsForReview;
const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => { const subtitleTracks = (Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : []).map((track) => {
const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null; const sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null; const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null;
@@ -5403,8 +5423,10 @@ class PipelineService extends EventEmitter {
...title, ...title,
filePath: rawPath, filePath: rawPath,
fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`, fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
durationSeconds: Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0), durationSeconds,
durationMinutes: Number((((reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0) / 60)).toFixed(2)), durationMinutes: Number(((durationSeconds / 60)).toFixed(2)),
selectedByMinLength: eligibleForEncode,
eligibleForEncode,
sizeBytes: Number(reviewTitleInfo?.sizeBytes || title?.sizeBytes || 0), sizeBytes: Number(reviewTitleInfo?.sizeBytes || title?.sizeBytes || 0),
playlistId: resolvedPlaylistInfo.playlistId || title?.playlistId || null, playlistId: resolvedPlaylistInfo.playlistId || title?.playlistId || null,
playlistFile: resolvedPlaylistInfo.playlistFile || title?.playlistFile || null, playlistFile: resolvedPlaylistInfo.playlistFile || title?.playlistFile || null,
@@ -6559,7 +6581,7 @@ class PipelineService extends EventEmitter {
for (let i = 0; i < mediaFiles.length; i += 1) { for (let i = 0; i < mediaFiles.length; i += 1) {
const file = mediaFiles[i]; const file = mediaFiles[i];
const percent = Number((((i + 1) / mediaFiles.length) * 100).toFixed(2)); const percent = Number((((i + 1) / mediaFiles.length) * 100).toFixed(2));
await this.updateProgress('MEDIAINFO_CHECK', percent, null, `Mediainfo ${i + 1}/${mediaFiles.length}: ${path.basename(file.path)}`); await this.updateProgress('MEDIAINFO_CHECK', percent, null, `Mediainfo ${i + 1}/${mediaFiles.length}: ${path.basename(file.path)}`, jobId);
const result = await this.runMediainfoForFile(jobId, file.path, { const result = await this.runMediainfoForFile(jobId, file.path, {
mediaProfile, mediaProfile,
@@ -7479,6 +7501,7 @@ class PipelineService extends EventEmitter {
setTimeout(async () => { setTimeout(async () => {
if (this.snapshot.state === 'FINISHED' && this.snapshot.activeJobId === jobId) { if (this.snapshot.state === 'FINISHED' && this.snapshot.activeJobId === jobId) {
await this.setState('IDLE', { await this.setState('IDLE', {
finishingJobId: jobId,
activeJobId: null, activeJobId: null,
progress: 0, progress: 0,
eta: null, eta: null,

View File

@@ -371,6 +371,18 @@ export default function DashboardPage({
refreshPipeline refreshPipeline
}) { }) {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [busyJobIds, setBusyJobIds] = useState(() => new Set());
const setJobBusy = (jobId, isBusy) => {
setBusyJobIds((prev) => {
const next = new Set(prev);
if (isBusy) {
next.add(jobId);
} else {
next.delete(jobId);
}
return next;
});
};
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false); const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
const [metadataDialogContext, setMetadataDialogContext] = useState(null); const [metadataDialogContext, setMetadataDialogContext] = useState(null);
const [cancelCleanupDialog, setCancelCleanupDialog] = useState({ const [cancelCleanupDialog, setCancelCleanupDialog] = useState({
@@ -713,7 +725,7 @@ export default function DashboardPage({
|| 'IDLE' || 'IDLE'
).trim().toUpperCase(); ).trim().toUpperCase();
setBusy(true); if (cancelledJobId) setJobBusy(cancelledJobId, true);
try { try {
await api.cancelPipeline(cancelledJobId); await api.cancelPipeline(cancelledJobId);
await refreshPipeline(); await refreshPipeline();
@@ -736,7 +748,7 @@ export default function DashboardPage({
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); if (cancelledJobId) setJobBusy(cancelledJobId, false);
} }
}; };
@@ -782,7 +794,7 @@ export default function DashboardPage({
} }
const startOptions = options && typeof options === 'object' ? options : {}; const startOptions = options && typeof options === 'object' ? options : {};
setBusy(true); setJobBusy(normalizedJobId, true);
try { try {
if (startOptions.ensureConfirmed) { if (startOptions.ensureConfirmed) {
await api.confirmEncodeReview(normalizedJobId, { await api.confirmEncodeReview(normalizedJobId, {
@@ -808,7 +820,7 @@ export default function DashboardPage({
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); setJobBusy(normalizedJobId, false);
} }
}; };
@@ -818,7 +830,8 @@ export default function DashboardPage({
selectedTrackSelection = null, selectedTrackSelection = null,
selectedPostEncodeScriptIds = undefined selectedPostEncodeScriptIds = undefined
) => { ) => {
setBusy(true); const normalizedJobId = normalizeJobId(jobId);
if (normalizedJobId) setJobBusy(normalizedJobId, true);
try { try {
await api.confirmEncodeReview(jobId, { await api.confirmEncodeReview(jobId, {
selectedEncodeTitleId, selectedEncodeTitleId,
@@ -827,16 +840,17 @@ export default function DashboardPage({
}); });
await refreshPipeline(); await refreshPipeline();
await loadDashboardJobs(); await loadDashboardJobs();
setExpandedJobId(normalizeJobId(jobId)); setExpandedJobId(normalizedJobId);
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); if (normalizedJobId) setJobBusy(normalizedJobId, false);
} }
}; };
const handleSelectPlaylist = async (jobId, selectedPlaylist = null) => { const handleSelectPlaylist = async (jobId, selectedPlaylist = null) => {
setBusy(true); const normalizedJobId = normalizeJobId(jobId);
if (normalizedJobId) setJobBusy(normalizedJobId, true);
try { try {
await api.selectMetadata({ await api.selectMetadata({
jobId, jobId,
@@ -844,16 +858,17 @@ export default function DashboardPage({
}); });
await refreshPipeline(); await refreshPipeline();
await loadDashboardJobs(); await loadDashboardJobs();
setExpandedJobId(normalizeJobId(jobId)); setExpandedJobId(normalizedJobId);
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); if (normalizedJobId) setJobBusy(normalizedJobId, false);
} }
}; };
const handleRetry = async (jobId) => { const handleRetry = async (jobId) => {
setBusy(true); const normalizedJobId = normalizeJobId(jobId);
if (normalizedJobId) setJobBusy(normalizedJobId, true);
try { try {
const response = await api.retryJob(jobId); const response = await api.retryJob(jobId);
const result = getQueueActionResult(response); const result = getQueueActionResult(response);
@@ -862,12 +877,12 @@ export default function DashboardPage({
if (result.queued) { if (result.queued) {
showQueuedToast(toastRef, 'Retry', result); showQueuedToast(toastRef, 'Retry', result);
} else { } else {
setExpandedJobId(normalizeJobId(jobId)); setExpandedJobId(normalizedJobId);
} }
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); if (normalizedJobId) setJobBusy(normalizedJobId, false);
} }
}; };
@@ -884,7 +899,8 @@ export default function DashboardPage({
} }
} }
setBusy(true); const normalizedJobId = normalizeJobId(jobId);
if (normalizedJobId) setJobBusy(normalizedJobId, true);
try { try {
const response = await api.restartEncodeWithLastSettings(jobId); const response = await api.restartEncodeWithLastSettings(jobId);
const result = getQueueActionResult(response); const result = getQueueActionResult(response);
@@ -893,12 +909,12 @@ export default function DashboardPage({
if (result.queued) { if (result.queued) {
showQueuedToast(toastRef, 'Encode-Neustart', result); showQueuedToast(toastRef, 'Encode-Neustart', result);
} else { } else {
setExpandedJobId(normalizeJobId(jobId)); setExpandedJobId(normalizedJobId);
} }
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); if (normalizedJobId) setJobBusy(normalizedJobId, false);
} }
}; };
@@ -908,7 +924,7 @@ export default function DashboardPage({
return; return;
} }
setBusy(true); setJobBusy(normalizedJobId, true);
try { try {
await api.restartReviewFromRaw(normalizedJobId); await api.restartReviewFromRaw(normalizedJobId);
await refreshPipeline(); await refreshPipeline();
@@ -917,7 +933,7 @@ export default function DashboardPage({
} catch (error) { } catch (error) {
showError(error); showError(error);
} finally { } finally {
setBusy(false); setJobBusy(normalizedJobId, false);
} }
}; };
@@ -975,6 +991,7 @@ export default function DashboardPage({
} }
setQueueReorderBusy(true); setQueueReorderBusy(true);
setJobBusy(normalizedJobId, true);
try { try {
await api.cancelPipeline(normalizedJobId); await api.cancelPipeline(normalizedJobId);
const latest = await api.getPipelineQueue(); const latest = await api.getPipelineQueue();
@@ -983,6 +1000,7 @@ export default function DashboardPage({
showError(error); showError(error);
} finally { } finally {
setQueueReorderBusy(false); setQueueReorderBusy(false);
setJobBusy(normalizedJobId, false);
} }
}; };
@@ -1429,7 +1447,7 @@ export default function DashboardPage({
severity="secondary" severity="secondary"
outlined outlined
onClick={() => setExpandedJobId(null)} onClick={() => setExpandedJobId(null)}
disabled={busy} disabled={busyJobIds.has(jobId)}
/> />
</div> </div>
<PipelineStatusCard <PipelineStatusCard
@@ -1446,7 +1464,7 @@ export default function DashboardPage({
onRetry={handleRetry} onRetry={handleRetry}
onRemoveFromQueue={handleRemoveQueuedJob} onRemoveFromQueue={handleRemoveQueuedJob}
isQueued={isQueued} isQueued={isQueued}
busy={busy} busy={busyJobIds.has(jobId)}
liveJobLog={isCurrentSession ? liveJobLog : ''} liveJobLog={isCurrentSession ? liveJobLog : ''}
/> />
</div> </div>