0.16.1-5 Ebcode Fix / presets
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -532,6 +532,9 @@ router.post(
|
|||||||
'/confirm-encode/:jobId',
|
'/confirm-encode/:jobId',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
const jobId = Number(req.params.jobId);
|
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 selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
|
||||||
const selectedEncodeTitleIds = req.body?.selectedEncodeTitleIds ?? null;
|
const selectedEncodeTitleIds = req.body?.selectedEncodeTitleIds ?? null;
|
||||||
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
|
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
|
||||||
@@ -541,8 +544,12 @@ router.post(
|
|||||||
const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
|
const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
|
||||||
const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
|
const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
|
||||||
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
|
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
|
||||||
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
|
const selectedUserPresetId = hasSelectedUserPresetId
|
||||||
const selectedHandBrakePreset = req.body?.selectedHandBrakePreset ?? null;
|
? body.selectedUserPresetId
|
||||||
|
: undefined;
|
||||||
|
const selectedHandBrakePreset = hasSelectedHandBrakePreset
|
||||||
|
? body.selectedHandBrakePreset
|
||||||
|
: undefined;
|
||||||
logger.info('post:confirm-encode', {
|
logger.info('post:confirm-encode', {
|
||||||
reqId: req.reqId,
|
reqId: req.reqId,
|
||||||
jobId,
|
jobId,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const pipelineService = require('../services/pipelineService');
|
|||||||
const wsService = require('../services/websocketService');
|
const wsService = require('../services/websocketService');
|
||||||
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
||||||
const userPresetService = require('../services/userPresetService');
|
const userPresetService = require('../services/userPresetService');
|
||||||
|
const userPresetDefaultsService = require('../services/userPresetDefaultsService');
|
||||||
const activationBytesService = require('../services/activationBytesService');
|
const activationBytesService = require('../services/activationBytesService');
|
||||||
const diskDetectionService = require('../services/diskDetectionService');
|
const diskDetectionService = require('../services/diskDetectionService');
|
||||||
const coverArtRecoveryService = require('../services/coverArtRecoveryService');
|
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(
|
router.put(
|
||||||
'/:key',
|
'/:key',
|
||||||
asyncHandler(async (req, res) => {
|
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(
|
router.post(
|
||||||
'/pushover/test',
|
'/pushover/test',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const { buildMediainfoReview, refreshAudioTrackActionsForPlanTitles } = require(
|
|||||||
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
|
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
|
||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
const userPresetService = require('./userPresetService');
|
const userPresetService = require('./userPresetService');
|
||||||
|
const userPresetDefaultsService = require('./userPresetDefaultsService');
|
||||||
const thumbnailService = require('./thumbnailService');
|
const thumbnailService = require('./thumbnailService');
|
||||||
const activationBytesService = require('./activationBytesService');
|
const activationBytesService = require('./activationBytesService');
|
||||||
const { toBoolean } = require('../utils/validators');
|
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 = []) {
|
function buildScriptDescriptorList(scriptIds, sourceScripts = []) {
|
||||||
const normalizedIds = normalizeScriptIdList(scriptIds);
|
const normalizedIds = normalizeScriptIdList(scriptIds);
|
||||||
if (normalizedIds.length === 0) {
|
if (normalizedIds.length === 0) {
|
||||||
@@ -18257,6 +18371,11 @@ class PipelineService extends EventEmitter {
|
|||||||
options?.multipartSettingsLock || null
|
options?.multipartSettingsLock || null
|
||||||
);
|
);
|
||||||
review = multipartLockResult.plan;
|
review = multipartLockResult.plan;
|
||||||
|
const reviewDefaultUserPresetResult = await applyConfiguredDefaultUserPresetToReviewPlan(review, {
|
||||||
|
mediaProfile,
|
||||||
|
isSeriesSelection: true
|
||||||
|
});
|
||||||
|
review = reviewDefaultUserPresetResult.plan;
|
||||||
resolvedSelectedTitleIds = normalizeReviewTitleIdList(review.selectedTitleIds || []);
|
resolvedSelectedTitleIds = normalizeReviewTitleIdList(review.selectedTitleIds || []);
|
||||||
resolvedPrimaryTitleId = normalizeReviewTitleId(review.encodeInputTitleId)
|
resolvedPrimaryTitleId = normalizeReviewTitleId(review.encodeInputTitleId)
|
||||||
|| resolvedSelectedTitleIds[0]
|
|| resolvedSelectedTitleIds[0]
|
||||||
@@ -18931,6 +19050,11 @@ class PipelineService extends EventEmitter {
|
|||||||
options?.multipartSettingsLock || null
|
options?.multipartSettingsLock || null
|
||||||
);
|
);
|
||||||
review = multipartLockResult.plan;
|
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) {
|
if (!Array.isArray(review.titles) || review.titles.length === 0) {
|
||||||
const error = new Error('Titel-/Spurprüfung aus RAW lieferte keine Titel.');
|
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.
|
// Resolve user preset: explicit payload wins, otherwise preserve currently selected preset from encode plan.
|
||||||
const hasExplicitUserPresetSelection = !multipartSettingsLocked
|
const hasUserPresetIdField = Object.prototype.hasOwnProperty.call(options || {}, 'selectedUserPresetId');
|
||||||
&& Object.prototype.hasOwnProperty.call(options || {}, 'selectedUserPresetId');
|
const hasHandBrakePresetField = Object.prototype.hasOwnProperty.call(options || {}, 'selectedHandBrakePreset');
|
||||||
const hasExplicitHandBrakePresetSelection = !multipartSettingsLocked
|
const explicitUserPresetId = hasUserPresetIdField
|
||||||
&& Object.prototype.hasOwnProperty.call(options || {}, 'selectedHandBrakePreset');
|
? normalizePositiveInteger(options?.selectedUserPresetId)
|
||||||
const rawHandBrakePreset = hasExplicitHandBrakePresetSelection
|
: null;
|
||||||
|
const rawHandBrakePreset = hasHandBrakePresetField
|
||||||
? String(options?.selectedHandBrakePreset || '').trim()
|
? String(options?.selectedHandBrakePreset || '').trim()
|
||||||
: '';
|
: '';
|
||||||
|
const hasExplicitUserPresetSelection = !multipartSettingsLocked && explicitUserPresetId !== null;
|
||||||
|
const hasExplicitHandBrakePresetSelection = !multipartSettingsLocked && Boolean(rawHandBrakePreset);
|
||||||
let resolvedUserPreset = null;
|
let resolvedUserPreset = null;
|
||||||
if (hasExplicitUserPresetSelection) {
|
if (hasExplicitUserPresetSelection) {
|
||||||
const rawUserPresetId = options?.selectedUserPresetId;
|
resolvedUserPreset = await userPresetService.getPresetById(explicitUserPresetId);
|
||||||
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) {
|
if (!resolvedUserPreset) {
|
||||||
const error = new Error(`User-Preset ${userPresetId} nicht gefunden.`);
|
const error = new Error(`User-Preset ${explicitUserPresetId} nicht gefunden.`);
|
||||||
error.statusCode = 404;
|
error.statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (!resolvedUserPreset && hasExplicitHandBrakePresetSelection) {
|
||||||
if ((!resolvedUserPreset || !hasExplicitUserPresetSelection) && hasExplicitHandBrakePresetSelection) {
|
|
||||||
resolvedUserPreset = rawHandBrakePreset
|
resolvedUserPreset = rawHandBrakePreset
|
||||||
? {
|
? {
|
||||||
name: `HandBrake: ${rawHandBrakePreset}`,
|
name: `HandBrake: ${rawHandBrakePreset}`,
|
||||||
@@ -22045,8 +22166,10 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
if (!hasExplicitUserPresetSelection && !hasExplicitHandBrakePresetSelection) {
|
if (!resolvedUserPreset) {
|
||||||
resolvedUserPreset = normalizeUserPresetForPlan(encodePlan?.userPreset || null);
|
resolvedUserPreset = normalizeUserPresetForPlan(
|
||||||
|
planForConfirm?.userPreset || encodePlan?.userPreset || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmedPlan = {
|
const confirmedPlan = {
|
||||||
@@ -22070,24 +22193,49 @@ class PipelineService extends EventEmitter {
|
|||||||
const readyMediaProfile = this.resolveMediaProfileForJob(job, {
|
const readyMediaProfile = this.resolveMediaProfileForJob(job, {
|
||||||
encodePlan: confirmedPlan
|
encodePlan: confirmedPlan
|
||||||
});
|
});
|
||||||
if (readyMediaProfile === 'converter' && !confirmedPlan.userPreset) {
|
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
|
||||||
const error = new Error('Bestätigung nicht möglich: Bitte ein User- oder HandBrake-Preset auswählen.');
|
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;
|
error.statusCode = 400;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
|
|
||||||
const actionDisplaySettings = {
|
const actionDisplaySettings = {
|
||||||
...(confirmSettings && typeof confirmSettings === 'object' ? confirmSettings : {}),
|
...(confirmSettings && typeof confirmSettings === 'object' ? confirmSettings : {}),
|
||||||
handbrake_preset: String(
|
handbrake_preset: effectivePresetName || null,
|
||||||
resolvedUserPreset?.handbrakePreset
|
handbrake_extra_args: effectiveExtraArgs
|
||||||
|| confirmSettings?.handbrake_preset
|
|
||||||
|| ''
|
|
||||||
).trim() || null,
|
|
||||||
handbrake_extra_args: String(
|
|
||||||
resolvedUserPreset?.extraArgs
|
|
||||||
|| confirmSettings?.handbrake_extra_args
|
|
||||||
|| ''
|
|
||||||
).trim()
|
|
||||||
};
|
};
|
||||||
confirmedPlan.titles = refreshAudioTrackActionsForPlanTitles(
|
confirmedPlan.titles = refreshAudioTrackActionsForPlanTitles(
|
||||||
confirmedPlan.titles,
|
confirmedPlan.titles,
|
||||||
@@ -23315,6 +23463,11 @@ class PipelineService extends EventEmitter {
|
|||||||
options?.multipartSettingsLock || null
|
options?.multipartSettingsLock || null
|
||||||
);
|
);
|
||||||
enrichedReview = multipartLockResult.plan;
|
enrichedReview = multipartLockResult.plan;
|
||||||
|
const reviewDefaultUserPresetResult = await applyConfiguredDefaultUserPresetToReviewPlan(enrichedReview, {
|
||||||
|
mediaProfile,
|
||||||
|
isSeriesSelection: effectiveIsSeriesDvdReview
|
||||||
|
});
|
||||||
|
enrichedReview = reviewDefaultUserPresetResult.plan;
|
||||||
const previousEncodePlan = options?.previousEncodePlan && typeof options.previousEncodePlan === 'object'
|
const previousEncodePlan = options?.previousEncodePlan && typeof options.previousEncodePlan === 'object'
|
||||||
? options.previousEncodePlan
|
? options.previousEncodePlan
|
||||||
: null;
|
: 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
|
||||||
|
};
|
||||||
@@ -186,12 +186,25 @@ CREATE TABLE user_presets (
|
|||||||
|
|
||||||
CREATE INDEX idx_user_presets_media_type ON user_presets(media_type);
|
CREATE INDEX idx_user_presets_media_type ON user_presets(media_type);
|
||||||
|
|
||||||
|
CREATE TABLE user_preset_defaults (
|
||||||
|
slot_key TEXT PRIMARY KEY,
|
||||||
|
preset_id INTEGER,
|
||||||
|
FOREIGN KEY (preset_id) REFERENCES user_presets(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_preset_defaults_preset_id ON user_preset_defaults(preset_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS aax_activation_bytes (
|
CREATE TABLE IF NOT EXISTS aax_activation_bytes (
|
||||||
checksum TEXT PRIMARY KEY,
|
checksum TEXT PRIMARY KEY,
|
||||||
activation_bytes TEXT NOT NULL,
|
activation_bytes TEXT NOT NULL,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO user_preset_defaults (slot_key, preset_id) VALUES ('bluray_movie', NULL);
|
||||||
|
INSERT OR IGNORE INTO user_preset_defaults (slot_key, preset_id) VALUES ('bluray_series', NULL);
|
||||||
|
INSERT OR IGNORE INTO user_preset_defaults (slot_key, preset_id) VALUES ('dvd_movie', NULL);
|
||||||
|
INSERT OR IGNORE INTO user_preset_defaults (slot_key, preset_id) VALUES ('dvd_series', NULL);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_prefs (
|
CREATE TABLE IF NOT EXISTS user_prefs (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ Frontend: React + PrimeReact + Vite.
|
|||||||
|
|
||||||
## Hauptseiten
|
## Hauptseiten
|
||||||
|
|
||||||
### `JobsInboxPage.jsx`
|
|
||||||
|
|
||||||
Zentrale Job-Inbox:
|
|
||||||
|
|
||||||
- einheitliche Liste über Ripper-, Converter- und Audiobook-Jobs
|
|
||||||
- View-/Status-Filter, Suche, Paginierung
|
|
||||||
- Schnellsprung in passende Detailseite (`Ripper`/`Converter`)
|
|
||||||
|
|
||||||
### `RipperPage.jsx`
|
### `RipperPage.jsx`
|
||||||
|
|
||||||
Pipeline-Steuerung:
|
Pipeline-Steuerung:
|
||||||
|
|||||||
+5
-7
@@ -6,7 +6,6 @@ Dieses Kapitel ist das Bedienhandbuch für die Oberflächen im Alltag.
|
|||||||
|
|
||||||
| Seite | Rolle im Betrieb | Typischer Einsatz |
|
| Seite | Rolle im Betrieb | Typischer Einsatz |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [Jobs](jobs.md) | zentrale Inbox | Gesamtlage prüfen und in Fachseite springen |
|
|
||||||
| [Ripper](ripper.md) | Live-Steuerung der Pipeline | Disc-Flow starten, Queue steuern, Review freigeben |
|
| [Ripper](ripper.md) | Live-Steuerung der Pipeline | Disc-Flow starten, Queue steuern, Review freigeben |
|
||||||
| [Audiobooks](audiobooks.md) | AAX-Spezialflow | Upload, Konfiguration und Encode von Audiobooks |
|
| [Audiobooks](audiobooks.md) | AAX-Spezialflow | Upload, Konfiguration und Encode von Audiobooks |
|
||||||
| [Settings](settings.md) | Konfiguration/Automatisierung | Pfade, Tools, Queue-Limits, Skripte, Cron |
|
| [Settings](settings.md) | Konfiguration/Automatisierung | Pfade, Tools, Queue-Limits, Skripte, Cron |
|
||||||
@@ -17,17 +16,16 @@ Dieses Kapitel ist das Bedienhandbuch für die Oberflächen im Alltag.
|
|||||||
|
|
||||||
## Empfohlener Tagesablauf
|
## Empfohlener Tagesablauf
|
||||||
|
|
||||||
1. `Jobs`: Statuslage und Auffälligkeiten prüfen.
|
1. `Ripper` oder `Converter`/`Audiobooks`: neue Arbeit starten.
|
||||||
2. `Ripper` oder `Converter`/`Audiobooks`: neue Arbeit starten.
|
2. `Historie`: Ergebnisse validieren und ggf. nachbearbeiten.
|
||||||
3. `Historie`: Ergebnisse validieren und ggf. nachbearbeiten.
|
3. `Downloads`: Exporte für externe Übergabe erzeugen.
|
||||||
4. `Downloads`: Exporte für externe Übergabe erzeugen.
|
4. `Settings`: Regeln und Limits nur kontrolliert anpassen.
|
||||||
5. `Settings`: Regeln und Limits nur kontrolliert anpassen.
|
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
Direkt in der Top-Navigation:
|
Direkt in der Top-Navigation:
|
||||||
|
|
||||||
- `Jobs`, `Ripper`, `Converter`, `Audiobooks`, `Settings`, `Historie`, `Downloads`
|
- `Ripper`, `Converter`, `Audiobooks`, `Settings`, `Historie`, `Downloads`
|
||||||
|
|
||||||
Nur per URL:
|
Nur per URL:
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# Jobs
|
|
||||||
|
|
||||||
`Jobs` ist die zentrale Inbox über alle Jobarten und dient als schneller Einstieg in den Tagesbetrieb.
|
|
||||||
|
|
||||||
## Zweck der Seite
|
|
||||||
|
|
||||||
Die Liste vereinheitlicht:
|
|
||||||
|
|
||||||
- Disc-/Ripper-Jobs
|
|
||||||
- Converter-Jobs
|
|
||||||
- Audiobook-Jobs
|
|
||||||
|
|
||||||
Dadurch kannst du unabhängig von der Fachseite zuerst den Gesamtzustand prüfen.
|
|
||||||
|
|
||||||
## Filterlogik
|
|
||||||
|
|
||||||
### View-Filter
|
|
||||||
|
|
||||||
- `Alle Jobs`
|
|
||||||
- `Ripper-View`
|
|
||||||
- `Converter-View`
|
|
||||||
- `Audiobooks`
|
|
||||||
|
|
||||||
### Status-Filter
|
|
||||||
|
|
||||||
- `Alle`
|
|
||||||
- `Aktiv`
|
|
||||||
- `Fehler/Abbruch`
|
|
||||||
|
|
||||||
Zusätzlich:
|
|
||||||
|
|
||||||
- Freitextsuche
|
|
||||||
- Paginierung
|
|
||||||
|
|
||||||
## Was ein Jobeintrag zeigt
|
|
||||||
|
|
||||||
- Job-ID und Titel
|
|
||||||
- Statusbadge
|
|
||||||
- Jobtypbadge (z. B. Blu-ray, DVD, Converter Audio, Audiobook)
|
|
||||||
- Aktualisierungszeit
|
|
||||||
|
|
||||||
Die Ansicht wird periodisch aktualisiert und ergänzt sich mit WebSocket-Events.
|
|
||||||
|
|
||||||
## Schnellaktion pro Zeile
|
|
||||||
|
|
||||||
- `Im Ripper öffnen` für Disc-/Audiobook-nahe Jobs
|
|
||||||
- `Im Converter öffnen` für Converter-Jobs
|
|
||||||
|
|
||||||
So kommst du ohne Umwege in die Oberfläche mit den passenden Detailaktionen.
|
|
||||||
|
|
||||||
## Empfohlener Einsatz
|
|
||||||
|
|
||||||
1. In `Jobs` Störungen und Auslastung identifizieren.
|
|
||||||
2. Danach gezielt in `Ripper`, `Converter` oder `Historie` springen.
|
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+33
-5
@@ -13,7 +13,6 @@ import HistoryPage from './pages/HistoryPage';
|
|||||||
import DatabasePage from './pages/DatabasePage';
|
import DatabasePage from './pages/DatabasePage';
|
||||||
import DownloadsPage from './pages/DownloadsPage';
|
import DownloadsPage from './pages/DownloadsPage';
|
||||||
import ConverterPage from './pages/ConverterPage';
|
import ConverterPage from './pages/ConverterPage';
|
||||||
import JobsInboxPage from './pages/JobsInboxPage';
|
|
||||||
import AudiobooksPage from './pages/AudiobooksPage';
|
import AudiobooksPage from './pages/AudiobooksPage';
|
||||||
|
|
||||||
function normalizeJobId(value) {
|
function normalizeJobId(value) {
|
||||||
@@ -150,6 +149,14 @@ function App() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const globalToastRef = useRef(null);
|
const globalToastRef = useRef(null);
|
||||||
const prevCdDrivesRef = useRef({});
|
const prevCdDrivesRef = useRef({});
|
||||||
|
const hardwareWarmupRef = useRef({ timer: null, attempts: 0 });
|
||||||
|
|
||||||
|
const clearHardwareWarmupRetry = () => {
|
||||||
|
if (hardwareWarmupRef.current?.timer) {
|
||||||
|
clearTimeout(hardwareWarmupRef.current.timer);
|
||||||
|
hardwareWarmupRef.current.timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// When a virtual CD drive is removed (CD encode/rip finished or failed),
|
// When a virtual CD drive is removed (CD encode/rip finished or failed),
|
||||||
// or when a CD drive transitions away from an active job, force both
|
// or when a CD drive transitions away from an active job, force both
|
||||||
@@ -194,7 +201,23 @@ function App() {
|
|||||||
const refreshPipeline = async () => {
|
const refreshPipeline = async () => {
|
||||||
const response = await api.getPipelineState();
|
const response = await api.getPipelineState();
|
||||||
setPipeline(response.pipeline);
|
setPipeline(response.pipeline);
|
||||||
setHardwareMonitoring(response?.hardwareMonitoring || null);
|
const monitoringPayload = response?.hardwareMonitoring || null;
|
||||||
|
setHardwareMonitoring(monitoringPayload);
|
||||||
|
const monitoringEnabled = Boolean(monitoringPayload?.enabled);
|
||||||
|
const hasSample = Boolean(monitoringPayload?.sample && typeof monitoringPayload.sample === 'object');
|
||||||
|
if (monitoringEnabled && !hasSample) {
|
||||||
|
const nextAttempts = Number(hardwareWarmupRef.current?.attempts || 0) + 1;
|
||||||
|
hardwareWarmupRef.current.attempts = nextAttempts;
|
||||||
|
if (nextAttempts <= 10 && !hardwareWarmupRef.current.timer) {
|
||||||
|
hardwareWarmupRef.current.timer = setTimeout(() => {
|
||||||
|
hardwareWarmupRef.current.timer = null;
|
||||||
|
refreshPipeline().catch(() => null);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hardwareWarmupRef.current.attempts = 0;
|
||||||
|
clearHardwareWarmupRetry();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,10 +333,17 @@ function App() {
|
|||||||
setExpertMode(val === 'true' || val === true);
|
setExpertMode(val === 'true' || val === true);
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
return () => {
|
||||||
|
clearHardwareWarmupRetry();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
onMessage: (message) => {
|
onMessage: (message) => {
|
||||||
|
if (message.type === 'WS_CONNECTED') {
|
||||||
|
refreshPipeline().catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === 'PIPELINE_STATE_CHANGED') {
|
if (message.type === 'PIPELINE_STATE_CHANGED') {
|
||||||
setPipeline(message.payload);
|
setPipeline(message.payload);
|
||||||
}
|
}
|
||||||
@@ -575,7 +605,6 @@ function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ label: 'Jobs', path: '/jobs' },
|
|
||||||
{ label: 'Ripper', path: '/ripper' },
|
{ label: 'Ripper', path: '/ripper' },
|
||||||
{ label: 'Converter', path: '/converter' },
|
{ label: 'Converter', path: '/converter' },
|
||||||
{ label: 'Audiobooks', path: '/audiobooks' },
|
{ label: 'Audiobooks', path: '/audiobooks' },
|
||||||
@@ -705,13 +734,12 @@ function App() {
|
|||||||
</RipperPage>
|
</RipperPage>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<Navigate to="jobs" replace />} />
|
<Route index element={<Navigate to="ripper" replace />} />
|
||||||
<Route path="ripper" element={null} />
|
<Route path="ripper" element={null} />
|
||||||
<Route path="settings" element={<SettingsPage />} />
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
<Route path="history" element={<HistoryPage refreshToken={historyJobsRefreshToken} />} />
|
<Route path="history" element={<HistoryPage refreshToken={historyJobsRefreshToken} />} />
|
||||||
<Route path="downloads" element={<DownloadsPage refreshToken={downloadsRefreshToken} />} />
|
<Route path="downloads" element={<DownloadsPage refreshToken={downloadsRefreshToken} />} />
|
||||||
<Route path="database" element={<DatabasePage />} />
|
<Route path="database" element={<DatabasePage />} />
|
||||||
<Route path="jobs" element={<JobsInboxPage />} />
|
|
||||||
<Route path="converter" element={<ConverterPage />} />
|
<Route path="converter" element={<ConverterPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="audiobooks"
|
path="audiobooks"
|
||||||
|
|||||||
@@ -1012,12 +1012,26 @@ export const api = {
|
|||||||
forceRefresh: options.forceRefresh
|
forceRefresh: options.forceRefresh
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getUserPresetDefaults(options = {}) {
|
||||||
|
return requestCachedGet('/settings/user-preset-defaults', {
|
||||||
|
ttlMs: 2 * 60 * 1000,
|
||||||
|
forceRefresh: options.forceRefresh
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateUserPresetDefaults(defaults = {}) {
|
||||||
|
const result = await request('/settings/user-preset-defaults', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ defaults })
|
||||||
|
});
|
||||||
|
afterMutationInvalidate(['/settings/user-preset-defaults']);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
async createUserPreset(payload = {}) {
|
async createUserPreset(payload = {}) {
|
||||||
const result = await request('/settings/user-presets', {
|
const result = await request('/settings/user-presets', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
afterMutationInvalidate(['/settings/user-presets']);
|
afterMutationInvalidate(['/settings/user-presets', '/settings/user-preset-defaults']);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
async updateUserPreset(id, payload = {}) {
|
async updateUserPreset(id, payload = {}) {
|
||||||
@@ -1025,14 +1039,14 @@ export const api = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
afterMutationInvalidate(['/settings/user-presets']);
|
afterMutationInvalidate(['/settings/user-presets', '/settings/user-preset-defaults']);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
async deleteUserPreset(id) {
|
async deleteUserPreset(id) {
|
||||||
const result = await request(`/settings/user-presets/${encodeURIComponent(id)}`, {
|
const result = await request(`/settings/user-presets/${encodeURIComponent(id)}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
afterMutationInvalidate(['/settings/user-presets']);
|
afterMutationInvalidate(['/settings/user-presets', '/settings/user-preset-defaults']);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -955,32 +955,7 @@ function parseAudioSelectorFromArgs(extraArgsString, baseSelector) {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenize: split on whitespace but respect quoted strings
|
const tokens = tokenizeCliArgs(args);
|
||||||
const tokens = [];
|
|
||||||
let current = '';
|
|
||||||
let inQuote = null;
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const ch = args[i];
|
|
||||||
if (inQuote) {
|
|
||||||
if (ch === inQuote) {
|
|
||||||
inQuote = null;
|
|
||||||
} else {
|
|
||||||
current += ch;
|
|
||||||
}
|
|
||||||
} else if (ch === '"' || ch === "'") {
|
|
||||||
inQuote = ch;
|
|
||||||
} else if (ch === ' ' || ch === '\t') {
|
|
||||||
if (current) {
|
|
||||||
tokens.push(current);
|
|
||||||
current = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current += ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current) {
|
|
||||||
tokens.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = { ...base };
|
const result = { ...base };
|
||||||
|
|
||||||
@@ -1026,6 +1001,113 @@ function parseAudioSelectorFromArgs(extraArgsString, baseSelector) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSubtitleSelectorFromArgs(extraArgsString, baseSelector) {
|
||||||
|
const base = baseSelector && typeof baseSelector === 'object' ? baseSelector : {};
|
||||||
|
const args = String(extraArgsString || '').trim();
|
||||||
|
if (!args) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = tokenizeCliArgs(args);
|
||||||
|
const result = { ...base };
|
||||||
|
const getNext = (i) => (i + 1 < tokens.length ? tokens[i + 1] : null);
|
||||||
|
const parseTrackId = (raw) => {
|
||||||
|
const first = String(raw || '').split(',').map((item) => String(item || '').trim()).filter(Boolean)[0] || '';
|
||||||
|
const parsed = Number(first);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
const eqIdx = token.indexOf('=');
|
||||||
|
const flag = eqIdx !== -1 ? token.slice(0, eqIdx) : token;
|
||||||
|
const inlineVal = eqIdx !== -1 ? token.slice(eqIdx + 1) : null;
|
||||||
|
|
||||||
|
const getValue = () => {
|
||||||
|
if (inlineVal !== null) return inlineVal;
|
||||||
|
const next = getNext(i);
|
||||||
|
if (next && !next.startsWith('-')) {
|
||||||
|
i++;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (flag === '--all-subtitles') {
|
||||||
|
result.mode = 'all';
|
||||||
|
} else if (flag === '--first-subtitle') {
|
||||||
|
result.mode = 'first';
|
||||||
|
result.firstOnly = true;
|
||||||
|
} else if (flag === '--subtitle-lang-list') {
|
||||||
|
const val = getValue();
|
||||||
|
if (val) {
|
||||||
|
result.mode = 'language';
|
||||||
|
result.languages = val.split(',').map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
} else if (flag === '--subtitle' || flag === '-s') {
|
||||||
|
const val = getValue();
|
||||||
|
if (val) {
|
||||||
|
const normalized = String(val || '').trim().toLowerCase();
|
||||||
|
result.mode = normalized === 'none' ? 'none' : 'explicit';
|
||||||
|
}
|
||||||
|
} else if (flag === '--subtitle-burned') {
|
||||||
|
result.burnBehavior = 'first';
|
||||||
|
const val = getValue();
|
||||||
|
const parsedTrack = parseTrackId(val);
|
||||||
|
if (parsedTrack !== null) {
|
||||||
|
result.burnedTrackId = parsedTrack;
|
||||||
|
}
|
||||||
|
} else if (flag === '--subtitle-default') {
|
||||||
|
const val = getValue();
|
||||||
|
const parsedTrack = parseTrackId(val);
|
||||||
|
if (parsedTrack !== null) {
|
||||||
|
result.defaultTrackId = parsedTrack;
|
||||||
|
}
|
||||||
|
} else if (flag === '--subtitle-forced') {
|
||||||
|
const val = getValue();
|
||||||
|
const parsedTrack = parseTrackId(val);
|
||||||
|
if (parsedTrack !== null) {
|
||||||
|
result.forcedTrackId = parsedTrack;
|
||||||
|
result.forcedOnly = false;
|
||||||
|
} else {
|
||||||
|
result.forcedOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeCliArgs(argsString) {
|
||||||
|
const source = String(argsString || '');
|
||||||
|
const tokens = [];
|
||||||
|
let current = '';
|
||||||
|
let inQuote = null;
|
||||||
|
for (let i = 0; i < source.length; i++) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (inQuote) {
|
||||||
|
if (ch === inQuote) {
|
||||||
|
inQuote = null;
|
||||||
|
} else {
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
} else if (ch === '"' || ch === "'") {
|
||||||
|
inQuote = ch;
|
||||||
|
} else if (ch === ' ' || ch === '\t') {
|
||||||
|
if (current) {
|
||||||
|
tokens.push(current);
|
||||||
|
current = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current) {
|
||||||
|
tokens.push(current);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
function buildAudioActionPreviewSummary(track, selectedIndex, audioSelector) {
|
function buildAudioActionPreviewSummary(track, selectedIndex, audioSelector) {
|
||||||
const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {};
|
const selector = audioSelector && typeof audioSelector === 'object' ? audioSelector : {};
|
||||||
const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : [];
|
const availableEncoders = Array.isArray(selector.encoders) ? selector.encoders : [];
|
||||||
@@ -1412,7 +1494,10 @@ export default function MediaInfoReviewPanel({
|
|||||||
selectedUserPresetId = null,
|
selectedUserPresetId = null,
|
||||||
onUserPresetChange = null,
|
onUserPresetChange = null,
|
||||||
selectedHandBrakePreset = '',
|
selectedHandBrakePreset = '',
|
||||||
comparisonReviewItems = []
|
onHandBrakePresetChange = null,
|
||||||
|
handBrakePresetOptions = [],
|
||||||
|
comparisonReviewItems = [],
|
||||||
|
topContent = null
|
||||||
}) {
|
}) {
|
||||||
if (!review) {
|
if (!review) {
|
||||||
return <p>Keine Mediainfo-Daten vorhanden.</p>;
|
return <p>Keine Mediainfo-Daten vorhanden.</p>;
|
||||||
@@ -1647,6 +1732,11 @@ export default function MediaInfoReviewPanel({
|
|||||||
const processedFiles = Number(review.processedFiles || displayTitles.length || 0);
|
const processedFiles = Number(review.processedFiles || displayTitles.length || 0);
|
||||||
const totalFiles = Number(review.totalFiles || displayTitles.length || 0);
|
const totalFiles = Number(review.totalFiles || displayTitles.length || 0);
|
||||||
const playlistRecommendation = review.playlistRecommendation || null;
|
const playlistRecommendation = review.playlistRecommendation || null;
|
||||||
|
const selectedPlaylistId = String(
|
||||||
|
review?.selectedPlaylistId
|
||||||
|
?? review?.selectedPlaylist
|
||||||
|
?? ''
|
||||||
|
).trim();
|
||||||
const rawPreset = String(review.selectors?.preset || '').trim();
|
const rawPreset = String(review.selectors?.preset || '').trim();
|
||||||
const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)';
|
const presetLabel = String(presetDisplayValue || rawPreset).trim() || '(kein Preset)';
|
||||||
const multipartSettingsLock = review?.multipartSettingsLock && typeof review.multipartSettingsLock === 'object'
|
const multipartSettingsLock = review?.multipartSettingsLock && typeof review.multipartSettingsLock === 'object'
|
||||||
@@ -1661,14 +1751,47 @@ export default function MediaInfoReviewPanel({
|
|||||||
&& Number(multipartSettingsLock.sourceDiscNumber) > 0
|
&& Number(multipartSettingsLock.sourceDiscNumber) > 0
|
||||||
? Math.trunc(Number(multipartSettingsLock.sourceDiscNumber))
|
? Math.trunc(Number(multipartSettingsLock.sourceDiscNumber))
|
||||||
: null;
|
: null;
|
||||||
|
const isTitleSelectionMode = Boolean(
|
||||||
|
review?.playlistDecisionRequired
|
||||||
|
|| review?.titleSelectionRequired
|
||||||
|
|| review?.handBrakeTitleDecisionRequired
|
||||||
|
);
|
||||||
|
const playlistAlreadyAccepted = !isTitleSelectionMode && Boolean(selectedPlaylistId);
|
||||||
|
const hiddenTechnicalNotePatternsWhenPlaylistAccepted = [
|
||||||
|
/^Preset:\s/i,
|
||||||
|
/^Extra Args:\s/i,
|
||||||
|
/^Preset-Quelle:\s/i,
|
||||||
|
/^Preset-Defaults werden als Basis genutzt\./i,
|
||||||
|
/^Preset-Hinweis:\s/i,
|
||||||
|
/^Manuelle Playlist-Auswahl aktiv:\s/i,
|
||||||
|
/^MakeMKV Full-Analyse wurde einmal für Playlist-\/Titel-Mapping verwendet\./i,
|
||||||
|
/^HandBrake Track-Analyse aktiv:\s/i
|
||||||
|
];
|
||||||
|
const visibleReviewNotes = Array.isArray(review.notes)
|
||||||
|
? review.notes.filter((note) => {
|
||||||
|
const normalized = String(note || '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!playlistAlreadyAccepted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !hiddenTechnicalNotePatternsWhenPlaylistAccepted
|
||||||
|
.some((pattern) => pattern.test(normalized));
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
// User preset resolution
|
// User preset resolution
|
||||||
const normalizedUserPresets = Array.isArray(userPresets) ? userPresets : [];
|
const normalizedUserPresets = Array.isArray(userPresets) ? userPresets : [];
|
||||||
|
const normalizedHandBrakePresetOptions = Array.isArray(handBrakePresetOptions) ? handBrakePresetOptions : [];
|
||||||
const selectedUserPreset = selectedUserPresetId
|
const selectedUserPreset = selectedUserPresetId
|
||||||
? normalizedUserPresets.find((p) => Number(p.id) === Number(selectedUserPresetId)) || null
|
? normalizedUserPresets.find((p) => Number(p.id) === Number(selectedUserPresetId)) || null
|
||||||
: null;
|
: null;
|
||||||
const hasUserPresets = normalizedUserPresets.length > 0;
|
const hasUserPresets = normalizedUserPresets.length > 0;
|
||||||
const allowUserPresetSelection = hasUserPresets && typeof onUserPresetChange === 'function' && allowEncodeItemSelection;
|
const hasHandBrakePresetOptions = normalizedHandBrakePresetOptions.some((option) => !option?.disabled);
|
||||||
|
const allowPresetSelection = allowEncodeItemSelection
|
||||||
|
&& (typeof onUserPresetChange === 'function' || typeof onHandBrakePresetChange === 'function')
|
||||||
|
&& (hasUserPresets || hasHandBrakePresetOptions);
|
||||||
const isUserPresetActive = Boolean(selectedUserPreset);
|
const isUserPresetActive = Boolean(selectedUserPreset);
|
||||||
const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
||||||
const isHandBrakePresetActive = !isUserPresetActive && Boolean(trimmedHandBrakePreset);
|
const isHandBrakePresetActive = !isUserPresetActive && Boolean(trimmedHandBrakePreset);
|
||||||
@@ -1678,9 +1801,97 @@ export default function MediaInfoReviewPanel({
|
|||||||
const effectiveAudioSelector = effectivePresetOverride?.extraArgs
|
const effectiveAudioSelector = effectivePresetOverride?.extraArgs
|
||||||
? parseAudioSelectorFromArgs(effectivePresetOverride.extraArgs, review?.selectors?.audio)
|
? parseAudioSelectorFromArgs(effectivePresetOverride.extraArgs, review?.selectors?.audio)
|
||||||
: (review?.selectors?.audio || null);
|
: (review?.selectors?.audio || null);
|
||||||
const presetSourceLabel = isUserPresetActive
|
const activePresetDisplayLabel = isUserPresetActive
|
||||||
? `User-Preset: ${selectedUserPreset?.name || 'Preset'}`
|
? 'User-Preset'
|
||||||
: (isHandBrakePresetActive ? 'HandBrake-Preset' : '');
|
: ((isHandBrakePresetActive || String(presetLabel || '').trim()) ? 'HandBrake-Preset' : '(kein Preset)');
|
||||||
|
const minLengthMinutes = Number(review?.minLengthMinutes);
|
||||||
|
const minLengthLabel = Number.isFinite(minLengthMinutes)
|
||||||
|
? (minLengthMinutes <= 0
|
||||||
|
? 'Kein Mindestwert (deaktiviert)'
|
||||||
|
: `${Math.trunc(minLengthMinutes)} Minute(n)`)
|
||||||
|
: '-';
|
||||||
|
const subtitleSelector = review?.selectors?.subtitle && typeof review.selectors.subtitle === 'object'
|
||||||
|
? review.selectors.subtitle
|
||||||
|
: {};
|
||||||
|
const effectiveSubtitleSelector = effectivePresetOverride?.extraArgs
|
||||||
|
? parseSubtitleSelectorFromArgs(effectivePresetOverride.extraArgs, subtitleSelector)
|
||||||
|
: subtitleSelector;
|
||||||
|
const subtitleModeLabelMap = {
|
||||||
|
all: 'Alle Spuren',
|
||||||
|
first: 'Erste Spur',
|
||||||
|
language: 'Nach Sprache',
|
||||||
|
explicit: 'Manuell (Track-IDs)',
|
||||||
|
none: 'Keine Spuren'
|
||||||
|
};
|
||||||
|
const subtitleModeLabel = subtitleModeLabelMap[String(effectiveSubtitleSelector?.mode || '').trim().toLowerCase()] || '-';
|
||||||
|
const subtitleFlagLabels = [];
|
||||||
|
if (effectiveSubtitleSelector?.burnBehavior === 'first') {
|
||||||
|
subtitleFlagLabels.push('Burn-In (erste Subtitle-Spur)');
|
||||||
|
}
|
||||||
|
if (effectiveSubtitleSelector?.defaultTrackId != null) {
|
||||||
|
subtitleFlagLabels.push(`Default-Track #${effectiveSubtitleSelector.defaultTrackId}`);
|
||||||
|
}
|
||||||
|
if (effectiveSubtitleSelector?.forcedTrackId != null) {
|
||||||
|
subtitleFlagLabels.push(`Forced-Track #${effectiveSubtitleSelector.forcedTrackId}`);
|
||||||
|
} else if (effectiveSubtitleSelector?.forcedOnly) {
|
||||||
|
subtitleFlagLabels.push('Forced-Only');
|
||||||
|
}
|
||||||
|
const subtitleFlagsLabel = subtitleFlagLabels.join(', ') || 'Keine';
|
||||||
|
const audioEncoderSourceLabel = String(effectiveAudioSelector?.encoderSource || '').trim() || 'default';
|
||||||
|
const audioCopyMaskSourceLabel = String(effectiveAudioSelector?.copyMaskSource || '').trim() || 'default';
|
||||||
|
const audioFallbackSourceLabel = String(effectiveAudioSelector?.fallbackSource || '').trim() || 'default';
|
||||||
|
const presetDropdownOptions = (() => {
|
||||||
|
const options = [];
|
||||||
|
if (hasUserPresets) {
|
||||||
|
options.push({ label: 'User-Presets/', value: '__group__userpresets', disabled: true });
|
||||||
|
for (const preset of normalizedUserPresets) {
|
||||||
|
const presetId = Number(preset?.id);
|
||||||
|
if (!Number.isFinite(presetId) || presetId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const mediaType = String(preset?.mediaType || '').trim().toLowerCase();
|
||||||
|
const mediaTypeLabel = mediaType && mediaType !== 'all'
|
||||||
|
? ` [${mediaType === 'bluray' ? 'Blu-ray' : mediaType === 'dvd' ? 'DVD' : mediaType}]`
|
||||||
|
: '';
|
||||||
|
options.push({
|
||||||
|
label: `\u00A0\u00A0\u00A0${String(preset?.name || `Preset #${presetId}`).trim() || `Preset #${presetId}`}${mediaTypeLabel}`,
|
||||||
|
value: `user:${Math.trunc(presetId)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const option of normalizedHandBrakePresetOptions) {
|
||||||
|
if (!option) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.disabled) {
|
||||||
|
options.push({
|
||||||
|
label: String(option?.label || '').trim() || 'HandBrake/',
|
||||||
|
value: String(option?.value || option?.label || '').trim() || `__group__hb_${options.length}`,
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = String(option?.value || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
options.push({
|
||||||
|
label: String(option?.label || value).trim() || value,
|
||||||
|
value: `hb:${value}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const selectedPresetValue = String(selectedHandBrakePreset || '').trim();
|
||||||
|
if (selectedPresetValue && !options.some((entry) => String(entry?.value || '').trim() === `hb:${selectedPresetValue}`)) {
|
||||||
|
options.push({
|
||||||
|
label: selectedPresetValue,
|
||||||
|
value: `hb:${selectedPresetValue}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
})();
|
||||||
|
const selectedPresetToken = isUserPresetActive
|
||||||
|
? `user:${selectedUserPreset.id}`
|
||||||
|
: (trimmedHandBrakePreset ? `hb:${trimmedHandBrakePreset}` : null);
|
||||||
|
|
||||||
const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : [])
|
const scriptCatalog = (Array.isArray(availableScripts) ? availableScripts : [])
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
@@ -1872,22 +2083,42 @@ export default function MediaInfoReviewPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="media-review-wrap">
|
<div className="media-review-wrap">
|
||||||
{!compactTitleSelection && allowUserPresetSelection && (
|
{topContent}
|
||||||
|
{!compactTitleSelection && allowPresetSelection && (
|
||||||
<div className="user-preset-selector" style={{ marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
<div className="user-preset-selector" style={{ marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '0.4rem', fontWeight: 600 }}>
|
<label style={{ display: 'block', marginBottom: '0.4rem', fontWeight: 600 }}>
|
||||||
Encode-Preset auswählen
|
Encode-Preset auswählen
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={selectedUserPresetId ? Number(selectedUserPresetId) : null}
|
value={selectedPresetToken}
|
||||||
options={[
|
options={presetDropdownOptions}
|
||||||
{ label: '(Einstellungen-Fallback)', value: null },
|
optionLabel="label"
|
||||||
...normalizedUserPresets.map((p) => ({
|
optionValue="value"
|
||||||
label: `${p.name}${p.mediaType !== 'all' ? ` [${p.mediaType === 'bluray' ? 'Blu-ray' : p.mediaType === 'dvd' ? 'DVD' : 'Sonstiges'}]` : ''}`,
|
optionDisabled="disabled"
|
||||||
value: Number(p.id)
|
onChange={(e) => {
|
||||||
}))
|
const value = String(e?.value || '').trim();
|
||||||
]}
|
if (value.startsWith('user:')) {
|
||||||
onChange={(e) => onUserPresetChange(e.value)}
|
const presetId = Number(value.slice(5));
|
||||||
placeholder="(Einstellungen-Fallback)"
|
if (typeof onUserPresetChange === 'function') {
|
||||||
|
onUserPresetChange(Number.isFinite(presetId) && presetId > 0 ? Math.trunc(presetId) : null);
|
||||||
|
}
|
||||||
|
if (typeof onHandBrakePresetChange === 'function') {
|
||||||
|
onHandBrakePresetChange('');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.startsWith('hb:')) {
|
||||||
|
const presetName = value.slice(3);
|
||||||
|
if (typeof onUserPresetChange === 'function') {
|
||||||
|
onUserPresetChange(null);
|
||||||
|
}
|
||||||
|
if (typeof onHandBrakePresetChange === 'function') {
|
||||||
|
onHandBrakePresetChange(presetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Preset auswählen …"
|
||||||
|
filter
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
{selectedUserPreset && (
|
{selectedUserPreset && (
|
||||||
@@ -1905,37 +2136,13 @@ export default function MediaInfoReviewPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!compactTitleSelection ? (
|
{!compactTitleSelection && !isTitleSelectionMode ? (
|
||||||
<div className="media-review-meta">
|
<div className="media-review-meta">
|
||||||
<div>
|
<div>
|
||||||
<strong>Preset:</strong>{' '}
|
<strong>Preset:</strong>{' '}
|
||||||
{effectivePresetOverride
|
{activePresetDisplayLabel}
|
||||||
? (effectivePresetOverride.handbrakePreset || '(kein Preset)')
|
|
||||||
: presetLabel}
|
|
||||||
{effectivePresetOverride && presetSourceLabel && (
|
|
||||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>
|
|
||||||
({presetSourceLabel})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div><strong>Titel-Mindestlänge:</strong> {minLengthLabel}</div>
|
||||||
<strong>Extra Args:</strong>{' '}
|
|
||||||
{effectivePresetOverride
|
|
||||||
? (effectivePresetOverride.extraArgs || '(keine)')
|
|
||||||
: (review.selectors?.extraArgs || '(keine)')}
|
|
||||||
{effectivePresetOverride && isUserPresetActive && !selectedUserPreset?.extraArgs && (
|
|
||||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', opacity: 0.7 }}>(aus User-Preset)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div><strong>Preset-Profil:</strong> {effectivePresetOverride ? (isUserPresetActive ? 'user-preset' : 'handbrake-preset') : (review.selectors?.presetProfileSource || '-')}</div>
|
|
||||||
<div><strong>MIN_LENGTH_MINUTES:</strong> {review.minLengthMinutes}</div>
|
|
||||||
<div><strong>Encode Input:</strong> {encodeInputTitle?.fileName || '-'}</div>
|
|
||||||
<div><strong>Audio Auswahl:</strong> {effectiveAudioSelector?.mode || '-'}</div>
|
|
||||||
<div><strong>Audio Encoder:</strong> {(effectiveAudioSelector?.encoders || []).join(', ') || 'Preset-Default'}</div>
|
|
||||||
<div><strong>Audio Copy-Mask:</strong> {(effectiveAudioSelector?.copyMask || []).join(', ') || '-'}</div>
|
|
||||||
<div><strong>Audio Fallback:</strong> {effectiveAudioSelector?.fallbackEncoder || '-'}</div>
|
|
||||||
<div><strong>Subtitle Auswahl:</strong> {review.selectors?.subtitle?.mode || '-'}</div>
|
|
||||||
<div><strong>Subtitle Flags:</strong> {review.selectors?.subtitle?.burnBehavior === 'first' ? 'burned(first)' : '-'}</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1943,7 +2150,7 @@ export default function MediaInfoReviewPanel({
|
|||||||
<small>Zwischenstand: {processedFiles}/{totalFiles} Datei(en) analysiert.</small>
|
<small>Zwischenstand: {processedFiles}/{totalFiles} Datei(en) analysiert.</small>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!compactTitleSelection && playlistRecommendation ? (
|
{!compactTitleSelection && !isTitleSelectionMode && !playlistAlreadyAccepted && playlistRecommendation ? (
|
||||||
<div className="playlist-recommendation-box">
|
<div className="playlist-recommendation-box">
|
||||||
<small>
|
<small>
|
||||||
<strong>Empfehlung:</strong> {playlistRecommendation.playlistFile || '-'}
|
<strong>Empfehlung:</strong> {playlistRecommendation.playlistFile || '-'}
|
||||||
@@ -1953,9 +2160,9 @@ export default function MediaInfoReviewPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!compactTitleSelection && Array.isArray(review.notes) && review.notes.length > 0 ? (
|
{!compactTitleSelection && !isTitleSelectionMode && visibleReviewNotes.length > 0 ? (
|
||||||
<div className="media-review-notes">
|
<div className="media-review-notes">
|
||||||
{review.notes.map((note, idx) => (
|
{visibleReviewNotes.map((note, idx) => (
|
||||||
<small key={`${idx}-${note}`}>{note}</small>
|
<small key={`${idx}-${note}`}>{note}</small>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -829,6 +829,16 @@ function formatPercentNoDecimals(value) {
|
|||||||
return `${Math.trunc(Math.max(0, Math.min(100, parsed)))}%`;
|
return `${Math.trunc(Math.max(0, Math.min(100, parsed)))}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlaylistTrackPreparationStatus(value) {
|
||||||
|
const normalized = String(value || '').trim().toUpperCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return normalized.includes('PLAYLIST')
|
||||||
|
&& normalized.includes('TRACKDATEN')
|
||||||
|
&& normalized.includes('VORBEREIT');
|
||||||
|
}
|
||||||
|
|
||||||
function getSeriesBatchTagSeverity(status) {
|
function getSeriesBatchTagSeverity(status) {
|
||||||
const normalized = normalizeSeriesBatchStatus(status);
|
const normalized = normalizeSeriesBatchStatus(status);
|
||||||
if (normalized === 'FINISHED') {
|
if (normalized === 'FINISHED') {
|
||||||
@@ -1401,7 +1411,7 @@ function resolveProfiledSetting(settings, key, mediaProfile) {
|
|||||||
? ['dvd']
|
? ['dvd']
|
||||||
: mediaProfile === 'dvd'
|
: mediaProfile === 'dvd'
|
||||||
? ['bluray']
|
? ['bluray']
|
||||||
: [];
|
: ['bluray', 'dvd', 'cd', 'audiobook'];
|
||||||
for (const fb of fallbackProfiles) {
|
for (const fb of fallbackProfiles) {
|
||||||
const fbKey = `${key}_${fb}`;
|
const fbKey = `${key}_${fb}`;
|
||||||
if (settings?.[fbKey] != null && settings[fbKey] !== '') {
|
if (settings?.[fbKey] != null && settings[fbKey] !== '') {
|
||||||
@@ -1850,12 +1860,17 @@ export default function PipelineStatusCard({
|
|||||||
const nextOutputFormat = String(mediaInfoReview?.outputFormat || 'mkv').trim().toLowerCase() || 'mkv';
|
const nextOutputFormat = String(mediaInfoReview?.outputFormat || 'mkv').trim().toLowerCase() || 'mkv';
|
||||||
setSelectedOutputFormat(nextOutputFormat);
|
setSelectedOutputFormat(nextOutputFormat);
|
||||||
const userPresetId = Number(mediaInfoReview?.userPreset?.id);
|
const userPresetId = Number(mediaInfoReview?.userPreset?.id);
|
||||||
|
const settingsPresetName = String(mediaInfoReview?.selectors?.preset || '').trim();
|
||||||
if (Number.isFinite(userPresetId) && userPresetId > 0) {
|
if (Number.isFinite(userPresetId) && userPresetId > 0) {
|
||||||
setSelectedUserPresetId(Math.trunc(userPresetId));
|
setSelectedUserPresetId(Math.trunc(userPresetId));
|
||||||
setSelectedHandBrakePreset('');
|
setSelectedHandBrakePreset('');
|
||||||
} else {
|
} else {
|
||||||
setSelectedUserPresetId(null);
|
setSelectedUserPresetId(null);
|
||||||
const hbPresetName = String(mediaInfoReview?.userPreset?.handbrakePreset || '').trim();
|
const hbPresetName = String(
|
||||||
|
mediaInfoReview?.userPreset?.handbrakePreset
|
||||||
|
|| settingsPresetName
|
||||||
|
|| ''
|
||||||
|
).trim();
|
||||||
setSelectedHandBrakePreset(hbPresetName);
|
setSelectedHandBrakePreset(hbPresetName);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -1866,6 +1881,7 @@ export default function PipelineStatusCard({
|
|||||||
mediaInfoReview?.outputFormat,
|
mediaInfoReview?.outputFormat,
|
||||||
mediaInfoReview?.userPreset?.id,
|
mediaInfoReview?.userPreset?.id,
|
||||||
mediaInfoReview?.userPreset?.handbrakePreset,
|
mediaInfoReview?.userPreset?.handbrakePreset,
|
||||||
|
mediaInfoReview?.selectors?.preset,
|
||||||
selectedMetadata?.seasonNumber,
|
selectedMetadata?.seasonNumber,
|
||||||
selectedMetadata?.episodes,
|
selectedMetadata?.episodes,
|
||||||
selectedMetadata?.metadataProvider,
|
selectedMetadata?.metadataProvider,
|
||||||
@@ -2294,10 +2310,12 @@ export default function PipelineStatusCard({
|
|||||||
const hasSelectedEncodeTitle = normalizeTitleIdList(selectedEncodeTitleIds).length > 0;
|
const hasSelectedEncodeTitle = normalizeTitleIdList(selectedEncodeTitleIds).length > 0;
|
||||||
const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
|
const canConfirmReview = !reviewPlaylistDecisionRequired || hasSelectedEncodeTitle;
|
||||||
const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
const trimmedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
||||||
|
const settingsPresetFallback = String(mediaInfoReview?.selectors?.preset || '').trim();
|
||||||
const hasSelectedPreset = Boolean(selectedUserPresetId) || Boolean(trimmedHandBrakePreset);
|
const hasSelectedPreset = Boolean(selectedUserPresetId) || Boolean(trimmedHandBrakePreset);
|
||||||
|
const hasEffectiveSelectedPreset = hasSelectedPreset || Boolean(settingsPresetFallback);
|
||||||
const converterMediaType = String(mediaInfoReview?.converterMediaType || 'video').trim().toLowerCase() || 'video';
|
const converterMediaType = String(mediaInfoReview?.converterMediaType || 'video').trim().toLowerCase() || 'video';
|
||||||
const showConverterConfig = jobMediaProfile === 'converter' && converterMediaType !== 'audio';
|
const showConverterConfig = jobMediaProfile === 'converter' && converterMediaType !== 'audio';
|
||||||
const requiresPresetSelection = showConverterConfig;
|
const requiresPresetSelection = showConverterConfig || jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray';
|
||||||
const multipartSettingsLock = mediaInfoReview?.multipartSettingsLock
|
const multipartSettingsLock = mediaInfoReview?.multipartSettingsLock
|
||||||
&& typeof mediaInfoReview.multipartSettingsLock === 'object'
|
&& typeof mediaInfoReview.multipartSettingsLock === 'object'
|
||||||
? mediaInfoReview.multipartSettingsLock
|
? mediaInfoReview.multipartSettingsLock
|
||||||
@@ -2335,7 +2353,7 @@ export default function PipelineStatusCard({
|
|||||||
const canStartReadyJob = (isPreRipReview
|
const canStartReadyJob = (isPreRipReview
|
||||||
? Boolean(retryJobId)
|
? Boolean(retryJobId)
|
||||||
: Boolean(retryJobId && encodeInputPath))
|
: Boolean(retryJobId && encodeInputPath))
|
||||||
&& (!requiresPresetSelection || hasSelectedPreset);
|
&& (!requiresPresetSelection || hasEffectiveSelectedPreset);
|
||||||
const canRestartEncodeFromLastSettings = Boolean(
|
const canRestartEncodeFromLastSettings = Boolean(
|
||||||
(state === 'ERROR' || state === 'CANCELLED')
|
(state === 'ERROR' || state === 'CANCELLED')
|
||||||
&& retryJobId
|
&& retryJobId
|
||||||
@@ -3076,6 +3094,8 @@ export default function PipelineStatusCard({
|
|||||||
+ `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
|
+ `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
|
||||||
)
|
)
|
||||||
: (pipeline?.statusText || 'Bereit');
|
: (pipeline?.statusText || 'Bereit');
|
||||||
|
const showIndeterminatePrimaryProgress = !hasSeriesBatchProgress
|
||||||
|
&& isPlaylistTrackPreparationStatus(pipeline?.statusText);
|
||||||
const ripIssueEntries = useMemo(
|
const ripIssueEntries = useMemo(
|
||||||
() => extractMakeMkvRipIssues(jobRow?.makemkvInfo),
|
() => extractMakeMkvRipIssues(jobRow?.makemkvInfo),
|
||||||
[jobRow?.makemkvInfo]
|
[jobRow?.makemkvInfo]
|
||||||
@@ -3594,13 +3614,19 @@ export default function PipelineStatusCard({
|
|||||||
|
|
||||||
{running && (
|
{running && (
|
||||||
<div className="progress-wrap">
|
<div className="progress-wrap">
|
||||||
|
{showIndeterminatePrimaryProgress ? (
|
||||||
|
<ProgressBar mode="indeterminate" showValue={false} />
|
||||||
|
) : (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={primaryProgressValue}
|
value={primaryProgressValue}
|
||||||
showValue
|
showValue
|
||||||
displayValueTemplate={(value) => formatPercentNoDecimals(value)}
|
displayValueTemplate={(value) => formatPercentNoDecimals(value)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<small>
|
<small>
|
||||||
{primaryEta
|
{showIndeterminatePrimaryProgress
|
||||||
|
? 'Vorbereitung läuft …'
|
||||||
|
: primaryEta
|
||||||
? (hasSeriesBatchProgress ? `ETA aktuelle Episode ${primaryEta}` : `ETA ${primaryEta}`)
|
? (hasSeriesBatchProgress ? `ETA aktuelle Episode ${primaryEta}` : `ETA ${primaryEta}`)
|
||||||
: 'ETA unbekannt'}
|
: 'ETA unbekannt'}
|
||||||
</small>
|
</small>
|
||||||
@@ -3791,10 +3817,14 @@ export default function PipelineStatusCard({
|
|||||||
selectedPostEncodeScriptIds: selectedPostScriptIds,
|
selectedPostEncodeScriptIds: selectedPostScriptIds,
|
||||||
selectedPreEncodeScriptIds: selectedPreScriptIds,
|
selectedPreEncodeScriptIds: selectedPreScriptIds,
|
||||||
selectedPostEncodeChainIds: selectedPostChainIds,
|
selectedPostEncodeChainIds: selectedPostChainIds,
|
||||||
selectedPreEncodeChainIds: selectedPreChainIds,
|
selectedPreEncodeChainIds: selectedPreChainIds
|
||||||
selectedHandBrakePreset
|
|
||||||
};
|
};
|
||||||
startPayload.selectedUserPresetId = selectedUserPresetId || null;
|
if (selectedUserPresetId !== null && selectedUserPresetId !== undefined) {
|
||||||
|
startPayload.selectedUserPresetId = selectedUserPresetId;
|
||||||
|
}
|
||||||
|
if (String(selectedHandBrakePreset || '').trim()) {
|
||||||
|
startPayload.selectedHandBrakePreset = String(selectedHandBrakePreset || '').trim();
|
||||||
|
}
|
||||||
await onStart(retryJobId, startPayload);
|
await onStart(retryJobId, startPayload);
|
||||||
}}
|
}}
|
||||||
loading={busy}
|
loading={busy}
|
||||||
@@ -4139,7 +4169,7 @@ export default function PipelineStatusCard({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
|
{(mediaInfoReview || state === 'READY_TO_ENCODE') && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
|
||||||
<div className="mediainfo-review-block">
|
<div className="mediainfo-review-block">
|
||||||
{!isDiscTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
|
{!isDiscTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
|
||||||
{state === 'READY_TO_ENCODE' && !queueLocked && !isDiscTitleSelectionRequired ? (
|
{state === 'READY_TO_ENCODE' && !queueLocked && !isDiscTitleSelectionRequired ? (
|
||||||
@@ -4150,16 +4180,6 @@ export default function PipelineStatusCard({
|
|||||||
{reviewPlaylistDecisionRequired ? ' Bitte den korrekten Titel per Checkbox auswählen.' : ''}
|
{reviewPlaylistDecisionRequired ? ' Bitte den korrekten Titel per Checkbox auswählen.' : ''}
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
{multipartSettingsLocked ? (
|
|
||||||
<div className="playlist-decision-block">
|
|
||||||
<h3>Multipart-Lock aktiv</h3>
|
|
||||||
<small>
|
|
||||||
Die Encode-Einstellungen wurden automatisch von der bereits vorhandenen Disc übernommen und sind gesperrt.
|
|
||||||
{multipartLockSourceJobId ? ` Quelle: Job #${multipartLockSourceJobId}.` : ''}
|
|
||||||
{multipartLockSourceDiscNumber ? ` Referenz-Disc: ${multipartLockSourceDiscNumber}.` : ''}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{showConverterConfig ? (
|
{showConverterConfig ? (
|
||||||
<div style={{ marginTop: '0.75rem', marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
<div style={{ marginTop: '0.75rem', marginBottom: '0.75rem', padding: '0.75rem', border: '1px solid var(--surface-border, #e0e0e0)', borderRadius: '6px', background: 'var(--surface-ground, #f8f8f8)' }}>
|
||||||
<div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
|
<div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
|
||||||
@@ -4227,7 +4247,7 @@ export default function PipelineStatusCard({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{allowConverterConfigEdit && !hasSelectedPreset ? (
|
{allowConverterConfigEdit && !hasEffectiveSelectedPreset ? (
|
||||||
<small style={{ display: 'block', marginTop: '0.5rem', color: 'var(--red-600)' }}>
|
<small style={{ display: 'block', marginTop: '0.5rem', color: 'var(--red-600)' }}>
|
||||||
Bitte wähle ein User- oder HandBrake-Preset aus, bevor das Encoding starten kann.
|
Bitte wähle ein User- oder HandBrake-Preset aus, bevor das Encoding starten kann.
|
||||||
</small>
|
</small>
|
||||||
@@ -4245,6 +4265,23 @@ export default function PipelineStatusCard({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<MediaInfoReviewPanel
|
||||||
|
review={mediaInfoReview}
|
||||||
|
presetDisplayValue={presetDisplayValue}
|
||||||
|
commandOutputPath={commandOutputPath}
|
||||||
|
commandOutputPathByTitle={commandOutputPathByTitle}
|
||||||
|
topContent={(
|
||||||
|
<>
|
||||||
|
{multipartSettingsLocked ? (
|
||||||
|
<div className="playlist-decision-block">
|
||||||
|
<h3>Multipart-Lock aktiv</h3>
|
||||||
|
<small>
|
||||||
|
Die Encode-Einstellungen wurden automatisch von der bereits vorhandenen Disc übernommen und sind gesperrt.
|
||||||
|
{multipartLockSourceJobId ? ` Quelle: Job #${multipartLockSourceJobId}.` : ''}
|
||||||
|
{multipartLockSourceDiscNumber ? ` Referenz-Disc: ${multipartLockSourceDiscNumber}.` : ''}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{isSeriesDvdReview && hasMultiEpisodeInJob ? (
|
{isSeriesDvdReview && hasMultiEpisodeInJob ? (
|
||||||
<div className="playlist-decision-block">
|
<div className="playlist-decision-block">
|
||||||
<h3>Hinweis Episoden-Zuordnung</h3>
|
<h3>Hinweis Episoden-Zuordnung</h3>
|
||||||
@@ -4256,11 +4293,21 @@ export default function PipelineStatusCard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<MediaInfoReviewPanel
|
{!showConverterConfig
|
||||||
review={mediaInfoReview}
|
&& requiresPresetSelection
|
||||||
presetDisplayValue={presetDisplayValue}
|
&& allowReviewSelectionEdit
|
||||||
commandOutputPath={commandOutputPath}
|
&& !hasEffectiveSelectedPreset
|
||||||
commandOutputPathByTitle={commandOutputPathByTitle}
|
&& state === 'READY_TO_ENCODE'
|
||||||
|
&& mediaInfoReview ? (
|
||||||
|
<div className="playlist-decision-block">
|
||||||
|
<h3>Preset-Auswahl erforderlich</h3>
|
||||||
|
<small>
|
||||||
|
Es ist weder ein User-Preset noch ein Settings-Preset gesetzt. Bitte ein Preset auswählen, bevor das Encoding starten kann.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
|
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
|
||||||
selectedEncodeTitleIds={normalizeTitleIdList(selectedEncodeTitleIds)}
|
selectedEncodeTitleIds={normalizeTitleIdList(selectedEncodeTitleIds)}
|
||||||
allowTitleSelection={allowTitleSelectionInReview}
|
allowTitleSelection={allowTitleSelectionInReview}
|
||||||
@@ -4451,8 +4498,24 @@ export default function PipelineStatusCard({
|
|||||||
postEncodeItems={postEncodeItems}
|
postEncodeItems={postEncodeItems}
|
||||||
userPresets={effectiveUserPresets}
|
userPresets={effectiveUserPresets}
|
||||||
selectedUserPresetId={selectedUserPresetId}
|
selectedUserPresetId={selectedUserPresetId}
|
||||||
onUserPresetChange={showConverterConfig || multipartSettingsLocked ? null : (presetId) => setSelectedUserPresetId(presetId)}
|
onUserPresetChange={showConverterConfig || multipartSettingsLocked ? null : (presetId) => {
|
||||||
|
const nextPresetId = Number.isFinite(Number(presetId)) && Number(presetId) > 0
|
||||||
|
? Math.trunc(Number(presetId))
|
||||||
|
: null;
|
||||||
|
setSelectedUserPresetId(nextPresetId);
|
||||||
|
if (nextPresetId) {
|
||||||
|
setSelectedHandBrakePreset('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
selectedHandBrakePreset={selectedHandBrakePreset}
|
selectedHandBrakePreset={selectedHandBrakePreset}
|
||||||
|
onHandBrakePresetChange={showConverterConfig || multipartSettingsLocked ? null : (presetName) => {
|
||||||
|
const nextPresetName = String(presetName || '').trim();
|
||||||
|
setSelectedHandBrakePreset(nextPresetName);
|
||||||
|
if (nextPresetName) {
|
||||||
|
setSelectedUserPresetId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
handBrakePresetOptions={handBrakePresetOptions}
|
||||||
allowEncodeItemSelection={allowEncodeItemSelectionInReview}
|
allowEncodeItemSelection={allowEncodeItemSelectionInReview}
|
||||||
comparisonReviewItems={multipartComparisonReviews}
|
comparisonReviewItems={multipartComparisonReviews}
|
||||||
onAddPreEncodeItem={(itemType) => {
|
onAddPreEncodeItem={(itemType) => {
|
||||||
|
|||||||
@@ -1,357 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { Card } from 'primereact/card';
|
|
||||||
import { Button } from 'primereact/button';
|
|
||||||
import { InputText } from 'primereact/inputtext';
|
|
||||||
import { Tag } from 'primereact/tag';
|
|
||||||
import { Paginator } from 'primereact/paginator';
|
|
||||||
import { api } from '../api/client';
|
|
||||||
import { classifyJob } from '../utils/jobTaxonomy';
|
|
||||||
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
|
|
||||||
|
|
||||||
const ACTIVE_STATUSES = [
|
|
||||||
'ANALYZING',
|
|
||||||
'METADATA_SELECTION',
|
|
||||||
'WAITING_FOR_USER_DECISION',
|
|
||||||
'READY_TO_START',
|
|
||||||
'MEDIAINFO_CHECK',
|
|
||||||
'READY_TO_ENCODE',
|
|
||||||
'RIPPING',
|
|
||||||
'ENCODING',
|
|
||||||
'CD_METADATA_SELECTION',
|
|
||||||
'CD_READY_TO_RIP',
|
|
||||||
'CD_ANALYZING',
|
|
||||||
'CD_RIPPING',
|
|
||||||
'CD_ENCODING'
|
|
||||||
];
|
|
||||||
|
|
||||||
const STATUS_FILTERS = [
|
|
||||||
{ key: 'all', label: 'Alle' },
|
|
||||||
{ key: 'active', label: 'Aktiv' },
|
|
||||||
{ key: 'errors', label: 'Fehler/Abbruch' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const VIEW_FILTERS = [
|
|
||||||
{ key: 'all', label: 'Alle Jobs' },
|
|
||||||
{ key: 'ripper', label: 'Ripper-View' },
|
|
||||||
{ key: 'converter', label: 'Converter-View' },
|
|
||||||
{ key: 'audiobook', label: 'Audiobooks' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function normalizeJobId(value) {
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Math.trunc(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUpdatedAt(value) {
|
|
||||||
if (!value) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
const parsed = new Date(value);
|
|
||||||
if (Number.isNaN(parsed.getTime())) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
return parsed.toLocaleString('de-DE');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKindLabel(meta) {
|
|
||||||
if (meta.family === 'converter') {
|
|
||||||
if (meta.converterMediaType === 'audio') {
|
|
||||||
return 'Converter Audio';
|
|
||||||
}
|
|
||||||
if (meta.converterMediaType === 'iso') {
|
|
||||||
return 'Converter ISO';
|
|
||||||
}
|
|
||||||
return 'Converter Video';
|
|
||||||
}
|
|
||||||
if (meta.family === 'audiobook') {
|
|
||||||
return 'Audiobook';
|
|
||||||
}
|
|
||||||
if (meta.mediaType === 'bluray') {
|
|
||||||
return 'Blu-ray';
|
|
||||||
}
|
|
||||||
if (meta.mediaType === 'dvd') {
|
|
||||||
return 'DVD';
|
|
||||||
}
|
|
||||||
if (meta.mediaType === 'cd') {
|
|
||||||
return 'CD';
|
|
||||||
}
|
|
||||||
return 'Sonstiges';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKindSeverity(meta) {
|
|
||||||
if (meta.family === 'converter') {
|
|
||||||
return 'info';
|
|
||||||
}
|
|
||||||
if (meta.family === 'audiobook') {
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
if (meta.mediaType === 'bluray' || meta.mediaType === 'dvd') {
|
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
return 'secondary';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JobsInboxPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [jobs, setJobs] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [viewFilter, setViewFilter] = useState('all');
|
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [queuedJobIds, setQueuedJobIds] = useState(new Set());
|
|
||||||
const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
|
|
||||||
const [first, setFirst] = useState(0);
|
|
||||||
const [rows, setRows] = useState(25);
|
|
||||||
|
|
||||||
const loadJobs = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const query = { limit: 500, lite: true };
|
|
||||||
if (statusFilter === 'active') {
|
|
||||||
query.statuses = ACTIVE_STATUSES;
|
|
||||||
} else if (statusFilter === 'errors') {
|
|
||||||
query.statuses = ['ERROR', 'CANCELLED'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [jobsResponse, queueResponse] = await Promise.allSettled([
|
|
||||||
api.getJobs(query),
|
|
||||||
api.getPipelineQueue()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const nextJobs = jobsResponse.status === 'fulfilled' && Array.isArray(jobsResponse.value?.jobs)
|
|
||||||
? jobsResponse.value.jobs
|
|
||||||
: [];
|
|
||||||
setJobs(nextJobs);
|
|
||||||
|
|
||||||
if (queueResponse.status === 'fulfilled') {
|
|
||||||
const rows = Array.isArray(queueResponse.value?.queue?.queuedJobs)
|
|
||||||
? queueResponse.value.queue.queuedJobs
|
|
||||||
: [];
|
|
||||||
setQueuedJobIds(new Set(rows.map((item) => normalizeJobId(item?.jobId)).filter(Boolean)));
|
|
||||||
} else {
|
|
||||||
setQueuedJobIds(new Set());
|
|
||||||
}
|
|
||||||
setLastUpdatedAt(new Date().toISOString());
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [statusFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadJobs();
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
loadJobs().catch(() => null);
|
|
||||||
}, 7000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [loadJobs]);
|
|
||||||
|
|
||||||
const jobsWithMeta = useMemo(() => (
|
|
||||||
jobs.map((job) => ({
|
|
||||||
job,
|
|
||||||
meta: classifyJob(job)
|
|
||||||
}))
|
|
||||||
), [jobs]);
|
|
||||||
|
|
||||||
const viewCounts = useMemo(() => {
|
|
||||||
let all = 0;
|
|
||||||
let ripper = 0;
|
|
||||||
let converter = 0;
|
|
||||||
let audiobook = 0;
|
|
||||||
for (const item of jobsWithMeta) {
|
|
||||||
all += 1;
|
|
||||||
if (item.meta.family === 'converter') {
|
|
||||||
converter += 1;
|
|
||||||
} else {
|
|
||||||
ripper += 1;
|
|
||||||
}
|
|
||||||
if (item.meta.family === 'audiobook') {
|
|
||||||
audiobook += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { all, ripper, converter, audiobook };
|
|
||||||
}, [jobsWithMeta]);
|
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
|
||||||
const normalizedSearch = String(search || '').trim().toLowerCase();
|
|
||||||
return jobsWithMeta.filter(({ job, meta }) => {
|
|
||||||
if (viewFilter === 'ripper' && meta.family === 'converter') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (viewFilter === 'converter' && meta.family !== 'converter') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (viewFilter === 'audiobook' && meta.family !== 'audiobook') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!normalizedSearch) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const haystack = [
|
|
||||||
job?.title,
|
|
||||||
job?.detected_title,
|
|
||||||
job?.imdb_id,
|
|
||||||
job?.status,
|
|
||||||
job?.job_kind,
|
|
||||||
job?.media_type
|
|
||||||
]
|
|
||||||
.map((value) => String(value || '').trim().toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
return haystack.includes(normalizedSearch);
|
|
||||||
});
|
|
||||||
}, [jobsWithMeta, search, viewFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFirst(0);
|
|
||||||
}, [viewFilter, statusFilter, search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (filteredRows.length === 0) {
|
|
||||||
if (first !== 0) {
|
|
||||||
setFirst(0);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (first >= filteredRows.length) {
|
|
||||||
const pageStart = Math.max(0, Math.floor((filteredRows.length - 1) / rows) * rows);
|
|
||||||
setFirst(pageStart);
|
|
||||||
}
|
|
||||||
}, [filteredRows.length, first, rows]);
|
|
||||||
|
|
||||||
const pagedRows = useMemo(
|
|
||||||
() => filteredRows.slice(first, first + rows),
|
|
||||||
[filteredRows, first, rows]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="Job Inbox"
|
|
||||||
subTitle="Zentrale Jobliste mit denselben Datensätzen für Ripper-, Converter- und Audiobook-Sicht."
|
|
||||||
>
|
|
||||||
<div className="job-inbox-toolbar">
|
|
||||||
<div className="job-inbox-filter-row">
|
|
||||||
{VIEW_FILTERS.map((filter) => {
|
|
||||||
const isActive = filter.key === viewFilter;
|
|
||||||
const count = viewCounts[filter.key] ?? 0;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={filter.key}
|
|
||||||
label={`${filter.label} (${count})`}
|
|
||||||
className={isActive ? 'job-inbox-filter-active' : 'job-inbox-filter'}
|
|
||||||
outlined={!isActive}
|
|
||||||
onClick={() => setViewFilter(filter.key)}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="job-inbox-filter-row">
|
|
||||||
{STATUS_FILTERS.map((filter) => {
|
|
||||||
const isActive = filter.key === statusFilter;
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={filter.key}
|
|
||||||
label={filter.label}
|
|
||||||
className={isActive ? 'job-inbox-filter-active' : 'job-inbox-filter'}
|
|
||||||
outlined={!isActive}
|
|
||||||
onClick={() => setStatusFilter(filter.key)}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
label="Aktualisieren"
|
|
||||||
icon="pi pi-refresh"
|
|
||||||
outlined
|
|
||||||
size="small"
|
|
||||||
loading={loading}
|
|
||||||
onClick={() => {
|
|
||||||
loadJobs().catch(() => null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="job-inbox-search-row">
|
|
||||||
<InputText
|
|
||||||
value={search}
|
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
|
||||||
placeholder="Suche nach Titel, Status oder IMDB-ID"
|
|
||||||
className="job-inbox-search-input"
|
|
||||||
/>
|
|
||||||
<small>
|
|
||||||
Letztes Update: {formatUpdatedAt(lastUpdatedAt)} | Treffer: {filteredRows.length}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="job-inbox-list">
|
|
||||||
{loading && filteredRows.length === 0 ? (
|
|
||||||
<p>Inbox wird geladen ...</p>
|
|
||||||
) : filteredRows.length === 0 ? (
|
|
||||||
<p>Keine Jobs für den aktuellen Filter gefunden.</p>
|
|
||||||
) : (
|
|
||||||
pagedRows.map(({ job, meta }) => {
|
|
||||||
const jobId = normalizeJobId(job?.id);
|
|
||||||
if (!jobId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const isQueued = queuedJobIds.has(jobId);
|
|
||||||
const normalizedStatus = normalizeStatus(job?.status);
|
|
||||||
const targetPath = meta.family === 'converter' ? '/converter' : '/ripper';
|
|
||||||
const jobTitle = String(job?.title || job?.detected_title || '').trim() || `Job #${jobId}`;
|
|
||||||
return (
|
|
||||||
<article key={jobId} className="job-inbox-row">
|
|
||||||
<div className="job-inbox-row-main">
|
|
||||||
<strong>#{jobId} | {jobTitle}</strong>
|
|
||||||
<small>
|
|
||||||
Status: {getStatusLabel(normalizedStatus, { queued: isQueued })}
|
|
||||||
{' | '}
|
|
||||||
Aktualisiert: {formatUpdatedAt(job?.updated_at || job?.created_at || null)}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div className="job-inbox-row-tags">
|
|
||||||
<Tag value={getKindLabel(meta)} severity={getKindSeverity(meta)} />
|
|
||||||
<Tag
|
|
||||||
value={getStatusLabel(normalizedStatus, { queued: isQueued })}
|
|
||||||
severity={getStatusSeverity(normalizedStatus, { queued: isQueued })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="job-inbox-row-actions">
|
|
||||||
<Button
|
|
||||||
label={meta.family === 'converter' ? 'Im Converter öffnen' : 'Im Ripper öffnen'}
|
|
||||||
icon={meta.family === 'converter' ? 'pi pi-external-link' : 'pi pi-arrow-right'}
|
|
||||||
outlined
|
|
||||||
size="small"
|
|
||||||
onClick={() => navigate(targetPath)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredRows.length > 0 ? (
|
|
||||||
<div className="job-inbox-pagination">
|
|
||||||
<Paginator
|
|
||||||
first={first}
|
|
||||||
rows={rows}
|
|
||||||
totalRecords={filteredRows.length}
|
|
||||||
rowsPerPageOptions={[25, 50, 100]}
|
|
||||||
onPageChange={(event) => {
|
|
||||||
setFirst(event.first);
|
|
||||||
setRows(event.rows);
|
|
||||||
}}
|
|
||||||
template="PrevPageLink PageLinks NextPageLink RowsPerPageDropdown CurrentPageReport"
|
|
||||||
currentPageReportTemplate="{first} - {last} von {totalRecords}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -129,6 +129,16 @@ function formatPercent(value) {
|
|||||||
return `${Math.trunc(parsed)}%`;
|
return `${Math.trunc(parsed)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlaylistTrackPreparationStatus(value) {
|
||||||
|
const normalized = String(value || '').trim().toUpperCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return normalized.includes('PLAYLIST')
|
||||||
|
&& normalized.includes('TRACKDATEN')
|
||||||
|
&& normalized.includes('VORBEREIT');
|
||||||
|
}
|
||||||
|
|
||||||
function formatTemperature(value) {
|
function formatTemperature(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed)) {
|
if (!Number.isFinite(parsed)) {
|
||||||
@@ -3135,6 +3145,7 @@ export default function RipperPage({
|
|||||||
let showProgress = false;
|
let showProgress = false;
|
||||||
let progressValue = 0;
|
let progressValue = 0;
|
||||||
let statusText = null;
|
let statusText = null;
|
||||||
|
let showIndeterminateDriveProgress = false;
|
||||||
|
|
||||||
if (cdDrive) {
|
if (cdDrive) {
|
||||||
const s = String(cdDrive.state || '').toUpperCase();
|
const s = String(cdDrive.state || '').toUpperCase();
|
||||||
@@ -3148,6 +3159,7 @@ export default function RipperPage({
|
|||||||
showProgress = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(s);
|
showProgress = ['CD_RIPPING', 'CD_ENCODING', 'CD_ANALYZING'].includes(s);
|
||||||
progressValue = Number(cdDrive.progress ?? 0);
|
progressValue = Number(cdDrive.progress ?? 0);
|
||||||
statusText = cdDrive.statusText || null;
|
statusText = cdDrive.statusText || null;
|
||||||
|
showIndeterminateDriveProgress = isPlaylistTrackPreparationStatus(statusText);
|
||||||
} else if (pipelineDevice) {
|
} else if (pipelineDevice) {
|
||||||
const s = String(drv.pipelineState || '').toUpperCase();
|
const s = String(drv.pipelineState || '').toUpperCase();
|
||||||
stateLabel = s || 'DISC_DETECTED';
|
stateLabel = s || 'DISC_DETECTED';
|
||||||
@@ -3287,7 +3299,9 @@ export default function RipperPage({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showProgress && (
|
{showProgress && (
|
||||||
<ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
showIndeterminateDriveProgress
|
||||||
|
? <ProgressBar mode="indeterminate" style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||||
|
: <ProgressBar value={progressValue} style={{ height: '5px', margin: '0.2rem 0 0' }} showValue={false} />
|
||||||
)}
|
)}
|
||||||
{showProgress && statusText && (
|
{showProgress && statusText && (
|
||||||
<div className="drive-list-status-text">{statusText}</div>
|
<div className="drive-list-status-text">{statusText}</div>
|
||||||
@@ -3463,7 +3477,8 @@ export default function RipperPage({
|
|||||||
const mediaProfile = String(pipelineForJob?.context?.mediaProfile || '').trim().toLowerCase();
|
const mediaProfile = String(pipelineForJob?.context?.mediaProfile || '').trim().toLowerCase();
|
||||||
const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
|
const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
|
||||||
const pipelineStage = String(pipelineForJob?.context?.stage || '').trim().toUpperCase();
|
const pipelineStage = String(pipelineForJob?.context?.stage || '').trim().toUpperCase();
|
||||||
const pipelineStatusText = String(pipelineForJob?.statusText || '').trim().toUpperCase();
|
const pipelineStatusTextRaw = String(pipelineForJob?.statusText || '').trim();
|
||||||
|
const pipelineStatusText = pipelineStatusTextRaw.toUpperCase();
|
||||||
const isCdJob = jobState.startsWith('CD_')
|
const isCdJob = jobState.startsWith('CD_')
|
||||||
|| pipelineStage.startsWith('CD_')
|
|| pipelineStage.startsWith('CD_')
|
||||||
|| mediaProfile === 'cd'
|
|| mediaProfile === 'cd'
|
||||||
@@ -3509,6 +3524,8 @@ export default function RipperPage({
|
|||||||
? Math.max(0, Math.min(100, rawProgress))
|
? Math.max(0, Math.min(100, rawProgress))
|
||||||
: 0;
|
: 0;
|
||||||
const progressLabel = `${Math.trunc(clampedProgress)}%`;
|
const progressLabel = `${Math.trunc(clampedProgress)}%`;
|
||||||
|
const showIndeterminateProgress = !hasSeriesBatchProgress
|
||||||
|
&& isPlaylistTrackPreparationStatus(pipelineStatusTextRaw);
|
||||||
const etaLabel = isCancelledState ? '' : String(
|
const etaLabel = isCancelledState ? '' : String(
|
||||||
hasSeriesBatchProgress
|
hasSeriesBatchProgress
|
||||||
? (runningSeriesChild?.eta || pipelineForJob?.eta || '')
|
? (runningSeriesChild?.eta || pipelineForJob?.eta || '')
|
||||||
@@ -3982,9 +3999,16 @@ export default function RipperPage({
|
|||||||
: null}
|
: null}
|
||||||
<JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
|
<JobStepChecks backupSuccess={Boolean(job?.backupSuccess)} encodeSuccess={Boolean(job?.encodeSuccess)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="ripper-job-row-progress" aria-label={`Job Fortschritt ${progressLabel}`}>
|
<div
|
||||||
|
className="ripper-job-row-progress"
|
||||||
|
aria-label={showIndeterminateProgress ? 'Job Fortschritt Vorbereitung läuft' : `Job Fortschritt ${progressLabel}`}
|
||||||
|
>
|
||||||
|
{showIndeterminateProgress ? (
|
||||||
|
<ProgressBar mode="indeterminate" showValue={false} />
|
||||||
|
) : (
|
||||||
<ProgressBar value={clampedProgress} showValue={false} />
|
<ProgressBar value={clampedProgress} showValue={false} />
|
||||||
<small>{etaLabel ? `${progressLabel} | ETA ${etaLabel}` : progressLabel}</small>
|
)}
|
||||||
|
<small>{showIndeterminateProgress ? 'Vorbereitung läuft …' : (etaLabel ? `${progressLabel} | ETA ${etaLabel}` : progressLabel)}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i className="pi pi-angle-down" aria-hidden="true" />
|
<i className="pi pi-angle-down" aria-hidden="true" />
|
||||||
@@ -4051,6 +4075,8 @@ export default function RipperPage({
|
|||||||
: (jobProgressEntry?.progress ?? 0)
|
: (jobProgressEntry?.progress ?? 0)
|
||||||
);
|
);
|
||||||
const clampedQueueProgress = Number.isFinite(rawQueueProgress) ? Math.max(0, Math.min(100, rawQueueProgress)) : 0;
|
const clampedQueueProgress = Number.isFinite(rawQueueProgress) ? Math.max(0, Math.min(100, rawQueueProgress)) : 0;
|
||||||
|
const queueStatusText = String(jobProgressEntry?.statusText || '').trim();
|
||||||
|
const showIndeterminateQueueProgress = isPlaylistTrackPreparationStatus(queueStatusText);
|
||||||
return (
|
return (
|
||||||
<div key={`running-${item.jobId}`} className="pipeline-queue-entry-wrap">
|
<div key={`running-${item.jobId}`} className="pipeline-queue-entry-wrap">
|
||||||
<div className="pipeline-queue-item running queue-job-item">
|
<div className="pipeline-queue-item running queue-job-item">
|
||||||
@@ -4062,8 +4088,12 @@ export default function RipperPage({
|
|||||||
</strong>
|
</strong>
|
||||||
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
||||||
<small>#{item.jobId} | {getMergeAwareStatusLabel(item, item.status)}</small>
|
<small>#{item.jobId} | {getMergeAwareStatusLabel(item, item.status)}</small>
|
||||||
{clampedQueueProgress > 0 && (
|
{showIndeterminateQueueProgress ? (
|
||||||
<ProgressBar value={clampedQueueProgress} style={{ height: '5px' }} showValue={false} />
|
<ProgressBar mode="indeterminate" style={{ height: '5px' }} showValue={false} />
|
||||||
|
) : (
|
||||||
|
clampedQueueProgress > 0
|
||||||
|
? <ProgressBar value={clampedQueueProgress} style={{ height: '5px' }} showValue={false} />
|
||||||
|
: null
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasScriptSummary ? (
|
{hasScriptSummary ? (
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ import CronJobsTab from '../components/CronJobsTab';
|
|||||||
import { confirmModal } from '../utils/confirmModal';
|
import { confirmModal } from '../utils/confirmModal';
|
||||||
|
|
||||||
const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode';
|
const EXPERT_MODE_SETTING_KEY = 'ui_expert_mode';
|
||||||
|
const USER_PRESET_DEFAULT_SLOTS = Object.freeze([
|
||||||
|
{ key: 'bluray_movie', label: 'Standard-Userpreset für BluRay Film', mediaType: 'bluray' },
|
||||||
|
{ key: 'bluray_series', label: 'Standard-Userpreset für BluRay Serie', mediaType: 'bluray' },
|
||||||
|
{ key: 'dvd_movie', label: 'Standard-Userpreset für DVD Film', mediaType: 'dvd' },
|
||||||
|
{ key: 'dvd_series', label: 'Standard-Userpreset für DVD Serie', mediaType: 'dvd' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const EMPTY_USER_PRESET_DEFAULTS = Object.freeze(
|
||||||
|
USER_PRESET_DEFAULT_SLOTS.reduce((acc, slot) => ({ ...acc, [slot.key]: null }), {})
|
||||||
|
);
|
||||||
|
|
||||||
// Validates template/string fields: only letters, digits, space, _, -, and {placeholders} allowed
|
// Validates template/string fields: only letters, digits, space, _, -, and {placeholders} allowed
|
||||||
function validateSettingValue(key, value) {
|
function validateSettingValue(key, value) {
|
||||||
@@ -45,6 +55,68 @@ function buildValuesMap(categories) {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePositiveId(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 normalizeUserPresetDefaultsMap(raw = {}) {
|
||||||
|
const source = raw && typeof raw === 'object' ? raw : {};
|
||||||
|
const normalized = { ...EMPTY_USER_PRESET_DEFAULTS };
|
||||||
|
for (const slot of USER_PRESET_DEFAULT_SLOTS) {
|
||||||
|
normalized[slot.key] = normalizePositiveId(source[slot.key]);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPresetCompatibleWithSlot(preset, slotMediaType) {
|
||||||
|
const mediaType = String(preset?.mediaType || '').trim().toLowerCase();
|
||||||
|
if (mediaType === 'all') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return mediaType === slotMediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUserPresetDefaultOptions(userPresets, slotMediaType, selectedPresetId = null) {
|
||||||
|
const presets = Array.isArray(userPresets) ? userPresets : [];
|
||||||
|
const options = [{ label: '(keine Standardzuordnung)', value: null }];
|
||||||
|
const seen = new Set([null]);
|
||||||
|
|
||||||
|
for (const preset of presets) {
|
||||||
|
const presetId = normalizePositiveId(preset?.id);
|
||||||
|
if (!presetId || !isPresetCompatibleWithSlot(preset, slotMediaType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(presetId);
|
||||||
|
options.push({
|
||||||
|
label: `#${presetId} - ${preset?.name || 'Preset'}`,
|
||||||
|
value: presetId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSelectedId = normalizePositiveId(selectedPresetId);
|
||||||
|
if (normalizedSelectedId && !seen.has(normalizedSelectedId)) {
|
||||||
|
const existingPreset = presets.find((preset) => normalizePositiveId(preset?.id) === normalizedSelectedId);
|
||||||
|
options.push({
|
||||||
|
label: existingPreset
|
||||||
|
? `#${normalizedSelectedId} - ${existingPreset?.name || 'Preset'} (nicht kompatibel)`
|
||||||
|
: `#${normalizedSelectedId} - (nicht gefunden)`,
|
||||||
|
value: normalizedSelectedId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
function isSameValue(a, b) {
|
function isSameValue(a, b) {
|
||||||
if (typeof a === 'number' && typeof b === 'number') {
|
if (typeof a === 'number' && typeof b === 'number') {
|
||||||
return Number(a) === Number(b);
|
return Number(a) === Number(b);
|
||||||
@@ -213,6 +285,10 @@ export default function SettingsPage() {
|
|||||||
const [userPresets, setUserPresets] = useState([]);
|
const [userPresets, setUserPresets] = useState([]);
|
||||||
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
|
const [userPresetsLoading, setUserPresetsLoading] = useState(false);
|
||||||
const [userPresetSaving, setUserPresetSaving] = useState(false);
|
const [userPresetSaving, setUserPresetSaving] = useState(false);
|
||||||
|
const [userPresetDefaults, setUserPresetDefaults] = useState(EMPTY_USER_PRESET_DEFAULTS);
|
||||||
|
const [userPresetDefaultsDraft, setUserPresetDefaultsDraft] = useState(EMPTY_USER_PRESET_DEFAULTS);
|
||||||
|
const [userPresetDefaultsLoading, setUserPresetDefaultsLoading] = useState(false);
|
||||||
|
const [userPresetDefaultsSaving, setUserPresetDefaultsSaving] = useState(false);
|
||||||
const [userPresetEditor, setUserPresetEditor] = useState({
|
const [userPresetEditor, setUserPresetEditor] = useState({
|
||||||
open: false,
|
open: false,
|
||||||
id: null,
|
id: null,
|
||||||
@@ -235,6 +311,23 @@ export default function SettingsPage() {
|
|||||||
),
|
),
|
||||||
[handBrakePresetSourceOptions, userPresetEditor.handbrakePreset]
|
[handBrakePresetSourceOptions, userPresetEditor.handbrakePreset]
|
||||||
);
|
);
|
||||||
|
const userPresetDefaultsDirty = useMemo(
|
||||||
|
() => USER_PRESET_DEFAULT_SLOTS.some((slot) => (
|
||||||
|
normalizePositiveId(userPresetDefaultsDraft?.[slot.key]) !== normalizePositiveId(userPresetDefaults?.[slot.key])
|
||||||
|
)),
|
||||||
|
[userPresetDefaultsDraft, userPresetDefaults]
|
||||||
|
);
|
||||||
|
const userPresetDefaultOptionsBySlot = useMemo(() => {
|
||||||
|
const next = {};
|
||||||
|
for (const slot of USER_PRESET_DEFAULT_SLOTS) {
|
||||||
|
next[slot.key] = buildUserPresetDefaultOptions(
|
||||||
|
userPresets,
|
||||||
|
slot.mediaType,
|
||||||
|
userPresetDefaultsDraft?.[slot.key]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}, [userPresets, userPresetDefaultsDraft]);
|
||||||
|
|
||||||
const loadScripts = async ({ silent = false } = {}) => {
|
const loadScripts = async ({ silent = false } = {}) => {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
@@ -291,6 +384,26 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadUserPresetDefaults = async ({ silent = false } = {}) => {
|
||||||
|
if (!silent) {
|
||||||
|
setUserPresetDefaultsLoading(true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await api.getUserPresetDefaults();
|
||||||
|
const normalized = normalizeUserPresetDefaultsMap(response?.defaults || {});
|
||||||
|
setUserPresetDefaults(normalized);
|
||||||
|
setUserPresetDefaultsDraft(normalized);
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) {
|
||||||
|
toastRef.current?.show({ severity: 'error', summary: 'Preset-Standards', detail: error.message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!silent) {
|
||||||
|
setUserPresetDefaultsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openNewUserPreset = () => {
|
const openNewUserPreset = () => {
|
||||||
setUserPresetEditor({ open: true, id: null, name: '', mediaType: 'all', handbrakePreset: '', extraArgs: '', description: '' });
|
setUserPresetEditor({ open: true, id: null, name: '', mediaType: 'all', handbrakePreset: '', extraArgs: '', description: '' });
|
||||||
setUserPresetErrors({});
|
setUserPresetErrors({});
|
||||||
@@ -352,12 +465,59 @@ export default function SettingsPage() {
|
|||||||
try {
|
try {
|
||||||
await api.deleteUserPreset(presetId);
|
await api.deleteUserPreset(presetId);
|
||||||
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset gelöscht.' });
|
toastRef.current?.show({ severity: 'success', summary: 'Preset', detail: 'Preset gelöscht.' });
|
||||||
await loadUserPresets({ silent: true });
|
await Promise.all([
|
||||||
|
loadUserPresets({ silent: true }),
|
||||||
|
loadUserPresetDefaults({ silent: true })
|
||||||
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastRef.current?.show({ severity: 'error', summary: 'Preset löschen', detail: error.message });
|
toastRef.current?.show({ severity: 'error', summary: 'Preset löschen', detail: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeUserPresetDefault = (slotKey, rawValue) => {
|
||||||
|
const normalized = normalizePositiveId(rawValue);
|
||||||
|
setUserPresetDefaultsDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[slotKey]: normalized
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetUserPresetDefaults = () => {
|
||||||
|
setUserPresetDefaultsDraft({ ...userPresetDefaults });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveUserPresetDefaults = async () => {
|
||||||
|
if (!userPresetDefaultsDirty) {
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Preset-Standards',
|
||||||
|
detail: 'Keine Änderungen zum Speichern.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUserPresetDefaultsSaving(true);
|
||||||
|
try {
|
||||||
|
const payload = normalizeUserPresetDefaultsMap(userPresetDefaultsDraft);
|
||||||
|
const response = await api.updateUserPresetDefaults(payload);
|
||||||
|
const normalized = normalizeUserPresetDefaultsMap(response?.defaults || payload);
|
||||||
|
setUserPresetDefaults(normalized);
|
||||||
|
setUserPresetDefaultsDraft(normalized);
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Preset-Standards',
|
||||||
|
detail: 'Standard-Zuordnungen gespeichert.'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Preset-Standards',
|
||||||
|
detail: error.message
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setUserPresetDefaultsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadEffectivePaths = async ({ silent = false } = {}) => {
|
const loadEffectivePaths = async ({ silent = false } = {}) => {
|
||||||
try {
|
try {
|
||||||
const paths = await api.getEffectivePaths({ forceRefresh: true });
|
const paths = await api.getEffectivePaths({ forceRefresh: true });
|
||||||
@@ -430,6 +590,7 @@ export default function SettingsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load();
|
||||||
loadUserPresets();
|
loadUserPresets();
|
||||||
|
loadUserPresetDefaults();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dirtyKeys = useMemo(() => {
|
const dirtyKeys = useMemo(() => {
|
||||||
@@ -1670,12 +1831,57 @@ export default function SettingsPage() {
|
|||||||
label="Presets neu laden"
|
label="Presets neu laden"
|
||||||
icon="pi pi-refresh"
|
icon="pi pi-refresh"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
onClick={() => loadUserPresets()}
|
onClick={() => Promise.all([loadUserPresets(), loadUserPresetDefaults()])}
|
||||||
loading={userPresetsLoading}
|
loading={userPresetsLoading || userPresetDefaultsLoading}
|
||||||
disabled={userPresetSaving}
|
disabled={userPresetSaving || userPresetDefaultsSaving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="script-list-box" style={{ marginTop: '1rem' }}>
|
||||||
|
<h4>Standard-Zuordnungen</h4>
|
||||||
|
<small>
|
||||||
|
Weise für jeden Medientyp/Fall ein Standard-Userpreset zu. Diese Zuordnungen werden persistent gespeichert.
|
||||||
|
</small>
|
||||||
|
<div className="user-preset-default-grid" style={{ marginTop: '0.8rem' }}>
|
||||||
|
{USER_PRESET_DEFAULT_SLOTS.map((slot) => (
|
||||||
|
<div key={slot.key} className="user-preset-default-grid-item">
|
||||||
|
<label htmlFor={`user-preset-default-${slot.key}`} style={{ display: 'block', marginBottom: '0.3rem' }}>
|
||||||
|
{slot.label}
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
id={`user-preset-default-${slot.key}`}
|
||||||
|
value={normalizePositiveId(userPresetDefaultsDraft?.[slot.key])}
|
||||||
|
options={userPresetDefaultOptionsBySlot?.[slot.key] || []}
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
onChange={(event) => handleChangeUserPresetDefault(slot.key, event.value)}
|
||||||
|
placeholder="Preset auswählen"
|
||||||
|
showClear
|
||||||
|
disabled={userPresetDefaultsLoading || userPresetDefaultsSaving}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="actions-row" style={{ marginTop: '0.8rem' }}>
|
||||||
|
<Button
|
||||||
|
label="Zuordnungen speichern"
|
||||||
|
icon="pi pi-save"
|
||||||
|
onClick={handleSaveUserPresetDefaults}
|
||||||
|
loading={userPresetDefaultsSaving}
|
||||||
|
disabled={userPresetDefaultsLoading || !userPresetDefaultsDirty}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Änderungen verwerfen"
|
||||||
|
icon="pi pi-undo"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
onClick={handleResetUserPresetDefaults}
|
||||||
|
disabled={userPresetDefaultsLoading || userPresetDefaultsSaving || !userPresetDefaultsDirty}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<small>
|
<small>
|
||||||
Encode-Presets fassen ein HandBrake-Preset und zusätzliche CLI-Argumente zusammen.
|
Encode-Presets fassen ein HandBrake-Preset und zusätzliche CLI-Argumente zusammen.
|
||||||
Sie sind medienbezogen (Blu-ray, DVD oder Universell) und können vor dem Encode
|
Sie sind medienbezogen (Blu-ray, DVD oder Universell) und können vor dem Encode
|
||||||
|
|||||||
+21
-102
@@ -345,106 +345,6 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-inbox-toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-filter-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-filter.p-button,
|
|
||||||
.job-inbox-filter-active.p-button {
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-search-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-search-input {
|
|
||||||
width: min(40rem, 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.9rem;
|
|
||||||
border: 1px solid var(--surface-border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.75rem 0.85rem;
|
|
||||||
background: var(--surface-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row-main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.22rem;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row-main strong {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row-main small {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row-tags {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-row-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-pagination {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-inbox-pagination .p-paginator {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.job-inbox-row {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.job-inbox-row-tags,
|
|
||||||
.job-inbox-row-actions {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.job-inbox-pagination .p-paginator {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.ripper-3col-grid {
|
.ripper-3col-grid {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
|
||||||
@@ -2809,6 +2709,22 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-preset-default-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-preset-default-grid-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.user-preset-default-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.script-editor-fields {
|
.script-editor-fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
@@ -3730,16 +3646,19 @@ body {
|
|||||||
|
|
||||||
.playlist-decision-block {
|
.playlist-decision-block {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
border: 1px solid var(--rip-border);
|
border: 1px solid #d4a29e;
|
||||||
|
border-left: 4px solid #8b2c2c;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: var(--rip-panel-soft);
|
background: #f8e1df;
|
||||||
|
color: #8b2c2c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-decision-block h3 {
|
.playlist-decision-block h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: #7a2525;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rip-error-notice {
|
.rip-error-notice {
|
||||||
|
|||||||
@@ -297,29 +297,3 @@ export function isSeriesDvdJob(job) {
|
|||||||
export function isConverterJob(job) {
|
export function isConverterJob(job) {
|
||||||
return resolveMediaType(job) === 'converter';
|
return resolveMediaType(job) === 'converter';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classifyJob(job) {
|
|
||||||
const jobKind = resolveJobKind(job);
|
|
||||||
const mediaType = resolveMediaType(job);
|
|
||||||
const converterMediaType = normalizeConverterMediaType(
|
|
||||||
(jobKind && jobKind.startsWith('converter_'))
|
|
||||||
? jobKind.replace('converter_', '')
|
|
||||||
: (getEncodePlan(job)?.converterMediaType || job?.converterMediaType)
|
|
||||||
);
|
|
||||||
|
|
||||||
let family = 'other';
|
|
||||||
if (mediaType === 'converter') {
|
|
||||||
family = 'converter';
|
|
||||||
} else if (mediaType === 'audiobook') {
|
|
||||||
family = 'audiobook';
|
|
||||||
} else if (mediaType === 'cd' || mediaType === 'dvd' || mediaType === 'bluray') {
|
|
||||||
family = 'rip';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobKind,
|
|
||||||
mediaType,
|
|
||||||
converterMediaType,
|
|
||||||
family
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ nav:
|
|||||||
- Erster Lauf: getting-started/quickstart.md
|
- Erster Lauf: getting-started/quickstart.md
|
||||||
- GUI-Seiten:
|
- GUI-Seiten:
|
||||||
- gui/index.md
|
- gui/index.md
|
||||||
- Jobs: gui/jobs.md
|
|
||||||
- Ripper: gui/ripper.md
|
- Ripper: gui/ripper.md
|
||||||
- Audiobooks: gui/audiobooks.md
|
- Audiobooks: gui/audiobooks.md
|
||||||
- Settings: gui/settings.md
|
- Settings: gui/settings.md
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.16.1-4",
|
"version": "0.16.1-5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
|
|||||||
Reference in New Issue
Block a user