fix(premiere-plugin): CSInterface init, correct API prefix, Node.js download, lazy thumbnails, proper ExtendScript export API: main.js
This commit is contained in:
parent
c162104b7c
commit
a239e30ef2
1 changed files with 382 additions and 396 deletions
|
|
@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue