diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5d1f15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# IDE files +*.iml +.idea/ + +# build files +/target + +# runtime files +*.log +/files + +# config +config.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6ecd614 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bumblebee" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +simple-log = "1.6.0" +toml = "0.7.3" +serde = { version = "1.0.162", features = ["derive"] } +glob = "0.3.0" \ No newline at end of file diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..a8ab2f8 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,35 @@ +[files] +input_path = "/data/input" +output_path = "/data/output" +include = [ 'mp4', 'avi', 'mkv' ] # file extensions to include +keep_file_structure = true # e.g. /data/input/foo/bar.mp4 -> /data/output/foo/bar.webm + +[files.cleanup] +enabled = true +original_cleanup_behavior = "delete" # delete, archive or keep + +[files.cleanup.archive] +path = "/data/archive" +keep_file_structure = true # e.g. /data/input/foo/bar.mp4 -> /data/archive/foo/bar.mp4 + +[files.cleanup.delete] +remove_empty_directories = true # if folder is empty after deleting file, delete folder + +[ffmpeg] +path = "/usr/bin/ffmpeg" # path to ffmpeg executable + +[ffmpeg.process] +niceness = 10 # 0 = highest priority +threads = 4 # 0 = auto + +[ffmpeg.output] +format = "webm" # webm, mp4, mkv, etc. + +[ffmpeg.output.video] +codec = "libsvtav1" +bitrate = 0 # 0 = auto +crf = 30 # 0 = lossless + +[ffmpeg.output.audio] +codec = "libopus" +bitrate = 0 # 0 = auto diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..acb31cf --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,132 @@ +use std::error::Error; +use serde::{Serialize, Deserialize}; + +#[allow(non_camel_case_types)] // this is allowed cuz we want to use snake case in the config file +#[derive(Serialize, Deserialize, Debug)] +pub enum ConfigFilesCleanupOriginalBehavior { + delete, + archive, + keep, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFilesCleanupArchive { + pub path: String, + pub keep_file_structure: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFilesCleanupDelete { + pub remove_empty_directories: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFilesCleanup { + pub enabled: bool, + pub original_cleanup_behavior: ConfigFilesCleanupOriginalBehavior, + pub archive: ConfigFilesCleanupArchive, + pub delete: ConfigFilesCleanupDelete, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFiles { + pub keep_file_structure: bool, + pub input_path: String, + pub output_path: String, + pub include: Vec, + pub cleanup: ConfigFilesCleanup, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFFmpegProcess { + pub threads: u8, + pub niceness: u8, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFFmpegOutputVideo { + pub codec: String, + pub bitrate: u32, + pub crf: u8, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFFmpegOutputAudio { + pub codec: String, + pub bitrate: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFFmpegOutput { + pub format: String, + pub video: ConfigFFmpegOutputVideo, + pub audio: ConfigFFmpegOutputAudio, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfigFFmpeg { + pub path: String, + pub process: ConfigFFmpegProcess, + pub output: ConfigFFmpegOutput, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + pub files: ConfigFiles, + pub ffmpeg: ConfigFFmpeg, +} + +impl Config { + pub fn new() -> Config { + Config { + files: ConfigFiles { + keep_file_structure: false, + input_path: String::from("/data/input"), + output_path: String::from("/data/output"), + include: Vec::new(), + cleanup: ConfigFilesCleanup { + enabled: false, + original_cleanup_behavior: ConfigFilesCleanupOriginalBehavior::delete, + archive: ConfigFilesCleanupArchive { + path: String::from("/data/archive"), + keep_file_structure: false, + }, + delete: ConfigFilesCleanupDelete { + remove_empty_directories: false + }, + }, + }, + ffmpeg: ConfigFFmpeg { + path: String::from("/usr/bin/ffmpeg"), + process: ConfigFFmpegProcess { + threads: 0, + niceness: 0, + }, + output: ConfigFFmpegOutput { + format: String::from("webm"), + video: ConfigFFmpegOutputVideo { + codec: String::from("libsvtav1"), + bitrate: 0, + crf: 0, + }, + audio: ConfigFFmpegOutputAudio { + codec: String::from("libopus"), + bitrate: 0, + }, + }, + }, + } + } + + pub fn from_file(path: &str) -> Config { + let config_file = std::fs::read_to_string(path).expect("Failed to read config file"); + match toml::from_str(&config_file) { + Ok(config) => config, + Err(e) => { + error!("Failed to parse config file: {}", e.message()); + error!("Please check your config file and try again."); + std::process::exit(1); + } + } + } +} \ No newline at end of file diff --git a/src/files.rs b/src/files.rs new file mode 100644 index 0000000..3ed35c2 --- /dev/null +++ b/src/files.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; + +pub fn get_files>(path: S) -> Vec { + let mut files = Vec::new(); + let mut dirs = Vec::new(); + dirs.push(PathBuf::from(path.into())); + + while let Some(dir) = dirs.pop() { + for entry in std::fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } else { + files.push(path); + } + } + } + + files +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e6d9d1a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,43 @@ +#[macro_use] +extern crate simple_log; + +use std::env; +use simple_log::LogConfigBuilder; + +mod configuration; +mod files; +mod processing; + +fn main() { + setup_logger(); + let config = configuration::Config::from_file(&env::var("CONFIG").unwrap_or(String::from("./config.toml"))); + debug!("Config: {:#?}", &config); + + let input_files = files::get_files(&config.files.input_path) + .into_iter() + .filter(|path| { + let path = path.to_str().unwrap(); + config.files.include.iter().any(|include| path.contains(include)) + }) + .collect::>(); + info!("Found {} file(s) to be processed.", input_files.len()); + + for files in input_files { + let operation = processing::TranscodeOperation::new(&config.ffmpeg, files); + operation.run(); + } +} + +fn setup_logger() { + let config = LogConfigBuilder::builder() + .path("backups.log") + .size(10 * 1000) + .roll_count(10) + .time_format("%Y-%m-%d %H:%M:%S") + .level("debug") + .output_file() + .output_console() + .build(); + + simple_log::new(config).unwrap(); +} diff --git a/src/processing.rs b/src/processing.rs new file mode 100644 index 0000000..22dd37c --- /dev/null +++ b/src/processing.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; +use crate::configuration::ConfigFFmpeg; + +pub struct TranscodeOperation<'f> { + pub ffmpeg: &'f ConfigFFmpeg, + pub file: PathBuf, +} + +impl<'f> TranscodeOperation<'f> { + pub fn new(ffmpeg: &'f ConfigFFmpeg, file: PathBuf) -> TranscodeOperation { + TranscodeOperation { + ffmpeg, + file, + } + } + + pub fn run(&self) { + info!("Transcoding file: {:?}...", &self.file); + todo!(); + } +} +