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} + * Calls csInterface.evalScript to import a local file into the open Premiere project. */ function importFileToPremiereProject(filePath) { return new Promise(function (resolve, reject) { - // Escape backslashes for Windows paths inside the script string literal var safePath = filePath.replace(/\\/g, '\\\\'); var script = [ @@ -577,7 +560,7 @@ function handleAssetClick(e) { card.classList.add('selected'); var assetId = card.dataset.assetId; - var asset = state.assets.find(function (a) { return a.id === assetId; }); + var asset = state.assets.find(function (a) { return a.id === assetId; }); if (asset) showAssetDetails(asset); } @@ -607,10 +590,10 @@ function showSuccessMessage(message) { } function _showFlash(message, className) { - var el = document.createElement('div'); - el.className = className; + var el = document.createElement('div'); + el.className = className; el.textContent = message; - var anchor = document.querySelector('.search-filter-area'); + var anchor = document.querySelector('.search-filter-area'); anchor.insertBefore(el, anchor.firstChild); setTimeout(function () { el.remove(); }, 5000); } @@ -641,9 +624,6 @@ function escapeHtml(str) { .replace(/'/g, '''); } -/** - * Strips characters illegal in file names on Windows/macOS. - */ function sanitizeFilename(name) { return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_'); } @@ -653,9 +633,7 @@ function formatDuration(seconds) { var h = Math.floor(seconds / 3600); var m = Math.floor((seconds % 3600) / 60); var s = Math.floor(seconds % 60); - if (h > 0) { - return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); - } + if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); return m + ':' + String(s).padStart(2, '0'); }