1use sha2::{Digest, Sha256};
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6#[derive(Clone, Debug)]
7pub struct Ffmpeg {
8 pub path: PathBuf,
9 pub version: String,
10 pub sha256: String,
11}
12
13impl Ffmpeg {
14 pub fn record(&self) -> serde_json::Value {
15 serde_json::json!({
16 "path": self.path.display().to_string(),
17 "version": self.version,
18 "sha256": self.sha256,
19 })
20 }
21}
22
23pub fn cache() -> Result<PathBuf, String> {
24 let dir = std::env::var_os("PFX_CACHE")
25 .map(PathBuf::from)
26 .or_else(|| {
27 std::env::var_os("XDG_CACHE_HOME")
28 .map(PathBuf::from)
29 .filter(|p| p.is_absolute())
30 .map(|p| p.join("pito-hd-renderer"))
31 })
32 .or_else(|| {
33 std::env::var_os("HOME")
34 .map(|h| PathBuf::from(h).join(".cache").join("pito-hd-renderer"))
35 })
36 .ok_or("no cache folder: set PFX_CACHE")?;
37 std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
38 Ok(dir)
39}
40
41fn hash_file(path: &Path) -> Result<String, String> {
42 let mut file = std::fs::File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
43 let mut hasher = Sha256::new();
44 let mut buf = vec![0u8; 1 << 20];
45 loop {
46 let n = file
47 .read(&mut buf)
48 .map_err(|e| format!("{}: {e}", path.display()))?;
49 if n == 0 {
50 break;
51 }
52 hasher.update(&buf[..n]);
53 }
54 Ok(hex::encode(hasher.finalize()))
55}
56
57fn on_path(exe: &str) -> Option<PathBuf> {
58 std::env::split_paths(&std::env::var_os("PATH")?)
59 .map(|dir| dir.join(exe))
60 .find(|p| p.is_file())
61}
62
63fn version_of(text: &str) -> Option<String> {
64 text.lines()
65 .next()?
66 .strip_prefix("ffmpeg version ")?
67 .split_whitespace()
68 .next()
69 .map(String::from)
70}
71
72pub fn system() -> Result<Ffmpeg, String> {
73 let exe = if cfg!(windows) {
74 "ffmpeg.exe"
75 } else {
76 "ffmpeg"
77 };
78 let path = on_path(exe).ok_or_else(|| {
79 format!("no {exe} on PATH: install the system's ffmpeg, with libx264 and zscale")
80 })?;
81 let path = path.canonicalize().unwrap_or(path);
82 let out = Command::new(&path)
83 .arg("-version")
84 .output()
85 .map_err(|e| format!("{}: {e}", path.display()))?;
86 if !out.status.success() {
87 return Err(format!("{} -version failed", path.display()));
88 }
89 let version = version_of(&String::from_utf8_lossy(&out.stdout))
90 .ok_or_else(|| format!("{} -version printed no version", path.display()))?;
91 let sha256 = hash_file(&path)?;
92 Ok(Ffmpeg {
93 path,
94 version,
95 sha256,
96 })
97}