Add high-res media folder fallback lookup for unmapped proxy files
- Added hiresMediaFolder setting to configuration UI - Implement findHighResFileForGves() to search for missing high-res media files - When .prproj has unlinked proxy media (FramelightX didn't populate paths), search configured folder for matches - Modified remapPrproj() to accept options with hiresMediaFolder path - Uses file Title metadata and video/audio extension matching as fallback - Server passes hiresMediaFolder setting when calling remapper
This commit is contained in:
parent
c323872a0f
commit
205ef3f50a
3 changed files with 120 additions and 8 deletions
|
|
@ -31,9 +31,10 @@ const NULL_IMPL_ID = '00000000-0000-0000-0000-000000000000';
|
|||
* Premiere expects (XML parsers tend to reformat and break .prproj files).
|
||||
*
|
||||
* @param {Buffer} prprojBuffer - The raw .prproj file (gzipped)
|
||||
* @param {Object} options - Optional config { hiresMediaFolder: string, fsModule?: fs }
|
||||
* @returns {Object} { buffer: Buffer, report: Object }
|
||||
*/
|
||||
async function remapPrproj(prprojBuffer) {
|
||||
async function remapPrproj(prprojBuffer, options = {}) {
|
||||
// Step 1: Decompress
|
||||
const xml = zlib.gunzipSync(prprojBuffer).toString('utf-8');
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ async function remapPrproj(prprojBuffer) {
|
|||
const proxyLinks = parseProxyLinks(xml, mediaBlocks);
|
||||
|
||||
// Step 4: Perform the path swaps
|
||||
const { remappedXml, swaps } = performSwaps(xml, mediaBlocks, proxyLinks);
|
||||
const { remappedXml, swaps } = performSwaps(xml, mediaBlocks, proxyLinks, options);
|
||||
|
||||
// Step 5: Recompress
|
||||
const outputBuffer = zlib.gzipSync(Buffer.from(remappedXml, 'utf-8'));
|
||||
|
|
@ -223,11 +224,94 @@ function parseProxyLinks(xml, mediaBlocks) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Perform the actual path swaps in the XML string.
|
||||
* Try to find a high-res file in the media folder when the .prproj doesn't have a path.
|
||||
* Strategy: Look for files in the high-res media folder that might correspond to this .gves.
|
||||
*
|
||||
* For example:
|
||||
* - .gves file: "01KMR6YXTCJHG0AGMWVF98JRMWC--01.gves"
|
||||
* - Media blocks may have metadata (Title) like "ZacIsCool--File7"
|
||||
* - High-res folder has: "ZacIsCool--File7.mp4"
|
||||
*
|
||||
* Fallback strategy:
|
||||
* 1. If hiresBlock has a Title, search for that title in the high-res folder
|
||||
* 2. Search for common video/audio extensions (.mp4, .mov, .wav, etc.)
|
||||
* 3. If still not found, return null (path won't be swapped)
|
||||
*
|
||||
* @param {string} gvesPath - The .gves file path
|
||||
* @param {string} hiresTitle - Title from the high-res Media block
|
||||
* @param {string} hiresMediaFolder - UNC/SMB path to search
|
||||
* @param {Object} fs - File system module
|
||||
* @returns {string|null} The high-res file path if found, null otherwise
|
||||
*/
|
||||
function performSwaps(xml, mediaBlocks, proxyLinks) {
|
||||
function findHighResFileForGves(gvesPath, hiresTitle, hiresMediaFolder, fs) {
|
||||
if (!hiresMediaFolder) return null;
|
||||
|
||||
try {
|
||||
// Normalize the folder path
|
||||
const searchFolder = hiresMediaFolder.replace(/\\/g, '/').replace(/\/$/, '');
|
||||
|
||||
// If Title is provided, search for it
|
||||
if (hiresTitle) {
|
||||
const videoExts = ['.mp4', '.mov', '.mxf', '.mov', '.wav', '.aif', '.aiff'];
|
||||
|
||||
for (const ext of videoExts) {
|
||||
const candidate = `${searchFolder}/${hiresTitle}${ext}`;
|
||||
try {
|
||||
if (fs.existsSync(candidate)) {
|
||||
// Convert back to UNC format if it was a local path
|
||||
return candidate.replace(/\//g, '\\');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore individual file check failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: list files in the folder and try to match by base name
|
||||
try {
|
||||
const files = fs.readdirSync(searchFolder);
|
||||
const videoExts = new Set(['.mp4', '.mov', '.mxf', '.wav', '.aif', '.aiff']);
|
||||
|
||||
// Extract base name from .gves path (without extension)
|
||||
const gvesFilename = gvesPath.split('\\').pop().split('/').pop();
|
||||
const gvesBaseName = gvesFilename.replace(/\.[^.]+$/, '').toLowerCase();
|
||||
|
||||
// Look for files that might match
|
||||
for (const file of files) {
|
||||
const ext = '.' + file.split('.').pop().toLowerCase();
|
||||
if (!videoExts.has(ext)) continue;
|
||||
|
||||
const fileBaseName = file.replace(/\.[^.]+$/, '').toLowerCase();
|
||||
|
||||
// Partial match: if the file name contains a key part of the gves name
|
||||
if (fileBaseName.includes(gvesBaseName.substring(0, Math.min(8, gvesBaseName.length)))) {
|
||||
const fullPath = `${searchFolder}/${file}`;
|
||||
return fullPath.replace(/\//g, '\\');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Folder read failed, return null
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
// Any errors during lookup, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual path swaps in the XML string.
|
||||
* @param {string} xml - The project XML
|
||||
* @param {Object} mediaBlocks - Parsed media blocks
|
||||
* @param {Object} proxyLinks - Proxy linkage map
|
||||
* @param {Object} options - { hiresMediaFolder?: string, fsModule?: fs }
|
||||
*/
|
||||
function performSwaps(xml, mediaBlocks, proxyLinks, options = {}) {
|
||||
let remappedXml = xml;
|
||||
const swaps = [];
|
||||
const fs = options.fsModule || require('fs');
|
||||
|
||||
// Collect all unique .gves UIDs that have a mapped high-res block
|
||||
const gvesUids = Object.keys(proxyLinks);
|
||||
|
|
@ -237,7 +321,21 @@ function performSwaps(xml, mediaBlocks, proxyLinks) {
|
|||
const gvesBlock = mediaBlocks[gvesUid];
|
||||
const hiresBlock = mediaBlocks[hiresUid];
|
||||
|
||||
if (!gvesBlock || !hiresBlock || !hiresBlock.filePath) continue;
|
||||
if (!gvesBlock || !hiresBlock) continue;
|
||||
|
||||
let hiresPath = hiresBlock.filePath;
|
||||
|
||||
// Fallback: if high-res path is null, try to find it in the media folder
|
||||
if (!hiresPath && options.hiresMediaFolder && gvesBlock.filePath) {
|
||||
hiresPath = findHighResFileForGves(
|
||||
gvesBlock.filePath,
|
||||
hiresBlock.title || gvesBlock.title,
|
||||
options.hiresMediaFolder,
|
||||
fs
|
||||
);
|
||||
}
|
||||
|
||||
if (!hiresPath) continue;
|
||||
|
||||
const hiresPath = hiresBlock.filePath;
|
||||
const hiresTitle = hiresBlock.title || pathToTitle(hiresPath);
|
||||
|
|
|
|||
|
|
@ -604,6 +604,11 @@
|
|||
<input type="text" id="setting-ame-log-dir" placeholder="e.g. /ame-logs or /Users/username/Documents/Adobe/Adobe Media Encoder/25.0">
|
||||
<div class="settings-help">Path to the folder containing <code>AMEEncodingLog.txt</code>. On macOS: <code>/Users/<user>/Documents/Adobe/Adobe Media Encoder/<version></code></div>
|
||||
</div>
|
||||
<div class="settings-field" style="grid-column: 1 / -1">
|
||||
<label>High-Res Media Folder (Optional)</label>
|
||||
<input type="text" id="setting-hires-media-folder" placeholder="e.g. //172.18.210.5/bmg_video/media">
|
||||
<div class="settings-help">UNC/SMB path where high-resolution source files are stored. When .prproj files reference proxy media that aren't linked in the project, the system will search this folder for matching high-res files. Leave blank to disable fallback lookup.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: SMB Credentials -->
|
||||
|
|
@ -807,6 +812,7 @@
|
|||
document.getElementById('setting-watch-folder').value = data.watchFolder || '';
|
||||
document.getElementById('setting-output-folder').value = data.outputFolder || '';
|
||||
document.getElementById('setting-ame-log-dir').value = data.ameLogDir || '';
|
||||
document.getElementById('setting-hires-media-folder').value = data.hiresMediaFolder || '';
|
||||
document.getElementById('setting-smb-username').value = data.smbUsername || '';
|
||||
document.getElementById('setting-smb-password').value = ''; // never pre-fill password
|
||||
document.getElementById('setting-smb-domain').value = data.smbDomain || '';
|
||||
|
|
@ -824,6 +830,7 @@
|
|||
watchFolder: document.getElementById('setting-watch-folder').value.trim(),
|
||||
outputFolder: document.getElementById('setting-output-folder').value.trim(),
|
||||
ameLogDir: document.getElementById('setting-ame-log-dir').value.trim(),
|
||||
hiresMediaFolder: document.getElementById('setting-hires-media-folder').value.trim(),
|
||||
smbUsername: document.getElementById('setting-smb-username').value.trim(),
|
||||
smbPassword: document.getElementById('setting-smb-password').value, // empty = keep existing
|
||||
smbDomain: document.getElementById('setting-smb-domain').value.trim(),
|
||||
|
|
|
|||
13
server.js
13
server.js
|
|
@ -83,7 +83,8 @@ function loadSettings() {
|
|||
smbUsername: '',
|
||||
smbPassword: '',
|
||||
smbDomain: '',
|
||||
smbNotes: ''
|
||||
smbNotes: '',
|
||||
hiresMediaFolder: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +155,7 @@ app.get('/api/settings', requireAuth, (req, res) => {
|
|||
watchFolder: settings.watchFolder || WATCH_FOLDER,
|
||||
outputFolder: settings.outputFolder || OUTPUT_FOLDER,
|
||||
ameLogDir: settings.ameLogDir || AME_LOG_DIR,
|
||||
hiresMediaFolder: settings.hiresMediaFolder || '',
|
||||
smbWatchPath: settings.smbWatchPath || '',
|
||||
smbUsername: settings.smbUsername || '',
|
||||
// Return password masked — UI shows whether one is saved without exposing it
|
||||
|
|
@ -171,13 +173,14 @@ app.get('/api/settings', requireAuth, (req, res) => {
|
|||
*/
|
||||
app.post('/api/settings', requireAuth, (req, res) => {
|
||||
const current = loadSettings();
|
||||
const { watchFolder, outputFolder, ameLogDir, smbWatchPath, smbUsername, smbPassword, smbDomain, smbNotes } = req.body;
|
||||
const { watchFolder, outputFolder, ameLogDir, hiresMediaFolder, smbWatchPath, smbUsername, smbPassword, smbDomain, smbNotes } = req.body;
|
||||
|
||||
const updated = {
|
||||
...current,
|
||||
...(watchFolder !== undefined && { watchFolder }),
|
||||
...(outputFolder !== undefined && { outputFolder }),
|
||||
...(ameLogDir !== undefined && { ameLogDir }),
|
||||
...(hiresMediaFolder !== undefined && { hiresMediaFolder }),
|
||||
...(smbWatchPath !== undefined && { smbWatchPath }),
|
||||
...(smbUsername !== undefined && { smbUsername }),
|
||||
// Only update password if a non-empty value was sent (empty = keep existing)
|
||||
|
|
@ -196,6 +199,7 @@ app.post('/api/settings', requireAuth, (req, res) => {
|
|||
watchFolder: updated.watchFolder,
|
||||
outputFolder: updated.outputFolder,
|
||||
ameLogDir: updated.ameLogDir,
|
||||
hiresMediaFolder: updated.hiresMediaFolder || '',
|
||||
smbWatchPath: updated.smbWatchPath || '',
|
||||
smbUsername: updated.smbUsername || '',
|
||||
smbPasswordSet: !!(updated.smbPassword),
|
||||
|
|
@ -256,7 +260,10 @@ app.post('/api/jobs', requireAuth, upload.single('prproj'), async (req, res) =>
|
|||
// Perform the remap
|
||||
let remapResult;
|
||||
try {
|
||||
remapResult = await remapPrproj(prprojBuffer);
|
||||
const settings = loadSettings();
|
||||
remapResult = await remapPrproj(prprojBuffer, {
|
||||
hiresMediaFolder: settings.hiresMediaFolder
|
||||
});
|
||||
} catch (err) {
|
||||
fs.unlinkSync(uploadedPath);
|
||||
return res.status(500).json({
|
||||
|
|
|
|||
Loading…
Reference in a new issue