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",
|
||||
"crossbeam",
|
||||
"html-escape",
|
||||
"id3",
|
||||
"lazy_static",
|
||||
"metaflac",
|
||||
"notify",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -23,6 +23,8 @@ walkdir = "2.3.2"
|
|||
|
||||
# tag reading
|
||||
audiotags = "0.4.1"
|
||||
id3 = "1.3.0"
|
||||
metaflac = "0.2.5"
|
||||
|
||||
# for genhtml command
|
||||
html-escape = "0.2.11"
|
||||
|
|
35
src/args.rs
35
src/args.rs
|
@ -17,52 +17,51 @@ pub enum Commands {
|
|||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ProcessCommandArgs {
|
||||
#[clap(value_parser)]
|
||||
pub source: String,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
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)]
|
||||
pub struct GenHTMLCommandArgs {
|
||||
#[clap(value_parser)]
|
||||
pub source: String,
|
||||
#[clap(value_parser)]
|
||||
pub dest: String,
|
||||
#[clap(long, value_parser, default_value = "musicutil")]
|
||||
#[clap(long, default_value = "musicutil")]
|
||||
pub title: String,
|
||||
#[clap(long, value_parser, default_value = "generated by musicutil")]
|
||||
#[clap(long, default_value = "generated by musicutil")]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct TranscodeCommandArgs {
|
||||
#[clap(value_parser)]
|
||||
pub source: String,
|
||||
#[clap(value_parser)]
|
||||
pub dest: String,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub ignore_extension: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CopyCommandArgs {
|
||||
#[clap(value_parser)]
|
||||
pub source: String,
|
||||
#[clap(value_parser)]
|
||||
pub dest: String,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub threads: Option<u32>,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub no_skip_existing: bool,
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(long)]
|
||||
pub single_directory: bool,
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ use crate::{
|
|||
args::{CLIArgs, CopyCommandArgs},
|
||||
types::File,
|
||||
utils::{
|
||||
scan_for_music,
|
||||
extract_tags_from_file,
|
||||
extract_tags_from_file, scan_for_music,
|
||||
transcoder::{
|
||||
presets::{print_presets, transcode_preset_or_config},
|
||||
transcode,
|
||||
|
@ -104,11 +103,7 @@ pub fn copy_command(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_files(
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for file in files.iter() {
|
||||
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();
|
||||
|
@ -129,6 +124,21 @@ fn copy_files(
|
|||
println!("Copying {:?} to {}", from_path, to_path_string);
|
||||
fs::copy(from_path, to_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_files(
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for file in files.iter() {
|
||||
copy_file(file, copy_args)?;
|
||||
|
||||
if !file.extra_files.is_empty() {
|
||||
for extra_file in file.extra_files.iter() {
|
||||
copy_file(extra_file, copy_args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -156,6 +166,12 @@ fn transcode_file(
|
|||
};
|
||||
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() {
|
||||
println!(
|
||||
"Skipping transcode for {} as file already exists",
|
||||
|
@ -193,7 +209,14 @@ fn transcode_files(
|
|||
scope(|s| {
|
||||
for _ in 0..copy_args.threads.unwrap() {
|
||||
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(
|
||||
&job,
|
||||
©_args_arc,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::args::CLIArgs;
|
||||
use crate::args::GenHTMLCommandArgs;
|
||||
use crate::types::File;
|
||||
use crate::utils::scan_for_music;
|
||||
use crate::utils::extract_tags_from_file;
|
||||
use crate::utils::scan_for_music;
|
||||
use std::cmp::Ordering;
|
||||
use std::io::Write;
|
||||
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crossbeam::scope;
|
||||
|
||||
use crate::args::CLIArgs;
|
||||
use crate::args::ProcessCommandArgs;
|
||||
use crate::types::File;
|
||||
use crate::utils::ascii_reduce::reduce_to_ascii;
|
||||
use crate::utils::scan_for_music;
|
||||
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) {
|
||||
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();
|
||||
new_file.filename = filename.clone();
|
||||
|
||||
let extension = &file.extension;
|
||||
|
||||
// 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 std::path::Path::new(&new_file.join_path_to()).exists() {
|
||||
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 {
|
||||
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(
|
||||
args: &CLIArgs,
|
||||
process_args: &ProcessCommandArgs,
|
||||
|
@ -78,5 +154,40 @@ pub fn process_command(
|
|||
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(())
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ pub fn transcode_command(
|
|||
_args: &CLIArgs,
|
||||
transcode_args: &TranscodeCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
if transcode_args.transcode_config.is_none() && transcode_args.transcode_preset.is_none() {
|
||||
panic!("Please provide Transcode Preset/Config");
|
||||
}
|
||||
|
@ -50,8 +49,7 @@ pub fn transcode_command(
|
|||
"please change it to {} ",
|
||||
"or run with --ignore-extension"
|
||||
),
|
||||
output_file.extension,
|
||||
file_extension
|
||||
output_file.extension, file_extension
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ use std::path::PathBuf;
|
|||
pub struct Tags {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
|
||||
pub supports_replaygain: bool,
|
||||
pub contains_replaygain_tags: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -11,6 +14,8 @@ pub struct File {
|
|||
pub filename: String,
|
||||
pub extension: String,
|
||||
|
||||
pub extra_files: Vec<File>,
|
||||
|
||||
pub path_to: PathBuf,
|
||||
pub path_from_source: PathBuf,
|
||||
pub tags: Tags,
|
||||
|
@ -44,9 +49,12 @@ impl File {
|
|||
extension,
|
||||
path_from_source: folder_path_from_src,
|
||||
path_to,
|
||||
extra_files: Vec::new(),
|
||||
tags: Tags {
|
||||
title: "".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 transcoder;
|
||||
pub(self) mod tag_extractor;
|
||||
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 tag_extractor::extract_tags_from_file;
|
||||
|
||||
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
||||
let ext = file_path.extension().unwrap().to_str().unwrap();
|
||||
|
|
|
@ -1,8 +1,32 @@
|
|||
use std::fs;
|
||||
|
||||
use crate::types::File;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
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>> {
|
||||
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) {
|
||||
let file = File::from_path(
|
||||
src_dir.clone(),
|
||||
entry_path,
|
||||
);
|
||||
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
|
||||
|
||||
file.extra_files
|
||||
.extend(find_extra_files(src_dir.clone(), &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 std::path::PathBuf;
|
||||
|
||||
pub fn extract_tags(filepath: PathBuf) -> Result<Tags, Box<dyn std::error::Error>> {
|
||||
let file_extension = filepath.extension().unwrap();
|
||||
|
||||
if file_extension == "mp3" || file_extension == "flac" {
|
||||
let tag = audiotags::Tag::new().read_from_path(filepath)?;
|
||||
if file_extension == "mp3" {
|
||||
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 {
|
||||
title: String::from(tag.title().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>> {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
pub mod presets;
|
||||
pub mod types;
|
||||
pub mod progress_monitor;
|
||||
#[allow(clippy::all)] mod transcoder;
|
||||
pub use self::transcoder::transcode;
|
||||
#[allow(clippy::all)]
|
||||
mod transcoder;
|
||||
pub mod types;
|
||||
pub(self) use self::progress_monitor::progress_monitor;
|
||||
pub use self::transcoder::transcode;
|
||||
|
|
|
@ -12,7 +12,6 @@ use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
|||
use serde::Deserialize;
|
||||
use string_error::static_err;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct FFProbeOutput {
|
||||
pub format: FFProbeFormat,
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
use std::{
|
||||
fs,
|
||||
process::Command,
|
||||
sync::mpsc::{Sender},
|
||||
thread::{JoinHandle},
|
||||
};
|
||||
|
||||
use std::{fs, process::Command, sync::mpsc::Sender, thread::JoinHandle};
|
||||
|
||||
use crate::types::File;
|
||||
use string_error::static_err;
|
||||
|
||||
use super::{progress_monitor, types::TranscodeConfig};
|
||||
|
||||
|
||||
pub fn transcode(
|
||||
file: File,
|
||||
dest: String,
|
||||
|
|
Loading…
Reference in a new issue