fix(premiere-plugin): CSInterface init, correct API prefix, Node.js download, lazy thumbnails, proper ExtendScript export API: main.js

This commit is contained in:
Zac Gaetano 2026-05-15 21:36:13 -04:00
parent c162104b7c
commit a239e30ef2

View file

@ -3,25 +3,28 @@
* Main JavaScript file for the CEP panel * Main JavaScript file for the CEP panel
*/ */
// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls
const csInterface = new CSInterface();
// ============================================================================ // ============================================================================
// State Management // State Management
// ============================================================================ // ============================================================================
const state = { const state = {
serverUrl: localStorage.getItem("mam_server_url") || "http://localhost:7434", serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434',
isConnected: false, isConnected: false,
isConnecting: false, isConnecting: false,
selectedAsset: null, selectedAsset: null,
assets: [], assets: [],
filteredAssets: [],
projects: [], projects: [],
selectedProject: "all", selectedProject: 'all',
searchQuery: "", searchQuery: '',
currentPage: 0, currentPage: 0,
pageSize: 50, pageSize: 50,
totalAssets: 0, totalAssets: 0,
downloadProgress: 0, downloadProgress: 0,
isDownloading: false isDownloading: false,
thumbCache: {}, // assetId -> signed URL
}; };
// ============================================================================ // ============================================================================
@ -32,70 +35,89 @@ let elements = {};
function initDOMElements() { function initDOMElements() {
elements = { elements = {
// Connection bar serverUrlInput: document.getElementById('server-url'),
serverUrlInput: document.getElementById("server-url"), connectBtn: document.getElementById('connect-btn'),
connectBtn: document.getElementById("connect-btn"), statusIndicator: document.getElementById('status-indicator'),
statusIndicator: document.getElementById("status-indicator"), searchInput: document.getElementById('search-input'),
projectFilter: document.getElementById('project-filter'),
// Search and filter assetGrid: document.getElementById('asset-grid'),
searchInput: document.getElementById("search-input"), emptyState: document.getElementById('empty-state'),
projectFilter: document.getElementById("project-filter"), detailsPanel: document.getElementById('details-panel'),
detailsFilename: document.getElementById('details-filename'),
// Asset grid detailsCodec: document.getElementById('details-codec'),
assetGrid: document.getElementById("asset-grid"), detailsResolution: document.getElementById('details-resolution'),
emptyState: document.getElementById("empty-state"), detailsFps: document.getElementById('details-fps'),
detailsDuration: document.getElementById('details-duration'),
// Details panel detailsSize: document.getElementById('details-size'),
detailsPanel: document.getElementById("details-panel"), detailsTags: document.getElementById('details-tags'),
detailsFilename: document.getElementById("details-filename"), importBtn: document.getElementById('import-btn'),
detailsCodec: document.getElementById("details-codec"), importAllBtn: document.getElementById('import-all-btn'),
detailsResolution: document.getElementById("details-resolution"), progressContainer: document.getElementById('progress-container'),
detailsFps: document.getElementById("details-fps"), progressLabel: document.getElementById('progress-label'),
detailsDuration: document.getElementById("details-duration"), progressFill: document.getElementById('progress-fill'),
detailsSize: document.getElementById("details-size"),
detailsTags: document.getElementById("details-tags"),
// Action bar
importBtn: document.getElementById("import-btn"),
importAllBtn: document.getElementById("import-all-btn"),
// Progress
progressContainer: document.getElementById("progress-container"),
progressLabel: document.getElementById("progress-label"),
progressFill: document.getElementById("progress-fill")
}; };
} }
// ============================================================================
// Thumbnail Lazy Loading (IntersectionObserver)
// ============================================================================
const thumbObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
const assetId = img.dataset.assetId;
if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
thumbObserver.unobserve(img);
}
});
}, { rootMargin: '100px' });
async function loadThumbnail(img, assetId) {
if (state.thumbCache[assetId]) {
img.src = state.thumbCache[assetId];
img.dataset.loaded = '1';
return;
}
try {
const r = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/thumbnail`, {
headers: { Accept: 'application/json' },
});
if (!r.ok) return;
const { url } = await r.json();
if (!url) return;
state.thumbCache[assetId] = url;
img.src = url;
img.dataset.loaded = '1';
} catch (_) {
// thumbnail unavailable — leave placeholder visible
}
}
// ============================================================================ // ============================================================================
// Initialization // Initialization
// ============================================================================ // ============================================================================
document.addEventListener("DOMContentLoaded", () => { document.addEventListener('DOMContentLoaded', () => {
initDOMElements(); initDOMElements();
setupEventListeners(); setupEventListeners();
restoreSettings(); restoreSettings();
logMessage("Wild Dragon MAM panel initialized"); logMessage('Wild Dragon MAM panel initialized');
}); });
function setupEventListeners() { function setupEventListeners() {
// Connection controls elements.serverUrlInput.addEventListener('change', (e) => {
elements.serverUrlInput.addEventListener("change", (e) => { state.serverUrl = e.target.value.trim().replace(/\/$/, '');
state.serverUrl = e.target.value; localStorage.setItem('mam_server_url', state.serverUrl);
localStorage.setItem("mam_server_url", state.serverUrl); state.thumbCache = {}; // bust cache when server changes
}); });
elements.connectBtn.addEventListener("click", connectToServer); elements.connectBtn.addEventListener('click', connectToServer);
elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
// Search and filter elements.projectFilter.addEventListener('change', handleProjectFilter);
elements.searchInput.addEventListener("input", debounce(handleSearch, 300)); elements.assetGrid.addEventListener('click', handleAssetClick);
elements.projectFilter.addEventListener("change", handleProjectFilter); elements.importBtn.addEventListener('click', importSelectedAsset);
elements.importAllBtn.addEventListener('click', importAllAssets);
// Asset grid events are delegated
elements.assetGrid.addEventListener("click", handleAssetClick);
// Import buttons
elements.importBtn.addEventListener("click", importSelectedAsset);
elements.importAllBtn.addEventListener("click", importAllAssets);
} }
function restoreSettings() { function restoreSettings() {
@ -110,34 +132,34 @@ async function connectToServer() {
if (state.isConnecting) return; if (state.isConnecting) return;
state.isConnecting = true; state.isConnecting = true;
updateConnectionStatus("connecting"); updateConnectionStatus('connecting');
elements.connectBtn.disabled = true; elements.connectBtn.disabled = true;
try { try {
const response = await fetch(`${state.serverUrl}/api/health`, { // Use the projects endpoint as a health check (no separate /health route exists)
method: "GET", const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
headers: { method: 'GET',
"Accept": "application/json" headers: { Accept: 'application/json' },
}
}); });
if (response.ok) { if (response.ok) {
state.isConnected = true; state.isConnected = true;
updateConnectionStatus("connected"); updateConnectionStatus('connected');
elements.connectBtn.textContent = "Reconnect"; elements.connectBtn.textContent = 'Reconnect';
logMessage("Connected to Wild Dragon MAM"); logMessage('Connected to Wild Dragon MAM');
// Fetch initial data // Pass the already-fetched JSON to avoid a second round-trip
await fetchProjects(); const projectData = await response.json();
await fetchProjects(projectData);
await fetchAssets(); await fetchAssets();
} else { } else {
throw new Error(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`);
} }
} catch (error) { } catch (error) {
console.error("Connection error:", error); console.error('Connection error:', error);
state.isConnected = false; state.isConnected = false;
updateConnectionStatus("disconnected"); updateConnectionStatus('disconnected');
elements.connectBtn.textContent = "Connect"; elements.connectBtn.textContent = 'Connect';
showErrorMessage(`Failed to connect: ${error.message}`); showErrorMessage(`Failed to connect: ${error.message}`);
} finally { } finally {
state.isConnecting = false; state.isConnecting = false;
@ -147,79 +169,67 @@ async function connectToServer() {
function updateConnectionStatus(status) { function updateConnectionStatus(status) {
const indicator = elements.statusIndicator; const indicator = elements.statusIndicator;
indicator.classList.remove('connected', 'connecting');
indicator.classList.remove("connected", "connecting"); if (status === 'connected') indicator.classList.add('connected');
else if (status === 'connecting') indicator.classList.add('connecting');
if (status === "connected") {
indicator.classList.add("connected");
} else if (status === "connecting") {
indicator.classList.add("connecting");
}
} }
// ============================================================================ // ============================================================================
// API Calls // API Calls
// ============================================================================ // ============================================================================
async function fetchProjects() { /**
* Load projects into state and the filter dropdown.
* @param {Array} [preloadedData] - If provided, skips the fetch (reuse connection-check response).
*/
async function fetchProjects(preloadedData) {
try { try {
const response = await fetch(`${state.serverUrl}/api/projects`, { let projects;
headers: { if (preloadedData) {
"Accept": "application/json" // GET /api/v1/projects returns a plain array (not { projects: [] })
} projects = Array.isArray(preloadedData) ? preloadedData : [];
} else {
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
headers: { Accept: 'application/json' },
}); });
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json(); const data = await response.json();
state.projects = data.projects || []; projects = Array.isArray(data) ? data : [];
}
// Update project filter state.projects = projects;
const oldSelectedProject = state.selectedProject;
const savedProject = state.selectedProject;
elements.projectFilter.innerHTML = '<option value="all">All Projects</option>'; elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
state.projects.forEach((p) => {
state.projects.forEach((project) => { const opt = document.createElement('option');
const option = document.createElement("option"); opt.value = p.id;
option.value = project.id; opt.textContent = p.name;
option.textContent = project.name; elements.projectFilter.appendChild(opt);
elements.projectFilter.appendChild(option);
}); });
elements.projectFilter.value = savedProject;
elements.projectFilter.value = oldSelectedProject;
logMessage(`Loaded ${state.projects.length} projects`); logMessage(`Loaded ${state.projects.length} projects`);
} catch (error) { } catch (error) {
console.error("Error fetching projects:", error); console.error('Error fetching projects:', error);
showErrorMessage("Failed to fetch projects");
} }
} }
async function fetchAssets(page = 0) { async function fetchAssets(page = 0) {
if (!state.isConnected) { if (!state.isConnected) return;
showErrorMessage("Not connected to server");
return;
}
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
offset: page * state.pageSize, offset: page * state.pageSize,
limit: state.pageSize limit: state.pageSize,
}); });
if (state.searchQuery) { if (state.searchQuery) params.append('search', state.searchQuery);
params.append("search", state.searchQuery); if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);
}
if (state.selectedProject !== "all") {
params.append("project_id", state.selectedProject);
}
const response = await fetch( const response = await fetch(
`${state.serverUrl}/api/assets?${params.toString()}`, `${state.serverUrl}/api/v1/assets?${params.toString()}`,
{ { headers: { Accept: 'application/json' } }
headers: {
"Accept": "application/json"
}
}
); );
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
@ -232,52 +242,37 @@ async function fetchAssets(page = 0) {
renderAssets(); renderAssets();
logMessage(`Loaded ${state.assets.length} assets`); logMessage(`Loaded ${state.assets.length} assets`);
} catch (error) { } catch (error) {
console.error("Error fetching assets:", error); console.error('Error fetching assets:', error);
showErrorMessage("Failed to fetch assets"); showErrorMessage('Failed to fetch assets');
} }
} }
async function fetchAssetDetails(assetId) { async function fetchAssetDetails(assetId) {
if (!state.isConnected) return null; if (!state.isConnected) return null;
try { try {
const response = await fetch(`${state.serverUrl}/api/assets/${assetId}`, { const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
headers: { headers: { Accept: 'application/json' },
"Accept": "application/json"
}
}); });
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error("Error fetching asset details:", error); console.error('Error fetching asset details:', error);
return null; return null;
} }
} }
async function getSignedUrl(assetId, proxyId = null) { /**
if (!state.isConnected) return null; * Returns a short-lived presigned URL for the H.264 proxy of the given asset.
* GET /api/v1/assets/:id/stream -> { url: '...' }
try { */
const endpoint = proxyId async function getSignedDownloadUrl(assetId) {
? `${state.serverUrl}/api/assets/${assetId}/proxies/${proxyId}/download` const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
: `${state.serverUrl}/api/assets/${assetId}/download`; headers: { Accept: 'application/json' },
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Accept": "application/json"
}
}); });
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
if (!response.ok) throw new Error(`HTTP ${response.status}`); const { url } = await response.json();
if (!url) throw new Error('Stream endpoint returned no URL');
const data = await response.json(); return url;
return data.signed_url;
} catch (error) {
console.error("Error getting signed URL:", error);
return null;
}
} }
// ============================================================================ // ============================================================================
@ -285,112 +280,108 @@ async function getSignedUrl(assetId, proxyId = null) {
// ============================================================================ // ============================================================================
function renderAssets() { function renderAssets() {
elements.assetGrid.innerHTML = ""; elements.assetGrid.innerHTML = '';
if (state.assets.length === 0) { if (state.assets.length === 0) {
elements.emptyState.style.display = "flex"; elements.emptyState.style.display = 'flex';
return; return;
} }
elements.emptyState.style.display = "none"; elements.emptyState.style.display = 'none';
state.assets.forEach((asset) => { state.assets.forEach((asset) => {
const card = createAssetCard(asset); elements.assetGrid.appendChild(createAssetCard(asset));
elements.assetGrid.appendChild(card);
}); });
} }
function createAssetCard(asset) { function createAssetCard(asset) {
const card = document.createElement("div"); const card = document.createElement('div');
card.className = "asset-card"; card.className = 'asset-card';
if (state.selectedAsset?.id === asset.id) { if (state.selectedAsset && state.selectedAsset.id === asset.id) {
card.classList.add("selected"); card.classList.add('selected');
} }
card.dataset.assetId = asset.id; card.dataset.assetId = asset.id;
// Thumbnail // Thumbnail — lazy loaded by IntersectionObserver
const thumbnail = document.createElement("div"); const thumbnail = document.createElement('div');
thumbnail.className = "asset-thumbnail"; thumbnail.className = 'asset-thumbnail';
if (asset.thumbnail_url) { const img = document.createElement('img');
const img = document.createElement("img"); img.dataset.assetId = asset.id;
img.src = asset.thumbnail_url; img.alt = escapeHtml(asset.display_name || asset.filename || '');
img.alt = asset.filename; img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
img.loading = "lazy"; img.onerror = () => { img.style.display = 'none'; };
img.onerror = () => {
img.style.display = "none";
thumbnail.textContent = "No preview";
};
thumbnail.appendChild(img); thumbnail.appendChild(img);
} else { thumbObserver.observe(img);
thumbnail.textContent = "No preview";
}
card.appendChild(thumbnail); card.appendChild(thumbnail);
// Info // Info row
const info = document.createElement("div"); const info = document.createElement('div');
info.className = "asset-info"; info.className = 'asset-info';
const filename = document.createElement("div"); const name = asset.display_name || asset.filename || 'Untitled';
filename.className = "asset-filename"; const filenameEl = document.createElement('div');
filename.title = asset.filename; filenameEl.className = 'asset-filename';
filename.textContent = asset.filename; filenameEl.title = name;
info.appendChild(filename); filenameEl.textContent = name;
info.appendChild(filenameEl);
const meta = document.createElement("div"); const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
meta.className = "asset-meta"; const codec = (asset.metadata && asset.metadata.codec) || asset.media_type || 'video';
meta.innerHTML = ` const meta = document.createElement('div');
<span>${asset.duration ? formatDuration(asset.duration) : "N/A"}</span> meta.className = 'asset-meta';
<span>${asset.codec || "N/A"}</span> meta.innerHTML = [
`; '<span>' + (durationSec ? formatDuration(durationSec) : 'N/A') + '</span>',
'<span>' + escapeHtml(codec.toUpperCase()) + '</span>',
].join('');
info.appendChild(meta); info.appendChild(meta);
const status = document.createElement("div"); const statusStr = asset.status || 'ready';
status.className = `asset-status-badge status-badge ${asset.status || "ready"}`; const statusBadge = document.createElement('div');
status.textContent = (asset.status || "ready").toUpperCase(); statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
info.appendChild(status); statusBadge.textContent = statusStr.toUpperCase();
info.appendChild(statusBadge);
card.appendChild(info); card.appendChild(info);
return card; return card;
} }
function showAssetDetails(asset) { function showAssetDetails(asset) {
state.selectedAsset = asset; state.selectedAsset = asset;
elements.detailsPanel.classList.remove('hidden');
elements.detailsPanel.classList.remove("hidden"); const meta = asset.metadata || {};
elements.detailsFilename.textContent = asset.filename; elements.detailsFilename.textContent = asset.display_name || asset.filename;
elements.detailsCodec.textContent = asset.codec || "Unknown"; elements.detailsCodec.textContent = meta.codec || asset.media_type || 'Unknown';
elements.detailsResolution.textContent = asset.resolution || "N/A"; elements.detailsResolution.textContent = meta.resolution || 'N/A';
elements.detailsFps.textContent = asset.fps || "N/A"; elements.detailsFps.textContent = meta.fps ? meta.fps + ' fps' : 'N/A';
elements.detailsDuration.textContent = asset.duration elements.detailsDuration.textContent = asset.duration_ms
? formatDuration(asset.duration) ? formatDuration(asset.duration_ms / 1000)
: "N/A"; : 'N/A';
elements.detailsSize.textContent = asset.file_size elements.detailsSize.textContent = meta.file_size
? formatFileSize(asset.file_size) ? formatFileSize(meta.file_size)
: "N/A"; : 'N/A';
// Render tags elements.detailsTags.innerHTML = '';
elements.detailsTags.innerHTML = ""; const tags = asset.tags || [];
if (asset.tags && asset.tags.length > 0) { if (tags.length > 0) {
asset.tags.forEach((tag) => { tags.forEach((tag) => {
const tagEl = document.createElement("span"); const el = document.createElement('span');
tagEl.className = "tag"; el.className = 'tag';
tagEl.textContent = tag; el.textContent = tag;
elements.detailsTags.appendChild(tagEl); elements.detailsTags.appendChild(el);
}); });
} else { } else {
elements.detailsTags.innerHTML = '<span style="color: var(--text-secondary);">No tags</span>'; elements.detailsTags.innerHTML =
'<span style="color:var(--text-secondary)">No tags</span>';
} }
// Update import button state
elements.importBtn.disabled = false; elements.importBtn.disabled = false;
} }
function hideAssetDetails() { function hideAssetDetails() {
state.selectedAsset = null; state.selectedAsset = null;
elements.detailsPanel.classList.add("hidden"); elements.detailsPanel.classList.add('hidden');
elements.importBtn.disabled = true; elements.importBtn.disabled = true;
} }
@ -416,158 +407,157 @@ function handleProjectFilter(e) {
async function importSelectedAsset() { async function importSelectedAsset() {
if (!state.selectedAsset) { if (!state.selectedAsset) {
showErrorMessage("No asset selected"); showErrorMessage('No asset selected');
return; return;
} }
await importAsset(state.selectedAsset); await importAsset(state.selectedAsset);
} }
async function importAllAssets() { async function importAllAssets() {
if (state.assets.length === 0) { if (state.assets.length === 0) {
showErrorMessage("No assets to import"); showErrorMessage('No assets to import');
return; return;
} }
for (const asset of state.assets) { for (const asset of state.assets) {
await importAsset(asset); await importAsset(asset);
} }
showSuccessMessage('All assets imported');
showSuccessMessage("All assets imported");
} }
async function importAsset(asset) { async function importAsset(asset) {
try { try {
elements.importBtn.disabled = true; elements.importBtn.disabled = true;
// Get the proxy file (or original if no proxy) // Step 1: get a presigned proxy URL from the MAM API
const proxyId = asset.proxy_file_id || asset.file_id; showProgress('Getting download link...', 5);
if (!proxyId) { const url = await getSignedDownloadUrl(asset.id);
showErrorMessage("No file available for import");
return;
}
// Get signed URL for download // Step 2: download the proxy (H.264) to the OS temp directory
showProgress("Getting download link...", 0); const safeName = sanitizeFilename(
const signedUrl = await getSignedUrl(asset.id, proxyId); (asset.display_name || asset.filename || asset.id) + '.mp4'
);
showProgress('Downloading ' + safeName + '...', 10);
const filePath = await downloadFile(url, safeName);
if (!signedUrl) { // Step 3: hand the local path to Premiere Pro via ExtendScript
showErrorMessage("Failed to get download link"); showProgress('Importing into Premiere Pro...', 85);
return;
}
// Download the file
showProgress(`Downloading ${asset.filename}...`, 30);
const filePath = await downloadFile(signedUrl, asset.filename);
if (!filePath) {
showErrorMessage("Failed to download file");
return;
}
// Import into Premiere
showProgress("Importing into Premiere Pro...", 70);
await importFileToPremiereProject(filePath); await importFileToPremiereProject(filePath);
hideProgress(); hideProgress();
showSuccessMessage(`Imported: ${asset.filename}`); showSuccessMessage('Imported: ' + safeName);
} catch (error) { } catch (error) {
console.error("Import error:", error); console.error('Import error:', error);
showErrorMessage(`Import failed: ${error.message}`); hideProgress();
showErrorMessage('Import failed: ' + error.message);
} finally { } finally {
elements.importBtn.disabled = state.selectedAsset === null; elements.importBtn.disabled = !state.selectedAsset;
} }
} }
async function downloadFile(url, filename) { /**
return new Promise((resolve, reject) => { * Downloads a remote URL to a local temp file using Node.js http/https.
// In a real implementation, use Node.js fs module or fetch API *
// For now, we'll use fetch with progress tracking * Node.js is available via require() in CEP when the manifest contains:
* <CEFCommandLine><Parameter>--enable-nodejs</Parameter></CEFCommandLine>
*
* @param {string} url - Presigned download URL (http or https)
* @param {string} filename - Desired local filename (sanitized)
* @returns {Promise<string>} Resolved with the absolute path to the saved file
*/
function downloadFile(url, filename) {
return new Promise(function (resolve, reject) {
try {
var https = require('https');
var http = require('http');
var fs = require('fs');
var path = require('path');
var os = require('os');
fetch(url) var tempPath = path.join(os.tmpdir(), filename);
.then((response) => { var file = fs.createWriteStream(tempPath);
if (!response.ok) throw new Error(`HTTP ${response.status}`); var protocol = url.startsWith('https') ? https : http;
const contentLength = response.headers.get("content-length"); protocol.get(url, function (res) {
const reader = response.body.getReader(); if (res.statusCode !== 200) {
let receivedLength = 0; file.close();
fs.unlink(tempPath, function () {});
const chunks = []; reject(new Error('Download HTTP ' + res.statusCode));
function read() {
reader.read().then(({ done, value }) => {
if (done) {
// Convert chunks to blob
const blob = new Blob(chunks);
// Save file (would need Node.js in production)
// For CEP, you'd use special file I/O
const tempPath = `${getTempPath()}/${filename}`;
// Update progress
state.downloadProgress = 100;
updateProgressUI();
resolve(tempPath);
return; return;
} }
chunks.push(value); var total = parseInt(res.headers['content-length'] || '0', 10);
receivedLength += value.length; var received = 0;
if (contentLength) { res.on('data', function (chunk) {
state.downloadProgress = 30 + (receivedLength / contentLength) * 40; received += chunk.length;
if (total > 0) {
state.downloadProgress = 10 + (received / total) * 75;
updateProgressUI(); updateProgressUI();
} }
read();
}); });
}
read(); res.pipe(file);
})
.catch(reject); file.on('finish', function () {
file.close(function () { resolve(tempPath); });
});
file.on('error', function (err) {
fs.unlink(tempPath, function () {});
reject(err);
});
}).on('error', function (err) {
fs.unlink(tempPath, function () {});
reject(err);
});
} catch (err) {
reject(new Error('Node.js unavailable for download: ' + err.message));
}
}); });
} }
function getTempPath() { /**
if (navigator.platform.includes("Win")) { * Uses csInterface.evalScript to call the Premiere Pro ExtendScript layer
return process.env.TEMP || "C:\\Users\\%USERNAME%\\AppData\\Local\\Temp"; * and import the given local file into the active project.
} else { *
return "/tmp"; * @param {string} filePath - Absolute local path to the downloaded proxy file
} * @returns {Promise<object>}
} */
function importFileToPremiereProject(filePath) {
return new Promise(function (resolve, reject) {
// Escape backslashes for Windows paths inside the script string literal
var safePath = filePath.replace(/\\/g, '\\\\');
async function importFileToPremiereProject(filePath) { var script = [
return new Promise((resolve, reject) => { '(function() {',
const script = ` ' var result = { success: false, message: "" };',
var result = {}; ' try {',
try { ' if (!app.project) {',
if (app.project) { ' result.message = "No active Premiere Pro project";',
app.project.importFiles(["${filePath}"]); ' return JSON.stringify(result);',
result.success = true; ' }',
result.message = "File imported successfully"; ' var f = new File("' + safePath + '");',
} else { ' if (!f.exists) {',
result.success = false; ' result.message = "File not found: ' + safePath + '";',
result.message = "No active project"; ' return JSON.stringify(result);',
} ' }',
} catch (error) { ' app.project.importFiles(["' + safePath + '"]);',
result.success = false; ' result.success = true;',
result.message = error.message; ' result.message = "Imported successfully";',
} ' } catch (e) {',
JSON.stringify(result); ' result.message = e.message;',
`; ' }',
' return JSON.stringify(result);',
'})();',
].join('\n');
csInterface.evalScript(script, (result) => { csInterface.evalScript(script, function (resultStr) {
try { try {
const parsed = JSON.parse(result); var parsed = JSON.parse(resultStr);
if (parsed.success) { if (parsed.success) resolve(parsed);
resolve(parsed); else reject(new Error(parsed.message));
} else { } catch (e) {
reject(new Error(parsed.message)); reject(new Error('ExtendScript error: ' + resultStr));
}
} catch (error) {
reject(error);
} }
}); });
}); });
@ -578,70 +568,55 @@ async function importFileToPremiereProject(filePath) {
// ============================================================================ // ============================================================================
function handleAssetClick(e) { function handleAssetClick(e) {
const card = e.target.closest(".asset-card"); var card = e.target.closest('.asset-card');
if (!card) return; if (!card) return;
// Clear previous selection document.querySelectorAll('.asset-card.selected').forEach(function (el) {
document.querySelectorAll(".asset-card.selected").forEach((el) => { el.classList.remove('selected');
el.classList.remove("selected");
}); });
card.classList.add('selected');
// Select new card var assetId = card.dataset.assetId;
card.classList.add("selected"); var asset = state.assets.find(function (a) { return a.id === assetId; });
if (asset) showAssetDetails(asset);
// Show details
const assetId = card.dataset.assetId;
const asset = state.assets.find((a) => a.id === assetId);
if (asset) {
showAssetDetails(asset);
}
} }
function showProgress(label, percent) { function showProgress(label, percent) {
elements.progressContainer.classList.add("visible"); elements.progressContainer.classList.add('visible');
elements.progressLabel.textContent = label; elements.progressLabel.textContent = label;
state.downloadProgress = percent; state.downloadProgress = percent;
updateProgressUI(); updateProgressUI();
} }
function hideProgress() { function hideProgress() {
elements.progressContainer.classList.remove("visible"); elements.progressContainer.classList.remove('visible');
state.downloadProgress = 0; state.downloadProgress = 0;
updateProgressUI();
} }
function updateProgressUI() { function updateProgressUI() {
elements.progressFill.style.width = `${state.downloadProgress}%`; elements.progressFill.style.width = state.downloadProgress + '%';
} }
function showErrorMessage(message) { function showErrorMessage(message) {
const container = document.createElement("div"); _showFlash(message, 'error-message');
container.className = "error-message";
container.textContent = message;
const searchArea = document.querySelector(".search-filter-area");
searchArea.insertBefore(container, searchArea.firstChild);
setTimeout(() => {
container.remove();
}, 5000);
} }
function showSuccessMessage(message) { function showSuccessMessage(message) {
const container = document.createElement("div"); _showFlash(message, 'success-message');
container.className = "success-message"; }
container.textContent = message;
const searchArea = document.querySelector(".search-filter-area"); function _showFlash(message, className) {
searchArea.insertBefore(container, searchArea.firstChild); var el = document.createElement('div');
el.className = className;
setTimeout(() => { el.textContent = message;
container.remove(); var anchor = document.querySelector('.search-filter-area');
}, 5000); anchor.insertBefore(el, anchor.firstChild);
setTimeout(function () { el.remove(); }, 5000);
} }
function logMessage(message) { function logMessage(message) {
console.log(`[MAM Panel] ${message}`); console.log('[MAM Panel] ' + message);
} }
// ============================================================================ // ============================================================================
@ -649,34 +624,45 @@ function logMessage(message) {
// ============================================================================ // ============================================================================
function debounce(func, delay) { function debounce(func, delay) {
let timeout; var timeout;
return function executedFunction(...args) { return function () {
const later = () => { var args = arguments;
clearTimeout(timeout); clearTimeout(timeout);
func(...args); timeout = setTimeout(function () { func.apply(null, args); }, delay);
};
clearTimeout(timeout);
timeout = setTimeout(later, delay);
}; };
} }
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* Strips characters illegal in file names on Windows/macOS.
*/
function sanitizeFilename(name) {
return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
}
function formatDuration(seconds) { function formatDuration(seconds) {
if (!seconds) return "N/A"; if (!seconds) return 'N/A';
const hours = Math.floor(seconds / 3600); var h = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); var m = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60); var s = Math.floor(seconds % 60);
if (h > 0) {
if (hours > 0) { return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
} else {
return `${minutes}:${String(secs).padStart(2, "0")}`;
} }
return m + ':' + String(s).padStart(2, '0');
} }
function formatFileSize(bytes) { function formatFileSize(bytes) {
if (!bytes) return "0 B"; if (!bytes) return '0 B';
const k = 1024; var k = 1024;
const sizes = ["B", "KB", "MB", "GB"]; var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
} }