use crate::{ formats::InternalVideoFormat, process::{Process, ProcessError}, store::{Store, StoreError}, }; use tokio::io::AsyncRead; #[derive(Clone, Copy, Debug)] pub(crate) enum ThumbnailFormat { Jpeg, // Webp, } #[derive(Debug, thiserror::Error)] pub(crate) enum FfMpegError { #[error("Error in ffmpeg process")] Process(#[source] ProcessError), #[error("Error reading output")] Read(#[source] std::io::Error), #[error("Error writing bytes")] Write(#[source] std::io::Error), #[error("Invalid output format")] Json(#[source] serde_json::Error), #[error("Error creating parent directory")] CreateDir(#[source] crate::store::file_store::FileError), #[error("Error reading file to stream")] ReadFile(#[source] crate::store::file_store::FileError), #[error("Error opening file")] OpenFile(#[source] std::io::Error), #[error("Error creating file")] CreateFile(#[source] std::io::Error), #[error("Error closing file")] CloseFile(#[source] std::io::Error), #[error("Error removing file")] RemoveFile(#[source] std::io::Error), #[error("Error in store")] Store(#[source] StoreError), #[error("Invalid file path")] Path, } impl FfMpegError { pub(crate) fn is_client_error(&self) -> bool { // Failing validation or ffmpeg bailing probably means bad input matches!(self, Self::Process(ProcessError::Status(_))) } pub(crate) fn is_not_found(&self) -> bool { if let Self::Store(e) = self { return e.is_not_found(); } false } } impl ThumbnailFormat { const fn as_ffmpeg_codec(self) -> &'static str { match self { Self::Jpeg => "mjpeg", // Self::Webp => "webp", } } const fn to_file_extension(self) -> &'static str { match self { Self::Jpeg => ".jpeg", // Self::Webp => ".webp", } } const fn as_ffmpeg_format(self) -> &'static str { match self { Self::Jpeg => "image2", // Self::Webp => "webp", } } pub(crate) fn media_type(self) -> mime::Mime { match self { Self::Jpeg => mime::IMAGE_JPEG, // Self::Webp => crate::formats::mimes::image_webp(), } } } #[tracing::instrument(skip(store))] pub(crate) async fn thumbnail( store: S, from: S::Identifier, input_format: InternalVideoFormat, format: ThumbnailFormat, ) -> Result { let input_file = crate::tmp_file::tmp_file(Some(input_format.file_extension())); let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?; crate::store::file_store::safe_create_parent(&input_file) .await .map_err(FfMpegError::CreateDir)?; let output_file = crate::tmp_file::tmp_file(Some(format.to_file_extension())); let output_file_str = output_file.to_str().ok_or(FfMpegError::Path)?; crate::store::file_store::safe_create_parent(&output_file) .await .map_err(FfMpegError::CreateDir)?; let mut tmp_one = crate::file::File::create(&input_file) .await .map_err(FfMpegError::CreateFile)?; let stream = store .to_stream(&from, None, None) .await .map_err(FfMpegError::Store)?; tmp_one .write_from_stream(stream) .await .map_err(FfMpegError::Write)?; tmp_one.close().await.map_err(FfMpegError::CloseFile)?; let process = Process::run( "ffmpeg", &[ "-hide_banner", "-v", "warning", "-i", input_file_str, "-frames:v", "1", "-codec", format.as_ffmpeg_codec(), "-f", format.as_ffmpeg_format(), output_file_str, ], ) .map_err(FfMpegError::Process)?; process.wait().await.map_err(FfMpegError::Process)?; tokio::fs::remove_file(input_file) .await .map_err(FfMpegError::RemoveFile)?; let tmp_two = crate::file::File::open(&output_file) .await .map_err(FfMpegError::OpenFile)?; let stream = tmp_two .read_to_stream(None, None) .await .map_err(FfMpegError::ReadFile)?; let reader = tokio_util::io::StreamReader::new(stream); let clean_reader = crate::tmp_file::cleanup_tmpfile(reader, output_file); Ok(Box::pin(clean_reader)) }