dragonflight/services/premiere-plugin-uxp/src/import-flow.js

161 lines
7 KiB
JavaScript

// import-flow.js — v2.1.2
// Download asset → write to temp → importFiles() via UXP premierepro.
// NOTE: `path` module is NOT available in PPro 26 UXP. Use _join() helper.
// Also exposes _tempPath / _streamToFile for timeline.js batch relink.
(function () {
const Import = {};
const fs = require('fs');
// `path` is stripped in this UXP build — manual join instead.
let os; try { os = require('os'); } catch (_) { os = {}; }
let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; }
// Simple path join that works on Windows (backslash) without require('path').
function _join(base, name) {
const sep = base.indexOf('/') === -1 ? '\\' : '/';
return base.replace(/[\\/]+$/, '') + sep + name;
}
// ── Temp folder ──────────────────────────────────────────────────
async function _getTempBase() {
// 1. os.tmpdir (may exist in some UXP builds)
try { if (os.tmpdir) { const t = os.tmpdir(); if (typeof t === 'string' && t.length) return t; } } catch (_) {}
// 2. UXP storage API
if (uxpFs && uxpFs.getTemporaryFolder) {
try { const tmp = await uxpFs.getTemporaryFolder(); if (tmp && tmp.nativePath) return tmp.nativePath; } catch (_) {}
}
// 3. Windows env vars (always present under PPro on Windows)
try {
const e = (typeof process !== 'undefined' && process.env) || {};
if (e.TEMP) return e.TEMP;
if (e.TMP) return e.TMP;
if (e.LOCALAPPDATA) return e.LOCALAPPDATA + '\\Temp';
} catch (_) {}
// 4. Homedir fallback (no path.join needed)
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 _join(base, 'dragonflight-' + safeName);
};
// ── Stream response body to disk ─────────────────────────────────
Import._streamToFile = async function (response, destPath, onProgress) {
const total = Number(response.headers.get('content-length') || 0);
const reader = response.body.getReader();
const out = fs.createWriteStream(destPath);
let received = 0;
const closed = new Promise((resolve, reject) => {
out.on('finish', resolve);
out.on('error', reject);
});
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!out.write(Buffer.from(value))) {
await new Promise(r => out.once('drain', r));
}
received += value.byteLength;
if (onProgress) onProgress({ received, total });
}
} finally {
out.end();
}
await closed;
return destPath;
};
// ── Manual redirect follow ───────────────────────────────────────
async function _fetchFollow(url, opts) {
opts = opts || {};
let current = url;
for (let hop = 0; hop < 6; hop++) {
const r = await fetch(current, Object.assign({}, opts, { redirect: 'manual' }));
if (r.status >= 200 && r.status < 300) return r;
if (r.status >= 300 && r.status < 400) {
const loc = r.headers.get('location');
if (!loc) throw new Error('Redirect with no Location');
const next = /^https?:\/\//i.test(loc) ? loc : new URL(loc, current).toString();
if (new URL(next).host !== new URL(current).host) {
opts = Object.assign({}, opts, { headers: Object.assign({}, opts.headers || {}) });
delete opts.headers['Authorization'];
}
current = next;
continue;
}
throw new Error('HTTP ' + r.status);
}
throw new Error('Too many redirects');
}
// ── 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.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();
const ok = await project.importFiles([filePath], true, root, false);
if (!ok) throw new Error('Premiere refused to import file');
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);
UI.showProgress('Resolving proxy URL…', 4);
const { url } = await API.getProxyUrl(asset.id);
UI.showProgress('Downloading ' + safeName + '…', 10);
const r = await _fetchFollow(url, { headers: { Authorization: 'Bearer ' + API.state.apiToken } });
if (!r.ok) throw new Error('Download HTTP ' + r.status);
await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 10 + (received / total) * 75 : 10;
UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
});
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);
UI.showProgress('Downloading ' + safeName + ' (' + UI.formatBytes(Number(info.file_size || 0)) + ')…', 8);
const r = await _fetchFollow(info.url, {});
if (!r.ok) throw new Error('Download HTTP ' + r.status);
await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 8 + (received / total) * 80 : 8;
UI.showProgress(UI.formatBytes(received) + (total ? ' / ' + UI.formatBytes(total) : '') + '…', pct);
});
UI.showProgress('Importing into Premiere…', 92);
await Import.importIntoProject(dest);
UI.hideProgress();
UI.toast('Hi-res imported: ' + safeName, 'ok');
return { localPath: dest, safeName };
};
window.Import = Import;
})();