merge
This commit is contained in:
@@ -857,7 +857,7 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rawDirCdLabel = 'CD RAW-Ordner';
|
const rawDirCdLabel = 'CD RAW-Ordner';
|
||||||
const rawDirCdDescription = 'Basisordner für CD-Rips. Enthält die WAV-Rohdaten (RAW) sowie den encodierten Audio-Output. Leer = Standardpfad (data/output/cd).';
|
const rawDirCdDescription = 'Basisordner für rohe CD-WAV-Dateien (cdparanoia-Output). Leer = Standardpfad (data/output/cd).';
|
||||||
const rawDirCdResult = await db.run(
|
const rawDirCdResult = await db.run(
|
||||||
`UPDATE settings_schema
|
`UPDATE settings_schema
|
||||||
SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
@@ -870,6 +870,26 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
label: rawDirCdLabel
|
label: rawDirCdLabel
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate raw_dir_cd_owner label
|
||||||
|
await db.run(
|
||||||
|
`UPDATE settings_schema SET label = 'Eigentümer CD RAW-Ordner', updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE key = 'raw_dir_cd_owner' AND label != 'Eigentümer CD RAW-Ordner'`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add movie_dir_cd if not already present
|
||||||
|
await db.run(
|
||||||
|
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
|
VALUES ('movie_dir_cd', 'Pfade', 'CD Output-Ordner', 'path', 0, 'Zielordner für encodierte CD-Ausgaben (FLAC, MP3 usw.). Leer = gleicher Ordner wie CD RAW-Ordner.', NULL, '[]', '{}', 114)`
|
||||||
|
);
|
||||||
|
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_cd', NULL)`);
|
||||||
|
|
||||||
|
// Add movie_dir_cd_owner if not already present
|
||||||
|
await db.run(
|
||||||
|
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
|
VALUES ('movie_dir_cd_owner', 'Pfade', 'Eigentümer CD Output-Ordner', 'string', 0, 'Eigentümer der encodierten CD-Ausgaben im Format user:gruppe. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1145)`
|
||||||
|
);
|
||||||
|
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_cd_owner', NULL)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDb() {
|
async function getDb() {
|
||||||
|
|||||||
@@ -161,6 +161,41 @@ function hasBlurayStructure(rawPath) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCdStructure(rawPath) {
|
||||||
|
const basePath = String(rawPath || '').trim();
|
||||||
|
if (!basePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(basePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stat = fs.statSync(basePath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const entries = fs.readdirSync(basePath);
|
||||||
|
const audioExtensions = new Set(['.flac', '.wav', '.mp3', '.opus', '.ogg', '.aiff', '.aif']);
|
||||||
|
return entries.some((entry) => audioExtensions.has(path.extname(entry).toLowerCase()));
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectOrphanMediaType(rawPath) {
|
||||||
|
if (hasBlurayStructure(rawPath)) {
|
||||||
|
return 'bluray';
|
||||||
|
}
|
||||||
|
if (hasDvdStructure(rawPath)) {
|
||||||
|
return 'dvd';
|
||||||
|
}
|
||||||
|
if (hasCdStructure(rawPath)) {
|
||||||
|
return 'cd';
|
||||||
|
}
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
|
||||||
function hasDvdStructure(rawPath) {
|
function hasDvdStructure(rawPath) {
|
||||||
const basePath = String(rawPath || '').trim();
|
const basePath = String(rawPath || '').trim();
|
||||||
if (!basePath) {
|
if (!basePath) {
|
||||||
@@ -439,17 +474,16 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed =
|
|||||||
const effectiveSettings = settingsService.resolveEffectiveToolSettings(settings || {}, mediaType);
|
const effectiveSettings = settingsService.resolveEffectiveToolSettings(settings || {}, mediaType);
|
||||||
const rawDir = String(effectiveSettings?.raw_dir || '').trim();
|
const rawDir = String(effectiveSettings?.raw_dir || '').trim();
|
||||||
const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim();
|
const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim();
|
||||||
const movieDir = mediaType === 'cd' ? rawDir : configuredMovieDir;
|
const movieDir = configuredMovieDir || rawDir;
|
||||||
const rawLookupDirs = getConfiguredMediaPathList(settings || {}, 'raw_dir')
|
const rawLookupDirs = getConfiguredMediaPathList(settings || {}, 'raw_dir')
|
||||||
.filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir));
|
.filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir));
|
||||||
const effectiveRawPath = job?.raw_path
|
const effectiveRawPath = job?.raw_path
|
||||||
? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs)
|
? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs)
|
||||||
: (job?.raw_path || null);
|
: (job?.raw_path || null);
|
||||||
const effectiveOutputPath = mediaType === 'cd'
|
// For CD, output_path is a directory (album folder) — skip path-relocation heuristic
|
||||||
? (job?.output_path || null)
|
const effectiveOutputPath = (mediaType !== 'cd' && configuredMovieDir && job?.output_path)
|
||||||
: (configuredMovieDir && job?.output_path
|
? resolveEffectiveOutputPath(job.output_path, configuredMovieDir)
|
||||||
? resolveEffectiveOutputPath(job.output_path, configuredMovieDir)
|
: (job?.output_path || null);
|
||||||
: (job?.output_path || null));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaType,
|
mediaType,
|
||||||
@@ -1398,6 +1432,7 @@ class HistoryService {
|
|||||||
|
|
||||||
const stat = fs.statSync(rawPath);
|
const stat = fs.statSync(rawPath);
|
||||||
const metadata = parseRawFolderMetadata(entry.name);
|
const metadata = parseRawFolderMetadata(entry.name);
|
||||||
|
const detectedMediaType = detectOrphanMediaType(rawPath);
|
||||||
orphanRows.push({
|
orphanRows.push({
|
||||||
rawPath,
|
rawPath,
|
||||||
folderName: entry.name,
|
folderName: entry.name,
|
||||||
@@ -1406,7 +1441,10 @@ class HistoryService {
|
|||||||
imdbId: metadata.imdbId,
|
imdbId: metadata.imdbId,
|
||||||
folderJobId: metadata.folderJobId,
|
folderJobId: metadata.folderJobId,
|
||||||
entryCount: Number(dirInfo.entryCount || 0),
|
entryCount: Number(dirInfo.entryCount || 0),
|
||||||
hasBlurayStructure: fs.existsSync(path.join(rawPath, 'BDMV', 'STREAM')),
|
detectedMediaType,
|
||||||
|
hasBlurayStructure: detectedMediaType === 'bluray',
|
||||||
|
hasDvdStructure: detectedMediaType === 'dvd',
|
||||||
|
hasCdStructure: detectedMediaType === 'cd',
|
||||||
lastModifiedAt: stat.mtime.toISOString()
|
lastModifiedAt: stat.mtime.toISOString()
|
||||||
});
|
});
|
||||||
seenOrphanPaths.add(normalizedPath);
|
seenOrphanPaths.add(normalizedPath);
|
||||||
@@ -1543,6 +1581,7 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detectedMediaType = detectOrphanMediaType(finalRawPath);
|
||||||
const orphanPosterUrl = omdbById?.poster || null;
|
const orphanPosterUrl = omdbById?.poster || null;
|
||||||
await this.updateJob(created.id, {
|
await this.updateJob(created.id, {
|
||||||
status: 'FINISHED',
|
status: 'FINISHED',
|
||||||
@@ -1567,7 +1606,8 @@ class HistoryService {
|
|||||||
status: 'SUCCESS',
|
status: 'SUCCESS',
|
||||||
source: 'orphan_raw_import',
|
source: 'orphan_raw_import',
|
||||||
importedAt,
|
importedAt,
|
||||||
rawPath: finalRawPath
|
rawPath: finalRawPath,
|
||||||
|
mediaProfile: detectedMediaType
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1585,8 +1625,8 @@ class HistoryService {
|
|||||||
created.id,
|
created.id,
|
||||||
'SYSTEM',
|
'SYSTEM',
|
||||||
renameSteps.length > 0
|
renameSteps.length > 0
|
||||||
? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
|
? `Historieneintrag aus RAW erstellt (Medientyp: ${detectedMediaType}). Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
|
||||||
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath}`
|
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath} (Medientyp: ${detectedMediaType})`
|
||||||
);
|
);
|
||||||
if (metadata.imdbId) {
|
if (metadata.imdbId) {
|
||||||
await this.appendLog(
|
await this.appendLog(
|
||||||
@@ -1600,7 +1640,8 @@ class HistoryService {
|
|||||||
|
|
||||||
logger.info('job:import-orphan-raw', {
|
logger.info('job:import-orphan-raw', {
|
||||||
jobId: created.id,
|
jobId: created.id,
|
||||||
rawPath: absRawPath
|
rawPath: absRawPath,
|
||||||
|
detectedMediaType
|
||||||
});
|
});
|
||||||
|
|
||||||
const imported = await this.getJobById(created.id);
|
const imported = await this.getJobById(created.id);
|
||||||
|
|||||||
@@ -10377,10 +10377,10 @@ 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 {
|
||||||
await historyService.closeProcessLog(jobId);
|
|
||||||
this.activeProcesses.delete(Number(normalizedJobId));
|
this.activeProcesses.delete(Number(normalizedJobId));
|
||||||
this.syncPrimaryActiveProcess();
|
|
||||||
this.cancelRequestedByJob.delete(Number(normalizedJobId));
|
this.cancelRequestedByJob.delete(Number(normalizedJobId));
|
||||||
|
this.syncPrimaryActiveProcess();
|
||||||
|
await historyService.closeProcessLog(jobId);
|
||||||
await this.emitQueueChanged();
|
await this.emitQueueChanged();
|
||||||
void this.pumpQueue();
|
void this.pumpQueue();
|
||||||
}
|
}
|
||||||
@@ -10430,16 +10430,16 @@ class PipelineService extends EventEmitter {
|
|||||||
hasRawPath = false;
|
hasRawPath = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedStage === 'ENCODING' && hasConfirmedPlan) {
|
if (normalizedStage === 'ENCODING' && hasConfirmedPlan && !isCancelled) {
|
||||||
try {
|
try {
|
||||||
await historyService.appendLog(
|
await historyService.appendLog(
|
||||||
jobId,
|
jobId,
|
||||||
'SYSTEM',
|
'SYSTEM',
|
||||||
`${isCancelled ? 'Abbruch' : 'Fehler'} in ${stage}: ${message}. Letzte Encode-Auswahl wird zur direkten Anpassung geladen.`
|
`Fehler in ${stage}: ${message}. Letzte Encode-Auswahl wird zur direkten Anpassung geladen.`
|
||||||
);
|
);
|
||||||
await this.restartEncodeWithLastSettings(jobId, {
|
await this.restartEncodeWithLastSettings(jobId, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
triggerReason: isCancelled ? 'cancelled_encode' : 'failed_encode'
|
triggerReason: 'failed_encode'
|
||||||
});
|
});
|
||||||
this.cancelRequestedByJob.delete(Number(jobId));
|
this.cancelRequestedByJob.delete(Number(jobId));
|
||||||
return;
|
return;
|
||||||
@@ -10992,20 +10992,22 @@ class PipelineService extends EventEmitter {
|
|||||||
const cdOutputTemplate = String(
|
const cdOutputTemplate = String(
|
||||||
settings.cd_output_template || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE
|
settings.cd_output_template || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE
|
||||||
).trim() || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;
|
).trim() || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;
|
||||||
const cdBaseDir = String(settings.raw_dir || '').trim() || settingsService.DEFAULT_CD_DIR;
|
const cdRawBaseDir = String(settings.raw_dir || '').trim() || settingsService.DEFAULT_CD_DIR;
|
||||||
const cdOutputOwner = String(settings.raw_dir_owner || '').trim();
|
const cdOutputBaseDir = String(settings.movie_dir || '').trim() || cdRawBaseDir;
|
||||||
|
const cdRawOwner = String(settings.raw_dir_owner || '').trim();
|
||||||
|
const cdOutputOwner = String(settings.movie_dir_owner || settings.raw_dir_owner || '').trim();
|
||||||
const cdMetadataBase = buildRawMetadataBase({
|
const cdMetadataBase = buildRawMetadataBase({
|
||||||
title: effectiveSelectedMeta?.album || effectiveSelectedMeta?.title || null,
|
title: effectiveSelectedMeta?.album || effectiveSelectedMeta?.title || null,
|
||||||
year: effectiveSelectedMeta?.year || null
|
year: effectiveSelectedMeta?.year || null
|
||||||
}, activeJobId);
|
}, activeJobId);
|
||||||
const rawDirName = buildRawDirName(cdMetadataBase, activeJobId, { state: RAW_FOLDER_STATES.INCOMPLETE });
|
const rawDirName = buildRawDirName(cdMetadataBase, activeJobId, { state: RAW_FOLDER_STATES.INCOMPLETE });
|
||||||
const rawJobDir = path.join(cdBaseDir, rawDirName);
|
const rawJobDir = path.join(cdRawBaseDir, rawDirName);
|
||||||
const rawWavDir = rawJobDir;
|
const rawWavDir = rawJobDir;
|
||||||
const outputDir = cdRipService.buildOutputDir(effectiveSelectedMeta, cdBaseDir, cdOutputTemplate);
|
const outputDir = cdRipService.buildOutputDir(effectiveSelectedMeta, cdOutputBaseDir, cdOutputTemplate);
|
||||||
ensureDir(cdBaseDir);
|
ensureDir(cdRawBaseDir);
|
||||||
ensureDir(rawJobDir);
|
ensureDir(rawJobDir);
|
||||||
ensureDir(outputDir);
|
ensureDir(outputDir);
|
||||||
chownRecursive(rawJobDir, cdOutputOwner);
|
chownRecursive(rawJobDir, cdRawOwner);
|
||||||
chownRecursive(outputDir, cdOutputOwner);
|
chownRecursive(outputDir, cdOutputOwner);
|
||||||
const previewTrackPos = effectiveSelectedTrackPositions[0] || mergedTracks[0]?.position || 1;
|
const previewTrackPos = effectiveSelectedTrackPositions[0] || mergedTracks[0]?.position || 1;
|
||||||
const previewWavPath = path.join(rawWavDir, `track${String(previewTrackPos).padStart(2, '0')}.cdda.wav`);
|
const previewWavPath = path.join(rawWavDir, `track${String(previewTrackPos).padStart(2, '0')}.cdda.wav`);
|
||||||
@@ -11108,12 +11110,13 @@ class PipelineService extends EventEmitter {
|
|||||||
devicePath,
|
devicePath,
|
||||||
cdparanoiaCmd,
|
cdparanoiaCmd,
|
||||||
rawWavDir,
|
rawWavDir,
|
||||||
rawBaseDir: cdBaseDir,
|
rawBaseDir: cdRawBaseDir,
|
||||||
cdMetadataBase,
|
cdMetadataBase,
|
||||||
outputDir,
|
outputDir,
|
||||||
format,
|
format,
|
||||||
formatOptions,
|
formatOptions,
|
||||||
outputTemplate: cdOutputTemplate,
|
outputTemplate: cdOutputTemplate,
|
||||||
|
rawOwner: cdRawOwner,
|
||||||
outputOwner: cdOutputOwner,
|
outputOwner: cdOutputOwner,
|
||||||
selectedTrackPositions: effectiveSelectedTrackPositions,
|
selectedTrackPositions: effectiveSelectedTrackPositions,
|
||||||
tocTracks: mergedTracks,
|
tocTracks: mergedTracks,
|
||||||
@@ -11142,6 +11145,7 @@ class PipelineService extends EventEmitter {
|
|||||||
format,
|
format,
|
||||||
formatOptions,
|
formatOptions,
|
||||||
outputTemplate,
|
outputTemplate,
|
||||||
|
rawOwner,
|
||||||
outputOwner,
|
outputOwner,
|
||||||
selectedTrackPositions,
|
selectedTrackPositions,
|
||||||
tocTracks,
|
tocTracks,
|
||||||
@@ -11397,7 +11401,7 @@ class PipelineService extends EventEmitter {
|
|||||||
await historyService.updateJob(jobId, { poster_url: cdPromotedUrl }).catch(() => {});
|
await historyService.updateJob(jobId, { poster_url: cdPromotedUrl }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
chownRecursive(activeRawDir, outputOwner);
|
chownRecursive(activeRawDir, rawOwner || outputOwner);
|
||||||
chownRecursive(outputDir, outputOwner);
|
chownRecursive(outputDir, outputOwner);
|
||||||
await historyService.appendLog(jobId, 'SYSTEM', `CD-Rip abgeschlossen. Ausgabe: ${outputDir}`);
|
await historyService.appendLog(jobId, 'SYSTEM', `CD-Rip abgeschlossen. Ausgabe: ${outputDir}`);
|
||||||
const finishedStatusText = postEncodeScriptsSummary.failed > 0
|
const finishedStatusText = postEncodeScriptsSummary.failed > 0
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ const PROFILED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
movie_dir: {
|
movie_dir: {
|
||||||
bluray: 'movie_dir_bluray',
|
bluray: 'movie_dir_bluray',
|
||||||
dvd: 'movie_dir_dvd'
|
dvd: 'movie_dir_dvd',
|
||||||
|
cd: 'movie_dir_cd'
|
||||||
},
|
},
|
||||||
movie_dir_owner: {
|
movie_dir_owner: {
|
||||||
bluray: 'movie_dir_bluray_owner',
|
bluray: 'movie_dir_bluray_owner',
|
||||||
dvd: 'movie_dir_dvd_owner'
|
dvd: 'movie_dir_dvd_owner',
|
||||||
|
cd: 'movie_dir_cd_owner'
|
||||||
},
|
},
|
||||||
mediainfo_extra_args: {
|
mediainfo_extra_args: {
|
||||||
bluray: 'mediainfo_extra_args_bluray',
|
bluray: 'mediainfo_extra_args_bluray',
|
||||||
@@ -690,7 +692,7 @@ class SettingsService {
|
|||||||
if (legacyKey === 'raw_dir') {
|
if (legacyKey === 'raw_dir') {
|
||||||
resolvedValue = normalizedRequestedProfile === 'cd' ? DEFAULT_CD_DIR : DEFAULT_RAW_DIR;
|
resolvedValue = normalizedRequestedProfile === 'cd' ? DEFAULT_CD_DIR : DEFAULT_RAW_DIR;
|
||||||
} else if (legacyKey === 'movie_dir') {
|
} else if (legacyKey === 'movie_dir') {
|
||||||
resolvedValue = DEFAULT_MOVIE_DIR;
|
resolvedValue = normalizedRequestedProfile === 'cd' ? DEFAULT_CD_DIR : DEFAULT_MOVIE_DIR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
effective[legacyKey] = resolvedValue;
|
effective[legacyKey] = resolvedValue;
|
||||||
@@ -725,7 +727,7 @@ class SettingsService {
|
|||||||
return {
|
return {
|
||||||
bluray: { raw: bluray.raw_dir, movies: bluray.movie_dir },
|
bluray: { raw: bluray.raw_dir, movies: bluray.movie_dir },
|
||||||
dvd: { raw: dvd.raw_dir, movies: dvd.movie_dir },
|
dvd: { raw: dvd.raw_dir, movies: dvd.movie_dir },
|
||||||
cd: { raw: cd.raw_dir },
|
cd: { raw: cd.raw_dir, movies: cd.movie_dir },
|
||||||
defaults: {
|
defaults: {
|
||||||
raw: DEFAULT_RAW_DIR,
|
raw: DEFAULT_RAW_DIR,
|
||||||
movies: DEFAULT_MOVIE_DIR,
|
movies: DEFAULT_MOVIE_DIR,
|
||||||
|
|||||||
@@ -373,13 +373,21 @@ VALUES ('cd_output_template', '{artist} - {album} ({year})/{trackNr} {artist} -
|
|||||||
|
|
||||||
-- Pfade – CD
|
-- Pfade – CD
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('raw_dir_cd', 'Pfade', 'CD RAW-Ordner', 'path', 0, 'Basisordner für CD-Rips. Enthält die WAV-Rohdaten (RAW) sowie den encodierten Audio-Output. Leer = Standardpfad (data/output/cd).', NULL, '[]', '{}', 104);
|
VALUES ('raw_dir_cd', 'Pfade', 'CD RAW-Ordner', 'path', 0, 'Basisordner für rohe CD-WAV-Dateien (cdparanoia-Output). Leer = Standardpfad (data/output/cd).', NULL, '[]', '{}', 104);
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_cd', NULL);
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_cd', NULL);
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('raw_dir_cd_owner', 'Pfade', 'Eigentümer CD-Ordner', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1045);
|
VALUES ('raw_dir_cd_owner', 'Pfade', 'Eigentümer CD RAW-Ordner', 'string', 0, 'Eigentümer der Dateien im Format user:gruppe. Nur aktiv wenn ein Pfad gesetzt ist. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1045);
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_cd_owner', NULL);
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('raw_dir_cd_owner', NULL);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
|
VALUES ('movie_dir_cd', 'Pfade', 'CD Output-Ordner', 'path', 0, 'Zielordner für encodierte CD-Ausgaben (FLAC, MP3 usw.). Leer = gleicher Ordner wie CD RAW-Ordner.', NULL, '[]', '{}', 114);
|
||||||
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_cd', NULL);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
|
VALUES ('movie_dir_cd_owner', 'Pfade', 'Eigentümer CD Output-Ordner', 'string', 0, 'Eigentümer der encodierten CD-Ausgaben im Format user:gruppe. Leer = Standardbenutzer des Dienstes.', NULL, '[]', '{}', 1145);
|
||||||
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('movie_dir_cd_owner', NULL);
|
||||||
|
|
||||||
-- Metadaten
|
-- Metadaten
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('omdb_api_key', 'Metadaten', 'OMDb API Key', 'string', 0, 'API Key für Metadatensuche.', NULL, '[]', '{}', 400);
|
VALUES ('omdb_api_key', 'Metadaten', 'OMDb API Key', 'string', 0, 'API Key für Metadatensuche.', NULL, '[]', '{}', 400);
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ function buildToolSections(settings) {
|
|||||||
// Path keys per medium — _owner keys are rendered inline
|
// Path keys per medium — _owner keys are rendered inline
|
||||||
const BLURAY_PATH_KEYS = ['raw_dir_bluray', 'movie_dir_bluray', 'output_template_bluray'];
|
const BLURAY_PATH_KEYS = ['raw_dir_bluray', 'movie_dir_bluray', 'output_template_bluray'];
|
||||||
const DVD_PATH_KEYS = ['raw_dir_dvd', 'movie_dir_dvd', 'output_template_dvd'];
|
const DVD_PATH_KEYS = ['raw_dir_dvd', 'movie_dir_dvd', 'output_template_dvd'];
|
||||||
const CD_PATH_KEYS = ['raw_dir_cd', 'cd_output_template'];
|
const CD_PATH_KEYS = ['raw_dir_cd', 'movie_dir_cd', 'cd_output_template'];
|
||||||
const LOG_PATH_KEYS = ['log_dir'];
|
const LOG_PATH_KEYS = ['log_dir'];
|
||||||
|
|
||||||
function buildSectionsForCategory(categoryName, settings) {
|
function buildSectionsForCategory(categoryName, settings) {
|
||||||
@@ -381,6 +381,7 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
|
|||||||
const dvdRaw = ep.dvd?.raw || defaultRaw;
|
const dvdRaw = ep.dvd?.raw || defaultRaw;
|
||||||
const dvdMovies = ep.dvd?.movies || defaultMovies;
|
const dvdMovies = ep.dvd?.movies || defaultMovies;
|
||||||
const cdRaw = ep.cd?.raw || defaultCd;
|
const cdRaw = ep.cd?.raw || defaultCd;
|
||||||
|
const cdMovies = ep.cd?.movies || cdRaw;
|
||||||
|
|
||||||
const isDefault = (path, def) => path === def;
|
const isDefault = (path, def) => path === def;
|
||||||
|
|
||||||
@@ -424,11 +425,15 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>CD / Audio (RAW + Output)</strong></td>
|
<td><strong>CD / Audio</strong></td>
|
||||||
<td colSpan={2}>
|
<td>
|
||||||
<code>{cdRaw}</code>
|
<code>{cdRaw}</code>
|
||||||
{isDefault(cdRaw, defaultCd) && <span className="path-default-badge">Standard</span>}
|
{isDefault(cdRaw, defaultCd) && <span className="path-default-badge">Standard</span>}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{cdMovies}</code>
|
||||||
|
{isDefault(cdMovies, cdRaw) && <span className="path-default-badge">Standard</span>}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -768,6 +768,20 @@ export default function DashboardPage({
|
|||||||
const memoryMetrics = monitoringSample?.memory || null;
|
const memoryMetrics = monitoringSample?.memory || null;
|
||||||
const gpuMetrics = monitoringSample?.gpu || null;
|
const gpuMetrics = monitoringSample?.gpu || null;
|
||||||
const storageMetrics = Array.isArray(monitoringSample?.storage) ? monitoringSample.storage : [];
|
const storageMetrics = Array.isArray(monitoringSample?.storage) ? monitoringSample.storage : [];
|
||||||
|
const storageGroups = useMemo(() => {
|
||||||
|
const groups = [];
|
||||||
|
const mountMap = new Map();
|
||||||
|
for (const entry of storageMetrics) {
|
||||||
|
const groupKey = entry?.mountPoint || `__no_mount_${entry?.key}`;
|
||||||
|
if (!mountMap.has(groupKey)) {
|
||||||
|
const group = { mountPoint: entry?.mountPoint || null, entries: [], representative: entry };
|
||||||
|
mountMap.set(groupKey, group);
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
mountMap.get(groupKey).entries.push(entry);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}, [storageMetrics]);
|
||||||
const cpuPerCoreMetrics = Array.isArray(cpuMetrics?.perCore) ? cpuMetrics.perCore : [];
|
const cpuPerCoreMetrics = Array.isArray(cpuMetrics?.perCore) ? cpuMetrics.perCore : [];
|
||||||
const gpuDevices = Array.isArray(gpuMetrics?.devices) ? gpuMetrics.devices : [];
|
const gpuDevices = Array.isArray(gpuMetrics?.devices) ? gpuMetrics.devices : [];
|
||||||
|
|
||||||
@@ -1854,57 +1868,55 @@ export default function DashboardPage({
|
|||||||
<section className="hardware-monitor-block">
|
<section className="hardware-monitor-block">
|
||||||
<h4>Freier Speicher in Pfaden</h4>
|
<h4>Freier Speicher in Pfaden</h4>
|
||||||
<div className="hardware-storage-list">
|
<div className="hardware-storage-list">
|
||||||
{storageMetrics.map((entry) => {
|
{storageGroups.map((group) => {
|
||||||
const tone = getStorageUsageTone(entry?.usagePercent);
|
const rep = group.representative;
|
||||||
const usagePercent = Number(entry?.usagePercent);
|
const tone = getStorageUsageTone(rep?.usagePercent);
|
||||||
|
const usagePercent = Number(rep?.usagePercent);
|
||||||
const barValue = Number.isFinite(usagePercent)
|
const barValue = Number.isFinite(usagePercent)
|
||||||
? Math.max(0, Math.min(100, usagePercent))
|
? Math.max(0, Math.min(100, usagePercent))
|
||||||
: 0;
|
: 0;
|
||||||
const hasError = Boolean(entry?.error);
|
const hasError = group.entries.every((e) => e?.error);
|
||||||
const entryKey = entry?.key || entry?.path || 'storage-entry';
|
const groupKey = group.mountPoint || group.entries.map((e) => e?.key).join('-');
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`storage-entry-${entryKey}`}
|
key={`storage-group-${groupKey}`}
|
||||||
className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
|
className={`hardware-storage-item compact${hasError ? ' has-error' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="hardware-storage-head">
|
<div className="hardware-storage-head">
|
||||||
<strong>{entry?.label || entry?.key || 'Pfad'}</strong>
|
<strong>{group.entries.map((e) => e?.label || e?.key || 'Pfad').join(' · ')}</strong>
|
||||||
<span className={`hardware-storage-percent tone-${tone}`}>
|
<span className={`hardware-storage-percent tone-${tone}`}>
|
||||||
{hasError ? 'Fehler' : formatPercent(entry?.usagePercent)}
|
{hasError ? 'Fehler' : formatPercent(rep?.usagePercent)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasError ? (
|
{hasError ? (
|
||||||
<small className="error-text">{entry?.error}</small>
|
<small className="error-text">{rep?.error}</small>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={`hardware-storage-bar tone-${tone}`}>
|
<div className={`hardware-storage-bar tone-${tone}`}>
|
||||||
<ProgressBar value={barValue} showValue={false} />
|
<ProgressBar value={barValue} showValue={false} />
|
||||||
</div>
|
</div>
|
||||||
<div className="hardware-storage-summary">
|
<div className="hardware-storage-summary">
|
||||||
<small>Frei: {formatBytes(entry?.freeBytes)}</small>
|
<small>Frei: {formatBytes(rep?.freeBytes)}</small>
|
||||||
<small>Gesamt: {formatBytes(entry?.totalBytes)}</small>
|
<small>Gesamt: {formatBytes(rep?.totalBytes)}</small>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="hardware-storage-paths">
|
{group.entries.map((entry) => (
|
||||||
<small className="hardware-storage-label-tag">Pfad:</small>
|
<div key={entry?.key} className="hardware-storage-paths">
|
||||||
<small className="hardware-storage-path" title={entry?.path || '-'}>
|
<small className="hardware-storage-label-tag">{entry?.label || entry?.key}:</small>
|
||||||
{entry?.path || '-'}
|
<small className="hardware-storage-path" title={entry?.path || '-'}>
|
||||||
</small>
|
{entry?.path || '-'}
|
||||||
{entry?.mountPoint ? (
|
|
||||||
<small className="hardware-storage-path" title={entry.mountPoint}>
|
|
||||||
Mount: {entry.mountPoint}
|
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
{entry?.queryPath && entry.queryPath !== entry.path ? (
|
||||||
{entry?.queryPath && entry.queryPath !== entry.path ? (
|
<small className="hardware-storage-path" title={entry.queryPath}>
|
||||||
<small className="hardware-storage-path" title={entry.queryPath}>
|
(Parent: {entry.queryPath})
|
||||||
Parent: {entry.queryPath}
|
</small>
|
||||||
</small>
|
) : null}
|
||||||
) : null}
|
{entry?.note ? <small className="hardware-storage-path">{entry.note}</small> : null}
|
||||||
{entry?.note ? <small className="hardware-storage-path">{entry.note}</small> : null}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user