feat(plugin): proxy URL fix, hi-res import, import path tracking, timeline export
- Fix: /stream returns relative URL — prepend serverUrl before Node.js download - Add: importAssetHires() calls /assets/:id/hires for original file - Add: saveImportMapping() stores tempPath→assetId in localStorage so timeline export can match Premiere clips back to MAM assets - Add: startExportTimeline() reads active sequence via exportTimelineData(), shows export panel with seq name + clip count - Add: confirmExportTimeline() resolves paths→assetIds, upserts sequence, PUT /sequences/:id/clips - Add: refreshCurrentSequenceInfo() shows active sequence name in info bar
This commit is contained in:
parent
5bb22c17c8
commit
16888d62e2
1 changed files with 425 additions and 66 deletions
|
|
@ -25,6 +25,11 @@ const state = {
|
||||||
downloadProgress: 0,
|
downloadProgress: 0,
|
||||||
isDownloading: false,
|
isDownloading: false,
|
||||||
thumbCache: {}, // assetId -> signed URL
|
thumbCache: {}, // assetId -> signed URL
|
||||||
|
// Maps tempFilePath → { assetId, displayName } for timeline export resolution.
|
||||||
|
// Also stores 'name:filename.mp4' → same as a fuzzy fallback.
|
||||||
|
importedAssets: JSON.parse(localStorage.getItem('mam_imported_assets') || '{}'),
|
||||||
|
exportPanelVisible: false,
|
||||||
|
currentSequenceName: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -51,10 +56,23 @@ function initDOMElements() {
|
||||||
detailsSize: document.getElementById('details-size'),
|
detailsSize: document.getElementById('details-size'),
|
||||||
detailsTags: document.getElementById('details-tags'),
|
detailsTags: document.getElementById('details-tags'),
|
||||||
importBtn: document.getElementById('import-btn'),
|
importBtn: document.getElementById('import-btn'),
|
||||||
|
importHiresBtn: document.getElementById('import-hires-btn'),
|
||||||
importAllBtn: document.getElementById('import-all-btn'),
|
importAllBtn: document.getElementById('import-all-btn'),
|
||||||
|
exportTimelineBtn: document.getElementById('export-timeline-btn'),
|
||||||
progressContainer: document.getElementById('progress-container'),
|
progressContainer: document.getElementById('progress-container'),
|
||||||
progressLabel: document.getElementById('progress-label'),
|
progressLabel: document.getElementById('progress-label'),
|
||||||
progressFill: document.getElementById('progress-fill'),
|
progressFill: document.getElementById('progress-fill'),
|
||||||
|
// Export panel
|
||||||
|
exportPanel: document.getElementById('export-panel'),
|
||||||
|
exportSeqName: document.getElementById('export-seq-name'),
|
||||||
|
exportProjSelect: document.getElementById('export-proj-select'),
|
||||||
|
exportClipInfo: document.getElementById('export-clip-info'),
|
||||||
|
exportConfirmBtn: document.getElementById('export-confirm-btn'),
|
||||||
|
exportCancelBtn: document.getElementById('export-cancel-btn'),
|
||||||
|
// Sequence info bar
|
||||||
|
seqInfoBar: document.getElementById('seq-info-bar'),
|
||||||
|
seqInfoName: document.getElementById('seq-info-name'),
|
||||||
|
seqRefreshBtn: document.getElementById('seq-refresh-btn'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +134,12 @@ function setupEventListeners() {
|
||||||
elements.projectFilter.addEventListener('change', handleProjectFilter);
|
elements.projectFilter.addEventListener('change', handleProjectFilter);
|
||||||
elements.assetGrid.addEventListener('click', handleAssetClick);
|
elements.assetGrid.addEventListener('click', handleAssetClick);
|
||||||
elements.importBtn.addEventListener('click', importSelectedAsset);
|
elements.importBtn.addEventListener('click', importSelectedAsset);
|
||||||
|
elements.importHiresBtn.addEventListener('click', importSelectedAssetHires);
|
||||||
elements.importAllBtn.addEventListener('click', importAllAssets);
|
elements.importAllBtn.addEventListener('click', importAllAssets);
|
||||||
|
elements.exportTimelineBtn.addEventListener('click', startExportTimeline);
|
||||||
|
elements.exportConfirmBtn.addEventListener('click', confirmExportTimeline);
|
||||||
|
elements.exportCancelBtn.addEventListener('click', cancelExportTimeline);
|
||||||
|
elements.seqRefreshBtn.addEventListener('click', refreshCurrentSequenceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreSettings() {
|
function restoreSettings() {
|
||||||
|
|
@ -135,7 +158,6 @@ async function connectToServer() {
|
||||||
elements.connectBtn.disabled = true;
|
elements.connectBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the projects endpoint as a connectivity check
|
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
|
|
@ -151,6 +173,7 @@ async function connectToServer() {
|
||||||
const projectData = await response.json();
|
const projectData = await response.json();
|
||||||
await fetchProjects(projectData);
|
await fetchProjects(projectData);
|
||||||
await fetchAssets();
|
await fetchAssets();
|
||||||
|
refreshCurrentSequenceInfo();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +227,9 @@ async function fetchProjects(preloadedData) {
|
||||||
});
|
});
|
||||||
elements.projectFilter.value = savedProject;
|
elements.projectFilter.value = savedProject;
|
||||||
|
|
||||||
|
// Also populate the export panel project selector
|
||||||
|
populateExportProjectSelect();
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -255,8 +281,9 @@ async function fetchAssetDetails(assetId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a short-lived presigned URL for the H.264 proxy of the given asset.
|
* Returns the download URL for the proxy. /stream returns a server-relative
|
||||||
* GET /api/v1/assets/:id/stream -> { url: '...' }
|
* path (/api/v1/assets/:id/video) designed for browser playback; we prepend
|
||||||
|
* serverUrl so Node.js http.get() receives a valid absolute URL.
|
||||||
*/
|
*/
|
||||||
async function getSignedDownloadUrl(assetId) {
|
async function getSignedDownloadUrl(assetId) {
|
||||||
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
|
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
|
||||||
|
|
@ -266,7 +293,22 @@ async function getSignedDownloadUrl(assetId) {
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
|
if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
|
||||||
const { url } = await response.json();
|
const { url } = await response.json();
|
||||||
if (!url) throw new Error('Stream endpoint returned no URL');
|
if (!url) throw new Error('Stream endpoint returned no URL');
|
||||||
return url;
|
return url.startsWith('/') ? state.serverUrl + url : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns presigned S3 URL + filename/ext/file_size for the hi-res original.
|
||||||
|
* GET /api/v1/assets/:id/hires
|
||||||
|
*/
|
||||||
|
async function getHiresDownloadInfo(assetId) {
|
||||||
|
const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/hires`, {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status} from /hires`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.url) throw new Error('Hi-res endpoint returned no URL');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -320,7 +362,6 @@ function createAssetCard(asset) {
|
||||||
info.appendChild(filenameEl);
|
info.appendChild(filenameEl);
|
||||||
|
|
||||||
const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
|
const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
|
||||||
// codec and media_type are top-level columns on the asset record
|
|
||||||
const codec = asset.codec || asset.media_type || 'video';
|
const codec = asset.codec || asset.media_type || 'video';
|
||||||
const meta = document.createElement('div');
|
const meta = document.createElement('div');
|
||||||
meta.className = 'asset-meta';
|
meta.className = 'asset-meta';
|
||||||
|
|
@ -344,7 +385,6 @@ function showAssetDetails(asset) {
|
||||||
state.selectedAsset = asset;
|
state.selectedAsset = asset;
|
||||||
elements.detailsPanel.classList.remove('hidden');
|
elements.detailsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
// All of these are top-level columns in the assets table
|
|
||||||
elements.detailsFilename.textContent = asset.display_name || asset.filename;
|
elements.detailsFilename.textContent = asset.display_name || asset.filename;
|
||||||
elements.detailsCodec.textContent = asset.codec || asset.media_type || 'Unknown';
|
elements.detailsCodec.textContent = asset.codec || asset.media_type || 'Unknown';
|
||||||
elements.detailsResolution.textContent = asset.resolution || 'N/A';
|
elements.detailsResolution.textContent = asset.resolution || 'N/A';
|
||||||
|
|
@ -370,13 +410,15 @@ function showAssetDetails(asset) {
|
||||||
'<span style="color:var(--text-secondary)">No tags</span>';
|
'<span style="color:var(--text-secondary)">No tags</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.importBtn.disabled = false;
|
elements.importBtn.disabled = false;
|
||||||
|
elements.importHiresBtn.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;
|
||||||
|
elements.importHiresBtn.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -396,7 +438,7 @@ function handleProjectFilter(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Import Functionality
|
// Import — Proxy
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
async function importSelectedAsset() {
|
async function importSelectedAsset() {
|
||||||
|
|
@ -422,7 +464,7 @@ async function importAsset(asset) {
|
||||||
try {
|
try {
|
||||||
elements.importBtn.disabled = true;
|
elements.importBtn.disabled = true;
|
||||||
|
|
||||||
// 1. Get a presigned proxy URL
|
// 1. Get proxy URL (absolute — /stream returns a relative path)
|
||||||
showProgress('Getting download link...', 5);
|
showProgress('Getting download link...', 5);
|
||||||
const url = await getSignedDownloadUrl(asset.id);
|
const url = await getSignedDownloadUrl(asset.id);
|
||||||
|
|
||||||
|
|
@ -437,6 +479,9 @@ async function importAsset(asset) {
|
||||||
showProgress('Importing into Premiere Pro...', 85);
|
showProgress('Importing into Premiere Pro...', 85);
|
||||||
await importFileToPremiereProject(filePath);
|
await importFileToPremiereProject(filePath);
|
||||||
|
|
||||||
|
// 4. Store path → assetId so timeline export can resolve this clip
|
||||||
|
saveImportMapping(filePath, safeName, asset);
|
||||||
|
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showSuccessMessage('Imported: ' + safeName);
|
showSuccessMessage('Imported: ' + safeName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -448,6 +493,376 @@ async function importAsset(asset) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Import — Hi-Res Original
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
async function importSelectedAssetHires() {
|
||||||
|
if (!state.selectedAsset) {
|
||||||
|
showErrorMessage('No asset selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await importAssetHires(state.selectedAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importAssetHires(asset) {
|
||||||
|
try {
|
||||||
|
elements.importHiresBtn.disabled = true;
|
||||||
|
|
||||||
|
// 1. Get hi-res presigned URL and metadata
|
||||||
|
showProgress('Getting hi-res link...', 5);
|
||||||
|
let hiresInfo;
|
||||||
|
try {
|
||||||
|
hiresInfo = await getHiresDownloadInfo(asset.id);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('No hi-res source: ' + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeNote = hiresInfo.file_size ? ' (' + formatFileSize(hiresInfo.file_size) + ')' : '';
|
||||||
|
const safeName = sanitizeFilename(
|
||||||
|
hiresInfo.filename || (asset.display_name || asset.id) + '.mxf'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Download hi-res file to OS temp dir
|
||||||
|
showProgress('Downloading hi-res' + sizeNote + '...', 10);
|
||||||
|
const filePath = await downloadFile(hiresInfo.url, safeName);
|
||||||
|
|
||||||
|
// 3. Import into Premiere Pro
|
||||||
|
showProgress('Importing hi-res into Premiere Pro...', 85);
|
||||||
|
await importFileToPremiereProject(filePath);
|
||||||
|
|
||||||
|
// 4. Map this path to the same asset ID (hi-res round-trips to same MAM record)
|
||||||
|
saveImportMapping(filePath, safeName, asset);
|
||||||
|
|
||||||
|
hideProgress();
|
||||||
|
showSuccessMessage('Hi-res imported: ' + safeName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Hi-res import error:', error);
|
||||||
|
hideProgress();
|
||||||
|
showErrorMessage('Hi-res import failed: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
elements.importHiresBtn.disabled = !state.selectedAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists two keys per import:
|
||||||
|
* - exact temp path → so Premiere getMediaPath() matches on the same machine
|
||||||
|
* - 'name:filename' → fuzzy fallback across sessions / machines
|
||||||
|
*/
|
||||||
|
function saveImportMapping(filePath, safeName, asset) {
|
||||||
|
const entry = {
|
||||||
|
assetId: asset.id,
|
||||||
|
displayName: asset.display_name || asset.filename || '',
|
||||||
|
};
|
||||||
|
state.importedAssets[filePath] = entry;
|
||||||
|
state.importedAssets['name:' + safeName] = entry;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('mam_imported_assets', JSON.stringify(state.importedAssets));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Active Sequence Info Bar
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function refreshCurrentSequenceInfo() {
|
||||||
|
csInterface.evalScript('getActiveSequence()', function (resultStr) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(resultStr);
|
||||||
|
state.currentSequenceName = parsed.sequenceName || '';
|
||||||
|
if (state.currentSequenceName) {
|
||||||
|
elements.seqInfoName.textContent = state.currentSequenceName;
|
||||||
|
elements.seqInfoBar.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
elements.seqInfoBar.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
elements.seqInfoBar.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Export Panel UI
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function populateExportProjectSelect() {
|
||||||
|
elements.exportProjSelect.innerHTML = '<option value="">— Select project —</option>';
|
||||||
|
state.projects.forEach(function (p) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
if (p.id === state.selectedProject) opt.selected = true;
|
||||||
|
elements.exportProjSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showExportPanel(timelineData) {
|
||||||
|
elements.exportSeqName.value = timelineData.sequenceName || 'Sequence 1';
|
||||||
|
populateExportProjectSelect();
|
||||||
|
|
||||||
|
var totalClips = (timelineData.clips || []).length;
|
||||||
|
var matchedClips = resolveClipsToAssets(timelineData.clips || []).filter(function (c) {
|
||||||
|
return c.asset_id;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
elements.exportClipInfo.textContent =
|
||||||
|
matchedClips + ' of ' + totalClips + ' clip(s) matched to MAM assets';
|
||||||
|
elements.exportClipInfo.style.color =
|
||||||
|
matchedClips === 0 ? 'var(--error)' : 'var(--text-secondary)';
|
||||||
|
|
||||||
|
// Stash the full timeline data for confirmExportTimeline
|
||||||
|
elements.exportPanel.dataset.timelineJson = JSON.stringify(timelineData);
|
||||||
|
elements.exportPanel.classList.remove('hidden');
|
||||||
|
state.exportPanelVisible = true;
|
||||||
|
elements.exportSeqName.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideExportPanel() {
|
||||||
|
elements.exportPanel.classList.add('hidden');
|
||||||
|
state.exportPanelVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Timeline Export — Premiere → MAM
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1 of 2: read the Premiere timeline then show the export confirmation
|
||||||
|
* panel with pre-filled sequence name and matched clip count.
|
||||||
|
*/
|
||||||
|
async function startExportTimeline() {
|
||||||
|
if (!state.isConnected) {
|
||||||
|
showErrorMessage('Connect to MAM first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle: clicking again closes the panel
|
||||||
|
if (state.exportPanelVisible) {
|
||||||
|
hideExportPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgress('Reading Premiere timeline...', 20);
|
||||||
|
|
||||||
|
const timelineData = await new Promise(function (resolve) {
|
||||||
|
csInterface.evalScript('exportTimelineData()', function (resultStr) {
|
||||||
|
try { resolve(JSON.parse(resultStr)); }
|
||||||
|
catch (e) { resolve({ success: false, message: 'Parse error: ' + e.message, clips: [] }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
hideProgress();
|
||||||
|
|
||||||
|
if (!timelineData.success) {
|
||||||
|
showErrorMessage('Timeline read failed: ' + (timelineData.message || 'unknown error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timelineData.clips || timelineData.clips.length === 0) {
|
||||||
|
showErrorMessage('No clips found in the active sequence');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showExportPanel(timelineData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 2 of 2: resolve clip paths to MAM asset IDs, upsert the sequence,
|
||||||
|
* then PUT the clip array to /api/v1/sequences/:id/clips.
|
||||||
|
*/
|
||||||
|
async function confirmExportTimeline() {
|
||||||
|
var timelineData;
|
||||||
|
try {
|
||||||
|
timelineData = JSON.parse(elements.exportPanel.dataset.timelineJson || '{}');
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage('Invalid timeline data — try reading again');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seqName = (elements.exportSeqName.value || '').trim() || 'Sequence 1';
|
||||||
|
var projectId = elements.exportProjSelect.value;
|
||||||
|
if (!projectId) {
|
||||||
|
showErrorMessage('Select a target project');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = resolveClipsToAssets(timelineData.clips || []);
|
||||||
|
var matched = resolved.filter(function (c) { return c.asset_id; });
|
||||||
|
|
||||||
|
if (matched.length === 0) {
|
||||||
|
hideExportPanel();
|
||||||
|
showErrorMessage('No clips matched MAM assets — import proxies or hi-res first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideExportPanel();
|
||||||
|
showProgress('Creating sequence in MAM...', 20);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var seqId = await upsertSequence(
|
||||||
|
projectId,
|
||||||
|
seqName,
|
||||||
|
timelineData.frameRate || 59.94,
|
||||||
|
timelineData.width || 1920,
|
||||||
|
timelineData.height || 1080
|
||||||
|
);
|
||||||
|
|
||||||
|
showProgress('Writing ' + matched.length + ' clip(s)...', 60);
|
||||||
|
|
||||||
|
var clipPayload = matched.map(function (c) {
|
||||||
|
return {
|
||||||
|
asset_id: c.asset_id,
|
||||||
|
track: c.trackIndex,
|
||||||
|
timeline_in_frames: c.timelineInFrames,
|
||||||
|
timeline_out_frames: c.timelineOutFrames,
|
||||||
|
source_in_frames: c.sourceInFrames,
|
||||||
|
source_out_frames: c.sourceOutFrames,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var clipsRes = await fetch(
|
||||||
|
state.serverUrl + '/api/v1/sequences/' + seqId + '/clips',
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(clipPayload),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!clipsRes.ok) throw new Error('Clip push failed: HTTP ' + clipsRes.status);
|
||||||
|
|
||||||
|
hideProgress();
|
||||||
|
showSuccessMessage('Timeline pushed: ' + matched.length + ' clip(s) → "' + seqName + '"');
|
||||||
|
|
||||||
|
var skipped = resolved.length - matched.length;
|
||||||
|
if (skipped > 0) {
|
||||||
|
_showFlash(skipped + ' clip(s) skipped — not in MAM (import them first)', 'info-message');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideProgress();
|
||||||
|
showErrorMessage('Export failed: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelExportTimeline() {
|
||||||
|
hideExportPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Premiere clip file paths back to MAM asset IDs.
|
||||||
|
* Lookup order: exact temp path → name-based fallback.
|
||||||
|
*/
|
||||||
|
function resolveClipsToAssets(clips) {
|
||||||
|
return clips.map(function (clip) {
|
||||||
|
var entry = state.importedAssets[clip.filePath];
|
||||||
|
if (!entry) entry = state.importedAssets['name:' + clip.fileName];
|
||||||
|
return Object.assign({}, clip, { asset_id: entry ? entry.assetId : null });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing sequence by name in the target project; creates it if
|
||||||
|
* absent. Updates frame_rate/width/height on the existing record.
|
||||||
|
* Returns the sequence ID.
|
||||||
|
*/
|
||||||
|
async function upsertSequence(projectId, name, frameRate, width, height) {
|
||||||
|
var listRes = await fetch(
|
||||||
|
state.serverUrl + '/api/v1/sequences?project_id=' + encodeURIComponent(projectId),
|
||||||
|
{ headers: { Accept: 'application/json' }, credentials: 'include' }
|
||||||
|
);
|
||||||
|
if (listRes.ok) {
|
||||||
|
var seqs = await listRes.json();
|
||||||
|
var existing = seqs.find(function (s) { return s.name === name; });
|
||||||
|
if (existing) {
|
||||||
|
await fetch(state.serverUrl + '/api/v1/sequences/' + existing.id, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ frame_rate: frameRate, width: width, height: height }),
|
||||||
|
});
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var createRes = await fetch(state.serverUrl + '/api/v1/sequences', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
project_id: projectId,
|
||||||
|
name: name,
|
||||||
|
frame_rate: frameRate,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!createRes.ok) throw new Error('Failed to create sequence: HTTP ' + createRes.status);
|
||||||
|
var seq = await createRes.json();
|
||||||
|
return seq.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// UI Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function handleAssetClick(e) {
|
||||||
|
var card = e.target.closest('.asset-card');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
document.querySelectorAll('.asset-card.selected').forEach(function (el) {
|
||||||
|
el.classList.remove('selected');
|
||||||
|
});
|
||||||
|
card.classList.add('selected');
|
||||||
|
|
||||||
|
var assetId = card.dataset.assetId;
|
||||||
|
var asset = state.assets.find(function (a) { return a.id === assetId; });
|
||||||
|
if (asset) showAssetDetails(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProgress(label, percent) {
|
||||||
|
elements.progressContainer.classList.add('visible');
|
||||||
|
elements.progressLabel.textContent = label;
|
||||||
|
state.downloadProgress = percent;
|
||||||
|
updateProgressUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideProgress() {
|
||||||
|
elements.progressContainer.classList.remove('visible');
|
||||||
|
state.downloadProgress = 0;
|
||||||
|
updateProgressUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressUI() {
|
||||||
|
elements.progressFill.style.width = state.downloadProgress + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
_showFlash(message, 'error-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
_showFlash(message, 'success-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showFlash(message, className) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = className;
|
||||||
|
el.textContent = message;
|
||||||
|
var anchor = document.querySelector('.search-filter-area');
|
||||||
|
anchor.insertBefore(el, anchor.firstChild);
|
||||||
|
setTimeout(function () { el.remove(); }, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logMessage(message) {
|
||||||
|
console.log('[MAM Panel] ' + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// File Download and Premiere Import (Node.js / ExtendScript)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a remote URL to a local temp file using Node.js http/https.
|
* Downloads a remote URL to a local temp file using Node.js http/https.
|
||||||
* Requires --enable-nodejs in the CEP manifest.
|
* Requires --enable-nodejs in the CEP manifest.
|
||||||
|
|
@ -546,62 +961,6 @@ function importFileToPremiereProject(filePath) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// UI Helpers
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function handleAssetClick(e) {
|
|
||||||
var card = e.target.closest('.asset-card');
|
|
||||||
if (!card) return;
|
|
||||||
|
|
||||||
document.querySelectorAll('.asset-card.selected').forEach(function (el) {
|
|
||||||
el.classList.remove('selected');
|
|
||||||
});
|
|
||||||
card.classList.add('selected');
|
|
||||||
|
|
||||||
var assetId = card.dataset.assetId;
|
|
||||||
var asset = state.assets.find(function (a) { return a.id === assetId; });
|
|
||||||
if (asset) showAssetDetails(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showProgress(label, percent) {
|
|
||||||
elements.progressContainer.classList.add('visible');
|
|
||||||
elements.progressLabel.textContent = label;
|
|
||||||
state.downloadProgress = percent;
|
|
||||||
updateProgressUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideProgress() {
|
|
||||||
elements.progressContainer.classList.remove('visible');
|
|
||||||
state.downloadProgress = 0;
|
|
||||||
updateProgressUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgressUI() {
|
|
||||||
elements.progressFill.style.width = state.downloadProgress + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showErrorMessage(message) {
|
|
||||||
_showFlash(message, 'error-message');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccessMessage(message) {
|
|
||||||
_showFlash(message, 'success-message');
|
|
||||||
}
|
|
||||||
|
|
||||||
function _showFlash(message, className) {
|
|
||||||
var el = document.createElement('div');
|
|
||||||
el.className = className;
|
|
||||||
el.textContent = message;
|
|
||||||
var anchor = document.querySelector('.search-filter-area');
|
|
||||||
anchor.insertBefore(el, anchor.firstChild);
|
|
||||||
setTimeout(function () { el.remove(); }, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logMessage(message) {
|
|
||||||
console.log('[MAM Panel] ' + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Utility Functions
|
// Utility Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue