use anyhow::Result;
use colored::*;
use indicatif::{MultiProgress, ProgressBar};
use mp3rgain::replaygain::{self, AlbumAnalysisReport};
use rayon::prelude::*;
use std::io::{self, Write as _};
use std::path::{Path, PathBuf};
use crate::cli::options::{Options, OutputFormat};
use crate::commands::threading::effective_threads;
use crate::json_output::{FileStatus, JsonFileResult, JsonOutput, JsonSummary};
use crate::progress::{
album_progress_callbacks, create_album_progress_pb_in, create_analysis_progress_bar,
create_file_count_pb_in, create_progress_bar, progress_finish, progress_inc,
progress_set_message,
};
use crate::util::get_filename;
pub fn run_album_analysis(
paths: &[&Path],
opts: &Options,
threads: usize,
parallel: bool,
skip_errors: bool,
) -> mp3rgain::error::Result<AlbumAnalysisReport> {
let show_progress = !opts.quiet && opts.output_format == OutputFormat::Text;
let mp = MultiProgress::new();
let pb = if show_progress {
Some(create_album_progress_pb_in(&mp, paths.len(), parallel))
} else {
None
};
let file_names: Vec<&str> = paths.iter().map(|p| get_filename(p)).collect();
let (on_progress, on_complete) = album_progress_callbacks(&pb, file_names);
let report = replaygain::analyze_album_with_options(
paths,
&replaygain::AlbumAnalysisOptions {
track_index: opts.track_index,
threads,
skip_errors,
on_progress: (!parallel).then_some(&on_progress as _),
on_complete: parallel.then_some(&on_complete as _),
cancel: None,
mode: opts.analysis_mode,
true_peak: opts.true_peak,
},
);
if let Some(pb) = pb {
pb.finish_and_clear();
}
report
}
pub const TSV_HEADER: &str =
"File\tMP3 gain\tdB gain\tMax Amplitude\tMax global_gain\tMin global_gain";
pub fn for_each_file<F>(
files: &[PathBuf],
opts: &Options,
per_file: F,
) -> Result<(Vec<JsonFileResult>, usize, usize)>
where
F: Fn(&Path) -> Result<(Option<JsonFileResult>, String)> + Sync,
{
for_each_file_impl(files, opts, false, |file, _| per_file(file))
}
pub fn for_each_file_with_analysis_bar<F>(
files: &[PathBuf],
opts: &Options,
per_file: F,
) -> Result<(Vec<JsonFileResult>, usize, usize)>
where
F: Fn(&Path, Option<&ProgressBar>) -> Result<(Option<JsonFileResult>, String)> + Sync,
{
for_each_file_impl(files, opts, true, per_file)
}
fn for_each_file_impl<F>(
files: &[PathBuf],
opts: &Options,
analysis_bars: bool,
per_file: F,
) -> Result<(Vec<JsonFileResult>, usize, usize)>
where
F: Fn(&Path, Option<&ProgressBar>) -> Result<(Option<JsonFileResult>, String)> + Sync,
{
let mp = MultiProgress::new();
let pb = if analysis_bars {
create_file_count_pb_in(&mp, files.len(), opts)
} else {
create_progress_bar(files.len(), opts)
};
let mut json_results: Vec<JsonFileResult> = Vec::with_capacity(files.len());
let mut successful = 0;
let mut failed = 0;
let parallel = effective_threads(opts) > 1 && files.len() > 1;
if parallel {
let pb_ref = pb.as_ref();
let collected: Vec<(Option<JsonFileResult>, String)> = files
.par_iter()
.map(|file| -> Result<(Option<JsonFileResult>, String)> {
let r = per_file(file, None)?;
if let Some(pb) = pb_ref {
pb.set_message(get_filename(file).to_string());
pb.inc(1);
}
Ok(r)
})
.collect::<Result<Vec<_>>>()?;
let stdout = io::stdout();
let mut handle = stdout.lock();
for (_, text) in &collected {
if !text.is_empty() {
handle.write_all(text.as_bytes())?;
}
}
drop(handle);
for (result, _) in collected {
if let Some(result) = result {
update_counters(&result, &mut successful, &mut failed);
json_results.push(result);
}
}
} else {
for file in files {
progress_set_message(&pb, get_filename(file));
let analysis_pb = if analysis_bars {
create_analysis_progress_bar(&mp, file, opts)
} else {
None
};
let (result, text) = per_file(file, analysis_pb.as_ref())?;
progress_finish(analysis_pb);
if !text.is_empty() {
print!("{}", text);
}
if let Some(result) = result {
update_counters(&result, &mut successful, &mut failed);
json_results.push(result);
}
progress_inc(&pb);
}
}
progress_finish(pb);
Ok((json_results, successful, failed))
}
pub fn report_zero_steps(files: &[PathBuf], opts: &Options) -> Result<()> {
if opts.output_format == OutputFormat::Json {
let output = JsonOutput {
files: Some(vec![]),
album: None,
summary: Some(create_json_summary(files.len(), 0, 0, opts.dry_run)),
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else if !opts.quiet {
println!("{}: gain is 0, nothing to do", "info".cyan());
}
Ok(())
}
pub fn finish_with_summary(
total_files: usize,
json_results: Vec<JsonFileResult>,
successful: usize,
failed: usize,
opts: &Options,
) -> Result<()> {
if opts.output_format == OutputFormat::Json {
let output = JsonOutput {
files: Some(json_results),
album: None,
summary: Some(create_json_summary(
total_files,
successful,
failed,
opts.dry_run,
)),
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
print_dry_run_notice(opts);
}
exit_if_failed(failed);
Ok(())
}
pub fn exit_if_failed(failed: usize) {
if failed > 0 {
std::process::exit(1);
}
}
pub fn finish_without_summary(json_results: Vec<JsonFileResult>, opts: &Options) -> Result<()> {
if opts.output_format == OutputFormat::Json {
let output = JsonOutput {
files: Some(json_results),
album: None,
summary: None,
};
println!("{}", serde_json::to_string_pretty(&output)?);
}
Ok(())
}
pub fn update_counters(result: &JsonFileResult, successful: &mut usize, failed: &mut usize) {
match result.status {
Some(FileStatus::Success) => *successful += 1,
Some(FileStatus::Error) => *failed += 1,
Some(FileStatus::Skipped)
| Some(FileStatus::DryRun)
| Some(FileStatus::Info)
| Some(FileStatus::NoTag)
| None => {}
}
}
pub fn create_json_summary(
total_files: usize,
successful: usize,
failed: usize,
dry_run: bool,
) -> JsonSummary {
JsonSummary {
total_files,
successful,
failed,
dry_run: if dry_run { Some(true) } else { None },
}
}
pub fn print_dry_run_notice(opts: &Options) {
if opts.dry_run && !opts.quiet && opts.output_format == OutputFormat::Text {
println!();
println!("{}", "No files were modified.".yellow());
}
}