UXP v2.1.0: api.js — add projects, live-path, sequences, conform, batch-trim endpoints
This commit is contained in:
parent
be57eb0a50
commit
cd18988d6d
1 changed files with 118 additions and 26 deletions
|
|
@ -1,9 +1,7 @@
|
||||||
// Dragonflight API client. Wraps UXP's fetch() with the Bearer header and
|
// Dragonflight API client v2.1.0
|
||||||
// handles the cross-origin-redirect quirk (UXP strips Authorization across
|
// Wraps UXP fetch() with Bearer auth + manual redirect follow (UXP strips
|
||||||
// hosts as a security fix — we follow such redirects manually).
|
// Authorization across origins per Adobe security policy).
|
||||||
//
|
// Persists serverUrl + apiToken in localStorage.
|
||||||
// Persists serverUrl + apiToken in localStorage so the panel reconnects on
|
|
||||||
// reopen.
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const API = {};
|
const API = {};
|
||||||
|
|
@ -31,22 +29,50 @@
|
||||||
|
|
||||||
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
||||||
|
|
||||||
// Core request. `url` may be a path (joined to serverUrl) or absolute.
|
// Core request. `urlOrPath` may be a path (/api/...) or absolute URL.
|
||||||
// Auth header is added only for requests to our own serverUrl.
|
// Bearer header added only for same-server requests.
|
||||||
API.request = async function (urlOrPath, opts) {
|
API.request = async function (urlOrPath, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
||||||
const base = trimUrl(API.state.serverUrl);
|
const base = trimUrl(API.state.serverUrl);
|
||||||
const url = isAbs ? urlOrPath : (base + urlOrPath);
|
const url = isAbs ? urlOrPath : (base + urlOrPath);
|
||||||
const headers = Object.assign({}, opts.headers || {});
|
const headers = Object.assign({}, opts.headers || {});
|
||||||
if (!isAbs || url.indexOf(base) === 0) {
|
if (!isAbs || url.startsWith(base)) {
|
||||||
if (API.state.apiToken) headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
if (API.state.apiToken) headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
||||||
}
|
}
|
||||||
// UXP fetch supports standard options. Don't pass `credentials` — UXP
|
|
||||||
// doesn't ship a cookie jar and warns about unsupported values.
|
|
||||||
return fetch(url, Object.assign({}, opts, { headers }));
|
return fetch(url, Object.assign({}, opts, { headers }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Manual redirect follow: UXP strips Authorization on cross-origin
|
||||||
|
// redirects — so we follow redirects manually and drop Bearer on hop
|
||||||
|
// to a different host (e.g., proxy URL → presigned S3).
|
||||||
|
API.requestFollow = async function (urlOrPath, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
||||||
|
const base = trimUrl(API.state.serverUrl);
|
||||||
|
let current = isAbs ? urlOrPath : (base + urlOrPath);
|
||||||
|
|
||||||
|
for (let hop = 0; hop < 6; hop++) {
|
||||||
|
const headers = Object.assign({}, opts.headers || {});
|
||||||
|
const sameOrigin = current.startsWith(base);
|
||||||
|
if (sameOrigin && API.state.apiToken) {
|
||||||
|
headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
||||||
|
} else {
|
||||||
|
delete headers['Authorization'];
|
||||||
|
}
|
||||||
|
const r = await fetch(current, Object.assign({}, opts, { headers, 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 header');
|
||||||
|
current = /^https?:\/\//i.test(loc) ? loc : new URL(loc, current).toString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error('HTTP ' + r.status);
|
||||||
|
}
|
||||||
|
throw new Error('Too many redirects');
|
||||||
|
};
|
||||||
|
|
||||||
API.json = async function (path, opts) {
|
API.json = async function (path, opts) {
|
||||||
const r = await API.request(path, opts);
|
const r = await API.request(path, opts);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|
@ -56,13 +82,12 @@
|
||||||
return r.json();
|
return r.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
// GET /api/v1/auth/me — used as the connect probe. Returns 200 on a valid
|
// ── Auth ─────────────────────────────────────────────────────────
|
||||||
// bearer, 401 otherwise.
|
|
||||||
API.connect = async function (serverUrl, apiToken) {
|
API.connect = async function (serverUrl, apiToken) {
|
||||||
API.state.serverUrl = trimUrl(serverUrl);
|
API.state.serverUrl = trimUrl(serverUrl);
|
||||||
API.state.apiToken = String(apiToken || '').trim();
|
API.state.apiToken = String(apiToken || '').trim();
|
||||||
if (!API.state.serverUrl || !API.state.apiToken) {
|
if (!API.state.serverUrl || !API.state.apiToken) {
|
||||||
throw new Error('Server URL and API token are required');
|
throw new Error('Server URL and API token required');
|
||||||
}
|
}
|
||||||
const me = await API.json('/api/v1/auth/me');
|
const me = await API.json('/api/v1/auth/me');
|
||||||
API.state.connected = true;
|
API.state.connected = true;
|
||||||
|
|
@ -70,20 +95,21 @@
|
||||||
return me;
|
return me;
|
||||||
};
|
};
|
||||||
|
|
||||||
API.disconnect = function () {
|
API.disconnect = function () { API.clear(); };
|
||||||
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());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Asset list. The web UI calls /api/v1/assets?... — we mirror that.
|
API.getAsset = async function (assetId) {
|
||||||
API.listAssets = async function (query) {
|
return API.json('/api/v1/assets/' + assetId);
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (query) params.set('q', query);
|
|
||||||
params.set('limit', '60');
|
|
||||||
return API.json('/api/v1/assets?' + params.toString());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve a proxy stream URL → returns an absolute URL we can fetch.
|
// /stream → { url, type, source }
|
||||||
// /stream returns { url: '/api/v1/assets/<id>/video', type: 'mp4', source }.
|
|
||||||
API.getProxyUrl = async function (assetId) {
|
API.getProxyUrl = async function (assetId) {
|
||||||
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
||||||
if (!data || !data.url) throw new Error('Asset has no proxy');
|
if (!data || !data.url) throw new Error('Asset has no proxy');
|
||||||
|
|
@ -92,13 +118,79 @@
|
||||||
return { url: abs, source: data.source || 'proxy' };
|
return { url: abs, source: data.source || 'proxy' };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve a hi-res URL → returns a presigned S3 URL (off-host).
|
// /hires → { url, filename, ext, file_size, type }
|
||||||
// /hires returns { url, filename, ext, file_size, type: 'hires' }.
|
|
||||||
API.getHiresInfo = async function (assetId) {
|
API.getHiresInfo = async function (assetId) {
|
||||||
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
||||||
if (!data || !data.url) throw new Error('Asset has no hi-res source');
|
if (!data || !data.url) throw new Error('Asset has no hi-res source');
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// /live-path → { win_path, posix_path, display_name }
|
||||||
|
API.getLivePath = async function (assetId) {
|
||||||
|
return API.json('/api/v1/assets/' + assetId + '/live-path');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Batch trim: POST /api/v1/assets/batch-trim
|
||||||
|
API.batchTrim = async function (clips) {
|
||||||
|
return API.json('/api/v1/assets/batch-trim', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ clips }),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 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),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Jobs ─────────────────────────────────────────────────────────
|
||||||
|
API.getJob = async function (jobId) {
|
||||||
|
return API.json('/api/v1/jobs/' + jobId);
|
||||||
|
};
|
||||||
|
|
||||||
window.API = API;
|
window.API = API;
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue