Created
June 11, 2025 02:06
-
-
Save jbis9051/4b6693b6856b9a4447249c6eff612ff6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
} | |
pub trait Format { | |
type Error; | |
const FORMAT_TYPE: FormatType; | |
const EXTENSIONS: &'static [&'static str]; | |
const METADATA_VERSION: i32; // bump this if the metadata format changes | |
fn is_supported(path: &Path) -> bool { | |
let ext = path.extension().unwrap_or_default().to_str().unwrap_or_default().to_lowercase(); | |
Self::EXTENSIONS.contains(&ext.as_str()) | |
} | |
fn get_metadata(path: &Path, app_config: &AppConfig) -> Result<MediaMetadata, Self::Error>; | |
} | |
pub trait Thumbnailable: Format { | |
const THUMBNAIL_VERSION: i32; // bump this if the thumbnail format changes | |
fn generate_thumbnail(path: &Path, width: u32, height: u32, app_config: &AppConfig) -> Result<RgbImage, Self::Error>; | |
fn generate_full(path: &Path, app_config: &AppConfig) -> Result<RgbImage, Self::Error> { | |
let metadata = Self::get_metadata(path, app_config)?; | |
let width = metadata.width; | |
let height = metadata.height; | |
Self::generate_thumbnail(path, width, height, app_config) | |
} | |
} | |
pub trait Audioable: Format { | |
fn convert_to_mp3(from: &Path, to: &Path, app_config: &AppConfig) -> Result<Output, Self::Error> | |
where <Self as Format>::Error :From<std::io::Error> | |
{ | |
Ok(Command::new(&app_config.ffmpeg_path) | |
.args(&["-i", from.to_string_lossy().to_string().as_str(), to.to_string_lossy().to_string().as_str()]) | |
.output()?) | |
} | |
fn convert_to_wav(from: &Path, to: &Path, app_config: &AppConfig) -> Result<Output, Self::Error> | |
where <Self as Format>::Error :From<std::io::Error> | |
{ | |
Ok(Command::new(&app_config.ffmpeg_path) | |
.args(&["-i", from.to_string_lossy().to_string().as_str(), to.to_string_lossy().to_string().as_str()]) | |
.output()?) | |
} | |
} | |
macro_rules! all_formats { | |
({ | |
map: { | |
$( $name:ident => $format_a:ty ),* | |
}, | |
all: [$( $all:ty ),*], | |
thumbnailable: [$( $thumbnailable:ty ),*], | |
audioable: [$( $audioable:ty ),*] | |
}) => { | |
#[derive(Debug, Copy, Clone, Serialize, sqlx::Type, Deserialize, PartialEq)] | |
#[serde(rename_all = "kebab-case")] | |
#[sqlx(type_name = "format_type", rename_all = "kebab-case")] | |
pub enum FormatType { | |
$( $name, )* | |
Unknown | |
} | |
impl FormatType { | |
pub const fn all() -> &'static [FormatType] { | |
&[ | |
$( <$format_a as Format>::FORMAT_TYPE, )* | |
] | |
} | |
pub const fn thumbnailable() -> &'static [FormatType] { | |
&[ | |
$( <$thumbnailable as Format>::FORMAT_TYPE, )* | |
] | |
} | |
pub const fn audioable() -> &'static [FormatType] { | |
&[ | |
$( <$audioable as Format>::FORMAT_TYPE, )* | |
] | |
} | |
} | |
impl AnyFormat { | |
pub fn try_new(path: PathBuf) -> Option<Self> { | |
let format = { | |
if false { | |
unreachable!() | |
} | |
$( | |
else if <$format_a as Format>::is_supported(&path) { | |
FormatType::$name | |
} | |
)* | |
else { | |
return None; | |
} | |
}; | |
Some(Self { | |
format, | |
path | |
}) | |
} | |
} | |
pub(crate) mod match_format { | |
#[macro_export] | |
macro_rules! _match_format { | |
($format: expr, |$format_type: ident| $code: block) => {{ | |
use $crate::media_processors::format::*; | |
match $format { | |
$( &<$all as Format>::FORMAT_TYPE => { | |
type $format_type = $all; | |
$code | |
}, )* | |
_ => panic!("invalid format type: {:?}", $format), | |
} | |
}}; | |
(thumbnailable: $format: expr, |$format_type: ident| $code: block) => { | |
match_format!(thumbnailable: $format, |$format_type| $code, { panic!("invalid format type, not thumbnailable: {:?}", $format) }) | |
}; | |
(thumbnailable: $format: expr, |$format_type: ident| $code: block, $code_not_thumbnailable: block) => {{ | |
use $crate::media_processors::format::*; | |
match $format { | |
$( &<$thumbnailable as Format>::FORMAT_TYPE => { | |
type $format_type = $thumbnailable; | |
$code | |
}, )* | |
_ => $code_not_thumbnailable, | |
} | |
}}; | |
(audioable: $format: expr, |$format_type: ident| $code: block) => { | |
match_format!(audioable: $format, |$format_type| $code, { panic!("invalid format type, not audioable: {:?}", $format) }) | |
}; | |
(audioable: $format: expr, |$format_type: ident| $code: block, $code_not_audioable: block) => {{ | |
use $crate::media_processors::format::*; | |
match $format { | |
$( &<$audioable as Format>::FORMAT_TYPE => { | |
type $format_type = $audioable; | |
$code | |
}, )* | |
_ => $code_not_audioable, | |
} | |
}}; | |
} | |
pub use _match_format as match_format; | |
} | |
}; | |
} | |
impl FormatType{ | |
pub fn is_thumbnailable(&self) -> bool { | |
for format in Self::thumbnailable() { | |
if self == format { | |
return true; | |
} | |
} | |
return false; | |
} | |
} | |
pub use match_format::match_format as match_format; | |
use crate::scan_config::AppConfig; | |
all_formats!({ | |
map: { | |
Standard => standard::Standard, | |
Heif => heif::Heif, | |
Video => video::Video, | |
Raw => raw::Raw, | |
Pdf => pdf::Pdf, | |
Audio => audio::Audio | |
}, | |
all: [standard::Standard, heif::Heif, video::Video, raw::Raw, pdf::Pdf, audio::Audio], | |
thumbnailable: [standard::Standard, heif::Heif, video::Video, raw::Raw, pdf::Pdf], | |
audioable: [video::Video, audio::Audio] | |
}); | |
pub struct AnyFormat { | |
format: FormatType, | |
path: PathBuf | |
} | |
impl AnyFormat { | |
pub fn format_type(&self) -> FormatType { | |
self.format | |
} | |
pub fn thumbnailable(&self) -> bool { | |
match_format!(thumbnailable: &self.format, |ActualFormat| { true }, { false }) | |
} | |
pub fn audioable(&self) -> bool { | |
match_format!(audioable: &self.format, |ActualFormat| { true }, { false }) | |
} | |
pub fn get_metadata(&self, app_config: &AppConfig) -> Result<MediaMetadata, MetadataError> { | |
match_format!(&self.format, |ActualFormat| { <ActualFormat as Format>::get_metadata(&self.path, app_config).map_err(|e| e.into()) }) | |
} | |
pub fn generate_thumbnail(&self, width: u32, height: u32, app_config: &AppConfig) -> Result<RgbImage, MetadataError> { | |
match_format!(thumbnailable: &self.format, |ActualFormat| { <ActualFormat as Thumbnailable>::generate_thumbnail(&self.path, width, height, app_config).map_err(|e| e.into()) }) | |
} | |
pub fn generate_full(&self, app_config: &AppConfig) -> Result<RgbImage, MetadataError> { | |
match_format!(thumbnailable: &self.format, |ActualFormat| { <ActualFormat as Thumbnailable>::generate_full(&self.path, app_config).map_err(|e| e.into()) }) | |
} | |
pub fn metadata_version(&self) -> i32 { | |
match_format!(&self.format, |ActualFormat| { <ActualFormat as Format>::METADATA_VERSION }) | |
} | |
pub fn thumbnail_version(&self) -> i32 { | |
match_format!(thumbnailable: &self.format, |ActualFormat| { <ActualFormat as Thumbnailable>::THUMBNAIL_VERSION }) | |
} | |
pub fn convert_to_mp3(&self, to: &Path, app_config: &AppConfig) -> Result<Output, MetadataError> { | |
match_format!(audioable: &self.format, |ActualFormat| { <ActualFormat as Audioable>::convert_to_mp3(&self.path, to, app_config).map_err(|e| e.into()) }) | |
} | |
pub fn convert_to_wav(&self, to: &Path, app_config: &AppConfig) -> Result<Output, MetadataError> { | |
match_format!(audioable: &self.format, |ActualFormat| { <ActualFormat as Audioable>::convert_to_wav(&self.path, to, app_config).map_err(|e| e.into()) }) | |
} | |
} | |
#[derive(Debug, thiserror::Error)] | |
pub enum MetadataError { | |
#[error("standard format error: {0}")] | |
Standard(#[from] standard::StandardError), | |
#[error("heif format error: {0}")] | |
Heif(#[from] heif::HeifError), | |
#[error("video format error: {0}")] | |
Video(#[from] video::VideoError), | |
#[error("raw format error: {0}")] | |
Raw(#[from] raw::RawError), | |
#[error("pdf format error: {0}")] | |
Pdf(#[from] pdf::PdfError), | |
#[error("audio format error: {0}")] | |
Audio(#[from] audio::AudioError), | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::tasks::RemoteTask; | |
#[macro_export] | |
macro_rules! impl_task { | |
( | |
@background [$($background_task: ident,)*], | |
$size: literal, | |
@background_remote [$($background_remote_task: ident,)*], | |
@custom [$($custom_task: ident,)*], | |
@custom_remote [$($custom_remote_task: ident,)*] | |
) => { | |
pub enum AnyTask { | |
$( | |
$background_task($background_task), | |
)* | |
} | |
// TODO: this is some actual garbage fix it at some point | |
impl AnyTask { | |
pub const BACKGROUND_TASK_NAMES: [&'static str; $size] = [$(<$background_task>::NAME,)*]; | |
pub fn name(&self) -> &'static str { | |
match self { | |
$( | |
AnyTask::$background_task(_) => $background_task::NAME, | |
)* | |
} | |
} | |
pub fn name_from_str(task: &str) -> Result<&'static str, TaskError> { | |
match task { | |
$( | |
$background_task::NAME => Ok($background_task::NAME), | |
)* | |
_ => Err(TaskError::TaskNotFound(task.to_string())), | |
} | |
} | |
pub async fn compatible(task: &str, media: &Media) -> bool { | |
match task { | |
$( | |
$background_task::NAME => $background_task::compatible(media).await, | |
)* | |
_ => false, | |
} | |
} | |
pub async fn new(task: &str, db: &mut impl AcquireClone, tasks: &Table, app_config: &AppConfig) -> Result<Self, TaskError> { | |
match task { | |
$( | |
$background_task::NAME => { | |
let config: <$background_task as Task>::Config = tasks.get($background_task::NAME).map(|v| v.clone().try_into()).transpose()?.unwrap_or_default(); | |
let task = $background_task::new(db, &config, &app_config).await.map_err(|e| TaskError::TaskError(e.into()))?; | |
Ok(Self::$background_task(task)) | |
} | |
)*, | |
_ => Err(TaskError::TaskNotFound(task.to_string())), | |
} | |
} | |
pub async fn run(&self, db: &mut impl AcquireClone, media: &Media) -> Result<Box<dyn Debug>, TaskError> { | |
match self { | |
$( | |
AnyTask::$background_task(task) => { | |
task.run(db, media).await.map_err(|e| TaskError::TaskError(e.into())).map(|d| Box::new(d) as Box<dyn Debug>) | |
} | |
)*, | |
} | |
} | |
pub async fn run_and_store(&self, db: &mut impl AcquireClone, media: &mut Media) -> Result<(), TaskError> { | |
match self { | |
$( | |
AnyTask::$background_task(task) => { | |
task.run_and_store(db, media).await.map_err(|e| TaskError::TaskError(e.into())) | |
} | |
)*, | |
} | |
} | |
pub async fn outdated(&self, db: &mut impl AcquireClone, media: &Media) -> Result<bool, TaskError> { | |
match self { | |
$( | |
AnyTask::$background_task(task) => { | |
task.outdated(db, media).await.map_err(|e| TaskError::TaskError(e.into())) | |
} | |
)*, | |
} | |
} | |
pub fn background_remotable(task: &str) -> bool { | |
match task { | |
$( | |
<$background_remote_task as Task>::NAME => true, | |
)* | |
_ => false, | |
} | |
} | |
async fn run_remote(&self, db: &mut impl AcquireClone, media: &Media, remote_configs: &Table) -> Result<Box<dyn Debug>, TaskError> { | |
match self { | |
$( | |
AnyTask::$background_remote_task(task) => { | |
let config: <$background_remote_task as RemoteTask>::ClientTaskConfig = remote_configs.get($background_remote_task::NAME).map(|v| v.clone().try_into()).transpose()?.expect("remote is not enabled for this task"); | |
task.run_remote(db, media, &config).await.map_err(|e| TaskError::TaskError(e.into())).map(|d| Box::new(d) as Box<dyn Debug>) | |
} | |
)*, | |
_ => panic!("not remotable") | |
} | |
} | |
async fn run_remote_and_store(&self, db: &mut impl AcquireClone, media: &mut Media, remote_configs: &Table) -> Result<(), TaskError> { | |
match self { | |
$( | |
AnyTask::$background_remote_task(task) => { | |
let config: <$background_remote_task as RemoteTask>::ClientTaskConfig = remote_configs.get($background_remote_task::NAME).map(|v| v.clone().try_into()).transpose()?.expect("remote is not enabled for this task"); | |
task.run_remote_and_store(db, media, &config).await.map_err(|e| TaskError::TaskError(e.into())) | |
} | |
)*, | |
_ => panic!("not remotable") | |
} | |
} | |
fn should_remote(task: &str, remote_configs: &Table) -> bool { | |
if !Self::background_remotable(task) && !Self::custom_remotable(task){ | |
return false; | |
} | |
remote_configs.get(task).is_some() | |
} | |
pub async fn run_anywhere(&self, db: &mut impl AcquireClone, media: &Media, remote_configs: &Table) -> Result<Box<dyn Debug>, TaskError> { | |
if Self::should_remote(self.name(), remote_configs) { | |
self.run_remote(db, media, remote_configs).await | |
} else { | |
self.run(db, media).await | |
} | |
} | |
pub async fn run_and_store_anywhere(&self, db: &mut impl AcquireClone, media: &mut Media, remote_configs: &Table) -> Result<(), TaskError> { | |
if Self::should_remote(self.name(), remote_configs) { | |
self.run_remote_and_store(db, media, remote_configs).await | |
} else { | |
self.run_and_store(db, media).await | |
} | |
} | |
pub async fn new_remote(task: &str, db: &mut impl AcquireClone, tasks: &Table, runner_config: &RemoteRunnerGlobalConfig) -> Result<Self, TaskError> { | |
match task { | |
$( | |
$background_remote_task::NAME => { | |
let config: <$background_remote_task as RemoteTask>::RunnerTaskConfig = tasks.get($background_remote_task::NAME).map(|v| v.clone().try_into()).transpose()?.unwrap_or_default(); | |
let task = $background_remote_task::new_remote(db, &config, &runner_config).await.map_err(|e| TaskError::TaskError(e.into()))?; | |
Ok(Self::$background_remote_task(task)) | |
} | |
)* | |
_ => Err(TaskError::TaskNotFound(task.to_string())), | |
} | |
} | |
pub async fn remote_handler(&self, request: Request, db: impl AcquireClone + Send + 'static, tasks: &Table, runner_config: &RemoteRunnerGlobalConfig) -> Result<Response, ErrorResponse> { | |
match self { | |
$( | |
Self::$background_remote_task(task) => { | |
let config: <$background_remote_task as RemoteTask>::RunnerTaskConfig = tasks.get($background_remote_task::NAME).map(|v| v.clone().try_into()).transpose().map_err(|e| TaskError::InvalidTaskConfig(e))?.unwrap_or_default(); | |
task.remote_handler(request, db, &config, &runner_config).await | |
} | |
)* | |
_ => unreachable!() | |
} | |
} | |
pub fn customable(task: &str) -> bool { | |
match task { | |
$( | |
<$custom_task as Task>::NAME => true, | |
)* | |
_ => false, | |
} | |
} | |
pub fn custom_remotable(task: &str) -> bool { | |
match task { | |
$( | |
<$custom_remote_task as Task>::NAME => true, | |
)* | |
_ => false, | |
} | |
} | |
pub async fn run_custom_anywhere(task: &str, db: &mut impl AcquireClone, remote_configs: &Table, app_config: &AppConfig, args_str: &str) -> Result<String, TaskError> { | |
if Self::should_remote(task, remote_configs) { | |
Self::run_custom_remote(task, db, remote_configs, app_config, args_str).await | |
} else { | |
Self::run_custom(task, db, &app_config.tasks, app_config, args_str).await | |
} | |
} | |
// TODO: Value instead of &str? | |
pub async fn run_custom(task: &str, db: &mut impl AcquireClone, tasks: &Table, app_config: &AppConfig, args_str: &str) -> Result<String, TaskError> { | |
match task { | |
$(<$custom_task as Task>::NAME => { | |
let config: <$custom_task as Task>::Config = tasks.get($custom_task::NAME).map(|v| v.clone().try_into()).transpose()?.unwrap_or_default(); | |
let args: <$custom_task as CustomTask>::Args = serde_json::from_str(&args_str).unwrap(); | |
let res = <$custom_task as CustomTask>::run_custom(db, &config, app_config, args).await.map_err(|e| TaskError::TaskError(e.into()))?; | |
Ok(serde_json::to_string(&res).expect("failed to serialize custom task response")) | |
})* | |
_ => unreachable!() | |
} | |
} | |
pub async fn run_custom_remote(task: &str, db: &mut impl AcquireClone, remote_configs: &Table, app_config: &AppConfig, args_str: &str) -> Result<String, TaskError> { | |
match task { | |
$(<$custom_remote_task as Task>::NAME => { | |
let config: <$custom_remote_task as RemoteTask>::ClientTaskConfig = remote_configs.get($custom_remote_task::NAME).map(|v| v.clone().try_into()).transpose()?.expect("no remote config found"); | |
let args: <$custom_remote_task as CustomTask>::Args = serde_json::from_str(&args_str).unwrap(); | |
let res = <$custom_remote_task as CustomRemoteTask>::run_custom_remote(db, &config, app_config, args).await.map_err(|e| TaskError::TaskError(e.into()))?; | |
Ok(serde_json::to_string(&res).expect("failed to serialize custom task response")) | |
})* | |
_ => unreachable!() | |
} | |
} | |
pub async fn remote_custom_handler(task: &str, request: Request, db: impl AcquireClone + Send + 'static, tasks: &Table, runner_config: &RemoteRunnerGlobalConfig) -> Result<Response, ErrorResponse> { | |
match task { | |
$(<$custom_remote_task as Task>::NAME => { | |
let config: <$custom_remote_task as RemoteTask>::RunnerTaskConfig = tasks.get($custom_remote_task::NAME).map(|v| v.clone().try_into()).transpose().map_err(|e| TaskError::InvalidTaskConfig(e))?.unwrap_or_default(); | |
<$custom_remote_task as CustomRemoteTask>::remote_custom_handler(request, db, &config, &runner_config).await | |
} | |
)* | |
_ => unreachable!() | |
} | |
} | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment