replaygain support
This commit is contained in:
parent
8c17f237e8
commit
f49647d622
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -538,7 +538,9 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
|
"id3",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"metaflac",
|
||||||
"notify",
|
"notify",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -23,6 +23,8 @@ walkdir = "2.3.2"
|
||||||
|
|
||||||
# tag reading
|
# tag reading
|
||||||
audiotags = "0.4.1"
|
audiotags = "0.4.1"
|
||||||
|
id3 = "1.3.0"
|
||||||
|
metaflac = "0.2.5"
|
||||||
|
|
||||||
# for genhtml command
|
# for genhtml command
|
||||||
html-escape = "0.2.11"
|
html-escape = "0.2.11"
|
||||||
|
|
35
src/args.rs
35
src/args.rs
|
@ -17,52 +17,51 @@ pub enum Commands {
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ProcessCommandArgs {
|
pub struct ProcessCommandArgs {
|
||||||
#[clap(value_parser)]
|
|
||||||
pub source: String,
|
pub source: String,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
#[clap(long)]
|
||||||
|
pub skip_replaygain: bool,
|
||||||
|
#[clap(long)]
|
||||||
|
pub force_replaygain: bool,
|
||||||
|
#[clap(long)]
|
||||||
|
pub replaygain_threads: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct GenHTMLCommandArgs {
|
pub struct GenHTMLCommandArgs {
|
||||||
#[clap(value_parser)]
|
|
||||||
pub source: String,
|
pub source: String,
|
||||||
#[clap(value_parser)]
|
|
||||||
pub dest: String,
|
pub dest: String,
|
||||||
#[clap(long, value_parser, default_value = "musicutil")]
|
#[clap(long, default_value = "musicutil")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[clap(long, value_parser, default_value = "generated by musicutil")]
|
#[clap(long, default_value = "generated by musicutil")]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct TranscodeCommandArgs {
|
pub struct TranscodeCommandArgs {
|
||||||
#[clap(value_parser)]
|
|
||||||
pub source: String,
|
pub source: String,
|
||||||
#[clap(value_parser)]
|
|
||||||
pub dest: String,
|
pub dest: String,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub transcode_preset: Option<String>,
|
pub transcode_preset: Option<String>,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub transcode_config: Option<String>,
|
pub transcode_config: Option<String>,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub ignore_extension: bool,
|
pub ignore_extension: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Args)]
|
#[derive(Debug, Clone, Args)]
|
||||||
pub struct CopyCommandArgs {
|
pub struct CopyCommandArgs {
|
||||||
#[clap(value_parser)]
|
|
||||||
pub source: String,
|
pub source: String,
|
||||||
#[clap(value_parser)]
|
|
||||||
pub dest: String,
|
pub dest: String,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub transcode_preset: Option<String>,
|
pub transcode_preset: Option<String>,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub transcode_config: Option<String>,
|
pub transcode_config: Option<String>,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub threads: Option<u32>,
|
pub threads: Option<u32>,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub no_skip_existing: bool,
|
pub no_skip_existing: bool,
|
||||||
#[clap(long, value_parser)]
|
#[clap(long)]
|
||||||
pub single_directory: bool,
|
pub single_directory: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,7 @@ use crate::{
|
||||||
args::{CLIArgs, CopyCommandArgs},
|
args::{CLIArgs, CopyCommandArgs},
|
||||||
types::File,
|
types::File,
|
||||||
utils::{
|
utils::{
|
||||||
scan_for_music,
|
extract_tags_from_file, scan_for_music,
|
||||||
extract_tags_from_file,
|
|
||||||
transcoder::{
|
transcoder::{
|
||||||
presets::{print_presets, transcode_preset_or_config},
|
presets::{print_presets, transcode_preset_or_config},
|
||||||
transcode,
|
transcode,
|
||||||
|
@ -104,30 +103,41 @@ pub fn copy_command(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let from_path = file.join_path_to();
|
||||||
|
|
||||||
|
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap();
|
||||||
|
let to_path = match copy_args.single_directory {
|
||||||
|
true => to_path_dest.join(file.join_filename()),
|
||||||
|
false => to_path_dest
|
||||||
|
.join(file.path_from_source.clone())
|
||||||
|
.join(file.join_filename()),
|
||||||
|
};
|
||||||
|
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
if !copy_args.no_skip_existing && to_path.exists() {
|
||||||
|
println!(
|
||||||
|
"Skipping {} as already exists in destination",
|
||||||
|
to_path_string
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("Copying {:?} to {}", from_path, to_path_string);
|
||||||
|
fs::copy(from_path, to_path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_files(
|
fn copy_files(
|
||||||
files: &[File],
|
files: &[File],
|
||||||
copy_args: &CopyCommandArgs,
|
copy_args: &CopyCommandArgs,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
let from_path = file.join_path_to();
|
copy_file(file, copy_args)?;
|
||||||
|
|
||||||
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap();
|
if !file.extra_files.is_empty() {
|
||||||
let to_path = match copy_args.single_directory {
|
for extra_file in file.extra_files.iter() {
|
||||||
true => to_path_dest.join(file.join_filename()),
|
copy_file(extra_file, copy_args)?;
|
||||||
false => to_path_dest
|
}
|
||||||
.join(file.path_from_source.clone())
|
|
||||||
.join(file.join_filename()),
|
|
||||||
};
|
|
||||||
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
|
|
||||||
|
|
||||||
if !copy_args.no_skip_existing && to_path.exists() {
|
|
||||||
println!(
|
|
||||||
"Skipping {} as already exists in destination",
|
|
||||||
to_path_string
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!("Copying {:?} to {}", from_path, to_path_string);
|
|
||||||
fs::copy(from_path, to_path)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +166,12 @@ fn transcode_file(
|
||||||
};
|
};
|
||||||
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
|
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
if !file.extra_files.is_empty() {
|
||||||
|
for extra_file in file.extra_files.iter() {
|
||||||
|
copy_file(extra_file, copy_args)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !copy_args.no_skip_existing && to_path.exists() {
|
if !copy_args.no_skip_existing && to_path.exists() {
|
||||||
println!(
|
println!(
|
||||||
"Skipping transcode for {} as file already exists",
|
"Skipping transcode for {} as file already exists",
|
||||||
|
@ -193,7 +209,14 @@ fn transcode_files(
|
||||||
scope(|s| {
|
scope(|s| {
|
||||||
for _ in 0..copy_args.threads.unwrap() {
|
for _ in 0..copy_args.threads.unwrap() {
|
||||||
s.spawn(|_| loop {
|
s.spawn(|_| loop {
|
||||||
let job = jobs.lock().unwrap().pop().unwrap().clone();
|
let mut jobs = jobs.lock().unwrap();
|
||||||
|
let job = jobs.pop();
|
||||||
|
if job.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let job = job.unwrap().clone();
|
||||||
|
drop(jobs);
|
||||||
|
|
||||||
let result = transcode_file(
|
let result = transcode_file(
|
||||||
&job,
|
&job,
|
||||||
©_args_arc,
|
©_args_arc,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::args::CLIArgs;
|
use crate::args::CLIArgs;
|
||||||
use crate::args::GenHTMLCommandArgs;
|
use crate::args::GenHTMLCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use crate::utils::scan_for_music;
|
|
||||||
use crate::utils::extract_tags_from_file;
|
use crate::utils::extract_tags_from_file;
|
||||||
|
use crate::utils::scan_for_music;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ pub fn genhtml_command(
|
||||||
if a.tags.title != b.tags.title {
|
if a.tags.title != b.tags.title {
|
||||||
return a.tags.title.cmp(&b.tags.title);
|
return a.tags.title.cmp(&b.tags.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.tags.artist.cmp(&b.tags.artist)
|
a.tags.artist.cmp(&b.tags.artist)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crossbeam::scope;
|
||||||
|
|
||||||
use crate::args::CLIArgs;
|
use crate::args::CLIArgs;
|
||||||
use crate::args::ProcessCommandArgs;
|
use crate::args::ProcessCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use crate::utils::ascii_reduce::reduce_to_ascii;
|
use crate::utils::ascii_reduce::reduce_to_ascii;
|
||||||
use crate::utils::scan_for_music;
|
|
||||||
use crate::utils::extract_tags_from_file;
|
use crate::utils::extract_tags_from_file;
|
||||||
|
use crate::utils::replaygain::analyze_replaygain;
|
||||||
|
use crate::utils::replaygain::tag_replaygain;
|
||||||
|
use crate::utils::scan_for_music;
|
||||||
|
|
||||||
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) {
|
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) {
|
||||||
let title = &file.tags.title;
|
let title = &file.tags.title;
|
||||||
|
@ -36,8 +43,13 @@ fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut Fi
|
||||||
let mut new_file = file.clone();
|
let mut new_file = file.clone();
|
||||||
new_file.filename = filename.clone();
|
new_file.filename = filename.clone();
|
||||||
|
|
||||||
|
let extension = &file.extension;
|
||||||
|
|
||||||
// Step 6: Rename File
|
// Step 6: Rename File
|
||||||
println!("Renaming File from {} to {}", file.filename, filename);
|
println!(
|
||||||
|
"Renaming File from {}.{extension} to {}.{extension}",
|
||||||
|
file.filename, filename
|
||||||
|
);
|
||||||
if !process_args.dry_run {
|
if !process_args.dry_run {
|
||||||
if std::path::Path::new(&new_file.join_path_to()).exists() {
|
if std::path::Path::new(&new_file.join_path_to()).exists() {
|
||||||
panic!(
|
panic!(
|
||||||
|
@ -53,11 +65,75 @@ fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut Fi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !file.extra_files.is_empty() {
|
||||||
|
let mut new_extra_files: Vec<File> = Vec::new();
|
||||||
|
|
||||||
|
for extra_file in file.extra_files.iter() {
|
||||||
|
let mut new_extra_file = extra_file.clone();
|
||||||
|
new_extra_file.filename = filename.clone();
|
||||||
|
|
||||||
|
let extra_extension = &extra_file.extension;
|
||||||
|
|
||||||
|
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 {
|
if !process_args.dry_run {
|
||||||
file.filename = filename;
|
file.filename = filename;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if !file.tags.supports_replaygain {
|
||||||
|
println!(
|
||||||
|
"Skipping replaygain for {:?}, not supported",
|
||||||
|
file.join_path_from_source()
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.tags.contains_replaygain_tags && !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(file.join_path_to())?;
|
||||||
|
|
||||||
|
tag_replaygain(file.join_path_to(), replaygain_data)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Applied replaygain tags for {:?}",
|
||||||
|
file.join_path_from_source()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_command(
|
pub fn process_command(
|
||||||
args: &CLIArgs,
|
args: &CLIArgs,
|
||||||
process_args: &ProcessCommandArgs,
|
process_args: &ProcessCommandArgs,
|
||||||
|
@ -78,5 +154,40 @@ pub fn process_command(
|
||||||
rename_file(args, process_args, file);
|
rename_file(args, process_args, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !process_args.skip_replaygain && !process_args.dry_run {
|
||||||
|
println!("Adding ReplayGain Tags to Files");
|
||||||
|
|
||||||
|
if process_args.replaygain_threads.is_some() && process_args.replaygain_threads.unwrap() > 1
|
||||||
|
{
|
||||||
|
let files_copy = files.to_vec();
|
||||||
|
|
||||||
|
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
|
||||||
|
|
||||||
|
scope(|s| {
|
||||||
|
for _ in 0..process_args.replaygain_threads.unwrap() {
|
||||||
|
s.spawn(|_| loop {
|
||||||
|
let mut jobs = jobs.lock().unwrap();
|
||||||
|
let job = jobs.pop();
|
||||||
|
if job.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let job = job.unwrap().clone();
|
||||||
|
drop(jobs);
|
||||||
|
|
||||||
|
let result = add_replaygain_tags(&job, process_args.force_replaygain);
|
||||||
|
if result.is_err() {
|
||||||
|
panic!("Error doing replaygain: {}", result.unwrap_err())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("threads haunted");
|
||||||
|
} else {
|
||||||
|
for file in files.iter_mut() {
|
||||||
|
add_replaygain_tags(file, process_args.force_replaygain)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ pub fn transcode_command(
|
||||||
_args: &CLIArgs,
|
_args: &CLIArgs,
|
||||||
transcode_args: &TranscodeCommandArgs,
|
transcode_args: &TranscodeCommandArgs,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
if transcode_args.transcode_config.is_none() && transcode_args.transcode_preset.is_none() {
|
if transcode_args.transcode_config.is_none() && transcode_args.transcode_preset.is_none() {
|
||||||
panic!("Please provide Transcode Preset/Config");
|
panic!("Please provide Transcode Preset/Config");
|
||||||
}
|
}
|
||||||
|
@ -50,8 +49,7 @@ pub fn transcode_command(
|
||||||
"please change it to {} ",
|
"please change it to {} ",
|
||||||
"or run with --ignore-extension"
|
"or run with --ignore-extension"
|
||||||
),
|
),
|
||||||
output_file.extension,
|
output_file.extension, file_extension
|
||||||
file_extension
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ use std::path::PathBuf;
|
||||||
pub struct Tags {
|
pub struct Tags {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub artist: String,
|
pub artist: String,
|
||||||
|
|
||||||
|
pub supports_replaygain: bool,
|
||||||
|
pub contains_replaygain_tags: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -11,6 +14,8 @@ pub struct File {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub extension: String,
|
pub extension: String,
|
||||||
|
|
||||||
|
pub extra_files: Vec<File>,
|
||||||
|
|
||||||
pub path_to: PathBuf,
|
pub path_to: PathBuf,
|
||||||
pub path_from_source: PathBuf,
|
pub path_from_source: PathBuf,
|
||||||
pub tags: Tags,
|
pub tags: Tags,
|
||||||
|
@ -44,9 +49,12 @@ impl File {
|
||||||
extension,
|
extension,
|
||||||
path_from_source: folder_path_from_src,
|
path_from_source: folder_path_from_src,
|
||||||
path_to,
|
path_to,
|
||||||
|
extra_files: Vec::new(),
|
||||||
tags: Tags {
|
tags: Tags {
|
||||||
title: "".to_string(),
|
title: "".to_string(),
|
||||||
artist: "".to_string(),
|
artist: "".to_string(),
|
||||||
|
supports_replaygain: false,
|
||||||
|
contains_replaygain_tags: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use std::path::{Path};
|
use std::path::Path;
|
||||||
|
|
||||||
pub mod ascii_reduce;
|
pub mod ascii_reduce;
|
||||||
pub mod transcoder;
|
|
||||||
pub(self) mod tag_extractor;
|
|
||||||
pub(self) mod music_scanner;
|
pub(self) mod music_scanner;
|
||||||
|
pub mod replaygain;
|
||||||
|
pub(self) mod tag_extractor;
|
||||||
|
pub mod transcoder;
|
||||||
|
|
||||||
pub use tag_extractor::extract_tags_from_file;
|
|
||||||
pub use music_scanner::scan_for_music;
|
pub use music_scanner::scan_for_music;
|
||||||
|
pub use tag_extractor::extract_tags_from_file;
|
||||||
|
|
||||||
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
||||||
let ext = file_path.extension().unwrap().to_str().unwrap();
|
let ext = file_path.extension().unwrap().to_str().unwrap();
|
||||||
|
|
|
@ -1,8 +1,32 @@
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use super::is_supported_file_extension;
|
use super::is_supported_file_extension;
|
||||||
|
|
||||||
|
pub fn find_extra_files(
|
||||||
|
src_dir: String,
|
||||||
|
file: &File,
|
||||||
|
) -> Result<Vec<File>, Box<dyn std::error::Error>> {
|
||||||
|
let mut extra_files: Vec<File> = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&file.path_to)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.metadata()?.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entry_path = entry.path();
|
||||||
|
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
|
||||||
|
&& entry_path.extension().unwrap().to_string_lossy() != file.extension
|
||||||
|
{
|
||||||
|
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(extra_files)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error::Error>> {
|
pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error::Error>> {
|
||||||
let mut files: Vec<File> = Vec::new();
|
let mut files: Vec<File> = Vec::new();
|
||||||
|
|
||||||
|
@ -14,10 +38,11 @@ pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error:
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_supported_file_extension(&entry_path) {
|
if is_supported_file_extension(&entry_path) {
|
||||||
let file = File::from_path(
|
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
|
||||||
src_dir.clone(),
|
|
||||||
entry_path,
|
file.extra_files
|
||||||
);
|
.extend(find_extra_files(src_dir.clone(), &file)?);
|
||||||
|
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
182
src/utils/replaygain/mod.rs
Normal file
182
src/utils/replaygain/mod.rs
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
path::PathBuf,
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
use id3::TagLike;
|
||||||
|
use string_error::static_err;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReplayGainData {
|
||||||
|
pub track_gain: f64,
|
||||||
|
pub track_peak: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainData, Box<dyn std::error::Error>> {
|
||||||
|
let output = Command::new(crate::meta::FFMPEG)
|
||||||
|
.args([
|
||||||
|
"-hide_banner",
|
||||||
|
"-nostats",
|
||||||
|
"-v",
|
||||||
|
"info",
|
||||||
|
"-i",
|
||||||
|
&path.to_string_lossy(),
|
||||||
|
"-filter_complex",
|
||||||
|
"[0:a]ebur128=framelog=verbose:peak=sample:dualmono=true[s6]",
|
||||||
|
"-map",
|
||||||
|
"[s6]",
|
||||||
|
"-f",
|
||||||
|
"null",
|
||||||
|
"/dev/null",
|
||||||
|
])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||||
|
return Err(static_err("FFmpeg Crashed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// info we need is in stdout
|
||||||
|
let output_str = String::from_utf8(output.stderr).unwrap();
|
||||||
|
|
||||||
|
let mut ebur128_summary = String::new();
|
||||||
|
|
||||||
|
// for some reason ffmpeg outputs the summary twice,
|
||||||
|
// the first time is garbage data
|
||||||
|
let mut has_seen_first_summary = false;
|
||||||
|
let mut should_start_reading_lines = false;
|
||||||
|
|
||||||
|
let output_reader = BufReader::new(output_str.as_bytes());
|
||||||
|
for line in output_reader.lines() {
|
||||||
|
if let Ok(line) = line {
|
||||||
|
if line.starts_with("[Parsed_ebur128_0") {
|
||||||
|
if has_seen_first_summary {
|
||||||
|
should_start_reading_lines = true;
|
||||||
|
ebur128_summary.push_str("Summary:\n")
|
||||||
|
} else {
|
||||||
|
has_seen_first_summary = true;
|
||||||
|
}
|
||||||
|
} else if should_start_reading_lines {
|
||||||
|
ebur128_summary.push_str(line.trim());
|
||||||
|
ebur128_summary.push('\n')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut track_gain: f64 = 0.0;
|
||||||
|
let mut track_peak: f64 = 0.0;
|
||||||
|
|
||||||
|
let summary_reader = BufReader::new(ebur128_summary.as_bytes());
|
||||||
|
for line in summary_reader.lines() {
|
||||||
|
if let Ok(line) = line {
|
||||||
|
if line.starts_with("I:") {
|
||||||
|
let mut l = line.split(':');
|
||||||
|
l.next();
|
||||||
|
|
||||||
|
let gain = l.next().unwrap().trim().trim_end_matches(" LUFS");
|
||||||
|
let gain = gain.parse::<f64>()?;
|
||||||
|
|
||||||
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Gain_calculation
|
||||||
|
// "In order to maintain backwards compatibility with RG1, RG2 uses a -18 LUFS reference, which based on lots of music, can give similar loudness compared to RG1."
|
||||||
|
let gain = -18_f64 - gain;
|
||||||
|
|
||||||
|
track_gain = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.starts_with("Peak:") {
|
||||||
|
let mut l = line.split(':');
|
||||||
|
l.next();
|
||||||
|
|
||||||
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Loudness_normalization
|
||||||
|
let peak = l.next().unwrap().trim().trim_end_matches(" dBFS");
|
||||||
|
let peak = peak.parse::<f64>()?;
|
||||||
|
let peak = f64::powf(10_f64, peak / 20.0_f64);
|
||||||
|
track_peak = peak;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ReplayGainData {
|
||||||
|
track_gain,
|
||||||
|
track_peak,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag_replaygain_mp3(
|
||||||
|
path: PathBuf,
|
||||||
|
data: ReplayGainData,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut tag = id3::Tag::read_from_path(&path)?;
|
||||||
|
|
||||||
|
let frames = tag.remove("TXXX");
|
||||||
|
|
||||||
|
for frame in frames {
|
||||||
|
if let Some(extended_text) = frame.content().extended_text() {
|
||||||
|
if extended_text.description.starts_with("REPLAYGAIN") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag.add_frame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.add_frame(id3::Frame::with_content(
|
||||||
|
"TXXX",
|
||||||
|
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||||
|
description: "REPLAYGAIN_TRACK_GAIN".to_string(),
|
||||||
|
value: format!("{:.2} dB", data.track_gain),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
tag.add_frame(id3::Frame::with_content(
|
||||||
|
"TXXX",
|
||||||
|
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||||
|
description: "REPLAYGAIN_TRACK_PEAK".to_string(),
|
||||||
|
value: format!("{:.6}", data.track_peak),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
tag.write_to_path(path, id3::Version::Id3v24)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag_replaygain_flac(
|
||||||
|
path: PathBuf,
|
||||||
|
data: ReplayGainData,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut tag = metaflac::Tag::read_from_path(&path)?;
|
||||||
|
tag.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
|
||||||
|
tag.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
|
||||||
|
|
||||||
|
tag.set_vorbis(
|
||||||
|
"REPLAYGAIN_TRACK_GAIN",
|
||||||
|
vec![format!("{:.2} dB", data.track_gain)],
|
||||||
|
);
|
||||||
|
tag.set_vorbis(
|
||||||
|
"REPLAYGAIN_TRACK_PEAK",
|
||||||
|
vec![format!("{:.6}", data.track_peak)],
|
||||||
|
);
|
||||||
|
|
||||||
|
tag.write_to_path(path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag_replaygain(
|
||||||
|
path: PathBuf,
|
||||||
|
data: ReplayGainData,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let extension = path.extension().unwrap().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if extension == "mp3" {
|
||||||
|
return tag_replaygain_mp3(path, data);
|
||||||
|
} else if extension == "flac" {
|
||||||
|
return tag_replaygain_flac(path, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("extension not tagable");
|
||||||
|
}
|
|
@ -1,18 +1,63 @@
|
||||||
|
use id3::TagLike;
|
||||||
|
|
||||||
use crate::types::{File, Tags};
|
use crate::types::{File, Tags};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn extract_tags(filepath: PathBuf) -> Result<Tags, Box<dyn std::error::Error>> {
|
pub fn extract_tags(filepath: PathBuf) -> Result<Tags, Box<dyn std::error::Error>> {
|
||||||
let file_extension = filepath.extension().unwrap();
|
let file_extension = filepath.extension().unwrap();
|
||||||
|
|
||||||
if file_extension == "mp3" || file_extension == "flac" {
|
if file_extension == "mp3" {
|
||||||
let tag = audiotags::Tag::new().read_from_path(filepath)?;
|
let tag = id3::Tag::read_from_path(&filepath)?;
|
||||||
|
|
||||||
|
let mut contains_replaygain_tags = false;
|
||||||
|
|
||||||
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#ID3v2
|
||||||
|
let frames = tag.frames();
|
||||||
|
for frame in frames {
|
||||||
|
if frame.id() == "TXXX" {
|
||||||
|
if let Some(extended_text) = frame.content().extended_text() {
|
||||||
|
if extended_text.value == "REPLAYGAIN_TRACK_GAIN" {
|
||||||
|
contains_replaygain_tags = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(Tags {
|
return Ok(Tags {
|
||||||
title: String::from(tag.title().unwrap()),
|
title: String::from(tag.title().unwrap()),
|
||||||
artist: String::from(tag.artist().unwrap()),
|
artist: String::from(tag.artist().unwrap()),
|
||||||
|
supports_replaygain: true,
|
||||||
|
contains_replaygain_tags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
panic!("oops")
|
if file_extension == "flac" {
|
||||||
|
let tag = metaflac::Tag::read_from_path(filepath)?;
|
||||||
|
|
||||||
|
fn get_first(tag: &metaflac::Tag, key: &str) -> Option<String> {
|
||||||
|
if let Some(Some(v)) = tag.vorbis_comments().map(|c| c.get(key)) {
|
||||||
|
if !v.is_empty() {
|
||||||
|
Some(v[0].to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rpg = get_first(&tag, "REPLAYGAIN_TRACK_GAIN");
|
||||||
|
|
||||||
|
return Ok(Tags {
|
||||||
|
title: get_first(&tag, "TITLE").unwrap(),
|
||||||
|
artist: get_first(&tag, "ARTIST").unwrap(),
|
||||||
|
supports_replaygain: true,
|
||||||
|
contains_replaygain_tags: rpg.is_some(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("wrong extension")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_tags_from_file(file: File) -> Result<Tags, Box<dyn std::error::Error>> {
|
pub fn extract_tags_from_file(file: File) -> Result<Tags, Box<dyn std::error::Error>> {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod presets;
|
pub mod presets;
|
||||||
pub mod types;
|
|
||||||
pub mod progress_monitor;
|
pub mod progress_monitor;
|
||||||
#[allow(clippy::all)] mod transcoder;
|
#[allow(clippy::all)]
|
||||||
|
mod transcoder;
|
||||||
|
pub mod types;
|
||||||
|
pub(self) use self::progress_monitor::progress_monitor;
|
||||||
pub use self::transcoder::transcode;
|
pub use self::transcoder::transcode;
|
||||||
pub(self) use self::progress_monitor::progress_monitor;
|
|
|
@ -12,7 +12,6 @@ use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use string_error::static_err;
|
use string_error::static_err;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
struct FFProbeOutput {
|
struct FFProbeOutput {
|
||||||
pub format: FFProbeFormat,
|
pub format: FFProbeFormat,
|
||||||
|
@ -132,4 +131,4 @@ pub fn progress_monitor(
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok((file_path_string, child))
|
Ok((file_path_string, child))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
use std::{
|
use std::{fs, process::Command, sync::mpsc::Sender, thread::JoinHandle};
|
||||||
fs,
|
|
||||||
process::Command,
|
|
||||||
sync::mpsc::{Sender},
|
|
||||||
thread::{JoinHandle},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use string_error::static_err;
|
use string_error::static_err;
|
||||||
|
|
||||||
use super::{progress_monitor, types::TranscodeConfig};
|
use super::{progress_monitor, types::TranscodeConfig};
|
||||||
|
|
||||||
|
|
||||||
pub fn transcode(
|
pub fn transcode(
|
||||||
file: File,
|
file: File,
|
||||||
dest: String,
|
dest: String,
|
||||||
|
|
Loading…
Reference in a new issue