0.12.0-17 Misc fixes
This commit is contained in:
@@ -898,6 +898,19 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
});
|
||||
}
|
||||
|
||||
const externalStorageDescription = 'Wird links unter "Freie Speicher" angezeigt.';
|
||||
const externalStorageDescResult = await db.run(
|
||||
`UPDATE settings_schema
|
||||
SET description = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = 'external_storage_paths' AND description != ?`,
|
||||
[externalStorageDescription, externalStorageDescription]
|
||||
);
|
||||
if (externalStorageDescResult?.changes > 0) {
|
||||
logger.info('migrate:settings-schema-external-storage-description-updated', {
|
||||
key: 'external_storage_paths'
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate raw_dir_cd_owner label
|
||||
await db.run(
|
||||
`UPDATE settings_schema SET label = 'Eigentümer CD RAW-Ordner', updated_at = CURRENT_TIMESTAMP
|
||||
@@ -1034,6 +1047,12 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('download_dir_owner', NULL)`);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('external_storage_paths', 'Pfade', 'Externe Speicher', 'path', 0, 'Wird links unter "Freie Speicher" angezeigt.', '[]', '[]', '{}', 119)`
|
||||
);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('external_storage_paths', '[]')`);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('drive_devices', 'Laufwerk', 'Explizite Laufwerke', 'path', 0, 'Laufwerke für den expliziten Modus als JSON-Array mit Pfad und MakeMKV-Index, z.B. [{\"path\":\"/dev/sr0\",\"makemkvIndex\":0},{\"path\":\"/dev/sr1\",\"makemkvIndex\":1}]. Nur relevant wenn Modus = Explizites Device.', '[]', '[]', '{}', 15)`
|
||||
|
||||
@@ -26,7 +26,8 @@ const RELEVANT_SETTINGS_KEYS = new Set([
|
||||
'movie_dir',
|
||||
'movie_dir_bluray',
|
||||
'movie_dir_dvd',
|
||||
'log_dir'
|
||||
'log_dir',
|
||||
'external_storage_paths'
|
||||
]);
|
||||
|
||||
function nowIso() {
|
||||
@@ -57,6 +58,52 @@ function normalizePathSetting(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function parseExternalStoragePaths(rawValue) {
|
||||
const normalized = String(rawValue || '').trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let parsed = [];
|
||||
try {
|
||||
parsed = JSON.parse(normalized);
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list = Array.isArray(parsed) ? parsed : [];
|
||||
const unique = [];
|
||||
const seen = new Set();
|
||||
for (const item of list) {
|
||||
const pathCandidate = typeof item === 'string'
|
||||
? item
|
||||
: (item && typeof item === 'object' ? item.path : '');
|
||||
const labelCandidate = item && typeof item === 'object'
|
||||
? (item.name ?? item.label ?? '')
|
||||
: '';
|
||||
const pathValue = normalizePathSetting(pathCandidate);
|
||||
const labelValue = normalizePathSetting(labelCandidate);
|
||||
if (!pathValue || seen.has(pathValue)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(pathValue);
|
||||
unique.push({
|
||||
path: pathValue,
|
||||
label: labelValue || null
|
||||
});
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
function isRootPath(inputPath) {
|
||||
const normalized = String(inputPath || '').trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const resolved = path.resolve(normalized);
|
||||
return resolved === path.parse(resolved).root;
|
||||
}
|
||||
|
||||
function clampIntervalMs(rawValue) {
|
||||
const parsed = Number(rawValue);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
@@ -405,13 +452,16 @@ class HardwareMonitorService {
|
||||
const cdRawPath = normalizePathSetting(cd?.raw_dir);
|
||||
const blurayMoviePath = normalizePathSetting(bluray?.movie_dir);
|
||||
const dvdMoviePath = normalizePathSetting(dvd?.movie_dir);
|
||||
const externalStoragePaths = parseExternalStoragePaths(sourceMap.external_storage_paths);
|
||||
const monitoredPaths = [];
|
||||
|
||||
const addPath = (key, label, monitoredPath) => {
|
||||
const addPath = (key, label, monitoredPath, options = {}) => {
|
||||
monitoredPaths.push({
|
||||
key,
|
||||
label,
|
||||
path: normalizePathSetting(monitoredPath)
|
||||
path: normalizePathSetting(monitoredPath),
|
||||
hideWhenUnavailable: Boolean(options?.hideWhenUnavailable),
|
||||
requireDedicatedMount: Boolean(options?.requireDedicatedMount)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -431,6 +481,19 @@ class HardwareMonitorService {
|
||||
}
|
||||
|
||||
addPath('log_dir', 'Log-Verzeichnis', sourceMap.log_dir);
|
||||
externalStoragePaths.forEach((externalEntry, index) => {
|
||||
const externalPath = normalizePathSetting(externalEntry?.path);
|
||||
const externalLabel = normalizePathSetting(externalEntry?.label) || `Externer Speicher ${index + 1}`;
|
||||
addPath(
|
||||
`external_storage_path_${index}`,
|
||||
externalLabel,
|
||||
externalPath,
|
||||
{
|
||||
hideWhenUnavailable: true,
|
||||
requireDedicatedMount: true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return monitoredPaths;
|
||||
}
|
||||
@@ -866,7 +929,10 @@ class HardwareMonitorService {
|
||||
async collectStorageMetrics() {
|
||||
const list = [];
|
||||
for (const entry of this.monitoredPaths) {
|
||||
list.push(await this.collectStorageForPath(entry));
|
||||
const metric = await this.collectStorageForPath(entry);
|
||||
if (metric && !metric.hidden) {
|
||||
list.push(metric);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -897,7 +963,24 @@ class HardwareMonitorService {
|
||||
const key = String(entry?.key || '');
|
||||
const label = String(entry?.label || key || 'Pfad');
|
||||
const rawPath = String(entry?.path || '').trim();
|
||||
const hideWhenUnavailable = Boolean(entry?.hideWhenUnavailable);
|
||||
const requireDedicatedMount = Boolean(entry?.requireDedicatedMount);
|
||||
|
||||
const hiddenResult = (base = {}) => ({
|
||||
key,
|
||||
label,
|
||||
hidden: true,
|
||||
...base
|
||||
});
|
||||
|
||||
if (!rawPath) {
|
||||
if (hideWhenUnavailable) {
|
||||
return hiddenResult({
|
||||
path: null,
|
||||
queryPath: null,
|
||||
exists: false
|
||||
});
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
@@ -919,6 +1002,13 @@ class HardwareMonitorService {
|
||||
const queryPath = exists ? resolvedPath : this.findNearestExistingPath(resolvedPath);
|
||||
|
||||
if (!queryPath) {
|
||||
if (hideWhenUnavailable) {
|
||||
return hiddenResult({
|
||||
path: resolvedPath,
|
||||
queryPath: null,
|
||||
exists: false
|
||||
});
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
@@ -942,6 +1032,13 @@ class HardwareMonitorService {
|
||||
});
|
||||
const parsed = parseDfStats(stdout);
|
||||
if (!parsed) {
|
||||
if (hideWhenUnavailable) {
|
||||
return hiddenResult({
|
||||
path: resolvedPath,
|
||||
queryPath,
|
||||
exists
|
||||
});
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
@@ -957,6 +1054,16 @@ class HardwareMonitorService {
|
||||
error: 'Dateisystemdaten konnten nicht geparst werden.'
|
||||
};
|
||||
}
|
||||
const mountPoint = parsed?.mountPoint ? path.resolve(parsed.mountPoint) : parsed.mountPoint;
|
||||
const hasDedicatedMount = Boolean(mountPoint) && !isRootPath(mountPoint);
|
||||
const shouldShowExternal = exists || hasDedicatedMount;
|
||||
if (hideWhenUnavailable && requireDedicatedMount && !shouldShowExternal) {
|
||||
return hiddenResult({
|
||||
path: resolvedPath,
|
||||
queryPath,
|
||||
exists
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
@@ -965,10 +1072,18 @@ class HardwareMonitorService {
|
||||
queryPath,
|
||||
exists,
|
||||
...parsed,
|
||||
mountPoint: mountPoint || null,
|
||||
note: exists ? null : `Pfad fehlt, Parent verwendet (${queryPath}).`,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
if (hideWhenUnavailable) {
|
||||
return hiddenResult({
|
||||
path: resolvedPath,
|
||||
queryPath,
|
||||
exists
|
||||
});
|
||||
}
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
|
||||
@@ -37,6 +37,22 @@ function sleep(ms) {
|
||||
}
|
||||
|
||||
class OmdbService {
|
||||
mapSearchResults(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return [];
|
||||
}
|
||||
if (data.Response === 'False' || !Array.isArray(data.Search)) {
|
||||
return [];
|
||||
}
|
||||
return data.Search.map((item) => ({
|
||||
title: item.Title,
|
||||
year: item.Year,
|
||||
imdbId: item.imdbID,
|
||||
type: item.Type,
|
||||
poster: item.Poster
|
||||
}));
|
||||
}
|
||||
|
||||
async requestJson(url, meta = {}, options = {}) {
|
||||
const timeoutMs = normalizeOmdbTimeoutMs(options?.timeoutMs);
|
||||
const maxAttempts = Math.max(1, Math.trunc(Number(options?.maxAttempts || OMDB_MAX_ATTEMPTS)));
|
||||
@@ -89,10 +105,26 @@ class OmdbService {
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
if (!query || query.trim().length === 0) {
|
||||
const normalizedQuery = String(query || '').trim();
|
||||
if (!normalizedQuery) {
|
||||
return [];
|
||||
}
|
||||
logger.info('search:start', { query });
|
||||
logger.info('search:start', { query: normalizedQuery });
|
||||
|
||||
// Allow direct IMDb-ID lookups in the search field (useful for History remapping).
|
||||
const imdbLike = normalizedQuery.toLowerCase();
|
||||
if (/^tt\d{6,12}$/.test(imdbLike)) {
|
||||
const byId = await this.fetchByImdbId(imdbLike);
|
||||
return byId
|
||||
? [{
|
||||
title: byId.title,
|
||||
year: byId.year != null ? String(byId.year) : null,
|
||||
imdbId: byId.imdbId,
|
||||
type: byId.type,
|
||||
poster: byId.poster
|
||||
}]
|
||||
: [];
|
||||
}
|
||||
|
||||
const settings = await settingsService.getSettingsMap();
|
||||
const apiKey = settings.omdb_api_key;
|
||||
@@ -101,28 +133,47 @@ class OmdbService {
|
||||
}
|
||||
const timeoutMs = normalizeOmdbTimeoutMs(settings.omdb_timeout_ms);
|
||||
|
||||
const type = settings.omdb_default_type || 'movie';
|
||||
const url = new URL(OMDB_BASE_URL);
|
||||
url.searchParams.set('apikey', apiKey);
|
||||
url.searchParams.set('s', query.trim());
|
||||
url.searchParams.set('type', type);
|
||||
const configuredType = String(settings.omdb_default_type || 'movie').trim().toLowerCase() || 'movie';
|
||||
const requestSearch = async (type = null) => {
|
||||
const url = new URL(OMDB_BASE_URL);
|
||||
url.searchParams.set('apikey', apiKey);
|
||||
url.searchParams.set('s', normalizedQuery);
|
||||
if (type) {
|
||||
url.searchParams.set('type', type);
|
||||
}
|
||||
const data = await this.requestJson(
|
||||
url,
|
||||
{ query: normalizedQuery, action: 'search', type: type || null },
|
||||
{ timeoutMs }
|
||||
);
|
||||
return {
|
||||
data,
|
||||
results: this.mapSearchResults(data)
|
||||
};
|
||||
};
|
||||
|
||||
const data = await this.requestJson(url, { query, action: 'search' }, { timeoutMs });
|
||||
if (!data || typeof data !== 'object') {
|
||||
let { data, results } = await requestSearch(configuredType);
|
||||
if (results.length === 0 && configuredType) {
|
||||
logger.info('search:fallback-no-type', {
|
||||
query: normalizedQuery,
|
||||
configuredType,
|
||||
response: data?.Response || null,
|
||||
error: data?.Error || null
|
||||
});
|
||||
({ data, results } = await requestSearch(null));
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
logger.warn('search:no-results', {
|
||||
query: normalizedQuery,
|
||||
response: data?.Response || null,
|
||||
error: data?.Error || null,
|
||||
configuredType
|
||||
});
|
||||
return [];
|
||||
}
|
||||
if (data.Response === 'False' || !Array.isArray(data.Search)) {
|
||||
logger.warn('search:no-results', { query, response: data.Response, error: data.Error });
|
||||
return [];
|
||||
}
|
||||
const results = data.Search.map((item) => ({
|
||||
title: item.Title,
|
||||
year: item.Year,
|
||||
imdbId: item.imdbID,
|
||||
type: item.Type,
|
||||
poster: item.Poster
|
||||
}));
|
||||
logger.info('search:done', { query, count: results.length });
|
||||
|
||||
logger.info('search:done', { query: normalizedQuery, count: results.length, configuredType });
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -5187,7 +5187,7 @@ class PipelineService extends EventEmitter {
|
||||
...previousJobProgress,
|
||||
state: previousJobProgress.state || this.snapshot.state,
|
||||
progress: previousJobProgress.progress ?? this.snapshot.progress ?? 0,
|
||||
eta: previousJobProgress.eta ?? this.snapshot.eta ?? null,
|
||||
eta: previousJobProgress.eta ?? null,
|
||||
statusText: previousJobProgress.statusText ?? this.snapshot.statusText ?? null,
|
||||
context: {
|
||||
...(previousJobProgress.context && typeof previousJobProgress.context === 'object'
|
||||
@@ -5434,6 +5434,54 @@ class PipelineService extends EventEmitter {
|
||||
return this.resolveCurrentRawPath(rawBaseDir, stored, rawExtraDirs);
|
||||
}
|
||||
|
||||
async resolveUsableRawInputForReadyJob(jobId, job, encodePlan = null) {
|
||||
const plan = encodePlan && typeof encodePlan === 'object'
|
||||
? encodePlan
|
||||
: this.safeParseJson(job?.encode_plan_json);
|
||||
const mkInfo = this.safeParseJson(job?.makemkv_info_json);
|
||||
const mediaProfile = this.resolveMediaProfileForJob(job, {
|
||||
makemkvInfo: mkInfo,
|
||||
encodePlan: plan,
|
||||
rawPath: job?.raw_path
|
||||
});
|
||||
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
||||
const resolvedRawPath = this.resolveCurrentRawPathForSettings(
|
||||
settings,
|
||||
mediaProfile,
|
||||
job?.raw_path
|
||||
) || String(job?.raw_path || '').trim() || null;
|
||||
if (!resolvedRawPath || !fs.existsSync(resolvedRawPath)) {
|
||||
return {
|
||||
mediaProfile,
|
||||
resolvedRawPath,
|
||||
inputPath: null,
|
||||
hasUsableRawInput: false
|
||||
};
|
||||
}
|
||||
|
||||
let inputPath = null;
|
||||
try {
|
||||
if (hasBluRayBackupStructure(resolvedRawPath)) {
|
||||
inputPath = resolvedRawPath;
|
||||
} else {
|
||||
const playlistDecision = this.resolvePlaylistDecisionForJob(jobId, job);
|
||||
inputPath = findPreferredRawInput(resolvedRawPath, {
|
||||
playlistAnalysis: playlistDecision.playlistAnalysis,
|
||||
selectedPlaylistId: playlistDecision.selectedPlaylist
|
||||
})?.path || null;
|
||||
}
|
||||
} catch (_error) {
|
||||
inputPath = null;
|
||||
}
|
||||
|
||||
return {
|
||||
mediaProfile,
|
||||
resolvedRawPath,
|
||||
inputPath,
|
||||
hasUsableRawInput: Boolean(inputPath)
|
||||
};
|
||||
}
|
||||
|
||||
async alignRawFolderJobId(rawPath, targetJobId) {
|
||||
const sourceRawPath = String(rawPath || '').trim();
|
||||
const normalizedTargetJobId = this.normalizeQueueJobId(targetJobId);
|
||||
@@ -5856,6 +5904,13 @@ class PipelineService extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
_broadcastPipelineStateChanged() {
|
||||
const snapshotPayload = this.getSnapshot();
|
||||
wsService.broadcast('PIPELINE_STATE_CHANGED', snapshotPayload);
|
||||
this.emit('stateChanged', snapshotPayload);
|
||||
return snapshotPayload;
|
||||
}
|
||||
|
||||
_setCdDriveState(devicePath, updates) {
|
||||
const existing = this.cdDrives.get(devicePath) || {
|
||||
devicePath,
|
||||
@@ -6059,6 +6114,7 @@ class PipelineService extends EventEmitter {
|
||||
source: owner.source,
|
||||
reason: owner.reason
|
||||
});
|
||||
this._broadcastPipelineStateChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6080,6 +6136,7 @@ class PipelineService extends EventEmitter {
|
||||
jobId: targetJobId,
|
||||
reason: String(options?.reason || '').trim() || null
|
||||
});
|
||||
this._broadcastPipelineStateChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7599,8 +7656,18 @@ class PipelineService extends EventEmitter {
|
||||
|
||||
async updateProgress(stage, percent, eta, statusText, jobIdOverride = null, options = {}) {
|
||||
const effectiveJobId = jobIdOverride != null ? Number(jobIdOverride) : this.snapshot.activeJobId;
|
||||
const previousJobProgress = effectiveJobId != null
|
||||
? (this.jobProgress.get(effectiveJobId) || {})
|
||||
: null;
|
||||
const effectiveProgress = percent ?? this.snapshot.progress;
|
||||
const effectiveEta = eta ?? this.snapshot.eta;
|
||||
let effectiveEta;
|
||||
if (eta !== undefined) {
|
||||
effectiveEta = eta;
|
||||
} else if (previousJobProgress && previousJobProgress.eta !== undefined) {
|
||||
effectiveEta = previousJobProgress.eta ?? null;
|
||||
} else {
|
||||
effectiveEta = this.snapshot.eta ?? null;
|
||||
}
|
||||
const effectiveStatusText = statusText ?? this.snapshot.statusText;
|
||||
const progressOptions = options && typeof options === 'object' ? options : {};
|
||||
const contextPatch = progressOptions.contextPatch && typeof progressOptions.contextPatch === 'object'
|
||||
@@ -7610,7 +7677,6 @@ class PipelineService extends EventEmitter {
|
||||
|
||||
// Update per-job progress so concurrent jobs don't overwrite each other.
|
||||
if (effectiveJobId != null) {
|
||||
const previousJobProgress = this.jobProgress.get(effectiveJobId) || {};
|
||||
const mergedContext = contextPatch
|
||||
? {
|
||||
...(previousJobProgress.context && typeof previousJobProgress.context === 'object'
|
||||
@@ -9926,7 +9992,13 @@ class PipelineService extends EventEmitter {
|
||||
// Pre-rip jobs bypass the encode queue because the next step is a rip, not an encode.
|
||||
const jobEncodePlan = this.safeParseJson(preloadedJob.encode_plan_json);
|
||||
const jobMode = String(jobEncodePlan?.mode || '').trim().toLowerCase();
|
||||
const willRipFirst = jobMode === 'pre_rip' || Boolean(jobEncodePlan?.preRip);
|
||||
let willRipFirst = jobMode === 'pre_rip' || Boolean(jobEncodePlan?.preRip);
|
||||
if (willRipFirst) {
|
||||
const rawReuse = await this.resolveUsableRawInputForReadyJob(jobId, preloadedJob, jobEncodePlan);
|
||||
if (rawReuse.hasUsableRawInput) {
|
||||
willRipFirst = false;
|
||||
}
|
||||
}
|
||||
if (willRipFirst) {
|
||||
return this.startPreparedJob(jobId, { ...options, immediate: true });
|
||||
}
|
||||
@@ -9999,6 +10071,52 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
|
||||
if (isPreRipReadyState) {
|
||||
const rawReuse = await this.resolveUsableRawInputForReadyJob(jobId, job, encodePlanForReadyState);
|
||||
if (rawReuse.hasUsableRawInput) {
|
||||
const updatePayload = {
|
||||
status: 'ENCODING',
|
||||
last_state: 'ENCODING',
|
||||
error_message: null,
|
||||
end_time: null
|
||||
};
|
||||
if (
|
||||
rawReuse.resolvedRawPath
|
||||
&& normalizeComparablePath(rawReuse.resolvedRawPath) !== normalizeComparablePath(job.raw_path)
|
||||
) {
|
||||
updatePayload.raw_path = rawReuse.resolvedRawPath;
|
||||
}
|
||||
if (
|
||||
rawReuse.inputPath
|
||||
&& normalizeComparablePath(rawReuse.inputPath) !== normalizeComparablePath(job.encode_input_path)
|
||||
) {
|
||||
updatePayload.encode_input_path = rawReuse.inputPath;
|
||||
}
|
||||
await historyService.updateJob(jobId, updatePayload);
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
`Pre-Rip Auswahl erkannt, nutzbares RAW gefunden (${rawReuse.resolvedRawPath}). Starte Encode direkt aus RAW; Laufwerk wird nicht angesprochen.`
|
||||
);
|
||||
|
||||
this.startEncodingFromPrepared(jobId).catch((error) => {
|
||||
logger.error('startPreparedJob:encode-background-from-prerip-raw-failed', {
|
||||
jobId,
|
||||
error: errorToMeta(error)
|
||||
});
|
||||
if (error?.jobAlreadyFailed) {
|
||||
return;
|
||||
}
|
||||
this.failJob(jobId, 'ENCODING', error).catch((failError) => {
|
||||
logger.error('startPreparedJob:encode-background-from-prerip-raw-failJob-failed', {
|
||||
jobId,
|
||||
error: errorToMeta(failError)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { started: true, stage: 'ENCODING', reusedRaw: true };
|
||||
}
|
||||
|
||||
await historyService.updateJob(jobId, {
|
||||
status: 'RIPPING',
|
||||
last_state: 'RIPPING',
|
||||
@@ -12864,6 +12982,15 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
const ripPlugin = await this.resolveAnalyzePlugin({ mediaProfile }, 'rip').catch(() => null);
|
||||
if (devicePath) {
|
||||
const existingLock = this._getDriveLockByPath(devicePath);
|
||||
const existingLockJobId = Number(existingLock?.jobId || existingLock?.owner?.jobId || 0) || null;
|
||||
const lockedByOtherJob = (
|
||||
(diskDetectionService.isDeviceLocked(devicePath) || Boolean(existingLock))
|
||||
&& existingLockJobId !== Number(jobId)
|
||||
);
|
||||
if (lockedByOtherJob) {
|
||||
throw this._buildDriveLockedError(devicePath, existingLock);
|
||||
}
|
||||
this._acquireDriveLockForJob(devicePath, jobId, {
|
||||
stage: 'RIPPING',
|
||||
source: 'MAKEMKV_RIP',
|
||||
@@ -14420,7 +14547,7 @@ class PipelineService extends EventEmitter {
|
||||
} else if (detail) {
|
||||
const jobEntry = this.jobProgress.get(Number(normalizedJobId));
|
||||
const currentProgress = jobEntry?.progress ?? Number(this.snapshot.progress || 0);
|
||||
const currentEta = jobEntry?.eta ?? this.snapshot.eta;
|
||||
const currentEta = jobEntry?.eta ?? runInfo.eta ?? null;
|
||||
const statusText = composeStatusText(stage, currentProgress, runInfo.lastDetail);
|
||||
void this.updateProgress(stage, currentProgress, currentEta, statusText, normalizedJobId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user