From 44c82fdffa5eda297a525ebe8707f171a82454de Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:29:48 -0400 Subject: [PATCH] Add ame-log-parser.js --- ame-log-parser.js | 367 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 ame-log-parser.js diff --git a/ame-log-parser.js b/ame-log-parser.js new file mode 100644 index 0000000..e7c1d69 --- /dev/null +++ b/ame-log-parser.js @@ -0,0 +1,367 @@ +/** + * AME Log Parser Module + * + * Parses Adobe Media Encoder's AMEEncodingLog.txt and AMEEncodingErrorLog.txt + * to extract encoding stats: completion status, encoding time, source/output files, + * presets, codec info, and errors. + * + * macOS log path: /Users//Documents/Adobe/Adobe Media Encoder// + * Windows log path: C:\Users\\Documents\Adobe\Adobe Media Encoder\\ + * + * 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'); + +/** + * 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) { + 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 " 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); + } 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; + + // Find log files + const encodingLogPath = path.join(logDir, 'AMEEncodingLog.txt'); + const errorLogPath = path.join(logDir, 'AMEEncodingErrorLog.txt'); + + // 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(); + const content = fs.readFileSync(encodingLogPath, 'utf-8'); + 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(); + const content = fs.readFileSync(errorLogPath, 'utf-8'); + result.errorLog.entries = parseAMELog(content); + } catch (e) { + result.errorLog.error = e.message; + } + } + + // Compute stats + const allSuccessEntries = result.encodingLog.entries.filter(e => + !e.status || e.status === 'done' || e.status === 'complete' || e.status === 'success' + ); + 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 };