// timeline.js — v2.1.0 // Reads active Premiere sequence via UXP premierepro API. // Replaces all CSInterface.evalScript() calls that died in PPro 26. // Also: FCP XML generation, push to MAM, conform, batch hi-res relink. (function () { const Timeline = {}; // ── premierepro lazy require ──────────────────────────────────── function ppro() { if (Timeline._ppro) return Timeline._ppro; try { Timeline._ppro = require('premierepro'); } catch (e) { throw new Error('UXP premierepro unavailable: ' + e.message); } return Timeline._ppro; } // ── Read active sequence ───────────────────────────────────────── // Returns { sequenceName, frameRate, width, height, clips[] } // clips: { fileName, filePath, trackIndex, timelineInFrames, timelineOutFrames, // sourceInFrames, sourceOutFrames } Timeline.readActiveSequence = async function () { const P = ppro(); const project = await P.Project.getActiveProject(); if (!project) throw new Error('No active Premiere project'); const seq = await project.getActiveSequence(); if (!seq) throw new Error('No active sequence in the project'); const name = await seq.getName().catch(() => 'Sequence 1'); const settings = await seq.getSettings().catch(() => ({})); const frameRate = settings.videoFrameRate ? (1 / settings.videoFrameRate) : 29.97; const width = settings.videoFrameWidth || 1920; const height = settings.videoFrameHeight || 1080; // Walk video tracks const clips = []; let trackCount = 0; try { trackCount = await seq.getVideoTrackCount(); } catch (_) {} for (let ti = 0; ti < trackCount; ti++) { let track; try { track = await seq.getVideoTrack(ti); } catch (_) { continue; } let clipCount = 0; try { clipCount = await track.getClipCount(); } catch (_) {} for (let ci = 0; ci < clipCount; ci++) { try { const clip = await track.getClip(ci); const projItem = await clip.getProjectItem().catch(() => null); if (!projItem) continue; const mediaPath = await projItem.getMediaPath().catch(() => ''); const itemName = await projItem.getName().catch(() => ''); const inPoint = await clip.getInPoint().catch(() => ({ ticks: 0 })); const outPoint = await clip.getOutPoint().catch(() => ({ ticks: 0 })); const srcIn = await clip.getSourceInPoint().catch(() => ({ ticks: 0 })); const srcOut = await clip.getSourceOutPoint().catch(() => ({ ticks: 0 })); // Premiere ticks = 1/254016000000 s. Convert to frames. const TICK = 254016000000; function ticksToFrames(t) { return Math.round((t.ticks / TICK) * frameRate); } clips.push({ fileName: itemName || (mediaPath ? mediaPath.split(/[\\/]/).pop() : 'clip'), filePath: mediaPath || '', trackIndex: ti, timelineInFrames: ticksToFrames(inPoint), timelineOutFrames: ticksToFrames(outPoint), sourceInFrames: ticksToFrames(srcIn), sourceOutFrames: ticksToFrames(srcOut), }); } catch (_) {} } } return { sequenceName: name, frameRate, width, height, clips }; }; // ── Active sequence info bar ───────────────────────────────────── Timeline.refreshSeqBar = async function () { try { const P = ppro(); const project = await P.Project.getActiveProject().catch(() => null); if (!project) { UI.setHidden('#seq-info-bar', true); return; } const seq = await project.getActiveSequence().catch(() => null); if (!seq) { UI.setHidden('#seq-info-bar', true); return; } const name = await seq.getName().catch(() => ''); if (name) { document.getElementById('seq-info-name').textContent = name; UI.setHidden('#seq-info-bar', false); } else { UI.setHidden('#seq-info-bar', true); } } catch (_) { UI.setHidden('#seq-info-bar', true); } }; // ── FCP XML generation ─────────────────────────────────────────── Timeline.generateFcpXml = function (timelineData) { const seqName = UI.escapeXml(timelineData.sequenceName || 'Sequence 1'); const frameRate = timelineData.frameRate || 29.97; const width = timelineData.width || 1920; const height = timelineData.height || 1080; const clips = timelineData.clips || []; let totalFrames = 0; clips.forEach(c => { if ((c.timelineOutFrames || 0) > totalFrames) totalFrames = c.timelineOutFrames; }); if (totalFrames < 1) totalFrames = 100; const duration = UI.timecodeFromFrames(totalFrames, frameRate); const frateStr = UI.formatFrameRate(frameRate); let xml = '\n'; xml += '\n'; xml += '\n'; xml += ' \n'; xml += ' \n'; const seen = {}; let rid = 1; clips.forEach(clip => { const key = clip.filePath || clip.fileName || 'c' + rid; if (!seen[key]) { seen[key] = 'r' + rid; const srcDur = UI.timecodeFromFrames( Math.max(1, (clip.sourceOutFrames || 100) - (clip.sourceInFrames || 0)), frameRate ); xml += ' \n'; rid++; } }); xml += ' \n'; xml += ' \n \n \n'; xml += ' \n \n'; clips.forEach(clip => { const resId = seen[clip.filePath || clip.fileName || 'x'] || 'r1'; const off = UI.timecodeFromFrames(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); xml += ' \n'; xml += ' \n'; xml += ' \n'; }); xml += ' \n \n \n \n \n'; return xml; }; // ── Push Timeline to MAM ───────────────────────────────────────── Timeline.pushToMAM = async function (seqName, projectId, timelineData) { const resolved = Library.resolveClipsToAssets(timelineData.clips || []); const matched = resolved.filter(c => c.asset_id); if (matched.length === 0) throw new Error('No clips matched MAM assets — import proxies first'); // Upsert sequence const seqs = await API.listSequences(projectId); let seqId; const existing = seqs.find(s => s.name === seqName); if (existing) { await API.updateSequence(existing.id, { frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height, }); seqId = existing.id; } else { const created = await API.createSequence( projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height ); seqId = created.id; } const payload = matched.map(c => ({ asset_id: c.asset_id, track: c.trackIndex, 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 }; }; // ── Conform ────────────────────────────────────────────────────── // opts: { codec, quality, resolution, audio } // Returns jobId for polling. Timeline.startConform = async function (projectId, seqName, timelineData, opts) { // Upsert sequence to get seqId const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData); const resMap = { '1080p': [1920,1080], '720p': [1280,720], 'uhd': [3840,2160], 'source': [0,0] }; const [w, h] = resMap[opts.resolution] || [1920,1080]; 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), }); return job.jobId || job.id; }; Timeline.pollConform = function (jobId, onProgress, onDone) { let timer = setInterval(async () => { try { const job = await API.getJob(jobId); const pct = job.progress || 0; onProgress(pct, job.status); if (job.status === 'completed' || job.status === 'failed') { clearInterval(timer); onDone(job); } } catch (_) {} }, 2000); return timer; }; // ── Batch Hi-Res Relink ────────────────────────────────────────── // Takes selectedClips (array of resolved clip objects with asset_id), // downloads hi-res for each, relinks in Premiere. Timeline.batchRelink = async function (selectedClips) { const P = ppro(); const project = await P.Project.getActiveProject(); if (!project) throw new Error('No active Premiere project'); const results = { succeeded: 0, failed: 0, errors: [] }; for (const clip of selectedClips) { if (!clip.asset_id) continue; try { UI.showProgress('Fetching hi-res for ' + clip.fileName + '…', 20); const info = await API.getHiresInfo(clip.asset_id); const safeName = UI.sanitizeFilename(info.filename || clip.fileName + '.' + (info.ext || 'mxf')); const dest = await Import._tempPath(safeName); UI.showProgress('Downloading ' + safeName + '…', 30); const r = await API.requestFollow(info.url, {}); 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('Downloading ' + UI.formatBytes(received) + '…', pct); }); // Relink: walk project items, find by old path, changeMediaPath UI.showProgress('Relinking ' + clip.fileName + '…', 85); await Timeline._relinkInProject(project, clip.filePath, dest); results.succeeded++; } catch (e) { results.failed++; results.errors.push(clip.fileName + ': ' + e.message); } } return results; }; // Walk all project items and changeMediaPath for items matching oldPath. Timeline._relinkInProject = async function (project, oldPath, newPath) { const root = await project.getRootItem(); let count = 0; async function walk(item) { try { const children = await item.getItems(); for (const child of children) { try { const mp = await child.getMediaPath().catch(() => null); if (mp && mp === oldPath) { await child.changeMediaPath(newPath, true); count++; } } catch (_) {} // Recurse into bins try { const sub = await child.getItems(); if (sub && sub.length) await walk(child); } catch (_) {} } } catch (_) {} } await walk(root); if (count === 0) throw new Error('No matching clips found for ' + (oldPath || 'unknown path')); return count; }; window.Timeline = Timeline; })();