diff --git a/backend/package-lock.json b/backend/package-lock.json index 1953e65..26e3a3c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.11.0", + "version": "0.11.0-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.11.0", + "version": "0.11.0-1", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 051086d..580a039 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.11.0", + "version": "0.11.0-1", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/db/database.js b/backend/src/db/database.js index 2b347af..3489651 100644 --- a/backend/src/db/database.js +++ b/backend/src/db/database.js @@ -964,7 +964,7 @@ async function migrateSettingsSchemaMetadata(db) { await db.run( `INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index) - VALUES ('drive_devices', 'Laufwerk', 'Explizite Laufwerke', 'path', 0, 'Laufwerkspfade für den expliziten Modus als JSON-Array, z.B. [\"/dev/sr0\",\"/dev/sr1\"]. Nur relevant wenn Modus = Explizites Device.', '[]', '[]', '{}', 15)` + VALUES ('drive_devices', 'Laufwerk', 'Explizite Laufwerke', 'path', 0, 'Laufwerke für den expliziten Modus als JSON-Array mit Pfad und MakeMKV-Index, z.B. [{\"path\":\"/dev/sr0\",\"makemkvIndex\":0},{\"path\":\"/dev/sr1\",\"makemkvIndex\":1}]. Nur relevant wenn Modus = Explizites Device.', '[]', '[]', '{}', 15)` ); await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('drive_devices', '[]')`); diff --git a/backend/src/services/diskDetectionService.js b/backend/src/services/diskDetectionService.js index 00fda4b..0ee1319 100644 --- a/backend/src/services/diskDetectionService.js +++ b/backend/src/services/diskDetectionService.js @@ -447,7 +447,9 @@ class DiskDetectionService extends EventEmitter { try { const parsed = JSON.parse(settingsMap.drive_devices || '[]'); if (Array.isArray(parsed)) { - explicitPaths = parsed.map((p) => String(p || '').trim()).filter(Boolean); + explicitPaths = parsed + .map((e) => (typeof e === 'string' ? e.trim() : String(e?.path || '').trim())) + .filter(Boolean); } } catch (_error) { // malformed JSON — ignore, fall through to legacy diff --git a/backend/src/services/pipelineService.js b/backend/src/services/pipelineService.js index a6cc60c..b0b9075 100644 --- a/backend/src/services/pipelineService.js +++ b/backend/src/services/pipelineService.js @@ -5158,9 +5158,11 @@ class PipelineService extends EventEmitter { const isNonJob = candidate.type && candidate.type !== 'job'; if (isNonJob) { - // Non-job entries (script, chain, wait) always start immediately - entryIndex = i; - break; + // Non-job entries (script, chain, wait) only start when no jobs are running + if (totalRunning === 0) { + entryIndex = i; + } + break; // FIFO: stop scanning regardless (non-job blocks everything behind it) } // Job entry: check hierarchical limits @@ -5311,7 +5313,10 @@ class PipelineService extends EventEmitter { try { const parsed = JSON.parse(settingsMap?.drive_devices || '[]'); if (Array.isArray(parsed)) { - parsed.map((p) => String(p || '').trim()).filter(Boolean).forEach((p) => devicesToEject.push(p)); + parsed + .map((e) => (typeof e === 'string' ? e.trim() : String(e?.path || '').trim())) + .filter(Boolean) + .forEach((p) => devicesToEject.push(p)); } } catch (_error) { // ignore @@ -13488,7 +13493,7 @@ class PipelineService extends EventEmitter { title: 'Ripster - CD Rip erfolgreich', message: `Job #${jobId}: ${selectedMeta?.title || 'Audio CD'}` }); - void this.ejectDriveIfEnabled(settings); + void settingsService.getSettingsMap().then((cdSettings) => this.ejectDriveIfEnabled(cdSettings, devicePath)); } catch (error) { settleLifecycle(); const failedCdLive = buildLiveContext(currentTrackPosition || null); diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index 6632e17..4b33697 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -572,6 +572,45 @@ function mapPresetEntriesToOptions(entries) { return options; } +/** + * 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 + * [{"path": "/dev/sr0", "makemkvIndex": 0}]. + * When makemkvIndex is missing, it is auto-derived from the device name (sr0→0, sr1→1). + */ +function parseDriveDeviceEntries(raw) { + try { + const arr = JSON.parse(raw || '[]'); + if (!Array.isArray(arr)) { + return []; + } + return arr.map((entry) => { + if (typeof entry === 'string') { + const p = entry.trim(); + if (!p) { + return null; + } + const m = p.match(/sr(\d+)$/); + return { path: p, makemkvIndex: m ? Number(m[1]) : 0 }; + } + if (entry && typeof entry === 'object' && entry.path) { + const p = String(entry.path || '').trim(); + if (!p) { + return null; + } + const idxRaw = entry.makemkvIndex != null ? Number(entry.makemkvIndex) : null; + const m = p.match(/sr(\d+)$/); + const derived = m ? Number(m[1]) : 0; + const idx = Number.isFinite(idxRaw) && idxRaw >= 0 ? Math.trunc(idxRaw) : derived; + return { path: p, makemkvIndex: idx }; + } + return null; + }).filter(Boolean); + } catch (_error) { + return []; + } +} + class SettingsService { constructor() { this.settingsSnapshotCache = { @@ -1192,21 +1231,12 @@ class SettingsService { resolveHandBrakeSourceArg(map, deviceInfo = null) { if (map.drive_mode === 'explicit') { - // Prefer drive_devices array, fall back to legacy drive_device - let explicitPaths = []; - try { - const parsed = JSON.parse(map.drive_devices || '[]'); - if (Array.isArray(parsed)) { - explicitPaths = parsed.map((p) => String(p || '').trim()).filter(Boolean); - } - } catch (_error) { - // ignore parse error - } - const firstExplicit = explicitPaths[0] || String(map.drive_device || '').trim(); - if (!firstExplicit) { + const entries = parseDriveDeviceEntries(map.drive_devices); + const firstPath = entries[0]?.path || String(map.drive_device || '').trim(); + if (!firstPath) { throw new Error('Kein Laufwerk konfiguriert, obwohl drive_mode=explicit gesetzt ist.'); } - return firstExplicit; + return firstPath; } const detectedPath = String(deviceInfo?.path || '').trim(); @@ -1214,15 +1244,10 @@ class SettingsService { return detectedPath; } - // Fallback: first explicit device, or legacy drive_device - try { - const parsed = JSON.parse(map.drive_devices || '[]'); - if (Array.isArray(parsed) && parsed.length > 0) { - const first = String(parsed[0] || '').trim(); - if (first) return first; - } - } catch (_error) { - // ignore + // Fallback: first configured device or legacy drive_device + const entries = parseDriveDeviceEntries(map.drive_devices); + if (entries.length > 0) { + return entries[0].path; } const configuredPath = String(map.drive_device || '').trim(); if (configuredPath) { @@ -1385,10 +1410,28 @@ class SettingsService { } resolveSourceArg(map, deviceInfo = null) { - // Single-drive setup: always use MakeMKV's first logical disc device. - void map; - void deviceInfo; - return 'disc:0'; + const devicePath = String(deviceInfo?.path || '').trim(); + + // In explicit mode: look up per-drive makemkvIndex from drive_devices + if (devicePath && map.drive_mode === 'explicit') { + const entries = parseDriveDeviceEntries(map.drive_devices); + const entry = entries.find((e) => e.path === devicePath); + if (entry) { + return `disc:${entry.makemkvIndex}`; + } + } + + // Auto-derive from device name (/dev/sr0 → disc:0, /dev/sr1 → disc:1) + if (devicePath) { + const match = devicePath.match(/sr(\d+)$/); + if (match) { + return `disc:${match[1]}`; + } + } + + // Fall back to global makemkv_source_index setting + const sourceIndex = Number(map.makemkv_source_index ?? 0); + return `disc:${Number.isFinite(sourceIndex) && sourceIndex >= 0 ? Math.trunc(sourceIndex) : 0}`; } async loadHandBrakePresetOptionsFromCli(map = {}) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0490d2..e391137 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.11.0", + "version": "0.11.0-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.11.0", + "version": "0.11.0-1", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 6c43ea4..4ca4ca7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.11.0", + "version": "0.11.0-1", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/components/DynamicSettingsForm.jsx b/frontend/src/components/DynamicSettingsForm.jsx index 0784c76..e87235b 100644 --- a/frontend/src/components/DynamicSettingsForm.jsx +++ b/frontend/src/components/DynamicSettingsForm.jsx @@ -92,7 +92,18 @@ function DriveDevicesEditor({ value, onChange, settingKey }) { const parseDevices = (val) => { try { const parsed = JSON.parse(val || '[]'); - return Array.isArray(parsed) ? parsed.map(String) : []; + if (!Array.isArray(parsed)) return []; + return parsed.map((e) => { + if (typeof e === 'string') { + const p = e.trim(); + const m = p.match(/sr(\d+)$/); + return { path: p, makemkvIndex: m ? Number(m[1]) : 0 }; + } + if (e && typeof e === 'object') { + return { path: String(e.path || '').trim(), makemkvIndex: Number.isFinite(Number(e.makemkvIndex)) ? Number(e.makemkvIndex) : 0 }; + } + return { path: '', makemkvIndex: 0 }; + }); } catch (_e) { return []; } @@ -109,18 +120,35 @@ function DriveDevicesEditor({ value, onChange, settingKey }) { {devices.length === 0 && (

Keine expliziten Laufwerke konfiguriert.

)} - {devices.map((path, idx) => ( + {devices.map((dev, idx) => (
{ const next = [...devices]; - next[idx] = e.target.value; + const p = e.target.value; + const m = p.match(/sr(\d+)$/); + next[idx] = { ...next[idx], path: p, makemkvIndex: m ? Number(m[1]) : next[idx].makemkvIndex }; updateDevices(next); }} className="drive-device-input" /> + { + const next = [...devices]; + next[idx] = { ...next[idx], makemkvIndex: e.value ?? 0 }; + updateDevices(next); + }} + />
diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 7d1bbac..9685c6a 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -2266,18 +2266,26 @@ export default function DashboardPage({ : driveState === 'ERROR' || driveState === 'CANCELLED' ? 'danger' : ['CD_RIPPING', 'CD_ENCODING'].includes(driveState) ? 'warning' : 'info'; + const driveProgress = Number(drive?.progress ?? 0); + const driveStatusText = drive?.statusText || null; + const isActiveRip = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(driveState); return (
- {driveDevice.model || 'CD-Laufwerk'} {drivePath}
- {driveDevice.discLabel && ( + {(driveDevice.discLabel || driveDevice.model) && (
- Disc: {driveDevice.discLabel} + {driveDevice.discLabel || driveDevice.model}
)} + {isActiveRip && ( + + )} + {isActiveRip && driveStatusText && ( +
{driveStatusText}
+ )}
{driveState === 'DISC_DETECTED' && (