0.16.1-5 Ebcode Fix / presets
Deploy Docs to GitHub Pages / Build Documentation (push) Has been cancelled
Deploy Docs to GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled

This commit is contained in:
2026-04-30 12:37:32 +00:00
parent f9cf3cef09
commit 6fbc5395a1
24 changed files with 1131 additions and 784 deletions
+9 -2
View File
@@ -532,6 +532,9 @@ router.post(
'/confirm-encode/:jobId',
asyncHandler(async (req, res) => {
const jobId = Number(req.params.jobId);
const body = req.body && typeof req.body === 'object' ? req.body : {};
const hasSelectedUserPresetId = Object.prototype.hasOwnProperty.call(body, 'selectedUserPresetId');
const hasSelectedHandBrakePreset = Object.prototype.hasOwnProperty.call(body, 'selectedHandBrakePreset');
const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
const selectedEncodeTitleIds = req.body?.selectedEncodeTitleIds ?? null;
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
@@ -541,8 +544,12 @@ router.post(
const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
const selectedHandBrakePreset = req.body?.selectedHandBrakePreset ?? null;
const selectedUserPresetId = hasSelectedUserPresetId
? body.selectedUserPresetId
: undefined;
const selectedHandBrakePreset = hasSelectedHandBrakePreset
? body.selectedHandBrakePreset
: undefined;
logger.info('post:confirm-encode', {
reqId: req.reqId,
jobId,
+67 -46
View File
@@ -8,6 +8,7 @@ const pipelineService = require('../services/pipelineService');
const wsService = require('../services/websocketService');
const hardwareMonitorService = require('../services/hardwareMonitorService');
const userPresetService = require('../services/userPresetService');
const userPresetDefaultsService = require('../services/userPresetDefaultsService');
const activationBytesService = require('../services/activationBytesService');
const diskDetectionService = require('../services/diskDetectionService');
const coverArtRecoveryService = require('../services/coverArtRecoveryService');
@@ -277,6 +278,72 @@ router.post(
})
);
// ── User Presets ──────────────────────────────────────────────────────────────
router.get(
'/user-presets',
asyncHandler(async (req, res) => {
const mediaType = req.query.media_type || null;
logger.debug('get:user-presets', { reqId: req.reqId, mediaType });
const presets = await userPresetService.listPresets(mediaType);
res.json({ presets });
})
);
router.get(
'/user-preset-defaults',
asyncHandler(async (req, res) => {
logger.debug('get:user-preset-defaults', { reqId: req.reqId });
const defaults = await userPresetDefaultsService.listDefaults();
res.json({ defaults });
})
);
router.put(
'/user-preset-defaults',
asyncHandler(async (req, res) => {
const payload = req.body || {};
logger.info('put:user-preset-defaults:update', { reqId: req.reqId });
const defaults = await userPresetDefaultsService.updateDefaults(payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'defaults-updated' });
res.json({ defaults });
})
);
router.post(
'/user-presets',
asyncHandler(async (req, res) => {
const payload = req.body || {};
logger.info('post:user-presets:create', { reqId: req.reqId, name: payload?.name });
const preset = await userPresetService.createPreset(payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'created', id: preset.id });
res.status(201).json({ preset });
})
);
router.put(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
const payload = req.body || {};
logger.info('put:user-presets:update', { reqId: req.reqId, presetId });
const preset = await userPresetService.updatePreset(presetId, payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'updated', id: preset.id });
res.json({ preset });
})
);
router.delete(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
logger.info('delete:user-presets', { reqId: req.reqId, presetId });
const removed = await userPresetService.deletePreset(presetId);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'deleted', id: removed.id });
res.json({ removed });
})
);
router.put(
'/:key',
asyncHandler(async (req, res) => {
@@ -408,52 +475,6 @@ router.put(
})
);
// ── User Presets ──────────────────────────────────────────────────────────────
router.get(
'/user-presets',
asyncHandler(async (req, res) => {
const mediaType = req.query.media_type || null;
logger.debug('get:user-presets', { reqId: req.reqId, mediaType });
const presets = await userPresetService.listPresets(mediaType);
res.json({ presets });
})
);
router.post(
'/user-presets',
asyncHandler(async (req, res) => {
const payload = req.body || {};
logger.info('post:user-presets:create', { reqId: req.reqId, name: payload?.name });
const preset = await userPresetService.createPreset(payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'created', id: preset.id });
res.status(201).json({ preset });
})
);
router.put(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
const payload = req.body || {};
logger.info('put:user-presets:update', { reqId: req.reqId, presetId });
const preset = await userPresetService.updatePreset(presetId, payload);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'updated', id: preset.id });
res.json({ preset });
})
);
router.delete(
'/user-presets/:id',
asyncHandler(async (req, res) => {
const presetId = Number(req.params.id);
logger.info('delete:user-presets', { reqId: req.reqId, presetId });
const removed = await userPresetService.deletePreset(presetId);
wsService.broadcast('USER_PRESETS_UPDATED', { action: 'deleted', id: removed.id });
res.json({ removed });
})
);
router.post(
'/pushover/test',
asyncHandler(async (req, res) => {
+185 -32
View File
@@ -27,6 +27,7 @@ const { buildMediainfoReview, refreshAudioTrackActionsForPlanTitles } = require(
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
const { errorToMeta } = require('../utils/errorMeta');
const userPresetService = require('./userPresetService');
const userPresetDefaultsService = require('./userPresetDefaultsService');
const thumbnailService = require('./thumbnailService');
const activationBytesService = require('./activationBytesService');
const { toBoolean } = require('../utils/validators');
@@ -8529,6 +8530,119 @@ function normalizeUserPresetForPlan(rawPreset) {
};
}
function resolveUserPresetDefaultSlotKey(mediaProfile, isSeriesSelection = false) {
const normalizedProfile = normalizeMediaProfile(mediaProfile);
if (normalizedProfile !== 'dvd' && normalizedProfile !== 'bluray') {
return null;
}
return `${normalizedProfile}_${isSeriesSelection ? 'series' : 'movie'}`;
}
async function applyConfiguredDefaultUserPresetToReviewPlan(reviewPlan, options = {}) {
const plan = reviewPlan && typeof reviewPlan === 'object'
? reviewPlan
: null;
if (!plan) {
return {
plan: reviewPlan,
applied: false,
slotKey: null,
presetId: null,
presetName: null
};
}
const existingUserPreset = normalizeUserPresetForPlan(plan?.userPreset || null);
if (existingUserPreset) {
return {
plan,
applied: false,
slotKey: null,
presetId: existingUserPreset.id || null,
presetName: existingUserPreset.name || null
};
}
const slotKey = resolveUserPresetDefaultSlotKey(options?.mediaProfile || null, Boolean(options?.isSeriesSelection));
if (!slotKey) {
return {
plan,
applied: false,
slotKey: null,
presetId: null,
presetName: null
};
}
try {
const defaultsMap = await userPresetDefaultsService.listDefaults();
const rawPresetId = defaultsMap && typeof defaultsMap === 'object'
? defaultsMap[slotKey]
: null;
const presetId = normalizePositiveInteger(rawPresetId);
if (!presetId) {
return {
plan,
applied: false,
slotKey,
presetId: null,
presetName: null
};
}
const preset = await userPresetService.getPresetById(presetId);
const normalizedPreset = normalizeUserPresetForPlan(preset);
if (!normalizedPreset) {
return {
plan,
applied: false,
slotKey,
presetId,
presetName: null
};
}
const presetMediaType = String(preset?.mediaType || '').trim().toLowerCase();
const normalizedProfile = normalizeMediaProfile(options?.mediaProfile || null);
const mediaTypeAllowed = presetMediaType === 'all'
|| !presetMediaType
|| presetMediaType === normalizedProfile;
if (!mediaTypeAllowed) {
return {
plan,
applied: false,
slotKey,
presetId,
presetName: normalizedPreset.name || null
};
}
return {
plan: {
...plan,
userPreset: normalizedPreset
},
applied: true,
slotKey,
presetId: normalizedPreset.id || presetId || null,
presetName: normalizedPreset.name || null
};
} catch (error) {
logger.warn('review:user-preset-default:resolve-failed', {
mediaProfile: options?.mediaProfile || null,
isSeriesSelection: Boolean(options?.isSeriesSelection),
error: errorToMeta(error)
});
return {
plan,
applied: false,
slotKey,
presetId: null,
presetName: null
};
}
}
function buildScriptDescriptorList(scriptIds, sourceScripts = []) {
const normalizedIds = normalizeScriptIdList(scriptIds);
if (normalizedIds.length === 0) {
@@ -18257,6 +18371,11 @@ class PipelineService extends EventEmitter {
options?.multipartSettingsLock || null
);
review = multipartLockResult.plan;
const reviewDefaultUserPresetResult = await applyConfiguredDefaultUserPresetToReviewPlan(review, {
mediaProfile,
isSeriesSelection: true
});
review = reviewDefaultUserPresetResult.plan;
resolvedSelectedTitleIds = normalizeReviewTitleIdList(review.selectedTitleIds || []);
resolvedPrimaryTitleId = normalizeReviewTitleId(review.encodeInputTitleId)
|| resolvedSelectedTitleIds[0]
@@ -18931,6 +19050,11 @@ class PipelineService extends EventEmitter {
options?.multipartSettingsLock || null
);
review = multipartLockResult.plan;
const reviewDefaultUserPresetResult = await applyConfiguredDefaultUserPresetToReviewPlan(review, {
mediaProfile,
isSeriesSelection: isSeriesDvdMetadataSelection(mediaProfile, selectedMetadata, analyzeContext)
});
review = reviewDefaultUserPresetResult.plan;
if (!Array.isArray(review.titles) || review.titles.length === 0) {
const error = new Error('Titel-/Spurprüfung aus RAW lieferte keine Titel.');
@@ -22014,29 +22138,26 @@ class PipelineService extends EventEmitter {
}
// Resolve user preset: explicit payload wins, otherwise preserve currently selected preset from encode plan.
const hasExplicitUserPresetSelection = !multipartSettingsLocked
&& Object.prototype.hasOwnProperty.call(options || {}, 'selectedUserPresetId');
const hasExplicitHandBrakePresetSelection = !multipartSettingsLocked
&& Object.prototype.hasOwnProperty.call(options || {}, 'selectedHandBrakePreset');
const rawHandBrakePreset = hasExplicitHandBrakePresetSelection
const hasUserPresetIdField = Object.prototype.hasOwnProperty.call(options || {}, 'selectedUserPresetId');
const hasHandBrakePresetField = Object.prototype.hasOwnProperty.call(options || {}, 'selectedHandBrakePreset');
const explicitUserPresetId = hasUserPresetIdField
? normalizePositiveInteger(options?.selectedUserPresetId)
: null;
const rawHandBrakePreset = hasHandBrakePresetField
? String(options?.selectedHandBrakePreset || '').trim()
: '';
const hasExplicitUserPresetSelection = !multipartSettingsLocked && explicitUserPresetId !== null;
const hasExplicitHandBrakePresetSelection = !multipartSettingsLocked && Boolean(rawHandBrakePreset);
let resolvedUserPreset = null;
if (hasExplicitUserPresetSelection) {
const rawUserPresetId = options?.selectedUserPresetId;
const userPresetId = rawUserPresetId !== null && rawUserPresetId !== undefined && String(rawUserPresetId).trim() !== ''
? Number(rawUserPresetId)
: null;
if (Number.isFinite(userPresetId) && userPresetId > 0) {
resolvedUserPreset = await userPresetService.getPresetById(userPresetId);
if (!resolvedUserPreset) {
const error = new Error(`User-Preset ${userPresetId} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
resolvedUserPreset = await userPresetService.getPresetById(explicitUserPresetId);
if (!resolvedUserPreset) {
const error = new Error(`User-Preset ${explicitUserPresetId} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
}
if ((!resolvedUserPreset || !hasExplicitUserPresetSelection) && hasExplicitHandBrakePresetSelection) {
if (!resolvedUserPreset && hasExplicitHandBrakePresetSelection) {
resolvedUserPreset = rawHandBrakePreset
? {
name: `HandBrake: ${rawHandBrakePreset}`,
@@ -22045,8 +22166,10 @@ class PipelineService extends EventEmitter {
}
: null;
}
if (!hasExplicitUserPresetSelection && !hasExplicitHandBrakePresetSelection) {
resolvedUserPreset = normalizeUserPresetForPlan(encodePlan?.userPreset || null);
if (!resolvedUserPreset) {
resolvedUserPreset = normalizeUserPresetForPlan(
planForConfirm?.userPreset || encodePlan?.userPreset || null
);
}
const confirmedPlan = {
@@ -22070,24 +22193,49 @@ class PipelineService extends EventEmitter {
const readyMediaProfile = this.resolveMediaProfileForJob(job, {
encodePlan: confirmedPlan
});
if (readyMediaProfile === 'converter' && !confirmedPlan.userPreset) {
const error = new Error('Bestätigung nicht möglich: Bitte ein User- oder HandBrake-Preset auswählen.');
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const confirmedConverterMediaType = String(
confirmedPlan?.converterMediaType || encodePlan?.converterMediaType || ''
).trim().toLowerCase();
const requiresExplicitPreset = readyMediaProfile === 'dvd'
|| readyMediaProfile === 'bluray'
|| (readyMediaProfile === 'converter' && confirmedConverterMediaType !== 'audio');
const planPresetName = String(
planForConfirm?.userPreset?.handbrakePreset
|| encodePlan?.userPreset?.handbrakePreset
|| planForConfirm?.selectors?.preset
|| encodePlan?.selectors?.preset
|| ''
).trim();
const planExtraArgs = String(
planForConfirm?.userPreset?.extraArgs
|| encodePlan?.userPreset?.extraArgs
|| planForConfirm?.selectors?.extraArgs
|| encodePlan?.selectors?.extraArgs
|| ''
).trim();
const effectivePresetName = String(
resolvedUserPreset?.handbrakePreset
|| rawHandBrakePreset
|| planPresetName
|| confirmSettings?.handbrake_preset
|| ''
).trim();
const effectiveExtraArgs = String(
resolvedUserPreset?.extraArgs
|| planExtraArgs
|| confirmSettings?.handbrake_extra_args
|| ''
).trim();
if (requiresExplicitPreset && !effectivePresetName && !effectiveExtraArgs) {
const error = new Error('Bestätigung nicht möglich: Kein Preset oder Extra-Args gesetzt. Bitte User-/HandBrake-Preset wählen oder passende Settings hinterlegen.');
error.statusCode = 400;
throw error;
}
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
const actionDisplaySettings = {
...(confirmSettings && typeof confirmSettings === 'object' ? confirmSettings : {}),
handbrake_preset: String(
resolvedUserPreset?.handbrakePreset
|| confirmSettings?.handbrake_preset
|| ''
).trim() || null,
handbrake_extra_args: String(
resolvedUserPreset?.extraArgs
|| confirmSettings?.handbrake_extra_args
|| ''
).trim()
handbrake_preset: effectivePresetName || null,
handbrake_extra_args: effectiveExtraArgs
};
confirmedPlan.titles = refreshAudioTrackActionsForPlanTitles(
confirmedPlan.titles,
@@ -23315,6 +23463,11 @@ class PipelineService extends EventEmitter {
options?.multipartSettingsLock || null
);
enrichedReview = multipartLockResult.plan;
const reviewDefaultUserPresetResult = await applyConfiguredDefaultUserPresetToReviewPlan(enrichedReview, {
mediaProfile,
isSeriesSelection: effectiveIsSeriesDvdReview
});
enrichedReview = reviewDefaultUserPresetResult.plan;
const previousEncodePlan = options?.previousEncodePlan && typeof options.previousEncodePlan === 'object'
? options.previousEncodePlan
: null;
@@ -0,0 +1,134 @@
const { getDb } = require('../db/database');
const logger = require('./logger').child('USER_PRESET_DEFAULTS');
const SLOT_KEYS = Object.freeze([
'bluray_movie',
'bluray_series',
'dvd_movie',
'dvd_series'
]);
const SLOT_KEY_SET = new Set(SLOT_KEYS);
const SLOT_ALLOWED_MEDIA_TYPES = Object.freeze({
bluray_movie: new Set(['bluray', 'all']),
bluray_series: new Set(['bluray', 'all']),
dvd_movie: new Set(['dvd', 'all']),
dvd_series: new Set(['dvd', 'all'])
});
function normalizePresetId(value) {
if (value === null || value === undefined) {
return null;
}
if (typeof value === 'string' && value.trim() === '') {
return null;
}
const numeric = Number(value);
if (!Number.isFinite(numeric) || numeric <= 0) {
return null;
}
return Math.trunc(numeric);
}
function buildDefaultsMap(rows = []) {
const map = Object.fromEntries(SLOT_KEYS.map((key) => [key, null]));
for (const row of rows) {
const slotKey = String(row?.slot_key || '').trim();
if (!SLOT_KEY_SET.has(slotKey)) {
continue;
}
const presetId = normalizePresetId(row?.preset_id);
map[slotKey] = presetId;
}
return map;
}
async function listDefaults() {
const db = await getDb();
const rows = await db.all(
`
SELECT slot_key, preset_id
FROM user_preset_defaults
WHERE slot_key IN (${SLOT_KEYS.map(() => '?').join(', ')})
ORDER BY slot_key ASC
`,
SLOT_KEYS
);
return buildDefaultsMap(rows);
}
async function updateDefaults(payload = {}) {
const defaults = payload && typeof payload === 'object'
? (payload.defaults && typeof payload.defaults === 'object' ? payload.defaults : payload)
: {};
const updates = [];
for (const [slotKey, rawPresetId] of Object.entries(defaults)) {
if (!SLOT_KEY_SET.has(slotKey)) {
const error = new Error(`Ungültiger Slot: ${slotKey}`);
error.statusCode = 400;
throw error;
}
updates.push({ slotKey, presetId: normalizePresetId(rawPresetId) });
}
if (updates.length === 0) {
return listDefaults();
}
const db = await getDb();
for (const update of updates) {
if (update.presetId === null) {
continue;
}
const exists = await db.get(
`SELECT id, media_type FROM user_presets WHERE id = ? LIMIT 1`,
[update.presetId]
);
if (!exists) {
const error = new Error(`Preset ${update.presetId} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
const mediaType = String(exists.media_type || '').trim().toLowerCase();
const allowedMediaTypes = SLOT_ALLOWED_MEDIA_TYPES[update.slotKey] || new Set(['all']);
if (!allowedMediaTypes.has(mediaType)) {
const error = new Error(
`Preset ${update.presetId} (${mediaType || 'unbekannt'}) ist für ${update.slotKey} nicht zulässig.`
);
error.statusCode = 400;
throw error;
}
}
await db.exec('BEGIN');
try {
for (const update of updates) {
await db.run(
`
INSERT INTO user_preset_defaults (slot_key, preset_id)
VALUES (?, ?)
ON CONFLICT(slot_key) DO UPDATE SET preset_id = excluded.preset_id
`,
[update.slotKey, update.presetId]
);
}
await db.exec('COMMIT');
} catch (error) {
await db.exec('ROLLBACK');
throw error;
}
logger.info('update', {
updates: updates.map((entry) => ({
slotKey: entry.slotKey,
presetId: entry.presetId
}))
});
return listDefaults();
}
module.exports = {
SLOT_KEYS,
listDefaults,
updateDefaults
};