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)
}