The redesigned UXP panel (left icon rail, compact list-view toggle, hover tooltips, single Export menu) was committed only to redesign/panel-icon-rail and never merged, so main + the website kept serving the old blocky-button build under the same version number (2.2.2). That branch had diverged off an old main and is missing recent worker/HLS/NVENC/import work, so it can't be merged wholesale — cherry-pick just the plugin instead. - services/premiere-plugin-uxp: replace source with the redesigned panel (adds src/tooltip.js; reworks index.html + styles.css + src/*). Verified byte-identical to the build installed on BMG-PC-Edit. - web-ui/public/downloads/dragonflight-mam-2.2.2.ccx: swap the served artifact to the redesigned 34708-byte build (download link unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
264 lines
11 KiB
JavaScript
264 lines
11 KiB
JavaScript
// import-flow.js — v2.1.6
|
|
// premierepro API: docs say sync, runtime returns Promises. Await everything.
|
|
|
|
(function () {
|
|
const Import = {};
|
|
|
|
const fs = require('fs');
|
|
// window.path is a UXP global (v6.4+) — no require('path')
|
|
let os; try { os = require('os'); } catch (_) { os = {}; }
|
|
let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; }
|
|
|
|
// ── Temp folder ──────────────────────────────────────────────────
|
|
async function _getTempBase() {
|
|
if (uxpFs && uxpFs.getTemporaryFolder) {
|
|
try {
|
|
const tmp = await uxpFs.getTemporaryFolder();
|
|
if (tmp && tmp.nativePath) return tmp.nativePath;
|
|
} catch (_) {}
|
|
}
|
|
try {
|
|
const e = (typeof process !== 'undefined' && process.env) || {};
|
|
if (e.TEMP && e.TEMP.length) return e.TEMP;
|
|
if (e.TMP && e.TMP.length) return e.TMP;
|
|
if (e.LOCALAPPDATA) return e.LOCALAPPDATA + '\\Temp';
|
|
} catch (_) {}
|
|
try {
|
|
if (os.homedir) {
|
|
const h = os.homedir();
|
|
if (h) return h + '\\AppData\\Local\\Temp';
|
|
}
|
|
} catch (_) {}
|
|
throw new Error('Cannot find writable temp folder');
|
|
}
|
|
|
|
Import._tempPath = async function (safeName) {
|
|
const base = await _getTempBase();
|
|
return path.join(base, 'dragonflight-' + safeName);
|
|
};
|
|
|
|
// Returns true if the path already exists on disk.
|
|
Import._fileExists = async function (filePath) {
|
|
try { await fs.stat(filePath); return true; } catch (_) { return false; }
|
|
};
|
|
|
|
// Write ArrayBuffer to disk via fs.writeFile.
|
|
// If the file is locked (EBUSY — Premiere already has it open from a
|
|
// previous import) we treat that as success: the bytes are already there.
|
|
Import._writeBuffer = async function (destPath, arrayBuffer) {
|
|
try {
|
|
await fs.writeFile(destPath, arrayBuffer);
|
|
} catch (e) {
|
|
const busy = e.code === 'EBUSY' || /resource busy/i.test(String(e.message));
|
|
if (!busy) throw e;
|
|
// File locked by Premiere — it's already there, proceed.
|
|
console.warn('[df] _writeBuffer EBUSY on', destPath, '— using existing file');
|
|
}
|
|
return destPath;
|
|
};
|
|
|
|
// Fetch — no redirect option (not supported in UXP)
|
|
// addAuth: only for same-origin proxy URLs, NOT for S3 presigned URLs
|
|
async function _fetch(url, addAuth) {
|
|
const headers = {};
|
|
if (addAuth && API.state.apiToken) {
|
|
headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
|
}
|
|
const r = await fetch(url, { headers });
|
|
if (!r.ok) throw new Error('HTTP ' + r.status + ' fetching ' + url);
|
|
return r;
|
|
}
|
|
|
|
// ── premierepro lazy require ─────────────────────────────────────
|
|
function _ppro() {
|
|
if (Import._ppro_mod) return Import._ppro_mod;
|
|
try { Import._ppro_mod = require('premierepro'); }
|
|
catch (e) { throw new Error('UXP premierepro unavailable: ' + e.message); }
|
|
return Import._ppro_mod;
|
|
}
|
|
|
|
// Import a file into the active Premiere project.
|
|
// ALL premierepro calls must be awaited — runtime returns Promises
|
|
// even though docs list them as synchronous.
|
|
// importFiles returns false when the file is already in the project — not an error.
|
|
Import.importIntoProject = async function (filePath) {
|
|
const P = _ppro();
|
|
const project = await P.Project.getActiveProject();
|
|
if (!project) throw new Error('No active Premiere project');
|
|
const root = await project.getRootItem();
|
|
await project.importFiles([filePath], true, root, false);
|
|
// Return value false = already in project. Either way, we're done.
|
|
return true;
|
|
};
|
|
|
|
// ── Proxy import ─────────────────────────────────────────────────
|
|
Import.proxy = async function (asset) {
|
|
const safeName = UI.sanitizeFilename((asset.display_name || asset.filename || asset.id) + '.mp4');
|
|
const dest = await Import._tempPath(safeName);
|
|
|
|
const alreadyOnDisk = await Import._fileExists(dest);
|
|
if (alreadyOnDisk) {
|
|
// File exists from a previous import — skip download, just (re-)import.
|
|
UI.showProgress('Importing into Premiere…', 92);
|
|
} else {
|
|
UI.showProgress('Resolving proxy URL…', 4);
|
|
const { url } = await API.getProxyUrl(asset.id);
|
|
|
|
UI.showProgress('Downloading ' + safeName + '…', 10);
|
|
const r = await _fetch(url, true); // same-origin — add auth
|
|
|
|
UI.showProgress('Writing to disk…', 70);
|
|
const buf = await r.arrayBuffer();
|
|
await Import._writeBuffer(dest, buf);
|
|
|
|
UI.showProgress('Importing into Premiere…', 92);
|
|
}
|
|
|
|
await Import.importIntoProject(dest);
|
|
UI.hideProgress();
|
|
UI.toast('Imported: ' + safeName, 'ok');
|
|
return { localPath: dest, safeName };
|
|
};
|
|
|
|
// ── Hi-Res import ────────────────────────────────────────────────
|
|
Import.hires = async function (asset) {
|
|
UI.showProgress('Resolving hi-res URL…', 4);
|
|
const info = await API.getHiresInfo(asset.id);
|
|
const safeName = UI.sanitizeFilename(info.filename || (asset.display_name || asset.id) + '.' + (info.ext || 'mxf'));
|
|
const dest = await Import._tempPath(safeName);
|
|
|
|
const alreadyOnDisk = await Import._fileExists(dest);
|
|
if (alreadyOnDisk) {
|
|
UI.showProgress('Importing into Premiere…', 92);
|
|
} else {
|
|
UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8);
|
|
const r = await _fetch(info.url, false); // presigned S3 — no auth
|
|
|
|
UI.showProgress('Writing to disk…', 75);
|
|
const buf = await r.arrayBuffer();
|
|
await Import._writeBuffer(dest, buf);
|
|
|
|
UI.showProgress('Importing into Premiere…', 92);
|
|
}
|
|
|
|
await Import.importIntoProject(dest);
|
|
UI.hideProgress();
|
|
UI.toast('Hi-res imported: ' + safeName, 'ok');
|
|
return { localPath: dest, safeName };
|
|
};
|
|
|
|
// Used by timeline.js batch relink
|
|
Import._streamToFile = async function (response, destPath /*, onProgress */) {
|
|
const buf = await response.arrayBuffer();
|
|
await Import._writeBuffer(destPath, buf);
|
|
return destPath;
|
|
};
|
|
|
|
// ── Upload (ingest editor media into the MAM) ────────────────────
|
|
const SIMPLE_MAX = 45 * 1024 * 1024; // server caps /simple at <50 MB
|
|
const PART_SIZE = 16 * 1024 * 1024; // chunk size for multipart
|
|
|
|
function _contentType(name) {
|
|
const ext = String(name).split('.').pop().toLowerCase();
|
|
const map = {
|
|
mp4:'video/mp4', m4v:'video/mp4', mov:'video/quicktime', mxf:'application/mxf',
|
|
mkv:'video/x-matroska', avi:'video/x-msvideo', mpg:'video/mpeg', mpeg:'video/mpeg',
|
|
mts:'video/mp2t', m2ts:'video/mp2t', wav:'audio/wav', aif:'audio/aiff', aiff:'audio/aiff',
|
|
mp3:'audio/mpeg', png:'image/png', jpg:'image/jpeg', jpeg:'image/jpeg',
|
|
};
|
|
return map[ext] || 'application/octet-stream';
|
|
}
|
|
|
|
// Read a local file and push it to the MAM. Returns the created asset row.
|
|
// NOTE: reads the whole file into memory once (fine for typical clips;
|
|
// very large multi-GB originals may strain memory — revisit with a
|
|
// positional-read stream if that becomes a problem).
|
|
Import.uploadFile = async function (nativePath, meta) {
|
|
meta = meta || {};
|
|
if (!meta.projectId) throw new Error('No target project for upload');
|
|
const filename = meta.filename || path.basename(nativePath);
|
|
const contentType = _contentType(filename);
|
|
|
|
const buf = await fs.readFile(nativePath);
|
|
const size = buf.byteLength != null ? buf.byteLength : buf.length;
|
|
|
|
if (size <= SIMPLE_MAX) {
|
|
const blob = new Blob([buf], { type: contentType });
|
|
return API.uploadSimple(blob, { filename, projectId: meta.projectId, binId: meta.binId, contentType });
|
|
}
|
|
|
|
// Chunked multipart for large files.
|
|
const init = await API.uploadInit({ filename, fileSize: size, contentType, projectId: meta.projectId, binId: meta.binId });
|
|
const parts = [];
|
|
try {
|
|
let partNumber = 1;
|
|
for (let off = 0; off < size; off += PART_SIZE, partNumber++) {
|
|
const chunk = buf.slice(off, Math.min(off + PART_SIZE, size));
|
|
const blob = new Blob([chunk], { type: contentType });
|
|
if (meta.onProgress) meta.onProgress(off, size);
|
|
const res = await API.uploadPart(blob, { uploadId: init.uploadId, key: init.key, partNumber });
|
|
parts.push({ PartNumber: partNumber, ETag: res.etag });
|
|
}
|
|
return API.uploadComplete({ uploadId: init.uploadId, key: init.key, assetId: init.assetId, parts });
|
|
} catch (e) {
|
|
await API.uploadAbort({ uploadId: init.uploadId, key: init.key, assetId: init.assetId });
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
// ── Bin selection (best-effort) + file-picker fallback ───────────
|
|
// Tries to read the highlighted project-panel item(s). The UXP premierepro
|
|
// selection surface varies by version, so every access is guarded; on any
|
|
// miss this returns [] and callers fall back to a native file picker.
|
|
Import.getSelectedBinPaths = async function () {
|
|
const paths = [];
|
|
try {
|
|
const P = _ppro();
|
|
const project = await P.Project.getActiveProject();
|
|
if (!project) return paths;
|
|
let sel = null;
|
|
try { if (project.getSelection) sel = await project.getSelection(); } catch (_) {}
|
|
let items = [];
|
|
if (sel) {
|
|
if (typeof sel.getItems === 'function') items = await sel.getItems();
|
|
else if (Array.isArray(sel)) items = sel;
|
|
else if (Array.isArray(sel.items)) items = sel.items;
|
|
}
|
|
for (const it of (items || [])) {
|
|
try {
|
|
const ci = await P.ClipProjectItem.cast(it);
|
|
const mp = await ci.getMediaFilePath();
|
|
if (mp) paths.push(mp);
|
|
} catch (_) {}
|
|
}
|
|
} catch (_) {}
|
|
return paths;
|
|
};
|
|
|
|
// Native file picker — returns array of native paths (may be empty).
|
|
Import.pickFiles = async function () {
|
|
if (!uxpFs || !uxpFs.getFileForOpening) throw new Error('File picker unavailable in this host');
|
|
const sel = await uxpFs.getFileForOpening({ allowMultiple: true });
|
|
if (!sel) return [];
|
|
const arr = Array.isArray(sel) ? sel : [sel];
|
|
return arr.map(f => f && f.nativePath).filter(Boolean);
|
|
};
|
|
|
|
// Upload any timeline clips not yet in the MAM, recording the path→asset
|
|
// mapping so resolveClipsToAssets picks them up on the next pass.
|
|
Import.ensureClipsInMam = async function (clips, projectId, onProgress) {
|
|
const missing = clips.filter(c => !c.asset_id && c.filePath);
|
|
for (let i = 0; i < missing.length; i++) {
|
|
const c = missing[i];
|
|
if (onProgress) onProgress(c.fileName || path.basename(c.filePath), i + 1, missing.length);
|
|
const asset = await Import.uploadFile(c.filePath, { projectId, filename: path.basename(c.filePath) });
|
|
if (asset && asset.id) {
|
|
Library.recordImport(c.filePath, { assetId: asset.id });
|
|
if (c.fileName) Library.recordImport('name:' + c.fileName, { assetId: asset.id });
|
|
}
|
|
}
|
|
return { uploaded: missing.length };
|
|
};
|
|
|
|
window.Import = Import;
|
|
})();
|