UXP v2.1.1: import-flow.js — expose _tempPath/_streamToFile, return {localPath,safeName} from proxy/hires

This commit is contained in:
Zac Gaetano 2026-05-28 02:22:58 -04:00
parent 5432c2dfa1
commit 76fff5efc2

View file

@ -1,55 +1,42 @@
// Download asset → import into the active Premiere project via UXP's // import-flow.js — v2.1.1
// `premierepro` module. This is what the CEP panel could no longer do // Download asset → write to temp → importFiles() via UXP premierepro.
// (csInterface.evalScript callback was lost in PPro 26). // Also exposes _tempPath / _streamToFile for timeline.js batch relink.
(function () { (function () {
const Import = {}; const Import = {};
// Use UXP's Node-style fs for streaming writes (avoids buffering multi-GB
// files in memory). `os` is a stripped subset — `os.tmpdir` is not exposed
// in all UXP builds, so resolve the temp folder defensively below.
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let os; try { os = require('os'); } catch (_) { os = {}; } let os; try { os = require('os'); } catch (_) { os = {}; }
let uxpFs; let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; }
try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; }
// Resolve a writable temp folder using whichever API this UXP build // ── Temp folder ──────────────────────────────────────────────────
// actually exposes. Returns an absolute path string suitable for both async function _getTempBase() {
// fs.createWriteStream and premierepro.Project.importFiles.
async function getTempBase() {
// 1. Node-style os.tmpdir (newer UXP, simplest case)
try { if (os.tmpdir) { const t = os.tmpdir(); if (typeof t === 'string' && t.length) return t; } } catch (_) {} try { if (os.tmpdir) { const t = os.tmpdir(); if (typeof t === 'string' && t.length) return t; } } catch (_) {}
// 2. UXP storage API — documented portable approach
if (uxpFs && uxpFs.getTemporaryFolder) { if (uxpFs && uxpFs.getTemporaryFolder) {
try { const tmp = await uxpFs.getTemporaryFolder(); if (tmp && tmp.nativePath) return tmp.nativePath; } catch (_) {} try { const tmp = await uxpFs.getTemporaryFolder(); if (tmp && tmp.nativePath) return tmp.nativePath; } catch (_) {}
} }
// 3. Windows env vars (always set under PPro)
try { try {
const e = (typeof process !== 'undefined' && process.env) || {}; const e = (typeof process !== 'undefined' && process.env) || {};
if (e.TEMP) return e.TEMP; if (e.TEMP) return e.TEMP;
if (e.TMP) return e.TMP; if (e.TMP) return e.TMP;
if (e.LOCALAPPDATA) return path.join(e.LOCALAPPDATA, 'Temp'); if (e.LOCALAPPDATA) return path.join(e.LOCALAPPDATA, 'Temp');
} catch (_) {} } catch (_) {}
// 4. Homedir + Windows-style fallback
try { if (os.homedir) return path.join(os.homedir(), 'AppData', 'Local', 'Temp'); } catch (_) {} try { if (os.homedir) return path.join(os.homedir(), 'AppData', 'Local', 'Temp'); } catch (_) {}
throw new Error('Could not determine a writable temp folder on this system'); throw new Error('Cannot find writable temp folder');
} }
async function tempPath(safeName) { Import._tempPath = async function (safeName) {
const base = await getTempBase(); const base = await _getTempBase();
return path.join(base, 'dragonflight-' + safeName); return path.join(base, 'dragonflight-' + safeName);
} };
// Stream the body of a fetch Response to a file on disk. // ── Stream response body to disk ─────────────────────────────────
// Returns the absolute path. Reports progress via onProgress({ received, total }). Import._streamToFile = async function (response, destPath, onProgress) {
async function streamToFile(response, destPath, onProgress) { const total = Number(response.headers.get('content-length') || 0);
const total = Number(response.headers.get('content-length') || 0);
const reader = response.body.getReader(); const reader = response.body.getReader();
const out = fs.createWriteStream(destPath); const out = fs.createWriteStream(destPath);
let received = 0; let received = 0;
// Wrap close in a Promise so we don't resolve before the OS finishes
// flushing — without this, importFiles can race the writer.
const closed = new Promise((resolve, reject) => { const closed = new Promise((resolve, reject) => {
out.on('finish', resolve); out.on('finish', resolve);
out.on('error', reject); out.on('error', reject);
@ -59,7 +46,6 @@
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) break; if (done) break;
if (!out.write(Buffer.from(value))) { if (!out.write(Buffer.from(value))) {
// Backpressure — wait for drain so memory stays flat.
await new Promise(r => out.once('drain', r)); await new Promise(r => out.once('drain', r));
} }
received += value.byteLength; received += value.byteLength;
@ -70,115 +56,100 @@
} }
await closed; await closed;
return destPath; return destPath;
} };
// Manual-follow fetch for redirects: UXP strips Authorization across // ── Manual redirect follow ───────────────────────────────────────
// origins (per Adobe security policy), so for any /api/... call that might // UXP strips Authorization on cross-origin redirects. Follow manually.
// 302 to S3 we follow the redirect ourselves and DROP the Bearer header async function _fetchFollow(url, opts) {
// on the next hop.
async function fetchFollow(url, opts) {
opts = opts || {}; opts = opts || {};
let current = url; let current = url;
let auth = (opts.headers || {})['Authorization'] || null; for (let hop = 0; hop < 6; hop++) {
for (let hop = 0; hop < 5; hop++) {
const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' })); const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' }));
if (r.status >= 300 && r.status < 400 && r.headers.get('location')) { if (r.status >= 200 && r.status < 300) return r;
const next = r.headers.get('location'); if (r.status >= 300 && r.status < 400) {
const absNext = /^https?:\/\//i.test(next) ? next : new URL(next, current).toString(); const loc = r.headers.get('location');
const sameHost = (new URL(absNext)).host === (new URL(current)).host; if (!loc) throw new Error('Redirect with no Location');
opts = Object.assign({}, opts, { const next = /^https?:\/\//i.test(loc) ? loc : new URL(loc, current).toString();
headers: Object.assign({}, opts.headers || {}, sameHost ? {} : { Authorization: undefined }), // Drop auth on cross-origin hop
}); if (new URL(next).host !== new URL(current).host) {
if (!sameHost) delete opts.headers.Authorization; opts = Object.assign({}, opts, { headers: Object.assign({}, opts.headers || {}) });
current = absNext; delete opts.headers['Authorization'];
}
current = next;
continue; continue;
} }
return r; throw new Error('HTTP ' + r.status);
} }
throw new Error('Too many redirects'); throw new Error('Too many redirects');
} }
// Find the new ProjectItem after importFiles. Premiere's UXP importFiles // ── premierepro lazy require ─────────────────────────────────────
// returns only a boolean — we diff the rootItem.getItems() snapshot. function _ppro() {
// (Not currently used but available for callers that need the item.) if (Import._ppro_mod) return Import._ppro_mod;
Import.locateImported = async function (project, filename) { try { Import._ppro_mod = require('premierepro'); }
try { catch (e) { throw new Error('UXP premierepro unavailable: ' + e.message); }
const root = await project.getRootItem(); return Import._ppro_mod;
const items = await root.getItems();
for (const it of items) {
const name = await it.getName().catch(() => null);
if (name && name === filename) return it;
}
} catch (_) {}
return null;
};
// Talk to Premiere. Lazy-require so the module isn't loaded until the user
// actually triggers an import (faster panel boot, clearer error if UXP host
// is missing the API).
function ppro() {
if (Import._ppro) return Import._ppro;
try { Import._ppro = require('premierepro'); }
catch (e) { throw new Error('UXP premierepro module unavailable: ' + e.message); }
return Import._ppro;
} }
// Hand a downloaded file off to Premiere. // Import a file that is already on disk into the active Premiere project.
Import.importIntoProject = async function (filePath) { Import.importIntoProject = async function (filePath) {
const P = ppro(); const P = _ppro();
const project = await P.Project.getActiveProject(); const project = await P.Project.getActiveProject();
if (!project) throw new Error('No active Premiere project. Open or create a project first.'); if (!project) throw new Error('No active Premiere project');
const root = await project.getRootItem(); const root = await project.getRootItem();
// suppressUI=true, targetBin=root, asNumberedStills=false.
const ok = await project.importFiles([filePath], true, root, false); const ok = await project.importFiles([filePath], true, root, false);
if (!ok) throw new Error('Premiere refused to import the file.'); if (!ok) throw new Error('Premiere refused to import file');
return true; return true;
}; };
// ── Proxy import: /stream returns a same-host /video URL ────────── // ── Proxy import ─────────────────────────────────────────────────
// Returns { localPath, safeName } for caller to record import mapping.
Import.proxy = async function (asset) { Import.proxy = async function (asset) {
const safeName = UI.sanitizeFilename((asset.display_name || asset.filename || asset.id) + '.mp4'); const safeName = UI.sanitizeFilename((asset.display_name || asset.filename || asset.id) + '.mp4');
const dest = await tempPath(safeName); const dest = await Import._tempPath(safeName);
UI.showProgress('Resolving proxy URL…', 4); UI.showProgress('Resolving proxy URL…', 4);
const { url } = await API.getProxyUrl(asset.id); const { url } = await API.getProxyUrl(asset.id);
UI.showProgress('Downloading ' + safeName + '…', 10); UI.showProgress('Downloading ' + safeName + '…', 10);
const r = await fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } }); const r = await _fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } });
if (!r.ok) throw new Error('Download HTTP ' + r.status); if (!r.ok) throw new Error('Download HTTP ' + r.status);
await streamToFile(r, dest, ({ received, total }) => { await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 10 + (received / total) * 75 : 10; const pct = total ? 10 + (received / total) * 75 : 10;
UI.showProgress('Downloading ' + UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
}); });
UI.showProgress('Importing into Premiere…', 92); UI.showProgress('Importing into Premiere…', 92);
await Import.importIntoProject(dest); await Import.importIntoProject(dest);
UI.hideProgress(); UI.hideProgress();
UI.toast('Imported: ' + safeName, 'ok'); UI.toast('Imported: ' + safeName, 'ok');
return { localPath: dest, safeName };
}; };
// ── Hi-Res import: /hires returns a presigned S3 URL on broadcastmgmt.cloud // ── Hi-Res import ────────────────────────────────────────────────
// Returns { localPath, safeName }.
Import.hires = async function (asset) { Import.hires = async function (asset) {
UI.showProgress('Resolving hi-res URL…', 4); UI.showProgress('Resolving hi-res URL…', 4);
const info = await API.getHiresInfo(asset.id); const info = await API.getHiresInfo(asset.id);
const safeName = UI.sanitizeFilename(info.filename || (asset.display_name || asset.id) + '.' + (info.ext || 'mxf')); const safeName = UI.sanitizeFilename(info.filename || (asset.display_name || asset.id) + '.' + (info.ext || 'mxf'));
const dest = await tempPath(safeName); const dest = await Import._tempPath(safeName);
UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8); UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8);
// info.url is presigned S3 — DO NOT add Bearer (would break signature). // Presigned S3 URL — no Bearer needed
const r = await fetchFollow(info.url, {}); const r = await _fetchFollow(info.url, {});
if (!r.ok) throw new Error('Download HTTP ' + r.status); if (!r.ok) throw new Error('Download HTTP ' + r.status);
await streamToFile(r, dest, ({ received, total }) => { await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 8 + (received / total) * 80 : 8; const pct = total ? 8 + (received / total) * 80 : 8;
UI.showProgress('Downloading ' + UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct); UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
}); });
UI.showProgress('Importing into Premiere…', 92); UI.showProgress('Importing into Premiere…', 92);
await Import.importIntoProject(dest); await Import.importIntoProject(dest);
UI.hideProgress(); UI.hideProgress();
UI.toast('Hi-res imported: ' + safeName, 'ok'); UI.toast('Hi-res imported: ' + safeName, 'ok');
return { localPath: dest, safeName };
}; };
window.Import = Import; window.Import = Import;