fix(panel): conform auto-resolves target project from clip assets
Conform was pushing clips to whatever project the operator picked in
#conform-proj-select. mam-api's PUT /sequences/:id/clips requires every clip
asset to belong to the sequence's project, so picking the "wrong" project (or
a timeline whose proxies were imported under a different project) produced a
silent HTTP 400 ("All clip assets must belong to the sequence's project") that
surfaced only as "clip push HTTP 400".
pushToMAM now looks up each matched asset's real project_id and:
- if all matched assets share ONE project, conforms into THAT project
(overriding the picked one) so the common "wrong project picked" case
just works;
- if the timeline genuinely mixes projects, throws a clear, actionable
error naming the clips per project instead of a generic 400.
Pure editor/conform-path change. Does NOT touch capture, recorders, the
deltacast bridge, framecache, or node-agent.
This commit is contained in:
parent
7a08b90ce7
commit
34809e9337
1 changed files with 51 additions and 4 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
// timeline.js — v2.1.6
|
// timeline.js — v2.2.0
|
||||||
// premierepro API: docs say sync, runtime returns Promises. Await everything.
|
// premierepro API: docs say sync, runtime returns Promises. Await everything.
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|
@ -195,19 +195,66 @@
|
||||||
return out.join('\n');
|
return out.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve the MAM project that actually owns the matched clip assets.
|
||||||
|
// mam-api requires every clip in PUT /sequences/:id/clips to belong to the
|
||||||
|
// sequence's project; pushing to the "wrong" project 400s. Rather than trust
|
||||||
|
// the operator's picked project, ask the server which project each asset
|
||||||
|
// lives in and reconcile:
|
||||||
|
// • all assets in one project → return that project (authoritative).
|
||||||
|
// • assets span >1 project → throw a clear, per-project breakdown.
|
||||||
|
// requestedProjectId is only used as a tie-break hint / for the error text.
|
||||||
|
Timeline._resolveClipProject = async function (matched, requestedProjectId) {
|
||||||
|
const ids = [...new Set(matched.map(c => c.asset_id).filter(Boolean))];
|
||||||
|
// Map asset_id → project_id (+ display name for error messages).
|
||||||
|
const byProject = {}; // projectId → [{id, name}]
|
||||||
|
for (const id of ids) {
|
||||||
|
let asset;
|
||||||
|
try { asset = await API.getAsset(id); }
|
||||||
|
catch (e) { throw new Error('Could not look up asset ' + id + ': ' + e.message); }
|
||||||
|
const pid = asset && (asset.project_id || asset.projectId);
|
||||||
|
if (!pid) throw new Error('Asset "' + ((asset && (asset.display_name || asset.filename)) || id) + '" has no project — re-import it before conforming.');
|
||||||
|
(byProject[pid] = byProject[pid] || []).push({
|
||||||
|
id, name: (asset.display_name || asset.filename || id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectIds = Object.keys(byProject);
|
||||||
|
if (projectIds.length === 0) throw new Error('No resolvable clip assets to conform.');
|
||||||
|
|
||||||
|
if (projectIds.length === 1) return projectIds[0];
|
||||||
|
|
||||||
|
// Genuinely mixed timeline — build an actionable message.
|
||||||
|
const lines = projectIds.map(pid => {
|
||||||
|
const names = byProject[pid].map(a => a.name).slice(0, 4);
|
||||||
|
const more = byProject[pid].length > 4 ? ' +' + (byProject[pid].length - 4) + ' more' : '';
|
||||||
|
return '• project ' + pid + ': ' + names.join(', ') + more;
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
'This timeline mixes clips from ' + projectIds.length + ' different MAM projects, ' +
|
||||||
|
'so it cannot conform into one sequence:\n' + lines.join('\n') +
|
||||||
|
'\nKeep each conform to clips from a single project (or re-import the sources into one project).'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ── Push Timeline to MAM ─────────────────────────────────────────
|
// ── Push Timeline to MAM ─────────────────────────────────────────
|
||||||
Timeline.pushToMAM = async function (seqName, projectId, td) {
|
Timeline.pushToMAM = async function (seqName, projectId, td) {
|
||||||
const resolved = Library.resolveClipsToAssets(td.clips || []);
|
const resolved = Library.resolveClipsToAssets(td.clips || []);
|
||||||
const matched = resolved.filter(c => c.asset_id);
|
const matched = resolved.filter(c => c.asset_id);
|
||||||
if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first so the plugin can map file paths to asset IDs');
|
if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first so the plugin can map file paths to asset IDs');
|
||||||
const seqs = await API.listSequences(projectId);
|
|
||||||
|
// Conform into the project that actually owns the clip assets. This makes
|
||||||
|
// the common "operator picked the wrong project" case just work, and turns
|
||||||
|
// a genuinely mixed timeline into a clear error instead of an opaque 400.
|
||||||
|
const effProjectId = await Timeline._resolveClipProject(matched, projectId);
|
||||||
|
|
||||||
|
const seqs = await API.listSequences(effProjectId);
|
||||||
let seqId;
|
let seqId;
|
||||||
const existing = seqs.find(s => s.name === seqName);
|
const existing = seqs.find(s => s.name === seqName);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await API.updateSequence(existing.id, { frame_rate: td.frameRate, width: td.width, height: td.height });
|
await API.updateSequence(existing.id, { frame_rate: td.frameRate, width: td.width, height: td.height });
|
||||||
seqId = existing.id;
|
seqId = existing.id;
|
||||||
} else {
|
} else {
|
||||||
const created = await API.createSequence(projectId, seqName, td.frameRate, td.width, td.height);
|
const created = await API.createSequence(effProjectId, seqName, td.frameRate, td.width, td.height);
|
||||||
seqId = created.id;
|
seqId = created.id;
|
||||||
}
|
}
|
||||||
await API.pushClips(seqId, matched.map(c => ({
|
await API.pushClips(seqId, matched.map(c => ({
|
||||||
|
|
@ -215,7 +262,7 @@
|
||||||
timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames,
|
timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames,
|
||||||
source_in_frames: c.sourceInFrames, source_out_frames: c.sourceOutFrames,
|
source_in_frames: c.sourceInFrames, source_out_frames: c.sourceOutFrames,
|
||||||
})));
|
})));
|
||||||
return { seqId, matched: matched.length, skipped: resolved.length - matched.length };
|
return { seqId, projectId: effProjectId, matched: matched.length, skipped: resolved.length - matched.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Conform ──────────────────────────────────────────────────────
|
// ── Conform ──────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue