When proxyLinks is empty (no explicit .gves→hires linkage), now also process unlinked high-res proxy blocks that have: - IsProxy: true - FilePath: null - Title metadata (e.g. 'File 1', 'File 2') Uses fallback lookup to find matching files in hiresMediaFolder by Title, then injects FilePath into the XML Media block. This handles the case where FramelightX creates proxies but doesn't establish explicit linkages.
566 lines
20 KiB
JavaScript
566 lines
20 KiB
JavaScript
/**
|
|
* .prproj Remapper Module
|
|
*
|
|
* Decompresses a Premiere Pro project file (gzipped XML), parses the media references,
|
|
* and swaps .gves file paths with their paired high-res media paths using the
|
|
* proxy linkage structure embedded in the project.
|
|
*
|
|
* The .prproj structure for FramelightX imports:
|
|
* - Each clip has a .gves Media block (the active media during editing)
|
|
* - Each clip also has a paired high-res Media block (marked IsProxy=true, OfflineReason=1)
|
|
* - AudioProxy/VideoProxy elements link .gves blocks → high-res blocks via ProxyMedia ObjectURef
|
|
*
|
|
* This module:
|
|
* 1. Finds all .gves Media blocks
|
|
* 2. Follows proxy linkages to find paired high-res paths
|
|
* 3. Replaces .gves paths with high-res UNC paths
|
|
* 4. Removes IsProxy/OfflineReason flags so AME treats high-res as online
|
|
* 5. Clears the FramelightX ImplementationID so AME uses native importers
|
|
*/
|
|
|
|
const zlib = require('zlib');
|
|
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
|
|
// FramelightX plugin importer GUID
|
|
const FRAMELIGHTX_IMPL_ID = '1fa18bfa-255c-44b1-ad73-56bcd99fceaf';
|
|
const NULL_IMPL_ID = '00000000-0000-0000-0000-000000000000';
|
|
|
|
/**
|
|
* Remap a .prproj buffer, swapping .gves references with high-res media paths.
|
|
* Uses string-based XML manipulation to preserve the exact XML structure that
|
|
* 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, options = {}) {
|
|
// Step 1: Decompress
|
|
const xml = zlib.gunzipSync(prprojBuffer).toString('utf-8');
|
|
|
|
// Step 2: Build a map of all Media blocks by ObjectUID
|
|
const mediaBlocks = parseMediaBlocks(xml);
|
|
|
|
// Step 3: Build proxy linkage map (gves ObjectUID → high-res ObjectUID)
|
|
const proxyLinks = parseProxyLinks(xml, mediaBlocks);
|
|
|
|
// Step 4: Perform the path swaps
|
|
const { remappedXml, swaps } = performSwaps(xml, mediaBlocks, proxyLinks, options);
|
|
|
|
// Step 5: Recompress
|
|
const outputBuffer = zlib.gzipSync(Buffer.from(remappedXml, 'utf-8'));
|
|
|
|
const report = {
|
|
totalMediaBlocks: Object.keys(mediaBlocks).length,
|
|
gvesBlocks: Object.values(mediaBlocks).filter(m => m.isGves).length,
|
|
highResBlocks: Object.values(mediaBlocks).filter(m => m.isProxy).length,
|
|
swapsPerformed: swaps.length,
|
|
swaps: swaps,
|
|
// "Unmapped" = .gves paths that were NOT covered by any swap (path-level, not block-level).
|
|
// Multiple Media blocks can share the same .gves path (video+audio streams), which is fine
|
|
// because the global string replacement catches all of them.
|
|
unmappedGves: (() => {
|
|
const swappedPaths = new Set(swaps.map(s => s.oldPath.toLowerCase()));
|
|
const unmapped = Object.values(mediaBlocks)
|
|
.filter(m => m.isGves && !swappedPaths.has(m.filePath.toLowerCase()));
|
|
// Deduplicate by path
|
|
const seen = new Set();
|
|
return unmapped.filter(m => {
|
|
if (seen.has(m.filePath.toLowerCase())) return false;
|
|
seen.add(m.filePath.toLowerCase());
|
|
return true;
|
|
}).map(m => ({ uid: m.objectUid, path: m.filePath }));
|
|
})()
|
|
};
|
|
|
|
return { buffer: outputBuffer, report };
|
|
}
|
|
|
|
/**
|
|
* Parse all <Media> blocks from the XML and extract key properties.
|
|
* Uses regex to avoid XML parser reformatting issues.
|
|
*/
|
|
function parseMediaBlocks(xml) {
|
|
const blocks = {};
|
|
// Match Media blocks with ObjectUID attribute
|
|
const mediaRegex = /<Media\s+ObjectUID="([^"]+)"[^>]*>([\s\S]*?)<\/Media>/g;
|
|
let match;
|
|
|
|
while ((match = mediaRegex.exec(xml)) !== null) {
|
|
const uid = match[1];
|
|
const body = match[2];
|
|
|
|
const filePath = extractTag(body, 'FilePath');
|
|
const actualMediaFilePath = extractTag(body, 'ActualMediaFilePath');
|
|
const isProxy = extractTag(body, 'IsProxy') === 'true';
|
|
const offlineReason = extractTag(body, 'OfflineReason');
|
|
const implementationID = extractTag(body, 'ImplementationID');
|
|
const title = extractTag(body, 'Title');
|
|
const relativePath = extractTag(body, 'RelativePath');
|
|
|
|
const isGves = filePath && filePath.toLowerCase().endsWith('.gves');
|
|
|
|
blocks[uid] = {
|
|
objectUid: uid,
|
|
filePath,
|
|
actualMediaFilePath,
|
|
relativePath,
|
|
isProxy,
|
|
offlineReason,
|
|
implementationID,
|
|
title,
|
|
isGves,
|
|
fullMatch: match[0],
|
|
startIndex: match.index
|
|
};
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* Parse AudioProxy and VideoProxy elements to build the linkage map.
|
|
* Structure: The proxy element is a child of a clip content block that references
|
|
* a .gves Media block. The proxy element's ProxyMedia ObjectURef points to the
|
|
* high-res Media block.
|
|
*/
|
|
function parseProxyLinks(xml, mediaBlocks) {
|
|
const links = {}; // highResUid → Set of gvesUids it's proxied by
|
|
|
|
// Find all AudioProxy elements
|
|
const proxyRegex = /<(?:Audio|Video)Proxy\s+ObjectID="[^"]*"[^>]*>([\s\S]*?)<\/(?:Audio|Video)Proxy>/g;
|
|
let match;
|
|
|
|
while ((match = proxyRegex.exec(xml)) !== null) {
|
|
const body = match[1];
|
|
const proxyMediaRef = extractAttr(body, 'ProxyMedia', 'ObjectURef');
|
|
|
|
if (proxyMediaRef && mediaBlocks[proxyMediaRef]) {
|
|
// proxyMediaRef points to the high-res (IsProxy=true) block
|
|
if (!links[proxyMediaRef]) {
|
|
links[proxyMediaRef] = new Set();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now we need to figure out which .gves block maps to which high-res block.
|
|
// The relationship is: a clip's content references a .gves Media block AND has
|
|
// AudioProxy/VideoProxy children that point to high-res blocks.
|
|
//
|
|
// Simpler approach: match by file name/title similarity, or by the fact that
|
|
// .gves and high-res blocks for the same clip appear in sequence and share
|
|
// the same Start timecode.
|
|
//
|
|
// Most reliable: match by Start timecode, which is identical for paired blocks.
|
|
|
|
const gvesBlocks = Object.values(mediaBlocks).filter(m => m.isGves);
|
|
const hiresBlocks = Object.values(mediaBlocks).filter(m => m.isProxy);
|
|
|
|
// Build a map from .gves UID → high-res block by matching via AudioProxy structure.
|
|
// We look for content blocks that contain both a Media ObjectURef (to .gves) and
|
|
// AudioProxy elements (pointing to high-res).
|
|
const gvesToHires = {};
|
|
|
|
// Strategy: find <Content> blocks that tie .gves Media to AudioProxy
|
|
const contentRegex = /<Content[^>]*>([\s\S]*?)<\/Content>/g;
|
|
while ((match = contentRegex.exec(xml)) !== null) {
|
|
const body = match[1];
|
|
|
|
// Find the Media ObjectURef in this content block
|
|
const mediaRef = extractAttr(body, 'Media', 'ObjectURef');
|
|
if (!mediaRef || !mediaBlocks[mediaRef] || !mediaBlocks[mediaRef].isGves) continue;
|
|
|
|
// Find AudioProxy references in this content block
|
|
const audioProxyRefs = [];
|
|
const apRegex = /<AudioProxyItem[^>]*ObjectRef="([^"]*)"[^>]*\/>/g;
|
|
let apMatch;
|
|
while ((apMatch = apRegex.exec(body)) !== null) {
|
|
audioProxyRefs.push(apMatch[1]);
|
|
}
|
|
|
|
// For each AudioProxy ObjectRef, find the actual AudioProxy element and its ProxyMedia
|
|
for (const apRef of audioProxyRefs) {
|
|
const apRegex2 = new RegExp(
|
|
`<(?:Audio|Video)Proxy\\s+ObjectID="${apRef}"[^>]*>([\\s\\S]*?)<\\/(?:Audio|Video)Proxy>`
|
|
);
|
|
const apMatch2 = apRegex2.exec(xml);
|
|
if (apMatch2) {
|
|
const proxyMediaRef = extractAttr(apMatch2[1], 'ProxyMedia', 'ObjectURef');
|
|
if (proxyMediaRef && mediaBlocks[proxyMediaRef] && mediaBlocks[proxyMediaRef].isProxy) {
|
|
gvesToHires[mediaRef] = proxyMediaRef;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: if Content-based matching didn't catch everything, try Start timecode matching
|
|
for (const gves of gvesBlocks) {
|
|
if (gvesToHires[gves.objectUid]) continue;
|
|
|
|
const gvesStart = extractTag(
|
|
xml.substring(gves.startIndex, gves.startIndex + gves.fullMatch.length),
|
|
'Start'
|
|
);
|
|
|
|
if (!gvesStart) continue;
|
|
|
|
for (const hires of hiresBlocks) {
|
|
if (Object.values(gvesToHires).includes(hires.objectUid)) continue;
|
|
|
|
const hiresStart = extractTag(
|
|
xml.substring(hires.startIndex, hires.startIndex + hires.fullMatch.length),
|
|
'Start'
|
|
);
|
|
|
|
if (hiresStart === gvesStart) {
|
|
gvesToHires[gves.objectUid] = hires.objectUid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return gvesToHires;
|
|
}
|
|
|
|
/**
|
|
* 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 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 files containing that title
|
|
if (hiresTitle) {
|
|
try {
|
|
const files = fs.readdirSync(searchFolder);
|
|
const videoExts = new Set(['.mp4', '.mov', '.mxf', '.wav', '.aif', '.aiff']);
|
|
const titleLower = hiresTitle.toLowerCase();
|
|
|
|
// Look for files that contain the title as a substring
|
|
for (const file of files) {
|
|
const ext = '.' + file.split('.').pop().toLowerCase();
|
|
if (!videoExts.has(ext)) continue;
|
|
|
|
const fileLower = file.toLowerCase();
|
|
// Match if filename contains the title
|
|
if (fileLower.includes(titleLower)) {
|
|
const fullPath = `${searchFolder}/${file}`;
|
|
return fullPath.replace(/\//g, '\\');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignore folder read failures, fall through to secondary search
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Also collect any .gves blocks that aren't in proxyLinks (for unlinked proxies)
|
|
const allGvesBlocks = Object.entries(mediaBlocks)
|
|
.filter(([uid, block]) => block.isGves)
|
|
.map(([uid, block]) => ({ uid, block }));
|
|
|
|
// Process linked proxy pairs first
|
|
for (const gvesUid of gvesUids) {
|
|
const hiresUid = proxyLinks[gvesUid];
|
|
const gvesBlock = mediaBlocks[gvesUid];
|
|
const hiresBlock = mediaBlocks[hiresUid];
|
|
|
|
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 hiresTitle = hiresBlock.title || pathToTitle(hiresPath);
|
|
|
|
// Calculate relative path for the high-res file
|
|
const hiresRelativePath = hiresPath; // UNC paths don't use relative paths
|
|
|
|
swaps.push({
|
|
gvesUid,
|
|
hiresUid,
|
|
oldPath: gvesBlock.filePath,
|
|
newPath: hiresPath,
|
|
newTitle: hiresTitle
|
|
});
|
|
|
|
// Replace ALL occurrences of this .gves path in the entire XML.
|
|
// This catches FilePath, ActualMediaFilePath, and any other references.
|
|
const gvesPath = gvesBlock.filePath;
|
|
const gvesPathEscaped = escapeRegex(gvesPath);
|
|
|
|
// Replace FilePath tags containing this .gves path
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<FilePath>${gvesPathEscaped}</FilePath>`, 'g'),
|
|
`<FilePath>${hiresPath}</FilePath>`
|
|
);
|
|
|
|
// Replace ActualMediaFilePath tags
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<ActualMediaFilePath>${gvesPathEscaped}</ActualMediaFilePath>`, 'g'),
|
|
`<ActualMediaFilePath>${hiresPath}</ActualMediaFilePath>`
|
|
);
|
|
|
|
// Replace RelativePath tags for this .gves
|
|
if (gvesBlock.relativePath) {
|
|
const relPathEscaped = escapeRegex(gvesBlock.relativePath);
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<RelativePath>${relPathEscaped}</RelativePath>`, 'g'),
|
|
`<RelativePath>${hiresPath}</RelativePath>`
|
|
);
|
|
}
|
|
|
|
// Replace Title tags that match the .gves filename
|
|
const gvesFilename = gvesPath.split('\\').pop().split('/').pop();
|
|
if (gvesFilename) {
|
|
const gvesTitleEscaped = escapeRegex(gvesFilename);
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<Title>${gvesTitleEscaped}</Title>`, 'g'),
|
|
`<Title>${hiresTitle}</Title>`
|
|
);
|
|
}
|
|
|
|
// Replace ImplementationID (FramelightX → null GUID so AME uses native importer)
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<ImplementationID>${escapeRegex(FRAMELIGHTX_IMPL_ID)}</ImplementationID>`, 'g'),
|
|
`<ImplementationID>${NULL_IMPL_ID}</ImplementationID>`
|
|
);
|
|
}
|
|
|
|
// Handle unlinked high-res proxy blocks (have IsProxy: true, FilePath: null, no proxyLinks entry)
|
|
// These occur when FramelightX created proxy blocks but didn't link them explicitly
|
|
if (options.hiresMediaFolder && allGvesBlocks.length > 0) {
|
|
// Get the first .gves block as reference (for path lookup)
|
|
const gvesRef = allGvesBlocks[0].block;
|
|
|
|
for (const [uid, hiresBlock] of Object.entries(mediaBlocks)) {
|
|
// Skip if already processed, not a proxy, or already has a path
|
|
if (gvesUids.includes(uid) || !hiresBlock.isProxy || hiresBlock.filePath) continue;
|
|
|
|
// Try to find a high-res file for this unlinked proxy using its Title
|
|
let hiresPath = findHighResFileForGves(
|
|
gvesRef.filePath, // Use first gves path for reference
|
|
hiresBlock.title,
|
|
options.hiresMediaFolder,
|
|
fs
|
|
);
|
|
|
|
if (!hiresPath) continue;
|
|
|
|
const hiresTitle = hiresBlock.title || pathToTitle(hiresPath);
|
|
|
|
// Perform replacements for this unlinked proxy
|
|
// We'll replace the Title in the XML with the found file
|
|
if (hiresBlock.title) {
|
|
const titleEscaped = escapeRegex(hiresBlock.title);
|
|
remappedXml = remappedXml.replace(
|
|
new RegExp(`<Title>${titleEscaped}</Title>`, 'g'),
|
|
`<Title>${hiresTitle}</Title>`
|
|
);
|
|
}
|
|
|
|
// Add a FilePath to the high-res block's Media section
|
|
// Look for the Media block with this Title and add FilePath if missing
|
|
const mediaBlockRegex = new RegExp(
|
|
`<Media[^>]*>([^<]*<Title>${escapeRegex(hiresBlock.title)}</Title>[^<]*)</Media>`,
|
|
'g'
|
|
);
|
|
remappedXml = remappedXml.replace(mediaBlockRegex, (match) => {
|
|
if (match.includes('<FilePath>')) return match; // Already has path
|
|
return match.replace(`</Media>`, `<FilePath>${hiresPath}</FilePath></Media>`);
|
|
});
|
|
|
|
swaps.push({
|
|
gvesUid: 'unlinked',
|
|
hiresUid: uid,
|
|
oldPath: 'null',
|
|
newPath: hiresPath,
|
|
newTitle: hiresTitle
|
|
});
|
|
}
|
|
}
|
|
|
|
// Remove IsProxy and OfflineReason from high-res blocks so they're treated as online
|
|
remappedXml = remappedXml.replace(/\s*<IsProxy>true<\/IsProxy>/g, '');
|
|
remappedXml = remappedXml.replace(/\s*<OfflineReason>\d+<\/OfflineReason>/g, '');
|
|
|
|
return { remappedXml, swaps };
|
|
}
|
|
|
|
// ─── Utility functions ─────────────────────────────────────────────
|
|
|
|
function extractTag(xml, tagName) {
|
|
const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`);
|
|
const match = regex.exec(xml);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
function extractAttr(xml, tagName, attrName) {
|
|
const regex = new RegExp(`<${tagName}\\s+${attrName}="([^"]*)"`, '');
|
|
const match = regex.exec(xml);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
function escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function pathToTitle(filePath) {
|
|
const filename = filePath.split('\\').pop().split('/').pop();
|
|
// Remove extension
|
|
return filename.replace(/\.[^.]+$/, '');
|
|
}
|
|
|
|
/**
|
|
* Analyze a .prproj file without modifying it.
|
|
* Returns a report of all media references and their linkages.
|
|
*/
|
|
async function analyzePrproj(prprojBuffer) {
|
|
const xml = zlib.gunzipSync(prprojBuffer).toString('utf-8');
|
|
const mediaBlocks = parseMediaBlocks(xml);
|
|
const proxyLinks = parseProxyLinks(xml, mediaBlocks);
|
|
|
|
const gvesEntries = Object.values(mediaBlocks).filter(m => m.isGves);
|
|
const hiresEntries = Object.values(mediaBlocks).filter(m => m.isProxy);
|
|
|
|
// Debug: log all media blocks found
|
|
console.log('=== DEBUG: All Media Blocks ===');
|
|
Object.entries(mediaBlocks).forEach(([uid, block]) => {
|
|
console.log(`UID: ${uid}`);
|
|
console.log(` FilePath: ${block.filePath}`);
|
|
console.log(` IsGves: ${block.isGves}`);
|
|
console.log(` IsProxy: ${block.isProxy}`);
|
|
console.log(` Title: ${block.title}`);
|
|
console.log('---');
|
|
});
|
|
console.log('=== DEBUG: Proxy Links ===');
|
|
console.log(JSON.stringify(proxyLinks, null, 2));
|
|
|
|
// Deduplicate mappings by .gves path (multiple blocks can share the same path)
|
|
const mappings = [];
|
|
const seenGvesPaths = new Set();
|
|
for (const [gvesUid, hiresUid] of Object.entries(proxyLinks)) {
|
|
const gves = mediaBlocks[gvesUid];
|
|
const hires = mediaBlocks[hiresUid];
|
|
const gvesPath = gves ? gves.filePath.toLowerCase() : '';
|
|
if (seenGvesPaths.has(gvesPath)) continue;
|
|
seenGvesPaths.add(gvesPath);
|
|
mappings.push({
|
|
gvesPath: gves ? gves.filePath : 'unknown',
|
|
hiresPath: hires ? hires.filePath : 'unknown',
|
|
hiresExtension: hires ? hires.filePath.split('.').pop().toLowerCase() : 'unknown'
|
|
});
|
|
}
|
|
|
|
// Count unique unmapped .gves paths
|
|
const unmappedPaths = new Set();
|
|
for (const g of gvesEntries) {
|
|
if (!seenGvesPaths.has(g.filePath.toLowerCase())) {
|
|
unmappedPaths.add(g.filePath.toLowerCase());
|
|
}
|
|
}
|
|
|
|
// Debug: include all media blocks so we can see the structure
|
|
const allMediaBlocks = Object.entries(mediaBlocks).map(([uid, block]) => ({
|
|
uid,
|
|
filePath: block.filePath,
|
|
isGves: block.isGves,
|
|
isProxy: block.isProxy,
|
|
title: block.title,
|
|
implementationID: block.implementationID
|
|
}));
|
|
|
|
return {
|
|
totalMediaBlocks: Object.keys(mediaBlocks).length,
|
|
gvesReferences: gvesEntries.length,
|
|
hiresReferences: hiresEntries.length,
|
|
mappedPairs: mappings.length,
|
|
unmappedGves: unmappedPaths.size,
|
|
mappings,
|
|
hiresFormats: [...new Set(mappings.map(m => m.hiresExtension))],
|
|
// Debug: include raw media blocks
|
|
_debug_mediaBlocks: allMediaBlocks,
|
|
_debug_proxyLinks: proxyLinks
|
|
};
|
|
}
|
|
|
|
module.exports = { remapPrproj, analyzePrproj };
|