// 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; })();