HandBrake

This commit is contained in:
2026-03-09 20:37:56 +00:00
parent 1b07fa4f14
commit 4c879d2485
22 changed files with 1590 additions and 773 deletions

View File

@@ -100,12 +100,14 @@ router.post(
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
logger.info('post:confirm-encode', {
reqId: req.reqId,
jobId,
selectedEncodeTitleId,
selectedTrackSelectionProvided: Boolean(selectedTrackSelection),
skipPipelineStateUpdate,
selectedUserPresetId,
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
? selectedPostEncodeScriptIds.length
: 0
@@ -114,7 +116,8 @@ router.post(
selectedEncodeTitleId,
selectedTrackSelection,
selectedPostEncodeScriptIds,
skipPipelineStateUpdate
skipPipelineStateUpdate,
selectedUserPresetId
});
res.json({ job });
})

View File

@@ -7,6 +7,7 @@ const notificationService = require('../services/notificationService');
const pipelineService = require('../services/pipelineService');
const wsService = require('../services/websocketService');
const hardwareMonitorService = require('../services/hardwareMonitorService');
const userPresetService = require('../services/userPresetService');
const logger = require('../services/logger').child('SETTINGS_ROUTE');
const router = express.Router();
@@ -304,6 +305,52 @@ 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) => {

View File

@@ -17,6 +17,7 @@ const { ensureDir, sanitizeFileName, renderTemplate, findMediaFiles } = require(
const { buildMediainfoReview } = require('../utils/encodePlan');
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
const { errorToMeta } = require('../utils/errorMeta');
const userPresetService = require('./userPresetService');
const RUNNING_STATES = new Set(['ANALYZING', 'RIPPING', 'ENCODING', 'MEDIAINFO_CHECK']);
const REVIEW_REFRESH_SETTING_PREFIXES = [
@@ -5583,6 +5584,21 @@ class PipelineService extends EventEmitter {
throw error;
}
// Resolve user preset if provided
const rawUserPresetId = options?.selectedUserPresetId ?? null;
const userPresetId = rawUserPresetId !== null && rawUserPresetId !== undefined
? Number(rawUserPresetId)
: null;
let resolvedUserPreset = 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;
}
}
const confirmedPlan = {
...planForConfirm,
postEncodeScriptIds: selectedPostEncodeScripts.map((item) => Number(item.id)),
@@ -5598,7 +5614,15 @@ class PipelineService extends EventEmitter {
postEncodeChainIds: selectedPostEncodeChainIds,
preEncodeChainIds: selectedPreEncodeChainIds,
reviewConfirmed: true,
reviewConfirmedAt: nowIso()
reviewConfirmedAt: nowIso(),
userPreset: resolvedUserPreset
? {
id: resolvedUserPreset.id,
name: resolvedUserPreset.name,
handbrakePreset: resolvedUserPreset.handbrakePreset,
extraArgs: resolvedUserPreset.extraArgs
}
: null
};
const inputPath = isPreRipMode
? null
@@ -5622,6 +5646,7 @@ class PipelineService extends EventEmitter {
+ ` Pre-Encode-Ketten: ${selectedPreEncodeChainIds.length > 0 ? selectedPreEncodeChainIds.join(',') : 'none'}.`
+ ` Post-Encode-Scripte: ${selectedPostEncodeScripts.length > 0 ? selectedPostEncodeScripts.map((item) => item.name).join(' -> ') : 'none'}.`
+ ` Post-Encode-Ketten: ${selectedPostEncodeChainIds.length > 0 ? selectedPostEncodeChainIds.join(',') : 'none'}.`
+ (resolvedUserPreset ? ` User-Preset: "${resolvedUserPreset.name}" (ID ${resolvedUserPreset.id}).` : '')
);
if (!skipPipelineStateUpdate) {
@@ -6683,7 +6708,8 @@ class PipelineService extends EventEmitter {
trackSelection,
titleId: handBrakeTitleId,
mediaProfile,
settingsMap: settings
settingsMap: settings,
userPreset: encodePlan?.userPreset || null
});
if (trackSelection) {
await historyService.appendLog(

View File

@@ -847,10 +847,20 @@ class SettingsService {
if (selectedTitleId !== null) {
baseArgs.push('-t', String(selectedTitleId));
}
if (map.handbrake_preset) {
baseArgs.push('-Z', map.handbrake_preset);
// User preset overrides settings-derived preset and extra args
const userPreset = options?.userPreset || null;
const effectiveHandbrakePreset = userPreset !== null
? (userPreset.handbrakePreset || null)
: (map.handbrake_preset || null);
const effectiveExtraArgs = userPreset !== null
? (userPreset.extraArgs || '')
: (map.handbrake_extra_args || '');
if (effectiveHandbrakePreset) {
baseArgs.push('-Z', effectiveHandbrakePreset);
}
const extra = splitArgs(map.handbrake_extra_args);
const extra = splitArgs(effectiveExtraArgs);
const rawSelection = options?.trackSelection || null;
const hasSelection = rawSelection && typeof rawSelection === 'object';
@@ -860,7 +870,8 @@ class SettingsService {
args: [...baseArgs, ...extra],
inputFile,
outputFile,
selectedTitleId
selectedTitleId,
userPresetId: userPreset?.id || null
});
return { cmd, args: [...baseArgs, ...extra] };
}

View File

@@ -0,0 +1,133 @@
const { getDb } = require('../db/database');
const logger = require('./logger').child('USER_PRESET');
const VALID_MEDIA_TYPES = new Set(['bluray', 'dvd', 'other', 'all']);
function normalizeMediaType(value) {
const v = String(value || '').trim().toLowerCase();
return VALID_MEDIA_TYPES.has(v) ? v : 'all';
}
function rowToPreset(row) {
if (!row) {
return null;
}
return {
id: row.id,
name: row.name,
mediaType: row.media_type,
handbrakePreset: row.handbrake_preset || null,
extraArgs: row.extra_args || null,
description: row.description || null,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
async function listPresets(mediaType = null) {
const db = await getDb();
let rows;
if (mediaType && VALID_MEDIA_TYPES.has(mediaType)) {
rows = await db.all(
`SELECT * FROM user_presets WHERE media_type = ? OR media_type = 'all' ORDER BY name ASC`,
[mediaType]
);
} else {
rows = await db.all(`SELECT * FROM user_presets ORDER BY media_type ASC, name ASC`);
}
return rows.map(rowToPreset);
}
async function getPresetById(id) {
const db = await getDb();
const row = await db.get(`SELECT * FROM user_presets WHERE id = ? LIMIT 1`, [id]);
return rowToPreset(row);
}
async function createPreset(payload) {
const name = String(payload?.name || '').trim();
if (!name) {
const error = new Error('Preset-Name darf nicht leer sein.');
error.statusCode = 400;
throw error;
}
const mediaType = normalizeMediaType(payload?.mediaType);
const handbrakePreset = String(payload?.handbrakePreset || '').trim() || null;
const extraArgs = String(payload?.extraArgs || '').trim() || null;
const description = String(payload?.description || '').trim() || null;
const db = await getDb();
const result = await db.run(
`INSERT INTO user_presets (name, media_type, handbrake_preset, extra_args, description)
VALUES (?, ?, ?, ?, ?)`,
[name, mediaType, handbrakePreset, extraArgs, description]
);
const preset = await getPresetById(result.lastID);
logger.info('create', { id: preset.id, name: preset.name, mediaType: preset.mediaType });
return preset;
}
async function updatePreset(id, payload) {
const db = await getDb();
const existing = await getPresetById(id);
if (!existing) {
const error = new Error(`Preset ${id} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
const name = payload?.name !== undefined ? String(payload.name || '').trim() : existing.name;
if (!name) {
const error = new Error('Preset-Name darf nicht leer sein.');
error.statusCode = 400;
throw error;
}
const mediaType = payload?.mediaType !== undefined
? normalizeMediaType(payload.mediaType)
: existing.mediaType;
const handbrakePreset = payload?.handbrakePreset !== undefined
? (String(payload.handbrakePreset || '').trim() || null)
: existing.handbrakePreset;
const extraArgs = payload?.extraArgs !== undefined
? (String(payload.extraArgs || '').trim() || null)
: existing.extraArgs;
const description = payload?.description !== undefined
? (String(payload.description || '').trim() || null)
: existing.description;
await db.run(
`UPDATE user_presets
SET name = ?, media_type = ?, handbrake_preset = ?, extra_args = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
[name, mediaType, handbrakePreset, extraArgs, description, id]
);
const updated = await getPresetById(id);
logger.info('update', { id: updated.id, name: updated.name });
return updated;
}
async function deletePreset(id) {
const db = await getDb();
const existing = await getPresetById(id);
if (!existing) {
const error = new Error(`Preset ${id} nicht gefunden.`);
error.statusCode = 404;
throw error;
}
await db.run(`DELETE FROM user_presets WHERE id = ?`, [id]);
logger.info('delete', { id: existing.id, name: existing.name });
return existing;
}
module.exports = {
listPresets,
getPresetById,
createPreset,
updatePreset,
deletePreset
};