diff --git a/.claude/settings.json b/.claude/settings.json index d9a1b64..6fc5d73 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -15,7 +15,8 @@ "Bash(node --check /home/michael/ripster/backend/src/services/diskDetectionService.js)", "Bash(node --check /home/michael/ripster/backend/src/services/settingsService.js)", "Bash(node --check /home/michael/ripster/backend/src/services/pipelineService.js)", - "Bash(node --check /home/michael/ripster/backend/src/db/database.js)" + "Bash(node --check /home/michael/ripster/backend/src/db/database.js)", + "Bash(node --check /home/michael/ripster/backend/src/routes/settingsRoutes.js)" ] } } diff --git a/backend/package-lock.json b/backend/package-lock.json index 0803141..b8468c3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-backend", - "version": "0.11.0-3", + "version": "0.11.0-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-backend", - "version": "0.11.0-3", + "version": "0.11.0-4", "dependencies": { "archiver": "^7.0.1", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index 18d6fe3..bcdfaa8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-backend", - "version": "0.11.0-3", + "version": "0.11.0-4", "private": true, "type": "commonjs", "scripts": { diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index d4b4993..d2ca698 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -9,6 +9,7 @@ const wsService = require('../services/websocketService'); const hardwareMonitorService = require('../services/hardwareMonitorService'); const userPresetService = require('../services/userPresetService'); const activationBytesService = require('../services/activationBytesService'); +const diskDetectionService = require('../services/diskDetectionService'); const logger = require('../services/logger').child('SETTINGS_ROUTE'); const { getDb } = require('../db/database'); @@ -402,6 +403,32 @@ router.post( }) ); +// ── Optical drive scan ──────────────────────────────────────────────────────── +router.get( + '/drives', + asyncHandler(async (req, res) => { + logger.debug('get:settings:drives', { reqId: req.reqId }); + const devices = await diskDetectionService.getBlockDeviceInfo(); + const drives = devices + .filter((d) => d.type === 'rom') + .map((d) => { + const name = String(d.name || ''); + const devicePath = d.path || (name ? `/dev/${name}` : null); + const match = name.match(/sr(\d+)$/); + const discIndex = match ? Number(match[1]) : null; + return { + path: devicePath, + name, + model: d.model || null, + discIndex, + discArg: discIndex != null ? `disc:${discIndex}` : null + }; + }) + .filter((d) => d.path); + res.json({ drives }); + }) +); + // User preferences (UI state, persisted per-installation) router.get( '/prefs/:key', diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4459135..1372b71 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ripster-frontend", - "version": "0.11.0-3", + "version": "0.11.0-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ripster-frontend", - "version": "0.11.0-3", + "version": "0.11.0-4", "dependencies": { "primeicons": "^7.0.0", "primereact": "^10.9.2", diff --git a/frontend/package.json b/frontend/package.json index 203d6d7..7d826a7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ripster-frontend", - "version": "0.11.0-3", + "version": "0.11.0-4", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index dcc29b9..dd5b5f2 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -321,6 +321,9 @@ export const api = { getPendingActivation() { return request('/pipeline/audiobook/pending-activation'); }, + getDetectedDrives() { + return request('/settings/drives'); + }, getHandBrakePresets(options = {}) { return requestCachedGet('/settings/handbrake-presets', { ttlMs: 10 * 60 * 1000, diff --git a/frontend/src/components/DynamicSettingsForm.jsx b/frontend/src/components/DynamicSettingsForm.jsx index e87235b..c274796 100644 --- a/frontend/src/components/DynamicSettingsForm.jsx +++ b/frontend/src/components/DynamicSettingsForm.jsx @@ -7,6 +7,7 @@ import { InputSwitch } from 'primereact/inputswitch'; import { Dropdown } from 'primereact/dropdown'; import { Tag } from 'primereact/tag'; import { Button } from 'primereact/button'; +import { api } from '../api/client'; function normalizeText(value) { return String(value || '').trim().toLowerCase(); @@ -49,6 +50,7 @@ const PUSHOVER_ENABLED_SETTING_KEY = 'pushover_enabled'; const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode'; const ALWAYS_HIDDEN_SETTING_KEYS = new Set([ 'drive_device', + 'makemkv_source_index', // auto-derived from drive path — shown as read-only in drive info 'makemkv_rip_mode', 'makemkv_rip_mode_bluray', 'makemkv_rip_mode_dvd', @@ -58,7 +60,6 @@ const EXPERT_ONLY_SETTING_KEYS = new Set([ 'pushover_device', 'pushover_priority', 'pushover_timeout_ms', - 'makemkv_source_index', 'disc_poll_interval_ms', 'hardware_monitoring_interval_ms', 'makemkv_command', @@ -177,6 +178,64 @@ function DriveDevicesEditor({ value, onChange, settingKey }) { ); } +function DetectedDrivesInfo() { + const [drives, setDrives] = useState(null); + const [loading, setLoading] = useState(false); + + const load = () => { + setLoading(true); + api.getDetectedDrives() + .then((res) => setDrives(Array.isArray(res?.drives) ? res.drives : [])) + .catch(() => setDrives([])) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + return ( +
+
+ Erkannte optische Laufwerke +
+ {drives === null || loading + ? Wird geladen… + : drives.length === 0 + ? Keine optischen Laufwerke erkannt. + : ( + + + + + + + + + + {drives.map((d) => ( + + + + + + ))} + +
GerätModellMakeMKV Index
{d.path}{d.model || '—'}
+ )} +
+ ); +} + function shouldHideSettingByExpertMode(settingKey, expertModeEnabled) { const key = normalizeSettingKey(settingKey); if (!key) { @@ -688,8 +747,10 @@ export default function DynamicSettingsForm({ const isNotificationCategory = normalizeText(category?.category) === 'benachrichtigungen'; const pushoverEnabled = toBoolean(values?.[PUSHOVER_ENABLED_SETTING_KEY]); + const isDriveCategory = normalizeText(category?.category) === 'laufwerk'; return (
+ {isDriveCategory && } {sections.map((section) => (