2026-03-31 15:29:48 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* AME Log Parser Module
|
|
|
|
|
|
*
|
2026-03-31 17:22:18 -04:00
|
|
|
|
* Parses Adobe Media Encoder's encoding logs to extract encoding stats:
|
|
|
|
|
|
* completion status, encoding time, source/output files, presets, codec info, and errors.
|
2026-03-31 15:29:48 -04:00
|
|
|
|
*
|
2026-03-31 17:22:18 -04:00
|
|
|
|
* Log file locations and names vary by platform:
|
|
|
|
|
|
*
|
|
|
|
|
|
* Windows:
|
|
|
|
|
|
* - Path: C:\Users\<user>\Documents\Adobe\Adobe Media Encoder\<version>\
|
|
|
|
|
|
* - Files: AMEEncodingLog.txt, AMEEncodingErrorLog.txt
|
|
|
|
|
|
* - Encoding: UTF-16 LE with BOM
|
|
|
|
|
|
*
|
|
|
|
|
|
* macOS:
|
|
|
|
|
|
* - Path: /Users/<user>/Documents/Adobe/Adobe Media Encoder/<version>/logs/
|
|
|
|
|
|
* - Files: Adobe Media Encoder Log.txt, Adobe Media Encoder Error Log.txt
|
|
|
|
|
|
* - Encoding: UTF-8 (usually)
|
2026-03-31 15:29:48 -04:00
|
|
|
|
*
|
|
|
|
|
|
* AME log format (typical entry):
|
|
|
|
|
|
* ────────────────────────────────────
|
|
|
|
|
|
* - Source File: /path/to/source.prproj
|
|
|
|
|
|
* - Output File: /path/to/output.mp4
|
|
|
|
|
|
* - Preset Used: Match Source - H.264
|
|
|
|
|
|
* - Video: 1920x1080 (1.0), 29.97 fps, Progressive, 00:05:30:00, VBR, 1 pass, Target 10.00 Mbps
|
|
|
|
|
|
* - Audio: AAC, 320 kbps, 48 kHz, Stereo
|
|
|
|
|
|
* - Encoding Time: 00:02:15
|
|
|
|
|
|
* - Status: Done
|
|
|
|
|
|
* 3/30/2026 10:15:32 AM
|
|
|
|
|
|
* ────────────────────────────────────
|
|
|
|
|
|
*
|
|
|
|
|
|
* Error log has similar format but with error messages.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
2026-03-31 17:12:41 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Read a file and auto-detect encoding (UTF-16 LE/BE or UTF-8).
|
|
|
|
|
|
* Windows AME writes logs as UTF-16 LE with BOM — Node's default
|
|
|
|
|
|
* fs.readFileSync('utf-8') produces garbled output on these files.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function readFileAutoEncoding(filePath) {
|
|
|
|
|
|
const buf = fs.readFileSync(filePath);
|
|
|
|
|
|
if (buf.length === 0) return '';
|
|
|
|
|
|
|
|
|
|
|
|
// UTF-16 LE BOM: FF FE
|
|
|
|
|
|
if (buf[0] === 0xFF && buf[1] === 0xFE) {
|
|
|
|
|
|
return buf.slice(2).toString('utf16le');
|
|
|
|
|
|
}
|
|
|
|
|
|
// UTF-16 BE BOM: FE FF
|
|
|
|
|
|
if (buf[0] === 0xFE && buf[1] === 0xFF) {
|
|
|
|
|
|
// Swap bytes for Node's utf16le decoder
|
|
|
|
|
|
const swapped = Buffer.alloc(buf.length - 2);
|
|
|
|
|
|
for (let i = 2; i < buf.length - 1; i += 2) {
|
|
|
|
|
|
swapped[i - 2] = buf[i + 1];
|
|
|
|
|
|
swapped[i - 1] = buf[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
return swapped.toString('utf16le');
|
|
|
|
|
|
}
|
|
|
|
|
|
// UTF-8 BOM: EF BB BF
|
|
|
|
|
|
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
|
|
|
|
|
|
return buf.slice(3).toString('utf-8');
|
|
|
|
|
|
}
|
|
|
|
|
|
// No BOM — check if it looks like UTF-16 LE (null bytes in even positions)
|
|
|
|
|
|
if (buf.length >= 4 && buf[1] === 0x00 && buf[3] === 0x00) {
|
|
|
|
|
|
return buf.toString('utf16le');
|
|
|
|
|
|
}
|
|
|
|
|
|
// Default: UTF-8
|
|
|
|
|
|
return buf.toString('utf-8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 15:29:48 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Parse AME encoding log entries from a log file.
|
|
|
|
|
|
* Handles multiple entry formats since AME versions vary.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseAMELog(logContent) {
|
|
|
|
|
|
const entries = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!logContent || !logContent.trim()) return entries;
|
|
|
|
|
|
|
|
|
|
|
|
// Split on common entry separators
|
|
|
|
|
|
// AME uses blank lines or dashes between entries
|
|
|
|
|
|
const lines = logContent.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
let currentEntry = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
|
const line = lines[i].trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!line) {
|
|
|
|
|
|
// Blank line might end an entry
|
|
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length > 1) {
|
|
|
|
|
|
entries.push(currentEntry);
|
|
|
|
|
|
currentEntry = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start a new entry on "Source File" or similar markers
|
|
|
|
|
|
if (line.match(/^-?\s*Source\s*File\s*:/i)) {
|
|
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length > 1) {
|
|
|
|
|
|
entries.push(currentEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
currentEntry = { raw: [] };
|
|
|
|
|
|
currentEntry.sourceFile = extractValue(line);
|
|
|
|
|
|
currentEntry.raw.push(line);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentEntry) {
|
|
|
|
|
|
// Check if this line starts an entry with a different format
|
|
|
|
|
|
// Some AME versions start with the date/time
|
|
|
|
|
|
const dateMatch = line.match(/^(\d{1,2}\/\d{1,2}\/\d{2,4})\s+(\d{1,2}:\d{2}:\d{2}\s*[AP]?M?)/i);
|
|
|
|
|
|
if (dateMatch) {
|
2026-03-31 17:12:41 -04:00
|
|
|
|
// Skip lines that are just queue start/stop/log header timestamps
|
|
|
|
|
|
if (line.match(/Queue\s+(Started|Stopped)/i) || line.match(/Log\s+File\s+Created/i)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-31 15:29:48 -04:00
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length > 1) {
|
|
|
|
|
|
entries.push(currentEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
currentEntry = { raw: [line] };
|
|
|
|
|
|
currentEntry.timestamp = line;
|
|
|
|
|
|
currentEntry.date = dateMatch[1];
|
|
|
|
|
|
currentEntry.time = dateMatch[2];
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Also handle "Encoding <filename>" as entry start
|
|
|
|
|
|
if (line.match(/^Encoding\s+/i)) {
|
|
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length > 1) {
|
|
|
|
|
|
entries.push(currentEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
currentEntry = { raw: [line] };
|
|
|
|
|
|
currentEntry.sourceFile = line.replace(/^Encoding\s+/i, '').trim();
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
currentEntry.raw.push(line);
|
|
|
|
|
|
|
|
|
|
|
|
// Parse known fields
|
|
|
|
|
|
if (line.match(/^-?\s*Output\s*File\s*:/i)) {
|
|
|
|
|
|
currentEntry.outputFile = extractValue(line);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Preset\s*(Used)?\s*:/i)) {
|
|
|
|
|
|
currentEntry.preset = extractValue(line);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Video\s*:/i)) {
|
|
|
|
|
|
currentEntry.video = extractValue(line);
|
|
|
|
|
|
parseVideoDetails(currentEntry);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Audio\s*:/i)) {
|
|
|
|
|
|
currentEntry.audio = extractValue(line);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Encoding\s*Time\s*:/i)) {
|
|
|
|
|
|
currentEntry.encodingTime = extractValue(line);
|
|
|
|
|
|
currentEntry.encodingTimeMs = parseTimeToMs(currentEntry.encodingTime);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Status\s*:/i)) {
|
|
|
|
|
|
currentEntry.status = extractValue(line).toLowerCase();
|
|
|
|
|
|
} else if (line.match(/^-?\s*Error\s*:/i)) {
|
|
|
|
|
|
currentEntry.error = extractValue(line);
|
|
|
|
|
|
if (!currentEntry.status) currentEntry.status = 'error';
|
|
|
|
|
|
} else if (line.match(/^-?\s*Warning\s*:/i)) {
|
|
|
|
|
|
currentEntry.warning = extractValue(line);
|
2026-03-31 17:12:41 -04:00
|
|
|
|
} else if (line.match(/^-?\s*Bitrate\s*:/i)) {
|
|
|
|
|
|
const val = extractValue(line);
|
|
|
|
|
|
if (val) currentEntry.bitrate = val;
|
|
|
|
|
|
} else if (line.match(/File\s+Encoded/i)) {
|
|
|
|
|
|
// AME Windows format: "03/31/2026 05:09:33 PM : File Encoded with warning"
|
|
|
|
|
|
if (line.match(/with\s+warning/i)) {
|
|
|
|
|
|
currentEntry.status = 'warning';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentEntry.status = 'done';
|
|
|
|
|
|
}
|
|
|
|
|
|
currentEntry.type = 'success';
|
|
|
|
|
|
} else if (line.match(/Queue\s+Started/i)) {
|
|
|
|
|
|
// Skip queue start lines — not an entry
|
|
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length <= 2) {
|
|
|
|
|
|
currentEntry = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} else if (line.match(/Queue\s+Stopped/i)) {
|
|
|
|
|
|
// Skip queue stop lines
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} else if (line.match(/^Log\s+File\s+Created/i)) {
|
|
|
|
|
|
// Skip header line
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} else if (line.match(/^-+$/)) {
|
|
|
|
|
|
// Separator line — might end an entry
|
|
|
|
|
|
continue;
|
|
|
|
|
|
} else if (line.match(/Offline\s+media/i) || line.match(/Missing\s+Asset/i)) {
|
|
|
|
|
|
// Warning details from AME
|
|
|
|
|
|
if (!currentEntry.warnings) currentEntry.warnings = [];
|
|
|
|
|
|
currentEntry.warnings.push(line);
|
2026-03-31 15:29:48 -04:00
|
|
|
|
} else if (line.match(/^\d{1,2}\/\d{1,2}\/\d{2,4}/)) {
|
|
|
|
|
|
// Timestamp line at end of entry
|
|
|
|
|
|
currentEntry.timestamp = line;
|
|
|
|
|
|
const dateMatch = line.match(/^(\d{1,2}\/\d{1,2}\/\d{2,4})\s+(\d{1,2}:\d{2}:\d{2}\s*[AP]?M?)/i);
|
|
|
|
|
|
if (dateMatch) {
|
|
|
|
|
|
currentEntry.date = dateMatch[1];
|
|
|
|
|
|
currentEntry.time = dateMatch[2];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (line.match(/^-?\s*Format\s*:/i)) {
|
|
|
|
|
|
currentEntry.format = extractValue(line);
|
|
|
|
|
|
} else if (line.match(/^-?\s*Duration\s*:/i)) {
|
|
|
|
|
|
currentEntry.duration = extractValue(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Don't forget the last entry
|
|
|
|
|
|
if (currentEntry && Object.keys(currentEntry).length > 1) {
|
|
|
|
|
|
entries.push(currentEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return entries;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Parse video details from the video line
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseVideoDetails(entry) {
|
|
|
|
|
|
if (!entry.video) return;
|
|
|
|
|
|
|
|
|
|
|
|
const v = entry.video;
|
|
|
|
|
|
|
|
|
|
|
|
// Resolution: e.g., "1920x1080"
|
|
|
|
|
|
const resMatch = v.match(/(\d{2,5})x(\d{2,5})/);
|
|
|
|
|
|
if (resMatch) {
|
|
|
|
|
|
entry.resolution = `${resMatch[1]}x${resMatch[2]}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Frame rate: e.g., "29.97 fps"
|
|
|
|
|
|
const fpsMatch = v.match(/([\d.]+)\s*fps/i);
|
|
|
|
|
|
if (fpsMatch) {
|
|
|
|
|
|
entry.frameRate = parseFloat(fpsMatch[1]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bitrate: e.g., "Target 10.00 Mbps" or "10 Mbps"
|
|
|
|
|
|
const brMatch = v.match(/([\d.]+)\s*[MKk]bps/i);
|
|
|
|
|
|
if (brMatch) {
|
|
|
|
|
|
entry.videoBitrate = brMatch[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Extract value from a "- Key: Value" or "Key: Value" line
|
|
|
|
|
|
*/
|
|
|
|
|
|
function extractValue(line) {
|
|
|
|
|
|
return line.replace(/^-?\s*[^:]+:\s*/, '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Parse "HH:MM:SS" or "HH:MM:SS.mmm" to milliseconds
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseTimeToMs(timeStr) {
|
|
|
|
|
|
if (!timeStr) return null;
|
|
|
|
|
|
const parts = timeStr.split(':');
|
|
|
|
|
|
if (parts.length >= 3) {
|
|
|
|
|
|
const hrs = parseInt(parts[0]) || 0;
|
|
|
|
|
|
const mins = parseInt(parts[1]) || 0;
|
|
|
|
|
|
const secs = parseFloat(parts[2]) || 0;
|
|
|
|
|
|
return (hrs * 3600 + mins * 60 + secs) * 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Read and parse AME log files from a directory.
|
|
|
|
|
|
* Returns combined results from both encoding and error logs.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function readAMELogs(logDir) {
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
encodingLog: { exists: false, entries: [], lastModified: null, path: null },
|
|
|
|
|
|
errorLog: { exists: false, entries: [], lastModified: null, path: null },
|
|
|
|
|
|
stats: {
|
|
|
|
|
|
totalEncoded: 0,
|
|
|
|
|
|
totalErrors: 0,
|
|
|
|
|
|
totalEncodingTimeMs: 0,
|
|
|
|
|
|
averageEncodingTimeMs: 0,
|
|
|
|
|
|
lastEncoded: null,
|
|
|
|
|
|
lastError: null,
|
|
|
|
|
|
recentEntries: []
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!logDir || !fs.existsSync(logDir)) return result;
|
|
|
|
|
|
|
2026-03-31 17:22:18 -04:00
|
|
|
|
// Find log files — handle both Windows and macOS naming conventions
|
|
|
|
|
|
// Windows: AMEEncodingLog.txt, AMEEncodingErrorLog.txt
|
|
|
|
|
|
// macOS: Adobe Media Encoder Log.txt, Adobe Media Encoder Error Log.txt
|
|
|
|
|
|
let encodingLogPath = path.join(logDir, 'AMEEncodingLog.txt');
|
|
|
|
|
|
if (!fs.existsSync(encodingLogPath)) {
|
|
|
|
|
|
// Try macOS naming
|
|
|
|
|
|
encodingLogPath = path.join(logDir, 'Adobe Media Encoder Log.txt');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let errorLogPath = path.join(logDir, 'AMEEncodingErrorLog.txt');
|
|
|
|
|
|
if (!fs.existsSync(errorLogPath)) {
|
|
|
|
|
|
// Try macOS naming
|
|
|
|
|
|
errorLogPath = path.join(logDir, 'Adobe Media Encoder Error Log.txt');
|
|
|
|
|
|
}
|
2026-03-31 15:29:48 -04:00
|
|
|
|
|
|
|
|
|
|
// Parse encoding log
|
|
|
|
|
|
if (fs.existsSync(encodingLogPath)) {
|
|
|
|
|
|
result.encodingLog.exists = true;
|
|
|
|
|
|
result.encodingLog.path = encodingLogPath;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stat = fs.statSync(encodingLogPath);
|
|
|
|
|
|
result.encodingLog.lastModified = stat.mtime.toISOString();
|
2026-03-31 17:12:41 -04:00
|
|
|
|
const content = readFileAutoEncoding(encodingLogPath);
|
2026-03-31 15:29:48 -04:00
|
|
|
|
result.encodingLog.entries = parseAMELog(content);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
result.encodingLog.error = e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Parse error log
|
|
|
|
|
|
if (fs.existsSync(errorLogPath)) {
|
|
|
|
|
|
result.errorLog.exists = true;
|
|
|
|
|
|
result.errorLog.path = errorLogPath;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stat = fs.statSync(errorLogPath);
|
|
|
|
|
|
result.errorLog.lastModified = stat.mtime.toISOString();
|
2026-03-31 17:12:41 -04:00
|
|
|
|
const content = readFileAutoEncoding(errorLogPath);
|
2026-03-31 15:29:48 -04:00
|
|
|
|
result.errorLog.entries = parseAMELog(content);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
result.errorLog.error = e.message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Compute stats
|
|
|
|
|
|
const allSuccessEntries = result.encodingLog.entries.filter(e =>
|
2026-03-31 17:12:41 -04:00
|
|
|
|
!e.status || e.status === 'done' || e.status === 'complete' || e.status === 'success' || e.status === 'warning'
|
2026-03-31 15:29:48 -04:00
|
|
|
|
);
|
|
|
|
|
|
const allErrorEntries = [
|
|
|
|
|
|
...result.errorLog.entries,
|
|
|
|
|
|
...result.encodingLog.entries.filter(e =>
|
|
|
|
|
|
e.status === 'error' || e.status === 'failed' || e.status === 'stopped'
|
|
|
|
|
|
)
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
result.stats.totalEncoded = allSuccessEntries.length;
|
|
|
|
|
|
result.stats.totalErrors = allErrorEntries.length;
|
|
|
|
|
|
|
|
|
|
|
|
// Encoding time stats
|
|
|
|
|
|
const timesMs = allSuccessEntries
|
|
|
|
|
|
.filter(e => e.encodingTimeMs)
|
|
|
|
|
|
.map(e => e.encodingTimeMs);
|
|
|
|
|
|
|
|
|
|
|
|
if (timesMs.length > 0) {
|
|
|
|
|
|
result.stats.totalEncodingTimeMs = timesMs.reduce((a, b) => a + b, 0);
|
|
|
|
|
|
result.stats.averageEncodingTimeMs = Math.round(result.stats.totalEncodingTimeMs / timesMs.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Last entries
|
|
|
|
|
|
if (allSuccessEntries.length > 0) {
|
|
|
|
|
|
result.stats.lastEncoded = allSuccessEntries[allSuccessEntries.length - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (allErrorEntries.length > 0) {
|
|
|
|
|
|
result.stats.lastError = allErrorEntries[allErrorEntries.length - 1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Recent entries (last 20, combined and sorted newest first)
|
|
|
|
|
|
const allEntries = [
|
|
|
|
|
|
...result.encodingLog.entries.map(e => ({ ...e, type: 'success' })),
|
|
|
|
|
|
...result.errorLog.entries.map(e => ({ ...e, type: 'error' }))
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Take last 20 (entries are appended, so last = newest)
|
|
|
|
|
|
result.stats.recentEntries = allEntries.slice(-20).reverse();
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Watch an AME log file for changes and call a callback with new entries.
|
|
|
|
|
|
* Returns a function to stop watching.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function watchAMELog(logDir, callback, intervalMs = 3000) {
|
|
|
|
|
|
let lastEncodingSize = 0;
|
|
|
|
|
|
let lastErrorSize = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const encodingLogPath = path.join(logDir, 'AMEEncodingLog.txt');
|
|
|
|
|
|
const errorLogPath = path.join(logDir, 'AMEEncodingErrorLog.txt');
|
|
|
|
|
|
|
|
|
|
|
|
// Get initial sizes
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(encodingLogPath)) lastEncodingSize = fs.statSync(encodingLogPath).size;
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(errorLogPath)) lastErrorSize = fs.statSync(errorLogPath).size;
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Check encoding log for new content
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(encodingLogPath)) {
|
|
|
|
|
|
const stat = fs.statSync(encodingLogPath);
|
|
|
|
|
|
if (stat.size > lastEncodingSize) {
|
|
|
|
|
|
// Read only new content
|
|
|
|
|
|
const fd = fs.openSync(encodingLogPath, 'r');
|
|
|
|
|
|
const newBytes = Buffer.alloc(stat.size - lastEncodingSize);
|
|
|
|
|
|
fs.readSync(fd, newBytes, 0, newBytes.length, lastEncodingSize);
|
|
|
|
|
|
fs.closeSync(fd);
|
|
|
|
|
|
lastEncodingSize = stat.size;
|
|
|
|
|
|
|
|
|
|
|
|
const newEntries = parseAMELog(newBytes.toString('utf-8'));
|
|
|
|
|
|
if (newEntries.length > 0) {
|
|
|
|
|
|
callback({ type: 'encoding', entries: newEntries });
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
// Check error log for new content
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (fs.existsSync(errorLogPath)) {
|
|
|
|
|
|
const stat = fs.statSync(errorLogPath);
|
|
|
|
|
|
if (stat.size > lastErrorSize) {
|
|
|
|
|
|
const fd = fs.openSync(errorLogPath, 'r');
|
|
|
|
|
|
const newBytes = Buffer.alloc(stat.size - lastErrorSize);
|
|
|
|
|
|
fs.readSync(fd, newBytes, 0, newBytes.length, lastErrorSize);
|
|
|
|
|
|
fs.closeSync(fd);
|
|
|
|
|
|
lastErrorSize = stat.size;
|
|
|
|
|
|
|
|
|
|
|
|
const newEntries = parseAMELog(newBytes.toString('utf-8'));
|
|
|
|
|
|
if (newEntries.length > 0) {
|
|
|
|
|
|
callback({ type: 'error', entries: newEntries });
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}, intervalMs);
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(timer);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Format milliseconds as human-readable duration
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatDuration(ms) {
|
|
|
|
|
|
if (!ms) return '–';
|
|
|
|
|
|
const secs = Math.floor(ms / 1000);
|
|
|
|
|
|
const hrs = Math.floor(secs / 3600);
|
|
|
|
|
|
const mins = Math.floor((secs % 3600) / 60);
|
|
|
|
|
|
const s = secs % 60;
|
|
|
|
|
|
if (hrs > 0) return `${hrs}h ${mins}m ${s}s`;
|
|
|
|
|
|
if (mins > 0) return `${mins}m ${s}s`;
|
|
|
|
|
|
return `${s}s`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { parseAMELog, readAMELogs, watchAMELog, formatDuration };
|