0.16.1-1 release snapshot
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const asyncHandler = require('../middleware/asyncHandler');
|
||||
const pipelineService = require('../services/pipelineService');
|
||||
const historyService = require('../services/historyService');
|
||||
const diskDetectionService = require('../services/diskDetectionService');
|
||||
const hardwareMonitorService = require('../services/hardwareMonitorService');
|
||||
const settingsService = require('../services/settingsService');
|
||||
const logger = require('../services/logger').child('PIPELINE_ROUTE');
|
||||
const activationBytesService = require('../services/activationBytesService');
|
||||
const { defaultAudiobookDir } = require('../config');
|
||||
const { getDb } = require('../db/database');
|
||||
|
||||
const router = express.Router();
|
||||
const audiobookUpload = multer({
|
||||
dest: path.join(os.tmpdir(), 'ripster-audiobook-uploads')
|
||||
});
|
||||
|
||||
const AUDIOBOOK_TREE_MAX_DEPTH = 8;
|
||||
|
||||
function buildAudiobookOutputTree(rootDir, relPath = '', depth = 0) {
|
||||
if (depth >= AUDIOBOOK_TREE_MAX_DEPTH) {
|
||||
return [];
|
||||
}
|
||||
const absDir = relPath ? path.join(rootDir, relPath) : rootDir;
|
||||
let dirents = [];
|
||||
try {
|
||||
dirents = fs.readdirSync(absDir, { withFileTypes: true });
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
dirents.sort((left, right) => {
|
||||
const leftOrder = left.isDirectory() ? 0 : 1;
|
||||
const rightOrder = right.isDirectory() ? 0 : 1;
|
||||
if (leftOrder !== rightOrder) {
|
||||
return leftOrder - rightOrder;
|
||||
}
|
||||
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
const nodes = [];
|
||||
for (const dirent of dirents) {
|
||||
if (dirent.name.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
const childRelPath = relPath ? `${relPath}/${dirent.name}` : dirent.name;
|
||||
const childAbsPath = path.join(rootDir, childRelPath);
|
||||
if (dirent.isDirectory()) {
|
||||
const children = buildAudiobookOutputTree(rootDir, childRelPath, depth + 1);
|
||||
const size = children.reduce((sum, item) => sum + Number(item?.size || 0), 0);
|
||||
let mtime = null;
|
||||
try {
|
||||
mtime = fs.statSync(childAbsPath).mtime.toISOString();
|
||||
} catch (_error) {
|
||||
mtime = null;
|
||||
}
|
||||
nodes.push({
|
||||
name: dirent.name,
|
||||
type: 'folder',
|
||||
path: childRelPath,
|
||||
size,
|
||||
mtime,
|
||||
children
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (dirent.isFile()) {
|
||||
let size = 0;
|
||||
let mtime = null;
|
||||
try {
|
||||
const stat = fs.statSync(childAbsPath);
|
||||
size = Number(stat?.size || 0);
|
||||
mtime = stat?.mtime ? stat.mtime.toISOString() : null;
|
||||
} catch (_error) {
|
||||
size = 0;
|
||||
mtime = null;
|
||||
}
|
||||
nodes.push({
|
||||
name: dirent.name,
|
||||
type: 'file',
|
||||
path: childRelPath,
|
||||
size,
|
||||
mtime
|
||||
});
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/state',
|
||||
asyncHandler(async (req, res) => {
|
||||
logger.debug('get:state', { reqId: req.reqId });
|
||||
res.json({
|
||||
pipeline: pipelineService.getSnapshot(),
|
||||
hardwareMonitoring: hardwareMonitorService.getSnapshot()
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/analyze',
|
||||
asyncHandler(async (req, res) => {
|
||||
const devicePath = String(req.body?.devicePath || '').trim() || null;
|
||||
logger.info('post:analyze', { reqId: req.reqId, devicePath });
|
||||
const result = await pipelineService.analyzeDisc(devicePath);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/cd/drives',
|
||||
asyncHandler(async (req, res) => {
|
||||
logger.debug('get:cd:drives', { reqId: req.reqId });
|
||||
const snapshot = pipelineService.getSnapshot();
|
||||
res.json({ cdDrives: snapshot.cdDrives || {} });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/rescan-disc',
|
||||
asyncHandler(async (req, res) => {
|
||||
logger.info('post:rescan-disc', { reqId: req.reqId });
|
||||
const result = await diskDetectionService.rescanAndEmit();
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/rescan-drive',
|
||||
asyncHandler(async (req, res) => {
|
||||
const devicePath = String(req.body?.devicePath || '').trim();
|
||||
logger.info('post:rescan-drive', { reqId: req.reqId, devicePath });
|
||||
if (!devicePath) {
|
||||
const err = new Error('devicePath ist erforderlich');
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
const result = await diskDetectionService.rescanDriveAndEmit(devicePath);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/omdb/search',
|
||||
asyncHandler(async (req, res) => {
|
||||
const query = req.query.q || '';
|
||||
logger.info('get:omdb:search', { reqId: req.reqId, query });
|
||||
const results = await pipelineService.searchOmdb(String(query));
|
||||
res.json({ results });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/tmdb/series/search',
|
||||
asyncHandler(async (req, res) => {
|
||||
const query = req.query.q || '';
|
||||
const seasonNumber = req.query.season || null;
|
||||
logger.info('get:tmdb:series-search', { reqId: req.reqId, query, seasonNumber });
|
||||
const results = await pipelineService.searchTmdbSeries(String(query), seasonNumber);
|
||||
res.json({ results });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/cd/musicbrainz/search',
|
||||
asyncHandler(async (req, res) => {
|
||||
const query = req.query.q || '';
|
||||
logger.info('get:cd:musicbrainz:search', { reqId: req.reqId, query });
|
||||
const results = await pipelineService.searchMusicBrainz(String(query));
|
||||
res.json({ results });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/cd/musicbrainz/release/:mbId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const mbId = String(req.params.mbId || '').trim();
|
||||
if (!mbId) {
|
||||
const error = new Error('mbId fehlt.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
logger.info('get:cd:musicbrainz:release', { reqId: req.reqId, mbId });
|
||||
const release = await pipelineService.getMusicBrainzReleaseById(mbId);
|
||||
res.json({ release });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/cd/select-metadata',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { jobId, title, artist, year, mbId, coverUrl, tracks } = req.body;
|
||||
if (!jobId) {
|
||||
const error = new Error('jobId fehlt.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
logger.info('post:cd:select-metadata', { reqId: req.reqId, jobId, title, artist, year, mbId });
|
||||
const job = await pipelineService.selectCdMetadata({
|
||||
jobId: Number(jobId),
|
||||
title,
|
||||
artist,
|
||||
year,
|
||||
mbId,
|
||||
coverUrl,
|
||||
tracks
|
||||
});
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/cd/start/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const ripConfig = req.body || {};
|
||||
logger.info('post:cd:start', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
format: ripConfig.format,
|
||||
selectedPreEncodeScriptIdsCount: Array.isArray(ripConfig?.selectedPreEncodeScriptIds)
|
||||
? ripConfig.selectedPreEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPostEncodeScriptIdsCount: Array.isArray(ripConfig?.selectedPostEncodeScriptIds)
|
||||
? ripConfig.selectedPostEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPreEncodeChainIdsCount: Array.isArray(ripConfig?.selectedPreEncodeChainIds)
|
||||
? ripConfig.selectedPreEncodeChainIds.length
|
||||
: 0,
|
||||
selectedPostEncodeChainIdsCount: Array.isArray(ripConfig?.selectedPostEncodeChainIds)
|
||||
? ripConfig.selectedPostEncodeChainIds.length
|
||||
: 0
|
||||
});
|
||||
const result = await pipelineService.enqueueOrStartCdAction(
|
||||
jobId,
|
||||
ripConfig,
|
||||
() => pipelineService.startCdRip(jobId, ripConfig)
|
||||
);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/audiobook/upload',
|
||||
audiobookUpload.single('file'),
|
||||
asyncHandler(async (req, res) => {
|
||||
if (!req.file) {
|
||||
const error = new Error('Upload-Datei fehlt.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
logger.info('post:audiobook:upload', {
|
||||
reqId: req.reqId,
|
||||
originalName: req.file.originalname,
|
||||
sizeBytes: Number(req.file.size || 0),
|
||||
mimeType: String(req.file.mimetype || '').trim() || null,
|
||||
tempPath: String(req.file.path || '').trim() || null
|
||||
});
|
||||
const result = await pipelineService.createFileJob({
|
||||
kind: 'audiobook_upload',
|
||||
file: req.file,
|
||||
options: {
|
||||
format: req.body?.format,
|
||||
startImmediately: req.body?.startImmediately
|
||||
}
|
||||
});
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/audiobook/pending-activation',
|
||||
asyncHandler(async (req, res) => {
|
||||
const db = await getDb();
|
||||
// Jobs die eine Checksum haben, aber noch keine Activation Bytes im Cache
|
||||
const pending = await db.all(`
|
||||
SELECT j.id AS jobId, j.aax_checksum AS checksum
|
||||
FROM jobs j
|
||||
WHERE j.aax_checksum IS NOT NULL
|
||||
AND j.status NOT IN ('DONE', 'ERROR', 'CANCELLED')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM aax_activation_bytes ab WHERE ab.checksum = j.aax_checksum
|
||||
)
|
||||
ORDER BY j.created_at DESC
|
||||
`);
|
||||
res.json({ pending });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/audiobook/start/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const config = req.body || {};
|
||||
logger.info('post:audiobook:start', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
format: config?.format,
|
||||
formatOptions: config?.formatOptions && typeof config.formatOptions === 'object'
|
||||
? config.formatOptions
|
||||
: null
|
||||
});
|
||||
const result = await pipelineService.startAudiobookWithConfig(jobId, config);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/audiobook/jobs',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const jobs = await pipelineService.getAudiobookJobs();
|
||||
res.json({ jobs });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/audiobook/output-tree',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const settings = await settingsService.getEffectiveSettingsMap('audiobook');
|
||||
const configuredOutputDir = String(settings?.movie_dir || '').trim();
|
||||
const rawOutputDir = configuredOutputDir || defaultAudiobookDir || null;
|
||||
const outputDir = rawOutputDir
|
||||
? (path.isAbsolute(rawOutputDir) ? rawOutputDir : path.resolve(process.cwd(), rawOutputDir))
|
||||
: null;
|
||||
|
||||
if (!outputDir || !fs.existsSync(outputDir)) {
|
||||
res.json({ outputDir, tree: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const children = buildAudiobookOutputTree(outputDir, '', 0);
|
||||
const size = children.reduce((sum, item) => sum + Number(item?.size || 0), 0);
|
||||
res.json({
|
||||
outputDir,
|
||||
tree: {
|
||||
name: path.basename(outputDir) || 'audiobooks',
|
||||
type: 'folder',
|
||||
path: '',
|
||||
size,
|
||||
mtime: null,
|
||||
children
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/select-metadata',
|
||||
asyncHandler(async (req, res) => {
|
||||
const {
|
||||
jobId,
|
||||
title,
|
||||
year,
|
||||
imdbId,
|
||||
poster,
|
||||
fromOmdb,
|
||||
selectedPlaylist,
|
||||
selectedHandBrakeTitleId,
|
||||
selectedHandBrakeTitleIds,
|
||||
metadataProvider,
|
||||
providerId,
|
||||
tmdbId,
|
||||
metadataKind,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
seasonName,
|
||||
episodeCount,
|
||||
episodes,
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
} = req.body;
|
||||
|
||||
if (!jobId) {
|
||||
const error = new Error('jobId fehlt.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info('post:select-metadata', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
title,
|
||||
year,
|
||||
imdbId,
|
||||
poster,
|
||||
fromOmdb,
|
||||
selectedPlaylist,
|
||||
selectedHandBrakeTitleId,
|
||||
selectedHandBrakeTitleIds: Array.isArray(selectedHandBrakeTitleIds) ? selectedHandBrakeTitleIds : null,
|
||||
metadataProvider,
|
||||
providerId,
|
||||
tmdbId,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
});
|
||||
|
||||
const job = await pipelineService.selectMetadata({
|
||||
jobId: Number(jobId),
|
||||
title,
|
||||
year,
|
||||
imdbId,
|
||||
poster,
|
||||
fromOmdb,
|
||||
selectedPlaylist,
|
||||
selectedHandBrakeTitleId,
|
||||
selectedHandBrakeTitleIds,
|
||||
metadataProvider,
|
||||
providerId,
|
||||
tmdbId,
|
||||
metadataKind,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
seasonName,
|
||||
episodeCount,
|
||||
episodes,
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
});
|
||||
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/jobs/:jobId/raw-decision',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const { decision } = req.body;
|
||||
logger.info('post:raw-decision', { reqId: req.reqId, jobId, decision });
|
||||
const result = await pipelineService.submitRawDecision(jobId, decision);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/start/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
logger.info('post:start-job', { reqId: req.reqId, jobId });
|
||||
const result = await pipelineService.startPreparedJob(jobId);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/multipart-merge/:jobId/reorder',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const orderedSourceJobIds = Array.isArray(req.body?.orderedSourceJobIds)
|
||||
? req.body.orderedSourceJobIds
|
||||
: [];
|
||||
logger.info('post:multipart-merge:reorder', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
orderedCount: orderedSourceJobIds.length
|
||||
});
|
||||
const job = await pipelineService.updateMultipartMergeSourceOrder(jobId, orderedSourceJobIds);
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/multipart-merge/:jobId/settings',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const deleteInputsAfterMerge = Boolean(req.body?.deleteInputsAfterMerge);
|
||||
logger.info('post:multipart-merge:settings', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
deleteInputsAfterMerge
|
||||
});
|
||||
const job = await pipelineService.updateMultipartMergeSettings(jobId, {
|
||||
deleteInputsAfterMerge
|
||||
});
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/multipart-merge/:jobId/preview',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
logger.info('get:multipart-merge:preview', {
|
||||
reqId: req.reqId,
|
||||
jobId
|
||||
});
|
||||
const preview = await pipelineService.getMultipartMergePreview(jobId);
|
||||
res.json({ preview });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/multipart-merge/:containerJobId/restore',
|
||||
asyncHandler(async (req, res) => {
|
||||
const containerJobId = Number(req.params.containerJobId);
|
||||
logger.info('post:multipart-merge:restore', {
|
||||
reqId: req.reqId,
|
||||
containerJobId
|
||||
});
|
||||
const job = await pipelineService.restoreMultipartMergeJobForContainer(containerJobId);
|
||||
await pipelineService.emitQueueChanged().catch((error) => {
|
||||
logger.warn('post:multipart-merge:restore:queue-emit-failed', {
|
||||
reqId: req.reqId,
|
||||
containerJobId,
|
||||
error: error?.message || String(error)
|
||||
});
|
||||
});
|
||||
res.json({
|
||||
result: {
|
||||
restored: true,
|
||||
mergeJobId: Number(job?.id || 0) || null
|
||||
},
|
||||
job
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/confirm-encode/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const selectedEncodeTitleId = req.body?.selectedEncodeTitleId ?? null;
|
||||
const selectedEncodeTitleIds = req.body?.selectedEncodeTitleIds ?? null;
|
||||
const selectedTrackSelection = req.body?.selectedTrackSelection ?? null;
|
||||
const episodeAssignments = req.body?.episodeAssignments ?? null;
|
||||
const selectedPostEncodeScriptIds = req.body?.selectedPostEncodeScriptIds;
|
||||
const selectedPreEncodeScriptIds = req.body?.selectedPreEncodeScriptIds;
|
||||
const selectedPostEncodeChainIds = req.body?.selectedPostEncodeChainIds;
|
||||
const selectedPreEncodeChainIds = req.body?.selectedPreEncodeChainIds;
|
||||
const skipPipelineStateUpdate = Boolean(req.body?.skipPipelineStateUpdate);
|
||||
const selectedUserPresetId = req.body?.selectedUserPresetId ?? null;
|
||||
const selectedHandBrakePreset = req.body?.selectedHandBrakePreset ?? null;
|
||||
logger.info('post:confirm-encode', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
selectedEncodeTitleId,
|
||||
selectedEncodeTitleIdsCount: Array.isArray(selectedEncodeTitleIds) ? selectedEncodeTitleIds.length : 0,
|
||||
selectedTrackSelectionProvided: Boolean(selectedTrackSelection),
|
||||
episodeAssignmentsProvided: Boolean(episodeAssignments && typeof episodeAssignments === 'object'),
|
||||
skipPipelineStateUpdate,
|
||||
selectedUserPresetId,
|
||||
selectedHandBrakePreset,
|
||||
selectedPostEncodeScriptIdsCount: Array.isArray(selectedPostEncodeScriptIds)
|
||||
? selectedPostEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPreEncodeScriptIdsCount: Array.isArray(selectedPreEncodeScriptIds)
|
||||
? selectedPreEncodeScriptIds.length
|
||||
: 0,
|
||||
selectedPostEncodeChainIdsCount: Array.isArray(selectedPostEncodeChainIds)
|
||||
? selectedPostEncodeChainIds.length
|
||||
: 0,
|
||||
selectedPreEncodeChainIdsCount: Array.isArray(selectedPreEncodeChainIds)
|
||||
? selectedPreEncodeChainIds.length
|
||||
: 0
|
||||
});
|
||||
const job = await pipelineService.confirmEncodeReview(jobId, {
|
||||
selectedEncodeTitleId,
|
||||
selectedEncodeTitleIds,
|
||||
selectedTrackSelection,
|
||||
episodeAssignments,
|
||||
selectedPostEncodeScriptIds,
|
||||
selectedPreEncodeScriptIds,
|
||||
selectedPostEncodeChainIds,
|
||||
selectedPreEncodeChainIds,
|
||||
skipPipelineStateUpdate,
|
||||
selectedUserPresetId,
|
||||
selectedHandBrakePreset
|
||||
});
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/cancel',
|
||||
asyncHandler(async (req, res) => {
|
||||
const rawJobId = req.body?.jobId;
|
||||
const jobId = rawJobId === null || rawJobId === undefined || String(rawJobId).trim() === ''
|
||||
? null
|
||||
: Number(rawJobId);
|
||||
logger.warn('post:cancel', { reqId: req.reqId, jobId });
|
||||
const result = await pipelineService.cancel(jobId);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/retry/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
logger.info('post:retry', { reqId: req.reqId, jobId });
|
||||
const result = await pipelineService.retry(jobId);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/resume-ready/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
logger.info('post:resume-ready', { reqId: req.reqId, jobId });
|
||||
const job = await pipelineService.resumeReadyToEncodeJob(jobId);
|
||||
res.json({ job });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/output-folders/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const folders = await historyService.getJobOutputFoldersForLineage(jobId);
|
||||
res.json({ folders });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/delete-output-folders/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const folderPaths = Array.isArray(req.body?.folderPaths) ? req.body.folderPaths : [];
|
||||
logger.info('post:delete-output-folders', { reqId: req.reqId, jobId, count: folderPaths.length });
|
||||
const result = await historyService.deleteSpecificOutputFolders(jobId, folderPaths);
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/reencode/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const keepBoth = Boolean(req.body?.keepBoth);
|
||||
const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
|
||||
logger.info('post:reencode', { reqId: req.reqId, jobId, keepBoth, deleteFolderCount: deleteFolders.length });
|
||||
const result = await pipelineService.reencodeFromRaw(jobId, { keepBoth, deleteFolders });
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/restart-review/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const keepBoth = Boolean(req.body?.keepBoth);
|
||||
const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
|
||||
const reuseCurrentJob = Boolean(req.body?.reuseCurrentJob);
|
||||
logger.info('post:restart-review', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
keepBoth,
|
||||
reuseCurrentJob,
|
||||
deleteFolderCount: deleteFolders.length
|
||||
});
|
||||
const result = await pipelineService.restartReviewFromRaw(jobId, { keepBoth, deleteFolders, reuseCurrentJob });
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/restart-encode/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const keepBoth = Boolean(req.body?.keepBoth);
|
||||
const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
|
||||
const restartMode = String(req.body?.restartMode || 'all').trim().toLowerCase() || 'all';
|
||||
logger.info('post:restart-encode', {
|
||||
reqId: req.reqId,
|
||||
jobId,
|
||||
keepBoth,
|
||||
restartMode,
|
||||
deleteFolderCount: deleteFolders.length
|
||||
});
|
||||
const result = await pipelineService.restartEncodeWithLastSettings(jobId, {
|
||||
keepBoth,
|
||||
deleteFolders,
|
||||
restartMode
|
||||
});
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/restart-cd-review/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const jobId = Number(req.params.jobId);
|
||||
const keepBoth = Boolean(req.body?.keepBoth);
|
||||
const deleteFolders = Array.isArray(req.body?.deleteFolders) ? req.body.deleteFolders : [];
|
||||
logger.info('post:restart-cd-review', { reqId: req.reqId, jobId, keepBoth, deleteFolderCount: deleteFolders.length });
|
||||
const result = await pipelineService.restartCdReviewFromRaw(jobId, { keepBoth, deleteFolders });
|
||||
res.json({ result });
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/queue',
|
||||
asyncHandler(async (req, res) => {
|
||||
logger.debug('get:queue', { reqId: req.reqId });
|
||||
const queue = await pipelineService.getQueueSnapshot();
|
||||
res.json({ queue });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/queue/reorder',
|
||||
asyncHandler(async (req, res) => {
|
||||
// Accept orderedEntryIds (new) or orderedJobIds (legacy fallback for job-only queues).
|
||||
const orderedEntryIds = Array.isArray(req.body?.orderedEntryIds)
|
||||
? req.body.orderedEntryIds
|
||||
: (Array.isArray(req.body?.orderedJobIds) ? req.body.orderedJobIds : []);
|
||||
logger.info('post:queue:reorder', { reqId: req.reqId, orderedEntryIds });
|
||||
const queue = await pipelineService.reorderQueue(orderedEntryIds);
|
||||
res.json({ queue });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/queue/entry',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { type, scriptId, chainId, waitSeconds, insertAfterEntryId } = req.body || {};
|
||||
logger.info('post:queue:entry', { reqId: req.reqId, type });
|
||||
const result = await pipelineService.enqueueNonJobEntry(
|
||||
type,
|
||||
{ scriptId, chainId, waitSeconds },
|
||||
insertAfterEntryId ?? null
|
||||
);
|
||||
const queue = await pipelineService.getQueueSnapshot();
|
||||
res.json({ result, queue });
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/queue/entry/:entryId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const entryId = req.params.entryId;
|
||||
logger.info('delete:queue:entry', { reqId: req.reqId, entryId });
|
||||
const queue = await pipelineService.removeQueueEntry(entryId);
|
||||
res.json({ queue });
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user