app
Premiere Pro Bridge
Public Made by Adomby adom
Let the AI edit video in Adobe Premiere Pro on your desktop (import, cut, trim, razor, effects, audio, previews, export) with no mouse or keyboard. Check adom-bridge bridge_list for 'premiere'; install with bridge_install + adom-wiki pkg install adom/premiere-bridge, then start with premiere_status.
main
anastasia
Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest
32fb583
10d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
//! Everything that runs on the bridge side without the UXP plugin: finding,
//! launching and quitting Premiere, installing the plugin, connection state,
//! and the read-only status / readiness / describe verbs.
use crate::{catalog, plugin_pkg, Shared, PREFIX, PROTOCOL, VERSION};
use serde_json::{json, Value};
use std::{
path::{Path, PathBuf},
time::Duration,
};
use tokio::process::Command;
pub fn home_dir() -> PathBuf {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir)
}
/// User state lives under ~/.adom/premiere-bridge — never beside the executable
/// (on macOS that would break the Adom Bridge.app code seal).
pub fn data_dir() -> PathBuf {
home_dir().join(".adom").join("premiere-bridge")
}
pub fn load_or_create_token(dir: &Path) -> String {
let path = dir.join("token");
if let Ok(t) = std::fs::read_to_string(&path) {
let t = t.trim().to_string();
if t.len() >= 32 {
return t;
}
}
use rand::Rng;
let token: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(48)
.map(char::from)
.collect();
let _ = std::fs::write(&path, &token);
restrict_permissions(&path);
token
}
#[cfg(unix)]
fn restrict_permissions(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
#[cfg(not(unix))]
fn restrict_permissions(_: &Path) {}
/// The UXP plugin reads this to find us. It re-reads it on every reconnect attempt,
/// so a port change after an Adom Bridge restart heals by itself.
pub fn write_connection_file(st: &Shared) {
let path = st.data_dir.join("connection.json");
let body = json!({
"port": st.port,
"token": st.token,
"url": format!("ws://127.0.0.1:{}/ws?token={}", st.port, st.token),
"bridgeVersion": VERSION,
"protocol": PROTOCOL,
"pid": std::process::id(),
});
let _ = std::fs::write(&path, serde_json::to_vec_pretty(&body).unwrap_or_default());
restrict_permissions(&path);
}
// ───────────────────────────── finding Premiere ─────────────────────────────
#[derive(Clone, Debug)]
pub struct Install {
pub year: u32,
pub app: PathBuf,
}
/// Every Premiere Pro install on this machine, newest first.
pub fn find_installs() -> Vec<Install> {
let mut out = Vec::new();
let roots: Vec<PathBuf> = if cfg!(target_os = "macos") {
vec![PathBuf::from("/Applications")]
} else if cfg!(windows) {
let pf = std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".into());
vec![PathBuf::from(pf).join("Adobe")]
} else {
vec![]
};
for root in roots {
let Ok(rd) = std::fs::read_dir(&root) else { continue };
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let Some(rest) = name.strip_prefix("Adobe Premiere Pro") else { continue };
let year: u32 = rest.trim().chars().filter(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap_or(0);
let app = if cfg!(target_os = "macos") {
e.path().join(format!("{name}.app"))
} else {
e.path().join("Adobe Premiere Pro.exe")
};
if app.exists() {
out.push(Install { year, app });
}
}
}
out.sort_by(|a, b| b.year.cmp(&a.year));
out
}
/// Main Premiere executables currently running (not crashpad / IPC helpers that
/// merely carry the app path in their argv).
pub async fn running_premiere() -> Vec<String> {
if cfg!(windows) {
let out = Command::new("tasklist")
.args(["/FO", "CSV", "/NH", "/FI", "IMAGENAME eq Adobe Premiere Pro.exe"])
.output()
.await;
return match out {
Ok(o) if String::from_utf8_lossy(&o.stdout).contains("Adobe Premiere Pro.exe") => {
vec!["Adobe Premiere Pro.exe".into()]
}
_ => vec![],
};
}
let Ok(o) = Command::new("ps").args(["-axo", "comm="]).output().await else { return vec![] };
String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| {
let base = l.rsplit('/').next().unwrap_or("");
base.starts_with("Adobe Premiere Pro") && l.contains("/Contents/MacOS/")
})
.map(str::to_string)
.collect()
}
pub async fn premiere_running() -> bool {
!running_premiere().await.is_empty()
}
// ───────────────────────────── local verbs ─────────────────────────────
/// Verbs answered here. `None` means "forward to the plugin".
pub async fn local_verb(st: &Shared, bare: &str, args: &Value) -> Option<Value> {
Some(match bare {
"describe" => catalog::describe(),
"status" => status(st).await,
"readiness" => readiness(st).await,
"launch" => launch(st, args).await,
"quit" => quit(st, args).await,
"install_plugin" => plugin_pkg::install(st, args).await,
"plugin_info" => plugin_pkg::info(st).await,
"events" => {
let since = args["sinceSeq"].as_u64().unwrap_or(0);
let (events, last) = st.recent_events(since).await;
json!({"success": true, "events": events, "lastSeq": last})
}
"wait_for_plugin" => wait_for_plugin(st, args).await,
"read_file_base64" => read_file_base64(args).await,
"preview_frame" => preview_frame(st, args).await,
"contact_sheet" => contact_sheet(st, args).await,
"list_export_presets" => list_export_presets(args).await,
"monitor_snapshot" => monitor_snapshot(st, args).await,
_ => return None,
})
}
pub async fn status(st: &Shared) -> Value {
let running = running_premiere().await;
let installs: Vec<Value> = find_installs()
.iter()
.map(|i| json!({"year": i.year, "app": i.app}))
.collect();
let hello = st.plugin_hello().await;
let cep = st.runtime_hello("cep").await;
let mut out = json!({
"success": true,
"cepHelperConnected": cep.is_some(),
"cepHelper": cep.map(|(h, _)| h),
"bridgeVersion": VERSION,
"port": st.port,
"premiereInstalled": !installs.is_empty(),
"installs": installs,
"premiereRunning": !running.is_empty(),
"runningProcesses": running,
"pluginConnected": hello.is_some(),
"pluginInstalled": plugin_pkg::installed_version().is_some(),
"pluginInstalledVersion": plugin_pkg::installed_version(),
"pluginBundledVersion": plugin_pkg::BUNDLED_VERSION,
"dataDir": st.data_dir,
});
if let Some((h, secs)) = hello {
out["plugin"] = h;
out["pluginConnectedForSec"] = json!(secs);
// Ask the plugin for a live snapshot; a hung Premiere (modal dialog) shows up here.
match st.call_plugin("ping", json!({}), Duration::from_secs(5)).await {
Ok(r) => {
out["responsive"] = json!(true);
out["live"] = r.get("result").cloned().unwrap_or(Value::Null);
}
Err(_) => out["responsive"] = json!(false),
}
}
out
}
/// Read-only, never launches or installs anything.
pub async fn readiness(st: &Shared) -> Value {
let installs = find_installs();
let running = premiere_running().await;
let connected = st.plugin_connected().await;
let installed = plugin_pkg::installed_version();
let state = if installs.is_empty() {
"no-app"
} else if connected {
"ready"
} else if installed.is_none() {
"needs-plugin"
} else if !running {
"app-not-running"
} else {
"waiting-for-plugin"
};
json!({
"success": true,
"ready": connected,
"state": state,
"installing": false,
"premiereInstalled": !installs.is_empty(),
"premiereRunning": running,
"pluginInstalled": installed.is_some(),
"pluginConnected": connected,
"lastError": Value::Null,
})
}
pub async fn not_connected_response(st: &Shared) -> Value {
let r = readiness(st).await;
let state = r["state"].as_str().unwrap_or("");
let hint = match state {
"no-app" => "Premiere Pro isn't installed on this machine. Offer to install it for the user through Creative Cloud (Adom Bridge can open the Creative Cloud app); never tell them to go download it themselves.",
"needs-plugin" => "The Adom UXP plugin isn't installed in Premiere yet. Call premiere_install_plugin (no user clicks needed), then premiere_wait_for_plugin.",
"app-not-running" => "Premiere Pro isn't running. Call premiere_launch (optionally with {project:\"/path/file.prproj\"}), then premiere_wait_for_plugin.",
_ => "Premiere is running and the plugin is installed but hasn't connected yet. Call premiere_wait_for_plugin {timeoutSec:60}. If it never connects, the plugin panel may need opening once: Window > UXP Plugins > Adom Bridge (after that it auto-starts).",
};
json!({
"success": false,
"errorCode": "plugin_not_connected",
"state": state,
"error": "The Adom plugin inside Premiere Pro is not connected, so this verb can't run yet.",
"_hint": hint,
"_next": match state {
"needs-plugin" => json!(["premiere_install_plugin", "premiere_wait_for_plugin"]),
"app-not-running" => json!(["premiere_launch", "premiere_wait_for_plugin"]),
_ => json!(["premiere_wait_for_plugin", "premiere_status"]),
},
})
}
async fn wait_for_plugin(st: &Shared, args: &Value) -> Value {
let timeout = args["timeoutSec"].as_u64().unwrap_or(90).clamp(1, 600);
let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout);
loop {
if let Some((hello, _)) = st.plugin_hello().await {
return json!({"success": true, "connected": true, "plugin": hello});
}
if tokio::time::Instant::now() >= deadline {
let mut r = not_connected_response(st).await;
r["error"] = json!(format!("plugin did not connect within {timeout}s"));
return r;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
async fn launch(st: &Shared, args: &Value) -> Value {
let installs = find_installs();
let want_year = args["year"].as_u64().map(|y| y as u32);
let Some(inst) = installs
.iter()
.find(|i| want_year.map_or(true, |y| i.year == y))
.cloned()
else {
return json!({
"success": false,
"errorCode": "premiere_not_installed",
"error": "No Adobe Premiere Pro install found.",
"installs": installs.iter().map(|i| json!({"year": i.year, "app": i.app})).collect::<Vec<_>>(),
});
};
let project = args["project"].as_str();
if premiere_running().await {
if let Some(p) = project {
if st.plugin_connected().await {
return crate::dispatch(st, "open_project", json!({"path": p})).await;
}
}
return json!({"success": true, "alreadyRunning": true, "app": inst.app});
}
let mut cmd = if cfg!(target_os = "macos") {
let mut c = Command::new("open");
// -g: launch without stealing focus unless the caller wants Premiere in front.
if !args["foreground"].as_bool().unwrap_or(true) {
c.arg("-g");
}
c.arg("-a").arg(&inst.app);
if let Some(p) = project {
c.arg(p);
}
c
} else {
let mut c = Command::new(&inst.app);
if let Some(p) = project {
c.arg(p);
}
c
};
match cmd.spawn() {
Ok(_) => {
let mut out = json!({"success": true, "launched": true, "app": inst.app, "year": inst.year});
if args["wait"].as_bool().unwrap_or(true) {
let w = wait_for_plugin(st, &json!({"timeoutSec": args["timeoutSec"].as_u64().unwrap_or(120)})).await;
out["pluginConnected"] = w["connected"].clone();
if w["connected"].as_bool() != Some(true) {
out["_hint"] = w["_hint"].clone();
}
}
out
}
Err(e) => json!({"success": false, "errorCode": "launch_failed", "error": e.to_string()}),
}
}
async fn quit(st: &Shared, args: &Value) -> Value {
if !premiere_running().await {
return json!({"success": true, "wasRunning": false});
}
if args["save"].as_bool().unwrap_or(false) && st.plugin_connected().await {
let _ = st.call_plugin("save_project", json!({}), Duration::from_secs(60)).await;
}
let res = if cfg!(target_os = "macos") {
let mut names: Vec<String> = running_premiere()
.await
.iter()
.filter_map(|p| p.rsplit('/').next().map(str::to_string))
.collect();
names.dedup();
let script = names
.iter()
.map(|n| format!("tell application \"{n}\" to quit"))
.collect::<Vec<_>>()
.join("\n");
Command::new("osascript").arg("-e").arg(script).output().await
} else {
let mut c = Command::new("taskkill");
c.args(["/IM", "Adobe Premiere Pro.exe"]);
if args["force"].as_bool().unwrap_or(false) {
c.arg("/F");
}
c.output().await
};
match res {
Ok(o) if o.status.success() => json!({"success": true, "quitRequested": true}),
Ok(o) => json!({"success": false, "errorCode": "quit_failed", "error": String::from_utf8_lossy(&o.stderr)}),
Err(e) => json!({"success": false, "errorCode": "quit_failed", "error": e.to_string()}),
}
}
/// Inline a file the plugin wrote (preview frame, thumbnail) so the AI can look at it
/// without a separate pull_file round-trip. Capped to keep responses sane.
async fn read_file_base64(args: &Value) -> Value {
use base64::Engine;
let Some(path) = args["path"].as_str() else {
return json!({"success": false, "errorCode": "bad_args", "error": "path is required"});
};
let max = args["maxBytes"].as_u64().unwrap_or(8 * 1024 * 1024);
wait_stable(path, Duration::from_secs(10)).await;
match tokio::fs::read(path).await {
Ok(bytes) if (bytes.len() as u64) <= max => json!({
"success": true,
"path": path,
"bytes": bytes.len(),
"base64": base64::engine::general_purpose::STANDARD.encode(&bytes),
}),
Ok(bytes) => json!({
"success": false,
"errorCode": "too_large",
"error": format!("{} bytes > maxBytes {max}", bytes.len()),
"_hint": "Use adom-bridge pull_file to fetch big files into the container.",
}),
Err(e) => json!({"success": false, "errorCode": "read_failed", "error": e.to_string()}),
}
}
#[allow(dead_code)]
pub fn prefixed(bare: &str) -> String {
format!("{PREFIX}{bare}")
}
pub async fn cep_not_connected_response(st: &Shared) -> Value {
let uxp = st.plugin_connected().await;
json!({
"success": false,
"errorCode": "cep_not_connected",
"error": "This verb runs in the Adom CEP helper (ExtendScript/QE), which isn't connected.",
"_hint": if uxp {
"The UXP plugin is up but the CEP helper isn't. Run premiere_install_plugin (installs the helper + enables unsigned CEP), then restart Premiere (premiere_quit {save:true}, premiere_launch). It starts invisibly with Premiere."
} else {
"Neither in-app plugin is connected. premiere_readiness says what's missing."
},
"_next": ["premiere_install_plugin", "premiere_quit", "premiere_launch"],
})
}
pub fn previews_dir(st: &Shared) -> PathBuf {
let d = st.data_dir.join("previews");
let _ = std::fs::create_dir_all(&d);
d
}
/// Bridge-side defaults so the AI rarely has to invent desktop paths.
pub fn fill_defaults(st: &Shared, bare: &str, mut args: Value) -> Value {
if !args.is_object() {
args = json!({});
}
match bare {
"export_frame" | "export_frames" if args.get("dir").is_none() => {
args["dir"] = json!(previews_dir(st));
}
"export_frame_qe" if args.get("path").is_none() => {
let t = args["time"].as_f64().unwrap_or(0.0);
args["path"] = json!(previews_dir(st).join(format!("qe_{}ms.png", (t * 1000.0) as i64)));
}
_ => {}
}
args
}
/// Premiere writes exported frames asynchronously after exportSequenceFrame resolves:
/// wait until the file exists and its size holds still before reading it.
async fn wait_stable(path: &str, timeout: Duration) -> bool {
let deadline = tokio::time::Instant::now() + timeout;
let mut last: Option<u64> = None;
loop {
let size = tokio::fs::metadata(path).await.map(|m| m.len()).ok();
if let (Some(a), Some(b)) = (last, size) {
if a == b && b > 0 {
return true;
}
}
last = size;
if tokio::time::Instant::now() >= deadline {
return size.unwrap_or(0) > 0;
}
tokio::time::sleep(Duration::from_millis(300)).await;
}
}
fn encode_file(path: &str) -> Option<(String, usize)> {
use base64::Engine;
let bytes = std::fs::read(path).ok()?;
Some((base64::engine::general_purpose::STANDARD.encode(&bytes), bytes.len()))
}
async fn preview_frame(st: &Shared, args: &Value) -> Value {
let mut a = args.clone();
if a.get("width").is_none() {
a["width"] = json!(960);
}
if a.get("name").is_none() {
a["name"] = json!(format!("preview_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0)));
}
let mut r = crate::dispatch(st, "export_frame", a).await;
if r["success"].as_bool() != Some(true) {
return r;
}
let path = r["path"].as_str().unwrap_or("").to_string();
wait_stable(&path, Duration::from_secs(20)).await;
if args["inline"].as_bool().unwrap_or(true) {
match encode_file(&path) {
Some((b64, n)) => {
r["image"] = json!({"mime": "image/png", "base64": b64, "bytes": n});
}
None => {
r["_hint"] = json!(format!("Frame export reported success but {path} isn't readable yet; retry premiere_read_file_base64 {{path}} in a second."));
}
}
}
r["savedTo"] = json!(path);
r
}
async fn contact_sheet(st: &Shared, args: &Value) -> Value {
let mut a = args.clone();
if a.get("width").is_none() {
a["width"] = json!(480);
}
if a.get("prefix").is_none() {
a["prefix"] = json!(format!("sheet_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)));
}
let mut r = crate::dispatch(st, "export_frames", a).await;
if r["success"].as_bool() != Some(true) {
return r;
}
if args["inline"].as_bool().unwrap_or(true) {
if let Some(frames) = r["frames"].as_array_mut() {
for f in frames.iter_mut() {
let p = f["path"].as_str().unwrap_or("").to_string();
wait_stable(&p, Duration::from_secs(20)).await;
if let Some((b64, n)) = encode_file(&p) {
f["image"] = json!({"mime": "image/png", "base64": b64, "bytes": n});
}
}
}
}
r
}
/// Walk the usual preset roots for .epr files. Depth-limited; fast enough to run per call.
async fn list_export_presets(args: &Value) -> Value {
let search = args["search"].as_str().map(|s| s.to_lowercase());
let home = home_dir();
let mut roots: Vec<PathBuf> = vec![
home.join("Documents/Adobe/Adobe Media Encoder"),
home.join("Documents/Adobe/Premiere Pro"),
];
for inst in find_installs() {
if cfg!(target_os = "macos") {
roots.push(inst.app.join("Contents/MediaIO/systempresets"));
roots.push(inst.app.join("Contents/Settings/IngestPresets"));
} else if let Some(dir) = inst.app.parent() {
roots.push(dir.join("MediaIO").join("systempresets"));
}
}
if cfg!(target_os = "macos") {
if let Ok(rd) = std::fs::read_dir("/Applications") {
for e in rd.flatten() {
let n = e.file_name().to_string_lossy().into_owned();
if n.starts_with("Adobe Media Encoder") {
roots.push(e.path().join(format!("{n}.app/Contents/MediaIO/systempresets")));
}
}
}
}
let mut found = Vec::new();
for r in &roots {
walk_epr(r, 0, &mut found);
}
let mut presets: Vec<Value> = found
.into_iter()
.filter_map(|p| {
let raw = read_preset_name(&p).unwrap_or_else(|| p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default());
let name = clean_preset_name(&raw, &p);
let format = preset_format(&p);
let hay = format!("{} {} {}", name, format.as_deref().unwrap_or(""), p.display()).to_lowercase().replace('.', "");
if search.as_ref().map_or(true, |q| hay.contains(&q.replace('.', ""))) {
Some(json!({"name": name, "format": format, "path": p}))
} else {
None
}
})
.collect();
presets.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
// Premiere and Media Encoder ship identical copies; keep the first of each name+format.
presets.dedup_by(|a, b| a["name"] == b["name"] && a["format"] == b["format"]);
let total = presets.len();
presets.truncate(args["limit"].as_u64().unwrap_or(80) as usize);
json!({"success": true, "count": total, "presets": presets, "searchedRoots": roots})
}
fn walk_epr(dir: &Path, depth: usize, out: &mut Vec<PathBuf>) {
if depth > 6 || out.len() > 5000 {
return;
}
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
walk_epr(&p, depth + 1, out);
} else if p.extension().map_or(false, |x| x.eq_ignore_ascii_case("epr")) {
out.push(p);
}
}
}
/// .epr files are XML with a <PresetName> or <Name> early on.
fn read_preset_name(p: &Path) -> Option<String> {
use std::io::Read;
let mut buf = vec![0u8; 16 * 1024];
let n = std::fs::File::open(p).ok()?.read(&mut buf).ok()?;
let s = String::from_utf8_lossy(&buf[..n]);
for tag in ["PresetName", "Name"] {
let open = format!("<{tag}>");
if let Some(i) = s.find(&open) {
let rest = &s[i + open.len()..];
if let Some(j) = rest.find('<') {
let v = rest[..j].trim();
if !v.is_empty() {
return Some(v.to_string());
}
}
}
}
None
}
/// Read-only capture of Premiere's main window via Adom Bridge (no focus change, no
/// clicks). Shows exactly what the Program Monitor renders — including animated values
/// that Exporter.exportSequenceFrame doesn't evaluate.
async fn monitor_snapshot(st: &Shared, args: &Value) -> Value {
if let Some(t) = args.get("time") {
let r = crate::dispatch(st, "set_playhead", json!({"time": t, "sequence": args["sequence"]})).await;
if r["success"].as_bool() != Some(true) {
return r;
}
tokio::time::sleep(Duration::from_millis(args["settleMs"].as_u64().unwrap_or(700))).await;
}
let list = match crate::ab::command("desktop_list_windows", json!({})).await {
Ok(v) => v,
Err(e) => return json!({"success": false, "errorCode": "ab_unreachable", "error": e,
"_hint": "This verb needs Adom Bridge's direct API (it's set when Adom Bridge spawns the bridge). Use premiere_preview_frame instead."}),
};
let wins = list["output"]["data"]["windows"].as_array().cloned().unwrap_or_default();
let win = wins
.iter()
.filter(|w| {
w["appBundleId"].as_str().map_or(false, |b| b.starts_with("com.adobe.PremierePro"))
|| w["title"].as_str().map_or(false, |t| t.contains(".prproj") || t.starts_with("Adobe Premiere Pro"))
})
.max_by_key(|w| w["rect"]["width"].as_i64().unwrap_or(0) * w["rect"]["height"].as_i64().unwrap_or(0));
let Some(win) = win else {
return json!({"success": false, "errorCode": "window_not_found", "error": "Couldn't find Premiere's main window.", "_hint": "Is Premiere running and not minimized? premiere_status."});
};
let shot = match crate::ab::command("desktop_screenshot_window", json!({"hwnd": win["hwnd"]})).await {
Ok(v) => v,
Err(e) => return json!({"success": false, "errorCode": "screenshot_failed", "error": e}),
};
let out = &shot["output"];
let image = out["data"]["image"].as_str().or(out["image"].as_str()).map(str::to_string);
json!({
"success": image.is_some() || out["data"]["safePath"].is_string(),
"window": {"hwnd": win["hwnd"], "title": win["title"]},
"image": image.map(|b| json!({"mime": "image/png", "base64": b})),
"safePath": out["data"]["safePath"].clone(),
"fullPath": out["data"]["fullPath"].clone(),
"error": if out.is_null() { shot["error"].clone() } else { Value::Null },
})
}
/// "($$$/AME/.../PresetName=H.264 MP4 Proxy)" -> "H.264 MP4 Proxy"; else the file stem.
fn clean_preset_name(raw: &str, p: &Path) -> String {
if let Some(i) = raw.find("PresetName=") {
let v = &raw[i + 11..];
let v = if raw.starts_with('(') { v.strip_suffix(')').unwrap_or(v) } else { v };
return v.trim().to_string();
}
if raw.starts_with("$$$") || raw.starts_with("($$$") {
return p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
}
raw.to_string()
}
/// systempresets/<vendor-hex>_<format-hex> — e.g. 4E49434B_48323634 = "NICK"_"H264".
fn preset_format(p: &Path) -> Option<String> {
let dir = p.parent()?.file_name()?.to_string_lossy().into_owned();
let (_, fmt) = dir.split_once('_')?;
if fmt.len() != 8 {
return None;
}
let bytes: Option<Vec<u8>> = (0..4).map(|i| u8::from_str_radix(&fmt[i * 2..i * 2 + 2], 16).ok()).collect();
let s: String = bytes?.into_iter().map(char::from).collect();
let s = s.trim().to_string();
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic() || c == ' ')).then_some(s)
}