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:
parent
d1d4fd0e9d
commit
f7f42351b9
1 changed files with 72 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue