0.15.0 MovieMerge
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-4",
|
||||
"version": "0.15.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-4",
|
||||
"version": "0.15.0",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-4",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -1015,6 +1015,12 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('ffmpeg_command', 'ffmpeg')`);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('mkvmerge_command', 'Tools', 'mkvmerge Kommando', 'string', 1, 'Pfad oder Befehl für mkvmerge. Wird für Multipart-Movie-Merges genutzt.', 'mkvmerge', '[]', '{"minLength":1}', 2325)`
|
||||
);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mkvmerge_command', 'mkvmerge')`);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('ffprobe_command', 'Tools', 'FFprobe Kommando', 'string', 1, 'Pfad oder Befehl für ffprobe. Wird für Audiobook-Metadaten und Kapitel genutzt.', 'ffprobe', '[]', '{"minLength":1}', 233)`
|
||||
|
||||
@@ -455,6 +455,62 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/multipart-merge/:jobId/reorder',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const orderedSourceJobIds = Array.isArray(req.body?.orderedSourceJobIds)
|
||||
? req.body.orderedSourceJobIds
|
||||
: [];
|
||||
logger.info('post:multipart-merge:reorder', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
orderedCount: orderedSourceJobIds.length
|
||||
});
|
||||
const job = await pipelineService.updateMultipartMergeSourceOrder(jobId, orderedSourceJobIds);
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/multipart-merge/:jobId/preview',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
logger.info('get:multipart-merge:preview', {
|
||||
reqId: req.reqId,
|
||||
jobId
|
||||
});
|
||||
const preview = await pipelineService.getMultipartMergePreview(jobId);
|
||||
res.json({ preview });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/multipart-merge/:containerJobId/restore',
|
||||
asyncHandler(async (req, res) => {
|
||||
const containerJobId = Number(req.params.containerJobId);
|
||||
logger.info('post:multipart-merge:restore', {
|
||||
reqId: req.reqId,
|
||||
containerJobId
|
||||
});
|
||||
const job = await pipelineService.restoreMultipartMergeJobForContainer(containerJobId);
|
||||
await pipelineService.emitQueueChanged().catch((error) => {
|
||||
logger.warn('post:multipart-merge:restore:queue-emit-failed', {
|
||||
reqId: req.reqId,
|
||||
containerJobId,
|
||||
error: error?.message || String(error)
|
||||
});
|
||||
});
|
||||
res.json({
|
||||
result: {
|
||||
restored: true,
|
||||
mergeJobId: Number(job?.id || 0) || null
|
||||
},
|
||||
job
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/confirm-encode/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
|
||||
@@ -394,6 +394,7 @@ function normalizeJobKindValue(value) {
|
||||
|| raw === 'dvd_series_child'
|
||||
|| raw === 'multipart_movie_container'
|
||||
|| raw === 'multipart_movie_child'
|
||||
|| raw === 'multipart_movie_merge'
|
||||
) {
|
||||
return raw;
|
||||
}
|
||||
@@ -2474,10 +2475,37 @@ function isSeriesContainerRow(row) {
|
||||
return String(row?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||
}
|
||||
|
||||
function isMultipartContainerRow(row) {
|
||||
return String(row?.job_kind || '').trim().toLowerCase() === 'multipart_movie_container';
|
||||
}
|
||||
|
||||
function isDiskContainerRow(row) {
|
||||
return isSeriesContainerRow(row) || isMultipartContainerRow(row);
|
||||
}
|
||||
|
||||
function isSeriesChildRow(row) {
|
||||
return String(row?.job_kind || '').trim().toLowerCase() === 'dvd_series_child';
|
||||
}
|
||||
|
||||
function isMultipartChildRow(row) {
|
||||
return String(row?.job_kind || '').trim().toLowerCase() === 'multipart_movie_child';
|
||||
}
|
||||
|
||||
function isMultipartMergeRow(row) {
|
||||
const jobKind = String(row?.job_kind || '').trim().toLowerCase();
|
||||
if (jobKind === 'multipart_movie_merge') {
|
||||
return true;
|
||||
}
|
||||
const encodePlan = parseInfoFromValue(row?.encodePlan ?? row?.encode_plan_json, null);
|
||||
const handbrakeInfo = parseInfoFromValue(row?.handbrakeInfo ?? row?.handbrake_info_json, null);
|
||||
const planJobKind = String(encodePlan?.jobKind || '').trim().toLowerCase();
|
||||
if (planJobKind === 'multipart_movie_merge') {
|
||||
return true;
|
||||
}
|
||||
const mode = String(encodePlan?.mode || handbrakeInfo?.mode || '').trim().toLowerCase();
|
||||
return mode === 'multipart_merge';
|
||||
}
|
||||
|
||||
function normalizeArchiveTarget(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (raw === 'raw') {
|
||||
@@ -3866,7 +3894,7 @@ class HistoryService {
|
||||
return job;
|
||||
});
|
||||
|
||||
const containerJobs = adjustedJobs.filter((job) => String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container');
|
||||
const containerJobs = adjustedJobs.filter((job) => isDiskContainerRow(job));
|
||||
const containerIds = containerJobs.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
|
||||
const seriesCandidates = adjustedJobs.filter((job) => {
|
||||
const mkInfo = parseJsonSafe(job?.makemkv_info_json, {});
|
||||
@@ -3983,13 +4011,20 @@ class HistoryService {
|
||||
if (!containerId) {
|
||||
continue;
|
||||
}
|
||||
const containerKind = String(container?.job_kind || '').trim().toLowerCase();
|
||||
const isMultipartContainer = containerKind === 'multipart_movie_container';
|
||||
const mkInfo = parseJsonSafe(container?.makemkv_info_json, {});
|
||||
const selectedMetadata = mkInfo?.analyzeContext?.selectedMetadata || mkInfo?.selectedMetadata || {};
|
||||
const episodeCount = Number(selectedMetadata?.episodeCount || 0);
|
||||
const episodesLength = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes.length : 0;
|
||||
const expectedTotal = episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0);
|
||||
const containerChildRows = directDiskRowsByContainerId.get(containerId) || [];
|
||||
const containerChildRowsRaw = directDiskRowsByContainerId.get(containerId) || [];
|
||||
const containerChildRows = isMultipartContainer
|
||||
? containerChildRowsRaw.filter((row) => !isMultipartMergeRow(row))
|
||||
: containerChildRowsRaw;
|
||||
const containerEpisodeRows = directEpisodeRowsByContainerId.get(containerId) || [];
|
||||
const expectedTotal = isMultipartContainer
|
||||
? containerChildRows.length
|
||||
: (episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0));
|
||||
const childIds = containerChildRows
|
||||
.map((row) => normalizeJobIdValue(row?.id))
|
||||
.filter(Boolean);
|
||||
@@ -4127,9 +4162,90 @@ class HistoryService {
|
||||
encodeSuccessCount = Math.min(1, totalChildren);
|
||||
}
|
||||
}
|
||||
const expectedFinal = expectedFromPlans > 0
|
||||
? expectedFromPlans
|
||||
: (expectedTotal > 0 ? expectedTotal : existingCount);
|
||||
let mergeSummary = null;
|
||||
if (isMultipartContainer) {
|
||||
const mergeRows = containerChildRowsRaw.filter((row) => isMultipartMergeRow(row));
|
||||
let mergeJobId = null;
|
||||
let mergeActive = false;
|
||||
let mergeCompleted = false;
|
||||
let mergeOutputExists = false;
|
||||
for (const mergeRow of mergeRows) {
|
||||
const mergeRowId = normalizeJobIdValue(mergeRow?.id);
|
||||
if (mergeRowId && (!mergeJobId || mergeRowId > mergeJobId)) {
|
||||
mergeJobId = mergeRowId;
|
||||
}
|
||||
const mergeStatus = String(mergeRow?.status || mergeRow?.last_state || '').trim().toUpperCase();
|
||||
if (mergeStatus === 'ENCODING') {
|
||||
mergeActive = true;
|
||||
}
|
||||
const outputCandidates = new Set();
|
||||
const directOutputPath = String(mergeRow?.output_path || '').trim();
|
||||
if (directOutputPath) {
|
||||
outputCandidates.add(directOutputPath);
|
||||
}
|
||||
if (mergeRowId) {
|
||||
const linkedOutputs = Array.from(childOutputMap.get(mergeRowId) || []);
|
||||
for (const outputPath of linkedOutputs) {
|
||||
if (outputPath) {
|
||||
outputCandidates.add(outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
let rowOutputExists = false;
|
||||
for (const outputPath of outputCandidates) {
|
||||
if (!outputPath) {
|
||||
continue;
|
||||
}
|
||||
if (!includeFsChecks || fs.existsSync(outputPath)) {
|
||||
rowOutputExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rowOutputExists) {
|
||||
mergeOutputExists = true;
|
||||
}
|
||||
const mergeHbInfo = parseJsonSafe(mergeRow?.handbrake_info_json, null);
|
||||
const mergeHbStatus = String(mergeHbInfo?.status || '').trim().toUpperCase();
|
||||
if (mergeStatus === 'FINISHED' || mergeHbStatus === 'SUCCESS' || rowOutputExists) {
|
||||
mergeCompleted = true;
|
||||
}
|
||||
}
|
||||
const mergeInputExpected = totalChildren;
|
||||
const mergeInputReady = mergeInputExpected > 0
|
||||
? Math.min(existingCount, mergeInputExpected)
|
||||
: 0;
|
||||
const mergeReady = mergeInputExpected >= 2 && mergeInputReady >= mergeInputExpected;
|
||||
const mergeMissingInputs = mergeInputExpected > mergeInputReady
|
||||
? (mergeInputExpected - mergeInputReady)
|
||||
: 0;
|
||||
let mergeState = 'missing';
|
||||
if (mergeActive) {
|
||||
mergeState = 'active';
|
||||
} else if (mergeCompleted) {
|
||||
mergeState = 'done';
|
||||
} else if (mergeReady) {
|
||||
mergeState = mergeRows.length > 0 ? 'ready' : 'restorable';
|
||||
} else if (mergeRows.length > 0) {
|
||||
mergeState = 'blocked';
|
||||
}
|
||||
mergeSummary = {
|
||||
hasJob: mergeRows.length > 0,
|
||||
jobId: mergeJobId,
|
||||
active: mergeActive,
|
||||
completed: mergeCompleted,
|
||||
ready: mergeReady,
|
||||
outputExists: mergeOutputExists,
|
||||
inputReady: mergeInputReady,
|
||||
inputExpected: mergeInputExpected,
|
||||
missingInputs: mergeMissingInputs,
|
||||
state: mergeState
|
||||
};
|
||||
}
|
||||
const expectedFinal = isMultipartContainer
|
||||
? (totalChildren > 0 ? totalChildren : existingCount)
|
||||
: (expectedFromPlans > 0
|
||||
? expectedFromPlans
|
||||
: (expectedTotal > 0 ? expectedTotal : existingCount));
|
||||
containerOutputSummary.set(containerId, {
|
||||
existing: existingCount,
|
||||
expected: expectedFinal
|
||||
@@ -4139,7 +4255,8 @@ class HistoryService {
|
||||
containerChildSummary.set(containerId, {
|
||||
raw: { existing: rawExistsCount, expected: totalChildren },
|
||||
backup: { existing: backupSuccessCount, expected: totalChildren },
|
||||
encode: { existing: encodeSuccessCount, expected: totalChildren }
|
||||
encode: { existing: encodeSuccessCount, expected: totalChildren },
|
||||
...(mergeSummary ? { merge: mergeSummary } : {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4586,6 +4703,8 @@ class HistoryService {
|
||||
childRows.map((row) => this.repairImportedOrphanJobClassification(row, settings))
|
||||
);
|
||||
const isSeriesContainer = String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||
const isMultipartContainer = String(job?.job_kind || '').trim().toLowerCase() === 'multipart_movie_container';
|
||||
const isDiskContainer = isSeriesContainer || isMultipartContainer;
|
||||
const detailChildJobs = isSeriesContainer
|
||||
? childJobs.filter((child) => !isSeriesBatchEpisodeSubJobRow(child))
|
||||
: childJobs;
|
||||
@@ -4636,12 +4755,90 @@ class HistoryService {
|
||||
);
|
||||
|
||||
let seriesChildSummary = null;
|
||||
if (isSeriesContainer && enrichedChildren.length > 0) {
|
||||
const total = enrichedChildren.length;
|
||||
if (isDiskContainer && enrichedChildren.length > 0) {
|
||||
const summaryChildren = isMultipartContainer
|
||||
? enrichedChildren.filter((child) => !isMultipartMergeRow(child))
|
||||
: enrichedChildren;
|
||||
const mergeChildren = isMultipartContainer
|
||||
? enrichedChildren.filter((child) => isMultipartMergeRow(child))
|
||||
: [];
|
||||
const total = summaryChildren.length;
|
||||
const outputReadyCount = summaryChildren.reduce((count, child) => (
|
||||
count + (child?.outputStatus?.exists ? 1 : 0)
|
||||
), 0);
|
||||
const buildMergeSummary = () => {
|
||||
if (!isMultipartContainer) {
|
||||
return null;
|
||||
}
|
||||
let mergeJobId = null;
|
||||
let mergeActive = false;
|
||||
let mergeCompleted = false;
|
||||
let mergeOutputExists = false;
|
||||
for (const child of mergeChildren) {
|
||||
const mergeRowId = normalizeJobIdValue(child?.id);
|
||||
if (mergeRowId && (!mergeJobId || mergeRowId > mergeJobId)) {
|
||||
mergeJobId = mergeRowId;
|
||||
}
|
||||
const mergeStatus = String(child?.status || child?.last_state || '').trim().toUpperCase();
|
||||
if (mergeStatus === 'ENCODING') {
|
||||
mergeActive = true;
|
||||
}
|
||||
const hasOutput = Boolean(child?.outputStatus?.exists || String(child?.output_path || '').trim());
|
||||
if (hasOutput) {
|
||||
mergeOutputExists = true;
|
||||
}
|
||||
if (
|
||||
mergeStatus === 'FINISHED'
|
||||
|| child?.encodeSuccess
|
||||
|| hasOutput
|
||||
|| String(child?.handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS'
|
||||
) {
|
||||
mergeCompleted = true;
|
||||
}
|
||||
}
|
||||
const mergeInputExpected = total;
|
||||
const mergeInputReady = mergeInputExpected > 0
|
||||
? Math.min(outputReadyCount, mergeInputExpected)
|
||||
: 0;
|
||||
const mergeReady = mergeInputExpected >= 2 && mergeInputReady >= mergeInputExpected;
|
||||
const mergeMissingInputs = mergeInputExpected > mergeInputReady
|
||||
? (mergeInputExpected - mergeInputReady)
|
||||
: 0;
|
||||
let mergeState = 'missing';
|
||||
if (mergeActive) {
|
||||
mergeState = 'active';
|
||||
} else if (mergeCompleted) {
|
||||
mergeState = 'done';
|
||||
} else if (mergeReady) {
|
||||
mergeState = mergeChildren.length > 0 ? 'ready' : 'restorable';
|
||||
} else if (mergeChildren.length > 0) {
|
||||
mergeState = 'blocked';
|
||||
}
|
||||
return {
|
||||
hasJob: mergeChildren.length > 0,
|
||||
jobId: mergeJobId,
|
||||
active: mergeActive,
|
||||
completed: mergeCompleted,
|
||||
ready: mergeReady,
|
||||
outputExists: mergeOutputExists,
|
||||
inputReady: mergeInputReady,
|
||||
inputExpected: mergeInputExpected,
|
||||
missingInputs: mergeMissingInputs,
|
||||
state: mergeState
|
||||
};
|
||||
};
|
||||
if (total <= 0) {
|
||||
seriesChildSummary = {
|
||||
raw: { existing: 0, expected: 0 },
|
||||
backup: { existing: 0, expected: 0 },
|
||||
encode: { existing: 0, expected: 0 },
|
||||
...(isMultipartContainer ? { merge: buildMergeSummary() } : {})
|
||||
};
|
||||
}
|
||||
let rawCount = 0;
|
||||
let backupCount = 0;
|
||||
let encodeCount = 0;
|
||||
for (const child of enrichedChildren) {
|
||||
for (const child of summaryChildren) {
|
||||
if (child?.rawStatus?.exists) {
|
||||
rawCount += 1;
|
||||
}
|
||||
@@ -4659,11 +4856,14 @@ class HistoryService {
|
||||
encodeCount += 1;
|
||||
}
|
||||
}
|
||||
seriesChildSummary = {
|
||||
raw: { existing: rawCount, expected: total },
|
||||
backup: { existing: backupCount, expected: total },
|
||||
encode: { existing: encodeCount, expected: total }
|
||||
};
|
||||
if (total > 0) {
|
||||
seriesChildSummary = {
|
||||
raw: { existing: rawCount, expected: total },
|
||||
backup: { existing: backupCount, expected: total },
|
||||
encode: { existing: encodeCount, expected: total },
|
||||
...(isMultipartContainer ? { merge: buildMergeSummary() } : {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const outputFolders = await this.getJobOutputFoldersForLineage(jobId);
|
||||
@@ -5856,7 +6056,13 @@ class HistoryService {
|
||||
: null;
|
||||
const isPrimarySeriesChild = isSeriesChildRow(primary)
|
||||
|| (parentRow && isSeriesContainerRow(parentRow));
|
||||
const isPrimaryMultipartChild = isMultipartChildRow(primary)
|
||||
|| isMultipartMergeRow(primary)
|
||||
|| (parentRow && isMultipartContainerRow(parentRow));
|
||||
const isPrimarySeriesContainer = isSeriesContainerRow(primary);
|
||||
const isPrimaryMultipartContainer = isMultipartContainerRow(primary);
|
||||
const isPrimaryScopedChild = isPrimarySeriesChild || isPrimaryMultipartChild;
|
||||
const isPrimaryDiskContainer = isPrimarySeriesContainer || isPrimaryMultipartContainer;
|
||||
const enqueue = (value) => {
|
||||
const id = normalizeJobIdValue(value);
|
||||
if (!id || visited.has(id)) {
|
||||
@@ -5877,7 +6083,7 @@ class HistoryService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(isPrimarySeriesChild || isPrimarySeriesContainer)) {
|
||||
if (!(isPrimaryScopedChild || isPrimaryDiskContainer)) {
|
||||
enqueue(row.parent_job_id);
|
||||
enqueue(parseSourceJobIdFromPlan(row.encode_plan_json));
|
||||
}
|
||||
@@ -5889,7 +6095,7 @@ class HistoryService {
|
||||
enqueue(childId);
|
||||
}
|
||||
|
||||
if (includeLogLinks && !(isPrimarySeriesChild || isPrimarySeriesContainer)) {
|
||||
if (includeLogLinks && !(isPrimaryScopedChild || isPrimaryDiskContainer)) {
|
||||
try {
|
||||
const processLog = await this.readProcessLogLines(currentId, { includeAll: true });
|
||||
const linkedJobIds = parseRetryLinkedJobIdsFromLogLines(processLog.lines);
|
||||
@@ -5902,7 +6108,7 @@ class HistoryService {
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrimarySeriesChild && parentRow && isSeriesContainerRow(parentRow)) {
|
||||
if (isPrimaryScopedChild && parentRow && isDiskContainerRow(parentRow)) {
|
||||
const parentId = normalizeJobIdValue(parentRow?.id);
|
||||
if (parentId) {
|
||||
const remainingChildren = rows.filter((row) => {
|
||||
@@ -7226,11 +7432,11 @@ class HistoryService {
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
if (isSeriesContainerRow(existing)) {
|
||||
if (isDiskContainerRow(existing)) {
|
||||
const containerChildren = await this.listChildJobs(normalizedJobId);
|
||||
if (Array.isArray(containerChildren) && containerChildren.length > 0) {
|
||||
const error = new Error(
|
||||
'Serien-Container kann nicht einzeln gelöscht werden, solange noch Child-Jobs vorhanden sind.'
|
||||
'Container kann nicht einzeln gelöscht werden, solange noch Child-Jobs vorhanden sind.'
|
||||
);
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
@@ -7385,7 +7591,7 @@ class HistoryService {
|
||||
// other children afterwards.
|
||||
for (const candidateId of [...deleteJobIds]) {
|
||||
const row = rowById.get(candidateId);
|
||||
if (!row || !isSeriesContainerRow(row)) {
|
||||
if (!row || !isDiskContainerRow(row)) {
|
||||
continue;
|
||||
}
|
||||
const childRows = await db.all(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,15 @@ const SENSITIVE_SETTING_KEYS = new Set([
|
||||
'pushover_token',
|
||||
'pushover_user'
|
||||
]);
|
||||
const IMMUTABLE_SETTING_KEYS = new Set([
|
||||
'makemkv_command',
|
||||
'mediainfo_command',
|
||||
'handbrake_command',
|
||||
'ffmpeg_command',
|
||||
'ffprobe_command',
|
||||
'cdparanoia_command',
|
||||
'mkvmerge_command'
|
||||
]);
|
||||
const AUDIO_SELECTION_KEYS_WITH_VALUE = new Set(['-a', '--audio', '--audio-lang-list']);
|
||||
const AUDIO_SELECTION_KEYS_FLAG_ONLY = new Set(['--all-audio', '--first-audio']);
|
||||
const SUBTITLE_SELECTION_KEYS_WITH_VALUE = new Set(['-s', '--subtitle', '--subtitle-lang-list']);
|
||||
@@ -159,6 +168,10 @@ function applyRuntimeLogDirSetting(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function isImmutableSettingKey(key) {
|
||||
return IMMUTABLE_SETTING_KEYS.has(String(key || '').trim().toLowerCase());
|
||||
}
|
||||
|
||||
function normalizeTrackIds(rawList) {
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
const seen = new Set();
|
||||
@@ -961,6 +974,12 @@ class SettingsService {
|
||||
}
|
||||
|
||||
async setSettingValue(key, rawValue) {
|
||||
if (isImmutableSettingKey(key)) {
|
||||
const error = new Error(`Setting ${key} ist schreibgeschützt und kann nicht geändert werden.`);
|
||||
error.statusCode = 403;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const schema = await db.get('SELECT * FROM settings_schema WHERE key = ?', [key]);
|
||||
if (!schema) {
|
||||
@@ -1022,6 +1041,14 @@ class SettingsService {
|
||||
const validationErrors = [];
|
||||
|
||||
for (const [key, rawValue] of entries) {
|
||||
if (isImmutableSettingKey(key)) {
|
||||
validationErrors.push({
|
||||
key,
|
||||
message: 'Dieses Setting ist schreibgeschützt und kann nicht geändert werden.'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const schema = schemaByKey.get(key);
|
||||
if (!schema) {
|
||||
const error = new Error(`Setting ${key} existiert nicht.`);
|
||||
@@ -1047,7 +1074,9 @@ class SettingsService {
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const error = new Error('Mindestens ein Setting ist ungültig.');
|
||||
error.statusCode = 400;
|
||||
error.statusCode = validationErrors.some((item) => String(item?.message || '').includes('schreibgeschützt'))
|
||||
? 403
|
||||
: 400;
|
||||
error.details = validationErrors;
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user