0.12.0-17 Misc fixes
This commit is contained in:
@@ -48,6 +48,7 @@ const NOTIFICATION_EVENT_TOGGLE_KEYS = new Set([
|
||||
|
||||
const PUSHOVER_ENABLED_SETTING_KEY = 'pushover_enabled';
|
||||
const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode';
|
||||
const EXTERNAL_STORAGE_PATHS_KEY = 'external_storage_paths';
|
||||
const ALWAYS_HIDDEN_SETTING_KEYS = new Set([
|
||||
'drive_device',
|
||||
'makemkv_source_index', // auto-derived from drive path — shown as read-only in drive info
|
||||
@@ -201,6 +202,96 @@ function DriveDevicesEditor({ value, onChange, settingKey }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ExternalStoragePathsEditor({ value, onChange, settingKey }) {
|
||||
const parsePaths = (rawValue) => {
|
||||
try {
|
||||
const parsed = JSON.parse(rawValue || '[]');
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
const normalized = [];
|
||||
for (const entry of parsed) {
|
||||
if (typeof entry === 'string') {
|
||||
normalized.push({
|
||||
name: '',
|
||||
path: String(entry || '')
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (entry && typeof entry === 'object') {
|
||||
normalized.push({
|
||||
name: String(entry.name || entry.label || ''),
|
||||
path: String(entry.path || '')
|
||||
});
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const entries = parsePaths(value);
|
||||
|
||||
const updatePaths = (nextPaths) => {
|
||||
const normalized = (Array.isArray(nextPaths) ? nextPaths : [])
|
||||
.map((entry) => ({
|
||||
name: String(entry?.name || ''),
|
||||
path: String(entry?.path || '')
|
||||
}));
|
||||
onChange?.(settingKey, JSON.stringify(normalized));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="external-storage-editor">
|
||||
{entries.length === 0 && (
|
||||
<p className="external-storage-empty">Keine externen Speicherpfade konfiguriert.</p>
|
||||
)}
|
||||
{entries.map((entry, idx) => (
|
||||
<div key={`external-storage-${idx}`} className="external-storage-row">
|
||||
<InputText
|
||||
value={entry.name}
|
||||
placeholder="Name (z.B. USB HDD)"
|
||||
onChange={(event) => {
|
||||
const next = [...entries];
|
||||
next[idx] = { ...next[idx], name: event.target.value };
|
||||
updatePaths(next);
|
||||
}}
|
||||
className="external-storage-name-input"
|
||||
/>
|
||||
<InputText
|
||||
value={entry.path}
|
||||
placeholder="/mnt/external-disk"
|
||||
onChange={(event) => {
|
||||
const next = [...entries];
|
||||
next[idx] = { ...next[idx], path: event.target.value };
|
||||
updatePaths(next);
|
||||
}}
|
||||
className="external-storage-input"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
onClick={() => updatePaths(entries.filter((_, entryIndex) => entryIndex !== idx))}
|
||||
type="button"
|
||||
aria-label="Externen Speicherpfad entfernen"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
icon="pi pi-plus"
|
||||
label="Pfad hinzufügen"
|
||||
severity="secondary"
|
||||
text
|
||||
size="small"
|
||||
onClick={() => updatePaths([...entries, { name: '', path: '' }])}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetectedDrivesInfo() {
|
||||
const [drives, setDrives] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -351,6 +442,7 @@ const CD_PATH_KEYS = ['raw_dir_cd', 'movie_dir_cd', 'cd_output_template'];
|
||||
const AUDIOBOOK_PATH_KEYS = ['raw_dir_audiobook', 'movie_dir_audiobook', 'output_template_audiobook', 'output_chapter_template_audiobook', 'audiobook_raw_template'];
|
||||
const DOWNLOAD_PATH_KEYS = ['download_dir'];
|
||||
const LOG_PATH_KEYS = ['log_dir'];
|
||||
const EXTERNAL_STORAGE_PATH_KEYS = [EXTERNAL_STORAGE_PATHS_KEY];
|
||||
const CONVERTER_PATH_KEYS = ['converter_raw_dir', 'converter_movie_dir', 'converter_audio_dir', 'converter_output_template_video', 'converter_output_template_audio'];
|
||||
|
||||
function buildSectionsForCategory(categoryName, settings) {
|
||||
@@ -432,7 +524,9 @@ function SettingField({
|
||||
</label>
|
||||
)}
|
||||
|
||||
{(setting.type === 'string' || setting.type === 'path') && setting.key !== 'drive_devices' ? (
|
||||
{(setting.type === 'string' || setting.type === 'path')
|
||||
&& setting.key !== 'drive_devices'
|
||||
&& setting.key !== EXTERNAL_STORAGE_PATHS_KEY ? (
|
||||
<InputText
|
||||
id={setting.key}
|
||||
value={value ?? ''}
|
||||
@@ -448,6 +542,14 @@ function SettingField({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{setting.key === EXTERNAL_STORAGE_PATHS_KEY ? (
|
||||
<ExternalStoragePathsEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
settingKey={setting.key}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{setting.type === 'number' ? (
|
||||
<InputNumber
|
||||
id={setting.key}
|
||||
@@ -540,6 +642,7 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
|
||||
const audiobookSettings = list.filter((s) => AUDIOBOOK_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && AUDIOBOOK_PATH_KEYS.includes(s.key.replace('_owner', ''))));
|
||||
const downloadSettings = list.filter((s) => DOWNLOAD_PATH_KEYS.includes(s.key) || (s.key.endsWith('_owner') && DOWNLOAD_PATH_KEYS.includes(s.key.replace('_owner', ''))));
|
||||
const logSettings = list.filter((s) => LOG_PATH_KEYS.includes(s.key));
|
||||
const externalStorageSettings = list.filter((s) => EXTERNAL_STORAGE_PATH_KEYS.includes(s.key));
|
||||
const converterSettings = list.filter((s) => CONVERTER_PATH_KEYS.includes(s.key));
|
||||
|
||||
const defaultRaw = effectivePaths?.defaults?.raw || 'data/output/raw';
|
||||
@@ -675,6 +778,7 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
|
||||
{ title: 'Audiobook', pathSettings: audiobookSettings },
|
||||
{ title: 'Converter', pathSettings: converterSettings },
|
||||
{ title: 'Downloads', pathSettings: downloadSettings },
|
||||
...(externalStorageSettings.length > 0 ? [{ title: 'Externe Speicher', pathSettings: externalStorageSettings }] : []),
|
||||
...(logSettings.length > 0 ? [{ title: 'Logs', pathSettings: logSettings }] : [])
|
||||
]
|
||||
.filter(({ pathSettings }) =>
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function MetadataSelectionDialog({
|
||||
const [manualYear, setManualYear] = useState('');
|
||||
const [manualImdb, setManualImdb] = useState('');
|
||||
const [extraResults, setExtraResults] = useState([]);
|
||||
const [searchBusy, setSearchBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
@@ -36,6 +37,7 @@ export default function MetadataSelectionDialog({
|
||||
setManualYear(defaultYear);
|
||||
setManualImdb(defaultImdb);
|
||||
setExtraResults([]);
|
||||
setSearchBusy(false);
|
||||
}, [visible, context]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
@@ -67,11 +69,17 @@ export default function MetadataSelectionDialog({
|
||||
);
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!query.trim()) {
|
||||
const trimmedQuery = String(query || '').trim();
|
||||
if (!trimmedQuery || typeof onSearch !== 'function') {
|
||||
return;
|
||||
}
|
||||
const results = await onSearch(query.trim());
|
||||
setExtraResults(results || []);
|
||||
setSearchBusy(true);
|
||||
try {
|
||||
const results = await onSearch(trimmedQuery);
|
||||
setExtraResults(Array.isArray(results) ? results : []);
|
||||
} finally {
|
||||
setSearchBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -110,9 +118,15 @@ export default function MetadataSelectionDialog({
|
||||
<InputText
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void handleSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Titel suchen"
|
||||
/>
|
||||
<Button label="OMDb Suche" icon="pi pi-search" onClick={handleSearch} loading={busy} />
|
||||
<Button label="OMDb Suche" icon="pi pi-search" onClick={handleSearch} loading={searchBusy} disabled={searchBusy || busy} />
|
||||
</div>
|
||||
|
||||
<div className="table-scroll-wrap table-scroll-medium">
|
||||
|
||||
@@ -59,6 +59,10 @@ function normalizeJobId(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeDrivePath(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function formatPercent(value, digits = 1) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
@@ -1508,8 +1512,17 @@ export default function DashboardPage({
|
||||
};
|
||||
|
||||
const handleAnalyzeAll = async () => {
|
||||
const lockedPaths = new Set(
|
||||
(Array.isArray(pipeline?.driveLocks) ? pipeline.driveLocks : [])
|
||||
.filter((lock) => Number(lock?.count || 0) > 0)
|
||||
.map((lock) => normalizeDrivePath(lock?.path))
|
||||
.filter(Boolean)
|
||||
);
|
||||
const drivesToAnalyze = allDrives
|
||||
.filter((drv) => {
|
||||
if (lockedPaths.has(normalizeDrivePath(drv.path))) {
|
||||
return false;
|
||||
}
|
||||
const cdState = drv.cdDrive ? String(drv.cdDrive.state || '').toUpperCase() : null;
|
||||
if (cdState) return cdState === 'DISC_DETECTED';
|
||||
if (Boolean(drv.pipelineDevice) && String(drv.pipelineState || '').toUpperCase() === 'DISC_DETECTED') return true;
|
||||
@@ -2299,6 +2312,26 @@ export default function DashboardPage({
|
||||
const lastNonCdDiscEvent = lastDiscEvent?.mediaProfile !== 'cd' ? lastDiscEvent : null;
|
||||
const contextDevice = pipeline?.context?.device;
|
||||
const device = lastNonCdDiscEvent || (contextDevice?.mediaProfile !== 'cd' ? contextDevice : null);
|
||||
const driveLocksByPath = useMemo(() => {
|
||||
const map = new Map();
|
||||
const locks = Array.isArray(pipeline?.driveLocks) ? pipeline.driveLocks : [];
|
||||
for (const lock of locks) {
|
||||
const path = normalizeDrivePath(lock?.path);
|
||||
const count = Number(lock?.count || 0);
|
||||
if (!path || !Number.isFinite(count) || count <= 0) {
|
||||
continue;
|
||||
}
|
||||
const owners = Array.isArray(lock?.owners) ? lock.owners : [];
|
||||
const owner = owners.length > 0 ? owners[owners.length - 1] : null;
|
||||
map.set(path, {
|
||||
path,
|
||||
count,
|
||||
owner,
|
||||
owners
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [pipeline?.driveLocks]);
|
||||
|
||||
// Unified list of all known drives: merge lsblk drives + CD state + non-CD pipeline state
|
||||
const allDrives = useMemo(() => {
|
||||
@@ -2621,7 +2654,14 @@ export default function DashboardPage({
|
||||
const driveCtx = cdDrive?.context && typeof cdDrive.context === 'object' ? cdDrive.context : {};
|
||||
const cdState = cdDrive ? String(cdDrive.state || '').toUpperCase() : null;
|
||||
const pipelineState = String(drv.pipelineState || '').toUpperCase();
|
||||
const driveLockEntry = driveLocksByPath.get(normalizeDrivePath(drivePath)) || null;
|
||||
const isCdDriveLocked = cdState ? activeCdDriveStates.includes(cdState) : false;
|
||||
const isDriveLocked = Boolean(driveLockEntry) || isCdDriveLocked;
|
||||
const lockedByJobId = normalizeJobId(driveLockEntry?.owner?.jobId);
|
||||
const lockStage = String(driveLockEntry?.owner?.stage || '').trim().toUpperCase();
|
||||
const lockTitle = lockedByJobId
|
||||
? `Laufwerk ist gesperrt (Job #${lockedByJobId}${lockStage ? `, ${lockStage}` : ''}).`
|
||||
: 'Laufwerk ist gesperrt, bis Rip/Encode abgeschlossen ist.';
|
||||
const canAnalyzeDrive = cdState
|
||||
? cdState === 'DISC_DETECTED'
|
||||
: (Boolean(pipelineDevice) && pipelineState === 'DISC_DETECTED') || Boolean(drv.detectedDisc);
|
||||
@@ -2645,45 +2685,51 @@ export default function DashboardPage({
|
||||
<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 className="drive-list-actions">
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isRescanBusy}
|
||||
disabled={isRescanBusy || isDriveActive || isCdDriveLocked}
|
||||
onClick={() => handleRescanDrive(drivePath)}
|
||||
title="Laufwerk neu lesen"
|
||||
aria-label="Laufwerk neu lesen"
|
||||
/>
|
||||
{canAnalyzeDrive && (
|
||||
{isDriveLocked ? (
|
||||
<Button
|
||||
icon="pi pi-search"
|
||||
icon="pi pi-lock"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isAnalyzeBusy}
|
||||
disabled={isAnalyzeBusy}
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
title="Disk analysieren"
|
||||
aria-label="Disk analysieren"
|
||||
disabled
|
||||
title={lockTitle}
|
||||
aria-label="Laufwerk gesperrt"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isRescanBusy}
|
||||
disabled={isRescanBusy || isDriveActive}
|
||||
onClick={() => handleRescanDrive(drivePath)}
|
||||
title="Laufwerk neu lesen"
|
||||
aria-label="Laufwerk neu lesen"
|
||||
/>
|
||||
{canAnalyzeDrive && (
|
||||
<Button
|
||||
icon="pi pi-search"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isAnalyzeBusy}
|
||||
disabled={isAnalyzeBusy}
|
||||
onClick={() => handleAnalyzeForDrive(drivePath)}
|
||||
title="Disk analysieren"
|
||||
aria-label="Disk analysieren"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{cdState === 'CD_METADATA_SELECTION' && (
|
||||
<Button
|
||||
|
||||
@@ -1856,6 +1856,35 @@ body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.external-storage-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.external-storage-empty {
|
||||
color: var(--rip-muted);
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.external-storage-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.external-storage-name-input {
|
||||
width: 14rem;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.external-storage-input {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Per-drive CD sections in Dashboard */
|
||||
/* Unified per-drive list (Disk-Information card) */
|
||||
.drive-list {
|
||||
|
||||
Reference in New Issue
Block a user