diff --git a/services/premiere-plugin/js/main.js b/services/premiere-plugin/js/main.js
index 2b57f08..e198bae 100644
--- a/services/premiere-plugin/js/main.js
+++ b/services/premiere-plugin/js/main.js
@@ -3,25 +3,28 @@
* Main JavaScript file for the CEP panel
*/
+// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls
+const csInterface = new CSInterface();
+
// ============================================================================
// State Management
// ============================================================================
const state = {
- serverUrl: localStorage.getItem("mam_server_url") || "http://localhost:7434",
- isConnected: false,
- isConnecting: false,
- selectedAsset: null,
- assets: [],
- filteredAssets: [],
- projects: [],
- selectedProject: "all",
- searchQuery: "",
- currentPage: 0,
- pageSize: 50,
- totalAssets: 0,
+ serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434',
+ isConnected: false,
+ isConnecting: false,
+ selectedAsset: null,
+ assets: [],
+ projects: [],
+ selectedProject: 'all',
+ searchQuery: '',
+ currentPage: 0,
+ pageSize: 50,
+ totalAssets: 0,
downloadProgress: 0,
- isDownloading: false
+ isDownloading: false,
+ thumbCache: {}, // assetId -> signed URL
};
// ============================================================================
@@ -32,70 +35,89 @@ let elements = {};
function initDOMElements() {
elements = {
- // Connection bar
- serverUrlInput: document.getElementById("server-url"),
- connectBtn: document.getElementById("connect-btn"),
- statusIndicator: document.getElementById("status-indicator"),
-
- // Search and filter
- searchInput: document.getElementById("search-input"),
- projectFilter: document.getElementById("project-filter"),
-
- // Asset grid
- assetGrid: document.getElementById("asset-grid"),
- emptyState: document.getElementById("empty-state"),
-
- // Details panel
- detailsPanel: document.getElementById("details-panel"),
- detailsFilename: document.getElementById("details-filename"),
- detailsCodec: document.getElementById("details-codec"),
- detailsResolution: document.getElementById("details-resolution"),
- detailsFps: document.getElementById("details-fps"),
- detailsDuration: document.getElementById("details-duration"),
- 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")
+ serverUrlInput: document.getElementById('server-url'),
+ connectBtn: document.getElementById('connect-btn'),
+ statusIndicator: document.getElementById('status-indicator'),
+ searchInput: document.getElementById('search-input'),
+ projectFilter: document.getElementById('project-filter'),
+ assetGrid: document.getElementById('asset-grid'),
+ emptyState: document.getElementById('empty-state'),
+ detailsPanel: document.getElementById('details-panel'),
+ detailsFilename: document.getElementById('details-filename'),
+ detailsCodec: document.getElementById('details-codec'),
+ detailsResolution: document.getElementById('details-resolution'),
+ detailsFps: document.getElementById('details-fps'),
+ detailsDuration: document.getElementById('details-duration'),
+ detailsSize: document.getElementById('details-size'),
+ detailsTags: document.getElementById('details-tags'),
+ importBtn: document.getElementById('import-btn'),
+ importAllBtn: document.getElementById('import-all-btn'),
+ 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
// ============================================================================
-document.addEventListener("DOMContentLoaded", () => {
+document.addEventListener('DOMContentLoaded', () => {
initDOMElements();
setupEventListeners();
restoreSettings();
- logMessage("Wild Dragon MAM panel initialized");
+ logMessage('Wild Dragon MAM panel initialized');
});
function setupEventListeners() {
- // Connection controls
- elements.serverUrlInput.addEventListener("change", (e) => {
- state.serverUrl = e.target.value;
- localStorage.setItem("mam_server_url", state.serverUrl);
+ elements.serverUrlInput.addEventListener('change', (e) => {
+ state.serverUrl = e.target.value.trim().replace(/\/$/, '');
+ localStorage.setItem('mam_server_url', state.serverUrl);
+ state.thumbCache = {}; // bust cache when server changes
});
- elements.connectBtn.addEventListener("click", connectToServer);
-
- // Search and filter
- elements.searchInput.addEventListener("input", debounce(handleSearch, 300));
- elements.projectFilter.addEventListener("change", handleProjectFilter);
-
- // Asset grid events are delegated
- elements.assetGrid.addEventListener("click", handleAssetClick);
-
- // Import buttons
- elements.importBtn.addEventListener("click", importSelectedAsset);
- elements.importAllBtn.addEventListener("click", importAllAssets);
+ elements.connectBtn.addEventListener('click', connectToServer);
+ elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
+ elements.projectFilter.addEventListener('change', handleProjectFilter);
+ elements.assetGrid.addEventListener('click', handleAssetClick);
+ elements.importBtn.addEventListener('click', importSelectedAsset);
+ elements.importAllBtn.addEventListener('click', importAllAssets);
}
function restoreSettings() {
@@ -110,34 +132,34 @@ async function connectToServer() {
if (state.isConnecting) return;
state.isConnecting = true;
- updateConnectionStatus("connecting");
+ updateConnectionStatus('connecting');
elements.connectBtn.disabled = true;
try {
- const response = await fetch(`${state.serverUrl}/api/health`, {
- method: "GET",
- headers: {
- "Accept": "application/json"
- }
+ // Use the projects endpoint as a health check (no separate /health route exists)
+ const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
+ method: 'GET',
+ headers: { Accept: 'application/json' },
});
if (response.ok) {
state.isConnected = true;
- updateConnectionStatus("connected");
- elements.connectBtn.textContent = "Reconnect";
- logMessage("Connected to Wild Dragon MAM");
+ updateConnectionStatus('connected');
+ elements.connectBtn.textContent = 'Reconnect';
+ logMessage('Connected to Wild Dragon MAM');
- // Fetch initial data
- await fetchProjects();
+ // Pass the already-fetched JSON to avoid a second round-trip
+ const projectData = await response.json();
+ await fetchProjects(projectData);
await fetchAssets();
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
- console.error("Connection error:", error);
+ console.error('Connection error:', error);
state.isConnected = false;
- updateConnectionStatus("disconnected");
- elements.connectBtn.textContent = "Connect";
+ updateConnectionStatus('disconnected');
+ elements.connectBtn.textContent = 'Connect';
showErrorMessage(`Failed to connect: ${error.message}`);
} finally {
state.isConnecting = false;
@@ -147,137 +169,110 @@ async function connectToServer() {
function updateConnectionStatus(status) {
const indicator = elements.statusIndicator;
-
- indicator.classList.remove("connected", "connecting");
-
- if (status === "connected") {
- indicator.classList.add("connected");
- } else if (status === "connecting") {
- indicator.classList.add("connecting");
- }
+ indicator.classList.remove('connected', 'connecting');
+ if (status === 'connected') indicator.classList.add('connected');
+ else if (status === 'connecting') indicator.classList.add('connecting');
}
// ============================================================================
// 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 {
- const response = await fetch(`${state.serverUrl}/api/projects`, {
- headers: {
- "Accept": "application/json"
- }
- });
+ let projects;
+ if (preloadedData) {
+ // 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}`);
+ const data = await response.json();
+ projects = Array.isArray(data) ? data : [];
+ }
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ state.projects = projects;
- const data = await response.json();
- state.projects = data.projects || [];
-
- // Update project filter
- const oldSelectedProject = state.selectedProject;
+ const savedProject = state.selectedProject;
elements.projectFilter.innerHTML = '';
-
- state.projects.forEach((project) => {
- const option = document.createElement("option");
- option.value = project.id;
- option.textContent = project.name;
- elements.projectFilter.appendChild(option);
+ state.projects.forEach((p) => {
+ const opt = document.createElement('option');
+ opt.value = p.id;
+ opt.textContent = p.name;
+ elements.projectFilter.appendChild(opt);
});
+ elements.projectFilter.value = savedProject;
- elements.projectFilter.value = oldSelectedProject;
logMessage(`Loaded ${state.projects.length} projects`);
} catch (error) {
- console.error("Error fetching projects:", error);
- showErrorMessage("Failed to fetch projects");
+ console.error('Error fetching projects:', error);
}
}
async function fetchAssets(page = 0) {
- if (!state.isConnected) {
- showErrorMessage("Not connected to server");
- return;
- }
+ if (!state.isConnected) return;
try {
const params = new URLSearchParams({
offset: page * state.pageSize,
- limit: state.pageSize
+ limit: state.pageSize,
});
- if (state.searchQuery) {
- params.append("search", state.searchQuery);
- }
-
- if (state.selectedProject !== "all") {
- params.append("project_id", state.selectedProject);
- }
+ if (state.searchQuery) params.append('search', state.searchQuery);
+ if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);
const response = await fetch(
- `${state.serverUrl}/api/assets?${params.toString()}`,
- {
- headers: {
- "Accept": "application/json"
- }
- }
+ `${state.serverUrl}/api/v1/assets?${params.toString()}`,
+ { headers: { Accept: 'application/json' } }
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
- state.assets = data.assets || [];
- state.totalAssets = data.total || 0;
+ state.assets = data.assets || [];
+ state.totalAssets = data.total || 0;
state.currentPage = page;
renderAssets();
logMessage(`Loaded ${state.assets.length} assets`);
} catch (error) {
- console.error("Error fetching assets:", error);
- showErrorMessage("Failed to fetch assets");
+ console.error('Error fetching assets:', error);
+ showErrorMessage('Failed to fetch assets');
}
}
async function fetchAssetDetails(assetId) {
if (!state.isConnected) return null;
-
try {
- const response = await fetch(`${state.serverUrl}/api/assets/${assetId}`, {
- headers: {
- "Accept": "application/json"
- }
+ const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
+ headers: { Accept: 'application/json' },
});
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
- console.error("Error fetching asset details:", error);
+ console.error('Error fetching asset details:', error);
return null;
}
}
-async function getSignedUrl(assetId, proxyId = null) {
- if (!state.isConnected) return null;
-
- try {
- const endpoint = proxyId
- ? `${state.serverUrl}/api/assets/${assetId}/proxies/${proxyId}/download`
- : `${state.serverUrl}/api/assets/${assetId}/download`;
-
- const response = await fetch(endpoint, {
- method: "POST",
- headers: {
- "Accept": "application/json"
- }
- });
-
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
- const data = await response.json();
- return data.signed_url;
- } catch (error) {
- console.error("Error getting signed URL:", error);
- return null;
- }
+/**
+ * Returns a short-lived presigned URL for the H.264 proxy of the given asset.
+ * GET /api/v1/assets/:id/stream -> { url: '...' }
+ */
+async function getSignedDownloadUrl(assetId) {
+ const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
+ headers: { Accept: 'application/json' },
+ });
+ if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
+ const { url } = await response.json();
+ if (!url) throw new Error('Stream endpoint returned no URL');
+ return url;
}
// ============================================================================
@@ -285,112 +280,108 @@ async function getSignedUrl(assetId, proxyId = null) {
// ============================================================================
function renderAssets() {
- elements.assetGrid.innerHTML = "";
+ elements.assetGrid.innerHTML = '';
if (state.assets.length === 0) {
- elements.emptyState.style.display = "flex";
+ elements.emptyState.style.display = 'flex';
return;
}
- elements.emptyState.style.display = "none";
-
+ elements.emptyState.style.display = 'none';
state.assets.forEach((asset) => {
- const card = createAssetCard(asset);
- elements.assetGrid.appendChild(card);
+ elements.assetGrid.appendChild(createAssetCard(asset));
});
}
function createAssetCard(asset) {
- const card = document.createElement("div");
- card.className = "asset-card";
- if (state.selectedAsset?.id === asset.id) {
- card.classList.add("selected");
+ const card = document.createElement('div');
+ card.className = 'asset-card';
+ if (state.selectedAsset && state.selectedAsset.id === asset.id) {
+ card.classList.add('selected');
}
card.dataset.assetId = asset.id;
- // Thumbnail
- const thumbnail = document.createElement("div");
- thumbnail.className = "asset-thumbnail";
+ // Thumbnail — lazy loaded by IntersectionObserver
+ const thumbnail = document.createElement('div');
+ thumbnail.className = 'asset-thumbnail';
- if (asset.thumbnail_url) {
- const img = document.createElement("img");
- img.src = asset.thumbnail_url;
- img.alt = asset.filename;
- img.loading = "lazy";
- img.onerror = () => {
- img.style.display = "none";
- thumbnail.textContent = "No preview";
- };
- thumbnail.appendChild(img);
- } else {
- thumbnail.textContent = "No preview";
- }
+ const img = document.createElement('img');
+ img.dataset.assetId = asset.id;
+ img.alt = escapeHtml(asset.display_name || asset.filename || '');
+ img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
+ img.onerror = () => { img.style.display = 'none'; };
+ thumbnail.appendChild(img);
+ thumbObserver.observe(img);
card.appendChild(thumbnail);
- // Info
- const info = document.createElement("div");
- info.className = "asset-info";
+ // Info row
+ const info = document.createElement('div');
+ info.className = 'asset-info';
- const filename = document.createElement("div");
- filename.className = "asset-filename";
- filename.title = asset.filename;
- filename.textContent = asset.filename;
- info.appendChild(filename);
+ const name = asset.display_name || asset.filename || 'Untitled';
+ const filenameEl = document.createElement('div');
+ filenameEl.className = 'asset-filename';
+ filenameEl.title = name;
+ filenameEl.textContent = name;
+ info.appendChild(filenameEl);
- const meta = document.createElement("div");
- meta.className = "asset-meta";
- meta.innerHTML = `
- ${asset.duration ? formatDuration(asset.duration) : "N/A"}
- ${asset.codec || "N/A"}
- `;
+ const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
+ const codec = (asset.metadata && asset.metadata.codec) || asset.media_type || 'video';
+ const meta = document.createElement('div');
+ meta.className = 'asset-meta';
+ meta.innerHTML = [
+ '' + (durationSec ? formatDuration(durationSec) : 'N/A') + '',
+ '' + escapeHtml(codec.toUpperCase()) + '',
+ ].join('');
info.appendChild(meta);
- const status = document.createElement("div");
- status.className = `asset-status-badge status-badge ${asset.status || "ready"}`;
- status.textContent = (asset.status || "ready").toUpperCase();
- info.appendChild(status);
+ const statusStr = asset.status || 'ready';
+ const statusBadge = document.createElement('div');
+ statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
+ statusBadge.textContent = statusStr.toUpperCase();
+ info.appendChild(statusBadge);
card.appendChild(info);
-
return card;
}
function showAssetDetails(asset) {
state.selectedAsset = asset;
+ elements.detailsPanel.classList.remove('hidden');
- elements.detailsPanel.classList.remove("hidden");
- elements.detailsFilename.textContent = asset.filename;
- elements.detailsCodec.textContent = asset.codec || "Unknown";
- elements.detailsResolution.textContent = asset.resolution || "N/A";
- elements.detailsFps.textContent = asset.fps || "N/A";
- elements.detailsDuration.textContent = asset.duration
- ? formatDuration(asset.duration)
- : "N/A";
- elements.detailsSize.textContent = asset.file_size
- ? formatFileSize(asset.file_size)
- : "N/A";
+ const meta = asset.metadata || {};
+ elements.detailsFilename.textContent = asset.display_name || asset.filename;
+ elements.detailsCodec.textContent = meta.codec || asset.media_type || 'Unknown';
+ elements.detailsResolution.textContent = meta.resolution || 'N/A';
+ elements.detailsFps.textContent = meta.fps ? meta.fps + ' fps' : 'N/A';
+ elements.detailsDuration.textContent = asset.duration_ms
+ ? formatDuration(asset.duration_ms / 1000)
+ : 'N/A';
+ elements.detailsSize.textContent = meta.file_size
+ ? formatFileSize(meta.file_size)
+ : 'N/A';
- // Render tags
- elements.detailsTags.innerHTML = "";
- if (asset.tags && asset.tags.length > 0) {
- asset.tags.forEach((tag) => {
- const tagEl = document.createElement("span");
- tagEl.className = "tag";
- tagEl.textContent = tag;
- elements.detailsTags.appendChild(tagEl);
+ elements.detailsTags.innerHTML = '';
+ const tags = asset.tags || [];
+ if (tags.length > 0) {
+ tags.forEach((tag) => {
+ const el = document.createElement('span');
+ el.className = 'tag';
+ el.textContent = tag;
+ elements.detailsTags.appendChild(el);
});
} else {
- elements.detailsTags.innerHTML = 'No tags';
+ elements.detailsTags.innerHTML =
+ 'No tags';
}
- // Update import button state
elements.importBtn.disabled = false;
}
function hideAssetDetails() {
state.selectedAsset = null;
- elements.detailsPanel.classList.add("hidden");
+ elements.detailsPanel.classList.add('hidden');
elements.importBtn.disabled = true;
}
@@ -416,158 +407,157 @@ function handleProjectFilter(e) {
async function importSelectedAsset() {
if (!state.selectedAsset) {
- showErrorMessage("No asset selected");
+ showErrorMessage('No asset selected');
return;
}
-
await importAsset(state.selectedAsset);
}
async function importAllAssets() {
if (state.assets.length === 0) {
- showErrorMessage("No assets to import");
+ showErrorMessage('No assets to import');
return;
}
-
for (const asset of state.assets) {
await importAsset(asset);
}
-
- showSuccessMessage("All assets imported");
+ showSuccessMessage('All assets imported');
}
async function importAsset(asset) {
try {
elements.importBtn.disabled = true;
- // Get the proxy file (or original if no proxy)
- const proxyId = asset.proxy_file_id || asset.file_id;
- if (!proxyId) {
- showErrorMessage("No file available for import");
- return;
- }
+ // Step 1: get a presigned proxy URL from the MAM API
+ showProgress('Getting download link...', 5);
+ const url = await getSignedDownloadUrl(asset.id);
- // Get signed URL for download
- showProgress("Getting download link...", 0);
- const signedUrl = await getSignedUrl(asset.id, proxyId);
+ // Step 2: download the proxy (H.264) to the OS temp directory
+ const safeName = sanitizeFilename(
+ (asset.display_name || asset.filename || asset.id) + '.mp4'
+ );
+ showProgress('Downloading ' + safeName + '...', 10);
+ const filePath = await downloadFile(url, safeName);
- if (!signedUrl) {
- showErrorMessage("Failed to get download link");
- 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);
+ // Step 3: hand the local path to Premiere Pro via ExtendScript
+ showProgress('Importing into Premiere Pro...', 85);
await importFileToPremiereProject(filePath);
hideProgress();
- showSuccessMessage(`Imported: ${asset.filename}`);
+ showSuccessMessage('Imported: ' + safeName);
} catch (error) {
- console.error("Import error:", error);
- showErrorMessage(`Import failed: ${error.message}`);
+ console.error('Import error:', error);
+ hideProgress();
+ showErrorMessage('Import failed: ' + error.message);
} finally {
- elements.importBtn.disabled = state.selectedAsset === null;
+ elements.importBtn.disabled = !state.selectedAsset;
}
}
-async function downloadFile(url, filename) {
- return new Promise((resolve, reject) => {
- // In a real implementation, use Node.js fs module or fetch API
- // For now, we'll use fetch with progress tracking
+/**
+ * Downloads a remote URL to a local temp file using Node.js http/https.
+ *
+ * Node.js is available via require() in CEP when the manifest contains:
+ * --enable-nodejs
+ *
+ * @param {string} url - Presigned download URL (http or https)
+ * @param {string} filename - Desired local filename (sanitized)
+ * @returns {Promise} 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)
- .then((response) => {
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ var tempPath = path.join(os.tmpdir(), filename);
+ var file = fs.createWriteStream(tempPath);
+ var protocol = url.startsWith('https') ? https : http;
- const contentLength = response.headers.get("content-length");
- const reader = response.body.getReader();
- let receivedLength = 0;
-
- const chunks = [];
-
- 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;
- }
-
- chunks.push(value);
- receivedLength += value.length;
-
- if (contentLength) {
- state.downloadProgress = 30 + (receivedLength / contentLength) * 40;
- updateProgressUI();
- }
-
- read();
- });
+ protocol.get(url, function (res) {
+ if (res.statusCode !== 200) {
+ file.close();
+ fs.unlink(tempPath, function () {});
+ reject(new Error('Download HTTP ' + res.statusCode));
+ return;
}
- read();
- })
- .catch(reject);
+ var total = parseInt(res.headers['content-length'] || '0', 10);
+ var received = 0;
+
+ res.on('data', function (chunk) {
+ received += chunk.length;
+ if (total > 0) {
+ state.downloadProgress = 10 + (received / total) * 75;
+ updateProgressUI();
+ }
+ });
+
+ res.pipe(file);
+
+ 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")) {
- return process.env.TEMP || "C:\\Users\\%USERNAME%\\AppData\\Local\\Temp";
- } else {
- return "/tmp";
- }
-}
+/**
+ * Uses csInterface.evalScript to call the Premiere Pro ExtendScript layer
+ * and import the given local file into the active project.
+ *
+ * @param {string} filePath - Absolute local path to the downloaded proxy file
+ * @returns {Promise