From 34809e93376fe3e9649774c21dd93ac51686c003 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 5 Jun 2026 11:51:55 -0400 Subject: [PATCH] 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. --- services/premiere-plugin-uxp/src/timeline.js | 55 ++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/services/premiere-plugin-uxp/src/timeline.js b/services/premiere-plugin-uxp/src/timeline.js index c32cf67..c8e9ff5 100644 --- a/services/premiere-plugin-uxp/src/timeline.js +++ b/services/premiere-plugin-uxp/src/timeline.js @@ -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 ──────────────────────────────────────────────────────