171 lines
6.3 KiB
JavaScript
171 lines
6.3 KiB
JavaScript
// Dragonflight API client — v2.1.4
|
|
// UXP fetch() notes:
|
|
// • redirect:'manual' is NOT a supported option — UXP auto-follows redirects
|
|
// • Authorization is stripped by UXP on cross-origin redirects (security fix)
|
|
// • For same-origin API calls: add Bearer header
|
|
// • For off-origin URLs (S3 presigned): do NOT add Bearer
|
|
|
|
(function () {
|
|
const API = {};
|
|
const LS_URL = 'df.uxp.serverUrl';
|
|
const LS_TOKEN = 'df.uxp.apiToken';
|
|
|
|
API.state = {
|
|
serverUrl: localStorage.getItem(LS_URL) || '',
|
|
apiToken: localStorage.getItem(LS_TOKEN) || '',
|
|
connected: false,
|
|
};
|
|
|
|
API.save = function () {
|
|
localStorage.setItem(LS_URL, API.state.serverUrl);
|
|
localStorage.setItem(LS_TOKEN, API.state.apiToken);
|
|
};
|
|
|
|
API.clear = function () {
|
|
API.state.serverUrl = '';
|
|
API.state.apiToken = '';
|
|
API.state.connected = false;
|
|
localStorage.removeItem(LS_URL);
|
|
localStorage.removeItem(LS_TOKEN);
|
|
};
|
|
|
|
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
|
|
|
// Core request — adds Bearer only for same-server URLs.
|
|
// No redirect option — UXP handles redirects automatically.
|
|
API.request = async function (urlOrPath, opts) {
|
|
opts = opts || {};
|
|
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
|
const base = trimUrl(API.state.serverUrl);
|
|
const url = isAbs ? urlOrPath : (base + urlOrPath);
|
|
const headers = Object.assign({}, opts.headers || {});
|
|
if (!isAbs || url.startsWith(base)) {
|
|
if (API.state.apiToken) headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
|
}
|
|
return fetch(url, Object.assign({}, opts, { headers }));
|
|
};
|
|
|
|
// Fetch an off-origin URL (e.g. presigned S3) — no auth header.
|
|
API.requestExternal = async function (url) {
|
|
return fetch(url);
|
|
};
|
|
|
|
API.json = async function (pathOrUrl, opts) {
|
|
const r = await API.request(pathOrUrl, opts);
|
|
if (!r.ok) {
|
|
const text = await r.text().catch(() => '');
|
|
throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : ''));
|
|
}
|
|
return r.json();
|
|
};
|
|
|
|
// ── Auth ─────────────────────────────────────────────────────────
|
|
API.connect = async function (serverUrl, apiToken) {
|
|
API.state.serverUrl = trimUrl(serverUrl);
|
|
API.state.apiToken = String(apiToken || '').trim();
|
|
if (!API.state.serverUrl || !API.state.apiToken) {
|
|
throw new Error('Server URL and API token required');
|
|
}
|
|
const me = await API.json('/api/v1/auth/me');
|
|
API.state.connected = true;
|
|
API.save();
|
|
return me;
|
|
};
|
|
|
|
API.disconnect = function () { API.clear(); };
|
|
|
|
// ── Assets ───────────────────────────────────────────────────────
|
|
API.listAssets = async function (query, projectId) {
|
|
const p = new URLSearchParams({ limit: '60' });
|
|
if (query) p.set('q', query);
|
|
if (projectId && projectId !== 'all') p.set('project_id', projectId);
|
|
return API.json('/api/v1/assets?' + p.toString());
|
|
};
|
|
|
|
API.getAsset = async function (assetId) {
|
|
return API.json('/api/v1/assets/' + assetId);
|
|
};
|
|
|
|
// /stream → { url, type, source } (same-origin proxy video URL)
|
|
API.getProxyUrl = async function (assetId) {
|
|
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
|
if (!data || !data.url) throw new Error('Asset has no proxy');
|
|
const u = data.url;
|
|
const abs = /^https?:\/\//i.test(u) ? u : trimUrl(API.state.serverUrl) + u;
|
|
return { url: abs, source: data.source || 'proxy' };
|
|
};
|
|
|
|
// /hires → { url, filename, ext, file_size, type } (presigned S3 URL)
|
|
API.getHiresInfo = async function (assetId) {
|
|
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
|
if (!data || !data.url) throw new Error('Asset has no hi-res source');
|
|
return data;
|
|
};
|
|
|
|
// /live-path → { win_path, posix_path, display_name }
|
|
API.getLivePath = async function (assetId) {
|
|
return API.json('/api/v1/assets/' + assetId + '/live-path');
|
|
};
|
|
|
|
// ── Projects ─────────────────────────────────────────────────────
|
|
API.listProjects = async function () {
|
|
const data = await API.json('/api/v1/projects');
|
|
return Array.isArray(data) ? data : [];
|
|
};
|
|
|
|
// ── Sequences ────────────────────────────────────────────────────
|
|
API.listSequences = async function (projectId) {
|
|
const p = new URLSearchParams();
|
|
if (projectId) p.set('project_id', projectId);
|
|
const data = await API.json('/api/v1/sequences?' + p.toString());
|
|
return Array.isArray(data) ? data : [];
|
|
};
|
|
|
|
API.createSequence = async function (projectId, name, frameRate, width, height) {
|
|
return API.json('/api/v1/sequences', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ project_id: projectId, name, frame_rate: frameRate, width, height }),
|
|
});
|
|
};
|
|
|
|
API.updateSequence = async function (seqId, fields) {
|
|
return API.json('/api/v1/sequences/' + seqId, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(fields),
|
|
});
|
|
};
|
|
|
|
API.pushClips = async function (seqId, clips) {
|
|
const r = await API.request('/api/v1/sequences/' + seqId + '/clips', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(clips),
|
|
});
|
|
if (!r.ok) throw new Error('Clip push HTTP ' + r.status);
|
|
return r.json();
|
|
};
|
|
|
|
API.startConform = async function (seqId, opts) {
|
|
return API.json('/api/v1/sequences/' + seqId + '/conform', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(opts),
|
|
});
|
|
};
|
|
|
|
API.getJob = async function (jobId) {
|
|
return API.json('/api/v1/jobs/' + jobId);
|
|
};
|
|
|
|
API.batchTrim = async function (clips) {
|
|
return API.json('/api/v1/assets/batch-trim', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ clips }),
|
|
});
|
|
};
|
|
|
|
window.API = API;
|
|
})();
|