diff --git a/.editorconfig b/.editorconfig index f9366fa..f8c5d3d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*] -indent_style = space +indent_style = tab indent_size = 4 charset = utf-8 trim_trailing_whitespace = true diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/config.rs b/src/config.rs index 21aef7e..03f7e64 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,318 +3,318 @@ use globset::GlobBuilder; use regex::bytes::{Regex, RegexBuilder}; use serde::Deserialize; use std::{ - io::Write, - path::{Path, PathBuf}, + io::Write, + path::{Path, PathBuf}, }; #[derive(Debug)] pub struct Config { - pub from: PathBuf, - pub to: PathBuf, - pub matches: Vec, - pub jobs: Option, + pub from: PathBuf, + pub to: PathBuf, + pub matches: Vec, + pub jobs: Option, } #[derive(Debug)] pub struct TranscodeMatch { - pub regexes: Vec, - pub to: Transcode, + pub regexes: Vec, + pub to: Transcode, } #[derive(Clone, Debug, Deserialize)] #[serde(tag = "codec")] pub enum Transcode { - #[serde(rename = "opus")] - Opus { - #[serde(default = "default_opus_bitrate")] - bitrate: u16, + #[serde(rename = "opus")] + Opus { + #[serde(default = "default_opus_bitrate")] + bitrate: u16, - #[serde(default = "bitrate_type_vbr")] - bitrate_type: BitrateType, - }, + #[serde(default = "bitrate_type_vbr")] + bitrate_type: BitrateType, + }, - #[serde(rename = "flac")] - Flac { - #[serde(default = "default_flac_compression")] - compression: u8, - }, + #[serde(rename = "flac")] + Flac { + #[serde(default = "default_flac_compression")] + compression: u8, + }, - #[serde(rename = "mp3")] - Mp3 { - #[serde(default = "default_mp3_bitrate")] - bitrate: u16, + #[serde(rename = "mp3")] + Mp3 { + #[serde(default = "default_mp3_bitrate")] + bitrate: u16, - #[serde(default = "bitrate_type_vbr")] - bitrate_type: BitrateType, - }, + #[serde(default = "bitrate_type_vbr")] + bitrate_type: BitrateType, + }, - #[serde(rename = "copy")] - Copy, + #[serde(rename = "copy")] + Copy, } impl Transcode { - pub fn extension(&self) -> &'static str { - match self { - Transcode::Opus { .. } => "opus", - Transcode::Flac { .. } => "flac", - Transcode::Mp3 { .. } => "mp3", - Transcode::Copy => "", - } - } + pub fn extension(&self) -> &'static str { + match self { + Transcode::Opus { .. } => "opus", + Transcode::Flac { .. } => "flac", + Transcode::Mp3 { .. } => "mp3", + Transcode::Copy => "", + } + } } fn default_opus_bitrate() -> u16 { - 160 + 160 } fn default_flac_compression() -> u8 { - 5 + 5 } fn bitrate_type_vbr() -> BitrateType { - BitrateType::Vbr + BitrateType::Vbr } fn default_mp3_bitrate() -> u16 { - 256 + 256 } impl Default for Transcode { - fn default() -> Self { - Transcode::Opus { - bitrate: default_opus_bitrate(), - bitrate_type: bitrate_type_vbr(), - } - } + fn default() -> Self { + Transcode::Opus { + bitrate: default_opus_bitrate(), + bitrate_type: bitrate_type_vbr(), + } + } } #[derive(Clone, Debug, Deserialize)] pub enum BitrateType { - #[serde(rename = "cbr")] - Cbr, - #[serde(rename = "vbr")] - Vbr, + #[serde(rename = "cbr")] + Cbr, + #[serde(rename = "vbr")] + Vbr, } #[derive(Debug, Default, Deserialize)] struct ConfigFile { - from: Option, - to: Option, + from: Option, + to: Option, - #[serde(default)] - matches: Vec, + #[serde(default)] + matches: Vec, } #[derive(Debug, Deserialize)] struct TranscodeMatchFile { - glob: Option, - regex: Option, + glob: Option, + regex: Option, - #[serde(default)] - extensions: Vec, + #[serde(default)] + extensions: Vec, - to: Transcode, + to: Transcode, } pub fn config() -> Result { - use clap::{App, Arg, SubCommand}; + use clap::{App, Arg, SubCommand}; - let arg_matches = App::new("audio-conv") - .version(clap::crate_version!()) - .about("Converts audio files") - .arg( - Arg::with_name("config") - .short("c") - .long("config") - .required(false) - .takes_value(true) - .help("Path to an audio-conv config file, defaults to \"audio-conv.yaml\""), - ) - .arg( - Arg::with_name("from") - .short("f") - .long("from") - .required(false) - .takes_value(true) - .help("\"from\" directory path"), - ) - .arg( - Arg::with_name("to") - .short("t") - .long("to") - .required(false) - .takes_value(true) - .help("\"to\" directory path"), - ) - .arg( - Arg::with_name("jobs") - .short("j") - .long("jobs") - .required(false) - .takes_value(true) - .help("Allow N jobs/transcodes at once. Defaults to number of logical cores"), - ) - .subcommand(SubCommand::with_name("init").about("writes an example config")) - .get_matches(); + let arg_matches = App::new("audio-conv") + .version(clap::crate_version!()) + .about("Converts audio files") + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .required(false) + .takes_value(true) + .help("Path to an audio-conv config file, defaults to \"audio-conv.yaml\""), + ) + .arg( + Arg::with_name("from") + .short("f") + .long("from") + .required(false) + .takes_value(true) + .help("\"from\" directory path"), + ) + .arg( + Arg::with_name("to") + .short("t") + .long("to") + .required(false) + .takes_value(true) + .help("\"to\" directory path"), + ) + .arg( + Arg::with_name("jobs") + .short("j") + .long("jobs") + .required(false) + .takes_value(true) + .help("Allow N jobs/transcodes at once. Defaults to number of logical cores"), + ) + .subcommand(SubCommand::with_name("init").about("writes an example config")) + .get_matches(); - let current_dir = std::env::current_dir().context("Could not get current directory")?; + let current_dir = std::env::current_dir().context("Could not get current directory")?; - let config_path = arg_matches.value_of_os("config"); - let force_load = config_path.is_some(); - let config_path = config_path - .map(AsRef::::as_ref) - .unwrap_or_else(|| AsRef::::as_ref("audio-conv.yaml")); - let config_path = current_dir.join(config_path); + let config_path = arg_matches.value_of_os("config"); + let force_load = config_path.is_some(); + let config_path = config_path + .map(AsRef::::as_ref) + .unwrap_or_else(|| AsRef::::as_ref("audio-conv.yaml")); + let config_path = current_dir.join(config_path); - if let Some("init") = arg_matches.subcommand_name() { - std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&config_path) - .and_then(|mut f| f.write_all(std::include_bytes!("../example.audio-conv.yaml"))) - .with_context(|| format!("Unable to write config file to {}", config_path.display()))?; + if let Some("init") = arg_matches.subcommand_name() { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&config_path) + .and_then(|mut f| f.write_all(std::include_bytes!("../example.audio-conv.yaml"))) + .with_context(|| format!("Unable to write config file to {}", config_path.display()))?; - std::process::exit(0); - } + std::process::exit(0); + } - let config_dir = config_path - .parent() - .context("Could not get parent directory of the config file")?; + let config_dir = config_path + .parent() + .context("Could not get parent directory of the config file")?; - let config_file = load_config_file(&config_path) - .with_context(|| format!("Failed loading config file {}", config_path.display()))?; + let config_file = load_config_file(&config_path) + .with_context(|| format!("Failed loading config file {}", config_path.display()))?; - if force_load && config_file.is_none() { - return Err(Error::msg(format!( - "could not find config file \"{}\"", - config_path.display() - ))); - } + if force_load && config_file.is_none() { + return Err(Error::msg(format!( + "could not find config file \"{}\"", + config_path.display() + ))); + } - let default_regex = RegexBuilder::new("\\.(flac|wav)$") - .case_insensitive(true) - .build() - .expect("Failed compiling default match regex"); + let default_regex = RegexBuilder::new("\\.(flac|wav)$") + .case_insensitive(true) + .build() + .expect("Failed compiling default match regex"); - let transcode_matches = config_file - .as_ref() - .map(|config_file| { - config_file - .matches - .iter() - .map(|m| { - let glob = m.glob.iter().map(|glob| { - let glob = GlobBuilder::new(glob) - .case_insensitive(true) - .build() - .context("Failed building glob")?; - let regex = Regex::new(glob.regex()).context("Failed compiling regex")?; - Ok(regex) - }); + let transcode_matches = config_file + .as_ref() + .map(|config_file| { + config_file + .matches + .iter() + .map(|m| { + let glob = m.glob.iter().map(|glob| { + let glob = GlobBuilder::new(glob) + .case_insensitive(true) + .build() + .context("Failed building glob")?; + let regex = Regex::new(glob.regex()).context("Failed compiling regex")?; + Ok(regex) + }); - let regex = m.regex.iter().map(|regex| { - let regex = RegexBuilder::new(regex) - .case_insensitive(true) - .build() - .context("Failed compiling regex")?; - Ok(regex) - }); + let regex = m.regex.iter().map(|regex| { + let regex = RegexBuilder::new(regex) + .case_insensitive(true) + .build() + .context("Failed compiling regex")?; + Ok(regex) + }); - let extensions = m.extensions.iter().map(|ext| { - let mut ext = regex::escape(ext); - ext.insert_str(0, &"\\."); - ext.push_str("$"); + let extensions = m.extensions.iter().map(|ext| { + let mut ext = regex::escape(ext); + ext.insert_str(0, &"\\."); + ext.push_str("$"); - let regex = RegexBuilder::new(&ext) - .case_insensitive(true) - .build() - .context("Failed compiling regex")?; - Ok(regex) - }); + let regex = RegexBuilder::new(&ext) + .case_insensitive(true) + .build() + .context("Failed compiling regex")?; + Ok(regex) + }); - let mut regexes = glob - .chain(regex) - .chain(extensions) - .collect::>>()?; + let mut regexes = glob + .chain(regex) + .chain(extensions) + .collect::>>()?; - if regexes.is_empty() { - regexes.push(default_regex.clone()); - } + if regexes.is_empty() { + regexes.push(default_regex.clone()); + } - Ok(TranscodeMatch { - regexes, - to: m.to.clone(), - }) - }) - .collect::>>() - }) - .transpose()? - .filter(|matches| !matches.is_empty()) - .unwrap_or_else(|| { - vec![TranscodeMatch { - regexes: vec![default_regex], - to: Transcode::default(), - }] - }); + Ok(TranscodeMatch { + regexes, + to: m.to.clone(), + }) + }) + .collect::>>() + }) + .transpose()? + .filter(|matches| !matches.is_empty()) + .unwrap_or_else(|| { + vec![TranscodeMatch { + regexes: vec![default_regex], + to: Transcode::default(), + }] + }); - Ok(Config { - from: { - arg_matches - .value_of_os("from") - .map(|p| current_dir.join(p)) - .or_else(|| { - config_file - .as_ref() - .map(|c| c.from.as_ref()) - .flatten() - .map(|p| config_dir.join(p)) - }) - .ok_or_else(|| Error::msg("\"from\" not configured"))? - .canonicalize() - .context("Could not canonicalize \"from\" path")? - }, - to: arg_matches - .value_of_os("to") - .map(|p| current_dir.join(p)) - .or_else(|| { - config_file - .as_ref() - .map(|c| c.to.as_ref()) - .flatten() - .map(|p| config_dir.join(p)) - }) - .ok_or_else(|| Error::msg("\"to\" not configured"))? - .canonicalize() - .context("Could not canonicalize \"to\" path")?, - matches: transcode_matches, - jobs: arg_matches - .value_of_os("jobs") - .map(|jobs_os_str| { - let jobs_str = jobs_os_str.to_str().with_context(|| { - // TODO: use `OsStr.display` when it lands - // https://github.com/rust-lang/rust/pull/80841 - format!( - "Could not convert \"jobs\" argument to string due to invalid characters", - ) - })?; - jobs_str.parse().with_context(|| { - format!( - "Could not parse \"jobs\" argument \"{}\" to a number", - &jobs_str - ) - }) - }) - .transpose()?, - }) + Ok(Config { + from: { + arg_matches + .value_of_os("from") + .map(|p| current_dir.join(p)) + .or_else(|| { + config_file + .as_ref() + .map(|c| c.from.as_ref()) + .flatten() + .map(|p| config_dir.join(p)) + }) + .ok_or_else(|| Error::msg("\"from\" not configured"))? + .canonicalize() + .context("Could not canonicalize \"from\" path")? + }, + to: arg_matches + .value_of_os("to") + .map(|p| current_dir.join(p)) + .or_else(|| { + config_file + .as_ref() + .map(|c| c.to.as_ref()) + .flatten() + .map(|p| config_dir.join(p)) + }) + .ok_or_else(|| Error::msg("\"to\" not configured"))? + .canonicalize() + .context("Could not canonicalize \"to\" path")?, + matches: transcode_matches, + jobs: arg_matches + .value_of_os("jobs") + .map(|jobs_os_str| { + let jobs_str = jobs_os_str.to_str().with_context(|| { + // TODO: use `OsStr.display` when it lands + // https://github.com/rust-lang/rust/pull/80841 + format!( + "Could not convert \"jobs\" argument to string due to invalid characters", + ) + })?; + jobs_str.parse().with_context(|| { + format!( + "Could not parse \"jobs\" argument \"{}\" to a number", + &jobs_str + ) + }) + }) + .transpose()?, + }) } fn load_config_file(path: &Path) -> Result> { - let mut file = match std::fs::File::open(path) { - Ok(file) => file, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(Error::new(err)), - }; - let config: ConfigFile = - serde_yaml::from_reader(&mut file).context("Could not parse config file")?; - Ok(Some(config)) + let mut file = match std::fs::File::open(path) { + Ok(file) => file, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(Error::new(err)), + }; + let config: ConfigFile = + serde_yaml::from_reader(&mut file).context("Could not parse config file")?; + Ok(Some(config)) } diff --git a/src/main.rs b/src/main.rs index 3f0b328..29d67d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,14 +8,14 @@ use glib::{GBoxed, GString}; use gstreamer::{element_error, prelude::*, Element}; use gstreamer_base::prelude::*; use std::{ - borrow::Cow, - error::Error as StdError, - ffi, fmt, - fmt::Write as FmtWrite, - path::{Path, PathBuf}, - result::Result as StdResult, - sync::Arc, - time::Duration, + borrow::Cow, + error::Error as StdError, + ffi, fmt, + fmt::Write as FmtWrite, + path::{Path, PathBuf}, + result::Result as StdResult, + sync::Arc, + time::Duration, }; use tokio::{fs, io::AsyncWriteExt, task, time::interval}; @@ -24,623 +24,623 @@ use tokio::{fs, io::AsyncWriteExt, task, time::interval}; struct GBoxErrorWrapper(Arc); impl GBoxErrorWrapper { - fn new(err: Error) -> Self { - GBoxErrorWrapper(Arc::new(err)) - } + fn new(err: Error) -> Self { + GBoxErrorWrapper(Arc::new(err)) + } } impl StdError for GBoxErrorWrapper { - fn source(&self) -> Option<&(dyn StdError + 'static)> { - self.0.source() - } + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.0.source() + } } impl fmt::Display for GBoxErrorWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { - self.0.fmt(f) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { + self.0.fmt(f) + } } #[derive(Debug, derive_more::Display, derive_more::Error)] #[display(fmt = "Received error from {}: {} (debug: {:?})", src, error, debug)] struct GErrorMessage { - src: String, - error: String, - debug: Option, - source: glib::Error, + src: String, + error: String, + debug: Option, + source: glib::Error, } fn gmake>(factory_name: &str) -> Result { - let res = gstreamer::ElementFactory::make(factory_name, None) - .with_context(|| format!("Could not make gstreamer Element \"{}\"", factory_name))? - .downcast() - .ok() - .with_context(|| { - format!( - "Could not cast gstreamer Element \"{}\" into `{}`", - factory_name, - std::any::type_name::() - ) - })?; - Ok(res) + let res = gstreamer::ElementFactory::make(factory_name, None) + .with_context(|| format!("Could not make gstreamer Element \"{}\"", factory_name))? + .downcast() + .ok() + .with_context(|| { + format!( + "Could not cast gstreamer Element \"{}\" into `{}`", + factory_name, + std::any::type_name::() + ) + })?; + Ok(res) } #[derive(Debug, Clone)] pub struct ConversionArgs { - rel_from_path: PathBuf, - transcode: Transcode, + rel_from_path: PathBuf, + transcode: Transcode, } fn get_conversion_args(config: &Config) -> impl Iterator> + '_ { - walkdir::WalkDir::new(&config.from) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .map(move |e| -> Result> { - let from_bytes = path_to_bytes(e.path()); + walkdir::WalkDir::new(&config.from) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .map(move |e| -> Result> { + let from_bytes = path_to_bytes(e.path()); - let transcode = config - .matches - .iter() - .filter(|m| { - m.regexes - .iter() - .any(|regex| regex.is_match(from_bytes.as_ref())) - }) - .map(|m| m.to.clone()) - .next(); - let transcode = if let Some(transcode) = transcode { - transcode - } else { - return Ok(None); - }; + let transcode = config + .matches + .iter() + .filter(|m| { + m.regexes + .iter() + .any(|regex| regex.is_match(from_bytes.as_ref())) + }) + .map(|m| m.to.clone()) + .next(); + let transcode = if let Some(transcode) = transcode { + transcode + } else { + return Ok(None); + }; - let rel_path = e.path().strip_prefix(&config.from).with_context(|| { - format!( - "Unable to get relative path for {} from {}", - e.path().display(), - config.from.display() - ) - })?; + let rel_path = e.path().strip_prefix(&config.from).with_context(|| { + format!( + "Unable to get relative path for {} from {}", + e.path().display(), + config.from.display() + ) + })?; - let mut to = config.to.join(&rel_path); - to.set_extension(transcode.extension()); + let mut to = config.to.join(&rel_path); + to.set_extension(transcode.extension()); - let is_newer = { - let from_mtime = e - .metadata() - .map_err(Error::new) - .and_then(|md| md.modified().map_err(Error::new)) - .with_context(|| { - format!( - "Unable to get mtime for \"from\" file {}", - e.path().display() - ) - })?; - let to_mtime = to.metadata().and_then(|md| md.modified()); - match to_mtime { - Ok(to_mtime) => to_mtime < from_mtime, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, - Err(err) => { - return Err(err).with_context(|| { - format!("Unable to get mtime for \"to\" file {}", to.display()) - }) - } - } - }; + let is_newer = { + let from_mtime = e + .metadata() + .map_err(Error::new) + .and_then(|md| md.modified().map_err(Error::new)) + .with_context(|| { + format!( + "Unable to get mtime for \"from\" file {}", + e.path().display() + ) + })?; + let to_mtime = to.metadata().and_then(|md| md.modified()); + match to_mtime { + Ok(to_mtime) => to_mtime < from_mtime, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, + Err(err) => { + return Err(err).with_context(|| { + format!("Unable to get mtime for \"to\" file {}", to.display()) + }) + } + } + }; - if is_newer { - Ok(Some(ConversionArgs { - rel_from_path: rel_path.to_path_buf(), - transcode, - })) - } else { - Ok(None) - } - }) - .filter_map(|e| e.transpose()) + if is_newer { + Ok(Some(ConversionArgs { + rel_from_path: rel_path.to_path_buf(), + transcode, + })) + } else { + Ok(None) + } + }) + .filter_map(|e| e.transpose()) } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - task::LocalSet::new() - .run_until(async move { - let (ui_queue, ui_fut) = ui::init(); + task::LocalSet::new() + .run_until(async move { + let (ui_queue, ui_fut) = ui::init(); - let main_handle = async move { - let ok = task::spawn_local(main_loop(ui_queue)) - .await - .context("Main task failed")??; - Result::<_>::Ok(ok) - }; + let main_handle = async move { + let ok = task::spawn_local(main_loop(ui_queue)) + .await + .context("Main task failed")??; + Result::<_>::Ok(ok) + }; - let ui_handle = async move { - let ok = task::spawn_local(ui_fut) - .await - .context("Ui task failed")? - .context("Ui failed")?; - Result::<_>::Ok(ok) - }; + let ui_handle = async move { + let ok = task::spawn_local(ui_fut) + .await + .context("Ui task failed")? + .context("Ui failed")?; + Result::<_>::Ok(ok) + }; - future::try_join(main_handle, ui_handle).await?; - Ok(()) - }) - .await + future::try_join(main_handle, ui_handle).await?; + Ok(()) + }) + .await } async fn main_loop(ui_queue: ui::MsgQueue) -> Result<()> { - let (config, conv_args) = task::spawn_blocking(|| -> Result<_> { - gstreamer::init()?; - let config = config::config().context("Could not get the config")?; + let (config, conv_args) = task::spawn_blocking(|| -> Result<_> { + gstreamer::init()?; + let config = config::config().context("Could not get the config")?; - let conv_args = get_conversion_args(&config) - .collect::>>() - .context("Failed loading dir structure")?; + let conv_args = get_conversion_args(&config) + .collect::>>() + .context("Failed loading dir structure")?; - Ok((config, conv_args)) - }) - .await - .context("Init task failed")??; + Ok((config, conv_args)) + }) + .await + .context("Init task failed")??; - let log_path = Path::new(".") - .canonicalize() - .context("Unable to canonicalize path to log file")? - .join("audio-conv.log"); + let log_path = Path::new(".") + .canonicalize() + .context("Unable to canonicalize path to log file")? + .join("audio-conv.log"); - ui_queue.push(ui::Msg::Init { - task_len: conv_args.len(), - log_path: log_path.clone(), - }); + ui_queue.push(ui::Msg::Init { + task_len: conv_args.len(), + log_path: log_path.clone(), + }); - let concurrent_jobs = config.jobs.unwrap_or_else(|| num_cpus::get()); + let concurrent_jobs = config.jobs.unwrap_or_else(|| num_cpus::get()); - stream::iter(conv_args.into_iter().enumerate()) - .map(Ok) - .try_for_each_concurrent(concurrent_jobs, |(i, args)| { - let config = &config; - let ui_queue = &ui_queue; - let log_path = &log_path; + stream::iter(conv_args.into_iter().enumerate()) + .map(Ok) + .try_for_each_concurrent(concurrent_jobs, |(i, args)| { + let config = &config; + let ui_queue = &ui_queue; + let log_path = &log_path; - async move { - ui_queue.push(ui::Msg::TaskStart { - id: i, - args: args.clone(), - }); + async move { + ui_queue.push(ui::Msg::TaskStart { + id: i, + args: args.clone(), + }); - match transcode(config, &args, i, ui_queue).await { - Ok(()) => ui_queue.push(ui::Msg::TaskEnd { id: i }), - Err(err) => { - let err = err.context(format!( - "Transcoding failed for {}", - args.rel_from_path.display() - )); + match transcode(config, &args, i, ui_queue).await { + Ok(()) => ui_queue.push(ui::Msg::TaskEnd { id: i }), + Err(err) => { + let err = err.context(format!( + "Transcoding failed for {}", + args.rel_from_path.display() + )); - let mut log_file = match fs::OpenOptions::new() - .create(true) - .append(true) - .open(log_path) - .await - { - Ok(log_file) => log_file, - Err(fs_err) => { - let err = err.context(fs_err).context("Unable to open log file"); - return Err(err); - } - }; + let mut log_file = match fs::OpenOptions::new() + .create(true) + .append(true) + .open(log_path) + .await + { + Ok(log_file) => log_file, + Err(fs_err) => { + let err = err.context(fs_err).context("Unable to open log file"); + return Err(err); + } + }; - let mut err_str = String::new(); - if let Err(write_err) = write!(&mut err_str, "{:?}\n", err) { - let err = err.context(format!( - "Unable to format transcoding error for logging (write error: {})", - write_err - )); - return Err(err); - } + let mut err_str = String::new(); + if let Err(write_err) = write!(&mut err_str, "{:?}\n", err) { + let err = err.context(format!( + "Unable to format transcoding error for logging (write error: {})", + write_err + )); + return Err(err); + } - log_file - .write_all(err_str.as_ref()) - .await - .map_err(|fs_err| { - err.context(format!( - "Unable to write transcoding error to log file (fs error: {})", - fs_err - )) - })?; + log_file + .write_all(err_str.as_ref()) + .await + .map_err(|fs_err| { + err.context(format!( + "Unable to write transcoding error to log file (fs error: {})", + fs_err + )) + })?; - ui_queue.push(ui::Msg::TaskError { id: i }); - } - } + ui_queue.push(ui::Msg::TaskError { id: i }); + } + } - Result::<_>::Ok(()) - } - }) - .await?; + Result::<_>::Ok(()) + } + }) + .await?; - ui_queue.push(ui::Msg::Exit); + ui_queue.push(ui::Msg::Exit); - Ok(()) + Ok(()) } async fn transcode( - config: &Config, - args: &ConversionArgs, - task_id: usize, - queue: &ui::MsgQueue, + config: &Config, + args: &ConversionArgs, + task_id: usize, + queue: &ui::MsgQueue, ) -> Result<()> { - let from_path = config.from.join(&args.rel_from_path); - let mut to_path = config.to.join(&args.rel_from_path); + let from_path = config.from.join(&args.rel_from_path); + let mut to_path = config.to.join(&args.rel_from_path); - fs::create_dir_all( - to_path - .parent() - .with_context(|| format!("Could not get parent dir for {}", to_path.display()))?, - ) - .await?; + fs::create_dir_all( + to_path + .parent() + .with_context(|| format!("Could not get parent dir for {}", to_path.display()))?, + ) + .await?; - // encode into a tmp file first, then rename to actuall file name, that way we're writing - // "whole" files to the intended file path, ignoring partial files in the mtime check - let to_path_tmp = to_path.with_extension("tmp"); + // encode into a tmp file first, then rename to actuall file name, that way we're writing + // "whole" files to the intended file path, ignoring partial files in the mtime check + let to_path_tmp = to_path.with_extension("tmp"); - rm_file_on_err(&to_path_tmp, async { - match args.transcode { - Transcode::Copy => { - fs::copy(&from_path, &to_path_tmp).await.with_context(|| { - format!( - "Could not copy file from {} to {}", - from_path.display(), - to_path_tmp.display() - ) - })?; - } - _ => { - to_path.set_extension(args.transcode.extension()); + rm_file_on_err(&to_path_tmp, async { + match args.transcode { + Transcode::Copy => { + fs::copy(&from_path, &to_path_tmp).await.with_context(|| { + format!( + "Could not copy file from {} to {}", + from_path.display(), + to_path_tmp.display() + ) + })?; + } + _ => { + to_path.set_extension(args.transcode.extension()); - transcode_gstreamer( - &from_path, - &to_path_tmp, - args.transcode.clone(), - task_id, - queue, - ) - .await? - } - } + transcode_gstreamer( + &from_path, + &to_path_tmp, + args.transcode.clone(), + task_id, + queue, + ) + .await? + } + } - fs::rename(&to_path_tmp, &to_path).await.with_context(|| { - format!( - "Could not rename temporary file {} to {}", - to_path_tmp.display(), - to_path.display() - ) - }) - }) - .await + fs::rename(&to_path_tmp, &to_path).await.with_context(|| { + format!( + "Could not rename temporary file {} to {}", + to_path_tmp.display(), + to_path.display() + ) + }) + }) + .await } async fn transcode_gstreamer( - from_path: &Path, - to_path: &Path, - transcode: Transcode, - task_id: usize, - queue: &ui::MsgQueue, + from_path: &Path, + to_path: &Path, + transcode: Transcode, + task_id: usize, + queue: &ui::MsgQueue, ) -> Result<()> { - let file_src: Element = gmake("filesrc")?; - file_src.set_property("location", &path_to_gstring(&from_path))?; + let file_src: Element = gmake("filesrc")?; + file_src.set_property("location", &path_to_gstring(&from_path))?; - let decodebin: Element = gmake("decodebin")?; + let decodebin: Element = gmake("decodebin")?; - let src_elems: &[&Element] = &[&file_src, &decodebin]; + let src_elems: &[&Element] = &[&file_src, &decodebin]; - let pipeline = gstreamer::Pipeline::new(None); + let pipeline = gstreamer::Pipeline::new(None); - pipeline.add_many(src_elems)?; - Element::link_many(src_elems)?; + pipeline.add_many(src_elems)?; + Element::link_many(src_elems)?; - // downgrade pipeline RC to a weak RC to break the reference cycle - let pipeline_weak = pipeline.downgrade(); + // downgrade pipeline RC to a weak RC to break the reference cycle + let pipeline_weak = pipeline.downgrade(); - let to_path_clone = to_path.to_owned(); - decodebin.connect_pad_added(move |decodebin, src_pad| { - let insert_sink = || -> Result<()> { - let pipeline = match pipeline_weak.upgrade() { - Some(pipeline) => pipeline, - None => { - // pipeline already destroyed... ignoring - return Ok(()); - } - }; + let to_path_clone = to_path.to_owned(); + decodebin.connect_pad_added(move |decodebin, src_pad| { + let insert_sink = || -> Result<()> { + let pipeline = match pipeline_weak.upgrade() { + Some(pipeline) => pipeline, + None => { + // pipeline already destroyed... ignoring + return Ok(()); + } + }; - let is_audio = src_pad.current_caps().and_then(|caps| { - caps.structure(0).map(|s| { - let name = s.name(); - name.starts_with("audio/") - }) - }); - match is_audio { - None => { - return Err(Error::msg(format!( - "Failed to get media type from pad {}", - src_pad.name() - ))); - } - Some(false) => { - // not audio pad... ignoring - return Ok(()); - } - Some(true) => {} - } + let is_audio = src_pad.current_caps().and_then(|caps| { + caps.structure(0).map(|s| { + let name = s.name(); + name.starts_with("audio/") + }) + }); + match is_audio { + None => { + return Err(Error::msg(format!( + "Failed to get media type from pad {}", + src_pad.name() + ))); + } + Some(false) => { + // not audio pad... ignoring + return Ok(()); + } + Some(true) => {} + } - let resample: Element = gmake("audioresample")?; - // quality from 0 to 10 - resample.set_property("quality", &10)?; + let resample: Element = gmake("audioresample")?; + // quality from 0 to 10 + resample.set_property("quality", &10)?; - let mut dest_elems = vec![ - resample, - // `audioconvert` converts audio format, bitdepth, ... - gmake("audioconvert")?, - ]; + let mut dest_elems = vec![ + resample, + // `audioconvert` converts audio format, bitdepth, ... + gmake("audioconvert")?, + ]; - match &transcode { - Transcode::Opus { - bitrate, - bitrate_type, - } => { - let encoder: Element = gmake("opusenc")?; - encoder.set_property( - "bitrate", - &i32::from(*bitrate) - .checked_mul(1_000) - .context("Bitrate overflowed")?, - )?; - encoder.set_property_from_str( - "bitrate-type", - match bitrate_type { - config::BitrateType::Vbr => "1", - config::BitrateType::Cbr => "0", - }, - ); + match &transcode { + Transcode::Opus { + bitrate, + bitrate_type, + } => { + let encoder: Element = gmake("opusenc")?; + encoder.set_property( + "bitrate", + &i32::from(*bitrate) + .checked_mul(1_000) + .context("Bitrate overflowed")?, + )?; + encoder.set_property_from_str( + "bitrate-type", + match bitrate_type { + config::BitrateType::Vbr => "1", + config::BitrateType::Cbr => "0", + }, + ); - dest_elems.push(encoder); - dest_elems.push(gmake("oggmux")?); - } + dest_elems.push(encoder); + dest_elems.push(gmake("oggmux")?); + } - Transcode::Flac { compression } => { - let encoder: Element = gmake("flacenc")?; - encoder.set_property_from_str("quality", &compression.to_string()); - dest_elems.push(encoder); - } + Transcode::Flac { compression } => { + let encoder: Element = gmake("flacenc")?; + encoder.set_property_from_str("quality", &compression.to_string()); + dest_elems.push(encoder); + } - Transcode::Mp3 { - bitrate, - bitrate_type, - } => { - let encoder: Element = gmake("lamemp3enc")?; - // target: "1" = "bitrate" - encoder.set_property_from_str("target", "1"); - encoder.set_property("bitrate", &i32::from(*bitrate))?; - encoder.set_property( - "cbr", - match bitrate_type { - config::BitrateType::Vbr => &false, - config::BitrateType::Cbr => &true, - }, - )?; + Transcode::Mp3 { + bitrate, + bitrate_type, + } => { + let encoder: Element = gmake("lamemp3enc")?; + // target: "1" = "bitrate" + encoder.set_property_from_str("target", "1"); + encoder.set_property("bitrate", &i32::from(*bitrate))?; + encoder.set_property( + "cbr", + match bitrate_type { + config::BitrateType::Vbr => &false, + config::BitrateType::Cbr => &true, + }, + )?; - dest_elems.push(encoder); - dest_elems.push(gmake("id3v2mux")?); - } + dest_elems.push(encoder); + dest_elems.push(gmake("id3v2mux")?); + } - Transcode::Copy => { - // already handled outside this fn - unreachable!(); - } - }; + Transcode::Copy => { + // already handled outside this fn + unreachable!(); + } + }; - let file_dest: gstreamer_base::BaseSink = gmake("filesink")?; - file_dest.set_property("location", &path_to_gstring(&to_path_clone))?; - file_dest.set_sync(false); - dest_elems.push(file_dest.upcast()); + let file_dest: gstreamer_base::BaseSink = gmake("filesink")?; + file_dest.set_property("location", &path_to_gstring(&to_path_clone))?; + file_dest.set_sync(false); + dest_elems.push(file_dest.upcast()); - let dest_elem_refs: Vec<_> = dest_elems.iter().collect(); - pipeline.add_many(&dest_elem_refs)?; - Element::link_many(&dest_elem_refs)?; + let dest_elem_refs: Vec<_> = dest_elems.iter().collect(); + pipeline.add_many(&dest_elem_refs)?; + Element::link_many(&dest_elem_refs)?; - for e in &dest_elems { - e.sync_state_with_parent()?; - } + for e in &dest_elems { + e.sync_state_with_parent()?; + } - let sink_pad = dest_elems - .get(0) - .unwrap() - .static_pad("sink") - .expect("1. dest element has no sinkpad"); - src_pad.link(&sink_pad)?; + let sink_pad = dest_elems + .get(0) + .unwrap() + .static_pad("sink") + .expect("1. dest element has no sinkpad"); + src_pad.link(&sink_pad)?; - Ok(()) - }; + Ok(()) + }; - if let Err(err) = insert_sink() { - let details = gstreamer::Structure::builder("error-details") - .field("error", &GBoxErrorWrapper::new(err)) - .build(); + if let Err(err) = insert_sink() { + let details = gstreamer::Structure::builder("error-details") + .field("error", &GBoxErrorWrapper::new(err)) + .build(); - element_error!( - decodebin, - gstreamer::LibraryError::Failed, - ("Failed to insert sink"), - details: details - ); - } - }); + element_error!( + decodebin, + gstreamer::LibraryError::Failed, + ("Failed to insert sink"), + details: details + ); + } + }); - let bus = pipeline.bus().context("Could not get bus for pipeline")?; + let bus = pipeline.bus().context("Could not get bus for pipeline")?; - pipeline - .set_state(gstreamer::State::Playing) - .context("Unable to set the pipeline to the `Playing` state")?; + pipeline + .set_state(gstreamer::State::Playing) + .context("Unable to set the pipeline to the `Playing` state")?; - let stream_processor = async { - bus.stream() - .map::, _>(|msg| { - use gstreamer::MessageView; + let stream_processor = async { + bus.stream() + .map::, _>(|msg| { + use gstreamer::MessageView; - match msg.view() { - // MessageView::Progress() => { + match msg.view() { + // MessageView::Progress() => { - // } - MessageView::Eos(..) => { - // we need to actively stop pulling the stream, that's because stream will - // never end despite yielding an `Eos` message - Ok(false) - } - MessageView::Error(err) => { - let pipe_stop_res = pipeline.set_state(gstreamer::State::Null); + // } + MessageView::Eos(..) => { + // we need to actively stop pulling the stream, that's because stream will + // never end despite yielding an `Eos` message + Ok(false) + } + MessageView::Error(err) => { + let pipe_stop_res = pipeline.set_state(gstreamer::State::Null); - let err: Error = err - .details() - .and_then(|details| { - if details.name() != "error-details" { - return None; - } + let err: Error = err + .details() + .and_then(|details| { + if details.name() != "error-details" { + return None; + } - let err = details - .get::<&GBoxErrorWrapper>("error") - .unwrap() - .clone() - .into(); - Some(err) - }) - .unwrap_or_else(|| { - GErrorMessage { - src: msg - .src() - .map(|s| String::from(s.path_string())) - .unwrap_or_else(|| String::from("None")), - error: err.error().to_string(), - debug: err.debug(), - source: err.error(), - } - .into() - }); + let err = details + .get::<&GBoxErrorWrapper>("error") + .unwrap() + .clone() + .into(); + Some(err) + }) + .unwrap_or_else(|| { + GErrorMessage { + src: msg + .src() + .map(|s| String::from(s.path_string())) + .unwrap_or_else(|| String::from("None")), + error: err.error().to_string(), + debug: err.debug(), + source: err.error(), + } + .into() + }); - if let Err(pipe_err) = pipe_stop_res { - let err = err.context(pipe_err).context( - "Unable to set the pipeline to the `Null` state, after error", - ); - Err(err) - } else { - Err(err) - } - } - _ => Ok(true), - } - }) - .take_while(|e| { - if let Ok(false) = e { - futures::future::ready(false) - } else { - futures::future::ready(true) - } - }) - .try_for_each(|_| futures::future::ready(Ok(()))) - .await?; + if let Err(pipe_err) = pipe_stop_res { + let err = err.context(pipe_err).context( + "Unable to set the pipeline to the `Null` state, after error", + ); + Err(err) + } else { + Err(err) + } + } + _ => Ok(true), + } + }) + .take_while(|e| { + if let Ok(false) = e { + futures::future::ready(false) + } else { + futures::future::ready(true) + } + }) + .try_for_each(|_| futures::future::ready(Ok(()))) + .await?; - Result::<_>::Ok(()) - }; - pin_mut!(stream_processor); + Result::<_>::Ok(()) + }; + pin_mut!(stream_processor); - let mut progress_interval = interval(Duration::from_millis(ui::UPDATE_INTERVAL_MILLIS / 2)); - let progress_processor = async { - use gstreamer::ClockTime; + let mut progress_interval = interval(Duration::from_millis(ui::UPDATE_INTERVAL_MILLIS / 2)); + let progress_processor = async { + use gstreamer::ClockTime; - loop { - progress_interval.tick().await; + loop { + progress_interval.tick().await; - let dur = decodebin - .query_duration::() - .map(|time| time.nseconds()); + let dur = decodebin + .query_duration::() + .map(|time| time.nseconds()); - let ratio = dur.and_then(|dur| { - if dur == 0 { - return None; - } + let ratio = dur.and_then(|dur| { + if dur == 0 { + return None; + } - let pos = decodebin - .query_position::() - .map(|time| time.nseconds()); + let pos = decodebin + .query_position::() + .map(|time| time.nseconds()); - pos.map(|pos| { - let ratio = pos as f64 / dur as f64; - ratio.clamp(0.0, 1.0) - }) - }); + pos.map(|pos| { + let ratio = pos as f64 / dur as f64; + ratio.clamp(0.0, 1.0) + }) + }); - if let Some(ratio) = ratio { - queue.push(ui::Msg::TaskProgress { id: task_id, ratio }); - } - } + if let Some(ratio) = ratio { + queue.push(ui::Msg::TaskProgress { id: task_id, ratio }); + } + } - #[allow(unreachable_code)] - Result::<_>::Ok(()) - }; - pin_mut!(progress_processor); + #[allow(unreachable_code)] + Result::<_>::Ok(()) + }; + pin_mut!(progress_processor); - future::try_select(stream_processor, progress_processor) - .await - .map_err(|err| err.factor_first().0)?; + future::try_select(stream_processor, progress_processor) + .await + .map_err(|err| err.factor_first().0)?; - pipeline - .set_state(gstreamer::State::Null) - .context("Unable to set the pipeline to the `Null` state")?; + pipeline + .set_state(gstreamer::State::Null) + .context("Unable to set the pipeline to the `Null` state")?; - Ok(()) + Ok(()) } async fn rm_file_on_err(path: &Path, f: F) -> Result where - F: Future>, + F: Future>, { - match f.await { - Err(err) => match fs::remove_file(path).await { - Ok(()) => Err(err), - Err(fs_err) if fs_err.kind() == std::io::ErrorKind::NotFound => Err(err), - Err(fs_err) => { - let err = err - .context(fs_err) - .context(format!("Removing file {} failed", path.display())); - Err(err) - } - }, - res @ Ok(..) => res, - } + match f.await { + Err(err) => match fs::remove_file(path).await { + Ok(()) => Err(err), + Err(fs_err) if fs_err.kind() == std::io::ErrorKind::NotFound => Err(err), + Err(fs_err) => { + let err = err + .context(fs_err) + .context(format!("Removing file {} failed", path.display())); + Err(err) + } + }, + res @ Ok(..) => res, + } } fn path_to_bytes(path: &Path) -> Cow<'_, [u8]> { - // https://stackoverflow.com/a/59224987/5572146 - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - Cow::Borrowed(path.as_os_str().as_bytes()) - } + // https://stackoverflow.com/a/59224987/5572146 + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + Cow::Borrowed(path.as_os_str().as_bytes()) + } - #[cfg(windows)] - { - // NOT TESTED - // FIXME: test and post answer to https://stackoverflow.com/questions/38948669 - use std::os::windows::ffi::OsStrExt; - let buf: Vec = path - .as_os_str() - .encode_wide() - .map(|char| char.to_ne_bytes()) - .flatten() - .collect(); - Cow::Owned(buf) - } + #[cfg(windows)] + { + // NOT TESTED + // FIXME: test and post answer to https://stackoverflow.com/questions/38948669 + use std::os::windows::ffi::OsStrExt; + let buf: Vec = path + .as_os_str() + .encode_wide() + .map(|char| char.to_ne_bytes()) + .flatten() + .collect(); + Cow::Owned(buf) + } } fn path_to_gstring(path: &Path) -> GString { - let buf = path_to_bytes(path); - ffi::CString::new(buf) - .expect("Path contained null byte") - .into() + let buf = path_to_bytes(path); + ffi::CString::new(buf) + .expect("Path contained null byte") + .into() } diff --git a/src/ui.rs b/src/ui.rs index 2f5cad6..1a7837c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,8 +2,8 @@ use crate::ConversionArgs; use anyhow::{Context, Result}; use futures::Future; use std::{ - borrow::Cow, cell::RefCell, collections::HashMap, io, mem, path::PathBuf, rc::Rc, - time::Duration, + borrow::Cow, cell::RefCell, collections::HashMap, io, mem, path::PathBuf, rc::Rc, + time::Duration, }; use tokio::{task, time::interval}; use tui::{backend::CrosstermBackend, Terminal}; @@ -12,272 +12,272 @@ pub const UPDATE_INTERVAL_MILLIS: u64 = 100; #[derive(Debug)] pub enum Msg { - Init { task_len: usize, log_path: PathBuf }, - Exit, - TaskStart { id: usize, args: ConversionArgs }, - TaskEnd { id: usize }, - TaskProgress { id: usize, ratio: f64 }, - TaskError { id: usize }, + Init { task_len: usize, log_path: PathBuf }, + Exit, + TaskStart { id: usize, args: ConversionArgs }, + TaskEnd { id: usize }, + TaskProgress { id: usize, ratio: f64 }, + TaskError { id: usize }, } #[derive(Debug, Clone)] pub struct MsgQueue { - inner: Rc>>, + inner: Rc>>, } impl MsgQueue { - fn new() -> MsgQueue { - MsgQueue { - inner: Rc::new(RefCell::new(Vec::new())), - } - } + fn new() -> MsgQueue { + MsgQueue { + inner: Rc::new(RefCell::new(Vec::new())), + } + } - pub fn push(&self, msg: Msg) { - self.inner.borrow_mut().push(msg); - } + pub fn push(&self, msg: Msg) { + self.inner.borrow_mut().push(msg); + } - fn swap_inner(&self, other: &mut Vec) { - let mut inner = self.inner.borrow_mut(); - mem::swap(&mut *inner, other) - } + fn swap_inner(&self, other: &mut Vec) { + let mut inner = self.inner.borrow_mut(); + mem::swap(&mut *inner, other) + } } struct State { - terminal: Terminal>, - log_path: Option, - task_len: Option, - ended_tasks: usize, - running_tasks: HashMap, - has_rendered: bool, - has_errored: bool, + terminal: Terminal>, + log_path: Option, + task_len: Option, + ended_tasks: usize, + running_tasks: HashMap, + has_rendered: bool, + has_errored: bool, } impl State { - fn new() -> Result { - let terminal = Terminal::new(CrosstermBackend::new(io::stdout())) - .context("Unable to create ui terminal")?; + fn new() -> Result { + let terminal = Terminal::new(CrosstermBackend::new(io::stdout())) + .context("Unable to create ui terminal")?; - Ok(State { - terminal, - log_path: None, - task_len: None, - ended_tasks: 0, - running_tasks: HashMap::new(), - has_rendered: false, - has_errored: false, - }) - } + Ok(State { + terminal, + log_path: None, + task_len: None, + ended_tasks: 0, + running_tasks: HashMap::new(), + has_rendered: false, + has_errored: false, + }) + } - fn process_msg(&mut self, msg: Msg) -> Result { - match msg { - Msg::Init { task_len, log_path } => { - self.task_len = Some(task_len); - self.log_path = Some(log_path); - } - Msg::Exit => return Ok(false), - Msg::TaskStart { id, args } => { - self.running_tasks.insert( - id, - Task { - id, - ratio: None, - args, - }, - ); - } - Msg::TaskEnd { id } => { - self.running_tasks - .remove(&id) - .context("Unable to remove finished task; could't find task")?; - self.ended_tasks += 1; - } - Msg::TaskProgress { id, ratio } => { - let mut task = self - .running_tasks - .get_mut(&id) - .context("Unable to update task progress; could't find task")?; - task.ratio = Some(ratio); - } - Msg::TaskError { id } => { - // TODO - self.running_tasks - .remove(&id) - .context("Unable to remove errored task; could't find task")?; - self.ended_tasks += 1; - self.has_errored = true; - } - } + fn process_msg(&mut self, msg: Msg) -> Result { + match msg { + Msg::Init { task_len, log_path } => { + self.task_len = Some(task_len); + self.log_path = Some(log_path); + } + Msg::Exit => return Ok(false), + Msg::TaskStart { id, args } => { + self.running_tasks.insert( + id, + Task { + id, + ratio: None, + args, + }, + ); + } + Msg::TaskEnd { id } => { + self.running_tasks + .remove(&id) + .context("Unable to remove finished task; could't find task")?; + self.ended_tasks += 1; + } + Msg::TaskProgress { id, ratio } => { + let mut task = self + .running_tasks + .get_mut(&id) + .context("Unable to update task progress; could't find task")?; + task.ratio = Some(ratio); + } + Msg::TaskError { id } => { + // TODO + self.running_tasks + .remove(&id) + .context("Unable to remove errored task; could't find task")?; + self.ended_tasks += 1; + self.has_errored = true; + } + } - Ok(true) - } + Ok(true) + } - fn render(&mut self) -> Result<()> { - use tui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::Text, - widgets::{Block, Borders, Gauge, Paragraph}, - }; + fn render(&mut self) -> Result<()> { + use tui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::Text, + widgets::{Block, Borders, Gauge, Paragraph}, + }; - let task_len = if let Some(task_len) = self.task_len { - task_len - } else { - return Ok(()); - }; + let task_len = if let Some(task_len) = self.task_len { + task_len + } else { + return Ok(()); + }; - if task_len == 0 { - return Ok(()); - } + if task_len == 0 { + return Ok(()); + } - let tasks_ended = self.ended_tasks; + let tasks_ended = self.ended_tasks; - let mut running_tasks: Vec<_> = self.running_tasks.values().cloned().collect(); + let mut running_tasks: Vec<_> = self.running_tasks.values().cloned().collect(); - running_tasks.sort_by_key(|task| task.id); + running_tasks.sort_by_key(|task| task.id); - if !self.has_rendered { - self.terminal.clear().context("Clearing ui failed")?; - self.has_rendered = true; - } + if !self.has_rendered { + self.terminal.clear().context("Clearing ui failed")?; + self.has_rendered = true; + } - let error_text = match self.has_errored { - true => { - let text: Cow<'static, str> = self - .log_path - .as_ref() - .map(|lp| { - let text = format!("Error(s) occurred and were logged to {}", lp.display()); - Cow::Owned(text) - }) - .unwrap_or_else(|| Cow::Borrowed("Error(s) occurred")); - Some(text) - } - false => None, - }; + let error_text = match self.has_errored { + true => { + let text: Cow<'static, str> = self + .log_path + .as_ref() + .map(|lp| { + let text = format!("Error(s) occurred and were logged to {}", lp.display()); + Cow::Owned(text) + }) + .unwrap_or_else(|| Cow::Borrowed("Error(s) occurred")); + Some(text) + } + false => None, + }; - self.terminal - .draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints([Constraint::Percentage(90), Constraint::Percentage(10)].as_ref()) - .split(f.size()); + self.terminal + .draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Percentage(90), Constraint::Percentage(10)].as_ref()) + .split(f.size()); - let mut task_rect = chunks[0]; + let mut task_rect = chunks[0]; - if error_text.is_some() { - task_rect.height -= 3; - } + if error_text.is_some() { + task_rect.height -= 3; + } - for (row, task) in running_tasks - .into_iter() - .take(task_rect.height as usize / 2) - .enumerate() - { - f.render_widget( - Gauge::default() - .label(task.args.rel_from_path.to_string_lossy().as_ref()) - .gauge_style( - Style::default() - .fg(Color::White) - .bg(Color::Black) - .add_modifier(Modifier::ITALIC), - ) - .ratio(task.ratio.unwrap_or(0.0)), - Rect::new( - task_rect.x, - task_rect.y + row as u16 * 2, - task_rect.width, - 1, - ), - ); - } + for (row, task) in running_tasks + .into_iter() + .take(task_rect.height as usize / 2) + .enumerate() + { + f.render_widget( + Gauge::default() + .label(task.args.rel_from_path.to_string_lossy().as_ref()) + .gauge_style( + Style::default() + .fg(Color::White) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .ratio(task.ratio.unwrap_or(0.0)), + Rect::new( + task_rect.x, + task_rect.y + row as u16 * 2, + task_rect.width, + 1, + ), + ); + } - if let Some(error_text) = error_text { - f.render_widget( - Paragraph::new(Text::raw(error_text)).style( - Style::default() - .fg(Color::Red) - .bg(Color::Black) - .add_modifier(Modifier::BOLD), - ), - Rect::new(task_rect.x, task_rect.height + 1, task_rect.width, 2), - ); - } + if let Some(error_text) = error_text { + f.render_widget( + Paragraph::new(Text::raw(error_text)).style( + Style::default() + .fg(Color::Red) + .bg(Color::Black) + .add_modifier(Modifier::BOLD), + ), + Rect::new(task_rect.x, task_rect.height + 1, task_rect.width, 2), + ); + } - f.render_widget( - Gauge::default() - .block( - Block::default() - .borders(Borders::ALL) - .title("Overall Progress"), - ) - .gauge_style( - Style::default() - .fg(Color::White) - .bg(Color::Black) - .add_modifier(Modifier::ITALIC), - ) - .ratio(tasks_ended as f64 / task_len as f64), - chunks[1], - ); - }) - .context("Rendering ui failed")?; + f.render_widget( + Gauge::default() + .block( + Block::default() + .borders(Borders::ALL) + .title("Overall Progress"), + ) + .gauge_style( + Style::default() + .fg(Color::White) + .bg(Color::Black) + .add_modifier(Modifier::ITALIC), + ) + .ratio(tasks_ended as f64 / task_len as f64), + chunks[1], + ); + }) + .context("Rendering ui failed")?; - Ok(()) - } + Ok(()) + } } #[derive(Debug, Clone)] struct Task { - id: usize, - ratio: Option, - args: ConversionArgs, + id: usize, + ratio: Option, + args: ConversionArgs, } pub fn init() -> (MsgQueue, impl Future>) { - let queue = MsgQueue::new(); + let queue = MsgQueue::new(); - let queue_clone = queue.clone(); - let fut = async move { - let mut interval = interval(Duration::from_millis(UPDATE_INTERVAL_MILLIS)); - let mut wrapped = Some((Vec::new(), State::new()?)); + let queue_clone = queue.clone(); + let fut = async move { + let mut interval = interval(Duration::from_millis(UPDATE_INTERVAL_MILLIS)); + let mut wrapped = Some((Vec::new(), State::new()?)); - loop { - interval.tick().await; + loop { + interval.tick().await; - let (mut current_queue, mut state) = wrapped.take().context("`wrapped` is None")?; + let (mut current_queue, mut state) = wrapped.take().context("`wrapped` is None")?; - queue_clone.swap_inner(&mut current_queue); + queue_clone.swap_inner(&mut current_queue); - let render_res = task::spawn_blocking(move || -> Result<_> { - let mut exit = false; - for msg in current_queue.drain(..) { - if !state.process_msg(msg)? { - exit = true; - } - } + let render_res = task::spawn_blocking(move || -> Result<_> { + let mut exit = false; + for msg in current_queue.drain(..) { + if !state.process_msg(msg)? { + exit = true; + } + } - state.render()?; + state.render()?; - if exit { - Ok(None) - } else { - Ok(Some((current_queue, state))) - } - }) - .await - .context("Ui update task failed")? - .context("Ui update failed")?; + if exit { + Ok(None) + } else { + Ok(Some((current_queue, state))) + } + }) + .await + .context("Ui update task failed")? + .context("Ui update failed")?; - match render_res { - Some(s) => wrapped = Some(s), - None => break, - } - } + match render_res { + Some(s) => wrapped = Some(s), + None => break, + } + } - Result::<_>::Ok(()) - }; + Result::<_>::Ok(()) + }; - (queue, fut) + (queue, fut) }