Multiple input files (#582)

* Multiple input files

* clippy fix

* reset logger for each input file

* remove comment
This commit is contained in:
Zen 2022-03-07 16:22:24 +02:00 committed by GitHub
parent f66a805256
commit 09e552b413
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 219 additions and 173 deletions

View file

@ -4,6 +4,7 @@ use anyhow::{anyhow, Context};
use anyhow::{bail, ensure}; use anyhow::{bail, ensure};
use av1an_core::progress_bar::{get_first_multi_progress_bar, get_progress_bar}; use av1an_core::progress_bar::{get_first_multi_progress_bar, get_progress_bar};
use av1an_core::settings::{InputPixelFormat, PixelFormat}; use av1an_core::settings::{InputPixelFormat, PixelFormat};
use av1an_core::util::read_in_dir;
use av1an_core::ScenecutMethod; use av1an_core::ScenecutMethod;
use av1an_core::{ffmpeg, into_vec}; use av1an_core::{ffmpeg, into_vec};
use av1an_core::{ChunkOrdering, Input}; use av1an_core::{ChunkOrdering, Input};
@ -98,7 +99,7 @@ pub struct CliOpts {
/// ///
/// Can be a video or vapoursynth (.py, .vpy) script. /// Can be a video or vapoursynth (.py, .vpy) script.
#[clap(short, parse(from_os_str))] #[clap(short, parse(from_os_str))]
pub input: PathBuf, pub input: Vec<PathBuf>,
/// Video output file /// Video output file
#[clap(short, parse(from_os_str))] #[clap(short, parse(from_os_str))]
@ -443,169 +444,202 @@ fn confirm(prompt: &str) -> io::Result<bool> {
} }
} }
pub fn parse_cli(args: CliOpts) -> anyhow::Result<EncodeArgs> { /// Given Folder and File path as inputs
let temp = if let Some(path) = args.temp.as_ref() { /// Converts them all to file paths
path.to_str().unwrap().to_owned() /// Converting only depth 1 of Folder paths
pub(crate) fn resolve_file_paths(path: &Path) -> anyhow::Result<Box<dyn Iterator<Item = PathBuf>>> {
// TODO: to validate file extensions
// let valid_media_extensions = ["mkv", "mov", "mp4", "webm", "avi", "qt", "ts", "m2t", "py", "vpy"];
if path.is_file() {
Ok(Box::new(std::iter::once(path.to_path_buf())))
} else if path.is_dir() {
Ok(Box::new(read_in_dir(path)?))
} else { } else {
format!(".{}", hash_path(args.input.as_path())) bail!("path {:?} is not a file or directory", path)
}; }
}
let input = Input::from(args.input.as_path()); /// Returns vector of Encode args ready to be fed to encoder
pub fn parse_cli(args: CliOpts) -> anyhow::Result<Vec<EncodeArgs>> {
let input_paths = &*args.input;
// TODO make an actual constructor for this let inputs: Vec<PathBuf> = input_paths
let mut encode_args = EncodeArgs { .iter()
frames: 0, .flat_map(|input| resolve_file_paths(input))
log_file: if let Some(log_file) = args.log_file.as_ref() { .flatten()
Path::new(&format!("{}.log", log_file)).to_owned() .collect();
} else {
Path::new(&temp).join("log.log")
},
ffmpeg_filter_args: if let Some(args) = args.ffmpeg_filter_args.as_ref() {
shlex::split(args).ok_or_else(|| anyhow!("Failed to split ffmpeg filter arguments"))?
} else {
Vec::new()
},
temp,
force: args.force,
passes: if let Some(passes) = args.passes {
passes
} else {
args.encoder.get_default_pass()
},
video_params: if let Some(args) = args.video_params.as_ref() {
shlex::split(args).ok_or_else(|| anyhow!("Failed to split video encoder arguments"))?
} else {
Vec::new()
},
output_file: if let Some(path) = args.output_file.as_ref() {
let path = PathAbs::new(path)?;
if let Ok(parent) = path.parent() { let mut valid_args: Vec<EncodeArgs> = Vec::with_capacity(inputs.len());
ensure!(parent.exists(), "Path to file {:?} is invalid", path);
for input in inputs {
let temp = if let Some(path) = args.temp.as_ref() {
path.to_str().unwrap().to_owned()
} else {
format!(".{}", hash_path(input.as_path()))
};
let input = Input::from(input);
// TODO make an actual constructor for this
let mut arg = EncodeArgs {
frames: 0,
log_file: if let Some(log_file) = args.log_file.as_ref() {
Path::new(&format!("{}.log", log_file)).to_owned()
} else { } else {
bail!("Failed to get parent directory of path: {:?}", path); Path::new(&temp).join("log.log")
}
path.to_string_lossy().to_string()
} else {
format!(
"{}_{}.mkv",
args
.input
.file_stem()
.unwrap_or_else(|| args.input.as_ref())
.to_string_lossy(),
args.encoder
)
},
audio_params: if let Some(args) = args.audio_params.as_ref() {
shlex::split(args).ok_or_else(|| anyhow!("Failed to split ffmpeg audio encoder arguments"))?
} else {
into_vec!["-c:a", "copy"]
},
chunk_method: args
.chunk_method
.unwrap_or_else(vapoursynth::best_available_chunk_method),
chunk_order: args.chunk_order,
concat: args.concat,
encoder: args.encoder,
extra_splits_len: match args.extra_split {
Some(0) => None,
Some(x) => Some(x),
// Make sure it's at least 10 seconds
None => match input.frame_rate() {
Ok(fps) => Some((fps * 10.0) as usize),
Err(_) => Some(240_usize),
}, },
}, ffmpeg_filter_args: if let Some(args) = args.ffmpeg_filter_args.as_ref() {
photon_noise: args.photon_noise, shlex::split(args).ok_or_else(|| anyhow!("Failed to split ffmpeg filter arguments"))?
sc_pix_format: args.sc_pix_format, } else {
keep: args.keep, Vec::new()
max_tries: args.max_tries, },
min_q: args.min_q, temp,
max_q: args.max_q, force: args.force,
min_scene_len: args.min_scene_len, passes: if let Some(passes) = args.passes {
vmaf_threads: args.vmaf_threads, passes
input_pix_format: { } else {
match &input { args.encoder.get_default_pass()
Input::Video(path) => InputPixelFormat::FFmpeg { },
format: ffmpeg::get_pixel_format(path.as_ref()).with_context(|| { video_params: if let Some(args) = args.video_params.as_ref() {
format!( shlex::split(args).ok_or_else(|| anyhow!("Failed to split video encoder arguments"))?
"ffmpeg: failed to get pixel format for input video {:?}", } else {
path Vec::new()
) },
})?, output_file: if let Some(path) = args.output_file.as_ref() {
let path = PathAbs::new(path)?;
if let Ok(parent) = path.parent() {
ensure!(parent.exists(), "Path to file {:?} is invalid", path);
} else {
bail!("Failed to get parent directory of path: {:?}", path);
}
path.to_string_lossy().to_string()
} else {
format!(
"{}_{}.mkv",
input
.as_path()
.file_stem()
.unwrap_or_else(|| input.as_path().as_ref())
.to_string_lossy(),
args.encoder
)
},
audio_params: if let Some(args) = args.audio_params.as_ref() {
shlex::split(args)
.ok_or_else(|| anyhow!("Failed to split ffmpeg audio encoder arguments"))?
} else {
into_vec!["-c:a", "copy"]
},
chunk_method: args
.chunk_method
.unwrap_or_else(vapoursynth::best_available_chunk_method),
chunk_order: args.chunk_order,
concat: args.concat,
encoder: args.encoder,
extra_splits_len: match args.extra_split {
Some(0) => None,
Some(x) => Some(x),
// Make sure it's at least 10 seconds
None => match input.frame_rate() {
Ok(fps) => Some((fps * 10.0) as usize),
Err(_) => Some(240_usize),
}, },
Input::VapourSynth(path) => InputPixelFormat::VapourSynth { },
bit_depth: crate::vapoursynth::bit_depth(path.as_ref()).with_context(|| { photon_noise: args.photon_noise,
format!( sc_pix_format: args.sc_pix_format,
"vapoursynth: failed to get bit depth for input video {:?}", keep: args.keep,
path max_tries: args.max_tries,
) min_q: args.min_q,
})?, max_q: args.max_q,
}, min_scene_len: args.min_scene_len,
} vmaf_threads: args.vmaf_threads,
}, input_pix_format: {
input, match &input {
output_pix_format: PixelFormat { Input::Video(path) => InputPixelFormat::FFmpeg {
format: args.pix_format, format: ffmpeg::get_pixel_format(path.as_ref()).with_context(|| {
bit_depth: args.encoder.get_format_bit_depth(args.pix_format)?, format!(
}, "FFmpeg failed to get pixel format for input video {:?}",
probe_slow: args.probe_slow, path
probes: args.probes, )
probing_rate: args.probing_rate, })?,
resume: args.resume, },
scenes: args.scenes, Input::VapourSynth(path) => InputPixelFormat::VapourSynth {
split_method: args.split_method, bit_depth: crate::vapoursynth::bit_depth(path.as_ref()).with_context(|| {
sc_method: args.sc_method, format!(
sc_only: args.sc_only, "VapourSynth failed to get bit depth for input video {:?}",
sc_downscale_height: args.sc_downscale_height, path
target_quality: args.target_quality, )
verbosity: if args.quiet { })?,
Verbosity::Quiet },
} else if args.verbose { }
Verbosity::Verbose },
} else { input,
Verbosity::Normal output_pix_format: PixelFormat {
}, format: args.pix_format,
vmaf: args.vmaf, bit_depth: args.encoder.get_format_bit_depth(args.pix_format)?,
vmaf_filter: args.vmaf_filter, },
vmaf_path: args.vmaf_path, probe_slow: args.probe_slow,
vmaf_res: args.vmaf_res, probes: args.probes,
workers: args.workers, probing_rate: args.probing_rate,
set_thread_affinity: args.set_thread_affinity, resume: args.resume,
vs_script: None, scenes: args.scenes.clone(),
}; split_method: args.split_method.clone(),
sc_method: args.sc_method,
sc_only: args.sc_only,
sc_downscale_height: args.sc_downscale_height,
target_quality: args.target_quality,
verbosity: if args.quiet {
Verbosity::Quiet
} else if args.verbose {
Verbosity::Verbose
} else {
Verbosity::Normal
},
vmaf: args.vmaf,
vmaf_filter: args.vmaf_filter.clone(),
vmaf_path: args.vmaf_path.clone(),
vmaf_res: args.vmaf_res.to_string().clone(),
workers: args.workers,
set_thread_affinity: args.set_thread_affinity,
vs_script: None,
};
encode_args.startup_check()?; arg.startup_check()?;
if !args.overwrite { if !args.overwrite {
if let Some(path) = args.output_file.as_ref() { // UGLY: taking first file for output file
if path.exists() if let Some(path) = args.output_file.as_ref() {
&& !confirm(&format!( if path.exists()
"Output file {:?} exists. Do you want to overwrite it? [Y/n]: ", && !confirm(&format!(
path "Output file {:?} exists. Do you want to overwrite it? [Y/n]: ",
))? path
{ ))?
println!("Not overwriting, aborting."); {
exit(0); println!("Not overwriting, aborting.");
} exit(0);
} else { }
let path: &Path = encode_args.output_file.as_ref(); } else {
let path: &Path = arg.output_file.as_ref();
if path.exists() if path.exists()
&& !confirm(&format!( && !confirm(&format!(
"Default output file {:?} exists. Do you want to overwrite it? [Y/n]: ", "Default output file {:?} exists. Do you want to overwrite it? [Y/n]: ",
path path
))? ))?
{ {
println!("Not overwriting, aborting."); println!("Not overwriting, aborting.");
exit(0); exit(0);
}
} }
} }
valid_args.push(arg)
} }
Ok(encode_args) Ok(valid_args)
} }
pub struct StderrLogger { pub struct StderrLogger {
@ -666,7 +700,7 @@ impl LogWriter for StderrLogger {
pub fn run() -> anyhow::Result<()> { pub fn run() -> anyhow::Result<()> {
let cli_args = CliOpts::parse(); let cli_args = CliOpts::parse();
let log_level = cli_args.log_level; let log_level = cli_args.log_level;
let mut args = parse_cli(cli_args)?; let args = parse_cli(cli_args)?;
let log = LogSpecBuilder::new() let log = LogSpecBuilder::new()
.default(LevelFilter::Error) .default(LevelFilter::Error)
@ -688,11 +722,13 @@ pub fn run() -> anyhow::Result<()> {
// because it will, in its Drop implementation, flush all writers to ensure that // because it will, in its Drop implementation, flush all writers to ensure that
// all buffered log lines are flushed before the program terminates, // all buffered log lines are flushed before the program terminates,
// and then it calls their shutdown method. // and then it calls their shutdown method.
let _logger = Logger::with(log) let logger = Logger::with(log)
.log_to_file_and_writer( .log_to_file_and_writer(
FileSpec::try_from(PathAbs::new(&args.log_file)?)?, // UGLY: take first or the files for log path
FileSpec::try_from(PathAbs::new(&args[0].log_file)?)?,
Box::new(StderrLogger { Box::new(StderrLogger {
level: match args.verbosity { // UGLY: take first or the files for verbosity
level: match args[0].verbosity {
Verbosity::Quiet => Level::Warn, Verbosity::Quiet => Level::Warn,
Verbosity::Normal | Verbosity::Verbose => Level::Info, Verbosity::Normal | Verbosity::Verbose => Level::Info,
}, },
@ -700,9 +736,14 @@ pub fn run() -> anyhow::Result<()> {
) )
.start()?; .start()?;
args.initialize()?; for mut arg in args {
// Change log file
let new_log_file = FileSpec::try_from(PathAbs::new(&arg.log_file)?)?;
let _ = &logger.reset_flw(&flexi_logger::writers::FileLogWriter::builder(new_log_file))?;
args.encode_file()?; arg.initialize()?;
arg.encode_file()?;
}
Ok(()) Ok(())
} }

View file

@ -19,7 +19,7 @@ use std::{
use path_abs::PathInfo; use path_abs::PathInfo;
use crate::encoder::Encoder; use crate::{encoder::Encoder, util::read_in_dir};
#[derive( #[derive(
PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Debug, strum::EnumString, strum::IntoStaticStr, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Debug, strum::EnumString, strum::IntoStaticStr,
@ -53,21 +53,7 @@ pub fn sort_files_by_filename(files: &mut [PathBuf]) {
} }
pub fn ivf(input: &Path, out: &Path) -> anyhow::Result<()> { pub fn ivf(input: &Path, out: &Path) -> anyhow::Result<()> {
let mut files: Vec<PathBuf> = fs::read_dir(input)? let mut files: Vec<PathBuf> = read_in_dir(input)?.collect();
.into_iter()
.filter_map(Result::ok)
.filter_map(|d| {
if let Ok(file_type) = d.file_type() {
if file_type.is_file() {
Some(d.path())
} else {
None
}
} else {
None
}
})
.collect();
sort_files_by_filename(&mut files); sort_files_by_filename(&mut files);

View file

@ -241,7 +241,7 @@ pub fn list_index(params: &[impl AsRef<str>], is_match: fn(&str) -> bool) -> Opt
}) })
} }
#[derive(Serialize, Deserialize, Debug, EnumString, IntoStaticStr, Display)] #[derive(Serialize, Deserialize, Debug, EnumString, IntoStaticStr, Display, Clone)]
pub enum SplitMethod { pub enum SplitMethod {
#[strum(serialize = "av-scenechange")] #[strum(serialize = "av-scenechange")]
AvScenechange, AvScenechange,

View file

@ -1,3 +1,5 @@
use std::path::{Path, PathBuf};
/// Count the number of elements passed to this macro. /// Count the number of elements passed to this macro.
/// ///
/// Extra commas in between other commas are counted as an element. /// Extra commas in between other commas are counted as an element.
@ -114,6 +116,23 @@ pub(crate) fn printable_base10_digits(x: usize) -> u32 {
(((x as f64).log10() + 1.0).floor() as u32).max(1) (((x as f64).log10() + 1.0).floor() as u32).max(1)
} }
/// Reads dir and returns all files
/// Depth 1
pub fn read_in_dir(path: &Path) -> anyhow::Result<impl Iterator<Item = PathBuf>> {
let dir = std::fs::read_dir(path)?;
Ok(dir.into_iter().filter_map(Result::ok).filter_map(|d| {
if let Ok(file_type) = d.file_type() {
if file_type.is_file() {
Some(d.path())
} else {
None
}
} else {
None
}
}))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::borrow::Cow; use std::borrow::Cow;