From f7f42351b965a84a3702fa0533f91ae960f6b08c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 17:12:41 -0400 Subject: [PATCH] fix: Handle UTF-16 LE encoded AME logs from Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ame-log-parser.js | 75 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/ame-log-parser.js b/ame-log-parser.js index e7c1d69..a1ed4fe 100644 --- a/ame-log-parser.js +++ b/ame-log-parser.js @@ -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,