main
anastasia Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest 32fb583 10d ago
//! Calling Adom Bridge back over its loopback direct API (ADOM_DIRECT_API_URL, fallback
//! ~/.adom/direct-api-port). Used for things only Adom Bridge can do — e.g. a read-only
//! window capture of Premiere's Program Monitor. Minimal HTTP/1.1 client: one POST, JSON.

use serde_json::{json, Value};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

fn base() -> Option<String> {
    if let Ok(u) = std::env::var("ADOM_DIRECT_API_URL") {
        return Some(u.trim_end_matches('/').to_string());
    }
    let p = crate::host::home_dir().join(".adom").join("direct-api-port");
    let s = std::fs::read_to_string(p).ok()?;
    let s = s.trim();
    Some(if s.contains(':') { format!("http://{s}") } else { format!("http://127.0.0.1:{s}") })
}

pub async fn command(verb: &str, args: Value) -> Result<Value, String> {
    let base = base().ok_or("Adom Bridge direct API not found (ADOM_DIRECT_API_URL unset)")?;
    let hostport = base.strip_prefix("http://").ok_or("direct API must be http://")?.to_string();
    let body = json!({"command": verb, "args": args}).to_string();
    let token = std::env::var("ADOM_BRIDGE_TOKEN").unwrap_or_default();
    let mut req = format!(
        "POST /command HTTP/1.1\r\nHost: {hostport}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
        body.len()
    );
    if !token.is_empty() {
        req.push_str(&format!("X-Adom-Bridge-Token: {token}\r\n"));
    }
    req.push_str("\r\n");
    req.push_str(&body);
    let fut = async {
        let mut s = tokio::net::TcpStream::connect(&hostport).await.map_err(|e| e.to_string())?;
        s.write_all(req.as_bytes()).await.map_err(|e| e.to_string())?;
        let mut buf = Vec::new();
        s.read_to_end(&mut buf).await.map_err(|e| e.to_string())?;
        Ok::<Vec<u8>, String>(buf)
    };
    let buf = tokio::time::timeout(std::time::Duration::from_secs(60), fut).await.map_err(|_| "Adom Bridge timed out".to_string())??;
    let text = String::from_utf8_lossy(&buf);
    let (head, rest) = text.split_once("\r\n\r\n").ok_or("bad HTTP response")?;
    let body = if head.to_ascii_lowercase().contains("transfer-encoding: chunked") { dechunk(rest) } else { rest.to_string() };
    let mut v: Value = serde_json::from_str(&body).map_err(|e| format!("bad JSON from Adom Bridge: {e}"))?;
    // Adom Bridge returns the verb's own JSON as a string in `output`.
    if let Some(inner) = v["output"].as_str().and_then(|s| serde_json::from_str::<Value>(s).ok()) {
        v["output"] = inner;
    }
    Ok(v)
}

fn dechunk(s: &str) -> String {
    let mut out = String::new();
    let mut rest = s;
    while let Some((len, after)) = rest.split_once("\r\n") {
        let n = usize::from_str_radix(len.trim(), 16).unwrap_or(0);
        if n == 0 || after.len() < n {
            break;
        }
        out.push_str(&after[..n]);
        rest = after[n..].trim_start_matches("\r\n");
    }
    out
}