0.12.0-2 Checkable Infrastructure

This commit is contained in:
michael committed 2026-03-19 20:19:07 +00:00
1 parent 24955a956d
commit d9969dfbe5
20 files changed
+1207 -278

No files matched your search

+31 -14
View File
@@ -22,13 +22,16 @@ const DEFAULT_CD_OUTPUT_TEMPLATE = '{artist} - {album} ({year})/{trackNr} {artis
function parseToc(tocOutput) {
const lines = String(tocOutput || '').split(/\r?\n/);
const tracks = [];
const tocTimePattern = /[\(\[]\s*\d+:\d+(?:[.,:]\d+)?\s*[\)\]]/g;
const tocTablePattern =
/^\s*(\d+)\.?\s+(\d+)\s+[\(\[]\s*\d+:\d+(?:[.,:]\d+)?\s*[\)\]]\s+(\d+)\s+[\(\[]\s*\d+:\d+(?:[.,:]\d+)?\s*[\)\]]/i;
for (const line of lines) {
const trackMatch = line.match(/^\s*track\s+(\d+)\s*:\s*(.+)$/i);
if (trackMatch) {
const position = Number(trackMatch[1]);
const payloadWithoutTimes = String(trackMatch[2] || '')
.replace(/[\(\[]\s*\d+:\d+\.\d+\s*[\)\]]/g, ' ');
.replace(tocTimePattern, ' ');
const sectorValues = payloadWithoutTimes.match(/\d+/g) || [];
if (sectorValues.length < 2) {
continue;
@@ -58,9 +61,7 @@ function parseToc(tocOutput) {
// Alternative cdparanoia -Q table style:
// 1. 16503 [03:40.03] 0 [00:00.00] no no 2
// ^ length sectors ^ start sector
const tableMatch = line.match(
/^\s*(\d+)\.?\s+(\d+)\s+[\(\[]\d+:\d+\.\d+[\)\]]\s+(\d+)\s+[\(\[]\d+:\d+\.\d+[\)\]]/i
);
const tableMatch = line.match(tocTablePattern);
if (!tableMatch) {
continue;
}
@@ -395,6 +396,7 @@ async function runProcessTracked({
* @param {object[]} options.tracks - TOC track list [{position, durationMs, title}]
* @param {object} options.meta - album metadata {title, artist, year}
* @param {string} options.outputTemplate - template for relative output path without extension
* @param {boolean} options.skipEncode - true => rip only (no encode)
* @param {Function} options.onProgress - ({phase, trackIndex, trackTotal, percent, track}) => void
* @param {Function} options.onLog - (level, msg) => void
* @param {Function} options.onProcessHandle- called with spawned process handle for cancellation integration
@@ -419,10 +421,11 @@ async function ripAndEncode(options) {
onProcessHandle,
isCancelled,
context,
skipRip = false
skipRip = false,
skipEncode = false
} = options;
if (!SUPPORTED_FORMATS.has(format)) {
if (!skipEncode && !SUPPORTED_FORMATS.has(format)) {
throw new Error(`Unbekanntes Ausgabeformat: ${format}`);
}
@@ -435,7 +438,9 @@ async function ripAndEncode(options) {
}
await ensureDir(rawWavDir);
await ensureDir(outputDir);
if (!skipEncode) {
await ensureDir(outputDir);
}
logger.info('rip:start', {
jobId,
@@ -448,6 +453,9 @@ async function ripAndEncode(options) {
logger[level] && logger[level](msg, { jobId });
onLog && onLog(level, msg);
};
const ripPercentSpan = skipEncode ? 100 : 50;
const encodePercentStart = ripPercentSpan;
const encodePercentSpan = Math.max(0, 100 - encodePercentStart);
// ── Phase 1: Rip each selected track to WAV ──────────────────────────────
if (skipRip) {
@@ -472,7 +480,7 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 0,
percent: (i / tracksToRip.length) * 50
percent: (i / tracksToRip.length) * ripPercentSpan
});
log('info', `Rippe Track ${track.position} von ${tracksToRip.length} …`);
@@ -486,7 +494,7 @@ async function ripAndEncode(options) {
onStderrLine(line) {
const parsed = parseCdParanoiaProgress(line);
if (parsed && parsed.percent !== null) {
const overallPercent = ((i + parsed.percent / 100) / tracksToRip.length) * 50;
const overallPercent = ((i + parsed.percent / 100) / tracksToRip.length) * ripPercentSpan;
onProgress && onProgress({
phase: 'rip',
trackEvent: 'progress',
@@ -518,13 +526,22 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 100,
percent: ((i + 1) / tracksToRip.length) * 50
percent: ((i + 1) / tracksToRip.length) * ripPercentSpan
});
log('info', `Track ${track.position} gerippt.`);
}
} // end if (!skipRip)
if (skipEncode) {
return {
outputDir: null,
format: 'raw',
trackCount: tracksToRip.length,
encodeResults: []
};
}
// ── Phase 2: Encode WAVs to target format ─────────────────────────────────
const encodeResults = [];
@@ -542,7 +559,7 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 0,
percent: 50 + ((i / tracksToRip.length) * 50)
percent: encodePercentStart + ((i / tracksToRip.length) * encodePercentSpan)
});
ensureDir(path.dirname(outFile));
log('info', `Promptkette [Copy ${i + 1}/${tracksToRip.length}]: cp ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`);
@@ -554,7 +571,7 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 100,
percent: 50 + ((i + 1) / tracksToRip.length) * 50
percent: encodePercentStart + ((i + 1) / tracksToRip.length) * encodePercentSpan
});
log('info', `WAV für Track ${track.position} gespeichert.`);
encodeResults.push({ position: track.position, title: track.title || '', outputFile: path.basename(outFile), success: true });
@@ -581,7 +598,7 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 0,
percent: 50 + ((i / tracksToRip.length) * 50)
percent: encodePercentStart + ((i / tracksToRip.length) * encodePercentSpan)
});
log('info', `Encodiere Track ${track.position} → ${outFilename} …`);
@@ -628,7 +645,7 @@ async function ripAndEncode(options) {
trackTotal: tracksToRip.length,
trackPosition: track.position,
trackPercent: 100,
percent: 50 + ((i + 1) / tracksToRip.length) * 50
percent: encodePercentStart + ((i + 1) / tracksToRip.length) * encodePercentSpan
});
log('info', `Track ${track.position} encodiert.`);
+66 -9
View File
@@ -159,6 +159,11 @@ function inferMediaProfileFromFsTypeAndModel(rawFsType, rawModel) {
return null;
}
function isIsoLikeFsType(rawFsType) {
const fstype = String(rawFsType || '').trim().toLowerCase();
return fstype.includes('iso9660') || fstype.includes('cdfs');
}
function inferMediaProfileFromUdevProperties(properties = {}) {
const flags = Object.entries(properties).reduce((acc, [key, rawValue]) => {
const normalizedKey = String(key || '').trim().toUpperCase();
@@ -535,6 +540,19 @@ class DiskDetectionService extends EventEmitter {
if (!normalized) {
return { present: false, emitted: 'none', device: null };
}
if (this.isDeviceLocked(normalized)) {
const existing = this.detectedDiscs.get(normalized) || null;
logger.info('rescan-drive:skip-locked', {
devicePath: normalized,
activeLocks: this.getActiveLocks()
});
return {
present: Boolean(existing),
emitted: 'none',
device: existing,
locked: true
};
}
try {
logger.info('rescan-drive:requested', { devicePath: normalized });
const detected = await this.detectExplicit(normalized);
@@ -598,6 +616,16 @@ class DiskDetectionService extends EventEmitter {
}
}
// Preserve currently tracked locked devices that are intentionally skipped
// by detectAll* while a rip lock is active.
for (const [devicePath, device] of this.detectedDiscs) {
if (newMap.has(devicePath) || !this.isDeviceLocked(devicePath)) {
continue;
}
newMap.set(devicePath, device);
results.push({ path: devicePath, emitted: 'none', device, locked: true });
}
// Check for removed devices
for (const [devicePath, device] of this.detectedDiscs) {
if (!newMap.has(devicePath)) {
@@ -652,9 +680,15 @@ class DiskDetectionService extends EventEmitter {
if (!mediaState.hasMedia && hasSizeMedia) {
logger.debug('detect:explicit:media-by-size-fallback', { devicePath, sizeBytes: match.sizeBytes });
}
const mediaType = mediaState.type;
const mediaType = String(mediaState.type || '').trim().toLowerCase() || null;
const discLabel = await this.getDiscLabel(devicePath);
const detectedFsType = String(match.fstype || mediaType || '').trim() || null;
// Preserve explicit audio-CD detection from checkMediaPresent even if lsblk
// reports ambiguous optical fs markers like iso9660/cdfs.
const detectedFsType = String(
mediaType === 'audio_cd'
? mediaType
: (match.fstype || mediaType || '')
).trim() || null;
const mediaProfile = await this.inferMediaProfile(devicePath, {
discLabel,
@@ -702,8 +736,13 @@ class DiskDetectionService extends EventEmitter {
if (!mediaState.hasMedia) {
continue;
}
const mediaType = String(mediaState.type || '').trim().toLowerCase() || null;
const discLabel = await this.getDiscLabel(path);
const detectedFsType = String(item.fstype || mediaState.type || '').trim() || null;
const detectedFsType = String(
mediaType === 'audio_cd'
? mediaType
: (item.fstype || mediaType || '')
).trim() || null;
const mediaProfile = await this.inferMediaProfile(path, {
discLabel,
@@ -764,9 +803,13 @@ class DiskDetectionService extends EventEmitter {
if (!mediaState.hasMedia && hasSizeMedia) {
logger.debug('detect:all-auto:media-by-size-fallback', { path, sizeBytes: item.sizeBytes });
}
const mediaType = mediaState.type;
const mediaType = String(mediaState.type || '').trim().toLowerCase() || null;
const discLabel = await this.getDiscLabel(path);
const detectedFsType = String(item.fstype || mediaType || '').trim() || null;
const detectedFsType = String(
mediaType === 'audio_cd'
? mediaType
: (item.fstype || mediaType || '')
).trim() || null;
const mediaProfile = await this.inferMediaProfile(path, {
discLabel,
@@ -871,6 +914,8 @@ class DiskDetectionService extends EventEmitter {
return { hasMedia: true, type: blkidType };
}
let hasOpticalMediaHintFromUdev = false;
// blkid found nothing – audio CDs have no filesystem, so fall back to udevadm
try {
const { stdout } = await execFileAsync('udevadm', [
@@ -902,7 +947,10 @@ class DiskDetectionService extends EventEmitter {
}
if (inferredByUdev === 'bluray' || inferredByUdev === 'dvd') {
logger.debug('udevadm:optical-media', { devicePath, inferredByUdev });
return { hasMedia: true, type: null };
// Keep this as a presence hint, but still probe cdparanoia. Some drives
// expose mixed DVD/CD flags for audio CDs and would otherwise be
// downgraded to "other" before TOC probing.
hasOpticalMediaHintFromUdev = true;
}
} catch (_udevError) {
// udevadm not available or failed – ignore
@@ -919,6 +967,10 @@ class DiskDetectionService extends EventEmitter {
return { hasMedia: true, type: 'audio_cd' };
}
if (hasOpticalMediaHintFromUdev) {
return { hasMedia: true, type: null };
}
// Final fallback: check block device size via sysfs.
// In VM/passthrough environments udev metadata may be absent even though
// the kernel reports a valid disc size (visible in lsblk). A non-zero
@@ -1046,7 +1098,12 @@ class DiskDetectionService extends EventEmitter {
// Also guard: when hintFstype is empty (no filesystem info at all), the drive model
// alone is not a reliable disc-type indicator — a BD-RE drive can contain a DVD.
// In that case skip this early return and let blkid -p determine the actual disc type.
if (hintFstype && byFsTypeHint && !(hintFstype.includes('udf') && byFsTypeHint !== 'bluray')) {
if (
hintFstype
&& byFsTypeHint
&& !(hintFstype.includes('udf') && byFsTypeHint !== 'bluray')
&& !(isIsoLikeFsType(hintFstype) && byFsTypeHint === 'dvd')
) {
return byFsTypeHint;
}
@@ -1090,14 +1147,14 @@ class DiskDetectionService extends EventEmitter {
}
const byBlkidFsType = inferMediaProfileFromFsTypeAndModel(type, hints?.model);
if (byBlkidFsType) {
if (byBlkidFsType && !(isIsoLikeFsType(type) && byBlkidFsType === 'dvd')) {
return byBlkidFsType;
}
// Last resort for drives that only expose TYPE=udf without VERSION/APPLICATION_ID:
// prefer DVD over "other" so DVDs in BD-capable drives do not fall back to Misc.
const byBlkidFsTypeWithoutModel = inferMediaProfileFromFsTypeAndModel(type, null);
if (byBlkidFsTypeWithoutModel) {
if (byBlkidFsTypeWithoutModel && !(isIsoLikeFsType(type) && byBlkidFsTypeWithoutModel === 'dvd')) {
return byBlkidFsTypeWithoutModel;
}
} catch (error) {
+1
View File
@@ -4114,6 +4114,7 @@ class HistoryService {
parentJobId: normalizeJobIdValue(job.parent_job_id),
title: buildJobDisplayTitle(job),
status: String(job.status || '').trim() || null,
discDevice: String(job.disc_device || '').trim() || null,
isPrimary: Number(job.id) === normalizedJobId,
createdAt: String(job.created_at || '').trim() || null
}));
File diff suppressed because it is too large. Load diff