UXP v2.1.4: timeline.js — replace API.requestFollow with API.requestExternal for batch relink S3 downloads

This commit is contained in:
Zac Gaetano 2026-05-28 08:32:18 -04:00
parent f3a640a7c5
commit 460b590d46

View file

@ -1,14 +1,6 @@
// timeline.js — v2.1.3 // timeline.js — v2.1.4
// Reads active Premiere sequence via the real UXP premierepro DOM API. // Reads active Premiere sequence via UXP premierepro DOM API.
// // All Premiere DOM methods are synchronous. No redirect:manual (not in UXP).
// Key corrections from official docs (developer.adobe.com/premiere-pro/uxp/ppro-reference/):
// • Most Premiere API calls are SYNCHRONOUS — no await needed
// • Track items via VideoTrack.getTrackItems(TrackItemType.Clip, false) → VideoClipTrackItem[]
// (no getClipCount / getClip — those don't exist)
// • Media file path via ClipProjectItem.cast(projItem).getMediaFilePath() (not getMediaPath)
// • Relink via ClipProjectItem.cast(projItem).changeMediaFilePath(newPath, false)
// • TickTime.seconds is the float-seconds property (use directly, no ticks math needed)
// • project.getRootItem() and getActiveProject() are synchronous
(function () { (function () {
const Timeline = {}; const Timeline = {};
@ -21,27 +13,19 @@
} }
// ── Read active sequence ───────────────────────────────────────── // ── Read active sequence ─────────────────────────────────────────
// Returns { sequenceName, frameRate, width, height, clips[] }
// clips: { fileName, filePath, trackIndex,
// timelineInSec, timelineOutSec, sourceInSec, sourceOutSec }
Timeline.readActiveSequence = async function () { Timeline.readActiveSequence = async function () {
const P = ppro(); const P = ppro();
const project = P.Project.getActiveProject(); // sync const project = P.Project.getActiveProject(); // sync
if (!project) throw new Error('No active Premiere project'); if (!project) throw new Error('No active Premiere project');
const seq = project.getActiveSequence(); // sync const seq = project.getActiveSequence(); // sync
if (!seq) throw new Error('No active sequence'); if (!seq) throw new Error('No active sequence');
const name = seq.name || 'Sequence 1'; const name = seq.name || 'Sequence 1';
const settings = seq.getSettings(); // sync const settings = seq.getSettings(); // sync
// SequenceSettings.videoFrameRate is a FrameRate object with .seconds property
// .seconds on FrameRate = 1/fps (duration of one frame in seconds)
let frameRate = 29.97; let frameRate = 29.97;
try { try {
const fr = settings.videoFrameRate; const fr = settings.videoFrameRate;
if (fr && typeof fr.seconds === 'number' && fr.seconds > 0) { if (fr && typeof fr.seconds === 'number' && fr.seconds > 0) frameRate = 1 / fr.seconds;
frameRate = 1 / fr.seconds;
}
} catch (_) {} } catch (_) {}
const width = settings.videoFrameWidth || 1920; const width = settings.videoFrameWidth || 1920;
const height = settings.videoFrameHeight || 1080; const height = settings.videoFrameHeight || 1080;
@ -52,44 +36,28 @@
for (let ti = 0; ti < trackCount; ti++) { for (let ti = 0; ti < trackCount; ti++) {
const track = seq.getVideoTrack(ti); // sync const track = seq.getVideoTrack(ti); // sync
if (!track) continue; if (!track) continue;
// getTrackItems(TrackItemType, includeEmptyItems) → VideoClipTrackItem[]
let items = []; let items = [];
try { try { items = track.getTrackItems(1, false); } catch (_) { continue; }
// Constants.TrackItemType.Clip = 1
items = track.getTrackItems(1, false);
} catch (_) { continue; }
if (!items || !items.length) continue; if (!items || !items.length) continue;
for (const clip of items) { for (const clip of items) {
try { try {
const projItem = clip.getProjectItem(); // sync const projItem = clip.getProjectItem(); // sync
if (!projItem) continue; if (!projItem) continue;
// Cast to ClipProjectItem to get file path
let filePath = ''; let filePath = '';
try { try {
const clipItem = P.ClipProjectItem.cast(projItem); const clipItem = P.ClipProjectItem.cast(projItem);
filePath = clipItem.getMediaFilePath() || ''; // sync filePath = clipItem.getMediaFilePath() || ''; // sync
} catch (_) {} } catch (_) {}
const fileName = clip.getName() || (filePath ? path.basename(filePath) : 'clip');
const fileName = clip.getName() || path.basename(filePath) || 'clip'; const tlIn = clip.getStartTime().seconds;
const tlOut = clip.getEndTime().seconds;
// TickTime.seconds is directly available as a number property const srcIn = clip.getInPoint().seconds;
const tlIn = clip.getStartTime().seconds; // timeline start const srcOut = clip.getOutPoint().seconds;
const tlOut = clip.getEndTime().seconds; // timeline end
const srcIn = clip.getInPoint().seconds; // source in
const srcOut= clip.getOutPoint().seconds; // source out
clips.push({ clips.push({
fileName, fileName, filePath, trackIndex: ti,
filePath, timelineInSec: tlIn, timelineOutSec: tlOut,
trackIndex: ti, sourceInSec: srcIn, sourceOutSec: srcOut,
timelineInSec: tlIn,
timelineOutSec: tlOut,
sourceInSec: srcIn,
sourceOutSec: srcOut,
// frame equivalents for FCP XML / MAM clip push
timelineInFrames: Math.round(tlIn * frameRate), timelineInFrames: Math.round(tlIn * frameRate),
timelineOutFrames: Math.round(tlOut * frameRate), timelineOutFrames: Math.round(tlOut * frameRate),
sourceInFrames: Math.round(srcIn * frameRate), sourceInFrames: Math.round(srcIn * frameRate),
@ -98,7 +66,6 @@
} catch (_) {} } catch (_) {}
} }
} }
return { sequenceName: name, frameRate, width, height, clips }; return { sequenceName: name, frameRate, width, height, clips };
}; };
@ -117,57 +84,41 @@
} else { } else {
UI.setHidden('#seq-info-bar', true); UI.setHidden('#seq-info-bar', true);
} }
} catch (_) { } catch (_) { UI.setHidden('#seq-info-bar', true); }
UI.setHidden('#seq-info-bar', true);
}
}; };
// ── FCP XML generation ─────────────────────────────────────────── // ── FCP XML ──────────────────────────────────────────────────────
Timeline.generateFcpXml = function (timelineData) { Timeline.generateFcpXml = function (timelineData) {
const seqName = UI.escapeXml(timelineData.sequenceName || 'Sequence 1'); const seqName = UI.escapeXml(timelineData.sequenceName || 'Sequence 1');
const frameRate = timelineData.frameRate || 29.97; const frameRate = timelineData.frameRate || 29.97;
const width = timelineData.width || 1920; const width = timelineData.width || 1920;
const height = timelineData.height || 1080; const height = timelineData.height || 1080;
const clips = timelineData.clips || []; const clips = timelineData.clips || [];
let totalFrames = 0; let totalFrames = 0;
clips.forEach(c => { if ((c.timelineOutFrames||0) > totalFrames) totalFrames = c.timelineOutFrames; }); clips.forEach(c => { if ((c.timelineOutFrames||0) > totalFrames) totalFrames = c.timelineOutFrames; });
if (totalFrames < 1) totalFrames = 100; if (totalFrames < 1) totalFrames = 100;
const duration = UI.timecodeFromFrames(totalFrames, frameRate); const duration = UI.timecodeFromFrames(totalFrames, frameRate);
const frateStr = UI.formatFrameRate(frameRate); const frateStr = UI.formatFrameRate(frameRate);
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE fcpxml>\n<fcpxml version="1.10">\n <resources>\n';
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE fcpxml>\n<fcpxml version="1.10">\n';
xml += ' <resources>\n';
xml += ' <format id="r0" frameDuration="' + frateStr + '" width="' + width + '" height="' + height + '"/>\n'; xml += ' <format id="r0" frameDuration="' + frateStr + '" width="' + width + '" height="' + height + '"/>\n';
const seen = {}; let rid = 1; const seen = {}; let rid = 1;
clips.forEach(clip => { clips.forEach(clip => {
const key = clip.filePath || clip.fileName || 'c' + rid; const key = clip.filePath || clip.fileName || 'c' + rid;
if (!seen[key]) { if (!seen[key]) {
seen[key] = 'r' + rid; seen[key] = 'r' + rid;
const srcDur = UI.timecodeFromFrames( const srcDur = UI.timecodeFromFrames(Math.max(1,(clip.sourceOutFrames||100)-(clip.sourceInFrames||0)), frameRate);
Math.max(1, (clip.sourceOutFrames||100) - (clip.sourceInFrames||0)), frameRate xml += ' <asset id="r' + rid + '" name="' + UI.escapeXml(clip.fileName||'Clip') + '" src="' + UI.escapeXml(clip.filePath||'') + '" duration="' + srcDur + '" start="0s" format="r0"/>\n';
);
xml += ' <asset id="r' + rid + '" name="' + UI.escapeXml(clip.fileName||'Clip') + '"'
+ ' src="' + UI.escapeXml(clip.filePath||'') + '"'
+ ' duration="' + srcDur + '" start="0s" format="r0"/>\n';
rid++; rid++;
} }
}); });
xml += ' </resources>\n <library>\n <event name="Conform Export">\n <project name="' + seqName + '">\n'; xml += ' </resources>\n <library>\n <event name="Conform Export">\n <project name="' + seqName + '">\n <sequence duration="' + duration + '" format="r0">\n <spine>\n';
xml += ' <sequence duration="' + duration + '" format="r0">\n <spine>\n';
clips.forEach(clip => { clips.forEach(clip => {
const resId = seen[clip.filePath || clip.fileName || 'x'] || 'r1'; const resId = seen[clip.filePath || clip.fileName || 'x'] || 'r1';
const off = UI.timecodeFromFrames(clip.timelineInFrames||0, frameRate); const off = UI.timecodeFromFrames(clip.timelineInFrames||0, frameRate);
const dur = UI.timecodeFromFrames(Math.max(1,(clip.timelineOutFrames||1)-(clip.timelineInFrames||0)), frameRate); const dur = UI.timecodeFromFrames(Math.max(1,(clip.timelineOutFrames||1)-(clip.timelineInFrames||0)), frameRate);
const srcIn = UI.timecodeFromFrames(clip.sourceInFrames||0, frameRate); const srcIn = UI.timecodeFromFrames(clip.sourceInFrames||0, frameRate);
xml += ' <clip name="' + UI.escapeXml(clip.fileName||'Clip') + '"' xml += ' <clip name="' + UI.escapeXml(clip.fileName||'Clip') + '" offset="' + off + '" duration="' + dur + '" start="' + srcIn + '">\n <asset-clip ref="' + resId + '"/>\n </clip>\n';
+ ' offset="' + off + '" duration="' + dur + '" start="' + srcIn + '">\n';
xml += ' <asset-clip ref="' + resId + '"/>\n </clip>\n';
}); });
xml += ' </spine>\n </sequence>\n </project>\n </event>\n </library>\n</fcpxml>'; xml += ' </spine>\n </sequence>\n </project>\n </event>\n </library>\n</fcpxml>';
return xml; return xml;
}; };
@ -177,31 +128,21 @@
const resolved = Library.resolveClipsToAssets(timelineData.clips || []); const resolved = Library.resolveClipsToAssets(timelineData.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'); if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first');
const seqs = await API.listSequences(projectId); const seqs = await API.listSequences(projectId);
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, { await API.updateSequence(existing.id, { frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height });
frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height,
});
seqId = existing.id; seqId = existing.id;
} else { } else {
const created = await API.createSequence( const created = await API.createSequence(projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height);
projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height
);
seqId = created.id; seqId = created.id;
} }
await API.pushClips(seqId, matched.map(c => ({
const payload = matched.map(c => ({ asset_id: c.asset_id, track: c.trackIndex,
asset_id: c.asset_id, timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames,
track: c.trackIndex, source_in_frames: c.sourceInFrames, source_out_frames: c.sourceOutFrames,
timeline_in_frames: c.timelineInFrames, })));
timeline_out_frames: c.timelineOutFrames,
source_in_frames: c.sourceInFrames,
source_out_frames: c.sourceOutFrames,
}));
await API.pushClips(seqId, payload);
return { seqId, matched: matched.length, skipped: resolved.length - matched.length }; return { seqId, matched: matched.length, skipped: resolved.length - matched.length };
}; };
@ -210,12 +151,7 @@
const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData); const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData);
const resMap = { '1080p':[1920,1080], '720p':[1280,720], 'uhd':[3840,2160], 'source':[0,0] }; const resMap = { '1080p':[1920,1080], '720p':[1280,720], 'uhd':[3840,2160], 'source':[0,0] };
const [w, h] = resMap[opts.resolution] || [1920,1080]; const [w, h] = resMap[opts.resolution] || [1920,1080];
const job = await API.startConform(seqId, { const job = await API.startConform(seqId, { codec: opts.codec, quality: opts.quality, width: w||undefined, height: h||undefined, audio: opts.audio, fcp_xml: Timeline.generateFcpXml(timelineData) });
codec: opts.codec, quality: opts.quality,
width: w || undefined, height: h || undefined,
audio: opts.audio,
fcp_xml: Timeline.generateFcpXml(timelineData),
});
return job.jobId || job.id; return job.jobId || job.id;
}; };
@ -224,22 +160,17 @@
try { try {
const job = await API.getJob(jobId); const job = await API.getJob(jobId);
onProgress(job.progress || 0, job.status); onProgress(job.progress || 0, job.status);
if (job.status === 'completed' || job.status === 'failed') { if (job.status === 'completed' || job.status === 'failed') { clearInterval(timer); onDone(job); }
clearInterval(timer);
onDone(job);
}
} catch (_) {} } catch (_) {}
}, 2000); }, 2000);
return timer; return timer;
}; };
// ── Batch Hi-Res Relink ────────────────────────────────────────── // ── Batch Hi-Res Relink ──────────────────────────────────────────
// selectedClips: resolved clip objects with asset_id + filePath
Timeline.batchRelink = async function (selectedClips) { Timeline.batchRelink = async function (selectedClips) {
const P = ppro(); const P = ppro();
const project = P.Project.getActiveProject(); // sync const project = P.Project.getActiveProject(); // sync
if (!project) throw new Error('No active Premiere project'); if (!project) throw new Error('No active Premiere project');
const results = { succeeded: 0, failed: 0, errors: [] }; const results = { succeeded: 0, failed: 0, errors: [] };
for (const clip of selectedClips) { for (const clip of selectedClips) {
@ -251,12 +182,13 @@
const dest = await Import._tempPath(safeName); const dest = await Import._tempPath(safeName);
UI.showProgress('Downloading ' + safeName + '…', 30); UI.showProgress('Downloading ' + safeName + '…', 30);
const r = await API.requestFollow(info.url, {}); // S3 presigned URL — use requestExternal (no auth header, no redirect:manual)
const r = await API.requestExternal(info.url);
if (!r.ok) throw new Error('Download HTTP ' + r.status); if (!r.ok) throw new Error('Download HTTP ' + r.status);
await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 30 + (received / total) * 50 : 30; UI.showProgress('Writing…', 60);
UI.showProgress(UI.formatBytes(received) + '…', pct); const buf = await r.arrayBuffer();
}); await Import._writeBuffer(dest, buf);
UI.showProgress('Relinking ' + clip.fileName + '…', 85); UI.showProgress('Relinking ' + clip.fileName + '…', 85);
await Timeline._relinkInProject(project, clip.filePath, dest); await Timeline._relinkInProject(project, clip.filePath, dest);
@ -269,58 +201,38 @@
return results; return results;
}; };
// Walk project items and changeMediaFilePath for items matching oldPath. // Walk project items and relink via ClipProjectItem.changeMediaFilePath()
// Uses ClipProjectItem.cast() + changeMediaFilePath() per official docs.
Timeline._relinkInProject = async function (project, oldPath, newPath) { Timeline._relinkInProject = async function (project, oldPath, newPath) {
const P = ppro(); const P = ppro();
const root = project.getRootItem(); // sync (FolderItem) const root = project.getRootItem(); // sync (FolderItem)
let count = 0; let count = 0;
function walkSync(folderItem) { function walkSync(folderItem) {
// FolderItem has no getItems() in docs — use project.getSequences() / rootItem children
// Actually FolderItem IS a ProjectItem; children accessible via getItems on the project
// Try both known APIs gracefully
let children = []; let children = [];
try { try { if (typeof folderItem.getItems === 'function') children = folderItem.getItems() || []; } catch (_) {}
// Some UXP builds expose children on FolderItem directly
if (typeof folderItem.getItems === 'function') {
children = folderItem.getItems() || [];
}
} catch (_) {}
for (const item of children) { for (const item of children) {
try { try {
const clipItem = P.ClipProjectItem.cast(item); const clipItem = P.ClipProjectItem.cast(item);
const mp = clipItem.getMediaFilePath(); const mp = clipItem.getMediaFilePath();
if (mp && mp === oldPath) { if (mp && mp === oldPath) { clipItem.changeMediaFilePath(newPath, false); count++; }
clipItem.changeMediaFilePath(newPath, false);
count++;
}
} catch (_) { } catch (_) {
// cast fails for FolderItems — try to recurse
try { walkSync(item); } catch (__) {} try { walkSync(item); } catch (__) {}
} }
} }
} }
walkSync(root); walkSync(root);
// If walkSync found nothing (getItems not on FolderItem in this build), // Fallback: findItemsMatchingMediaPath
// fall back to findItemsMatchingMediaPath on root
if (count === 0) { if (count === 0) {
try { try {
const matches = P.ClipProjectItem.cast(root).findItemsMatchingMediaPath(oldPath, true); const matches = P.ClipProjectItem.cast(root).findItemsMatchingMediaPath(oldPath, true);
for (const item of (matches || [])) { for (const item of (matches || [])) {
try { try { P.ClipProjectItem.cast(item).changeMediaFilePath(newPath, false); count++; } catch (_) {}
const clipItem = P.ClipProjectItem.cast(item);
clipItem.changeMediaFilePath(newPath, false);
count++;
} catch (_) {}
} }
} catch (_) {} } catch (_) {}
} }
if (count === 0) throw new Error('No matching clips found for path: ' + (oldPath || 'unknown')); if (count === 0) throw new Error('No matching clips found for: ' + (oldPath || 'unknown'));
return count; return count;
}; };