274 lines
6.4 KiB
Rust
274 lines
6.4 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::thread::scope;
|
|
|
|
use crate::args::CLIArgs;
|
|
use crate::types::AudioFileInfo;
|
|
use crate::types::AudioFile;
|
|
use crate::utils::formats::get_format_handler;
|
|
use crate::utils::scan_for_music;
|
|
|
|
use ascii_reduce::reduce;
|
|
use relative_file::RelativeFile;
|
|
use relative_file::traits::RelativeFileTrait;
|
|
#[cfg(feature = "replaygain")]
|
|
use replaygain::analyze_replaygain_track;
|
|
|
|
#[derive(Debug, Clone, clap::Args)]
|
|
pub struct ProcessCommandArgs {
|
|
pub source: String,
|
|
#[clap(long)]
|
|
pub dry_run: bool,
|
|
#[cfg(feature = "replaygain")]
|
|
#[clap(long)]
|
|
pub skip_replaygain: bool,
|
|
#[cfg(feature = "replaygain")]
|
|
#[clap(long)]
|
|
pub force_replaygain: bool,
|
|
#[cfg(feature = "replaygain")]
|
|
#[clap(long)]
|
|
pub replaygain_threads: Option<u32>,
|
|
#[clap(long)]
|
|
pub analyze_threads: Option<u32>,
|
|
}
|
|
|
|
fn rename_file(process_args: &ProcessCommandArgs, file: &mut AudioFile) {
|
|
let title = &file.info.tags.title;
|
|
let artist = &file.info.tags.artist;
|
|
|
|
let replace_char = "_".to_string();
|
|
|
|
// Step 1: Remove Newlines
|
|
let title = title.replace('\n', "");
|
|
let artist = artist.replace('\n', "");
|
|
|
|
// Step 2: Strip ASCII
|
|
let title = reduce(title);
|
|
let artist = reduce(artist);
|
|
|
|
// Step 3: Remove File Seperators
|
|
let title = title.replace('\\', &replace_char);
|
|
let title = title.replace('/', &replace_char);
|
|
let artist = artist.replace('\\', &replace_char);
|
|
let artist = artist.replace('/', &replace_char);
|
|
|
|
// Step 4: Join Filename
|
|
let filename = match file.folder_meta.is_album {
|
|
true => {
|
|
format!(
|
|
"{}. {} - {}",
|
|
file.info.tags.track_number.unwrap(),
|
|
title,
|
|
artist
|
|
)
|
|
}
|
|
false => {
|
|
format!("{} - {}", artist, title)
|
|
}
|
|
};
|
|
|
|
if filename == file.filename() {
|
|
return;
|
|
}
|
|
|
|
// Step 5: Make new File with filename set
|
|
let mut new_file = file.clone();
|
|
new_file.change_filename(filename.clone());
|
|
|
|
let extension = &file.extension().unwrap();
|
|
|
|
// Step 6: Rename File
|
|
println!(
|
|
"Renaming File from {}.{extension} to {}.{extension}",
|
|
file.filename(), filename
|
|
);
|
|
if !process_args.dry_run {
|
|
if std::path::Path::new(&new_file.join_path_to()).exists() {
|
|
panic!(
|
|
"Refusing to rename {} to {}, please retag to be distinct",
|
|
&file.join_filename(),
|
|
&new_file.join_filename()
|
|
);
|
|
} else {
|
|
let err = std::fs::rename(&file.join_path_to(), &new_file.join_path_to());
|
|
if err.is_err() {
|
|
panic!("Could not rename {:?}", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !file.extra_files.is_empty() {
|
|
let mut new_extra_files: Vec<RelativeFile> = Vec::new();
|
|
|
|
for extra_file in file.extra_files.iter() {
|
|
let mut new_extra_file = extra_file.clone();
|
|
|
|
new_extra_file.change_filename(filename.clone());
|
|
|
|
let extra_extension = &extra_file.extension().unwrap();
|
|
|
|
println!(
|
|
"Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}",
|
|
file.filename(), filename
|
|
);
|
|
|
|
if !process_args.dry_run {
|
|
let err =
|
|
std::fs::rename(&extra_file.join_path_to(), &new_extra_file.join_path_to());
|
|
if err.is_err() {
|
|
panic!("Could not rename {:?}", err)
|
|
}
|
|
new_extra_files.push(new_extra_file);
|
|
}
|
|
}
|
|
if !process_args.dry_run {
|
|
file.extra_files.clear();
|
|
file.extra_files.extend(new_extra_files);
|
|
}
|
|
}
|
|
|
|
if !process_args.dry_run {
|
|
file.change_filename(filename);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "replaygain")]
|
|
pub fn add_replaygain_tags(file: &AudioFile, force: bool) -> Result<(), Box<dyn std::error::Error>> {
|
|
if !file.info.supports_replaygain {
|
|
println!(
|
|
"Skipping replaygain for {:?}, not supported",
|
|
file.join_path_from_source()
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
if file.info.contains_replaygain && !force {
|
|
println!(
|
|
"Skipping replaygain for {:?}, contains already",
|
|
file.join_path_from_source()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
println!(
|
|
"Analyzing replaygain for {:?}",
|
|
file.join_path_from_source()
|
|
);
|
|
|
|
let replaygain_data = analyze_replaygain_track(file.join_path_to(), Some(crate::meta::FFMPEG))
|
|
.expect("could not analyze replaygain");
|
|
|
|
let mut handler = get_format_handler(file)?;
|
|
|
|
handler.set_replaygain_data(replaygain_data)?;
|
|
handler.save_changes()?;
|
|
|
|
println!(
|
|
"Applied replaygain tags for {:?}",
|
|
file.join_path_from_source()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn analyze_file(file: &AudioFile) -> Result<AudioFileInfo, Box<dyn std::error::Error>> {
|
|
println!("Analysing: {:?}", file.join_path_from_source());
|
|
let mut handler = get_format_handler(file)?;
|
|
|
|
handler.get_audio_file_info(false)
|
|
}
|
|
|
|
pub fn process_command(
|
|
_args: CLIArgs,
|
|
process_args: &ProcessCommandArgs,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("Scanning For Music");
|
|
let mut files = scan_for_music(&process_args.source)?;
|
|
|
|
println!("Analysing Files");
|
|
let threads = process_args.analyze_threads.unwrap_or(0);
|
|
|
|
if threads <= 1 {
|
|
for file in files.iter_mut() {
|
|
let info = analyze_file(file)?;
|
|
file.info = info;
|
|
}
|
|
} else {
|
|
let jobs: Arc<Mutex<Vec<AudioFile>>> = Arc::new(Mutex::new(files.clone()));
|
|
let new_files: Arc<Mutex<Vec<AudioFile>>> = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
scope(|s| {
|
|
for _ in 0..threads {
|
|
s.spawn(|| loop {
|
|
let mut jobs = jobs.lock().unwrap();
|
|
let job = jobs.pop();
|
|
drop(jobs);
|
|
|
|
if let Some(job) = job {
|
|
let result = analyze_file(&job);
|
|
if let Err(err) = result {
|
|
panic!("Error analyzing: {}", err)
|
|
} else {
|
|
let mut file = job;
|
|
file.info = result.unwrap();
|
|
new_files.lock().unwrap().push(file);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
let new_files = new_files.lock().unwrap();
|
|
files = Vec::new();
|
|
for file in new_files.iter() {
|
|
files.push(file.clone())
|
|
}
|
|
}
|
|
|
|
println!("Renaming Files");
|
|
for file in files.iter_mut() {
|
|
rename_file(process_args, file);
|
|
}
|
|
|
|
#[cfg(feature = "replaygain")]
|
|
if !process_args.skip_replaygain && !process_args.dry_run {
|
|
println!("Adding ReplayGain Tags to Files");
|
|
|
|
let threads = process_args.replaygain_threads.unwrap_or(2);
|
|
|
|
if threads <= 1 {
|
|
for file in files.iter_mut() {
|
|
add_replaygain_tags(file, process_args.force_replaygain)?;
|
|
}
|
|
|
|
return Ok(());
|
|
} else {
|
|
let jobs: Arc<Mutex<Vec<AudioFile>>> = Arc::new(Mutex::new(files));
|
|
|
|
scope(|s| {
|
|
for _ in 0..threads {
|
|
s.spawn(|| loop {
|
|
let mut jobs = jobs.lock().unwrap();
|
|
let job = jobs.pop();
|
|
drop(jobs);
|
|
|
|
if let Some(job) = job {
|
|
let result = add_replaygain_tags(&job, process_args.force_replaygain);
|
|
if let Err(err) = result {
|
|
panic!("Error doing replaygain: {}", err)
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|