diff --git a/services/premiere-plugin/js/main.js b/services/premiere-plugin/js/main.js
index e198bae..421aaef 100644
--- a/services/premiere-plugin/js/main.js
+++ b/services/premiere-plugin/js/main.js
@@ -11,20 +11,20 @@ const csInterface = new CSInterface();
// ============================================================================
const state = {
- 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,
+ 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,
- thumbCache: {}, // assetId -> signed URL
+ isDownloading: false,
+ thumbCache: {}, // assetId -> signed URL
};
// ============================================================================
@@ -65,7 +65,7 @@ function initDOMElements() {
const thumbObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
- const img = entry.target;
+ const img = entry.target;
const assetId = img.dataset.assetId;
if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
thumbObserver.unobserve(img);
@@ -90,7 +90,7 @@ async function loadThumbnail(img, assetId) {
img.src = url;
img.dataset.loaded = '1';
} catch (_) {
- // thumbnail unavailable — leave placeholder visible
+ // thumbnail unavailable — leave placeholder
}
}
@@ -109,9 +109,8 @@ function setupEventListeners() {
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
+ state.thumbCache = {};
});
-
elements.connectBtn.addEventListener('click', connectToServer);
elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
elements.projectFilter.addEventListener('change', handleProjectFilter);
@@ -136,10 +135,11 @@ async function connectToServer() {
elements.connectBtn.disabled = true;
try {
- // Use the projects endpoint as a health check (no separate /health route exists)
+ // Use the projects endpoint as a connectivity check
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
method: 'GET',
headers: { Accept: 'application/json' },
+ credentials: 'include',
});
if (response.ok) {
@@ -148,7 +148,6 @@ async function connectToServer() {
elements.connectBtn.textContent = 'Reconnect';
logMessage('Connected to Wild Dragon MAM');
- // Pass the already-fetched JSON to avoid a second round-trip
const projectData = await response.json();
await fetchProjects(projectData);
await fetchAssets();
@@ -170,7 +169,7 @@ async function connectToServer() {
function updateConnectionStatus(status) {
const indicator = elements.statusIndicator;
indicator.classList.remove('connected', 'connecting');
- if (status === 'connected') indicator.classList.add('connected');
+ if (status === 'connected') indicator.classList.add('connected');
else if (status === 'connecting') indicator.classList.add('connecting');
}
@@ -178,19 +177,15 @@ function updateConnectionStatus(status) {
// API Calls
// ============================================================================
-/**
- * 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 {
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' },
+ credentials: 'include',
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
@@ -203,7 +198,7 @@ async function fetchProjects(preloadedData) {
elements.projectFilter.innerHTML = '';
state.projects.forEach((p) => {
const opt = document.createElement('option');
- opt.value = p.id;
+ opt.value = p.id;
opt.textContent = p.name;
elements.projectFilter.appendChild(opt);
});
@@ -217,24 +212,21 @@ async function fetchProjects(preloadedData) {
async function fetchAssets(page = 0) {
if (!state.isConnected) return;
-
try {
const params = new URLSearchParams({
offset: page * state.pageSize,
limit: state.pageSize,
});
-
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/v1/assets?${params.toString()}`,
- { headers: { Accept: 'application/json' } }
+ { headers: { Accept: 'application/json' }, credentials: 'include' }
);
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const data = await response.json();
+ const data = await response.json();
state.assets = data.assets || [];
state.totalAssets = data.total || 0;
state.currentPage = page;
@@ -252,6 +244,7 @@ async function fetchAssetDetails(assetId) {
try {
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
headers: { Accept: 'application/json' },
+ credentials: 'include',
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
@@ -263,11 +256,12 @@ async function fetchAssetDetails(assetId) {
/**
* Returns a short-lived presigned URL for the H.264 proxy of the given asset.
- * GET /api/v1/assets/:id/stream -> { url: '...' }
+ * 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' },
+ credentials: 'include',
});
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
const { url } = await response.json();
@@ -301,44 +295,44 @@ function createAssetCard(asset) {
}
card.dataset.assetId = asset.id;
- // Thumbnail — lazy loaded by IntersectionObserver
+ // Thumbnail — lazy loaded via IntersectionObserver
const thumbnail = document.createElement('div');
thumbnail.className = 'asset-thumbnail';
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'; };
+ 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 row
const info = document.createElement('div');
info.className = 'asset-info';
- const name = asset.display_name || asset.filename || 'Untitled';
+ const name = asset.display_name || asset.filename || 'Untitled';
const filenameEl = document.createElement('div');
- filenameEl.className = 'asset-filename';
- filenameEl.title = name;
+ filenameEl.className = 'asset-filename';
+ filenameEl.title = name;
filenameEl.textContent = name;
info.appendChild(filenameEl);
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 = [
+ // codec and media_type are top-level columns on the asset record
+ const codec = asset.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 statusStr = asset.status || 'ready';
+ const statusStr = asset.status || 'ready';
const statusBadge = document.createElement('div');
- statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
+ statusBadge.className = 'asset-status-badge status-badge ' + statusStr;
statusBadge.textContent = statusStr.toUpperCase();
info.appendChild(statusBadge);
@@ -350,24 +344,24 @@ function showAssetDetails(asset) {
state.selectedAsset = asset;
elements.detailsPanel.classList.remove('hidden');
- const meta = asset.metadata || {};
+ // All of these are top-level columns in the assets table
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.detailsCodec.textContent = asset.codec || asset.media_type || 'Unknown';
+ elements.detailsResolution.textContent = asset.resolution || 'N/A';
+ elements.detailsFps.textContent = asset.fps ? asset.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)
+ elements.detailsSize.textContent = asset.file_size
+ ? formatFileSize(asset.file_size)
: 'N/A';
elements.detailsTags.innerHTML = '';
const tags = asset.tags || [];
if (tags.length > 0) {
tags.forEach((tag) => {
- const el = document.createElement('span');
- el.className = 'tag';
+ const el = document.createElement('span');
+ el.className = 'tag';
el.textContent = tag;
elements.detailsTags.appendChild(el);
});
@@ -397,7 +391,7 @@ function handleSearch(e) {
function handleProjectFilter(e) {
state.selectedProject = e.target.value;
- state.currentPage = 0;
+ state.currentPage = 0;
fetchAssets();
}
@@ -428,18 +422,18 @@ async function importAsset(asset) {
try {
elements.importBtn.disabled = true;
- // Step 1: get a presigned proxy URL from the MAM API
+ // 1. Get a presigned proxy URL
showProgress('Getting download link...', 5);
const url = await getSignedDownloadUrl(asset.id);
- // Step 2: download the proxy (H.264) to the OS temp directory
+ // 2. Download proxy to OS temp dir via Node.js
const safeName = sanitizeFilename(
(asset.display_name || asset.filename || asset.id) + '.mp4'
);
showProgress('Downloading ' + safeName + '...', 10);
const filePath = await downloadFile(url, safeName);
- // Step 3: hand the local path to Premiere Pro via ExtendScript
+ // 3. Hand the local path to Premiere Pro
showProgress('Importing into Premiere Pro...', 85);
await importFileToPremiereProject(filePath);
@@ -456,22 +450,16 @@ async function importAsset(asset) {
/**
* 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
+ * Requires --enable-nodejs in the CEP manifest.
*/
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');
+ var https = require('https');
+ var http = require('http');
+ var fs = require('fs');
+ var path = require('path');
+ var os = require('os');
var tempPath = path.join(os.tmpdir(), filename);
var file = fs.createWriteStream(tempPath);
@@ -517,15 +505,10 @@ function downloadFile(url, filename) {
}
/**
- * 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