Skip to content

Instantly share code, notes, and snippets.

@jbis9051
Created June 11, 2025 02:06
Show Gist options
  • Save jbis9051/4b6693b6856b9a4447249c6eff612ff6 to your computer and use it in GitHub Desktop.
Save jbis9051/4b6693b6856b9a4447249c6eff612ff6 to your computer and use it in GitHub Desktop.
}
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),
}
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