0.10.2-3 Hop it works
This commit is contained in:
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"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.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -788,7 +788,8 @@ async function removeDeprecatedSettings(db) {
|
|||||||
'filename_template_bluray',
|
'filename_template_bluray',
|
||||||
'filename_template_dvd',
|
'filename_template_dvd',
|
||||||
'output_folder_template_bluray',
|
'output_folder_template_bluray',
|
||||||
'output_folder_template_dvd'
|
'output_folder_template_dvd',
|
||||||
|
'output_extension_audiobook'
|
||||||
];
|
];
|
||||||
for (const key of deprecatedKeys) {
|
for (const key of deprecatedKeys) {
|
||||||
const schemaResult = await db.run('DELETE FROM settings_schema WHERE key = ?', [key]);
|
const schemaResult = await db.run('DELETE FROM settings_schema WHERE key = ?', [key]);
|
||||||
@@ -906,11 +907,6 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
);
|
);
|
||||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('ffprobe_command', 'ffprobe')`);
|
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('ffprobe_command', 'ffprobe')`);
|
||||||
|
|
||||||
await db.run(
|
|
||||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
|
||||||
VALUES ('output_extension_audiobook', 'Tools', 'Ausgabeformat', 'select', 1, 'Dateiendung für finale Audiobook-Datei.', 'mp3', '[{"label":"M4B","value":"m4b"},{"label":"MP3","value":"mp3"},{"label":"FLAC","value":"flac"}]', '{}', 730)`
|
|
||||||
);
|
|
||||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_extension_audiobook', 'mp3')`);
|
|
||||||
|
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const notificationService = require('./notificationService');
|
|||||||
const settingsService = require('./settingsService');
|
const settingsService = require('./settingsService');
|
||||||
const wsService = require('./websocketService');
|
const wsService = require('./websocketService');
|
||||||
const runtimeActivityService = require('./runtimeActivityService');
|
const runtimeActivityService = require('./runtimeActivityService');
|
||||||
|
const { spawnTrackedProcess } = require('./processRunner');
|
||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
|
|
||||||
// Maximale Zeilen pro Log-Eintrag (Output-Truncation)
|
// Maximale Zeilen pro Log-Eintrag (Output-Truncation)
|
||||||
@@ -252,33 +253,57 @@ async function runCronJob(job) {
|
|||||||
let prepared = null;
|
let prepared = null;
|
||||||
try {
|
try {
|
||||||
prepared = await scriptService.createExecutableScriptFile(script, { source: 'cron', cronJobId: job.id });
|
prepared = await scriptService.createExecutableScriptFile(script, { source: 'cron', cronJobId: job.id });
|
||||||
const result = await new Promise((resolve, reject) => {
|
let stdout = '';
|
||||||
const { spawn } = require('child_process');
|
let stderr = '';
|
||||||
const child = spawn(prepared.cmd, prepared.args, {
|
let stdoutTruncated = false;
|
||||||
env: process.env,
|
let stderrTruncated = false;
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
const processHandle = spawnTrackedProcess({
|
||||||
});
|
cmd: prepared.cmd,
|
||||||
let stdout = '';
|
args: prepared.args,
|
||||||
let stderr = '';
|
context: { source: 'cron', cronJobId: job.id, scriptId: script.id },
|
||||||
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
onStdoutLine: (line) => {
|
||||||
child.stderr?.on('data', (chunk) => { stderr += String(chunk); });
|
const next = stdout.length <= MAX_OUTPUT_CHARS
|
||||||
child.on('error', reject);
|
? `${stdout}${line}\n`
|
||||||
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
: stdout;
|
||||||
|
stdout = next.length > MAX_OUTPUT_CHARS ? next.slice(-MAX_OUTPUT_CHARS) : next;
|
||||||
|
stdoutTruncated = stdoutTruncated || next.length > MAX_OUTPUT_CHARS;
|
||||||
|
runtimeActivityService.appendActivityOutput(scriptActivityId, { stdout: line });
|
||||||
|
},
|
||||||
|
onStderrLine: (line) => {
|
||||||
|
const next = stderr.length <= MAX_OUTPUT_CHARS
|
||||||
|
? `${stderr}${line}\n`
|
||||||
|
: stderr;
|
||||||
|
stderr = next.length > MAX_OUTPUT_CHARS ? next.slice(-MAX_OUTPUT_CHARS) : next;
|
||||||
|
stderrTruncated = stderrTruncated || next.length > MAX_OUTPUT_CHARS;
|
||||||
|
runtimeActivityService.appendActivityOutput(scriptActivityId, { stderr: line });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
let exitCode = 0;
|
||||||
|
try {
|
||||||
|
const result = await processHandle.promise;
|
||||||
|
exitCode = Number.isFinite(Number(result?.code)) ? Number(result.code) : 0;
|
||||||
|
} catch (error) {
|
||||||
|
exitCode = Number.isFinite(Number(error?.code)) ? Number(error.code) : null;
|
||||||
|
if (exitCode === null) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
output = [stdout, stderr].filter(Boolean).join('\n');
|
||||||
if (output.length > MAX_OUTPUT_CHARS) output = output.slice(0, MAX_OUTPUT_CHARS) + '\n...[truncated]';
|
if (output.length > MAX_OUTPUT_CHARS) output = output.slice(0, MAX_OUTPUT_CHARS) + '\n...[truncated]';
|
||||||
success = result.code === 0;
|
success = exitCode === 0;
|
||||||
if (!success) errorMessage = `Exit-Code ${result.code}`;
|
if (!success) errorMessage = `Exit-Code ${exitCode}`;
|
||||||
runtimeActivityService.completeActivity(scriptActivityId, {
|
runtimeActivityService.completeActivity(scriptActivityId, {
|
||||||
status: success ? 'success' : 'error',
|
status: success ? 'success' : 'error',
|
||||||
success,
|
success,
|
||||||
outcome: success ? 'success' : 'error',
|
outcome: success ? 'success' : 'error',
|
||||||
exitCode: result.code,
|
exitCode,
|
||||||
message: success ? null : errorMessage,
|
message: success ? null : errorMessage,
|
||||||
output: output || null,
|
output: output || null,
|
||||||
stdout: result.stdout || null,
|
stdout: stdout || null,
|
||||||
stderr: result.stderr || null,
|
stderr: stderr || null,
|
||||||
|
stdoutTruncated,
|
||||||
|
stderrTruncated,
|
||||||
errorMessage: success ? null : (errorMessage || null)
|
errorMessage: success ? null : (errorMessage || null)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ function buildAudiobookOutputConfig(settings, job, makemkvInfo = null, encodePla
|
|||||||
|| audiobookService.DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE
|
|| audiobookService.DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE
|
||||||
).trim() || audiobookService.DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE;
|
).trim() || audiobookService.DEFAULT_AUDIOBOOK_CHAPTER_OUTPUT_TEMPLATE;
|
||||||
const outputFormat = audiobookService.normalizeOutputFormat(
|
const outputFormat = audiobookService.normalizeOutputFormat(
|
||||||
encodePlan?.format || settings?.output_extension || 'mp3'
|
encodePlan?.format || 'm4b'
|
||||||
);
|
);
|
||||||
const numericJobId = Number(fallbackJobId || job?.id || 0);
|
const numericJobId = Number(fallbackJobId || job?.id || 0);
|
||||||
const incompleteFolder = Number.isFinite(numericJobId) && numericJobId > 0
|
const incompleteFolder = Number.isFinite(numericJobId) && numericJobId > 0
|
||||||
@@ -799,6 +799,28 @@ function truncateLine(value, max = 180) {
|
|||||||
return `${raw.slice(0, max)}...`;
|
return `${raw.slice(0, max)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendTailText(currentValue, nextChunk, maxChars = 12000) {
|
||||||
|
const chunk = String(nextChunk || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
if (!chunk) {
|
||||||
|
return {
|
||||||
|
value: currentValue || '',
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const normalizedChunk = chunk.endsWith('\n') ? chunk : `${chunk}\n`;
|
||||||
|
const combined = `${String(currentValue || '')}${normalizedChunk}`;
|
||||||
|
if (combined.length <= maxChars) {
|
||||||
|
return {
|
||||||
|
value: combined,
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: combined.slice(-maxChars),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function extractProgressDetail(source, line) {
|
function extractProgressDetail(source, line) {
|
||||||
const text = truncateLine(line, 220);
|
const text = truncateLine(line, 220);
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -4854,42 +4876,59 @@ class PipelineService extends EventEmitter {
|
|||||||
let prepared = null;
|
let prepared = null;
|
||||||
try {
|
try {
|
||||||
prepared = await scriptService.createExecutableScriptFile(script, { source: 'queue', scriptId: script.id, scriptName: script.name });
|
prepared = await scriptService.createExecutableScriptFile(script, { source: 'queue', scriptId: script.id, scriptName: script.name });
|
||||||
const { spawn } = require('child_process');
|
let stdout = '';
|
||||||
await new Promise((resolve, reject) => {
|
let stderr = '';
|
||||||
const child = spawn(prepared.cmd, prepared.args, { env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
let stdoutTruncated = false;
|
||||||
let stdout = '';
|
let stderrTruncated = false;
|
||||||
let stderr = '';
|
const processHandle = spawnTrackedProcess({
|
||||||
child.stdout?.on('data', (chunk) => {
|
cmd: prepared.cmd,
|
||||||
stdout += String(chunk);
|
args: prepared.args,
|
||||||
if (stdout.length > 12000) {
|
context: { source: 'queue', scriptId: script.id },
|
||||||
stdout = `${stdout.slice(0, 12000)}\n...[truncated]`;
|
onStdoutLine: (line) => {
|
||||||
}
|
const next = appendTailText(stdout, line);
|
||||||
});
|
stdout = next.value;
|
||||||
child.stderr?.on('data', (chunk) => {
|
stdoutTruncated = stdoutTruncated || next.truncated;
|
||||||
stderr += String(chunk);
|
runtimeActivityService.appendActivityOutput(activityId, { stdout: line });
|
||||||
if (stderr.length > 12000) {
|
},
|
||||||
stderr = `${stderr.slice(0, 12000)}\n...[truncated]`;
|
onStderrLine: (line) => {
|
||||||
}
|
const next = appendTailText(stderr, line);
|
||||||
});
|
stderr = next.value;
|
||||||
child.on('error', reject);
|
stderrTruncated = stderrTruncated || next.truncated;
|
||||||
child.on('close', (code) => {
|
runtimeActivityService.appendActivityOutput(activityId, { stderr: line });
|
||||||
logger.info('queue:script:done', { scriptId: script.id, exitCode: code });
|
}
|
||||||
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
||||||
const success = Number(code) === 0;
|
|
||||||
runtimeActivityService.completeActivity(activityId, {
|
|
||||||
status: success ? 'success' : 'error',
|
|
||||||
success,
|
|
||||||
outcome: success ? 'success' : 'error',
|
|
||||||
exitCode: Number.isFinite(Number(code)) ? Number(code) : null,
|
|
||||||
message: success ? 'Queue-Skript abgeschlossen' : `Queue-Skript fehlgeschlagen (Exit ${code})`,
|
|
||||||
output: output || null,
|
|
||||||
stdout: stdout || null,
|
|
||||||
stderr: stderr || null,
|
|
||||||
errorMessage: success ? null : `Queue-Skript fehlgeschlagen (Exit ${code})`
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
let exitCode = 0;
|
||||||
|
let runError = null;
|
||||||
|
try {
|
||||||
|
const result = await processHandle.promise;
|
||||||
|
exitCode = Number.isFinite(Number(result?.code)) ? Number(result.code) : 0;
|
||||||
|
} catch (error) {
|
||||||
|
runError = error;
|
||||||
|
exitCode = Number.isFinite(Number(error?.code)) ? Number(error.code) : null;
|
||||||
|
if (exitCode === null) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('queue:script:done', { scriptId: script.id, exitCode });
|
||||||
|
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
||||||
|
const success = Number(exitCode) === 0;
|
||||||
|
runtimeActivityService.completeActivity(activityId, {
|
||||||
|
status: success ? 'success' : 'error',
|
||||||
|
success,
|
||||||
|
outcome: success ? 'success' : 'error',
|
||||||
|
exitCode: Number.isFinite(Number(exitCode)) ? Number(exitCode) : null,
|
||||||
|
message: success ? 'Queue-Skript abgeschlossen' : `Queue-Skript fehlgeschlagen (Exit ${exitCode ?? 'n/a'})`,
|
||||||
|
output: output || null,
|
||||||
|
stdout: stdout || null,
|
||||||
|
stderr: stderr || null,
|
||||||
|
stdoutTruncated,
|
||||||
|
stderrTruncated,
|
||||||
|
errorMessage: success ? null : `Queue-Skript fehlgeschlagen (Exit ${exitCode ?? 'n/a'})`
|
||||||
|
});
|
||||||
|
if (runError && !success) {
|
||||||
|
logger.warn('queue:script:exit-nonzero', { scriptId: script.id, exitCode });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
runtimeActivityService.completeActivity(activityId, {
|
runtimeActivityService.completeActivity(activityId, {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -8366,7 +8405,13 @@ class PipelineService extends EventEmitter {
|
|||||||
source: 'PRE_ENCODE_SCRIPT',
|
source: 'PRE_ENCODE_SCRIPT',
|
||||||
cmd: prepared.cmd,
|
cmd: prepared.cmd,
|
||||||
args: prepared.args,
|
args: prepared.args,
|
||||||
argsForLog: prepared.argsForLog
|
argsForLog: prepared.argsForLog,
|
||||||
|
onStdoutLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stdout: line });
|
||||||
|
},
|
||||||
|
onStderrLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stderr: line });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
succeeded += 1;
|
succeeded += 1;
|
||||||
results.push({ scriptId: script.id, scriptName: script.name, status: 'SUCCESS', runInfo });
|
results.push({ scriptId: script.id, scriptName: script.name, status: 'SUCCESS', runInfo });
|
||||||
@@ -8377,7 +8422,11 @@ class PipelineService extends EventEmitter {
|
|||||||
outcome: 'success',
|
outcome: 'success',
|
||||||
exitCode: Number.isFinite(Number(runInfo?.exitCode)) ? Number(runInfo.exitCode) : null,
|
exitCode: Number.isFinite(Number(runInfo?.exitCode)) ? Number(runInfo.exitCode) : null,
|
||||||
message: 'Pre-Encode Skript erfolgreich',
|
message: 'Pre-Encode Skript erfolgreich',
|
||||||
output: runOutput || null
|
output: runOutput || null,
|
||||||
|
stdout: runInfo?.stdoutTail || null,
|
||||||
|
stderr: runInfo?.stderrTail || null,
|
||||||
|
stdoutTruncated: Boolean(runInfo?.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(runInfo?.stderrTruncated)
|
||||||
});
|
});
|
||||||
await historyService.appendLog(jobId, 'SYSTEM', `Pre-Encode Skript erfolgreich: ${script.name}`);
|
await historyService.appendLog(jobId, 'SYSTEM', `Pre-Encode Skript erfolgreich: ${script.name}`);
|
||||||
if (progressTracker?.onStepComplete) {
|
if (progressTracker?.onStepComplete) {
|
||||||
@@ -8395,7 +8444,11 @@ class PipelineService extends EventEmitter {
|
|||||||
cancelled,
|
cancelled,
|
||||||
message: error?.message || 'Pre-Encode Skriptfehler',
|
message: error?.message || 'Pre-Encode Skriptfehler',
|
||||||
errorMessage: error?.message || 'Pre-Encode Skriptfehler',
|
errorMessage: error?.message || 'Pre-Encode Skriptfehler',
|
||||||
output: runOutput || null
|
output: runOutput || null,
|
||||||
|
stdout: runInfo?.stdoutTail || null,
|
||||||
|
stderr: runInfo?.stderrTail || null,
|
||||||
|
stdoutTruncated: Boolean(runInfo?.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(runInfo?.stderrTruncated)
|
||||||
});
|
});
|
||||||
failed += 1;
|
failed += 1;
|
||||||
aborted = true;
|
aborted = true;
|
||||||
@@ -8527,7 +8580,13 @@ class PipelineService extends EventEmitter {
|
|||||||
source: 'POST_ENCODE_SCRIPT',
|
source: 'POST_ENCODE_SCRIPT',
|
||||||
cmd: prepared.cmd,
|
cmd: prepared.cmd,
|
||||||
args: prepared.args,
|
args: prepared.args,
|
||||||
argsForLog: prepared.argsForLog
|
argsForLog: prepared.argsForLog,
|
||||||
|
onStdoutLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stdout: line });
|
||||||
|
},
|
||||||
|
onStderrLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stderr: line });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
succeeded += 1;
|
succeeded += 1;
|
||||||
@@ -8544,7 +8603,11 @@ class PipelineService extends EventEmitter {
|
|||||||
outcome: 'success',
|
outcome: 'success',
|
||||||
exitCode: Number.isFinite(Number(runInfo?.exitCode)) ? Number(runInfo.exitCode) : null,
|
exitCode: Number.isFinite(Number(runInfo?.exitCode)) ? Number(runInfo.exitCode) : null,
|
||||||
message: 'Post-Encode Skript erfolgreich',
|
message: 'Post-Encode Skript erfolgreich',
|
||||||
output: runOutput || null
|
output: runOutput || null,
|
||||||
|
stdout: runInfo?.stdoutTail || null,
|
||||||
|
stderr: runInfo?.stderrTail || null,
|
||||||
|
stdoutTruncated: Boolean(runInfo?.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(runInfo?.stderrTruncated)
|
||||||
});
|
});
|
||||||
await historyService.appendLog(
|
await historyService.appendLog(
|
||||||
jobId,
|
jobId,
|
||||||
@@ -8566,7 +8629,11 @@ class PipelineService extends EventEmitter {
|
|||||||
cancelled,
|
cancelled,
|
||||||
message: error?.message || 'Post-Encode Skriptfehler',
|
message: error?.message || 'Post-Encode Skriptfehler',
|
||||||
errorMessage: error?.message || 'Post-Encode Skriptfehler',
|
errorMessage: error?.message || 'Post-Encode Skriptfehler',
|
||||||
output: runOutput || null
|
output: runOutput || null,
|
||||||
|
stdout: runInfo?.stdoutTail || null,
|
||||||
|
stderr: runInfo?.stderrTail || null,
|
||||||
|
stdoutTruncated: Boolean(runInfo?.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(runInfo?.stderrTruncated)
|
||||||
});
|
});
|
||||||
failed += 1;
|
failed += 1;
|
||||||
aborted = true;
|
aborted = true;
|
||||||
@@ -10550,7 +10617,9 @@ class PipelineService extends EventEmitter {
|
|||||||
collectStdoutLines = true,
|
collectStdoutLines = true,
|
||||||
collectStderrLines = true,
|
collectStderrLines = true,
|
||||||
argsForLog = null,
|
argsForLog = null,
|
||||||
silent = false
|
silent = false,
|
||||||
|
onStdoutLine = null,
|
||||||
|
onStderrLine = null
|
||||||
}) {
|
}) {
|
||||||
const normalizedJobId = this.normalizeQueueJobId(jobId) || Number(jobId) || jobId;
|
const normalizedJobId = this.normalizeQueueJobId(jobId) || Number(jobId) || jobId;
|
||||||
const loggableArgs = Array.isArray(argsForLog) ? argsForLog : args;
|
const loggableArgs = Array.isArray(argsForLog) ? argsForLog : args;
|
||||||
@@ -10570,6 +10639,10 @@ class PipelineService extends EventEmitter {
|
|||||||
exitCode: null,
|
exitCode: null,
|
||||||
stdoutLines: 0,
|
stdoutLines: 0,
|
||||||
stderrLines: 0,
|
stderrLines: 0,
|
||||||
|
stdoutTail: '',
|
||||||
|
stderrTail: '',
|
||||||
|
stdoutTruncated: false,
|
||||||
|
stderrTruncated: false,
|
||||||
lastProgress: 0,
|
lastProgress: 0,
|
||||||
eta: null,
|
eta: null,
|
||||||
lastDetail: null,
|
lastDetail: null,
|
||||||
@@ -10594,6 +10667,10 @@ class PipelineService extends EventEmitter {
|
|||||||
exitCode: null,
|
exitCode: null,
|
||||||
stdoutLines: 0,
|
stdoutLines: 0,
|
||||||
stderrLines: 0,
|
stderrLines: 0,
|
||||||
|
stdoutTail: '',
|
||||||
|
stderrTail: '',
|
||||||
|
stdoutTruncated: false,
|
||||||
|
stderrTruncated: false,
|
||||||
lastProgress: 0,
|
lastProgress: 0,
|
||||||
eta: null,
|
eta: null,
|
||||||
lastDetail: null,
|
lastDetail: null,
|
||||||
@@ -10643,6 +10720,16 @@ class PipelineService extends EventEmitter {
|
|||||||
collectLines.push(line);
|
collectLines.push(line);
|
||||||
}
|
}
|
||||||
void historyService.appendProcessLog(jobId, source, line);
|
void historyService.appendProcessLog(jobId, source, line);
|
||||||
|
const nextStdout = appendTailText(runInfo.stdoutTail, line);
|
||||||
|
runInfo.stdoutTail = nextStdout.value;
|
||||||
|
runInfo.stdoutTruncated = runInfo.stdoutTruncated || nextStdout.truncated;
|
||||||
|
if (typeof onStdoutLine === 'function') {
|
||||||
|
try {
|
||||||
|
onStdoutLine(line);
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore observer failures for live runtime mirroring
|
||||||
|
}
|
||||||
|
}
|
||||||
applyLine(line, false);
|
applyLine(line, false);
|
||||||
},
|
},
|
||||||
onStderrLine: (line) => {
|
onStderrLine: (line) => {
|
||||||
@@ -10650,6 +10737,16 @@ class PipelineService extends EventEmitter {
|
|||||||
collectLines.push(line);
|
collectLines.push(line);
|
||||||
}
|
}
|
||||||
void historyService.appendProcessLog(jobId, `${source}_ERR`, line);
|
void historyService.appendProcessLog(jobId, `${source}_ERR`, line);
|
||||||
|
const nextStderr = appendTailText(runInfo.stderrTail, line);
|
||||||
|
runInfo.stderrTail = nextStderr.value;
|
||||||
|
runInfo.stderrTruncated = runInfo.stderrTruncated || nextStderr.truncated;
|
||||||
|
if (typeof onStderrLine === 'function') {
|
||||||
|
try {
|
||||||
|
onStderrLine(line);
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore observer failures for live runtime mirroring
|
||||||
|
}
|
||||||
|
}
|
||||||
applyLine(line, true);
|
applyLine(line, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -10900,7 +10997,7 @@ class PipelineService extends EventEmitter {
|
|||||||
settings?.output_template || audiobookService.DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE
|
settings?.output_template || audiobookService.DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE
|
||||||
).trim() || audiobookService.DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE;
|
).trim() || audiobookService.DEFAULT_AUDIOBOOK_OUTPUT_TEMPLATE;
|
||||||
const outputFormat = audiobookService.normalizeOutputFormat(
|
const outputFormat = audiobookService.normalizeOutputFormat(
|
||||||
requestedFormat || settings?.output_extension || 'mp3'
|
requestedFormat || 'm4b'
|
||||||
);
|
);
|
||||||
const formatOptions = audiobookService.getDefaultFormatOptions(outputFormat);
|
const formatOptions = audiobookService.getDefaultFormatOptions(outputFormat);
|
||||||
const ffprobeCommand = String(settings?.ffprobe_command || 'ffprobe').trim() || 'ffprobe';
|
const ffprobeCommand = String(settings?.ffprobe_command || 'ffprobe').trim() || 'ffprobe';
|
||||||
@@ -11196,7 +11293,7 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const format = audiobookService.normalizeOutputFormat(
|
const format = audiobookService.normalizeOutputFormat(
|
||||||
config?.format || encodePlan?.format || 'mp3'
|
config?.format || encodePlan?.format || 'm4b'
|
||||||
);
|
);
|
||||||
const formatOptions = audiobookService.normalizeFormatOptions(
|
const formatOptions = audiobookService.normalizeFormatOptions(
|
||||||
format,
|
format,
|
||||||
|
|||||||
@@ -109,5 +109,6 @@ function spawnTrackedProcess({
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
spawnTrackedProcess
|
spawnTrackedProcess,
|
||||||
|
streamLines
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const wsService = require('./websocketService');
|
|||||||
const MAX_RECENT_ACTIVITIES = 120;
|
const MAX_RECENT_ACTIVITIES = 120;
|
||||||
const MAX_ACTIVITY_OUTPUT_CHARS = 12000;
|
const MAX_ACTIVITY_OUTPUT_CHARS = 12000;
|
||||||
const MAX_ACTIVITY_TEXT_CHARS = 2000;
|
const MAX_ACTIVITY_TEXT_CHARS = 2000;
|
||||||
|
const OUTPUT_BROADCAST_THROTTLE_MS = 180;
|
||||||
|
|
||||||
function nowIso() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
@@ -28,12 +29,52 @@ function normalizeText(value, { trim = true, maxChars = MAX_ACTIVITY_TEXT_CHARS
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (text.length > maxChars) {
|
if (text.length > maxChars) {
|
||||||
const suffix = trim ? ' ...[gekürzt]' : '\n...[gekürzt]';
|
if (trim) {
|
||||||
text = `${text.slice(0, Math.max(0, maxChars - suffix.length))}${suffix}`;
|
const suffix = ' ...[gekürzt]';
|
||||||
|
text = `${text.slice(0, Math.max(0, maxChars - suffix.length))}${suffix}`;
|
||||||
|
} else {
|
||||||
|
const prefix = '...[gekürzt]\n';
|
||||||
|
text = `${prefix}${text.slice(-Math.max(0, maxChars - prefix.length))}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOutputChunk(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const normalized = String(value).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOutputTail(currentValue, chunk, maxChars = MAX_ACTIVITY_OUTPUT_CHARS) {
|
||||||
|
const normalizedChunk = normalizeOutputChunk(chunk);
|
||||||
|
const currentText = currentValue == null ? '' : String(currentValue);
|
||||||
|
if (!normalizedChunk) {
|
||||||
|
return {
|
||||||
|
value: currentText || null,
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const combined = `${currentText}${normalizedChunk}`;
|
||||||
|
if (combined.length <= maxChars) {
|
||||||
|
return {
|
||||||
|
value: combined,
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: combined.slice(-maxChars),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeActivity(input = {}) {
|
function sanitizeActivity(input = {}) {
|
||||||
const source = input && typeof input === 'object' ? input : {};
|
const source = input && typeof input === 'object' ? input : {};
|
||||||
const normalizedOutcome = normalizeText(source.outcome, { trim: true, maxChars: 40 });
|
const normalizedOutcome = normalizeText(source.outcome, { trim: true, maxChars: 40 });
|
||||||
@@ -61,6 +102,7 @@ function sanitizeActivity(input = {}) {
|
|||||||
output: normalizeText(source.output, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
output: normalizeText(source.output, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
||||||
stdout: normalizeText(source.stdout, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
stdout: normalizeText(source.stdout, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
||||||
stderr: normalizeText(source.stderr, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
stderr: normalizeText(source.stderr, { trim: false, maxChars: MAX_ACTIVITY_OUTPUT_CHARS }),
|
||||||
|
outputTruncated: Boolean(source.outputTruncated),
|
||||||
stdoutTruncated: Boolean(source.stdoutTruncated),
|
stdoutTruncated: Boolean(source.stdoutTruncated),
|
||||||
stderrTruncated: Boolean(source.stderrTruncated),
|
stderrTruncated: Boolean(source.stderrTruncated),
|
||||||
startedAt: source.startedAt || nowIso(),
|
startedAt: source.startedAt || nowIso(),
|
||||||
@@ -77,6 +119,7 @@ class RuntimeActivityService {
|
|||||||
this.active = new Map();
|
this.active = new Map();
|
||||||
this.recent = [];
|
this.recent = [];
|
||||||
this.controls = new Map();
|
this.controls = new Map();
|
||||||
|
this.outputBroadcastTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSnapshot() {
|
buildSnapshot() {
|
||||||
@@ -92,9 +135,23 @@ class RuntimeActivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
broadcastSnapshot() {
|
broadcastSnapshot() {
|
||||||
|
if (this.outputBroadcastTimer) {
|
||||||
|
clearTimeout(this.outputBroadcastTimer);
|
||||||
|
this.outputBroadcastTimer = null;
|
||||||
|
}
|
||||||
wsService.broadcast('RUNTIME_ACTIVITY_CHANGED', this.buildSnapshot());
|
wsService.broadcast('RUNTIME_ACTIVITY_CHANGED', this.buildSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleOutputBroadcast() {
|
||||||
|
if (this.outputBroadcastTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.outputBroadcastTimer = setTimeout(() => {
|
||||||
|
this.outputBroadcastTimer = null;
|
||||||
|
wsService.broadcast('RUNTIME_ACTIVITY_CHANGED', this.buildSnapshot());
|
||||||
|
}, OUTPUT_BROADCAST_THROTTLE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
startActivity(type, payload = {}) {
|
startActivity(type, payload = {}) {
|
||||||
const id = this.nextId;
|
const id = this.nextId;
|
||||||
this.nextId += 1;
|
this.nextId += 1;
|
||||||
@@ -134,6 +191,35 @@ class RuntimeActivityService {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendActivityOutput(activityId, patch = {}) {
|
||||||
|
const id = normalizeNumber(activityId);
|
||||||
|
if (!id || !this.active.has(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = this.active.get(id);
|
||||||
|
const nextOutput = appendOutputTail(current.output, patch?.output, MAX_ACTIVITY_OUTPUT_CHARS);
|
||||||
|
const nextStdout = appendOutputTail(current.stdout, patch?.stdout, MAX_ACTIVITY_OUTPUT_CHARS);
|
||||||
|
const nextStderr = appendOutputTail(current.stderr, patch?.stderr, MAX_ACTIVITY_OUTPUT_CHARS);
|
||||||
|
const next = sanitizeActivity({
|
||||||
|
...current,
|
||||||
|
...patch,
|
||||||
|
id: current.id,
|
||||||
|
type: current.type,
|
||||||
|
status: current.status,
|
||||||
|
startedAt: current.startedAt,
|
||||||
|
output: nextOutput.value,
|
||||||
|
stdout: nextStdout.value,
|
||||||
|
stderr: nextStderr.value,
|
||||||
|
outputTruncated: Boolean(current.outputTruncated || patch?.outputTruncated || nextOutput.truncated),
|
||||||
|
stdoutTruncated: Boolean(current.stdoutTruncated || patch?.stdoutTruncated || nextStdout.truncated),
|
||||||
|
stderrTruncated: Boolean(current.stderrTruncated || patch?.stderrTruncated || nextStderr.truncated)
|
||||||
|
});
|
||||||
|
this.active.set(id, next);
|
||||||
|
this.scheduleOutputBroadcast();
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
completeActivity(activityId, payload = {}) {
|
completeActivity(activityId, payload = {}) {
|
||||||
const id = normalizeNumber(activityId);
|
const id = normalizeNumber(activityId);
|
||||||
if (!id || !this.active.has(id)) {
|
if (!id || !this.active.has(id)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { spawn } = require('child_process');
|
|
||||||
const { getDb } = require('../db/database');
|
const { getDb } = require('../db/database');
|
||||||
const logger = require('./logger').child('SCRIPT_CHAINS');
|
const logger = require('./logger').child('SCRIPT_CHAINS');
|
||||||
const runtimeActivityService = require('./runtimeActivityService');
|
const runtimeActivityService = require('./runtimeActivityService');
|
||||||
|
const { spawnTrackedProcess } = require('./processRunner');
|
||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
|
|
||||||
const CHAIN_NAME_MAX_LENGTH = 120;
|
const CHAIN_NAME_MAX_LENGTH = 120;
|
||||||
@@ -76,6 +76,28 @@ function terminateChildProcess(child, { immediate = false } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendTailText(currentValue, nextChunk, maxChars = 12000) {
|
||||||
|
const chunk = String(nextChunk || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
if (!chunk) {
|
||||||
|
return {
|
||||||
|
value: currentValue || '',
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const normalizedChunk = chunk.endsWith('\n') ? chunk : `${chunk}\n`;
|
||||||
|
const combined = `${String(currentValue || '')}${normalizedChunk}`;
|
||||||
|
if (combined.length <= maxChars) {
|
||||||
|
return {
|
||||||
|
value: combined,
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: combined.slice(-maxChars),
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function validateSteps(rawSteps) {
|
function validateSteps(rawSteps) {
|
||||||
const steps = Array.isArray(rawSteps) ? rawSteps : [];
|
const steps = Array.isArray(rawSteps) ? rawSteps : [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
@@ -615,29 +637,58 @@ class ScriptChainService {
|
|||||||
scriptName: script.name,
|
scriptName: script.name,
|
||||||
source: context?.source || 'chain'
|
source: context?.source || 'chain'
|
||||||
});
|
});
|
||||||
const run = await new Promise((resolve, reject) => {
|
let stdout = '';
|
||||||
const child = spawn(prepared.cmd, prepared.args, {
|
let stderr = '';
|
||||||
env: process.env,
|
let stdoutTruncated = false;
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
let stderrTruncated = false;
|
||||||
detached: true
|
const processHandle = spawnTrackedProcess({
|
||||||
});
|
cmd: prepared.cmd,
|
||||||
controlState.activeChild = child;
|
args: prepared.args,
|
||||||
controlState.activeChildTermination = null;
|
context: { source: context?.source || 'chain', chainId: chain.id, scriptId: script.id },
|
||||||
let stdout = '';
|
onStart: (child) => {
|
||||||
let stderr = '';
|
controlState.activeChild = child;
|
||||||
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
||||||
child.stderr?.on('data', (chunk) => { stderr += String(chunk); });
|
|
||||||
child.on('error', (error) => {
|
|
||||||
controlState.activeChild = null;
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
child.on('close', (code, signal) => {
|
|
||||||
const termination = controlState.activeChildTermination;
|
|
||||||
controlState.activeChild = null;
|
|
||||||
controlState.activeChildTermination = null;
|
controlState.activeChildTermination = null;
|
||||||
resolve({ code, signal, stdout, stderr, termination });
|
},
|
||||||
});
|
onStdoutLine: (line) => {
|
||||||
|
const next = appendTailText(stdout, line);
|
||||||
|
stdout = next.value;
|
||||||
|
stdoutTruncated = stdoutTruncated || next.truncated;
|
||||||
|
runtimeActivityService.appendActivityOutput(scriptActivityId, { stdout: line });
|
||||||
|
},
|
||||||
|
onStderrLine: (line) => {
|
||||||
|
const next = appendTailText(stderr, line);
|
||||||
|
stderr = next.value;
|
||||||
|
stderrTruncated = stderrTruncated || next.truncated;
|
||||||
|
runtimeActivityService.appendActivityOutput(scriptActivityId, { stderr: line });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
let runError = null;
|
||||||
|
let exitCode = 0;
|
||||||
|
let signal = null;
|
||||||
|
try {
|
||||||
|
const result = await processHandle.promise;
|
||||||
|
exitCode = Number.isFinite(Number(result?.code)) ? Number(result.code) : 0;
|
||||||
|
signal = result?.signal || null;
|
||||||
|
} catch (error) {
|
||||||
|
runError = error;
|
||||||
|
exitCode = Number.isFinite(Number(error?.code)) ? Number(error.code) : null;
|
||||||
|
signal = error?.signal || null;
|
||||||
|
}
|
||||||
|
const termination = controlState.activeChildTermination;
|
||||||
|
controlState.activeChild = null;
|
||||||
|
controlState.activeChildTermination = null;
|
||||||
|
if (runError && exitCode === null && !termination) {
|
||||||
|
throw runError;
|
||||||
|
}
|
||||||
|
const run = {
|
||||||
|
code: exitCode,
|
||||||
|
signal,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
stdoutTruncated,
|
||||||
|
stderrTruncated,
|
||||||
|
termination
|
||||||
|
};
|
||||||
controlState.currentStepType = null;
|
controlState.currentStepType = null;
|
||||||
|
|
||||||
if (run.termination === 'skip') {
|
if (run.termination === 'skip') {
|
||||||
@@ -648,7 +699,11 @@ class ScriptChainService {
|
|||||||
skipped: true,
|
skipped: true,
|
||||||
currentStep: null,
|
currentStep: null,
|
||||||
message: 'Schritt übersprungen',
|
message: 'Schritt übersprungen',
|
||||||
output: [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null
|
output: [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null,
|
||||||
|
stdout: run.stdout || null,
|
||||||
|
stderr: run.stderr || null,
|
||||||
|
stdoutTruncated: Boolean(run.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(run.stderrTruncated)
|
||||||
});
|
});
|
||||||
if (typeof appendLog === 'function') {
|
if (typeof appendLog === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -678,6 +733,10 @@ class ScriptChainService {
|
|||||||
currentStep: null,
|
currentStep: null,
|
||||||
message: controlState.cancelReason || 'Von Benutzer abgebrochen',
|
message: controlState.cancelReason || 'Von Benutzer abgebrochen',
|
||||||
output: [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null,
|
output: [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null,
|
||||||
|
stdout: run.stdout || null,
|
||||||
|
stderr: run.stderr || null,
|
||||||
|
stdoutTruncated: Boolean(run.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(run.stderrTruncated),
|
||||||
errorMessage: controlState.cancelReason || 'Von Benutzer abgebrochen'
|
errorMessage: controlState.cancelReason || 'Von Benutzer abgebrochen'
|
||||||
});
|
});
|
||||||
if (typeof appendLog === 'function') {
|
if (typeof appendLog === 'function') {
|
||||||
@@ -709,6 +768,8 @@ class ScriptChainService {
|
|||||||
output: success ? null : [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null,
|
output: success ? null : [run.stdout || '', run.stderr || ''].filter(Boolean).join('\n').trim() || null,
|
||||||
stderr: success ? null : (run.stderr || null),
|
stderr: success ? null : (run.stderr || null),
|
||||||
stdout: success ? null : (run.stdout || null),
|
stdout: success ? null : (run.stdout || null),
|
||||||
|
stdoutTruncated: Boolean(run.stdoutTruncated),
|
||||||
|
stderrTruncated: Boolean(run.stderrTruncated),
|
||||||
errorMessage: success ? null : `Fehler (Exit ${run.code})`
|
errorMessage: success ? null : `Fehler (Exit ${run.code})`
|
||||||
});
|
});
|
||||||
logger.info('chain:step:script-done', { chainId, scriptId: script.id, exitCode: run.code, success });
|
logger.info('chain:step:script-done', { chainId, scriptId: script.id, exitCode: run.code, success });
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { getDb } = require('../db/database');
|
|||||||
const logger = require('./logger').child('SCRIPTS');
|
const logger = require('./logger').child('SCRIPTS');
|
||||||
const settingsService = require('./settingsService');
|
const settingsService = require('./settingsService');
|
||||||
const runtimeActivityService = require('./runtimeActivityService');
|
const runtimeActivityService = require('./runtimeActivityService');
|
||||||
|
const { streamLines } = require('./processRunner');
|
||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
|
|
||||||
const SCRIPT_NAME_MAX_LENGTH = 120;
|
const SCRIPT_NAME_MAX_LENGTH = 120;
|
||||||
@@ -206,7 +207,15 @@ function killChildProcessTree(child, signal = 'SIGTERM') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd = process.cwd(), onChild = null }) {
|
function runProcessCapture({
|
||||||
|
cmd,
|
||||||
|
args,
|
||||||
|
timeoutMs = SCRIPT_TEST_TIMEOUT_MS,
|
||||||
|
cwd = process.cwd(),
|
||||||
|
onChild = null,
|
||||||
|
onStdoutLine = null,
|
||||||
|
onStderrLine = null
|
||||||
|
}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const effectiveTimeoutMs = normalizeScriptTestTimeoutMs(timeoutMs, SCRIPT_TEST_TIMEOUT_MS);
|
const effectiveTimeoutMs = normalizeScriptTestTimeoutMs(timeoutMs, SCRIPT_TEST_TIMEOUT_MS);
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
@@ -259,6 +268,13 @@ function runProcessCapture({ cmd, args, timeoutMs = SCRIPT_TEST_TIMEOUT_MS, cwd
|
|||||||
child.stdout?.on('data', (chunk) => onData('stdout', chunk));
|
child.stdout?.on('data', (chunk) => onData('stdout', chunk));
|
||||||
child.stderr?.on('data', (chunk) => onData('stderr', chunk));
|
child.stderr?.on('data', (chunk) => onData('stderr', chunk));
|
||||||
|
|
||||||
|
if (child.stdout && typeof onStdoutLine === 'function') {
|
||||||
|
streamLines(child.stdout, onStdoutLine);
|
||||||
|
}
|
||||||
|
if (child.stderr && typeof onStderrLine === 'function') {
|
||||||
|
streamLines(child.stderr, onStderrLine);
|
||||||
|
}
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
ended = true;
|
ended = true;
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@@ -597,6 +613,12 @@ class ScriptService {
|
|||||||
timeoutMs: effectiveTimeoutMs,
|
timeoutMs: effectiveTimeoutMs,
|
||||||
onChild: (child) => {
|
onChild: (child) => {
|
||||||
controlState.child = child;
|
controlState.child = child;
|
||||||
|
},
|
||||||
|
onStdoutLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stdout: line });
|
||||||
|
},
|
||||||
|
onStderrLine: (line) => {
|
||||||
|
runtimeActivityService.appendActivityOutput(activityId, { stderr: line });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const exitCode = Number.isFinite(Number(run.code)) ? Number(run.code) : null;
|
const exitCode = Number.isFinite(Number(run.code)) ? Number(run.code) : null;
|
||||||
|
|||||||
@@ -97,8 +97,7 @@ const PROFILED_SETTINGS = {
|
|||||||
},
|
},
|
||||||
output_extension: {
|
output_extension: {
|
||||||
bluray: 'output_extension_bluray',
|
bluray: 'output_extension_bluray',
|
||||||
dvd: 'output_extension_dvd',
|
dvd: 'output_extension_dvd'
|
||||||
audiobook: 'output_extension_audiobook'
|
|
||||||
},
|
},
|
||||||
output_template: {
|
output_template: {
|
||||||
bluray: 'output_template_bluray',
|
bluray: 'output_template_bluray',
|
||||||
|
|||||||
@@ -297,8 +297,10 @@ VALUES ('mediainfo_extra_args_bluray', 'Tools', 'Mediainfo Extra Args', 'string'
|
|||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mediainfo_extra_args_bluray', NULL);
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('mediainfo_extra_args_bluray', NULL);
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('makemkv_rip_mode_bluray', 'Tools', 'MakeMKV Rip Modus', 'select', 1, 'mkv: direkte MKV-Dateien; backup: vollständige Blu-ray Struktur im RAW-Ordner.', 'backup', '[{"label":"MKV","value":"mkv"},{"label":"Backup","value":"backup"}]', '{}', 305);
|
VALUES ('makemkv_rip_mode_bluray', 'Tools', 'MakeMKV Rip Modus', 'select', 1, 'backup: vollständige Blu-ray Struktur im RAW-Ordner (empfohlen, ermöglicht --decrypt).', 'backup', '[{"label":"Backup","value":"backup"},{"label":"MKV","value":"mkv"}]', '{}', 305);
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_mode_bluray', 'backup');
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('makemkv_rip_mode_bluray', 'backup');
|
||||||
|
UPDATE settings_schema SET default_value = 'backup', description = 'backup: vollständige Blu-ray Struktur im RAW-Ordner (empfohlen, ermöglicht --decrypt).' WHERE key = 'makemkv_rip_mode_bluray';
|
||||||
|
UPDATE settings_values SET value = 'backup' WHERE key = 'makemkv_rip_mode_bluray';
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('makemkv_analyze_extra_args_bluray', 'Tools', 'MakeMKV Analyze Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Analyze (Blu-ray).', NULL, '[]', '{}', 310);
|
VALUES ('makemkv_analyze_extra_args_bluray', 'Tools', 'MakeMKV Analyze Extra Args', 'string', 0, 'Zusätzliche CLI-Parameter für Analyze (Blu-ray).', NULL, '[]', '{}', 310);
|
||||||
@@ -389,10 +391,6 @@ INSERT OR IGNORE INTO settings_values (key, value)
|
|||||||
VALUES ('cd_output_template', '{artist} - {album} ({year})/{trackNr} {artist} - {title}');
|
VALUES ('cd_output_template', '{artist} - {album} ({year})/{trackNr} {artist} - {title}');
|
||||||
|
|
||||||
-- Tools – Audiobook
|
-- Tools – Audiobook
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
|
||||||
VALUES ('output_extension_audiobook', 'Tools', 'Ausgabeformat', 'select', 1, 'Dateiendung für finale Audiobook-Datei.', 'mp3', '[{"label":"M4B","value":"m4b"},{"label":"MP3","value":"mp3"},{"label":"FLAC","value":"flac"}]', '{}', 730);
|
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_extension_audiobook', 'mp3');
|
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('output_template_audiobook', 'Pfade', 'Output Template (Audiobook)', 'string', 1, 'Template für relative Audiobook-Ausgabepfade ohne Dateiendung. Platzhalter: {author}, {title}, {year}, {narrator}, {series}, {part}, {format}. Unterordner sind über "/" möglich.', '{author}/{author} - {title} ({year})', '[]', '{"minLength":1}', 735);
|
VALUES ('output_template_audiobook', 'Pfade', 'Output Template (Audiobook)', 'string', 1, 'Template für relative Audiobook-Ausgabepfade ohne Dateiendung. Platzhalter: {author}, {title}, {year}, {narrator}, {series}, {part}, {format}. Unterordner sind über "/" möglich.', '{author}/{author} - {title} ({year})', '[]', '{"minLength":1}', 735);
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_audiobook', '{author}/{author} - {title} ({year})');
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_audiobook', '{author}/{author} - {title} ({year})');
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"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.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Dialog } from 'primereact/dialog';
|
|||||||
import { InputNumber } from 'primereact/inputnumber';
|
import { InputNumber } from 'primereact/inputnumber';
|
||||||
import { InputText } from 'primereact/inputtext';
|
import { InputText } from 'primereact/inputtext';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
|
import { useWebSocket } from '../hooks/useWebSocket';
|
||||||
import PipelineStatusCard from '../components/PipelineStatusCard';
|
import PipelineStatusCard from '../components/PipelineStatusCard';
|
||||||
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
|
import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
|
||||||
import CdMetadataDialog from '../components/CdMetadataDialog';
|
import CdMetadataDialog from '../components/CdMetadataDialog';
|
||||||
@@ -130,6 +131,7 @@ function normalizeRuntimeActivitiesPayload(rawPayload) {
|
|||||||
output: source.output != null ? String(source.output) : null,
|
output: source.output != null ? String(source.output) : null,
|
||||||
stdout: source.stdout != null ? String(source.stdout) : null,
|
stdout: source.stdout != null ? String(source.stdout) : null,
|
||||||
stderr: source.stderr != null ? String(source.stderr) : null,
|
stderr: source.stderr != null ? String(source.stderr) : null,
|
||||||
|
outputTruncated: Boolean(source.outputTruncated),
|
||||||
stdoutTruncated: Boolean(source.stdoutTruncated),
|
stdoutTruncated: Boolean(source.stdoutTruncated),
|
||||||
stderrTruncated: Boolean(source.stderrTruncated),
|
stderrTruncated: Boolean(source.stderrTruncated),
|
||||||
exitCode: Number.isFinite(Number(source.exitCode)) ? Number(source.exitCode) : null,
|
exitCode: Number.isFinite(Number(source.exitCode)) ? Number(source.exitCode) : null,
|
||||||
@@ -190,6 +192,53 @@ function hasRuntimeOutputDetails(item) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasRuntimeLogContent(item) {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(
|
||||||
|
String(item.output || '').trim()
|
||||||
|
|| String(item.stdout || '').trim()
|
||||||
|
|| String(item.stderr || '').trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuntimeActivityDetails({
|
||||||
|
item,
|
||||||
|
summary,
|
||||||
|
emptyLabel = 'Noch keine Log-Ausgabe vorhanden.'
|
||||||
|
}) {
|
||||||
|
const hasLogs = hasRuntimeLogContent(item);
|
||||||
|
const hasOutput = Boolean(String(item?.output || '').trim());
|
||||||
|
const hasStdout = Boolean(String(item?.stdout || '').trim());
|
||||||
|
const hasStderr = Boolean(String(item?.stderr || '').trim());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className="runtime-activity-details">
|
||||||
|
<summary>{summary}</summary>
|
||||||
|
{!hasLogs ? <small>{emptyLabel}</small> : null}
|
||||||
|
{hasOutput ? (
|
||||||
|
<div className="runtime-activity-details-block">
|
||||||
|
<small><strong>Ausgabe:</strong>{item?.outputTruncated ? ' (gekürzt)' : ''}</small>
|
||||||
|
<pre>{item.output}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasStdout ? (
|
||||||
|
<div className="runtime-activity-details-block">
|
||||||
|
<small><strong>stdout:</strong>{item?.stdoutTruncated ? ' (gekürzt)' : ''}</small>
|
||||||
|
<pre>{item.stdout}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hasStderr ? (
|
||||||
|
<div className="runtime-activity-details-block">
|
||||||
|
<small><strong>stderr:</strong>{item?.stderrTruncated ? ' (gekürzt)' : ''}</small>
|
||||||
|
<pre>{item.stderr}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeHardwareMonitoringPayload(rawPayload) {
|
function normalizeHardwareMonitoringPayload(rawPayload) {
|
||||||
const payload = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
|
const payload = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
|
||||||
return {
|
return {
|
||||||
@@ -1016,13 +1065,23 @@ export default function DashboardPage({
|
|||||||
void load(false);
|
void load(false);
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
void load(true);
|
void load(true);
|
||||||
}, 2500);
|
}, 10000);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useWebSocket({
|
||||||
|
onMessage: (message) => {
|
||||||
|
if (message?.type !== 'RUNTIME_ACTIVITY_CHANGED') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRuntimeActivities(normalizeRuntimeActivitiesPayload(message.payload));
|
||||||
|
setRuntimeLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const normalizedExpanded = normalizeJobId(expandedJobId);
|
const normalizedExpanded = normalizeJobId(expandedJobId);
|
||||||
const hasExpanded = dashboardJobs.some((job) => normalizeJobId(job?.id) === normalizedExpanded);
|
const hasExpanded = dashboardJobs.some((job) => normalizeJobId(job?.id) === normalizedExpanded);
|
||||||
@@ -2447,6 +2506,10 @@ export default function DashboardPage({
|
|||||||
{item?.currentStep ? <small>Schritt: {item.currentStep}</small> : null}
|
{item?.currentStep ? <small>Schritt: {item.currentStep}</small> : null}
|
||||||
{item?.currentScriptName ? <small>Laufendes Skript: {item.currentScriptName}</small> : null}
|
{item?.currentScriptName ? <small>Laufendes Skript: {item.currentScriptName}</small> : null}
|
||||||
{item?.message ? <small>{item.message}</small> : null}
|
{item?.message ? <small>{item.message}</small> : null}
|
||||||
|
<RuntimeActivityDetails
|
||||||
|
item={item}
|
||||||
|
summary="Live-Ausgabe anzeigen"
|
||||||
|
/>
|
||||||
<small>Gestartet: {formatUpdatedAt(item?.startedAt)}</small>
|
<small>Gestartet: {formatUpdatedAt(item?.startedAt)}</small>
|
||||||
{canCancel || canNextStep ? (
|
{canCancel || canNextStep ? (
|
||||||
<div className="runtime-activity-actions">
|
<div className="runtime-activity-actions">
|
||||||
@@ -2515,27 +2578,10 @@ export default function DashboardPage({
|
|||||||
{item?.message ? <small>{item.message}</small> : null}
|
{item?.message ? <small>{item.message}</small> : null}
|
||||||
{item?.errorMessage ? <small className="error-text">{item.errorMessage}</small> : null}
|
{item?.errorMessage ? <small className="error-text">{item.errorMessage}</small> : null}
|
||||||
{hasRuntimeOutputDetails(item) ? (
|
{hasRuntimeOutputDetails(item) ? (
|
||||||
<details className="runtime-activity-details">
|
<RuntimeActivityDetails
|
||||||
<summary>Details anzeigen</summary>
|
item={item}
|
||||||
{item?.output ? (
|
summary="Details anzeigen"
|
||||||
<div className="runtime-activity-details-block">
|
/>
|
||||||
<small><strong>Ausgabe:</strong></small>
|
|
||||||
<pre>{item.output}</pre>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{item?.stderr ? (
|
|
||||||
<div className="runtime-activity-details-block">
|
|
||||||
<small><strong>stderr:</strong>{item?.stderrTruncated ? ' (gekürzt)' : ''}</small>
|
|
||||||
<pre>{item.stderr}</pre>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{item?.stdout ? (
|
|
||||||
<div className="runtime-activity-details-block">
|
|
||||||
<small><strong>stdout:</strong>{item?.stdoutTruncated ? ' (gekürzt)' : ''}</small>
|
|
||||||
<pre>{item.stdout}</pre>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</details>
|
|
||||||
) : null}
|
) : null}
|
||||||
<small>
|
<small>
|
||||||
Ende: {formatUpdatedAt(item?.finishedAt || item?.startedAt)}
|
Ende: {formatUpdatedAt(item?.finishedAt || item?.startedAt)}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.2-2",
|
"version": "0.10.2-3",
|
||||||
"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