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