fix: Handle UTF-16 LE encoded AME logs from Windows

Windows Adobe Media Encoder writes AMEEncodingLog.txt as UTF-16 LE with
BOM. The parser was reading files as UTF-8, producing garbled output where
every character had a null byte between it — causing all regex matching
to fail silently and return zero parsed entries.

Added readFileAutoEncoding() that detects UTF-16 LE/BE BOM and converts
to UTF-8 before parsing. Also handles BOM-less UTF-16 by checking for
null byte patterns.

Additionally improved parser to handle Windows AME log format:
- "File Encoded with warning" status lines (not just "Status: Done")
- "Queue Started/Stopped" lines are now skipped
- "Log File Created" header lines are skipped
- Separator lines (dashes) are skipped
- Offline media warnings and missing asset lines are captured
- "warning" status counts as success in stats

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-03-31 17:12:41 -04:00
parent d1d4fd0e9d
commit f7f42351b9

View file

@ -26,6 +26,41 @@
const fs = require('fs');
const path = require('path');
/**
* 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');
}
/**
* Parse AME encoding log entries from a log file.
* Handles multiple entry formats since AME versions vary.
@ -69,6 +104,10 @@ function parseAMELog(logContent) {
// 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) {
// 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;
}
if (currentEntry && Object.keys(currentEntry).length > 1) {
entries.push(currentEntry);
}
@ -114,6 +153,36 @@ function parseAMELog(logContent) {
if (!currentEntry.status) currentEntry.status = 'error';
} else if (line.match(/^-?\s*Warning\s*:/i)) {
currentEntry.warning = extractValue(line);
} 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);
} else if (line.match(/^\d{1,2}\/\d{1,2}\/\d{2,4}/)) {
// Timestamp line at end of entry
currentEntry.timestamp = line;
@ -218,7 +287,7 @@ function readAMELogs(logDir) {
try {
const stat = fs.statSync(encodingLogPath);
result.encodingLog.lastModified = stat.mtime.toISOString();
const content = fs.readFileSync(encodingLogPath, 'utf-8');
const content = readFileAutoEncoding(encodingLogPath);
result.encodingLog.entries = parseAMELog(content);
} catch (e) {
result.encodingLog.error = e.message;
@ -232,7 +301,7 @@ function readAMELogs(logDir) {
try {
const stat = fs.statSync(errorLogPath);
result.errorLog.lastModified = stat.mtime.toISOString();
const content = fs.readFileSync(errorLogPath, 'utf-8');
const content = readFileAutoEncoding(errorLogPath);
result.errorLog.entries = parseAMELog(content);
} catch (e) {
result.errorLog.error = e.message;
@ -241,7 +310,7 @@ function readAMELogs(logDir) {
// Compute stats
const allSuccessEntries = result.encodingLog.entries.filter(e =>
!e.status || e.status === 'done' || e.status === 'complete' || e.status === 'success'
!e.status || e.status === 'done' || e.status === 'complete' || e.status === 'success' || e.status === 'warning'
);
const allErrorEntries = [
...result.errorLog.entries,