use std::fs::{self, File};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FileResolverError {
#[error("Path is not a directory: {0}")]
NotADirectory(PathBuf),
#[error("Failed to read directory {path}: {source}")]
ReadDirectoryFailed { path: PathBuf, source: io::Error },
#[error("Failed to read directory entry: {0}")]
ReadEntryFailed(#[from] io::Error),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoFile {
pub path: PathBuf,
}
pub(crate) fn scan_for_videos(dir_path: &Path) -> Result<Vec<VideoFile>, FileResolverError> {
let mut video_files = Vec::new();
scan_directory_recursive(dir_path, &mut video_files)?;
Ok(video_files)
}
fn scan_directory_recursive(
dir_path: &Path,
video_files: &mut Vec<VideoFile>,
) -> Result<(), FileResolverError> {
if !dir_path.is_dir() {
return Err(FileResolverError::NotADirectory(dir_path.to_path_buf()));
}
for entry in fs::read_dir(dir_path).map_err(|e| FileResolverError::ReadDirectoryFailed {
path: dir_path.to_path_buf(),
source: e,
})? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
scan_directory_recursive(&path, video_files)?;
} else if path.is_file() {
if is_video_file(&path) {
video_files.push(VideoFile { path });
}
}
}
Ok(())
}
fn is_video_file(file_path: &Path) -> bool {
const BUFFER_SIZE: usize = 8192;
let mut file = match File::open(file_path) {
Ok(f) => f,
Err(_) => return false,
};
let mut buffer = vec![0u8; BUFFER_SIZE];
let bytes_read = match file.read(&mut buffer) {
Ok(n) => n,
Err(_) => return false,
};
buffer.truncate(bytes_read);
infer::is_video(&buffer)
}
pub(crate) fn compute_video_hash(video_path: &Path) -> Result<String, FileResolverError> {
let hash = blake3::Hasher::new()
.update_mmap_rayon(video_path)
.map_err(FileResolverError::ReadEntryFailed)?
.finalize();
Ok(hash.to_hex().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
#[test]
fn test_scan_nonexistent_directory() {
let result = scan_for_videos(Path::new("/nonexistent/path/that/does/not/exist"));
assert!(result.is_err());
}
#[test]
fn test_scan_file_instead_of_directory() {
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join("test_file.txt");
File::create(&temp_file).unwrap();
let result = scan_for_videos(&temp_file);
assert!(result.is_err());
fs::remove_file(&temp_file).ok();
}
}