Add server.js
This commit is contained in:
parent
47ad07366d
commit
8caaeaf13c
1 changed files with 538 additions and 0 deletions
538
server.js
Normal file
538
server.js
Normal file
|
|
@ -0,0 +1,538 @@
|
||||||
|
/**
|
||||||
|
* AME Remote Job Manager — Express Server
|
||||||
|
*
|
||||||
|
* Manages Adobe Media Encoder jobs via watch folder integration.
|
||||||
|
* Editors upload .prproj files, the server remaps .gves paths to high-res UNC paths,
|
||||||
|
* and delivers the remapped file to AME's watch folder for rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { remapPrproj, analyzePrproj } = require('./prproj-remapper');
|
||||||
|
const { readAMELogs, watchAMELog } = require('./ame-log-parser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// ─── Configuration ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3100;
|
||||||
|
const WATCH_FOLDER = process.env.WATCH_FOLDER || '/watch';
|
||||||
|
const OUTPUT_FOLDER = process.env.OUTPUT_FOLDER || '/output';
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||||
|
const UPLOAD_TEMP = process.env.UPLOAD_TEMP || '/tmp/uploads';
|
||||||
|
const AUTH_USER = process.env.AUTH_USER || 'admin';
|
||||||
|
const AUTH_PASS = process.env.AUTH_PASS || 'password';
|
||||||
|
const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS) || 5000;
|
||||||
|
// Job timeout: if a file has been in the watch folder longer than this, mark as stuck
|
||||||
|
const JOB_TIMEOUT_MS = parseInt(process.env.JOB_TIMEOUT_MS) || 3600000; // 1 hour default
|
||||||
|
// AME log directory (mount the folder containing AMEEncodingLog.txt)
|
||||||
|
const AME_LOG_DIR = process.env.AME_LOG_DIR || '/ame-logs';
|
||||||
|
|
||||||
|
// ─── Data Store ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const DB_FILE = path.join(DATA_DIR, 'jobs.json');
|
||||||
|
const SESSIONS_FILE = path.join(DATA_DIR, 'sessions.json');
|
||||||
|
const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json');
|
||||||
|
|
||||||
|
function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDb() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(DB_FILE, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return { jobs: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDb(db) {
|
||||||
|
ensureDir(DATA_DIR);
|
||||||
|
fs.writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSessions() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSessions(sessions) {
|
||||||
|
ensureDir(DATA_DIR);
|
||||||
|
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
watchFolder: WATCH_FOLDER,
|
||||||
|
outputFolder: OUTPUT_FOLDER,
|
||||||
|
ameLogDir: AME_LOG_DIR,
|
||||||
|
smbWatchPath: '',
|
||||||
|
smbUsername: '',
|
||||||
|
smbPassword: '',
|
||||||
|
smbDomain: '',
|
||||||
|
smbNotes: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings(settings) {
|
||||||
|
ensureDir(DATA_DIR);
|
||||||
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveWatchFolder() {
|
||||||
|
const s = loadSettings();
|
||||||
|
return s.watchFolder || WATCH_FOLDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveAmeLogDir() {
|
||||||
|
const s = loadSettings();
|
||||||
|
return s.ameLogDir || AME_LOG_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Session Auth (matches s3-uploader pattern) ────────────────────
|
||||||
|
|
||||||
|
const sessions = loadSessions();
|
||||||
|
|
||||||
|
function generateSessionId() {
|
||||||
|
return crypto.randomBytes(32).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
const sessionId = req.headers['x-session-id'];
|
||||||
|
if (sessionId && sessions[sessionId]) {
|
||||||
|
sessions[sessionId].lastAccess = Date.now();
|
||||||
|
req.username = sessions[sessionId].username;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
app.post('/api/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (username === AUTH_USER && password === AUTH_PASS) {
|
||||||
|
const sessionId = generateSessionId();
|
||||||
|
sessions[sessionId] = { username, created: Date.now(), lastAccess: Date.now() };
|
||||||
|
saveSessions(sessions);
|
||||||
|
return res.json({ sessionId, username });
|
||||||
|
}
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
app.post('/api/logout', (req, res) => {
|
||||||
|
const sessionId = req.headers['x-session-id'];
|
||||||
|
if (sessionId) {
|
||||||
|
delete sessions[sessionId];
|
||||||
|
saveSessions(sessions);
|
||||||
|
}
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Settings API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/settings — Get current configurable settings
|
||||||
|
*/
|
||||||
|
app.get('/api/settings', requireAuth, (req, res) => {
|
||||||
|
const settings = loadSettings();
|
||||||
|
res.json({
|
||||||
|
watchFolder: settings.watchFolder || WATCH_FOLDER,
|
||||||
|
outputFolder: settings.outputFolder || OUTPUT_FOLDER,
|
||||||
|
ameLogDir: settings.ameLogDir || AME_LOG_DIR,
|
||||||
|
smbWatchPath: settings.smbWatchPath || '',
|
||||||
|
smbUsername: settings.smbUsername || '',
|
||||||
|
// Return password masked — UI shows whether one is saved without exposing it
|
||||||
|
smbPasswordSet: !!(settings.smbPassword),
|
||||||
|
smbDomain: settings.smbDomain || '',
|
||||||
|
smbNotes: settings.smbNotes || '',
|
||||||
|
defaultWatchFolder: WATCH_FOLDER,
|
||||||
|
defaultOutputFolder: OUTPUT_FOLDER,
|
||||||
|
defaultAmeLogDir: AME_LOG_DIR
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/settings — Update configurable settings
|
||||||
|
*/
|
||||||
|
app.post('/api/settings', requireAuth, (req, res) => {
|
||||||
|
const current = loadSettings();
|
||||||
|
const { watchFolder, outputFolder, ameLogDir, smbWatchPath, smbUsername, smbPassword, smbDomain, smbNotes } = req.body;
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...current,
|
||||||
|
...(watchFolder !== undefined && { watchFolder }),
|
||||||
|
...(outputFolder !== undefined && { outputFolder }),
|
||||||
|
...(ameLogDir !== undefined && { ameLogDir }),
|
||||||
|
...(smbWatchPath !== undefined && { smbWatchPath }),
|
||||||
|
...(smbUsername !== undefined && { smbUsername }),
|
||||||
|
// Only update password if a non-empty value was sent (empty = keep existing)
|
||||||
|
...(smbPassword !== undefined && smbPassword !== '' && { smbPassword }),
|
||||||
|
...(smbDomain !== undefined && { smbDomain }),
|
||||||
|
...(smbNotes !== undefined && { smbNotes })
|
||||||
|
};
|
||||||
|
|
||||||
|
saveSettings(updated);
|
||||||
|
console.log('Settings updated (password redacted):', { ...updated, smbPassword: updated.smbPassword ? '***' : '' });
|
||||||
|
|
||||||
|
// Return same masked response as GET
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
settings: {
|
||||||
|
watchFolder: updated.watchFolder,
|
||||||
|
outputFolder: updated.outputFolder,
|
||||||
|
ameLogDir: updated.ameLogDir,
|
||||||
|
smbWatchPath: updated.smbWatchPath || '',
|
||||||
|
smbUsername: updated.smbUsername || '',
|
||||||
|
smbPasswordSet: !!(updated.smbPassword),
|
||||||
|
smbDomain: updated.smbDomain || '',
|
||||||
|
smbNotes: updated.smbNotes || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Multer upload config ──────────────────────────────────────────
|
||||||
|
|
||||||
|
ensureDir(UPLOAD_TEMP);
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: UPLOAD_TEMP,
|
||||||
|
limits: { fileSize: 500 * 1024 * 1024 }, // 500MB max
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (path.extname(file.originalname).toLowerCase() === '.prproj') {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only .prproj files are accepted'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── API Routes ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/jobs — Upload a .prproj and create a new job
|
||||||
|
*/
|
||||||
|
app.post('/api/jobs', requireAuth, upload.single('prproj'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = loadDb();
|
||||||
|
const jobId = crypto.randomUUID();
|
||||||
|
const originalName = req.file.originalname;
|
||||||
|
const uploadedPath = req.file.path;
|
||||||
|
|
||||||
|
// Read the uploaded file
|
||||||
|
const prprojBuffer = fs.readFileSync(uploadedPath);
|
||||||
|
|
||||||
|
// Analyze first to show what we found
|
||||||
|
let analysis;
|
||||||
|
try {
|
||||||
|
analysis = await analyzePrproj(prprojBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
// Clean up temp file
|
||||||
|
fs.unlinkSync(uploadedPath);
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Failed to parse .prproj file',
|
||||||
|
details: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the remap
|
||||||
|
let remapResult;
|
||||||
|
try {
|
||||||
|
remapResult = await remapPrproj(prprojBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
fs.unlinkSync(uploadedPath);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Failed to remap .prproj file',
|
||||||
|
details: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write remapped file to watch folder (use settings-driven path)
|
||||||
|
const currentWatchFolder = getActiveWatchFolder();
|
||||||
|
const outputFilename = `${path.basename(originalName, '.prproj')}_${jobId.substring(0, 8)}.prproj`;
|
||||||
|
const watchFolderPath = path.join(currentWatchFolder, outputFilename);
|
||||||
|
|
||||||
|
ensureDir(currentWatchFolder);
|
||||||
|
fs.writeFileSync(watchFolderPath, remapResult.buffer);
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
fs.unlinkSync(uploadedPath);
|
||||||
|
|
||||||
|
// Create job record
|
||||||
|
const job = {
|
||||||
|
id: jobId,
|
||||||
|
originalFilename: originalName,
|
||||||
|
remappedFilename: outputFilename,
|
||||||
|
submittedBy: req.username,
|
||||||
|
submittedAt: new Date().toISOString(),
|
||||||
|
status: 'queued', // queued → encoding → complete → error
|
||||||
|
statusUpdatedAt: new Date().toISOString(),
|
||||||
|
analysis,
|
||||||
|
remapReport: remapResult.report,
|
||||||
|
watchFolderPath: outputFilename,
|
||||||
|
outputFiles: [],
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
db.jobs.unshift(job);
|
||||||
|
saveDb(db);
|
||||||
|
|
||||||
|
res.json({ job });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Job submission error:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/jobs/analyze — Analyze a .prproj without submitting (dry run)
|
||||||
|
*/
|
||||||
|
app.post('/api/jobs/analyze', requireAuth, upload.single('prproj'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const prprojBuffer = fs.readFileSync(req.file.path);
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
|
||||||
|
const analysis = await analyzePrproj(prprojBuffer);
|
||||||
|
res.json({ analysis });
|
||||||
|
} catch (err) {
|
||||||
|
if (req.file && fs.existsSync(req.file.path)) {
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/jobs — List all jobs
|
||||||
|
*/
|
||||||
|
app.get('/api/jobs', requireAuth, (req, res) => {
|
||||||
|
const db = loadDb();
|
||||||
|
res.json({ jobs: db.jobs });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/jobs/:id — Get a single job
|
||||||
|
*/
|
||||||
|
app.get('/api/jobs/:id', requireAuth, (req, res) => {
|
||||||
|
const db = loadDb();
|
||||||
|
const job = db.jobs.find(j => j.id === req.params.id);
|
||||||
|
if (!job) return res.status(404).json({ error: 'Job not found' });
|
||||||
|
res.json({ job });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/jobs/:id — Delete a job record
|
||||||
|
*/
|
||||||
|
app.delete('/api/jobs/:id', requireAuth, (req, res) => {
|
||||||
|
const db = loadDb();
|
||||||
|
const idx = db.jobs.findIndex(j => j.id === req.params.id);
|
||||||
|
if (idx === -1) return res.status(404).json({ error: 'Job not found' });
|
||||||
|
db.jobs.splice(idx, 1);
|
||||||
|
saveDb(db);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/status — System status (watch folder, output folder, job counts)
|
||||||
|
*/
|
||||||
|
app.get('/api/status', requireAuth, (req, res) => {
|
||||||
|
const db = loadDb();
|
||||||
|
const currentWatchFolder = getActiveWatchFolder();
|
||||||
|
const watchFolderExists = fs.existsSync(currentWatchFolder);
|
||||||
|
const outputFolderExists = fs.existsSync(OUTPUT_FOLDER);
|
||||||
|
|
||||||
|
let watchFolderFiles = [];
|
||||||
|
let outputFolderFiles = [];
|
||||||
|
|
||||||
|
if (watchFolderExists) {
|
||||||
|
try {
|
||||||
|
watchFolderFiles = fs.readdirSync(currentWatchFolder).filter(f => !f.startsWith('.'));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputFolderExists) {
|
||||||
|
try {
|
||||||
|
outputFolderFiles = fs.readdirSync(OUTPUT_FOLDER).filter(f => !f.startsWith('.'));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const counts = {
|
||||||
|
queued: db.jobs.filter(j => j.status === 'queued').length,
|
||||||
|
encoding: db.jobs.filter(j => j.status === 'encoding').length,
|
||||||
|
complete: db.jobs.filter(j => j.status === 'complete').length,
|
||||||
|
error: db.jobs.filter(j => j.status === 'error').length,
|
||||||
|
total: db.jobs.length
|
||||||
|
};
|
||||||
|
|
||||||
|
// AME log stats
|
||||||
|
const activeAmeLogDir = getActiveAmeLogDir();
|
||||||
|
const ameLogs = readAMELogs(activeAmeLogDir);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
watchFolder: { exists: watchFolderExists, path: currentWatchFolder, files: watchFolderFiles },
|
||||||
|
outputFolder: { exists: outputFolderExists, path: OUTPUT_FOLDER, files: outputFolderFiles },
|
||||||
|
counts,
|
||||||
|
ame: {
|
||||||
|
logDir: activeAmeLogDir,
|
||||||
|
logDirExists: fs.existsSync(activeAmeLogDir),
|
||||||
|
encodingLog: {
|
||||||
|
exists: ameLogs.encodingLog.exists,
|
||||||
|
lastModified: ameLogs.encodingLog.lastModified,
|
||||||
|
entryCount: ameLogs.encodingLog.entries.length
|
||||||
|
},
|
||||||
|
errorLog: {
|
||||||
|
exists: ameLogs.errorLog.exists,
|
||||||
|
lastModified: ameLogs.errorLog.lastModified,
|
||||||
|
entryCount: ameLogs.errorLog.entries.length
|
||||||
|
},
|
||||||
|
stats: ameLogs.stats
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/ame/logs — Full AME log data with recent entries
|
||||||
|
*/
|
||||||
|
app.get('/api/ame/logs', requireAuth, (req, res) => {
|
||||||
|
const ameLogs = readAMELogs(getActiveAmeLogDir());
|
||||||
|
res.json(ameLogs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Watch Folder Monitor ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls the watch folder and output folder to infer job status.
|
||||||
|
* - File in watch folder → queued (AME hasn't picked it up yet)
|
||||||
|
* - File disappeared from watch folder → encoding (AME is working on it)
|
||||||
|
* - New file in output folder → complete
|
||||||
|
* - File gone from watch folder + no output + timeout exceeded → error/stuck
|
||||||
|
*/
|
||||||
|
function pollFolders() {
|
||||||
|
const db = loadDb();
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// Get current watch folder contents
|
||||||
|
const currentWatchFolder = getActiveWatchFolder();
|
||||||
|
let watchFiles = new Set();
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(currentWatchFolder)) {
|
||||||
|
watchFiles = new Set(fs.readdirSync(currentWatchFolder).filter(f => !f.startsWith('.')));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Get current output folder contents
|
||||||
|
let outputFiles = [];
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(OUTPUT_FOLDER)) {
|
||||||
|
outputFiles = fs.readdirSync(OUTPUT_FOLDER).filter(f => !f.startsWith('.'));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
for (const job of db.jobs) {
|
||||||
|
if (job.status === 'complete' || job.status === 'error') continue;
|
||||||
|
|
||||||
|
const inWatchFolder = watchFiles.has(job.remappedFilename);
|
||||||
|
|
||||||
|
if (job.status === 'queued' && !inWatchFolder) {
|
||||||
|
// File disappeared from watch folder — AME picked it up
|
||||||
|
job.status = 'encoding';
|
||||||
|
job.statusUpdatedAt = new Date().toISOString();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.status === 'encoding') {
|
||||||
|
// Check if any new output files appeared that match this job
|
||||||
|
// AME typically names output based on sequence name or project name
|
||||||
|
const jobBaseName = path.basename(job.remappedFilename, '.prproj').toLowerCase();
|
||||||
|
const matchingOutputs = outputFiles.filter(f =>
|
||||||
|
f.toLowerCase().includes(jobBaseName.split('_')[0]) // Match on original project name prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also check for any new files that appeared since the job started encoding
|
||||||
|
if (matchingOutputs.length > 0) {
|
||||||
|
job.status = 'complete';
|
||||||
|
job.statusUpdatedAt = new Date().toISOString();
|
||||||
|
job.outputFiles = matchingOutputs;
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
// Check for timeout
|
||||||
|
const elapsed = Date.now() - new Date(job.statusUpdatedAt).getTime();
|
||||||
|
if (elapsed > JOB_TIMEOUT_MS) {
|
||||||
|
job.status = 'error';
|
||||||
|
job.statusUpdatedAt = new Date().toISOString();
|
||||||
|
job.error = `Job timed out after ${Math.round(JOB_TIMEOUT_MS / 60000)} minutes with no output detected`;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.status === 'queued') {
|
||||||
|
// Check if file has been sitting in watch folder too long
|
||||||
|
const elapsed = Date.now() - new Date(job.submittedAt).getTime();
|
||||||
|
if (elapsed > JOB_TIMEOUT_MS && inWatchFolder) {
|
||||||
|
job.status = 'error';
|
||||||
|
job.statusUpdatedAt = new Date().toISOString();
|
||||||
|
job.error = 'AME did not pick up the file from the watch folder within the timeout period';
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
saveDb(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Start Server ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ensureDir(DATA_DIR);
|
||||||
|
ensureDir(UPLOAD_TEMP);
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`AME Remote Job Manager running on port ${PORT}`);
|
||||||
|
console.log(` Watch folder: ${WATCH_FOLDER}`);
|
||||||
|
console.log(` Output folder: ${OUTPUT_FOLDER}`);
|
||||||
|
console.log(` AME log dir: ${AME_LOG_DIR}`);
|
||||||
|
console.log(` Data dir: ${DATA_DIR}`);
|
||||||
|
|
||||||
|
// Start polling folders
|
||||||
|
setInterval(pollFolders, POLL_INTERVAL_MS);
|
||||||
|
console.log(` Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
||||||
|
|
||||||
|
// Start watching AME logs for real-time updates
|
||||||
|
if (fs.existsSync(AME_LOG_DIR)) {
|
||||||
|
watchAMELog(AME_LOG_DIR, ({ type, entries }) => {
|
||||||
|
console.log(`AME ${type} log updated: ${entries.length} new entries`);
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.sourceFile) {
|
||||||
|
console.log(` ${type}: ${entry.sourceFile} ${entry.encodingTime ? '(' + entry.encodingTime + ')' : ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(` Watching AME logs for changes`);
|
||||||
|
} else {
|
||||||
|
console.log(` AME log dir not found: ${AME_LOG_DIR} (mount it to enable AME stats)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue