0.12.0-3 Plugin Integration
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -454,8 +454,10 @@ router.get(
|
|||||||
.map((d) => {
|
.map((d) => {
|
||||||
const name = String(d.name || '');
|
const name = String(d.name || '');
|
||||||
const devicePath = d.path || (name ? `/dev/${name}` : null);
|
const devicePath = d.path || (name ? `/dev/${name}` : null);
|
||||||
const match = name.match(/sr(\d+)$/);
|
const resolvedIndex = Number(d.makemkvIndex);
|
||||||
const discIndex = match ? Number(match[1]) : null;
|
const discIndex = Number.isFinite(resolvedIndex) && resolvedIndex >= 0
|
||||||
|
? Math.trunc(resolvedIndex)
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
path: devicePath,
|
path: devicePath,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -52,6 +52,40 @@ function flattenDevices(nodes, acc = []) {
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpticalDevicePath(entry) {
|
||||||
|
const directPath = String(entry?.path || '').trim();
|
||||||
|
if (directPath) {
|
||||||
|
return directPath;
|
||||||
|
}
|
||||||
|
const name = String(entry?.name || '').trim();
|
||||||
|
return name ? `/dev/${name}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareOpticalDevicePaths(left, right) {
|
||||||
|
return String(left || '').localeCompare(String(right || ''), undefined, {
|
||||||
|
numeric: true,
|
||||||
|
sensitivity: 'base'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMakeMkvIndexByDevicePath(entries = []) {
|
||||||
|
const candidates = (Array.isArray(entries) ? entries : [])
|
||||||
|
.filter((entry) => String(entry?.type || '').trim() === 'rom')
|
||||||
|
.map((entry) => normalizeOpticalDevicePath(entry))
|
||||||
|
.filter(Boolean);
|
||||||
|
const sortedUniquePaths = Array.from(new Set(candidates)).sort(compareOpticalDevicePaths);
|
||||||
|
const map = new Map();
|
||||||
|
for (let index = 0; index < sortedUniquePaths.length; index += 1) {
|
||||||
|
const devicePath = sortedUniquePaths[index];
|
||||||
|
map.set(devicePath, index);
|
||||||
|
const devName = devicePath.startsWith('/dev/') ? devicePath.slice(5) : '';
|
||||||
|
if (devName) {
|
||||||
|
map.set(devName, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
function buildSignature(info) {
|
function buildSignature(info) {
|
||||||
return `${info.path || ''}|${info.discLabel || ''}|${info.label || ''}|${info.model || ''}|${info.mountpoint || ''}|${info.fstype || ''}|${info.mediaProfile || ''}`;
|
return `${info.path || ''}|${info.discLabel || ''}|${info.label || ''}|${info.model || ''}|${info.mountpoint || ''}|${info.fstype || ''}|${info.mediaProfile || ''}`;
|
||||||
}
|
}
|
||||||
@@ -187,10 +221,13 @@ function inferMediaProfileFromUdevProperties(properties = {}) {
|
|||||||
return parsed;
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFlag = (prefix) => Object.entries(flags).some(([key, value]) => key.startsWith(prefix) && value === '1');
|
const hasExactFlag = (key) => flags[String(key || '').trim().toUpperCase()] === '1';
|
||||||
const hasBD = hasFlag('ID_CDROM_MEDIA_BD');
|
// Only use exact media-presence keys here. Prefix matching would also catch
|
||||||
const hasDVD = hasFlag('ID_CDROM_MEDIA_DVD');
|
// drive capability flags (e.g. ID_CDROM_MEDIA_BD_R=1) and misclassify DVDs
|
||||||
const hasCD = hasFlag('ID_CDROM_MEDIA_CD');
|
// in BD-capable drives as Blu-ray media.
|
||||||
|
const hasBD = hasExactFlag('ID_CDROM_MEDIA_BD');
|
||||||
|
const hasDVD = hasExactFlag('ID_CDROM_MEDIA_DVD');
|
||||||
|
const hasCD = hasExactFlag('ID_CDROM_MEDIA_CD');
|
||||||
const audioTrackCount = parseTrackCount(flags.ID_CDROM_MEDIA_TRACK_COUNT_AUDIO);
|
const audioTrackCount = parseTrackCount(flags.ID_CDROM_MEDIA_TRACK_COUNT_AUDIO);
|
||||||
const dataTrackCount = parseTrackCount(flags.ID_CDROM_MEDIA_TRACK_COUNT_DATA);
|
const dataTrackCount = parseTrackCount(flags.ID_CDROM_MEDIA_TRACK_COUNT_DATA);
|
||||||
const hasAudioTracks = Number.isFinite(audioTrackCount) && audioTrackCount > 0;
|
const hasAudioTracks = Number.isFinite(audioTrackCount) && audioTrackCount > 0;
|
||||||
@@ -395,6 +432,28 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceUnlockDevice(devicePath, options = {}) {
|
||||||
|
const normalized = this.normalizeDevicePath(devicePath);
|
||||||
|
if (!normalized) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = this.deviceLocks.get(normalized);
|
||||||
|
if (!entry) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousCount = Math.max(0, Number(entry.count) || 0);
|
||||||
|
this.deviceLocks.delete(normalized);
|
||||||
|
logger.warn('lock:force-remove', {
|
||||||
|
devicePath: normalized,
|
||||||
|
previousCount,
|
||||||
|
reason: String(options?.reason || '').trim() || null,
|
||||||
|
deletedJobIds: Array.isArray(options?.deletedJobIds) ? options.deletedJobIds : []
|
||||||
|
});
|
||||||
|
return previousCount;
|
||||||
|
}
|
||||||
|
|
||||||
isDeviceLocked(devicePath) {
|
isDeviceLocked(devicePath) {
|
||||||
const normalized = this.normalizeDevicePath(devicePath);
|
const normalized = this.normalizeDevicePath(devicePath);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -666,7 +725,13 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const details = await this.getBlockDeviceInfo();
|
const details = await this.getBlockDeviceInfo();
|
||||||
|
const makemkvIndexByPath = buildMakeMkvIndexByDevicePath(details);
|
||||||
const match = details.find((entry) => entry.path === devicePath || `/dev/${entry.name}` === devicePath) || {};
|
const match = details.find((entry) => entry.path === devicePath || `/dev/${entry.name}` === devicePath) || {};
|
||||||
|
const inferredIndex = Number(
|
||||||
|
makemkvIndexByPath.get(devicePath)
|
||||||
|
?? makemkvIndexByPath.get(match.name || '')
|
||||||
|
?? match.makemkvIndex
|
||||||
|
);
|
||||||
|
|
||||||
// Always call checkMediaPresent to get the filesystem type (needed for accurate
|
// Always call checkMediaPresent to get the filesystem type (needed for accurate
|
||||||
// mediaProfile detection). Use lsblk SIZE as fallback presence indicator for
|
// mediaProfile detection). Use lsblk SIZE as fallback presence indicator for
|
||||||
@@ -708,7 +773,9 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
mountpoint: match.mountpoint || null,
|
mountpoint: match.mountpoint || null,
|
||||||
fstype: detectedFsType,
|
fstype: detectedFsType,
|
||||||
mediaProfile: mediaProfile || null,
|
mediaProfile: mediaProfile || null,
|
||||||
index: this.guessDiscIndex(match.name || devicePath)
|
index: Number.isFinite(inferredIndex) && inferredIndex >= 0
|
||||||
|
? Math.trunc(inferredIndex)
|
||||||
|
: this.guessDiscIndex(match.name || devicePath)
|
||||||
};
|
};
|
||||||
logger.debug('detect:explicit:success', { detected });
|
logger.debug('detect:explicit:success', { detected });
|
||||||
return detected;
|
return detected;
|
||||||
@@ -716,6 +783,7 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
|
|
||||||
async detectAuto() {
|
async detectAuto() {
|
||||||
const details = await this.getBlockDeviceInfo();
|
const details = await this.getBlockDeviceInfo();
|
||||||
|
const makemkvIndexByPath = buildMakeMkvIndexByDevicePath(details);
|
||||||
const romCandidates = details.filter((entry) => entry.type === 'rom');
|
const romCandidates = details.filter((entry) => entry.type === 'rom');
|
||||||
|
|
||||||
for (const item of romCandidates) {
|
for (const item of romCandidates) {
|
||||||
@@ -751,6 +819,11 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
fstype: detectedFsType,
|
fstype: detectedFsType,
|
||||||
mountpoint: item.mountpoint
|
mountpoint: item.mountpoint
|
||||||
});
|
});
|
||||||
|
const detectedIndex = Number(
|
||||||
|
makemkvIndexByPath.get(path)
|
||||||
|
?? makemkvIndexByPath.get(item.name || '')
|
||||||
|
?? item.makemkvIndex
|
||||||
|
);
|
||||||
|
|
||||||
const detected = {
|
const detected = {
|
||||||
mode: 'auto',
|
mode: 'auto',
|
||||||
@@ -762,7 +835,9 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
mountpoint: item.mountpoint || null,
|
mountpoint: item.mountpoint || null,
|
||||||
fstype: detectedFsType,
|
fstype: detectedFsType,
|
||||||
mediaProfile: mediaProfile || null,
|
mediaProfile: mediaProfile || null,
|
||||||
index: this.guessDiscIndex(item.name)
|
index: Number.isFinite(detectedIndex) && detectedIndex >= 0
|
||||||
|
? Math.trunc(detectedIndex)
|
||||||
|
: this.guessDiscIndex(item.name)
|
||||||
};
|
};
|
||||||
logger.debug('detect:auto:success', { detected });
|
logger.debug('detect:auto:success', { detected });
|
||||||
return detected;
|
return detected;
|
||||||
@@ -774,6 +849,7 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
|
|
||||||
async detectAllAuto() {
|
async detectAllAuto() {
|
||||||
const details = await this.getBlockDeviceInfo();
|
const details = await this.getBlockDeviceInfo();
|
||||||
|
const makemkvIndexByPath = buildMakeMkvIndexByDevicePath(details);
|
||||||
const romCandidates = details.filter((entry) => entry.type === 'rom');
|
const romCandidates = details.filter((entry) => entry.type === 'rom');
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
@@ -818,6 +894,11 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
fstype: detectedFsType,
|
fstype: detectedFsType,
|
||||||
mountpoint: item.mountpoint
|
mountpoint: item.mountpoint
|
||||||
});
|
});
|
||||||
|
const detectedIndex = Number(
|
||||||
|
makemkvIndexByPath.get(path)
|
||||||
|
?? makemkvIndexByPath.get(item.name || '')
|
||||||
|
?? item.makemkvIndex
|
||||||
|
);
|
||||||
|
|
||||||
const detected = {
|
const detected = {
|
||||||
mode: 'auto',
|
mode: 'auto',
|
||||||
@@ -829,7 +910,9 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
mountpoint: item.mountpoint || null,
|
mountpoint: item.mountpoint || null,
|
||||||
fstype: detectedFsType,
|
fstype: detectedFsType,
|
||||||
mediaProfile: mediaProfile || null,
|
mediaProfile: mediaProfile || null,
|
||||||
index: this.guessDiscIndex(item.name)
|
index: Number.isFinite(detectedIndex) && detectedIndex >= 0
|
||||||
|
? Math.trunc(detectedIndex)
|
||||||
|
: this.guessDiscIndex(item.name)
|
||||||
};
|
};
|
||||||
logger.debug('detect:all-auto:found', { detected });
|
logger.debug('detect:all-auto:found', { detected });
|
||||||
results.push(detected);
|
results.push(detected);
|
||||||
@@ -858,8 +941,25 @@ class DiskDetectionService extends EventEmitter {
|
|||||||
model: entry.model,
|
model: entry.model,
|
||||||
sizeBytes: Number(entry.size) || 0
|
sizeBytes: Number(entry.size) || 0
|
||||||
}));
|
}));
|
||||||
|
const makemkvIndexByPath = buildMakeMkvIndexByDevicePath(devices);
|
||||||
|
const withMakeMkvIndex = devices.map((entry) => {
|
||||||
|
if (entry.type !== 'rom') {
|
||||||
|
return { ...entry, makemkvIndex: null };
|
||||||
|
}
|
||||||
|
const devicePath = normalizeOpticalDevicePath(entry);
|
||||||
|
const inferredIndex = Number(
|
||||||
|
makemkvIndexByPath.get(devicePath)
|
||||||
|
?? makemkvIndexByPath.get(entry.name || '')
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
makemkvIndex: Number.isFinite(inferredIndex) && inferredIndex >= 0
|
||||||
|
? Math.trunc(inferredIndex)
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
});
|
||||||
logger.debug('lsblk:ok', { deviceCount: devices.length });
|
logger.debug('lsblk:ok', { deviceCount: devices.length });
|
||||||
return devices;
|
return withMakeMkvIndex;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('lsblk:failed', { error: errorToMeta(error) });
|
logger.warn('lsblk:failed', { error: errorToMeta(error) });
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -4059,10 +4059,25 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTrackedOutputSource = (sources = []) => {
|
||||||
|
const normalizedSources = Array.isArray(sources)
|
||||||
|
? sources
|
||||||
|
.map((source) => String(source || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
return normalizedSources.includes('output_path') || normalizedSources.includes('lineage_output_path');
|
||||||
|
};
|
||||||
|
|
||||||
const buildList = (target) => Array.from(candidateMap.values())
|
const buildList = (target) => Array.from(candidateMap.values())
|
||||||
.filter((row) => row.target === target)
|
.filter((row) => row.target === target)
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const inspection = inspectDeletionPath(row.path);
|
const inspection = inspectDeletionPath(row.path);
|
||||||
|
const sources = Array.from(row.sources).sort((left, right) => left.localeCompare(right));
|
||||||
|
// Do not expose stale output file paths from DB/lineage in delete preview.
|
||||||
|
// They cannot be deleted anyway and show up as "ghost paths" in UI/QA checks.
|
||||||
|
if (target === 'movie' && !inspection.exists && hasTrackedOutputSource(sources)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
path: row.path,
|
path: row.path,
|
||||||
@@ -4070,9 +4085,10 @@ class HistoryService {
|
|||||||
isDirectory: Boolean(inspection.isDirectory),
|
isDirectory: Boolean(inspection.isDirectory),
|
||||||
isFile: Boolean(inspection.isFile),
|
isFile: Boolean(inspection.isFile),
|
||||||
jobIds: Array.from(row.jobIds).sort((left, right) => left - right),
|
jobIds: Array.from(row.jobIds).sort((left, right) => left - right),
|
||||||
sources: Array.from(row.sources).sort((left, right) => left.localeCompare(right))
|
sources
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
.sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));
|
.sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -4289,6 +4305,8 @@ class HistoryService {
|
|||||||
raw: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null },
|
raw: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null },
|
||||||
movie: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null }
|
movie: { attempted: false, deleted: false, filesDeleted: 0, dirsRemoved: 0, reason: null }
|
||||||
};
|
};
|
||||||
|
let movieDeletedPathForTracking = null;
|
||||||
|
let movieCandidatePathForTracking = null;
|
||||||
|
|
||||||
if (target === 'raw' || target === 'both') {
|
if (target === 'raw' || target === 'both') {
|
||||||
summary.raw.attempted = true;
|
summary.raw.attempted = true;
|
||||||
@@ -4328,7 +4346,59 @@ class HistoryService {
|
|||||||
error.statusCode = 400;
|
error.statusCode = 400;
|
||||||
throw error;
|
throw error;
|
||||||
} else if (!fs.existsSync(effectiveOutputPath)) {
|
} else if (!fs.existsSync(effectiveOutputPath)) {
|
||||||
|
const movieRoot = normalizeComparablePath(effectiveMovieDir);
|
||||||
|
const trackedFolders = await this.getJobOutputFolders(jobId);
|
||||||
|
const trackedExistingPaths = Array.from(new Set(
|
||||||
|
trackedFolders
|
||||||
|
.map((row) => normalizeComparablePath(row?.output_path))
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((candidatePath) => isPathInside(effectiveMovieDir, candidatePath))
|
||||||
|
.filter((candidatePath) => candidatePath !== movieRoot)
|
||||||
|
.filter((candidatePath) => fs.existsSync(candidatePath))
|
||||||
|
));
|
||||||
|
|
||||||
|
if (trackedExistingPaths.length === 1) {
|
||||||
|
const trackedPath = trackedExistingPaths[0];
|
||||||
|
const stat = fs.lstatSync(trackedPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
const keepRoot = trackedPath === movieRoot;
|
||||||
|
const result = deleteFilesRecursively(trackedPath, keepRoot);
|
||||||
|
summary.movie.deleted = true;
|
||||||
|
summary.movie.filesDeleted = result.filesDeleted;
|
||||||
|
summary.movie.dirsRemoved = result.dirsRemoved;
|
||||||
|
movieDeletedPathForTracking = trackedPath;
|
||||||
|
movieCandidatePathForTracking = trackedPath;
|
||||||
|
summary.movie.reason = null;
|
||||||
|
} else {
|
||||||
|
const parentDir = normalizeComparablePath(path.dirname(trackedPath));
|
||||||
|
const canDeleteParentDir = parentDir
|
||||||
|
&& parentDir !== movieRoot
|
||||||
|
&& isPathInside(movieRoot, parentDir)
|
||||||
|
&& fs.existsSync(parentDir)
|
||||||
|
&& fs.lstatSync(parentDir).isDirectory();
|
||||||
|
|
||||||
|
if (canDeleteParentDir) {
|
||||||
|
const result = deleteFilesRecursively(parentDir, false);
|
||||||
|
summary.movie.deleted = true;
|
||||||
|
summary.movie.filesDeleted = result.filesDeleted;
|
||||||
|
summary.movie.dirsRemoved = result.dirsRemoved;
|
||||||
|
movieDeletedPathForTracking = parentDir;
|
||||||
|
movieCandidatePathForTracking = trackedPath;
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(trackedPath);
|
||||||
|
summary.movie.deleted = true;
|
||||||
|
summary.movie.filesDeleted = 1;
|
||||||
|
summary.movie.dirsRemoved = 0;
|
||||||
|
movieDeletedPathForTracking = trackedPath;
|
||||||
|
movieCandidatePathForTracking = trackedPath;
|
||||||
|
}
|
||||||
|
summary.movie.reason = null;
|
||||||
|
}
|
||||||
|
} else if (trackedExistingPaths.length > 1) {
|
||||||
|
summary.movie.reason = 'Mehrere bekannte Output-Ordner gefunden. Bitte gezielt über die Ordnerauswahl löschen.';
|
||||||
|
} else {
|
||||||
summary.movie.reason = 'Movie-Datei/Pfad existiert nicht.';
|
summary.movie.reason = 'Movie-Datei/Pfad existiert nicht.';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const outputPath = normalizeComparablePath(effectiveOutputPath);
|
const outputPath = normalizeComparablePath(effectiveOutputPath);
|
||||||
const movieRoot = normalizeComparablePath(effectiveMovieDir);
|
const movieRoot = normalizeComparablePath(effectiveMovieDir);
|
||||||
@@ -4339,6 +4409,8 @@ class HistoryService {
|
|||||||
summary.movie.deleted = true;
|
summary.movie.deleted = true;
|
||||||
summary.movie.filesDeleted = result.filesDeleted;
|
summary.movie.filesDeleted = result.filesDeleted;
|
||||||
summary.movie.dirsRemoved = result.dirsRemoved;
|
summary.movie.dirsRemoved = result.dirsRemoved;
|
||||||
|
movieDeletedPathForTracking = outputPath;
|
||||||
|
movieCandidatePathForTracking = outputPath;
|
||||||
} else {
|
} else {
|
||||||
const parentDir = normalizeComparablePath(path.dirname(outputPath));
|
const parentDir = normalizeComparablePath(path.dirname(outputPath));
|
||||||
const canDeleteParentDir = parentDir
|
const canDeleteParentDir = parentDir
|
||||||
@@ -4352,19 +4424,29 @@ class HistoryService {
|
|||||||
summary.movie.deleted = true;
|
summary.movie.deleted = true;
|
||||||
summary.movie.filesDeleted = result.filesDeleted;
|
summary.movie.filesDeleted = result.filesDeleted;
|
||||||
summary.movie.dirsRemoved = result.dirsRemoved;
|
summary.movie.dirsRemoved = result.dirsRemoved;
|
||||||
|
movieDeletedPathForTracking = parentDir;
|
||||||
|
movieCandidatePathForTracking = outputPath;
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(outputPath);
|
fs.unlinkSync(outputPath);
|
||||||
summary.movie.deleted = true;
|
summary.movie.deleted = true;
|
||||||
summary.movie.filesDeleted = 1;
|
summary.movie.filesDeleted = 1;
|
||||||
summary.movie.dirsRemoved = 0;
|
summary.movie.dirsRemoved = 0;
|
||||||
|
movieDeletedPathForTracking = outputPath;
|
||||||
|
movieCandidatePathForTracking = outputPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove deleted movie paths from output folders tracking
|
// Remove deleted movie paths from output folders tracking
|
||||||
if (summary.movie?.deleted && effectiveOutputPath) {
|
if (summary.movie?.deleted) {
|
||||||
await this.removeJobOutputFolder(jobId, effectiveOutputPath);
|
const trackingCleanupPaths = Array.from(new Set([
|
||||||
|
movieCandidatePathForTracking,
|
||||||
|
movieDeletedPathForTracking
|
||||||
|
].filter(Boolean)));
|
||||||
|
for (const candidatePath of trackingCleanupPaths) {
|
||||||
|
await this.removeJobOutputFolder(jobId, candidatePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.appendLog(
|
await this.appendLog(
|
||||||
|
|||||||
@@ -4900,16 +4900,21 @@ class PipelineService extends EventEmitter {
|
|||||||
: (existing?.device && typeof existing.device === 'object'
|
: (existing?.device && typeof existing.device === 'object'
|
||||||
? existing.device
|
? existing.device
|
||||||
: { path: normalizedPath, mediaProfile: 'cd' });
|
: { path: normalizedPath, mediaProfile: 'cd' });
|
||||||
|
const releasedDevice = {
|
||||||
|
...fallbackDevice,
|
||||||
|
path: normalizedPath,
|
||||||
|
mediaProfile: 'cd'
|
||||||
|
};
|
||||||
|
|
||||||
this._setCdDriveState(normalizedPath, {
|
this._setCdDriveState(normalizedPath, {
|
||||||
state: 'DISC_DETECTED',
|
state: 'DISC_DETECTED',
|
||||||
jobId: null,
|
jobId: null,
|
||||||
device: fallbackDevice,
|
device: releasedDevice,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
eta: null,
|
eta: null,
|
||||||
statusText: null,
|
statusText: null,
|
||||||
context: {
|
context: {
|
||||||
device: fallbackDevice,
|
device: releasedDevice,
|
||||||
devicePath: normalizedPath,
|
devicePath: normalizedPath,
|
||||||
mediaProfile: 'cd'
|
mediaProfile: 'cd'
|
||||||
}
|
}
|
||||||
@@ -5143,6 +5148,149 @@ class PipelineService extends EventEmitter {
|
|||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_forceUnlockDriveLocksForDeletedJobs(devicePaths = [], deletedJobIds = []) {
|
||||||
|
const normalizedPaths = Array.from(new Set(
|
||||||
|
(Array.isArray(devicePaths) ? devicePaths : [])
|
||||||
|
.map((value) => this.normalizeDrivePath(value))
|
||||||
|
.filter((value) => Boolean(value) && !value.startsWith('__virtual__'))
|
||||||
|
));
|
||||||
|
if (normalizedPaths.length === 0 || typeof diskDetectionService.forceUnlockDevice !== 'function') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedSet = new Set(
|
||||||
|
(Array.isArray(deletedJobIds) ? deletedJobIds : [])
|
||||||
|
.map((value) => Number(value))
|
||||||
|
.filter((value) => Number.isFinite(value) && value > 0)
|
||||||
|
.map((value) => Math.trunc(value))
|
||||||
|
);
|
||||||
|
const activeLocks = diskDetectionService.getActiveLocks();
|
||||||
|
const forceUnlockedPaths = [];
|
||||||
|
|
||||||
|
for (const devicePath of normalizedPaths) {
|
||||||
|
const lock = activeLocks.find((entry) => this.normalizeDrivePath(entry?.path) === devicePath) || null;
|
||||||
|
if (!lock) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const owners = Array.isArray(lock?.owners) ? lock.owners : [];
|
||||||
|
const ownerJobIds = owners
|
||||||
|
.map((owner) => Number(owner?.jobId))
|
||||||
|
.filter((value) => Number.isFinite(value) && value > 0)
|
||||||
|
.map((value) => Math.trunc(value));
|
||||||
|
const hasForeignOwner = ownerJobIds.some((jobId) => !deletedSet.has(jobId));
|
||||||
|
if (hasForeignOwner) {
|
||||||
|
logger.warn('drive-lock:force-unlock:skipped-foreign-owner', {
|
||||||
|
devicePath,
|
||||||
|
ownerJobIds
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const clearedCount = Number(diskDetectionService.forceUnlockDevice(devicePath, {
|
||||||
|
reason: 'job_deleted',
|
||||||
|
deletedJobIds: Array.from(deletedSet)
|
||||||
|
}) || 0);
|
||||||
|
if (clearedCount > 0) {
|
||||||
|
forceUnlockedPaths.push(devicePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceUnlockedPaths.length > 0) {
|
||||||
|
logger.warn('drive-lock:force-unlocked-after-delete', {
|
||||||
|
devicePaths: forceUnlockedPaths
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return forceUnlockedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _rescanDrivesAfterDelete(devicePaths = []) {
|
||||||
|
const normalizedPaths = Array.from(new Set(
|
||||||
|
(Array.isArray(devicePaths) ? devicePaths : [])
|
||||||
|
.map((value) => this.normalizeDrivePath(value))
|
||||||
|
.filter((value) => Boolean(value) && !value.startsWith('__virtual__'))
|
||||||
|
));
|
||||||
|
if (normalizedPaths.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryDelaysMs = [0, 450, 1250];
|
||||||
|
for (const devicePath of normalizedPaths) {
|
||||||
|
let lastResult = null;
|
||||||
|
for (let attempt = 0; attempt < retryDelaysMs.length; attempt += 1) {
|
||||||
|
const waitMs = retryDelaysMs[attempt];
|
||||||
|
if (waitMs > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
lastResult = await diskDetectionService.rescanDriveAndEmit(devicePath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('job-delete:drive-rescan:failed', {
|
||||||
|
devicePath,
|
||||||
|
attempt: attempt + 1,
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedProfile = String(lastResult?.device?.mediaProfile || '').trim().toLowerCase();
|
||||||
|
const present = Boolean(lastResult?.present);
|
||||||
|
const locked = Boolean(lastResult?.locked);
|
||||||
|
if (locked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!present) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (detectedProfile && detectedProfile !== 'other' && detectedProfile !== 'unknown') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info('job-delete:drive-rescan:done', {
|
||||||
|
devicePath,
|
||||||
|
emitted: lastResult?.emitted || 'none',
|
||||||
|
present: Boolean(lastResult?.present),
|
||||||
|
mediaProfile: lastResult?.device?.mediaProfile || null,
|
||||||
|
locked: Boolean(lastResult?.locked)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_forceStopActiveProcessForJob(jobId, options = {}) {
|
||||||
|
const normalizedJobId = Number(jobId);
|
||||||
|
if (!Number.isFinite(normalizedJobId) || normalizedJobId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetJobId = Math.trunc(normalizedJobId);
|
||||||
|
const processHandle = this.activeProcesses.get(targetJobId) || null;
|
||||||
|
if (!processHandle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childPid = Number(processHandle?.child?.pid);
|
||||||
|
try {
|
||||||
|
processHandle?.cancel?.();
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore cancellation race errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(childPid) && childPid > 0) {
|
||||||
|
try { process.kill(-childPid, 'SIGKILL'); } catch (_error) { /* noop */ }
|
||||||
|
try { process.kill(childPid, 'SIGKILL'); } catch (_error) { /* noop */ }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
processHandle?.child?.kill?.('SIGKILL');
|
||||||
|
} catch (_error) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn('job-delete:active-process-killed', {
|
||||||
|
jobId: targetJobId,
|
||||||
|
reason: String(options?.reason || '').trim() || null,
|
||||||
|
pid: Number.isFinite(childPid) && childPid > 0 ? childPid : null
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async onJobsDeleted(jobIds = [], options = {}) {
|
async onJobsDeleted(jobIds = [], options = {}) {
|
||||||
const normalizedIds = Array.isArray(jobIds)
|
const normalizedIds = Array.isArray(jobIds)
|
||||||
? jobIds
|
? jobIds
|
||||||
@@ -5167,7 +5315,8 @@ class PipelineService extends EventEmitter {
|
|||||||
const removedQueueEntries = previousQueueLength - this.queueEntries.length;
|
const removedQueueEntries = previousQueueLength - this.queueEntries.length;
|
||||||
|
|
||||||
for (const jobId of normalizedIds) {
|
for (const jobId of normalizedIds) {
|
||||||
this.cancelRequestedByJob.delete(jobId);
|
this.cancelRequestedByJob.add(jobId);
|
||||||
|
this._forceStopActiveProcessForJob(jobId, { reason: 'job_deleted' });
|
||||||
const lockForJob = this._getDriveLockByJobId(jobId);
|
const lockForJob = this._getDriveLockByJobId(jobId);
|
||||||
if (lockForJob?.devicePath) {
|
if (lockForJob?.devicePath) {
|
||||||
affectedDevicePaths.add(this.normalizeDrivePath(lockForJob.devicePath));
|
affectedDevicePaths.add(this.normalizeDrivePath(lockForJob.devicePath));
|
||||||
@@ -5190,6 +5339,14 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.activeProcesses.delete(jobId);
|
this.activeProcesses.delete(jobId);
|
||||||
}
|
}
|
||||||
|
// Keep the cancellation marker briefly so any late async step in the old
|
||||||
|
// pipeline thread aborts before spawning follow-up processes.
|
||||||
|
for (const jobId of normalizedIds) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.cancelRequestedByJob.delete(jobId);
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
const forceUnlockedPaths = this._forceUnlockDriveLocksForDeletedJobs(Array.from(affectedDevicePaths), normalizedIds);
|
||||||
|
|
||||||
let driveResetChanged = false;
|
let driveResetChanged = false;
|
||||||
if (resetDriveState) {
|
if (resetDriveState) {
|
||||||
@@ -5222,6 +5379,10 @@ class PipelineService extends EventEmitter {
|
|||||||
await this.emitQueueChanged();
|
await this.emitQueueChanged();
|
||||||
void this.pumpQueue();
|
void this.pumpQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resetDriveState || forceUnlockedPaths.length > 0) {
|
||||||
|
void this._rescanDrivesAfterDelete(Array.from(affectedDevicePaths));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
cleaned: normalizedIds.length,
|
cleaned: normalizedIds.length,
|
||||||
removedQueueEntries
|
removedQueueEntries
|
||||||
@@ -12780,8 +12941,9 @@ class PipelineService extends EventEmitter {
|
|||||||
applyLine(line, true);
|
applyLine(line, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const normalizedProcessKey = Number(normalizedJobId);
|
||||||
this.activeProcesses.set(Number(normalizedJobId), processHandle);
|
const previousProcessHandle = this.activeProcesses.get(normalizedProcessKey) || null;
|
||||||
|
this.activeProcesses.set(normalizedProcessKey, processHandle);
|
||||||
this.syncPrimaryActiveProcess();
|
this.syncPrimaryActiveProcess();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -12814,8 +12976,18 @@ class PipelineService extends EventEmitter {
|
|||||||
logger.error('command:failed', { jobId, stage, source, error: errorToMeta(error) });
|
logger.error('command:failed', { jobId, stage, source, error: errorToMeta(error) });
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.activeProcesses.delete(Number(normalizedJobId));
|
const activeHandleForJob = this.activeProcesses.get(normalizedProcessKey) || null;
|
||||||
this.cancelRequestedByJob.delete(Number(normalizedJobId));
|
if (activeHandleForJob === processHandle) {
|
||||||
|
if (previousProcessHandle && previousProcessHandle !== processHandle) {
|
||||||
|
this.activeProcesses.set(normalizedProcessKey, previousProcessHandle);
|
||||||
|
} else {
|
||||||
|
this.activeProcesses.delete(normalizedProcessKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Preserve cancellation marker for outer/orchestrating handles (e.g. CD shared handle).
|
||||||
|
if (!(previousProcessHandle && previousProcessHandle !== processHandle)) {
|
||||||
|
this.cancelRequestedByJob.delete(normalizedProcessKey);
|
||||||
|
}
|
||||||
this.syncPrimaryActiveProcess();
|
this.syncPrimaryActiveProcess();
|
||||||
await historyService.closeProcessLog(jobId);
|
await historyService.closeProcessLog(jobId);
|
||||||
await this.emitQueueChanged();
|
await this.emitQueueChanged();
|
||||||
@@ -14963,6 +15135,8 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let currentTrackPosition = null;
|
||||||
|
let buildLiveContext = () => null;
|
||||||
this.activeProcesses.set(processKey, sharedHandle);
|
this.activeProcesses.set(processKey, sharedHandle);
|
||||||
this.syncPrimaryActiveProcess();
|
this.syncPrimaryActiveProcess();
|
||||||
|
|
||||||
@@ -14995,8 +15169,8 @@ class PipelineService extends EventEmitter {
|
|||||||
let encodeCompletedCount = 0;
|
let encodeCompletedCount = 0;
|
||||||
let currentPhase = 'rip';
|
let currentPhase = 'rip';
|
||||||
let currentTrackIndex = effectiveTrackTotal > 0 ? 1 : 0;
|
let currentTrackIndex = effectiveTrackTotal > 0 ? 1 : 0;
|
||||||
let currentTrackPosition = liveTrackRows[0]?.position || null;
|
currentTrackPosition = liveTrackRows[0]?.position || null;
|
||||||
const buildLiveContext = (failedTrackPosition = null) => buildCdLiveProgressSnapshot({
|
buildLiveContext = (failedTrackPosition = null) => buildCdLiveProgressSnapshot({
|
||||||
trackRows: liveTrackRows,
|
trackRows: liveTrackRows,
|
||||||
phase: currentPhase,
|
phase: currentPhase,
|
||||||
trackIndex: currentTrackIndex,
|
trackIndex: currentTrackIndex,
|
||||||
|
|||||||
@@ -576,7 +576,8 @@ function mapPresetEntriesToOptions(entries) {
|
|||||||
* Parses the drive_devices JSON setting into a normalized array of {path, makemkvIndex}.
|
* Parses the drive_devices JSON setting into a normalized array of {path, makemkvIndex}.
|
||||||
* Supports both the legacy string format ["/dev/sr0"] and the object format
|
* Supports both the legacy string format ["/dev/sr0"] and the object format
|
||||||
* [{"path": "/dev/sr0", "makemkvIndex": 0}].
|
* [{"path": "/dev/sr0", "makemkvIndex": 0}].
|
||||||
* When makemkvIndex is missing, it is auto-derived from the device name (sr0→0, sr1→1).
|
* When makemkvIndex is missing, it is assigned by list order (0,1,2,...) so
|
||||||
|
* sparse kernel names like /dev/sr2 still map to contiguous MakeMKV indices.
|
||||||
*/
|
*/
|
||||||
function parseDriveDeviceEntries(raw) {
|
function parseDriveDeviceEntries(raw) {
|
||||||
try {
|
try {
|
||||||
@@ -584,28 +585,49 @@ function parseDriveDeviceEntries(raw) {
|
|||||||
if (!Array.isArray(arr)) {
|
if (!Array.isArray(arr)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return arr.map((entry) => {
|
const normalized = arr.map((entry) => {
|
||||||
if (typeof entry === 'string') {
|
if (typeof entry === 'string') {
|
||||||
const p = entry.trim();
|
const p = entry.trim();
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const m = p.match(/sr(\d+)$/);
|
return { path: p, makemkvIndex: null };
|
||||||
return { path: p, makemkvIndex: m ? Number(m[1]) : 0 };
|
|
||||||
}
|
}
|
||||||
if (entry && typeof entry === 'object' && entry.path) {
|
if (entry && typeof entry === 'object' && entry.path) {
|
||||||
const p = String(entry.path || '').trim();
|
const p = String(entry.path || '').trim();
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const idxRaw = entry.makemkvIndex != null ? Number(entry.makemkvIndex) : null;
|
const idxRaw = Number(entry.makemkvIndex);
|
||||||
const m = p.match(/sr(\d+)$/);
|
return {
|
||||||
const derived = m ? Number(m[1]) : 0;
|
path: p,
|
||||||
const idx = Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : derived;
|
makemkvIndex: Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : null
|
||||||
return { path: p, makemkvIndex: idx };
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
const used = new Set();
|
||||||
|
let nextAutoIndex = 0;
|
||||||
|
return normalized.map((entry) => {
|
||||||
|
if (entry.makemkvIndex != null) {
|
||||||
|
used.add(entry.makemkvIndex);
|
||||||
|
if (entry.makemkvIndex >= nextAutoIndex) {
|
||||||
|
nextAutoIndex = entry.makemkvIndex + 1;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
while (used.has(nextAutoIndex)) {
|
||||||
|
nextAutoIndex += 1;
|
||||||
|
}
|
||||||
|
const resolved = {
|
||||||
|
path: entry.path,
|
||||||
|
makemkvIndex: nextAutoIndex
|
||||||
|
};
|
||||||
|
used.add(nextAutoIndex);
|
||||||
|
nextAutoIndex += 1;
|
||||||
|
return resolved;
|
||||||
|
});
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1413,17 +1435,31 @@ class SettingsService {
|
|||||||
|
|
||||||
resolveSourceArg(map, deviceInfo = null) {
|
resolveSourceArg(map, deviceInfo = null) {
|
||||||
const devicePath = String(deviceInfo?.path || '').trim();
|
const devicePath = String(deviceInfo?.path || '').trim();
|
||||||
|
const deviceIndex = Number(deviceInfo?.makemkvIndex ?? deviceInfo?.index);
|
||||||
|
const configuredEntries = parseDriveDeviceEntries(map.drive_devices);
|
||||||
|
|
||||||
// In explicit mode: look up per-drive makemkvIndex from drive_devices
|
// In explicit mode: look up per-drive makemkvIndex from drive_devices
|
||||||
if (devicePath && map.drive_mode === 'explicit') {
|
if (devicePath && map.drive_mode === 'explicit') {
|
||||||
const entries = parseDriveDeviceEntries(map.drive_devices);
|
const entry = configuredEntries.find((e) => e.path === devicePath);
|
||||||
const entry = entries.find((e) => e.path === devicePath);
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
return `disc:${entry.makemkvIndex}`;
|
return `disc:${entry.makemkvIndex}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-derive from device name (/dev/sr0 → disc:0, /dev/sr1 → disc:1)
|
// Prefer configured per-drive index when a path match exists (also in auto mode).
|
||||||
|
if (devicePath) {
|
||||||
|
const configured = configuredEntries.find((e) => e.path === devicePath);
|
||||||
|
if (configured) {
|
||||||
|
return `disc:${configured.makemkvIndex}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer device-provided MakeMKV index from disk detection service.
|
||||||
|
if (Number.isFinite(deviceIndex) && deviceIndex >= 0) {
|
||||||
|
return `disc:${Math.trunc(deviceIndex)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last automatic fallback: derive from device name (/dev/sr0 → disc:0).
|
||||||
if (devicePath) {
|
if (devicePath) {
|
||||||
const match = devicePath.match(/sr(\d+)$/);
|
const match = devicePath.match(/sr(\d+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+117
-8
@@ -28,6 +28,15 @@ function clampPercent(value) {
|
|||||||
return Math.max(0, Math.min(100, parsed));
|
return Math.max(0, Math.min(100, parsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStage(value) {
|
||||||
|
return String(value || '').trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalStage(value) {
|
||||||
|
const normalized = normalizeStage(value);
|
||||||
|
return normalized === 'FINISHED' || normalized === 'ERROR' || normalized === 'CANCELLED';
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(value) {
|
function formatBytes(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||||
@@ -297,8 +306,49 @@ function App() {
|
|||||||
: null;
|
: null;
|
||||||
setPipeline((prev) => {
|
setPipeline((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
|
const normalizedProgressJobId = normalizeJobId(progressJobId);
|
||||||
|
const progressStage = normalizeStage(payload?.state);
|
||||||
|
const isCdProgressStage = progressStage === 'CD_ANALYZING'
|
||||||
|
|| progressStage === 'CD_RIPPING'
|
||||||
|
|| progressStage === 'CD_ENCODING';
|
||||||
|
const incomingIsTerminal = isTerminalStage(progressStage);
|
||||||
|
const prevActiveJobId = normalizeJobId(prev?.activeJobId || prev?.context?.jobId);
|
||||||
|
const prevJobProgress = normalizedProgressJobId
|
||||||
|
? (prev?.jobProgress?.[normalizedProgressJobId] || null)
|
||||||
|
: null;
|
||||||
|
const prevJobProgressState = normalizeStage(prevJobProgress?.state);
|
||||||
|
const prevJobProgressIsTerminal = isTerminalStage(prevJobProgressState);
|
||||||
|
const matchingCdDriveEntry = normalizedProgressJobId
|
||||||
|
? Object.values(prev?.cdDrives || {}).find((driveState) => normalizeJobId(driveState?.jobId) === normalizedProgressJobId)
|
||||||
|
: null;
|
||||||
|
const matchingCdDriveIsTerminal = isTerminalStage(matchingCdDriveEntry?.state);
|
||||||
|
const hasKnownBinding = Boolean(
|
||||||
|
normalizedProgressJobId
|
||||||
|
&& (
|
||||||
|
prevActiveJobId === normalizedProgressJobId
|
||||||
|
|| prevJobProgress
|
||||||
|
|| matchingCdDriveEntry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ignore late/stale progress packets that arrive after a terminal state
|
||||||
|
// or for jobs that are no longer bound in pipeline state.
|
||||||
|
if (
|
||||||
|
normalizedProgressJobId
|
||||||
|
&& !incomingIsTerminal
|
||||||
|
&& (
|
||||||
|
prevJobProgressIsTerminal
|
||||||
|
|| matchingCdDriveIsTerminal
|
||||||
|
|| !hasKnownBinding
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
if (progressJobId != null) {
|
if (progressJobId != null) {
|
||||||
const previousJobProgress = prev?.jobProgress?.[progressJobId] || {};
|
const previousJobProgress = prev?.jobProgress?.[progressJobId] || {};
|
||||||
|
const previousJobStage = normalizeStage(previousJobProgress?.state);
|
||||||
|
const keepPreviousJobStage = isTerminalStage(previousJobStage) && !incomingIsTerminal;
|
||||||
const mergedJobContext = contextPatch
|
const mergedJobContext = contextPatch
|
||||||
? {
|
? {
|
||||||
...(previousJobProgress?.context && typeof previousJobProgress.context === 'object'
|
...(previousJobProgress?.context && typeof previousJobProgress.context === 'object'
|
||||||
@@ -313,19 +363,21 @@ function App() {
|
|||||||
...(prev?.jobProgress || {}),
|
...(prev?.jobProgress || {}),
|
||||||
[progressJobId]: {
|
[progressJobId]: {
|
||||||
...previousJobProgress,
|
...previousJobProgress,
|
||||||
state: payload.state,
|
state: keepPreviousJobStage ? previousJobProgress.state : payload.state,
|
||||||
progress: payload.progress,
|
progress: keepPreviousJobStage ? previousJobProgress.progress : payload.progress,
|
||||||
eta: payload.eta,
|
eta: keepPreviousJobStage ? previousJobProgress.eta : payload.eta,
|
||||||
statusText: payload.statusText,
|
statusText: keepPreviousJobStage ? previousJobProgress.statusText : payload.statusText,
|
||||||
...(mergedJobContext !== undefined ? { context: mergedJobContext } : {})
|
...(mergedJobContext !== undefined ? { context: mergedJobContext } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (progressJobId === prev?.activeJobId || progressJobId == null) {
|
if (progressJobId === prev?.activeJobId || progressJobId == null) {
|
||||||
next.state = payload.state ?? prev?.state;
|
const previousGlobalStage = normalizeStage(prev?.state);
|
||||||
next.progress = payload.progress ?? prev?.progress;
|
const keepPreviousGlobalStage = isTerminalStage(previousGlobalStage) && !incomingIsTerminal;
|
||||||
next.eta = payload.eta ?? prev?.eta;
|
next.state = keepPreviousGlobalStage ? prev?.state : (payload.state ?? prev?.state);
|
||||||
next.statusText = payload.statusText ?? prev?.statusText;
|
next.progress = keepPreviousGlobalStage ? prev?.progress : (payload.progress ?? prev?.progress);
|
||||||
|
next.eta = keepPreviousGlobalStage ? prev?.eta : (payload.eta ?? prev?.eta);
|
||||||
|
next.statusText = keepPreviousGlobalStage ? prev?.statusText : (payload.statusText ?? prev?.statusText);
|
||||||
if (contextPatch) {
|
if (contextPatch) {
|
||||||
next.context = {
|
next.context = {
|
||||||
...(prev?.context && typeof prev.context === 'object' ? prev.context : {}),
|
...(prev?.context && typeof prev.context === 'object' ? prev.context : {}),
|
||||||
@@ -333,6 +385,63 @@ function App() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep per-drive CD progress in sync with live progress events.
|
||||||
|
// Backend sends frequent PIPELINE_PROGRESS updates, while cdDrives
|
||||||
|
// snapshots are only broadcast on state transitions.
|
||||||
|
if (isCdProgressStage && prev?.cdDrives && typeof prev.cdDrives === 'object') {
|
||||||
|
const cdDrivesEntries = Object.entries(prev.cdDrives);
|
||||||
|
const nextCdDrives = { ...prev.cdDrives };
|
||||||
|
const patchDrive = (driveState) => {
|
||||||
|
const currentDriveStage = normalizeStage(driveState?.state);
|
||||||
|
if (isTerminalStage(currentDriveStage) && !incomingIsTerminal) {
|
||||||
|
return driveState;
|
||||||
|
}
|
||||||
|
const mergedContext = contextPatch
|
||||||
|
? {
|
||||||
|
...(driveState?.context && typeof driveState.context === 'object' ? driveState.context : {}),
|
||||||
|
...contextPatch
|
||||||
|
}
|
||||||
|
: driveState?.context;
|
||||||
|
return {
|
||||||
|
...driveState,
|
||||||
|
state: payload?.state ?? driveState?.state,
|
||||||
|
progress: payload?.progress ?? driveState?.progress,
|
||||||
|
eta: payload?.eta ?? driveState?.eta,
|
||||||
|
statusText: payload?.statusText ?? driveState?.statusText,
|
||||||
|
...(mergedContext !== undefined ? { context: mergedContext } : {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
if (normalizedProgressJobId) {
|
||||||
|
for (const [drivePath, driveState] of cdDrivesEntries) {
|
||||||
|
if (normalizeJobId(driveState?.jobId) === normalizedProgressJobId) {
|
||||||
|
nextCdDrives[drivePath] = patchDrive(driveState);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
const activeCdEntries = cdDrivesEntries.filter(([, driveState]) => {
|
||||||
|
const driveStage = String(driveState?.state || '').trim().toUpperCase();
|
||||||
|
return driveStage === 'CD_ANALYZING'
|
||||||
|
|| driveStage === 'CD_RIPPING'
|
||||||
|
|| driveStage === 'CD_ENCODING';
|
||||||
|
});
|
||||||
|
if (activeCdEntries.length === 1) {
|
||||||
|
const [drivePath, driveState] = activeCdEntries[0];
|
||||||
|
nextCdDrives[drivePath] = patchDrive(driveState);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
next.cdDrives = nextCdDrives;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -826,7 +826,6 @@ export const api = {
|
|||||||
forceRefresh: options.forceRefresh
|
forceRefresh: options.forceRefresh
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── User Presets ───────────────────────────────────────────────────────────
|
// ── User Presets ───────────────────────────────────────────────────────────
|
||||||
getUserPresets(mediaType = null, options = {}) {
|
getUserPresets(mediaType = null, options = {}) {
|
||||||
const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
|
const suffix = mediaType ? `?media_type=${encodeURIComponent(mediaType)}` : '';
|
||||||
|
|||||||
@@ -731,6 +731,7 @@ export default function CdRipConfigPanel({
|
|||||||
const completedEncodeCount = selectedTrackRows.filter((track) => track.encodeStatus === 'done').length;
|
const completedEncodeCount = selectedTrackRows.filter((track) => track.encodeStatus === 'done').length;
|
||||||
const liveCurrentTrackPosition = normalizePosition(cdLive?.trackPosition);
|
const liveCurrentTrackPosition = normalizePosition(cdLive?.trackPosition);
|
||||||
const lastState = String(context?.lastState || '').trim().toUpperCase();
|
const lastState = String(context?.lastState || '').trim().toUpperCase();
|
||||||
|
const lastStateLabel = getStatusLabel(lastState);
|
||||||
const failureStage = String(context?.stage || '').trim().toUpperCase();
|
const failureStage = String(context?.stage || '').trim().toUpperCase();
|
||||||
const normalizedLivePhase = String(cdLive?.phase || '').trim().toLowerCase();
|
const normalizedLivePhase = String(cdLive?.phase || '').trim().toLowerCase();
|
||||||
const normalizedStatusText = String(statusText || '').trim().toUpperCase();
|
const normalizedStatusText = String(statusText || '').trim().toUpperCase();
|
||||||
@@ -897,12 +898,11 @@ export default function CdRipConfigPanel({
|
|||||||
<div><strong>Pre-Ketten:</strong> {preChainNames.length > 0 ? preChainNames.join(' | ') : '-'}</div>
|
<div><strong>Pre-Ketten:</strong> {preChainNames.length > 0 ? preChainNames.join(' | ') : '-'}</div>
|
||||||
<div><strong>Post-Skripte:</strong> {postScriptNames.length > 0 ? postScriptNames.join(' | ') : '-'}</div>
|
<div><strong>Post-Skripte:</strong> {postScriptNames.length > 0 ? postScriptNames.join(' | ') : '-'}</div>
|
||||||
<div><strong>Post-Ketten:</strong> {postChainNames.length > 0 ? postChainNames.join(' | ') : '-'}</div>
|
<div><strong>Post-Ketten:</strong> {postChainNames.length > 0 ? postChainNames.join(' | ') : '-'}</div>
|
||||||
<div><strong>Tracks fertig:</strong> {completedEncodeCount} / {selectedTrackRows.length || effectiveTrackRows.length}</div>
|
|
||||||
<div><strong>Auswahl:</strong> {selectedTrackNumbers || '-'}</div>
|
<div><strong>Auswahl:</strong> {selectedTrackNumbers || '-'}</div>
|
||||||
<div><strong>Gesamtdauer:</strong> {formatTotalDuration(selectedTrackDurationSec)}</div>
|
<div><strong>Gesamtdauer:</strong> {formatTotalDuration(selectedTrackDurationSec)}</div>
|
||||||
<div><strong>Laufwerk:</strong> {devicePath}</div>
|
<div><strong>Laufwerk:</strong> {devicePath}</div>
|
||||||
<div><strong>Output-Pfad:</strong> {outputPath}</div>
|
<div><strong>Output-Pfad:</strong> {outputPath}</div>
|
||||||
{lastState ? <div><strong>Letzter Pipeline-State:</strong> {lastState}</div> : null}
|
{lastState ? <div><strong>Letzter Pipeline-State:</strong> {lastStateLabel}</div> : null}
|
||||||
{jobId ? <div><strong>Job-ID:</strong> #{jobId}</div> : null}
|
{jobId ? <div><strong>Job-ID:</strong> #{jobId}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,16 +94,40 @@ function DriveDevicesEditor({ value, onChange, settingKey }) {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(val || '[]');
|
const parsed = JSON.parse(val || '[]');
|
||||||
if (!Array.isArray(parsed)) return [];
|
if (!Array.isArray(parsed)) return [];
|
||||||
return parsed.map((e) => {
|
const normalized = parsed.map((e) => {
|
||||||
if (typeof e === 'string') {
|
if (typeof e === 'string') {
|
||||||
const p = e.trim();
|
const p = e.trim();
|
||||||
const m = p.match(/sr(\d+)$/);
|
if (!p) return null;
|
||||||
return { path: p, makemkvIndex: m ? Number(m[1]) : 0 };
|
return { path: p, makemkvIndex: null };
|
||||||
}
|
}
|
||||||
if (e && typeof e === 'object') {
|
if (e && typeof e === 'object') {
|
||||||
return { path: String(e.path || '').trim(), makemkvIndex: Number.isFinite(Number(e.makemkvIndex)) ? Number(e.makemkvIndex) : 0 };
|
const p = String(e.path || '').trim();
|
||||||
|
if (!p) return null;
|
||||||
|
const idxRaw = Number(e.makemkvIndex);
|
||||||
|
return {
|
||||||
|
path: p,
|
||||||
|
makemkvIndex: Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { path: '', makemkvIndex: 0 };
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
const used = new Set();
|
||||||
|
let nextAutoIndex = 0;
|
||||||
|
return normalized.map((entry) => {
|
||||||
|
if (entry.makemkvIndex != null) {
|
||||||
|
used.add(entry.makemkvIndex);
|
||||||
|
if (entry.makemkvIndex >= nextAutoIndex) {
|
||||||
|
nextAutoIndex = entry.makemkvIndex + 1;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
while (used.has(nextAutoIndex)) {
|
||||||
|
nextAutoIndex += 1;
|
||||||
|
}
|
||||||
|
const resolved = { path: entry.path, makemkvIndex: nextAutoIndex };
|
||||||
|
used.add(nextAutoIndex);
|
||||||
|
nextAutoIndex += 1;
|
||||||
|
return resolved;
|
||||||
});
|
});
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return [];
|
return [];
|
||||||
@@ -129,8 +153,7 @@ function DriveDevicesEditor({ value, onChange, settingKey }) {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = [...devices];
|
const next = [...devices];
|
||||||
const p = e.target.value;
|
const p = e.target.value;
|
||||||
const m = p.match(/sr(\d+)$/);
|
next[idx] = { ...next[idx], path: p };
|
||||||
next[idx] = { ...next[idx], path: p, makemkvIndex: m ? Number(m[1]) : next[idx].makemkvIndex };
|
|
||||||
updateDevices(next);
|
updateDevices(next);
|
||||||
}}
|
}}
|
||||||
className="drive-device-input"
|
className="drive-device-input"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import blurayIndicatorIcon from '../assets/media-bluray.svg';
|
|||||||
import discIndicatorIcon from '../assets/media-disc.svg';
|
import discIndicatorIcon from '../assets/media-disc.svg';
|
||||||
import otherIndicatorIcon from '../assets/media-other.svg';
|
import otherIndicatorIcon from '../assets/media-other.svg';
|
||||||
import { resolveJobPluginExecution } from '../utils/pluginExecution';
|
import { resolveJobPluginExecution } from '../utils/pluginExecution';
|
||||||
import { getStatusLabel } from '../utils/statusPresentation';
|
import { getProcessStatusLabel, getStatusLabel } from '../utils/statusPresentation';
|
||||||
|
|
||||||
const CD_FORMAT_LABELS = {
|
const CD_FORMAT_LABELS = {
|
||||||
flac: 'FLAC',
|
flac: 'FLAC',
|
||||||
@@ -26,9 +26,10 @@ function JsonView({ title, value }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ScriptResultRow({ result }) {
|
function ScriptResultRow({ result }) {
|
||||||
const status = String(result?.status || '').toUpperCase();
|
const statusCode = String(result?.status || '').toUpperCase();
|
||||||
const isSuccess = status === 'SUCCESS';
|
const status = getProcessStatusLabel(statusCode);
|
||||||
const isError = status === 'ERROR';
|
const isSuccess = statusCode === 'SUCCESS';
|
||||||
|
const isError = statusCode === 'ERROR';
|
||||||
const icon = isSuccess ? 'pi-check-circle' : isError ? 'pi-times-circle' : 'pi-minus-circle';
|
const icon = isSuccess ? 'pi-check-circle' : isError ? 'pi-times-circle' : 'pi-minus-circle';
|
||||||
const tone = isSuccess ? 'success' : isError ? 'danger' : 'warning';
|
const tone = isSuccess ? 'success' : isError ? 'danger' : 'warning';
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -197,6 +197,40 @@ function runtimeOutcomeMeta(outcome, status) {
|
|||||||
return runtimeStatusMeta(status);
|
return runtimeStatusMeta(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function driveStateIconMeta(stateLabel) {
|
||||||
|
const normalized = String(stateLabel || '').trim().toUpperCase();
|
||||||
|
if (!normalized || normalized === 'LEER' || normalized === 'IDLE') {
|
||||||
|
return { icon: 'pi-stop', spin: false };
|
||||||
|
}
|
||||||
|
if (normalized === 'DISC_DETECTED') {
|
||||||
|
return { icon: 'pi-check', spin: false };
|
||||||
|
}
|
||||||
|
if (normalized === 'FINISHED') {
|
||||||
|
return { icon: 'pi-check-circle', spin: false };
|
||||||
|
}
|
||||||
|
if (normalized === 'ERROR' || normalized === 'CANCELLED') {
|
||||||
|
return { icon: 'pi-times-circle', spin: false };
|
||||||
|
}
|
||||||
|
if (processingStates.includes(normalized)) {
|
||||||
|
return { icon: 'pi-spinner', spin: true };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized === 'METADATA_SELECTION'
|
||||||
|
|| normalized === 'CD_METADATA_SELECTION'
|
||||||
|
|| normalized === 'WAITING_FOR_USER_DECISION'
|
||||||
|
) {
|
||||||
|
return { icon: 'pi-list', spin: false };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized === 'READY_TO_START'
|
||||||
|
|| normalized === 'READY_TO_ENCODE'
|
||||||
|
|| normalized === 'CD_READY_TO_RIP'
|
||||||
|
) {
|
||||||
|
return { icon: 'pi-play', spin: false };
|
||||||
|
}
|
||||||
|
return { icon: 'pi-question-circle', spin: false };
|
||||||
|
}
|
||||||
|
|
||||||
function hasRuntimeOutputDetails(item) {
|
function hasRuntimeOutputDetails(item) {
|
||||||
if (!item || typeof item !== 'object') {
|
if (!item || typeof item !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
@@ -304,6 +338,20 @@ function normalizeQueue(queue) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCdDriveLifecycleKey(cdDrives) {
|
||||||
|
if (!cdDrives || typeof cdDrives !== 'object') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const entries = Object.entries(cdDrives)
|
||||||
|
.map(([devicePath, driveState]) => {
|
||||||
|
const state = String(driveState?.state || '').trim().toUpperCase();
|
||||||
|
const jobId = normalizeJobId(driveState?.jobId || driveState?.context?.jobId) || 0;
|
||||||
|
return `${devicePath}|${jobId}|${state}`;
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
return entries.join('::');
|
||||||
|
}
|
||||||
|
|
||||||
function getQueueActionResult(response) {
|
function getQueueActionResult(response) {
|
||||||
return response?.result && typeof response.result === 'object' ? response.result : {};
|
return response?.result && typeof response.result === 'object' ? response.result : {};
|
||||||
}
|
}
|
||||||
@@ -917,6 +965,10 @@ export default function DashboardPage({
|
|||||||
() => normalizeHardwareMonitoringPayload(hardwareMonitoring),
|
() => normalizeHardwareMonitoringPayload(hardwareMonitoring),
|
||||||
[hardwareMonitoring]
|
[hardwareMonitoring]
|
||||||
);
|
);
|
||||||
|
const cdDriveLifecycleKey = useMemo(
|
||||||
|
() => buildCdDriveLifecycleKey(pipeline?.cdDrives),
|
||||||
|
[pipeline?.cdDrives]
|
||||||
|
);
|
||||||
const monitoringSample = monitoringState.sample;
|
const monitoringSample = monitoringState.sample;
|
||||||
const cpuMetrics = monitoringSample?.cpu || null;
|
const cpuMetrics = monitoringSample?.cpu || null;
|
||||||
const memoryMetrics = monitoringSample?.memory || null;
|
const memoryMetrics = monitoringSample?.memory || null;
|
||||||
@@ -1115,13 +1167,43 @@ export default function DashboardPage({
|
|||||||
}
|
}
|
||||||
}, [pipeline?.cdDrives]);
|
}, [pipeline?.cdDrives]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cdMetadataDialogVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dialogJobId = normalizeJobId(cdMetadataDialogContext?.jobId);
|
||||||
|
if (!dialogJobId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cdDrives = pipeline?.cdDrives;
|
||||||
|
if (!cdDrives || typeof cdDrives !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stillInMetadataSelection = false;
|
||||||
|
for (const drive of Object.values(cdDrives)) {
|
||||||
|
const driveState = String(drive?.state || '').trim().toUpperCase();
|
||||||
|
const ctx = drive?.context && typeof drive.context === 'object' ? drive.context : null;
|
||||||
|
const driveJobId = normalizeJobId(ctx?.jobId || drive?.jobId);
|
||||||
|
if (driveJobId === dialogJobId && driveState === 'CD_METADATA_SELECTION') {
|
||||||
|
stillInMetadataSelection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stillInMetadataSelection) {
|
||||||
|
setCdMetadataDialogVisible(false);
|
||||||
|
setCdMetadataDialogContext(null);
|
||||||
|
}
|
||||||
|
}, [cdMetadataDialogVisible, cdMetadataDialogContext?.jobId, pipeline?.cdDrives]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQueueState(normalizeQueue(pipeline?.queue));
|
setQueueState(normalizeQueue(pipeline?.queue));
|
||||||
}, [pipeline?.queue]);
|
}, [pipeline?.queue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadDashboardJobs();
|
void loadDashboardJobs();
|
||||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, pipeline?.cdDrives, jobsRefreshToken]);
|
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, cdDriveLifecycleKey, jobsRefreshToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getDetectedDrives()
|
api.getDetectedDrives()
|
||||||
@@ -1553,18 +1635,28 @@ export default function DashboardPage({
|
|||||||
try {
|
try {
|
||||||
const response = await api.deleteJobFiles(jobId, effectiveTarget);
|
const response = await api.deleteJobFiles(jobId, effectiveTarget);
|
||||||
const summary = response?.summary || {};
|
const summary = response?.summary || {};
|
||||||
const deletedFiles = effectiveTarget === 'raw'
|
const targetSummary = effectiveTarget === 'raw'
|
||||||
? (summary.raw?.filesDeleted ?? 0)
|
? (summary.raw || {})
|
||||||
: (summary.movie?.filesDeleted ?? 0);
|
: (summary.movie || {});
|
||||||
const removedDirs = effectiveTarget === 'raw'
|
const deletedFiles = targetSummary.filesDeleted ?? 0;
|
||||||
? (summary.raw?.dirsRemoved ?? 0)
|
const removedDirs = targetSummary.dirsRemoved ?? 0;
|
||||||
: (summary.movie?.dirsRemoved ?? 0);
|
const deletedSomething = Boolean(targetSummary.deleted);
|
||||||
|
|
||||||
|
if (!deletedSomething) {
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: effectiveTarget === 'raw' ? 'RAW nicht gelöscht' : 'Movie nicht gelöscht',
|
||||||
|
detail: targetSummary.reason || 'Keine passenden Dateien/Ordner gefunden.',
|
||||||
|
life: 4200
|
||||||
|
});
|
||||||
|
} else {
|
||||||
toastRef.current?.show({
|
toastRef.current?.show({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
|
summary: effectiveTarget === 'raw' ? 'RAW gelöscht' : 'Movie gelöscht',
|
||||||
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
|
detail: `Entfernt: ${deletedFiles} Datei(en), ${removedDirs} Ordner.`,
|
||||||
life: 4000
|
life: 4000
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await loadDashboardJobs();
|
await loadDashboardJobs();
|
||||||
await refreshPipeline();
|
await refreshPipeline();
|
||||||
setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
|
setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
|
||||||
@@ -2503,6 +2595,8 @@ export default function DashboardPage({
|
|||||||
const canAnalyzeDrive = cdState
|
const canAnalyzeDrive = cdState
|
||||||
? cdState === 'DISC_DETECTED'
|
? cdState === 'DISC_DETECTED'
|
||||||
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
|
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
|
||||||
|
const stateIconMeta = driveStateIconMeta(stateLabel);
|
||||||
|
const discArg = String(drv.discArg || '').trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={drivePath} className="drive-list-item">
|
<div key={drivePath} className="drive-list-item">
|
||||||
@@ -2510,10 +2604,27 @@ export default function DashboardPage({
|
|||||||
<div className="drive-list-info">
|
<div className="drive-list-info">
|
||||||
<code className="drive-list-path">{drivePath}</code>
|
<code className="drive-list-path">{drivePath}</code>
|
||||||
{drv.model && <span className="drive-list-model">{drv.model}</span>}
|
{drv.model && <span className="drive-list-model">{drv.model}</span>}
|
||||||
{drv.discArg && <span className="drive-list-disc-arg">{drv.discArg}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="drive-list-state">
|
<div className="drive-list-state">
|
||||||
{stateLabel && <Tag value={stateLabel} severity={stateSeverity} className="drive-list-status-tag" />}
|
{stateLabel ? (
|
||||||
|
<span
|
||||||
|
className={`drive-list-status-icon tone-${stateSeverity}`}
|
||||||
|
title={getStatusLabel(stateLabel)}
|
||||||
|
aria-label={`Status: ${getStatusLabel(stateLabel)}`}
|
||||||
|
>
|
||||||
|
<i className={`pi ${stateIconMeta.icon}${stateIconMeta.spin ? ' pi-spin' : ''}`} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{isCdDriveLocked ? (
|
||||||
|
<span
|
||||||
|
className="drive-list-lock-indicator"
|
||||||
|
title="Laufwerk ist gesperrt, bis Rip/Encode abgeschlossen ist."
|
||||||
|
aria-label="Laufwerk gesperrt"
|
||||||
|
>
|
||||||
|
<i className="pi pi-lock" aria-hidden="true" />
|
||||||
|
<span>gesperrt</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="drive-list-actions">
|
<div className="drive-list-actions">
|
||||||
<Button
|
<Button
|
||||||
@@ -2522,6 +2633,7 @@ export default function DashboardPage({
|
|||||||
rounded
|
rounded
|
||||||
size="small"
|
size="small"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
|
className="drive-list-action-btn"
|
||||||
loading={isRescanBusy}
|
loading={isRescanBusy}
|
||||||
disabled={isRescanBusy || isDriveActive || isCdDriveLocked}
|
disabled={isRescanBusy || isDriveActive || isCdDriveLocked}
|
||||||
onClick={() => handleRescanDrive(drivePath)}
|
onClick={() => handleRescanDrive(drivePath)}
|
||||||
@@ -2535,6 +2647,7 @@ export default function DashboardPage({
|
|||||||
rounded
|
rounded
|
||||||
size="small"
|
size="small"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
|
className="drive-list-action-btn"
|
||||||
loading={isAnalyzeBusy}
|
loading={isAnalyzeBusy}
|
||||||
disabled={isAnalyzeBusy}
|
disabled={isAnalyzeBusy}
|
||||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||||
@@ -2544,15 +2657,19 @@ export default function DashboardPage({
|
|||||||
)}
|
)}
|
||||||
{cdState === 'CD_METADATA_SELECTION' && (
|
{cdState === 'CD_METADATA_SELECTION' && (
|
||||||
<Button
|
<Button
|
||||||
label="Metadaten"
|
|
||||||
icon="pi pi-list"
|
icon="pi pi-list"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
size="small"
|
size="small"
|
||||||
severity="info"
|
severity="info"
|
||||||
|
className="drive-list-action-btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
setCdMetadataDialogContext({ ...driveCtx, jobId: driveJobId });
|
||||||
setCdMetadataDialogVisible(true);
|
setCdMetadataDialogVisible(true);
|
||||||
}}
|
}}
|
||||||
disabled={!driveJobId}
|
disabled={!driveJobId}
|
||||||
|
title="Metadaten auswählen"
|
||||||
|
aria-label="Metadaten auswählen"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
|
{(cdState === 'ERROR' || cdState === 'CANCELLED') && driveJobId && (
|
||||||
@@ -2562,6 +2679,7 @@ export default function DashboardPage({
|
|||||||
rounded
|
rounded
|
||||||
size="small"
|
size="small"
|
||||||
severity="warning"
|
severity="warning"
|
||||||
|
className="drive-list-action-btn"
|
||||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||||
loading={isAnalyzeBusy}
|
loading={isAnalyzeBusy}
|
||||||
disabled={isAnalyzeBusy}
|
disabled={isAnalyzeBusy}
|
||||||
@@ -2571,7 +2689,12 @@ export default function DashboardPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{discInfo && <div className="drive-list-disc-label">{discInfo}</div>}
|
{(discArg || discInfo) ? (
|
||||||
|
<div className="drive-list-disc-meta">
|
||||||
|
{discArg ? <span className="drive-list-disc-arg">{discArg}</span> : null}
|
||||||
|
{discInfo ? <span className="drive-list-disc-label" title={discInfo}>{discInfo}</span> : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{showProgress && (
|
{showProgress && (
|
||||||
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||||
)}
|
)}
|
||||||
@@ -2663,7 +2786,7 @@ export default function DashboardPage({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card title="Job Übersicht" subTitle="Kompakte Liste; Klick auf Zeile öffnet die volle Job-Detailansicht mit passenden CTAs">
|
<Card title="Job Übersicht" subTitle="Kompakte Liste; Klick auf Zeile öffnet die volle Job-Detailansicht mit passenden CTAs">
|
||||||
{jobsLoading ? (
|
{jobsLoading && dashboardJobs.length === 0 ? (
|
||||||
<p>Jobs werden geladen ...</p>
|
<p>Jobs werden geladen ...</p>
|
||||||
) : dashboardJobs.length === 0 ? (
|
) : dashboardJobs.length === 0 ? (
|
||||||
<p>Keine relevanten Jobs im Dashboard (aktive/fortsetzbare Status).</p>
|
<p>Keine relevanten Jobs im Dashboard (aktive/fortsetzbare Status).</p>
|
||||||
|
|||||||
@@ -823,12 +823,34 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
try {
|
try {
|
||||||
const response = await api.deleteJobFiles(row.id, target);
|
const response = await api.deleteJobFiles(row.id, target);
|
||||||
const summary = response.summary || {};
|
const summary = response.summary || {};
|
||||||
|
const rawSummary = summary.raw || {};
|
||||||
|
const movieSummary = summary.movie || {};
|
||||||
|
const deletedSomething = target === 'raw'
|
||||||
|
? Boolean(rawSummary.deleted)
|
||||||
|
: target === 'movie'
|
||||||
|
? Boolean(movieSummary.deleted)
|
||||||
|
: Boolean(rawSummary.deleted || movieSummary.deleted);
|
||||||
|
|
||||||
|
if (!deletedSomething) {
|
||||||
|
const reason = target === 'raw'
|
||||||
|
? (rawSummary.reason || 'Keine passenden RAW-Dateien/Ordner gefunden.')
|
||||||
|
: target === 'movie'
|
||||||
|
? (movieSummary.reason || `Keine passenden ${outputShortLabel}-Dateien/Ordner gefunden.`)
|
||||||
|
: (movieSummary.reason || rawSummary.reason || 'Keine passenden Dateien/Ordner gefunden.');
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Nichts gelöscht',
|
||||||
|
detail: reason,
|
||||||
|
life: 4200
|
||||||
|
});
|
||||||
|
} else {
|
||||||
toastRef.current?.show({
|
toastRef.current?.show({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Dateien gelöscht',
|
summary: 'Dateien gelöscht',
|
||||||
detail: `RAW: ${summary.raw?.filesDeleted ?? 0}, ${outputShortLabel}: ${summary.movie?.filesDeleted ?? 0}`,
|
detail: `RAW: ${rawSummary.filesDeleted ?? 0}, ${outputShortLabel}: ${movieSummary.filesDeleted ?? 0}`,
|
||||||
life: 3500
|
life: 3500
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await load();
|
await load();
|
||||||
await refreshDetailIfOpen(row.id);
|
await refreshDetailIfOpen(row.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1617,7 +1639,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
<ul className="history-delete-preview-list">
|
<ul className="history-delete-preview-list">
|
||||||
{previewRelatedJobs.map((item) => (
|
{previewRelatedJobs.map((item) => (
|
||||||
<li key={`delete-related-${item.id}`}>
|
<li key={`delete-related-${item.id}`}>
|
||||||
<strong>#{item.id}</strong> | {item.title || '-'} | {item.status || '-'} {item.isPrimary ? '(aktuell)' : '(Alt-Eintrag)'}
|
<strong>#{item.id}</strong> | {item.title || '-'} | {getStatusLabel(item.status)} {item.isPrimary ? '(aktuell)' : '(Alt-Eintrag)'}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+90
-17
@@ -1872,7 +1872,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-item {
|
.drive-list-item {
|
||||||
padding: 0.4rem 0.6rem;
|
padding: 0.35rem 0.55rem;
|
||||||
border: 1px solid var(--rip-border);
|
border: 1px solid var(--rip-border);
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
background: var(--rip-panel-soft);
|
background: var(--rip-panel-soft);
|
||||||
@@ -1882,14 +1882,14 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-info {
|
.drive-list-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.4rem;
|
gap: 0.28rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1906,41 +1906,104 @@ body {
|
|||||||
.drive-list-model {
|
.drive-list-model {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--rip-muted);
|
color: var(--rip-muted);
|
||||||
|
max-width: 14rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-disc-arg {
|
.drive-list-disc-arg {
|
||||||
font-size: 0.7rem;
|
font-size: 0.72rem;
|
||||||
color: var(--rip-muted);
|
color: var(--rip-muted);
|
||||||
opacity: 0.7;
|
opacity: 0.85;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-state {
|
.drive-list-state {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-status-tag.p-tag {
|
.drive-list-status-icon {
|
||||||
max-width: 10rem;
|
width: 1.05rem;
|
||||||
padding: 0.15rem 0.45rem;
|
height: 1.05rem;
|
||||||
font-size: 0.68rem;
|
display: inline-flex;
|
||||||
line-height: 1.2;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--rip-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-icon.tone-success {
|
||||||
|
color: #1c8a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-icon.tone-danger {
|
||||||
|
color: #9c2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-icon.tone-warning {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-icon.tone-info {
|
||||||
|
color: #24588b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-status-icon.tone-secondary {
|
||||||
|
color: var(--rip-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-lock-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.08rem 0.32rem;
|
||||||
|
border: 1px solid rgba(155, 111, 22, 0.4);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(251, 233, 194, 0.65);
|
||||||
|
color: #8a5a0a;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
.drive-list-lock-indicator .pi {
|
||||||
|
font-size: 0.62rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drive-list-actions {
|
.drive-list-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.16rem;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drive-list-action-btn.p-button.p-button-icon-only {
|
||||||
|
width: 1.65rem;
|
||||||
|
height: 1.65rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-action-btn .p-button-icon {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drive-list-disc-meta {
|
||||||
|
margin-top: 0.08rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.drive-list-disc-label {
|
.drive-list-disc-label {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
margin-top: 0.2rem;
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -2903,7 +2966,7 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 0.15rem 0.4rem;
|
gap: 0.15rem 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2911,8 +2974,8 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-path-field-value > span {
|
.job-path-field-value > span {
|
||||||
@@ -2925,6 +2988,16 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-path-download-button.p-button.p-button-icon-only {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-path-download-button .p-button-icon {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.job-meta-col-span-2 {
|
.job-meta-col-span-2 {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const STATUS_LABELS = {
|
|||||||
METADATA_SELECTION: 'Metadatenauswahl',
|
METADATA_SELECTION: 'Metadatenauswahl',
|
||||||
WAITING_FOR_USER_DECISION: 'Warte auf Auswahl',
|
WAITING_FOR_USER_DECISION: 'Warte auf Auswahl',
|
||||||
READY_TO_START: 'Startbereit',
|
READY_TO_START: 'Startbereit',
|
||||||
MEDIAINFO_CHECK: 'Mediainfo-Pruefung',
|
MEDIAINFO_CHECK: 'Mediainfo-Prüfung',
|
||||||
READY_TO_ENCODE: 'Bereit zum Encodieren',
|
READY_TO_ENCODE: 'Bereit zum Encodieren',
|
||||||
RIPPING: 'Rippen',
|
RIPPING: 'Rippen',
|
||||||
ENCODING: 'Encodieren',
|
ENCODING: 'Encodieren',
|
||||||
@@ -16,19 +16,82 @@ const STATUS_LABELS = {
|
|||||||
CD_ANALYZING: 'CD-Analyse',
|
CD_ANALYZING: 'CD-Analyse',
|
||||||
CD_METADATA_SELECTION: 'CD-Metadatenauswahl',
|
CD_METADATA_SELECTION: 'CD-Metadatenauswahl',
|
||||||
CD_READY_TO_RIP: 'CD bereit (Rip/Encode)',
|
CD_READY_TO_RIP: 'CD bereit (Rip/Encode)',
|
||||||
CD_RIPPING: 'CD rippen',
|
CD_RIPPING: 'CD-Rippen',
|
||||||
CD_ENCODING: 'CD encodieren'
|
CD_ENCODING: 'CD-Encodieren'
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROCESS_STATUS_LABELS = {
|
const PROCESS_STATUS_LABELS = {
|
||||||
SUCCESS: 'Erfolgreich',
|
SUCCESS: 'Erfolgreich',
|
||||||
ERROR: 'Fehler',
|
ERROR: 'Fehler',
|
||||||
CANCELLED: 'Abgebrochen',
|
CANCELLED: 'Abgebrochen',
|
||||||
RUNNING: 'Laeuft',
|
RUNNING: 'Läuft',
|
||||||
STARTED: 'Gestartet',
|
STARTED: 'Gestartet',
|
||||||
PENDING: 'Ausstehend'
|
PENDING: 'Ausstehend',
|
||||||
|
DONE: 'Erledigt',
|
||||||
|
FAILED: 'Fehlgeschlagen',
|
||||||
|
COMPLETE: 'Abgeschlossen',
|
||||||
|
COMPLETED: 'Abgeschlossen',
|
||||||
|
SKIPPED: 'Übersprungen',
|
||||||
|
OK: 'OK'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FALLBACK_TOKEN_LABELS = {
|
||||||
|
IDLE: 'Bereit',
|
||||||
|
DISC: 'Medium',
|
||||||
|
DETECTED: 'erkannt',
|
||||||
|
READY: 'bereit',
|
||||||
|
TO: 'zu',
|
||||||
|
RIP: 'rippen',
|
||||||
|
RIPPING: 'rippen',
|
||||||
|
ENCODE: 'encodieren',
|
||||||
|
ENCODING: 'encodieren',
|
||||||
|
ANALYZING: 'analyse',
|
||||||
|
METADATA: 'metadaten',
|
||||||
|
SELECTION: 'auswahl',
|
||||||
|
WAITING: 'warte',
|
||||||
|
FOR: 'auf',
|
||||||
|
USER: 'Benutzer',
|
||||||
|
DECISION: 'entscheidung',
|
||||||
|
CHECK: 'prüfung',
|
||||||
|
POST: 'post',
|
||||||
|
SCRIPTS: 'skripte',
|
||||||
|
FINISHED: 'fertig',
|
||||||
|
CANCELLED: 'abgebrochen',
|
||||||
|
ERROR: 'fehler',
|
||||||
|
SUCCESS: 'erfolgreich',
|
||||||
|
RUNNING: 'läuft',
|
||||||
|
STARTED: 'gestartet',
|
||||||
|
PENDING: 'ausstehend',
|
||||||
|
FAILED: 'fehlgeschlagen',
|
||||||
|
SKIPPED: 'übersprungen',
|
||||||
|
CD: 'CD',
|
||||||
|
DVD: 'DVD',
|
||||||
|
RAW: 'RAW'
|
||||||
|
};
|
||||||
|
|
||||||
|
function prettifyUnknownStatus(status) {
|
||||||
|
const raw = String(status || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
if (!raw.includes('_')) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
const parts = raw
|
||||||
|
.split('_')
|
||||||
|
.map((part) => String(part || '').trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
const mapped = parts.map((token) => FALLBACK_TOKEN_LABELS[token] || token.toLowerCase());
|
||||||
|
const joined = mapped.join(' ').trim();
|
||||||
|
if (!joined) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return joined.charAt(0).toUpperCase() + joined.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeStatus(status) {
|
export function normalizeStatus(status) {
|
||||||
return String(status || '').trim().toUpperCase();
|
return String(status || '').trim().toUpperCase();
|
||||||
}
|
}
|
||||||
@@ -38,7 +101,7 @@ export function getStatusLabel(status, options = {}) {
|
|||||||
return 'In der Queue';
|
return 'In der Queue';
|
||||||
}
|
}
|
||||||
const normalized = normalizeStatus(status);
|
const normalized = normalizeStatus(status);
|
||||||
return STATUS_LABELS[normalized] || (String(status || '').trim() || '-');
|
return STATUS_LABELS[normalized] || prettifyUnknownStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStatusSeverity(status, options = {}) {
|
export function getStatusSeverity(status, options = {}) {
|
||||||
@@ -71,7 +134,7 @@ export function getStatusSeverity(status, options = {}) {
|
|||||||
|
|
||||||
export function getProcessStatusLabel(status) {
|
export function getProcessStatusLabel(status) {
|
||||||
const normalized = normalizeStatus(status);
|
const normalized = normalizeStatus(status);
|
||||||
return PROCESS_STATUS_LABELS[normalized] || (String(status || '').trim() || '-');
|
return PROCESS_STATUS_LABELS[normalized] || prettifyUnknownStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STATUS_FILTER_OPTIONS = [
|
export const STATUS_FILTER_OPTIONS = [
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -1,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.12.0-2",
|
"version": "0.12.0-3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
"dev:frontend": "npm run dev --prefix frontend",
|
"dev:frontend": "npm run dev --prefix frontend",
|
||||||
"start": "npm run start --prefix backend",
|
"start": "npm run start --prefix backend",
|
||||||
"build:frontend": "npm run build --prefix frontend",
|
"build:frontend": "npm run build --prefix frontend",
|
||||||
|
"qa:cd:analysis": "node ./scripts/smoketest/qa-cd-analysis.js",
|
||||||
|
"qa:dvd:analysis": "node ./scripts/smoketest/qa-dvd-analysis.js",
|
||||||
|
"qa:audiobook:analysis": "node ./scripts/smoketest/qa-audiobook-analysis.js",
|
||||||
|
"qa:cd:check": "bash ./scripts/smoketest/qa-cd-check.sh",
|
||||||
|
"qa:audiobook:check": "bash ./scripts/smoketest/qa-audiobook-check.sh",
|
||||||
|
"qa:logs:dashboard": "node ./scripts/smoketest/qa-log-dashboard.js",
|
||||||
"release:interactive": "bash ./scripts/release.sh"
|
"release:interactive": "bash ./scripts/release.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user