doe 1.1.85

doe is a powerful Rust crate designed to enhance development workflow by providing an extensive collection of useful macros and utility functions. It not only simplifies common tasks but also offers convenient features for clipboard management,robust cryptographic functions,keyboard input, and mouse interaction.
Documentation
///all exiftool utils functions, activate feature by `cargo add doe -F exiftool`
/// 
/// 
/// ```ignore
/// let metadata =
/// doe::exiftool::run(r"zsoXO.mp4").unwrap();
/// let modify_datetime = metadata.get("System:FileModifyDate").unwrap();
/// let create_datetime = metadata.get("System:FileCreateDate").unwrap();
/// use doe::date::*;
/// let modify_datetime = parse_datetime(modify_datetime).unwrap();
/// let create_datetime = parse_datetime(create_datetime).unwrap();
/// let diff = create_datetime - modify_datetime;
/// diff.num_days().println();
/// ```
/// 
/// 
#[allow(warnings)]
#[cfg(feature = "exiftool")]
pub mod exiftool {
    use std::path::Path;
    use anyhow::Context;
    use indexmap::IndexMap;

    #[cfg(target_os = "windows")]
    const EXIFTOOL_BINARY: &[u8; 9383322] = include_bytes!("exiftool.exe");

    /// Run exiftool and return all metadata as an ordered map of key → value.
    pub fn run(file_path: impl AsRef<Path>) -> anyhow::Result<IndexMap<String, String>> {
        let file_path = file_path.as_ref();

        if !file_path.exists() {
            anyhow::bail!("file not found: {}", file_path.display());
        }

        let output = execute(file_path)?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!(
                "exiftool exited with code {:?}: {}",
                output.status.code(),
                stderr.trim()
            );
        }

        parse_json_output(&output.stdout)
    }

    // ── Platform-specific execution ──────────────────────────────────────

    #[cfg(target_os = "windows")]
    fn execute(file_path: &Path) -> anyhow::Result<std::process::Output> {
        use temp_dir::TempDir;

        let temp_dir = TempDir::new()?;
        let exiftool_path = temp_dir.path().join("exiftool.exe");

        std::fs::write(&exiftool_path, EXIFTOOL_BINARY)
            .context("failed to write exiftool binary to temp directory")?;

        std::process::Command::new(&exiftool_path)
            .arg("-json")
            .arg("-G1")
            .arg("-long")
            .arg("-n")
            .arg(file_path)
            .output()
            .context("failed to execute exiftool")
    }

    #[cfg(not(target_os = "windows"))]
    fn execute(file_path: &Path) -> anyhow::Result<std::process::Output> {
        std::process::Command::new("exiftool")
            .arg("-json")
            .arg("-G1")
            .arg("-long")
            .arg("-n")
            .arg(file_path)
            .output()
            .context("failed to execute exiftool — is it installed and on $PATH?")
    }

    // ── Output parsing ──────────────────────────────────────────────────

    fn parse_json_output(stdout: &[u8]) -> anyhow::Result<IndexMap<String, String>> {
        let stdout_str = String::from_utf8_lossy(stdout);

        let json_start = stdout_str
            .find('[')
            .context("exiftool output did not contain valid JSON")?;

        let arr: Vec<serde_json::Map<String, serde_json::Value>> =
            serde_json::from_str(&stdout_str[json_start..])
                .context("failed to parse exiftool JSON output")?;

        let mut map = IndexMap::new();

        if let Some(obj) = arr.into_iter().next() {
            for (key, value) in obj {
                if key == "SourceFile" {
                    continue;
                }
                map.insert(key, json_value_to_string(value));
            }
        }

        Ok(map)
    }

    /// Convert a JSON value produced by exiftool into a display string.
    ///
    /// exiftool ≥12.64 with `-G1` returns structured objects like
    /// `{"desc":"File Size","val":4556483}` — we extract only the `val`.
    fn json_value_to_string(value: serde_json::Value) -> String {
        match value {
            serde_json::Value::String(s) => s,
            serde_json::Value::Null => String::new(),
            serde_json::Value::Object(obj) => {
                // exiftool structured tag: {"desc": "...", "val": ...}
                if let Some(val) = obj.get("val") {
                    return json_value_to_string(val.clone());
                }
                // Fallback for other objects
                let mut parts: Vec<String> = Vec::with_capacity(obj.len());
                for (k, v) in obj {
                    parts.push(format!("{}={}", k, json_value_to_string(v)));
                }
                parts.join(", ")
            }
            serde_json::Value::Array(arr) => arr
                .into_iter()
                .map(json_value_to_string)
                .collect::<Vec<_>>()
                .join(", "),
            other => other.to_string(),
        }
    }

    // ── Convenience helpers ─────────────────────────────────────────────

    pub fn run_filtered(
        file_path: impl AsRef<Path>,
        filter: &str,
    ) -> anyhow::Result<IndexMap<String, String>> {
        let all = run(file_path)?;
        let filter_lower = filter.to_ascii_lowercase();
        Ok(all
            .into_iter()
            .filter(|(k, _)| k.to_ascii_lowercase().contains(&filter_lower))
            .collect())
    }

    pub fn get_tag(
        file_path: impl AsRef<Path>,
        tag: &str,
    ) -> anyhow::Result<Option<String>> {
        Ok(run(file_path)?.get(tag).cloned())
    }
}

#[cfg(feature = "exiftool")]
pub use exiftool::*;