use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use blake3::{Hash, hash};
use serde::{Deserialize, Serialize};
use crate::{
cache_dir,
errors::FfmpegError,
ffmpeg::{command_mutators::FfmpegPresets, ffmpeg, output_ffmpeg},
};
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct CoverArt {
hash: Hash,
path: PathBuf,
}
impl CoverArt {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, FfmpegError> {
let path = path.as_ref();
Ok(Self {
hash: hash_video_stream(path)?,
path: path.to_path_buf(),
})
}
#[must_use]
pub fn hash(&self) -> Hash {
self.hash
}
#[must_use]
pub fn source(&self) -> &Path {
self.path.as_path()
}
pub fn cache_file(&self, y: usize, x: usize) -> Result<PathBuf, FfmpegError> {
let cover_dir = cache_dir().join("covers");
let cache_file = cover_dir.join(format!("{id}_{x}_{y}.webp", id = self.hash()));
if cache_file.exists() {
return Ok(cache_file);
}
let mut command = ffmpeg();
command.input_file(self.source());
command.args(["-map", "0:v:0", "-frames:v", "1", "-filter:v"]);
command.arg(format!("scale={x}:{y}:flags=lanczos"));
command.args([
"-c:v",
"libwebp",
"-qscale:v",
"80",
"-compression_level",
"6",
"-y",
]);
command.arg(&cache_file);
fs::create_dir_all(&cover_dir)?;
output_ffmpeg(command)?;
Ok(cache_file)
}
}
pub fn has_video_stream(path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
let output = Command::new("ffprobe")
.args([
"-v",
"error",
"-select_streams",
"v",
"-show_entries",
"stream=index",
"-of",
"csv=p=0",
path.to_str().unwrap(),
])
.output();
let Ok(output) = output else { return false };
!output.stdout.is_empty()
}
pub fn hash_video_stream(path: impl AsRef<Path>) -> Result<Hash, FfmpegError> {
let path = path.as_ref();
let mut command = ffmpeg();
command.input_file(path);
command.args(["-map", "0:v:0", "-c", "copy", "-f", "rawvideo", "-"]);
let result = output_ffmpeg(command)?;
Ok(hash(result.as_bytes()))
}