use anyhow::{Context, Result, anyhow}; use log::*; use std::io::Write; use std::process::{Command, Stdio, Child}; use which::which; use rusqlite::Connection; use crate::meta_plugin::MetaPlugin; pub struct MetaPluginProgram { pub program: String, pub args: Vec, pub supported: bool, pub meta_name: String, pub split_whitespace: bool, process: Option, writer: Option>, item_id: Option, result: Option, outputs: std::collections::HashMap, } impl std::fmt::Debug for MetaPluginProgram { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MetaPluginProgram") .field("program", &self.program) .field("args", &self.args) .field("supported", &self.supported) .field("meta_name", &self.meta_name) .field("split_whitespace", &self.split_whitespace) .field("process", &self.process) .field("writer", &"Box") .field("outputs", &self.outputs) .finish() } } impl MetaPluginProgram { pub fn new( program: &str, args: Vec<&str>, meta_name: String, split_whitespace: bool, options: Option>, outputs: Option>, ) -> MetaPluginProgram { let program_path = which(program); let supported = program_path.is_ok(); // Start with default outputs let mut final_outputs = std::collections::HashMap::new(); final_outputs.insert(meta_name.clone(), serde_yaml::Value::String(meta_name.clone())); if let Some(outs) = outputs { for (key, value) in outs { final_outputs.insert(key, value); } } MetaPluginProgram { program: program_path.map_or_else(|_| program.to_string(), |p| p.to_string_lossy().to_string()), args: args.iter().map(|s| s.to_string()).collect(), supported, meta_name, split_whitespace, process: None, writer: None, item_id: None, result: None, outputs: final_outputs, } } pub fn new_simple(program: &str, args: Vec<&str>, meta_name: String, split_whitespace: bool) -> MetaPluginProgram { Self::new(program, args, meta_name, split_whitespace, None, None) } } impl MetaPlugin for MetaPluginProgram { fn is_supported(&self) -> bool { self.supported } fn is_internal(&self) -> bool { false } fn initialize(&mut self, _conn: &rusqlite::Connection, item_id: i64) -> Result<()> { debug!("META: Initializing program plugin: {:?}", self); // Store item ID for later use self.item_id = Some(item_id); let program = self.program.clone(); let args = self.args.clone(); debug!("META: Executing command: {:?} {:?}", program, args); let mut process = Command::new(program.clone()) .args(args.clone()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .context(anyhow!( "Problem spawning child process: {:?} {:?}", program, args ))?; let stdin = process.stdin.take().unwrap(); self.writer = Some(Box::new(stdin)); self.process = Some(process); Ok(()) } fn finalize(&mut self, conn: &Connection) -> Result<()> { debug!("META: Finalizing program plugin"); if let Some(process) = self.process.take() { // Close stdin to signal end of input drop(self.writer.take()); // Wait for the process to complete let output = process.wait_with_output()?; if output.status.success() { // Process the output let output_str = String::from_utf8_lossy(&output.stdout); let result = if self.split_whitespace { output_str.split_whitespace().next().unwrap_or("").to_string() } else { output_str.trim().to_string() }; if !result.is_empty() { debug!("META: Program output: {}", result); self.result = Some(result); // Save the result to database if we have item_id if let Some(item_id) = self.item_id { let _ = self.save_meta(conn, item_id, &self.meta_name.clone(), self.result.clone().unwrap()); } } } else { debug!("META: Program failed with status: {:?}", output.status); let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.is_empty() { debug!("META: Program stderr: {}", stderr); } } } Ok(()) } fn update(&mut self, data: &[u8], _conn: &Connection) { if let Some(ref mut writer) = self.writer { if let Err(e) = writer.write_all(data) { debug!("META: Failed to write to process stdin: {}", e); } } } fn meta_name(&mut self) -> String { self.meta_name.clone() } fn program_info(&self) -> Option<(&str, Vec<&str>)> { if self.supported { Some((&self.program, self.args.iter().map(|s| s.as_str()).collect())) } else { None } } fn outputs(&self) -> &std::collections::HashMap { &self.outputs } fn outputs_mut(&mut self) -> &mut std::collections::HashMap { &mut self.outputs } fn default_outputs(&self) -> Vec { vec![self.meta_name.clone()] } fn default_options(&self) -> std::collections::HashMap { std::collections::HashMap::new() } }