7 Commits

Author SHA1 Message Date
466e7a7a3d Remove gitea scripts from tracking 2026-03-13 22:30:46 +00:00
e67c0d316d kk 2026-03-13 22:27:59 +00:00
1da5ee3e34 Fix 2026-03-13 22:11:24 +00:00
4d377f3eb4 ignore 2026-03-13 18:43:08 +00:00
df708485b5 merge 2026-03-13 15:50:45 +00:00
b6cac5efb4 merge 2026-03-13 15:15:50 +00:00
f38081649f merge 2026-03-13 11:21:29 +00:00
10 changed files with 325 additions and 126 deletions

View File

@@ -855,6 +855,41 @@ async function migrateSettingsSchemaMetadata(db) {
logger.info('migrate:settings-schema-category-moved', { key: move.key, category: move.category });
}
}
const rawDirCdLabel = 'CD RAW-Ordner';
const rawDirCdDescription = 'Basisordner für rohe CD-WAV-Dateien (cdparanoia-Output). Leer = Standardpfad (data/output/cd).';
const rawDirCdResult = await db.run(
`UPDATE settings_schema
SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE key = 'raw_dir_cd' AND (label != ? OR description != ?)`,
[rawDirCdLabel, rawDirCdDescription, rawDirCdLabel, rawDirCdDescription]
);
if (rawDirCdResult?.changes > 0) {
logger.info('migrate:settings-schema-cd-raw-updated', {
key: 'raw_dir_cd',
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() {

View File

@@ -321,6 +321,20 @@ function formatCommandLine(cmd, args = []) {
return [quoteShellArg(cmd), ...normalizedArgs.map((arg) => quoteShellArg(arg))].join(' ');
}
function copyFilePreservingRaw(sourcePath, targetPath) {
const rawSource = String(sourcePath || '').trim();
const rawTarget = String(targetPath || '').trim();
if (!rawSource || !rawTarget) {
return;
}
const source = path.resolve(rawSource);
const target = path.resolve(rawTarget);
if (source === target) {
return;
}
fs.copyFileSync(source, target);
}
async function runProcessTracked({
cmd,
args,
@@ -492,7 +506,7 @@ async function ripAndEncode(options) {
// ── Phase 2: Encode WAVs to target format ─────────────────────────────────
if (format === 'wav') {
// Just move WAV files to output dir with proper names
// Keep RAW WAVs in place and copy them to the final output structure.
for (let i = 0; i < tracksToRip.length; i++) {
assertNotCancelled(isCancelled);
const track = tracksToRip[i];
@@ -508,8 +522,8 @@ async function ripAndEncode(options) {
percent: 50 + ((i / tracksToRip.length) * 50)
});
ensureDir(path.dirname(outFile));
log('info', `Promptkette [Move ${i + 1}/${tracksToRip.length}]: mv ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`);
fs.renameSync(wavFile, outFile);
log('info', `Promptkette [Copy ${i + 1}/${tracksToRip.length}]: cp ${quoteShellArg(wavFile)} ${quoteShellArg(outFile)}`);
copyFilePreservingRaw(wavFile, outFile);
onProgress && onProgress({
phase: 'encode',
trackEvent: 'complete',

View File

@@ -421,7 +421,7 @@ class HardwareMonitorService {
} else {
addPath('raw_dir', 'RAW-Verzeichnis', blurayRawPath || dvdRawPath || sourceMap.raw_dir);
}
addPath('raw_dir_cd', 'CD-Verzeichnis', cdRawPath || sourceMap.raw_dir_cd);
addPath('raw_dir_cd', 'CD RAW-Ordner', cdRawPath || sourceMap.raw_dir_cd);
if (blurayMoviePath && dvdMoviePath && blurayMoviePath !== dvdMoviePath) {
addPath('movie_dir_bluray', 'Movie-Verzeichnis (Blu-ray)', blurayMoviePath);

View File

@@ -21,7 +21,7 @@ function parseJsonSafe(raw, fallback = null) {
const PROCESS_LOG_TAIL_MAX_BYTES = 1024 * 1024;
const processLogStreams = new Map();
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'other'];
const PROFILE_PATH_SUFFIXES = ['bluray', 'dvd', 'cd', 'other'];
const RAW_INCOMPLETE_PREFIX = 'Incomplete_';
const RAW_RIP_COMPLETE_PREFIX = 'Rip_Complete_';
@@ -161,6 +161,41 @@ function hasBlurayStructure(rawPath) {
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) {
const basePath = String(rawPath || '').trim();
if (!basePath) {
@@ -356,12 +391,46 @@ function toProcessLogStreamKey(jobId) {
return String(Math.trunc(normalizedId));
}
function resolveEffectiveRawPath(storedPath, rawDir) {
function resolveEffectiveRawPath(storedPath, rawDir, extraDirs = []) {
const stored = String(storedPath || '').trim();
if (!stored || !rawDir) return stored;
if (!stored) return stored;
const folderName = path.basename(stored);
if (!folderName) return stored;
return path.join(String(rawDir).trim(), folderName);
const candidates = [];
const seen = new Set();
const pushCandidate = (candidatePath) => {
const normalized = String(candidatePath || '').trim();
if (!normalized) {
return;
}
const comparable = normalizeComparablePath(normalized);
if (!comparable || seen.has(comparable)) {
return;
}
seen.add(comparable);
candidates.push(normalized);
};
pushCandidate(stored);
if (rawDir) {
pushCandidate(path.join(String(rawDir).trim(), folderName));
}
for (const extraDir of Array.isArray(extraDirs) ? extraDirs : []) {
pushCandidate(path.join(String(extraDir || '').trim(), folderName));
}
for (const candidate of candidates) {
try {
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
return candidate;
}
} catch (_error) {
// ignore fs errors and continue with fallbacks
}
}
return rawDir ? path.join(String(rawDir).trim(), folderName) : stored;
}
function resolveEffectiveOutputPath(storedPath, movieDir) {
@@ -405,17 +474,16 @@ function resolveEffectiveStoragePathsForJob(settings = null, job = {}, parsed =
const effectiveSettings = settingsService.resolveEffectiveToolSettings(settings || {}, mediaType);
const rawDir = String(effectiveSettings?.raw_dir || '').trim();
const configuredMovieDir = String(effectiveSettings?.movie_dir || '').trim();
const movieDir = mediaType === 'cd' ? rawDir : configuredMovieDir;
const effectiveRawPath = mediaType === 'cd'
? (job?.raw_path || null)
: (rawDir && job?.raw_path
? resolveEffectiveRawPath(job.raw_path, rawDir)
: (job?.raw_path || null));
const effectiveOutputPath = mediaType === 'cd'
? (job?.output_path || null)
: (configuredMovieDir && job?.output_path
const movieDir = configuredMovieDir || rawDir;
const rawLookupDirs = getConfiguredMediaPathList(settings || {}, 'raw_dir')
.filter((candidate) => normalizeComparablePath(candidate) !== normalizeComparablePath(rawDir));
const effectiveRawPath = job?.raw_path
? resolveEffectiveRawPath(job.raw_path, rawDir, rawLookupDirs)
: (job?.raw_path || null);
// For CD, output_path is a directory (album folder) — skip path-relocation heuristic
const effectiveOutputPath = (mediaType !== 'cd' && configuredMovieDir && job?.output_path)
? resolveEffectiveOutputPath(job.output_path, configuredMovieDir)
: (job?.output_path || null));
: (job?.output_path || null);
return {
mediaType,
@@ -1364,6 +1432,7 @@ class HistoryService {
const stat = fs.statSync(rawPath);
const metadata = parseRawFolderMetadata(entry.name);
const detectedMediaType = detectOrphanMediaType(rawPath);
orphanRows.push({
rawPath,
folderName: entry.name,
@@ -1372,7 +1441,10 @@ class HistoryService {
imdbId: metadata.imdbId,
folderJobId: metadata.folderJobId,
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()
});
seenOrphanPaths.add(normalizedPath);
@@ -1509,6 +1581,7 @@ class HistoryService {
}
}
const detectedMediaType = detectOrphanMediaType(finalRawPath);
const orphanPosterUrl = omdbById?.poster || null;
await this.updateJob(created.id, {
status: 'FINISHED',
@@ -1533,7 +1606,11 @@ class HistoryService {
status: 'SUCCESS',
source: 'orphan_raw_import',
importedAt,
rawPath: finalRawPath
rawPath: finalRawPath,
mediaProfile: detectedMediaType,
analyzeContext: {
mediaProfile: detectedMediaType
}
})
});
@@ -1551,8 +1628,8 @@ class HistoryService {
created.id,
'SYSTEM',
renameSteps.length > 0
? `Historieneintrag aus RAW erstellt. Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath}`
? `Historieneintrag aus RAW erstellt (Medientyp: ${detectedMediaType}). Ordner umbenannt: ${renameSteps.map((step) => `${step.from} -> ${step.to}`).join(' | ')}`
: `Historieneintrag aus bestehendem RAW-Ordner erstellt: ${finalRawPath} (Medientyp: ${detectedMediaType})`
);
if (metadata.imdbId) {
await this.appendLog(
@@ -1566,7 +1643,8 @@ class HistoryService {
logger.info('job:import-orphan-raw', {
jobId: created.id,
rawPath: absRawPath
rawPath: absRawPath,
detectedMediaType
});
const imported = await this.getJobById(created.id);

View File

@@ -3694,6 +3694,46 @@ class PipelineService extends EventEmitter {
return existingDirectories[0];
}
buildRawPathLookupConfig(settingsMap = {}, mediaProfile = null) {
const sourceMap = settingsMap && typeof settingsMap === 'object' ? settingsMap : {};
const normalizedMediaProfile = normalizeMediaProfile(mediaProfile);
const effectiveSettings = settingsService.resolveEffectiveToolSettings(sourceMap, normalizedMediaProfile);
const preferredDefaultRawDir = normalizedMediaProfile === 'cd'
? settingsService.DEFAULT_CD_DIR
: settingsService.DEFAULT_RAW_DIR;
const uniqueRawDirs = Array.from(
new Set(
[
effectiveSettings?.raw_dir,
sourceMap?.raw_dir,
sourceMap?.raw_dir_bluray,
sourceMap?.raw_dir_dvd,
sourceMap?.raw_dir_cd,
preferredDefaultRawDir,
settingsService.DEFAULT_RAW_DIR,
settingsService.DEFAULT_CD_DIR
]
.map((item) => String(item || '').trim())
.filter(Boolean)
)
);
return {
effectiveSettings,
rawBaseDir: uniqueRawDirs[0] || String(preferredDefaultRawDir || '').trim() || null,
rawExtraDirs: uniqueRawDirs.slice(1)
};
}
resolveCurrentRawPathForSettings(settingsMap = {}, mediaProfile = null, storedRawPath = null) {
const stored = String(storedRawPath || '').trim();
if (!stored) {
return null;
}
const { rawBaseDir, rawExtraDirs } = this.buildRawPathLookupConfig(settingsMap, mediaProfile);
return this.resolveCurrentRawPath(rawBaseDir, stored, rawExtraDirs);
}
async migrateRawFolderNamingOnStartup(db) {
const settings = await settingsService.getSettingsMap();
const rawBaseDir = String(settings?.raw_dir || settingsService.DEFAULT_RAW_DIR || '').trim();
@@ -5385,15 +5425,17 @@ class PipelineService extends EventEmitter {
};
}
const existingPlan = this.safeParseJson(job.encode_plan_json);
const refreshSettings = await settingsService.getSettingsMap();
const refreshRawBaseDir = settingsService.DEFAULT_RAW_DIR;
const refreshRawExtraDirs = [
refreshSettings?.raw_dir_bluray,
refreshSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedRefreshRawPath = job.raw_path
? this.resolveCurrentRawPath(refreshRawBaseDir, job.raw_path, refreshRawExtraDirs)
: null;
const refreshMediaProfile = this.resolveMediaProfileForJob(job, {
encodePlan: existingPlan,
rawPath: job.raw_path
});
const resolvedRefreshRawPath = this.resolveCurrentRawPathForSettings(
refreshSettings,
refreshMediaProfile,
job.raw_path
);
if (!resolvedRefreshRawPath) {
return {
@@ -5409,7 +5451,6 @@ class PipelineService extends EventEmitter {
await historyService.updateJob(activeJobId, { raw_path: resolvedRefreshRawPath });
}
const existingPlan = this.safeParseJson(job.encode_plan_json);
const mode = existingPlan?.mode || this.snapshot.context?.mode || 'rip';
const sourceJobId = existingPlan?.sourceJobId || this.snapshot.context?.sourceJobId || null;
@@ -7339,14 +7380,11 @@ class PipelineService extends EventEmitter {
encodePlan: confirmedPlan
});
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const confirmRawBaseDir = String(confirmSettings?.raw_dir || '').trim();
const confirmRawExtraDirs = [
confirmSettings?.raw_dir_bluray,
confirmSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedConfirmRawPath = job.raw_path
? this.resolveCurrentRawPath(confirmRawBaseDir, job.raw_path, confirmRawExtraDirs)
: null;
const resolvedConfirmRawPath = this.resolveCurrentRawPathForSettings(
confirmSettings,
readyMediaProfile,
job.raw_path
);
const activeConfirmRawPath = resolvedConfirmRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode
@@ -7460,13 +7498,16 @@ class PipelineService extends EventEmitter {
throw error;
}
const reencodeMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
makemkvInfo: mkInfo,
rawPath: sourceJob.raw_path
});
const reencodeSettings = await settingsService.getSettingsMap();
const reencodeRawBaseDir = settingsService.DEFAULT_RAW_DIR;
const reencodeRawExtraDirs = [
reencodeSettings?.raw_dir_bluray,
reencodeSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedReencodeRawPath = this.resolveCurrentRawPath(reencodeRawBaseDir, sourceJob.raw_path, reencodeRawExtraDirs);
const resolvedReencodeRawPath = this.resolveCurrentRawPathForSettings(
reencodeSettings,
reencodeMediaProfile,
sourceJob.raw_path
);
if (!resolvedReencodeRawPath) {
const error = new Error(`Re-Encode nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`);
error.statusCode = 400;
@@ -8339,14 +8380,7 @@ class PipelineService extends EventEmitter {
rawPath: job.raw_path
});
const settings = await settingsService.getEffectiveSettingsMap(mediaProfile);
const rawBaseDir = String(settings.raw_dir || '').trim();
const rawExtraDirs = [
settings.raw_dir_bluray,
settings.raw_dir_dvd
].map((item) => String(item || '').trim()).filter(Boolean);
const resolvedRawPath = job.raw_path
? this.resolveCurrentRawPath(rawBaseDir, job.raw_path, rawExtraDirs)
: null;
const resolvedRawPath = this.resolveCurrentRawPathForSettings(settings, mediaProfile, job.raw_path);
const activeRawPath = resolvedRawPath || String(job.raw_path || '').trim() || null;
if (activeRawPath && normalizeComparablePath(activeRawPath) !== normalizeComparablePath(job.raw_path)) {
await historyService.updateJob(jobId, { raw_path: activeRawPath });
@@ -9251,14 +9285,15 @@ class PipelineService extends EventEmitter {
};
} else {
const retrySettings = await settingsService.getEffectiveSettingsMap(mediaProfile);
const retryRawBaseDir = String(retrySettings?.raw_dir || '').trim();
const retryRawExtraDirs = [
retrySettings?.raw_dir_bluray,
retrySettings?.raw_dir_dvd
].map((dirPath) => String(dirPath || '').trim()).filter(Boolean);
const resolvedOldRawPath = sourceJob.raw_path
? this.resolveCurrentRawPath(retryRawBaseDir, sourceJob.raw_path, retryRawExtraDirs)
: null;
const { rawBaseDir: retryRawBaseDir, rawExtraDirs: retryRawExtraDirs } = this.buildRawPathLookupConfig(
retrySettings,
mediaProfile
);
const resolvedOldRawPath = this.resolveCurrentRawPathForSettings(
retrySettings,
mediaProfile,
sourceJob.raw_path
);
if (resolvedOldRawPath) {
const oldRawFolderName = path.basename(resolvedOldRawPath);
@@ -9419,15 +9454,13 @@ class PipelineService extends EventEmitter {
const mode = String(encodePlan?.mode || 'rip').trim().toLowerCase();
const isPreRipMode = mode === 'pre_rip' || Boolean(encodePlan?.preRip);
const reviewConfirmed = Boolean(Number(job.encode_review_confirmed || 0) || encodePlan?.reviewConfirmed);
const resumeSettings = await settingsService.getEffectiveSettingsMap(this.resolveMediaProfileForJob(job, { encodePlan }));
const resumeRawBaseDir = String(resumeSettings?.raw_dir || '').trim();
const resumeRawExtraDirs = [
resumeSettings?.raw_dir_bluray,
resumeSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedResumeRawPath = job.raw_path
? this.resolveCurrentRawPath(resumeRawBaseDir, job.raw_path, resumeRawExtraDirs)
: null;
const readyMediaProfile = this.resolveMediaProfileForJob(job, { encodePlan });
const resumeSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const resolvedResumeRawPath = this.resolveCurrentRawPathForSettings(
resumeSettings,
readyMediaProfile,
job.raw_path
);
const activeResumeRawPath = resolvedResumeRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode
@@ -9463,10 +9496,6 @@ class PipelineService extends EventEmitter {
imdbId: job.imdb_id || null,
poster: job.poster_url || null
};
const readyMediaProfile = this.resolveMediaProfileForJob(job, {
encodePlan
});
await this.setState('READY_TO_ENCODE', {
activeJobId: jobId,
progress: 0,
@@ -9613,14 +9642,11 @@ class PipelineService extends EventEmitter {
encodePlan: restartPlan
});
const restartSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const restartRawBaseDir = String(restartSettings?.raw_dir || '').trim();
const restartRawExtraDirs = [
restartSettings?.raw_dir_bluray,
restartSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedRestartRawPath = job.raw_path
? this.resolveCurrentRawPath(restartRawBaseDir, job.raw_path, restartRawExtraDirs)
: null;
const resolvedRestartRawPath = this.resolveCurrentRawPathForSettings(
restartSettings,
readyMediaProfile,
job.raw_path
);
const activeRestartRawPath = resolvedRestartRawPath || String(job.raw_path || '').trim() || null;
let inputPath = isPreRipMode
@@ -9761,13 +9787,19 @@ class PipelineService extends EventEmitter {
throw error;
}
const reviewMakemkvInfo = this.safeParseJson(sourceJob.makemkv_info_json);
const reviewEncodePlan = this.safeParseJson(sourceJob.encode_plan_json);
const reviewMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
makemkvInfo: reviewMakemkvInfo,
encodePlan: reviewEncodePlan,
rawPath: sourceJob.raw_path
});
const reviewSettings = await settingsService.getSettingsMap();
const reviewRawBaseDir = settingsService.DEFAULT_RAW_DIR;
const reviewRawExtraDirs = [
reviewSettings?.raw_dir_bluray,
reviewSettings?.raw_dir_dvd
].map((d) => String(d || '').trim()).filter(Boolean);
const resolvedReviewRawPath = this.resolveCurrentRawPath(reviewRawBaseDir, sourceJob.raw_path, reviewRawExtraDirs);
const resolvedReviewRawPath = this.resolveCurrentRawPathForSettings(
reviewSettings,
reviewMediaProfile,
sourceJob.raw_path
);
if (!resolvedReviewRawPath) {
const error = new Error(`Review-Neustart nicht möglich: RAW-Pfad existiert nicht (${sourceJob.raw_path}).`);
error.statusCode = 400;
@@ -9855,11 +9887,9 @@ class PipelineService extends EventEmitter {
encode_plan_json: null,
encode_input_path: normalizedReviewInputPath || null,
encode_review_confirmed: 0,
makemkv_info_json: nextMakemkvInfoJson
makemkv_info_json: nextMakemkvInfoJson,
raw_path: resolvedReviewRawPath
};
if (resolvedReviewRawPath !== sourceJob.raw_path) {
jobUpdatePayload.raw_path = resolvedReviewRawPath;
}
const replacementJob = await historyService.createJob({
discDevice: sourceJob.disc_device || null,
@@ -10345,10 +10375,10 @@ class PipelineService extends EventEmitter {
logger.error('command:failed', { jobId, stage, source, error: errorToMeta(error) });
throw error;
} finally {
await historyService.closeProcessLog(jobId);
this.activeProcesses.delete(Number(normalizedJobId));
this.syncPrimaryActiveProcess();
this.cancelRequestedByJob.delete(Number(normalizedJobId));
this.syncPrimaryActiveProcess();
await historyService.closeProcessLog(jobId);
await this.emitQueueChanged();
void this.pumpQueue();
}
@@ -10398,16 +10428,16 @@ class PipelineService extends EventEmitter {
hasRawPath = false;
}
if (normalizedStage === 'ENCODING' && hasConfirmedPlan) {
if (normalizedStage === 'ENCODING' && hasConfirmedPlan && !isCancelled) {
try {
await historyService.appendLog(
jobId,
'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, {
immediate: true,
triggerReason: isCancelled ? 'cancelled_encode' : 'failed_encode'
triggerReason: 'failed_encode'
});
this.cancelRequestedByJob.delete(Number(jobId));
return;
@@ -10960,20 +10990,22 @@ class PipelineService extends EventEmitter {
const cdOutputTemplate = String(
settings.cd_output_template || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE
).trim() || cdRipService.DEFAULT_CD_OUTPUT_TEMPLATE;
const cdBaseDir = String(settings.raw_dir || '').trim() || settingsService.DEFAULT_CD_DIR;
const cdOutputOwner = String(settings.raw_dir_owner || '').trim();
const cdRawBaseDir = String(settings.raw_dir || '').trim() || settingsService.DEFAULT_CD_DIR;
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({
title: effectiveSelectedMeta?.album || effectiveSelectedMeta?.title || null,
year: effectiveSelectedMeta?.year || null
}, activeJobId);
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 outputDir = cdRipService.buildOutputDir(effectiveSelectedMeta, cdBaseDir, cdOutputTemplate);
ensureDir(cdBaseDir);
const outputDir = cdRipService.buildOutputDir(effectiveSelectedMeta, cdOutputBaseDir, cdOutputTemplate);
ensureDir(cdRawBaseDir);
ensureDir(rawJobDir);
ensureDir(outputDir);
chownRecursive(rawJobDir, cdOutputOwner);
chownRecursive(rawJobDir, cdRawOwner);
chownRecursive(outputDir, cdOutputOwner);
const previewTrackPos = effectiveSelectedTrackPositions[0] || mergedTracks[0]?.position || 1;
const previewWavPath = path.join(rawWavDir, `track${String(previewTrackPos).padStart(2, '0')}.cdda.wav`);
@@ -11076,12 +11108,13 @@ class PipelineService extends EventEmitter {
devicePath,
cdparanoiaCmd,
rawWavDir,
rawBaseDir: cdBaseDir,
rawBaseDir: cdRawBaseDir,
cdMetadataBase,
outputDir,
format,
formatOptions,
outputTemplate: cdOutputTemplate,
rawOwner: cdRawOwner,
outputOwner: cdOutputOwner,
selectedTrackPositions: effectiveSelectedTrackPositions,
tocTracks: mergedTracks,
@@ -11110,6 +11143,7 @@ class PipelineService extends EventEmitter {
format,
formatOptions,
outputTemplate,
rawOwner,
outputOwner,
selectedTrackPositions,
tocTracks,
@@ -11365,7 +11399,7 @@ class PipelineService extends EventEmitter {
await historyService.updateJob(jobId, { poster_url: cdPromotedUrl }).catch(() => {});
}
chownRecursive(activeRawDir, outputOwner);
chownRecursive(activeRawDir, rawOwner || outputOwner);
chownRecursive(outputDir, outputOwner);
await historyService.appendLog(jobId, 'SYSTEM', `CD-Rip abgeschlossen. Ausgabe: ${outputDir}`);
const finishedStatusText = postEncodeScriptsSummary.failed > 0

View File

@@ -52,11 +52,13 @@ const PROFILED_SETTINGS = {
},
movie_dir: {
bluray: 'movie_dir_bluray',
dvd: 'movie_dir_dvd'
dvd: 'movie_dir_dvd',
cd: 'movie_dir_cd'
},
movie_dir_owner: {
bluray: 'movie_dir_bluray_owner',
dvd: 'movie_dir_dvd_owner'
dvd: 'movie_dir_dvd_owner',
cd: 'movie_dir_cd_owner'
},
mediainfo_extra_args: {
bluray: 'mediainfo_extra_args_bluray',
@@ -690,7 +692,7 @@ class SettingsService {
if (legacyKey === 'raw_dir') {
resolvedValue = normalizedRequestedProfile === 'cd' ? DEFAULT_CD_DIR : DEFAULT_RAW_DIR;
} else if (legacyKey === 'movie_dir') {
resolvedValue = DEFAULT_MOVIE_DIR;
resolvedValue = normalizedRequestedProfile === 'cd' ? DEFAULT_CD_DIR : DEFAULT_MOVIE_DIR;
}
}
effective[legacyKey] = resolvedValue;
@@ -725,7 +727,7 @@ class SettingsService {
return {
bluray: { raw: bluray.raw_dir, movies: bluray.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: {
raw: DEFAULT_RAW_DIR,
movies: DEFAULT_MOVIE_DIR,

View File

@@ -373,13 +373,21 @@ VALUES ('cd_output_template', '{artist} - {album} ({year})/{trackNr} {artist} -
-- Pfade CD
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_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_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
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);

View File

@@ -92,7 +92,7 @@ export default function CdMetadataDialog({
}
setSelected(null);
setQuery('');
setManualTitle(context?.detectedTitle || '');
setManualTitle('');
setManualArtist('');
setManualYear(null);
setResults([]);

View File

@@ -160,7 +160,7 @@ function buildToolSections(settings) {
// Path keys per medium — _owner keys are rendered inline
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 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'];
function buildSectionsForCategory(categoryName, settings) {
@@ -380,7 +380,8 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
const blurayMovies = ep.bluray?.movies || defaultMovies;
const dvdRaw = ep.dvd?.raw || defaultRaw;
const dvdMovies = ep.dvd?.movies || defaultMovies;
const cdOutput = ep.cd?.raw || defaultCd;
const cdRaw = ep.cd?.raw || defaultCd;
const cdMovies = ep.cd?.movies || cdRaw;
const isDefault = (path, def) => path === def;
@@ -425,9 +426,13 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
</tr>
<tr>
<td><strong>CD / Audio</strong></td>
<td colSpan={2}>
<code>{cdOutput}</code>
{isDefault(cdOutput, defaultCd) && <span className="path-default-badge">Standard</span>}
<td>
<code>{cdRaw}</code>
{isDefault(cdRaw, defaultCd) && <span className="path-default-badge">Standard</span>}
</td>
<td>
<code>{cdMovies}</code>
{isDefault(cdMovies, cdRaw) && <span className="path-default-badge">Standard</span>}
</td>
</tr>
</tbody>

View File

@@ -42,6 +42,9 @@ function resolveMediaType(row) {
if (['dvd', 'disc', 'dvdvideo', 'dvd-video', 'dvdrom', 'dvd-rom', 'video_ts', 'iso9660'].includes(raw)) {
return 'dvd';
}
if (['cd', 'audio_cd'].includes(raw)) {
return 'cd';
}
}
return 'other';
}
@@ -467,7 +470,7 @@ export default function DatabasePage() {
const handleImportOrphanRaw = async (row) => {
const target = row?.rawPath || row?.folderName || '-';
const confirmed = window.confirm(`Für RAW-Ordner "${target}" einen neuen Historienjob anlegen?`);
const confirmed = window.confirm(`Für RAW-Ordner "${target}" einen neuen Historienjob anlegen und direkt scannen?`);
if (!confirmed) {
return;
}
@@ -475,12 +478,32 @@ export default function DatabasePage() {
setOrphanImportBusyPath(row.rawPath);
try {
const response = await api.importOrphanRawFolder(row.rawPath);
const newJobId = response?.job?.id;
if (newJobId) {
try {
await api.reencodeJob(newJobId);
toastRef.current?.show({
severity: 'success',
summary: 'Job angelegt & Scan gestartet',
detail: `Historieneintrag #${newJobId} erstellt, Mediainfo-Scan läuft.`,
life: 4000
});
} catch (scanError) {
toastRef.current?.show({
severity: 'info',
summary: 'Job angelegt',
detail: `Historieneintrag #${newJobId} erstellt. Scan konnte nicht automatisch gestartet werden: ${scanError.message}`,
life: 6000
});
}
} else {
toastRef.current?.show({
severity: 'success',
summary: 'Job angelegt',
detail: `Historieneintrag #${response?.job?.id || '-'} wurde erstellt.`,
detail: `Historieneintrag wurde erstellt.`,
life: 3500
});
}
await load();
} catch (error) {
toastRef.current?.show({