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.
|
||||
|
||||
(function () {
|
||||
|
|
@ -195,19 +195,66 @@
|
|||
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 ─────────────────────────────────────────
|
||||
Timeline.pushToMAM = async function (seqName, projectId, td) {
|
||||
const resolved = Library.resolveClipsToAssets(td.clips || []);
|
||||
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');
|
||||
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;
|
||||
const existing = seqs.find(s => s.name === seqName);
|
||||
if (existing) {
|
||||
await API.updateSequence(existing.id, { frame_rate: td.frameRate, width: td.width, height: td.height });
|
||||
seqId = existing.id;
|
||||
} 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;
|
||||
}
|
||||
await API.pushClips(seqId, matched.map(c => ({
|
||||
|
|
@ -215,7 +262,7 @@
|
|||
timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames,
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue