#![feature(exit_status_error)] use anyhow::Context; use clap::{arg, command, value_parser}; use encoder::{FFMPEGCommand, MediaType}; use std::{path::PathBuf, process::Stdio, sync::Arc}; use tokio::{ io::{AsyncBufReadExt, BufReader}, sync::Mutex, }; use ui::display; use crate::encoder::EncodingStatus; mod encoder; mod ui; #[derive(Debug, Clone)] pub enum VideoCodec { WEBM, HEVC, } impl std::fmt::Display for VideoCodec { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Self::WEBM => write!(f, "WEBM"), Self::HEVC => write!(f, "HEVC"), } } } impl VideoCodec { pub fn from_string(string: &str) -> Option { match string.to_lowercase().as_str() { "webm" => Some(Self::WEBM), "hevc" => Some(Self::HEVC), _ => None, } } } #[tokio::main] async fn main() -> anyhow::Result<()> { let args = command!() .about("Simple program to parse files to the most efficient formats within a set size") .arg( arg!(-s --size "Target megabyte size. If not set, default of 25mb (Discords free limit)") .required(false) .default_value("25") .value_parser(value_parser!(u16)) ) .arg( arg!(-c --codec "Choose video codec between `HEVC` (H.265) and `WEBM` (vp9).") .required(false) .default_value("WEBM") ) .arg( arg!(-f --files "Comma separated files to convert. EG: -f=,") .required(true) .value_parser(value_parser!(PathBuf)) .value_delimiter(',') .num_args(1..=std::usize::MAX) ).get_matches(); let size = args .get_one::("size") .expect("Default value dissapeared from rate") * 8; let files = args .get_many::("files") .context("No files specified")? .collect::>(); let binding = "webm".to_owned(); let codec = args.get_one::("codec").unwrap_or(&binding); let codec = VideoCodec::from_string(codec).unwrap_or(VideoCodec::WEBM); let commands: Arc>> = Arc::new(Mutex::new(vec![])); { let mut commands_mut = commands.try_lock().unwrap(); for file in files { let mut command: FFMPEGCommand; let extension = file .extension() .context("File doesn't have extension - is folder or is invalid file")?; match extension .to_str() .expect("Somehow Extension contains charcters we can't decode lol") .to_lowercase() .as_str() { "webm" | "mp4" | "mov" | "avi" | "mpeg" | "mkv" => { command = FFMPEGCommand::new(MediaType::Video, file, size, codec.clone()).await?; } "mp3" | "wav" | "ogg" | "opus" | "flac" | "aiff" => { command = FFMPEGCommand::new(MediaType::Audio, file, size, codec.clone()).await?; } "jpg" | "png" | "webp" | "exr" | "jpeg" | "tiff" | "bpm" | "raw" | "tif" => { command = FFMPEGCommand::new(MediaType::Image, file, size, codec.clone()).await?; } "gif" => { command = FFMPEGCommand::new(MediaType::AnimatedImage, file, size, codec.clone()) .await?; } _ => break, } command.command.0.stdout(Stdio::piped()); command.command.0.stderr(Stdio::piped()); command.command.0.stdin(Stdio::null()); if command.media_type == MediaType::Video { let mut pass2 = command.command.1.unwrap(); pass2.stdout(Stdio::piped()); pass2.stderr(Stdio::piped()); pass2.stdin(Stdio::null()); command.command.1 = Some(pass2) } command.exec_handle = Some(command.command.0.spawn()?); command.buff_reader = Some( BufReader::new( command .exec_handle .as_mut() .unwrap() .stdout .take() .expect("encoder stdout missing - exited early or unavailable"), ) .lines(), ); commands_mut.push(command); } } let mut command_spawns = vec![]; let mut buff_readers = vec![]; let ui = tokio::spawn(display(commands.clone())); { for (i, command) in commands.lock().await.iter_mut().enumerate() { buff_readers.push((i, command.buff_reader.take().unwrap())); } } for mut buff_reader in buff_readers.into_iter() { use std::time::Duration; use tokio::time::interval; let commands_ref = commands.clone(); let mut intv = interval(Duration::from_millis(50)); command_spawns.push(tokio::spawn(async move { intv.tick().await; 'line: while let Ok(Some(line)) = buff_reader.1.next_line().await { // dbg!(&line); if let Some(time_start) = line.find("out_time=") { let time: Vec = line[time_start + 10..] .split(':') .map(|s| s.to_owned()) .collect(); let mut parsed_time = vec![]; for part in time { if let Ok(number) = part.parse::() { parsed_time.push(number) } else { break 'line; } } let time = parsed_time[0] * 3600. + parsed_time[1] * 60. + parsed_time[2]; let mut command = commands_ref.lock().await; let command = command.get_mut(buff_reader.0).unwrap(); command.status = EncodingStatus::InProgress; command.progressed_time = time; } if let Some(progress_i) = line.find("progress=") { let mut command = commands_ref.lock().await; let command = command.get_mut(buff_reader.0).unwrap(); match &line[progress_i + 9..] { "end" => match command.media_type { //Executes 2nd pass MediaType::Video => match command.passed_pass_1 { true => command.status = EncodingStatus::Finished, false => { command.exec_handle = Some(command.command.1.as_mut().unwrap().spawn().unwrap()); buff_reader = ( buff_reader.0, BufReader::new( command.exec_handle.as_mut().unwrap().stdout.take().expect( "encoder stdout missing - exited early or unavailable", ), ) .lines(), ); command.passed_pass_1 = true; } }, _ => { command.status = EncodingStatus::Finished; } }, "continue" => command.status = EncodingStatus::InProgress, _ => (), }; } } })); } for spawn in command_spawns { spawn.await?; } ui.await?; Ok(()) }