pre release
This commit is contained in:
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user