pre release
This commit is contained in:
@@ -99,6 +99,9 @@ router.post(
|
||||
const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
|
||||
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
|
||||
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 selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
|
||||
logger.info('post:confirm-encode', {
|
||||
@@ -110,12 +113,24 @@ router.post(
|
||||
selectedUserPresetId,
|
||||
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
|
||||
? selectedPostEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPreEncodeScriptIdsCount: Array.isArray(selectedPreEncodeScriptIds)
|
||||
? selectedPreEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPostEncodeChainIdsCount: Array.isArray(selectedPostEncodeChainIds)
|
||||
? selectedPostEncodeChainIds.length
|
||||
: 0,
|
||||
selectedPreEncodeChainIdsCount: Array.isArray(selectedPreEncodeChainIds)
|
||||
? selectedPreEncodeChainIds.length
|
||||
: 0
|
||||
});
|
||||
const job = await pipelineService.confirmEncodeReview(jobId, {
|
||||
selectedEncodeTitleId,
|
||||
selectedTrackSelection,
|
||||
selectedPostEncodeScriptIds,
|
||||
selectedPreEncodeScriptIds,
|
||||
selectedPostEncodeChainIds,
|
||||
selectedPreEncodeChainIds,
|
||||
skipPipelineStateUpdate,
|
||||
selectedUserPresetId
|
||||
});
|
||||
|
||||
@@ -98,8 +98,12 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return 'bluray';
|
||||
return null;
|
||||
}
|
||||
if (hasDvdModelMarker) {
|
||||
return 'dvd';
|
||||
@@ -108,9 +112,8 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
|
||||
}
|
||||
|
||||
if (fstype.includes('iso9660') || fstype.includes('cdfs')) {
|
||||
if (hasBlurayModelMarker) {
|
||||
return 'bluray';
|
||||
}
|
||||
// iso9660/cdfs is never used by Blu-ray discs (they use UDF 2.x).
|
||||
// Ignore hasBlurayModelMarker – it only reflects drive capability.
|
||||
if (hasCdOnlyModelMarker) {
|
||||
return 'other';
|
||||
}
|
||||
@@ -120,6 +123,30 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
|
||||
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 {
|
||||
constructor() {
|
||||
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 = {}) {
|
||||
const explicit = normalizeMediaProfile(hints?.mediaProfile);
|
||||
if (isSpecificMediaProfile(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([
|
||||
hints?.discLabel,
|
||||
hints?.label,
|
||||
hints?.fstype,
|
||||
hints?.model
|
||||
]);
|
||||
if (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();
|
||||
if (mountpoint) {
|
||||
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 {
|
||||
const { stdout } = await execFileAsync('blkid', ['-o', 'export', devicePath]);
|
||||
const { stdout } = await execFileAsync('blkid', ['-p', '-o', 'export', devicePath]);
|
||||
const payload = {};
|
||||
for (const line of String(stdout || '').split(/\r?\n/)) {
|
||||
const idx = line.indexOf('=');
|
||||
@@ -555,28 +628,40 @@ class DiskDetectionService extends EventEmitter {
|
||||
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([
|
||||
payload.LABEL,
|
||||
payload.TYPE,
|
||||
payload.VERSION,
|
||||
payload.APPLICATION_ID,
|
||||
hints?.model
|
||||
]);
|
||||
if (byBlkidMarker) {
|
||||
return byBlkidMarker;
|
||||
}
|
||||
|
||||
const type = String(payload.TYPE || '').trim().toLowerCase();
|
||||
const byBlkidFsType = inferMediaProfileFromFsTypeAndModel(type, hints?.model);
|
||||
if (byBlkidFsType) {
|
||||
// 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);
|
||||
if (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) {
|
||||
logger.debug('infer-media-profile:blkid-failed', {
|
||||
devicePath,
|
||||
@@ -584,7 +669,11 @@ class DiskDetectionService extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
return explicit === 'other' ? 'other' : null;
|
||||
if (udfHintFallback) {
|
||||
return udfHintFallback;
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
guessDiscIndex(name) {
|
||||
|
||||
@@ -128,8 +128,12 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return 'bluray';
|
||||
return null;
|
||||
}
|
||||
if (hasDvdModelMarker) {
|
||||
return 'dvd';
|
||||
@@ -138,9 +142,8 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
|
||||
}
|
||||
|
||||
if (fstype.includes('iso9660') || fstype.includes('cdfs')) {
|
||||
if (hasBlurayModelMarker) {
|
||||
return 'bluray';
|
||||
}
|
||||
// iso9660/cdfs is never used by Blu-ray discs (they use UDF 2.5/2.6).
|
||||
// Ignore hasBlurayModelMarker here – it only reflects drive capability.
|
||||
if (hasCdOnlyModelMarker) {
|
||||
return 'other';
|
||||
}
|
||||
@@ -266,20 +269,22 @@ function inferMediaProfileFromDeviceInfo(deviceInfo = null) {
|
||||
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.label,
|
||||
device.fstype,
|
||||
device.model
|
||||
]
|
||||
.map((value) => String(value || '').trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.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';
|
||||
}
|
||||
if (/(^|[\s_-])video_ts($|[\s_-])|dvd/.test(markerText)) {
|
||||
if (/(^|[\s_-])video_ts($|[\s_-])|dvd/.test(discMarkerText)) {
|
||||
return 'dvd';
|
||||
}
|
||||
|
||||
@@ -1166,10 +1171,14 @@ function parseMakeMkvDurationSeconds(rawValue) {
|
||||
}
|
||||
|
||||
function buildSyntheticMediaInfoFromMakeMkvTitle(titleInfo) {
|
||||
const durationSeconds = Math.max(0, Math.trunc(Number(titleInfo?.durationSeconds || 0)));
|
||||
const tracks = [];
|
||||
tracks.push({
|
||||
'@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 : [];
|
||||
@@ -3903,10 +3912,17 @@ class PipelineService extends EventEmitter {
|
||||
eta: patch.eta ?? 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
|
||||
// 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', {
|
||||
from: previous,
|
||||
@@ -4917,7 +4933,8 @@ class PipelineService extends EventEmitter {
|
||||
'MEDIAINFO_CHECK',
|
||||
25,
|
||||
null,
|
||||
'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet'
|
||||
'HandBrake Trackdaten für Playlist-Auswahl werden vorbereitet',
|
||||
jobId
|
||||
);
|
||||
try {
|
||||
const resolveScanLines = [];
|
||||
@@ -5169,16 +5186,15 @@ class PipelineService extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isPrimaryJob(jobId)) {
|
||||
await this.updateProgress(
|
||||
'MEDIAINFO_CHECK',
|
||||
30,
|
||||
null,
|
||||
hasCachedHandBrakeEntry
|
||||
? `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 handBrakeTitleRunInfo = null;
|
||||
@@ -5371,6 +5387,8 @@ class PipelineService extends EventEmitter {
|
||||
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) => {
|
||||
@@ -5382,6 +5400,8 @@ class PipelineService extends EventEmitter {
|
||||
const normalizedTitles = (Array.isArray(review.titles) ? review.titles : [])
|
||||
.slice(0, 1)
|
||||
.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 sourceTrackId = normalizeTrackIdList([track?.sourceTrackId ?? track?.id])[0] || null;
|
||||
const sourceMeta = sourceTrackId ? (subtitleTrackMetaBySourceId.get(sourceTrackId) || null) : null;
|
||||
@@ -5403,8 +5423,10 @@ class PipelineService extends EventEmitter {
|
||||
...title,
|
||||
filePath: rawPath,
|
||||
fileName: reviewTitleInfo?.fileName || title?.fileName || `Title #${selectedTitleForReview}`,
|
||||
durationSeconds: Number(reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0),
|
||||
durationMinutes: Number((((reviewTitleInfo?.durationSeconds || title?.durationSeconds || 0) / 60)).toFixed(2)),
|
||||
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,
|
||||
@@ -6559,7 +6581,7 @@ class PipelineService extends EventEmitter {
|
||||
for (let i = 0; i < mediaFiles.length; i += 1) {
|
||||
const file = mediaFiles[i];
|
||||
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, {
|
||||
mediaProfile,
|
||||
@@ -7479,6 +7501,7 @@ class PipelineService extends EventEmitter {
|
||||
setTimeout(async () => {
|
||||
if (this.snapshot.state === 'FINISHED' && this.snapshot.activeJobId === jobId) {
|
||||
await this.setState('IDLE', {
|
||||
finishingJobId: jobId,
|
||||
activeJobId: null,
|
||||
progress: 0,
|
||||
eta: null,
|
||||
|
||||
@@ -371,6 +371,18 @@ export default function DashboardPage({
|
||||
refreshPipeline
|
||||
}) {
|
||||
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 [metadataDialogContext, setMetadataDialogContext] = useState(null);
|
||||
const [cancelCleanupDialog, setCancelCleanupDialog] = useState({
|
||||
@@ -713,7 +725,7 @@ export default function DashboardPage({
|
||||
|| 'IDLE'
|
||||
).trim().toUpperCase();
|
||||
|
||||
setBusy(true);
|
||||
if (cancelledJobId) setJobBusy(cancelledJobId, true);
|
||||
try {
|
||||
await api.cancelPipeline(cancelledJobId);
|
||||
await refreshPipeline();
|
||||
@@ -736,7 +748,7 @@ export default function DashboardPage({
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (cancelledJobId) setJobBusy(cancelledJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -782,7 +794,7 @@ export default function DashboardPage({
|
||||
}
|
||||
|
||||
const startOptions = options && typeof options === 'object' ? options : {};
|
||||
setBusy(true);
|
||||
setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
if (startOptions.ensureConfirmed) {
|
||||
await api.confirmEncodeReview(normalizedJobId, {
|
||||
@@ -808,7 +820,7 @@ export default function DashboardPage({
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -818,7 +830,8 @@ export default function DashboardPage({
|
||||
selectedTrackSelection = null,
|
||||
selectedPostEncodeScriptIds = undefined
|
||||
) => {
|
||||
setBusy(true);
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
await api.confirmEncodeReview(jobId, {
|
||||
selectedEncodeTitleId,
|
||||
@@ -827,16 +840,17 @@ export default function DashboardPage({
|
||||
});
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
setExpandedJobId(normalizeJobId(jobId));
|
||||
setExpandedJobId(normalizedJobId);
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectPlaylist = async (jobId, selectedPlaylist = null) => {
|
||||
setBusy(true);
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
await api.selectMetadata({
|
||||
jobId,
|
||||
@@ -844,16 +858,17 @@ export default function DashboardPage({
|
||||
});
|
||||
await refreshPipeline();
|
||||
await loadDashboardJobs();
|
||||
setExpandedJobId(normalizeJobId(jobId));
|
||||
setExpandedJobId(normalizedJobId);
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = async (jobId) => {
|
||||
setBusy(true);
|
||||
const normalizedJobId = normalizeJobId(jobId);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
const response = await api.retryJob(jobId);
|
||||
const result = getQueueActionResult(response);
|
||||
@@ -862,12 +877,12 @@ export default function DashboardPage({
|
||||
if (result.queued) {
|
||||
showQueuedToast(toastRef, 'Retry', result);
|
||||
} else {
|
||||
setExpandedJobId(normalizeJobId(jobId));
|
||||
setExpandedJobId(normalizedJobId);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} 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 {
|
||||
const response = await api.restartEncodeWithLastSettings(jobId);
|
||||
const result = getQueueActionResult(response);
|
||||
@@ -893,12 +909,12 @@ export default function DashboardPage({
|
||||
if (result.queued) {
|
||||
showQueuedToast(toastRef, 'Encode-Neustart', result);
|
||||
} else {
|
||||
setExpandedJobId(normalizeJobId(jobId));
|
||||
setExpandedJobId(normalizedJobId);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
if (normalizedJobId) setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -908,7 +924,7 @@ export default function DashboardPage({
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
await api.restartReviewFromRaw(normalizedJobId);
|
||||
await refreshPipeline();
|
||||
@@ -917,7 +933,7 @@ export default function DashboardPage({
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -975,6 +991,7 @@ export default function DashboardPage({
|
||||
}
|
||||
|
||||
setQueueReorderBusy(true);
|
||||
setJobBusy(normalizedJobId, true);
|
||||
try {
|
||||
await api.cancelPipeline(normalizedJobId);
|
||||
const latest = await api.getPipelineQueue();
|
||||
@@ -983,6 +1000,7 @@ export default function DashboardPage({
|
||||
showError(error);
|
||||
} finally {
|
||||
setQueueReorderBusy(false);
|
||||
setJobBusy(normalizedJobId, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1429,7 +1447,7 @@ export default function DashboardPage({
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => setExpandedJobId(null)}
|
||||
disabled={busy}
|
||||
disabled={busyJobIds.has(jobId)}
|
||||
/>
|
||||
</div>
|
||||
<PipelineStatusCard
|
||||
@@ -1446,7 +1464,7 @@ export default function DashboardPage({
|
||||
onRetry={handleRetry}
|
||||
onRemoveFromQueue={handleRemoveQueuedJob}
|
||||
isQueued={isQueued}
|
||||
busy={busy}
|
||||
busy={busyJobIds.has(jobId)}
|
||||
liveJobLog={isCurrentSession ? liveJobLog : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user