change default formatting to use tabs instead of spaces

This commit is contained in:
chaos 2023-10-19 17:22:01 +01:00
parent 1dc74c2cb0
commit 2f5f493f9b
No known key found for this signature in database
38 changed files with 2118 additions and 2126 deletions

2
.rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
hard_tabs = true
use_field_init_shorthand = true

View file

@ -2,35 +2,35 @@ use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=src/wrapper.h");
println!("cargo:rerun-if-changed=src/wrapper.cxx");
println!("cargo:rerun-if-changed=src/wrapper.h");
println!("cargo:rerun-if-changed=src/wrapper.cxx");
let taglib = pkg_config::Config::new().probe("taglib").unwrap();
let taglib = pkg_config::Config::new().probe("taglib").unwrap();
let mut cc_builder = cc::Build::new();
let mut cc_builder = cc::Build::new();
cc_builder
.file("src/wrapper.cxx")
.flag("-std=c++2b")
.flag("-Og")
.include("src")
.cpp(true);
cc_builder
.file("src/wrapper.cxx")
.flag("-std=c++2b")
.flag("-Og")
.include("src")
.cpp(true);
for include in taglib.include_paths {
cc_builder.include(include);
}
for include in taglib.include_paths {
cc_builder.include(include);
}
cc_builder.compile("wrapper");
cc_builder.compile("wrapper");
let bindings = bindgen::Builder::default()
.header("src/wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let bindings = bindgen::Builder::default()
.header("src/wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}

View file

@ -2,10 +2,10 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum TagLibError {
#[error("could not save file")]
SaveError,
#[error("invalid file")]
InvalidFile,
#[error("metadata unavailable")]
MetadataUnavailable,
#[error("could not save file")]
SaveError,
#[error("invalid file")]
InvalidFile,
#[error("metadata unavailable")]
MetadataUnavailable,
}

View file

@ -5,83 +5,83 @@ use crate::{bindings, errors::TagLibError, traits::File, TagLibFileType};
use super::{oggtag::TagLibOggTag, tag::TagLibTag};
pub struct TagLibFile {
ctx: *mut bindings::TagLib_File,
taglib_type: Option<TagLibFileType>,
ctx: *mut bindings::TagLib_File,
taglib_type: Option<TagLibFileType>,
}
pub fn new_taglib_file(
filepath: String,
taglib_type: Option<TagLibFileType>,
filepath: String,
taglib_type: Option<TagLibFileType>,
) -> Result<TagLibFile, TagLibError> {
let filename_c = CString::new(filepath).unwrap();
let filename_c_ptr = filename_c.as_ptr();
let filename_c = CString::new(filepath).unwrap();
let filename_c_ptr = filename_c.as_ptr();
let file = unsafe {
if let Some(taglib_type) = taglib_type {
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
} else {
bindings::wrap_taglib_file_new(filename_c_ptr)
}
};
let file = unsafe {
if let Some(taglib_type) = taglib_type {
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
} else {
bindings::wrap_taglib_file_new(filename_c_ptr)
}
};
if file.is_null() {
return Err(TagLibError::InvalidFile);
}
if file.is_null() {
return Err(TagLibError::InvalidFile);
}
Ok(TagLibFile {
ctx: file,
taglib_type,
})
Ok(TagLibFile {
ctx: file,
taglib_type,
})
}
impl TagLibFile {
pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() {
return Err(TagLibError::MetadataUnavailable);
}
pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() {
return Err(TagLibError::MetadataUnavailable);
}
Ok(TagLibTag { ctx: tag })
}
Ok(TagLibTag { ctx: tag })
}
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
if let Some(taglib_type) = &self.taglib_type {
let supported = match taglib_type {
TagLibFileType::OggFLAC
| TagLibFileType::OggOpus
| TagLibFileType::OggSpeex
| TagLibFileType::OggVorbis => true,
};
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
if let Some(taglib_type) = &self.taglib_type {
let supported = match taglib_type {
TagLibFileType::OggFLAC
| TagLibFileType::OggOpus
| TagLibFileType::OggSpeex
| TagLibFileType::OggVorbis => true,
};
if !supported {
panic!("ogg tag not supported")
}
}
if !supported {
panic!("ogg tag not supported")
}
}
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() {
return Err(TagLibError::MetadataUnavailable);
}
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() {
return Err(TagLibError::MetadataUnavailable);
}
Ok(TagLibOggTag { ctx: tag })
}
Ok(TagLibOggTag { ctx: tag })
}
}
impl File for TagLibFile {
fn save(&mut self) -> Result<(), TagLibError> {
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
fn save(&mut self) -> Result<(), TagLibError> {
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
match result {
true => Ok(()),
false => Err(TagLibError::SaveError),
}
}
match result {
true => Ok(()),
false => Err(TagLibError::SaveError),
}
}
}
impl Drop for TagLibFile {
fn drop(&mut self) {
unsafe {
bindings::wrap_taglib_file_free(self.ctx);
}
}
fn drop(&mut self) {
unsafe {
bindings::wrap_taglib_file_free(self.ctx);
}
}
}

View file

@ -3,31 +3,31 @@ use std::ffi::CString;
use crate::{bindings, utils::c_str_to_str};
pub struct TagLibOggTag {
pub ctx: *mut bindings::TagLib_Tag,
pub ctx: *mut bindings::TagLib_Tag,
}
impl TagLibOggTag {
pub fn get_field(&self, key: String) -> Option<String> {
let key = CString::new(key).unwrap();
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
pub fn get_field(&self, key: String) -> Option<String> {
let key = CString::new(key).unwrap();
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
if value.is_null() {
None
} else {
c_str_to_str(value)
}
}
if value.is_null() {
None
} else {
c_str_to_str(value)
}
}
pub fn add_field(&self, key: String, value: String) {
let key = CString::new(key).unwrap();
let value = CString::new(value).unwrap();
pub fn add_field(&self, key: String, value: String) {
let key = CString::new(key).unwrap();
let value = CString::new(value).unwrap();
unsafe { bindings::wrap_taglib_opustag_add_field(self.ctx, key.as_ptr(), value.as_ptr()) };
}
unsafe { bindings::wrap_taglib_opustag_add_field(self.ctx, key.as_ptr(), value.as_ptr()) };
}
pub fn remove_fields(&self, key: String) {
let key = CString::new(key).unwrap();
pub fn remove_fields(&self, key: String) {
let key = CString::new(key).unwrap();
unsafe { bindings::wrap_taglib_opustag_remove_fields(self.ctx, key.as_ptr()) };
}
unsafe { bindings::wrap_taglib_opustag_remove_fields(self.ctx, key.as_ptr()) };
}
}

View file

@ -3,28 +3,28 @@ use std::ffi::CString;
use crate::{bindings, traits::Tag, utils::c_str_to_str};
pub struct TagLibTag {
pub ctx: *mut bindings::TagLib_Tag,
pub ctx: *mut bindings::TagLib_Tag,
}
impl Tag for TagLibTag {
fn title(&self) -> Option<String> {
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
fn title(&self) -> Option<String> {
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
c_str_to_str(title_ref)
}
fn set_title(&mut self, title: String) {
let title = CString::new(title).unwrap();
c_str_to_str(title_ref)
}
fn set_title(&mut self, title: String) {
let title = CString::new(title).unwrap();
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
}
fn artist(&self) -> Option<String> {
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
}
fn artist(&self) -> Option<String> {
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
c_str_to_str(artist_ref)
}
fn set_artist(&mut self, artist: String) {
let artist = CString::new(artist).unwrap();
c_str_to_str(artist_ref)
}
fn set_artist(&mut self, artist: String) {
let artist = CString::new(artist).unwrap();
unsafe { bindings::wrap_taglib_tag_set_artist(self.ctx, artist.as_ptr()) };
}
unsafe { bindings::wrap_taglib_tag_set_artist(self.ctx, artist.as_ptr()) };
}
}

View file

@ -6,17 +6,17 @@ pub mod traits;
pub(crate) mod utils;
pub(crate) mod bindings {
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
#[derive(Debug, Clone, Copy)]
pub enum TagLibFileType {
OggFLAC = 1,
OggOpus = 2,
OggSpeex = 3,
OggVorbis = 4,
OggFLAC = 1,
OggOpus = 2,
OggSpeex = 3,
OggVorbis = 4,
}
pub use impls::file::*;

View file

@ -1,12 +1,12 @@
use crate::errors::TagLibError;
pub trait File {
fn save(&mut self) -> Result<(), TagLibError>;
fn save(&mut self) -> Result<(), TagLibError>;
}
pub trait Tag {
fn title(&self) -> Option<String>;
fn artist(&self) -> Option<String>;
fn set_title(&mut self, title: String);
fn set_artist(&mut self, artist: String);
fn title(&self) -> Option<String>;
fn artist(&self) -> Option<String>;
fn set_title(&mut self, title: String);
fn set_artist(&mut self, artist: String);
}

View file

@ -1,15 +1,15 @@
use std::{ffi::CStr, os::raw::c_char};
pub fn c_str_to_str(c_str: *const c_char) -> Option<String> {
if c_str.is_null() {
None
} else {
let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() };
if c_str.is_null() {
None
} else {
let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() };
if bytes.is_empty() {
None
} else {
Some(String::from_utf8_lossy(bytes).to_string())
}
}
if bytes.is_empty() {
None
} else {
Some(String::from_utf8_lossy(bytes).to_string())
}
}
}

View file

@ -3,87 +3,87 @@ use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
#[clap()]
pub struct CLIArgs {
#[clap(subcommand)]
pub command: Commands,
#[clap(subcommand)]
pub command: Commands,
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
Process(ProcessCommandArgs),
Genhtml(GenHTMLCommandArgs),
Transcode(TranscodeCommandArgs),
Copy(CopyCommandArgs),
SetTags(SetTagsCommandArgs),
GetTags(GetTagsCommandArgs),
Process(ProcessCommandArgs),
Genhtml(GenHTMLCommandArgs),
Transcode(TranscodeCommandArgs),
Copy(CopyCommandArgs),
SetTags(SetTagsCommandArgs),
GetTags(GetTagsCommandArgs),
}
#[derive(Debug, Clone, Args)]
pub struct ProcessCommandArgs {
pub source: String,
#[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>,
pub source: String,
#[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, Clone, Args)]
pub struct GenHTMLCommandArgs {
pub source: String,
pub dest: String,
#[clap(long, default_value = "musicutil")]
pub title: String,
#[clap(long, default_value = "generated by musicutil")]
pub description: String,
#[clap(long)]
pub link_base: Option<String>,
pub source: String,
pub dest: String,
#[clap(long, default_value = "musicutil")]
pub title: String,
#[clap(long, default_value = "generated by musicutil")]
pub description: String,
#[clap(long)]
pub link_base: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct TranscodeCommandArgs {
pub source: String,
pub dest: String,
#[clap(long)]
pub transcode_preset: Option<String>,
#[clap(long)]
pub transcode_config: Option<String>,
#[clap(long)]
pub ignore_extension: bool,
#[clap(long)]
pub hide_progress: bool,
pub source: String,
pub dest: String,
#[clap(long)]
pub transcode_preset: Option<String>,
#[clap(long)]
pub transcode_config: Option<String>,
#[clap(long)]
pub ignore_extension: bool,
#[clap(long)]
pub hide_progress: bool,
}
#[derive(Debug, Clone, Args)]
pub struct CopyCommandArgs {
pub source: String,
pub dest: String,
#[clap(long)]
pub transcode_preset: Option<String>,
#[clap(long)]
pub transcode_config: Option<String>,
#[clap(long)]
pub threads: Option<u32>,
#[clap(long)]
pub no_skip_existing: bool,
#[clap(long)]
pub single_directory: bool,
pub source: String,
pub dest: String,
#[clap(long)]
pub transcode_preset: Option<String>,
#[clap(long)]
pub transcode_config: Option<String>,
#[clap(long)]
pub threads: Option<u32>,
#[clap(long)]
pub no_skip_existing: bool,
#[clap(long)]
pub single_directory: bool,
}
#[derive(Debug, Clone, Args)]
pub struct SetTagsCommandArgs {
pub files: Vec<String>,
#[clap(long)]
pub title: Option<String>,
#[clap(long)]
pub artist: Option<String>,
pub files: Vec<String>,
#[clap(long)]
pub title: Option<String>,
#[clap(long)]
pub artist: Option<String>,
}
#[derive(Debug, Clone, Args)]
pub struct GetTagsCommandArgs {
pub files: Vec<String>,
#[clap(long)]
pub json: bool,
pub files: Vec<String>,
#[clap(long)]
pub json: bool,
}

View file

@ -1,243 +1,243 @@
use std::{
collections::{hash_map::Entry, HashMap},
fs,
path::PathBuf,
process::exit,
str::FromStr,
sync::{Arc, Mutex},
thread::scope,
collections::{hash_map::Entry, HashMap},
fs,
path::PathBuf,
process::exit,
str::FromStr,
sync::{Arc, Mutex},
thread::scope,
};
use crate::{
args::{CLIArgs, CopyCommandArgs},
types::File,
utils::{
formats::get_format_handler,
scan_for_music,
transcoder::{
presets::{print_presets, transcode_preset_or_config},
transcode,
types::TranscodeConfig,
},
},
args::{CLIArgs, CopyCommandArgs},
types::File,
utils::{
formats::get_format_handler,
scan_for_music,
transcoder::{
presets::{print_presets, transcode_preset_or_config},
transcode,
types::TranscodeConfig,
},
},
};
pub fn copy_command(
_args: CLIArgs,
copy_args: &CopyCommandArgs,
_args: CLIArgs,
copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
panic!("Please provide Transcode Preset/Config");
}
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
panic!("Please provide Transcode Preset/Config");
}
if let Some(preset) = &copy_args.transcode_preset {
if preset == "list" {
print_presets();
exit(0);
}
}
if let Some(preset) = &copy_args.transcode_preset {
if preset == "list" {
print_presets();
exit(0);
}
}
println!("Scanning For Music");
let mut files = scan_for_music(&copy_args.source)?;
println!("Scanning For Music");
let mut files = scan_for_music(&copy_args.source)?;
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
let mut handler = get_format_handler(file)?;
let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(true)?;
}
file.info = handler.get_audio_file_info(true)?;
}
if copy_args.single_directory {
println!("Checking for Duplicates");
let mut seen: HashMap<String, bool> = HashMap::new();
let mut dupes: Vec<String> = Vec::new();
if copy_args.single_directory {
println!("Checking for Duplicates");
let mut seen: HashMap<String, bool> = HashMap::new();
let mut dupes: Vec<String> = Vec::new();
for file in files.iter() {
let filename = file.join_filename();
for file in files.iter() {
let filename = file.join_filename();
if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
entry.insert(true);
} else {
dupes.push(filename);
}
}
if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
entry.insert(true);
} else {
dupes.push(filename);
}
}
if !dupes.is_empty() {
panic!("Duplicates Found: {}", dupes.join(","))
}
}
if !dupes.is_empty() {
panic!("Duplicates Found: {}", dupes.join(","))
}
}
if !copy_args.single_directory {
println!("Creating Directories");
if !copy_args.single_directory {
println!("Creating Directories");
let mut directories: Vec<String> = Vec::new();
for file in files.iter() {
let file_directory = file.path_from_source.to_string_lossy().to_string();
let mut directories: Vec<String> = Vec::new();
for file in files.iter() {
let file_directory = file.path_from_source.to_string_lossy().to_string();
if !directories.contains(&file_directory) {
directories.push(file_directory.clone());
}
}
for directory in directories.iter() {
fs::create_dir_all(
PathBuf::from_str(copy_args.dest.as_str())
.expect("invalid destination")
.join(directory),
)?;
}
}
if !directories.contains(&file_directory) {
directories.push(file_directory.clone());
}
}
for directory in directories.iter() {
fs::create_dir_all(
PathBuf::from_str(copy_args.dest.as_str())
.expect("invalid destination")
.join(directory),
)?;
}
}
if copy_args
.transcode_preset
.as_ref()
.unwrap_or(&"".to_string())
== "copy"
{
println!("Copying Files Into Dest");
copy_files(&files, copy_args)?;
} else {
println!("Transcoding Files");
transcode_files(&files, copy_args)?;
}
if copy_args
.transcode_preset
.as_ref()
.unwrap_or(&"".to_string())
== "copy"
{
println!("Copying Files Into Dest");
copy_files(&files, copy_args)?;
} else {
println!("Transcoding Files");
transcode_files(&files, copy_args)?;
}
Ok(())
Ok(())
}
fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> {
let from_path = file.join_path_to();
let from_path = file.join_path_to();
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
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_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
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.to_string_lossy();
let to_path_string = to_path.to_string_lossy();
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(())
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(
files: &[File],
copy_args: &CopyCommandArgs,
files: &[File],
copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
for file in files.iter() {
copy_file(file, copy_args)?;
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)?;
}
}
}
if !file.extra_files.is_empty() {
for extra_file in file.extra_files.iter() {
copy_file(extra_file, copy_args)?;
}
}
}
Ok(())
Ok(())
}
fn transcode_file(
file: &File,
copy_args: &CopyCommandArgs,
config: &TranscodeConfig,
is_threaded: bool,
file: &File,
copy_args: &CopyCommandArgs,
config: &TranscodeConfig,
is_threaded: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let new_filename_full: String = match config.file_extension.clone() {
Some(ext) => format!("{}.{}", file.filename, ext),
None => {
panic!("file_extension is required in custom transcode configs");
}
};
let new_filename_full: String = match config.file_extension.clone() {
Some(ext) => format!("{}.{}", file.filename, ext),
None => {
panic!("file_extension is required in custom transcode configs");
}
};
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory {
true => to_path_dest.join(new_filename_full),
false => to_path_dest
.join(file.path_from_source.clone())
.join(new_filename_full),
};
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory {
true => to_path_dest.join(new_filename_full),
false => to_path_dest
.join(file.path_from_source.clone())
.join(new_filename_full),
};
let to_path_string = to_path.to_string_lossy();
let to_path_string = to_path.to_string_lossy();
if !file.extra_files.is_empty() {
for extra_file in file.extra_files.iter() {
copy_file(extra_file, copy_args)?;
}
}
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",
to_path_string
);
if !copy_args.no_skip_existing && to_path.exists() {
println!(
"Skipping transcode for {} as file already exists",
to_path_string
);
return Ok(());
}
return Ok(());
}
println!("Transcoding {}", to_path_string);
println!("Transcoding {}", to_path_string);
transcode(file.to_owned(), to_path_string.to_string(), config, None)?;
transcode(file.to_owned(), to_path_string.to_string(), config, None)?;
if is_threaded {
println!("Finished Transcoding {}", to_path_string);
}
if is_threaded {
println!("Finished Transcoding {}", to_path_string);
}
Ok(())
Ok(())
}
fn transcode_files(
files: &[File],
copy_args: &CopyCommandArgs,
files: &[File],
copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let transcode_config = transcode_preset_or_config(
copy_args.transcode_preset.as_ref(),
copy_args.transcode_config.as_ref(),
)
.expect("transcode config error");
let transcode_config = transcode_preset_or_config(
copy_args.transcode_preset.as_ref(),
copy_args.transcode_config.as_ref(),
)
.expect("transcode config error");
let threads = copy_args.threads.unwrap_or(1);
let threads = copy_args.threads.unwrap_or(1);
if threads > 1 {
let files_copy = files.to_vec();
if threads > 1 {
let files_copy = files.to_vec();
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
let copy_args = Arc::new(copy_args);
let transcode_config = Arc::new(transcode_config);
let copy_args = Arc::new(copy_args);
let transcode_config = Arc::new(transcode_config);
scope(|s| {
for _ in 0..threads {
s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap();
scope(|s| {
for _ in 0..threads {
s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap();
let job = jobs.pop();
if let Some(job) = job {
let result = transcode_file(&job, &copy_args, &transcode_config, true);
if let Err(err) = result {
panic!("Error Transcoding: {}", err)
}
} else {
break;
}
});
}
});
} else {
for file in files.iter() {
transcode_file(file, copy_args, &transcode_config, false)?;
}
}
let job = jobs.pop();
if let Some(job) = job {
let result = transcode_file(&job, &copy_args, &transcode_config, true);
if let Err(err) = result {
panic!("Error Transcoding: {}", err)
}
} else {
break;
}
});
}
});
} else {
for file in files.iter() {
transcode_file(file, copy_args, &transcode_config, false)?;
}
}
Ok(())
Ok(())
}

View file

@ -11,21 +11,21 @@ use html_escape::encode_text;
use urlencoding::encode as url_encode;
fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<String>) -> String {
let mut html_content = String::new();
let mut html_content = String::new();
let mut path_head = String::new();
if includes_path {
path_head.push_str("<th>Path</th>")
}
let mut path_head = String::new();
if includes_path {
path_head.push_str("<th>Path</th>")
}
let mut link_head = String::new();
if link_base.is_some() {
link_head.push_str("<th>Link</th>")
}
let mut link_head = String::new();
if link_base.is_some() {
link_head.push_str("<th>Link</th>")
}
html_content.push_str(
format!(
"
html_content.push_str(
format!(
"
<table class=\"pure-table pure-table-horizontal\">
<thead>
<tr>
@ -38,69 +38,69 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
</thead>
<tbody>
",
link_head, path_head
)
.as_str(),
);
link_head, path_head
)
.as_str(),
);
let mut is_odd = true;
for file in files.iter() {
let td_class = match is_odd {
true => "pure-table-odd",
false => "pure-table-even",
};
let mut is_odd = true;
for file in files.iter() {
let td_class = match is_odd {
true => "pure-table-odd",
false => "pure-table-even",
};
let data_title = encode_text(&file.info.tags.title);
let data_artist = encode_text(&file.info.tags.artist);
let data_title = encode_text(&file.info.tags.title);
let data_artist = encode_text(&file.info.tags.artist);
let format = if let Some(format) = &file.info.format {
format.to_string()
} else {
"unknown".to_string()
};
let format = if let Some(format) = &file.info.format {
format.to_string()
} else {
"unknown".to_string()
};
let data_format = encode_text(&format);
let data_format = encode_text(&format);
let mut path_data = String::new();
if includes_path {
let file_directory = file.path_from_source.to_string_lossy().to_string();
let mut path_data = String::new();
if includes_path {
let file_directory = file.path_from_source.to_string_lossy().to_string();
path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str());
}
path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str());
}
let mut url_data = String::new();
if let Some(link_base) = &link_base {
let mut url = String::new();
let mut url_data = String::new();
if let Some(link_base) = &link_base {
let mut url = String::new();
url.push_str(link_base.as_str());
url.push('/');
url.push_str(link_base.as_str());
url.push('/');
let file_path = file.join_path_from_source();
let file_path: Vec<&OsStr> = file_path.iter().collect();
let file_path = file.join_path_from_source();
let file_path: Vec<&OsStr> = file_path.iter().collect();
for i in 0..(file_path.len()) {
let file_path_element = file_path.get(i).unwrap();
for i in 0..(file_path.len()) {
let file_path_element = file_path.get(i).unwrap();
url.push_str(
url_encode(
file_path_element
.to_str()
.expect("invalid character in filename"),
)
.to_string()
.as_str(),
);
if i != file_path.len() - 1 {
url.push('/');
}
}
url.push_str(
url_encode(
file_path_element
.to_str()
.expect("invalid character in filename"),
)
.to_string()
.as_str(),
);
if i != file_path.len() - 1 {
url.push('/');
}
}
url_data.push_str(format!("<td><a href=\"{}\">🔗</a></td>", url).as_str());
}
url_data.push_str(format!("<td><a href=\"{}\">🔗</a></td>", url).as_str());
}
html_content.push_str(
format!(
"
html_content.push_str(
format!(
"
<tr class=\"{}\">
{}
{}
@ -109,58 +109,58 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
<td>{}</td>
</tr>
",
td_class, url_data, path_data, data_title, data_artist, data_format
)
.as_str(),
);
is_odd = !is_odd;
}
td_class, url_data, path_data, data_title, data_artist, data_format
)
.as_str(),
);
is_odd = !is_odd;
}
html_content.push_str(
"
html_content.push_str(
"
</tbody>
</table>
",
);
);
html_content
html_content
}
pub fn genhtml_command(
_args: CLIArgs,
genhtml_args: &GenHTMLCommandArgs,
_args: CLIArgs,
genhtml_args: &GenHTMLCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Scanning For Music");
let mut files = scan_for_music(&genhtml_args.source)?;
println!("Scanning For Music");
let mut files = scan_for_music(&genhtml_args.source)?;
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
let mut handler = get_format_handler(file)?;
let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(false)?;
}
file.info = handler.get_audio_file_info(false)?;
}
files.sort_by(|a, b| -> Ordering {
if a.path_from_source != b.path_from_source {
return a.path_from_source.cmp(&b.path_from_source);
}
files.sort_by(|a, b| -> Ordering {
if a.path_from_source != b.path_from_source {
return a.path_from_source.cmp(&b.path_from_source);
}
let a_tags = &a.info.tags;
let b_tags = &b.info.tags;
let a_tags = &a.info.tags;
let b_tags = &b.info.tags;
if a_tags.title != b_tags.title {
return a_tags.title.cmp(&b_tags.title);
}
if a_tags.title != 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)
});
let mut html_content = String::new();
html_content.push_str(
format!(
"
let mut html_content = String::new();
html_content.push_str(
format!(
"
<!DOCTYPE HTML>
<html>
<head>
@ -173,30 +173,30 @@ pub fn genhtml_command(
</head>
<body>
",
encode_text(&genhtml_args.title),
encode_text(&genhtml_args.title),
encode_text(&genhtml_args.description)
)
.as_str(),
);
encode_text(&genhtml_args.title),
encode_text(&genhtml_args.title),
encode_text(&genhtml_args.description)
)
.as_str(),
);
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
html_content.push_str("</body></html>");
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
html_content.push_str("</body></html>");
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html");
let html_index_file = std::fs::File::create(file_path);
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html");
let html_index_file = std::fs::File::create(file_path);
match html_index_file {
Ok(mut file) => match file.write_all(html_content.as_bytes()) {
Ok(_) => {}
Err(e) => {
panic!("Could not write HTML file: {}", e);
}
},
Err(e) => {
panic!("Could not create HTML file: {}", e);
}
}
match html_index_file {
Ok(mut file) => match file.write_all(html_content.as_bytes()) {
Ok(_) => {}
Err(e) => {
panic!("Could not write HTML file: {}", e);
}
},
Err(e) => {
panic!("Could not create HTML file: {}", e);
}
}
Ok(())
Ok(())
}

View file

@ -10,62 +10,62 @@ use crate::utils::formats::get_format_handler;
#[derive(Debug, Clone, Serialize)]
struct Tags {
title: String,
artist: String,
title: String,
artist: String,
}
fn from_main_tags(tags: &crate::types::Tags) -> Tags {
Tags {
title: tags.title.clone(),
artist: tags.artist.clone(),
}
Tags {
title: tags.title.clone(),
artist: tags.artist.clone(),
}
}
pub fn get_tags_command(
_args: CLIArgs,
get_tags_args: &GetTagsCommandArgs,
_args: CLIArgs,
get_tags_args: &GetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let mut files: Vec<File> = Vec::new();
let mut files: Vec<File> = Vec::new();
for file in get_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file)));
}
for file in get_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file)));
}
for file in files.iter_mut() {
let handler = get_format_handler(file)?;
for file in files.iter_mut() {
let handler = get_format_handler(file)?;
file.info.tags = handler.get_tags(true)?;
}
file.info.tags = handler.get_tags(true)?;
}
if files.len() == 1 {
let file = files.first().unwrap();
if files.len() == 1 {
let file = files.first().unwrap();
if get_tags_args.json {
println!(
"{}",
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
);
} else {
println!("{:#?}", from_main_tags(&file.info.tags));
}
} else if get_tags_args.json {
let mut result: HashMap<String, Tags> = HashMap::new();
for file in files.iter() {
result.insert(
file.join_path_to().to_string_lossy().to_string(),
from_main_tags(&file.info.tags),
);
}
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
for file in files.iter() {
println!(
"{}: {:#?}",
file.join_path_to().to_string_lossy(),
from_main_tags(&file.info.tags)
);
}
}
if get_tags_args.json {
println!(
"{}",
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
);
} else {
println!("{:#?}", from_main_tags(&file.info.tags));
}
} else if get_tags_args.json {
let mut result: HashMap<String, Tags> = HashMap::new();
for file in files.iter() {
result.insert(
file.join_path_to().to_string_lossy().to_string(),
from_main_tags(&file.info.tags),
);
}
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
for file in files.iter() {
println!(
"{}: {:#?}",
file.join_path_to().to_string_lossy(),
from_main_tags(&file.info.tags)
);
}
}
Ok(())
Ok(())
}

View file

@ -11,184 +11,184 @@ use crate::utils::replaygain::analyze_replaygain;
use crate::utils::scan_for_music;
fn rename_file(process_args: &ProcessCommandArgs, file: &mut File) {
let title = &file.info.tags.title;
let artist = &file.info.tags.artist;
let title = &file.info.tags.title;
let artist = &file.info.tags.artist;
let replace_char = "_".to_string();
let replace_char = "_".to_string();
// Step 1: Remove Newlines
let title = title.replace('\n', "");
let artist = artist.replace('\n', "");
// Step 1: Remove Newlines
let title = title.replace('\n', "");
let artist = artist.replace('\n', "");
// Step 2: Strip ASCII
let title = reduce_to_ascii(title);
let artist = reduce_to_ascii(artist);
// Step 2: Strip ASCII
let title = reduce_to_ascii(title);
let artist = reduce_to_ascii(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 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 = format!("{} - {}", artist, title);
// Step 4: Join Filename
let filename = format!("{} - {}", artist, title);
if filename == file.filename {
return;
}
if filename == file.filename {
return;
}
// Step 5: Make new File with filename set
let mut new_file = file.clone();
new_file.filename = filename.clone();
// Step 5: Make new File with filename set
let mut new_file = file.clone();
new_file.filename = filename.clone();
let extension = &file.extension;
let extension = &file.extension;
// 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)
}
}
}
// 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<File> = Vec::new();
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();
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;
let extra_extension = &extra_file.extension;
println!(
"Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}",
file.filename, filename
);
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 {
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;
}
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.info.supports_replaygain {
println!(
"Skipping replaygain for {:?}, not supported",
file.join_path_from_source()
);
if !file.info.supports_replaygain {
println!(
"Skipping replaygain for {:?}, not supported",
file.join_path_from_source()
);
return Ok(());
}
return Ok(());
}
if file.info.contains_replaygain && !force {
println!(
"Skipping replaygain for {:?}, contains already",
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()
);
println!(
"Analyzing replaygain for {:?}",
file.join_path_from_source()
);
let replaygain_data = analyze_replaygain(file.join_path_to())?;
let replaygain_data = analyze_replaygain(file.join_path_to())?;
let mut handler = get_format_handler(file)?;
let mut handler = get_format_handler(file)?;
handler.set_replaygain_data(replaygain_data)?;
handler.save_changes()?;
handler.set_replaygain_data(replaygain_data)?;
handler.save_changes()?;
println!(
"Applied replaygain tags for {:?}",
file.join_path_from_source()
);
println!(
"Applied replaygain tags for {:?}",
file.join_path_from_source()
);
Ok(())
Ok(())
}
pub fn process_command(
_args: CLIArgs,
process_args: &ProcessCommandArgs,
_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!("Scanning For Music");
let mut files = scan_for_music(&process_args.source)?;
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
println!("Analysing Files");
for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source());
let mut handler = get_format_handler(file)?;
let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(false)?;
}
file.info = handler.get_audio_file_info(false)?;
}
println!("Renaming Files");
for file in files.iter_mut() {
rename_file(process_args, file);
}
println!("Renaming Files");
for file in files.iter_mut() {
rename_file(process_args, file);
}
if !process_args.skip_replaygain && !process_args.dry_run {
println!("Adding ReplayGain Tags to Files");
if !process_args.skip_replaygain && !process_args.dry_run {
println!("Adding ReplayGain Tags to Files");
let threads = process_args.replaygain_threads.unwrap_or(0);
let threads = process_args.replaygain_threads.unwrap_or(0);
if threads <= 1 {
for file in files.iter_mut() {
add_replaygain_tags(file, process_args.force_replaygain)?;
}
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<File>>> = Arc::new(Mutex::new(files));
return Ok(());
} else {
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files));
scope(|s| {
for _ in 0..threads {
s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap();
scope(|s| {
for _ in 0..threads {
s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap();
let job = jobs.pop();
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;
}
});
}
});
}
}
let job = jobs.pop();
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(())
Ok(())
}

View file

@ -6,28 +6,28 @@ use crate::types::File;
use crate::utils::formats::get_format_handler;
pub fn set_tags_command(
_args: CLIArgs,
add_tags_args: &SetTagsCommandArgs,
_args: CLIArgs,
add_tags_args: &SetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let mut files: Vec<File> = Vec::new();
let mut files: Vec<File> = Vec::new();
for file in add_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file)));
}
for file in add_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file)));
}
for file in files.iter() {
let mut handler = get_format_handler(file)?;
for file in files.iter() {
let mut handler = get_format_handler(file)?;
if let Some(title) = &add_tags_args.title {
handler.set_title(title.clone())?;
}
if let Some(title) = &add_tags_args.title {
handler.set_title(title.clone())?;
}
if let Some(artist) = &add_tags_args.artist {
handler.set_artist(artist.clone())?;
}
if let Some(artist) = &add_tags_args.artist {
handler.set_artist(artist.clone())?;
}
handler.save_changes()?;
}
handler.save_changes()?;
}
Ok(())
Ok(())
}

View file

@ -12,73 +12,73 @@ use crate::utils::transcoder::presets::transcode_preset_or_config;
use crate::utils::transcoder::transcode;
pub fn transcode_command(
_args: CLIArgs,
transcode_args: &TranscodeCommandArgs,
_args: CLIArgs,
transcode_args: &TranscodeCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(preset) = &transcode_args.transcode_preset {
if preset == "list" {
print_presets();
exit(0);
}
}
if let Some(preset) = &transcode_args.transcode_preset {
if preset == "list" {
print_presets();
exit(0);
}
}
let transcode_config = transcode_preset_or_config(
transcode_args.transcode_preset.as_ref(),
transcode_args.transcode_config.as_ref(),
)
.expect("transcode config error");
let transcode_config = transcode_preset_or_config(
transcode_args.transcode_preset.as_ref(),
transcode_args.transcode_config.as_ref(),
)
.expect("transcode config error");
println!("Transcoding");
println!("Transcoding");
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
if !transcode_args.ignore_extension {
if let Some(ref file_extension) = transcode_config.file_extension {
if file_extension != &output_file.extension {
panic!(
concat!(
"{} is not the recommended ",
"extension for specified transcode config ",
"please change it to {} ",
"or run with --ignore-extension"
),
output_file.extension, file_extension
);
}
}
}
if !transcode_args.ignore_extension {
if let Some(ref file_extension) = transcode_config.file_extension {
if file_extension != &output_file.extension {
panic!(
concat!(
"{} is not the recommended ",
"extension for specified transcode config ",
"please change it to {} ",
"or run with --ignore-extension"
),
output_file.extension, file_extension
);
}
}
}
let (tx, rx) = mpsc::channel::<String>();
let mut child: Option<JoinHandle<()>> = None;
let (tx, rx) = mpsc::channel::<String>();
let mut child: Option<JoinHandle<()>> = None;
if !transcode_args.hide_progress {
child = Some(thread::spawn(move || loop {
let progress = rx.recv();
if !transcode_args.hide_progress {
child = Some(thread::spawn(move || loop {
let progress = rx.recv();
if let Ok(progress_str) = progress {
println!("Transcode Progress: {}", progress_str);
} else {
break;
}
}));
}
if let Ok(progress_str) = progress {
println!("Transcode Progress: {}", progress_str);
} else {
break;
}
}));
}
transcode(
input_file,
transcode_args.dest.clone(),
&transcode_config,
match transcode_args.hide_progress {
true => None,
false => Some(tx),
},
)?;
transcode(
input_file,
transcode_args.dest.clone(),
&transcode_config,
match transcode_args.hide_progress {
true => None,
false => Some(tx),
},
)?;
if let Some(child) = child {
child.join().expect("oops! the child thread panicked");
}
if let Some(child) = child {
child.join().expect("oops! the child thread panicked");
}
println!("Transcode Finished");
println!("Transcode Finished");
Ok(())
Ok(())
}

View file

@ -15,28 +15,28 @@ use commands::set_tags::set_tags_command;
use commands::transcode::transcode_command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = CLIArgs::parse();
let cli = CLIArgs::parse();
let command = cli.command.to_owned();
let command = cli.command.to_owned();
match command {
Commands::Process(process_args) => {
return process_command(cli, &process_args);
}
Commands::Genhtml(genhtml_args) => {
return genhtml_command(cli, &genhtml_args);
}
Commands::Transcode(transcode_args) => {
return transcode_command(cli, &transcode_args);
}
Commands::Copy(copy_args) => {
return copy_command(cli, &copy_args);
}
Commands::SetTags(set_tags_args) => {
return set_tags_command(cli, &set_tags_args);
}
Commands::GetTags(get_tags_args) => {
return get_tags_command(cli, &get_tags_args);
}
}
match command {
Commands::Process(process_args) => {
return process_command(cli, &process_args);
}
Commands::Genhtml(genhtml_args) => {
return genhtml_command(cli, &genhtml_args);
}
Commands::Transcode(transcode_args) => {
return transcode_command(cli, &transcode_args);
}
Commands::Copy(copy_args) => {
return copy_command(cli, &copy_args);
}
Commands::SetTags(set_tags_args) => {
return set_tags_command(cli, &set_tags_args);
}
Commands::GetTags(get_tags_args) => {
return get_tags_command(cli, &get_tags_args);
}
}
}

View file

@ -4,115 +4,115 @@ use crate::utils::format_detection::FileFormat;
#[derive(Debug, Clone)]
pub struct Tags {
pub title: String,
pub artist: String,
pub title: String,
pub artist: String,
}
impl Default for Tags {
fn default() -> Self {
Tags {
title: "".to_string(),
artist: "".to_string(),
}
}
fn default() -> Self {
Tags {
title: "".to_string(),
artist: "".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct ReplayGainData {
pub track_gain: String,
pub track_peak: String,
pub track_gain: String,
pub track_peak: String,
}
#[derive(Debug, Clone)]
pub struct ReplayGainRawData {
pub track_gain: f64,
pub track_peak: f64,
pub track_gain: f64,
pub track_peak: f64,
}
impl ReplayGainRawData {
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
if is_ogg_opus {
ReplayGainData {
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
track_peak: "".to_string(), // Not Required
}
} else {
ReplayGainData {
track_gain: format!("{:.2} dB", self.track_gain),
track_peak: format!("{:.6}", self.track_peak),
}
}
}
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
if is_ogg_opus {
ReplayGainData {
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
track_peak: "".to_string(), // Not Required
}
} else {
ReplayGainData {
track_gain: format!("{:.2} dB", self.track_gain),
track_peak: format!("{:.6}", self.track_peak),
}
}
}
}
#[derive(Default, Debug, Clone)]
pub struct AudioFileInfo {
pub tags: Tags,
pub contains_replaygain: bool,
pub supports_replaygain: bool,
pub format: Option<FileFormat>,
pub tags: Tags,
pub contains_replaygain: bool,
pub supports_replaygain: bool,
pub format: Option<FileFormat>,
}
#[derive(Debug, Clone)]
pub struct File {
pub filename: String,
pub extension: String,
pub filename: String,
pub extension: String,
// relative path to file's folder
pub path_to: PathBuf,
// path to folder from source
pub path_from_source: PathBuf,
// relative path to file's folder
pub path_to: PathBuf,
// path to folder from source
pub path_from_source: PathBuf,
pub extra_files: Vec<File>,
pub extra_files: Vec<File>,
pub info: AudioFileInfo,
pub info: AudioFileInfo,
}
impl File {
pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
let full_file_path = PathBuf::from(&source_dir).join(full_path);
pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
let full_file_path = PathBuf::from(&source_dir).join(full_path);
let filename_without_extension = full_file_path
.file_stem()
.expect("filename invalid")
.to_string_lossy()
.to_string();
let filename_without_extension = full_file_path
.file_stem()
.expect("filename invalid")
.to_string_lossy()
.to_string();
let extension = full_file_path.extension();
let extension = full_file_path.extension();
let extension = if let Some(extension) = extension {
extension.to_string_lossy().to_string()
} else {
"".to_string()
};
let extension = if let Some(extension) = extension {
extension.to_string_lossy().to_string()
} else {
"".to_string()
};
let path_from_src = full_file_path
.strip_prefix(&source_dir)
.expect("couldn't get path relative to source");
let path_from_src = full_file_path
.strip_prefix(&source_dir)
.expect("couldn't get path relative to source");
let mut folder_path_from_src = path_from_src.to_path_buf();
folder_path_from_src.pop();
let mut folder_path_from_src = path_from_src.to_path_buf();
folder_path_from_src.pop();
let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src);
let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src);
File {
filename: filename_without_extension,
extension,
path_from_source: folder_path_from_src,
path_to,
extra_files: Vec::new(),
info: AudioFileInfo::default(),
}
}
File {
filename: filename_without_extension,
extension,
path_from_source: folder_path_from_src,
path_to,
extra_files: Vec::new(),
info: AudioFileInfo::default(),
}
}
pub fn join_filename(&self) -> String {
format!("{}.{}", self.filename, self.extension)
}
pub fn join_path_to(&self) -> PathBuf {
PathBuf::from(&self.path_to).join(self.join_filename())
}
pub fn join_path_from_source(&self) -> PathBuf {
PathBuf::from(&self.path_from_source).join(self.join_filename())
}
pub fn join_filename(&self) -> String {
format!("{}.{}", self.filename, self.extension)
}
pub fn join_path_to(&self) -> PathBuf {
PathBuf::from(&self.path_to).join(self.join_filename())
}
pub fn join_path_from_source(&self) -> PathBuf {
PathBuf::from(&self.path_from_source).join(self.join_filename())
}
}

View file

@ -4,47 +4,47 @@ use std::collections::HashMap;
const MAPPINGS_DATA: &str = include_str!("mappings.json");
lazy_static! {
static ref MAPPINGS: HashMap<char, String> = {
let data: HashMap<String, String> =
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
static ref MAPPINGS: HashMap<char, String> = {
let data: HashMap<String, String> =
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
let mut replacement_map: HashMap<char, String> = HashMap::new();
for (chr, repl) in &data {
match chr.parse::<u32>() {
Ok(n) => {
let b = char::from_u32(n).expect("invalid char in string");
let mut replacement_map: HashMap<char, String> = HashMap::new();
for (chr, repl) in &data {
match chr.parse::<u32>() {
Ok(n) => {
let b = char::from_u32(n).expect("invalid char in string");
replacement_map.insert(b, repl.to_string());
}
Err(e) => {
panic!(
"mapping data broken, could not parse char {} with error {}",
chr, e
);
}
}
}
replacement_map.insert(b, repl.to_string());
}
Err(e) => {
panic!(
"mapping data broken, could not parse char {} with error {}",
chr, e
);
}
}
}
replacement_map
};
replacement_map
};
}
pub fn reduce_to_ascii(input: String) -> String {
if input.is_ascii() {
return input;
}
if input.is_ascii() {
return input;
}
let mut output = String::with_capacity(input.len());
for c in input.chars() {
if c.is_ascii() {
output.push(c);
continue;
}
let mut output = String::with_capacity(input.len());
for c in input.chars() {
if c.is_ascii() {
output.push(c);
continue;
}
if let Some(replacement) = MAPPINGS.get(&c) {
output.push_str(replacement);
}
}
if let Some(replacement) = MAPPINGS.get(&c) {
output.push_str(replacement);
}
}
output
output
}

View file

@ -2,34 +2,34 @@ use std::{fmt, io, process};
#[derive(Debug)]
pub enum AnalyzeError {
FFProbeError(FFProbeError),
IOError(io::Error),
ParseError(serde_json::Error),
FFProbeError(FFProbeError),
IOError(io::Error),
ParseError(serde_json::Error),
}
impl fmt::Display for AnalyzeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnalyzeError::FFProbeError(err) => write!(f, "{}", err),
AnalyzeError::IOError(err) => write!(f, "{}", err),
AnalyzeError::ParseError(err) => write!(f, "{}", err),
}
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnalyzeError::FFProbeError(err) => write!(f, "{}", err),
AnalyzeError::IOError(err) => write!(f, "{}", err),
AnalyzeError::ParseError(err) => write!(f, "{}", err),
}
}
}
#[derive(Debug, Clone)]
pub struct FFProbeError {
pub exit_status: process::ExitStatus,
pub stderr: String,
pub exit_status: process::ExitStatus,
pub stderr: String,
}
impl fmt::Display for FFProbeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"ffprobe exited with error code {}, stderr: {}",
self.exit_status.code().unwrap(),
self.stderr
)
}
}
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"ffprobe exited with error code {}, stderr: {}",
self.exit_status.code().unwrap(),
self.stderr
)
}
}

View file

@ -2,34 +2,34 @@ use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct FFProbeOutput {
pub format: FFProbeOutputFormat,
pub format: FFProbeOutputFormat,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FFProbeOutputFormat {
pub tags: FFProbeOutputTags,
pub tags: FFProbeOutputTags,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FFProbeOutputTags {
#[serde(alias = "TITLE")]
pub title: String,
#[serde(default, alias = "ARTIST")]
pub artist: String,
#[serde(alias = "TITLE")]
pub title: String,
#[serde(default, alias = "ARTIST")]
pub artist: String,
#[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")]
pub replaygain_track_peak: Option<String>,
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
pub replaygain_track_gain: Option<String>,
#[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")]
pub replaygain_track_peak: Option<String>,
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
pub replaygain_track_gain: Option<String>,
}
impl Default for FFProbeOutputTags {
fn default() -> Self {
FFProbeOutputTags {
title: "".to_string(),
artist: "".to_string(),
replaygain_track_peak: None,
replaygain_track_gain: None,
}
}
}
fn default() -> Self {
FFProbeOutputTags {
title: "".to_string(),
artist: "".to_string(),
replaygain_track_peak: None,
replaygain_track_gain: None,
}
}
}

View file

@ -1,47 +1,43 @@
mod ffprobe_output;
pub mod errors;
mod ffprobe_output;
pub mod types;
use std::{
convert::Into,
path::Path,
process::Command,
};
use std::{convert::Into, path::Path, process::Command};
use self::errors::{AnalyzeError, FFProbeError};
pub fn analyze(path: &Path) -> Result<types::FFProbeData, AnalyzeError> {
let output = Command::new(crate::meta::FFPROBE)
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
path.to_str().unwrap(),
])
.output();
let output = Command::new(crate::meta::FFPROBE)
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
path.to_str().unwrap(),
])
.output();
if let Err(err) = output {
return Err(AnalyzeError::IOError(err));
}
if let Err(err) = output {
return Err(AnalyzeError::IOError(err));
}
let output = output.unwrap();
let output = output.unwrap();
if !output.status.success() {
return Err(AnalyzeError::FFProbeError(FFProbeError {
exit_status: output.status,
stderr: String::from_utf8(output.stderr).unwrap(),
}));
}
if !output.status.success() {
return Err(AnalyzeError::FFProbeError(FFProbeError {
exit_status: output.status,
stderr: String::from_utf8(output.stderr).unwrap(),
}));
}
let output = String::from_utf8(output.stdout).unwrap();
let ffprobe_out: serde_json::Result<ffprobe_output::FFProbeOutput> =
serde_json::from_str(output.as_str());
let output = String::from_utf8(output.stdout).unwrap();
let ffprobe_out: serde_json::Result<ffprobe_output::FFProbeOutput> =
serde_json::from_str(output.as_str());
let ffprobe_out = ffprobe_out.unwrap();
let ffprobe_out = ffprobe_out.unwrap();
Ok(types::FFProbeData {
tags: ffprobe_out.format.tags.into(),
})
Ok(types::FFProbeData {
tags: ffprobe_out.format.tags.into(),
})
}

View file

@ -4,24 +4,24 @@ use super::ffprobe_output;
#[derive(Debug, Clone, Serialize)]
pub struct FFProbeTags {
pub title: String,
pub artist: String,
pub replaygain_track_peak: Option<String>,
pub replaygain_track_gain: Option<String>,
pub title: String,
pub artist: String,
pub replaygain_track_peak: Option<String>,
pub replaygain_track_gain: Option<String>,
}
impl From<ffprobe_output::FFProbeOutputTags> for FFProbeTags {
fn from(val: ffprobe_output::FFProbeOutputTags) -> Self {
FFProbeTags {
title: val.title,
artist: val.artist,
replaygain_track_peak: val.replaygain_track_peak,
replaygain_track_gain: val.replaygain_track_gain,
}
}
fn from(val: ffprobe_output::FFProbeOutputTags) -> Self {
FFProbeTags {
title: val.title,
artist: val.artist,
replaygain_track_peak: val.replaygain_track_peak,
replaygain_track_gain: val.replaygain_track_gain,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FFProbeData {
pub tags: FFProbeTags,
}
pub tags: FFProbeTags,
}

View file

@ -4,126 +4,126 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum FormatDetectionError {
#[error("could not read file")]
FileReadError,
#[error("file format unrecognised")]
UnrecognisedFormat,
#[error("could not read file")]
FileReadError,
#[error("file format unrecognised")]
UnrecognisedFormat,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FileFormat {
FLAC,
MP3,
OggVorbis,
OggOpus,
OggFLAC,
OggSpeex,
OggTheora,
AIFF,
Wav,
WavPack,
FLAC,
MP3,
OggVorbis,
OggOpus,
OggFLAC,
OggSpeex,
OggTheora,
AIFF,
Wav,
WavPack,
}
impl ToString for FileFormat {
fn to_string(&self) -> String {
match self {
FileFormat::FLAC => "FLAC".to_string(),
FileFormat::MP3 => "MP3".to_string(),
FileFormat::OggVorbis => "Ogg (Vorbis)".to_string(),
FileFormat::OggOpus => "Ogg (Opus)".to_string(),
FileFormat::OggFLAC => "Ogg (FLAC)".to_string(),
FileFormat::OggSpeex => "Ogg (Speex)".to_string(),
FileFormat::OggTheora => "Ogg (Theora)".to_string(),
FileFormat::AIFF => "AIFF".to_string(),
FileFormat::Wav => "Wav".to_string(),
FileFormat::WavPack => "WavPack".to_string(),
}
}
fn to_string(&self) -> String {
match self {
FileFormat::FLAC => "FLAC".to_string(),
FileFormat::MP3 => "MP3".to_string(),
FileFormat::OggVorbis => "Ogg (Vorbis)".to_string(),
FileFormat::OggOpus => "Ogg (Opus)".to_string(),
FileFormat::OggFLAC => "Ogg (FLAC)".to_string(),
FileFormat::OggSpeex => "Ogg (Speex)".to_string(),
FileFormat::OggTheora => "Ogg (Theora)".to_string(),
FileFormat::AIFF => "AIFF".to_string(),
FileFormat::Wav => "Wav".to_string(),
FileFormat::WavPack => "WavPack".to_string(),
}
}
}
pub fn detect_ogg_subformat(path: &Path) -> Result<FileFormat, FormatDetectionError> {
let file = File::open(path);
let file = File::open(path);
if let Err(_error) = file {
return Err(FormatDetectionError::FileReadError);
}
if let Err(_error) = file {
return Err(FormatDetectionError::FileReadError);
}
let file = file.unwrap();
let file = file.unwrap();
let limit = file
.metadata()
.map(|m| std::cmp::min(m.len(), 128) as usize + 1)
.unwrap_or(0);
let limit = file
.metadata()
.map(|m| std::cmp::min(m.len(), 128) as usize + 1)
.unwrap_or(0);
let mut bytes = Vec::with_capacity(limit);
if let Err(_err) = file.take(128).read_to_end(&mut bytes) {
return Err(FormatDetectionError::FileReadError);
}
let mut bytes = Vec::with_capacity(limit);
if let Err(_err) = file.take(128).read_to_end(&mut bytes) {
return Err(FormatDetectionError::FileReadError);
}
let mut mem = Bytes::from(bytes);
mem.advance(28);
let mut mem = Bytes::from(bytes);
mem.advance(28);
let vorbis_type = mem.get_u8();
let vorbis_type = mem.get_u8();
match vorbis_type {
0x01 => return Ok(FileFormat::OggVorbis),
0x7f => return Ok(FileFormat::OggFLAC),
0x80 => return Ok(FileFormat::OggTheora),
// S for speex
0x53 => return Ok(FileFormat::OggSpeex),
_ => {}
}
match vorbis_type {
0x01 => return Ok(FileFormat::OggVorbis),
0x7f => return Ok(FileFormat::OggFLAC),
0x80 => return Ok(FileFormat::OggTheora),
// S for speex
0x53 => return Ok(FileFormat::OggSpeex),
_ => {}
}
Err(FormatDetectionError::UnrecognisedFormat)
Err(FormatDetectionError::UnrecognisedFormat)
}
fn wavpack_matcher(buf: &[u8]) -> bool {
// 77 76 70 6b
buf.len() >= 4 && buf[0] == 0x77 && buf[1] == 0x76 && buf[2] == 0x70 && buf[3] == 0x6b
// 77 76 70 6b
buf.len() >= 4 && buf[0] == 0x77 && buf[1] == 0x76 && buf[2] == 0x70 && buf[3] == 0x6b
}
pub fn detect_format(path: &Path) -> Result<FileFormat, FormatDetectionError> {
let mut info = infer::Infer::new();
info.add("custom/wavpack", "wv", wavpack_matcher);
let mut info = infer::Infer::new();
info.add("custom/wavpack", "wv", wavpack_matcher);
let kind = info.get_from_path(path);
let kind = info.get_from_path(path);
if let Err(_error) = kind {
return Err(FormatDetectionError::FileReadError);
}
if let Err(_error) = kind {
return Err(FormatDetectionError::FileReadError);
}
let kind = kind.unwrap();
let kind = kind.unwrap();
if kind.is_none() {
return Err(FormatDetectionError::UnrecognisedFormat);
}
if kind.is_none() {
return Err(FormatDetectionError::UnrecognisedFormat);
}
let kind = kind.unwrap();
let kind = kind.unwrap();
match kind.mime_type() {
"audio/mpeg" => {
return Ok(FileFormat::MP3);
}
"audio/x-wav" => {
return Ok(FileFormat::Wav);
}
"custom/wavpack" => {
return Ok(FileFormat::WavPack);
}
"audio/ogg" => {
return detect_ogg_subformat(path);
}
"audio/x-flac" => {
return Ok(FileFormat::FLAC);
}
"audio/x-aiff" => {
return Ok(FileFormat::AIFF);
}
"audio/opus" => {
return Ok(FileFormat::OggOpus);
}
_ => {}
}
match kind.mime_type() {
"audio/mpeg" => {
return Ok(FileFormat::MP3);
}
"audio/x-wav" => {
return Ok(FileFormat::Wav);
}
"custom/wavpack" => {
return Ok(FileFormat::WavPack);
}
"audio/ogg" => {
return detect_ogg_subformat(path);
}
"audio/x-flac" => {
return Ok(FileFormat::FLAC);
}
"audio/x-aiff" => {
return Ok(FileFormat::AIFF);
}
"audio/opus" => {
return Ok(FileFormat::OggOpus);
}
_ => {}
}
Err(FormatDetectionError::UnrecognisedFormat)
Err(FormatDetectionError::UnrecognisedFormat)
}

View file

@ -1,189 +1,189 @@
use std::{
path::{Path, PathBuf},
process::Command,
path::{Path, PathBuf},
process::Command,
};
use string_error::into_err;
use crate::{
types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags},
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
utils::{
ffprobe,
format_detection::{detect_format, FileFormat},
},
types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags},
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
utils::{
ffprobe,
format_detection::{detect_format, FileFormat},
},
};
#[derive(Default)]
struct Changes {
title: Option<String>,
artist: Option<String>,
title: Option<String>,
artist: Option<String>,
}
#[derive(Default)]
struct ExtractedData {
tags: Tags,
replaygain_data: Option<ReplayGainData>,
tags: Tags,
replaygain_data: Option<ReplayGainData>,
}
pub struct GenericFFMpegAudioFormat {
file_format: FileFormat,
path: Box<PathBuf>,
file_format: FileFormat,
path: Box<PathBuf>,
extracted_data: ExtractedData,
changes: Changes,
extracted_data: ExtractedData,
changes: Changes,
}
impl GenericFFMpegAudioFormat {
fn analyze(&mut self) -> Result<(), BoxedError> {
let output = ffprobe::analyze(&self.path);
fn analyze(&mut self) -> Result<(), BoxedError> {
let output = ffprobe::analyze(&self.path);
if let Err(err) = output {
return Err(into_err(format!("{}", err)));
}
if let Err(err) = output {
return Err(into_err(format!("{}", err)));
}
let output = output.unwrap();
let output = output.unwrap();
self.extracted_data.tags = Tags {
title: output.tags.title,
artist: output.tags.artist,
};
self.extracted_data.tags = Tags {
title: output.tags.title,
artist: output.tags.artist,
};
if output.tags.replaygain_track_gain.is_some()
&& output.tags.replaygain_track_peak.is_some()
{
self.extracted_data.replaygain_data = Some(ReplayGainData {
track_gain: output.tags.replaygain_track_gain.unwrap(),
track_peak: output.tags.replaygain_track_peak.unwrap(),
});
} else {
self.extracted_data.replaygain_data = None;
}
if output.tags.replaygain_track_gain.is_some()
&& output.tags.replaygain_track_peak.is_some()
{
self.extracted_data.replaygain_data = Some(ReplayGainData {
track_gain: output.tags.replaygain_track_gain.unwrap(),
track_peak: output.tags.replaygain_track_peak.unwrap(),
});
} else {
self.extracted_data.replaygain_data = None;
}
Ok(())
}
Ok(())
}
}
impl FormatHandler for GenericFFMpegAudioFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let mut tags = self.extracted_data.tags.clone();
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let mut tags = self.extracted_data.tags.clone();
if let Some(title) = &self.changes.title {
tags.title = title.clone();
}
if let Some(title) = &self.changes.title {
tags.title = title.clone();
}
if let Some(artist) = &self.changes.title {
tags.artist = artist.clone();
}
if let Some(artist) = &self.changes.title {
tags.artist = artist.clone();
}
if !allow_missing {
if tags.title.is_empty() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if tags.artist.is_empty() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
if !allow_missing {
if tags.title.is_empty() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if tags.artist.is_empty() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
Ok(tags)
}
Ok(tags)
}
fn contains_replaygain_tags(&self) -> bool {
false
}
fn contains_replaygain_tags(&self) -> bool {
false
}
fn supports_replaygain(&self) -> bool {
false
}
fn supports_replaygain(&self) -> bool {
false
}
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.changes.title = Some(title);
Ok(())
}
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.changes.title = Some(title);
Ok(())
}
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.changes.artist = Some(artist);
Ok(())
}
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.changes.artist = Some(artist);
Ok(())
}
fn set_replaygain_data(&mut self, _data: ReplayGainRawData) -> Result<(), BoxedError> {
panic!("ffprobe doesn't support setting replaygain data, check supports_replaygain() f")
}
fn set_replaygain_data(&mut self, _data: ReplayGainRawData) -> Result<(), BoxedError> {
panic!("ffprobe doesn't support setting replaygain data, check supports_replaygain() f")
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
if self.changes.title.is_none() && self.changes.artist.is_none() {
return Ok(());
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
if self.changes.title.is_none() && self.changes.artist.is_none() {
return Ok(());
}
let mut args: Vec<String> = Vec::new();
let mut args: Vec<String> = Vec::new();
let tempdir = tempfile::tempdir()?;
let temp_file = tempdir
.path()
.join(PathBuf::from(self.path.file_name().unwrap()));
let tempdir = tempfile::tempdir()?;
let temp_file = tempdir
.path()
.join(PathBuf::from(self.path.file_name().unwrap()));
args.extend(vec![
"-i".to_string(),
self.path.to_string_lossy().to_string(),
]);
args.extend(vec![
"-i".to_string(),
self.path.to_string_lossy().to_string(),
]);
args.extend(vec!["-c".to_string(), "copy".to_string()]);
args.extend(vec!["-c".to_string(), "copy".to_string()]);
if let Some(title) = &self.changes.title {
args.extend(vec![
"-metadata".to_string(),
format!("title=\"{}\"", title.as_str()),
])
}
if let Some(title) = &self.changes.title {
args.extend(vec![
"-metadata".to_string(),
format!("title=\"{}\"", title.as_str()),
])
}
if let Some(artist) = &self.changes.artist {
args.extend(vec![
"-metadata".to_string(),
format!("artist={}", artist.as_str()),
])
}
if let Some(artist) = &self.changes.artist {
args.extend(vec![
"-metadata".to_string(),
format!("artist={}", artist.as_str()),
])
}
args.push(temp_file.to_string_lossy().to_string());
args.push(temp_file.to_string_lossy().to_string());
let output = Command::new(crate::meta::FFMPEG).args(args).output()?;
let output = Command::new(crate::meta::FFMPEG).args(args).output()?;
println!("{:?}", String::from_utf8(output.stderr));
println!("{:?}", String::from_utf8(output.stderr));
std::fs::copy(temp_file, self.path.to_path_buf())?;
std::fs::copy(temp_file, self.path.to_path_buf())?;
Ok(())
}
Ok(())
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: Some(self.file_format),
})
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: Some(self.file_format),
})
}
}
pub fn new_handler(
path: &Path,
file_format: Option<FileFormat>,
path: &Path,
file_format: Option<FileFormat>,
) -> Result<GenericFFMpegAudioFormat, BoxedError> {
let mut file_format = file_format;
if file_format.is_none() {
file_format = Some(detect_format(path)?);
}
let mut file_format = file_format;
if file_format.is_none() {
file_format = Some(detect_format(path)?);
}
let mut handler = GenericFFMpegAudioFormat {
file_format: file_format.unwrap(),
path: Box::new(path.to_path_buf()),
extracted_data: ExtractedData::default(),
changes: Changes::default(),
};
let mut handler = GenericFFMpegAudioFormat {
file_format: file_format.unwrap(),
path: Box::new(path.to_path_buf()),
extracted_data: ExtractedData::default(),
changes: Changes::default(),
};
handler.analyze()?;
handler.analyze()?;
Ok(handler)
Ok(handler)
}

View file

@ -1,115 +1,114 @@
use std::path::PathBuf;
use crate::{
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{FormatHandler, AudioFormatError, BoxedError}
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
};
pub struct FLACAudioFormat {
flac_tags: metaflac::Tag,
path: Box<PathBuf>,
flac_tags: metaflac::Tag,
path: Box<PathBuf>,
}
fn flac_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
}
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
}
}
impl FormatHandler for FLACAudioFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let title = flac_get_first(&self.flac_tags, "TITLE");
let artist = flac_get_first(&self.flac_tags, "ARTIST");
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let title = flac_get_first(&self.flac_tags, "TITLE");
let artist = flac_get_first(&self.flac_tags, "ARTIST");
if !allow_missing {
if title.is_none() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if artist.is_none() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
if !allow_missing {
if title.is_none() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if artist.is_none() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
Ok(Tags {
title: title.unwrap(),
artist: artist.unwrap(),
})
}
Ok(Tags {
title: title.unwrap(),
artist: artist.unwrap(),
})
}
fn contains_replaygain_tags(&self) -> bool {
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
fn contains_replaygain_tags(&self) -> bool {
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
if track_gain.is_none() || track_peak.is_none() {
return false;
}
if track_gain.is_none() || track_peak.is_none() {
return false;
}
true
}
true
}
fn supports_replaygain(&self) -> bool {
true
}
fn supports_replaygain(&self) -> bool {
true
}
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("TITLE");
self.flac_tags.set_vorbis("TITLE", vec![title]);
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("TITLE");
self.flac_tags.set_vorbis("TITLE", vec![title]);
Ok(())
}
Ok(())
}
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("ARTIST");
self.flac_tags.set_vorbis("ARTIST", vec![artist]);
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("ARTIST");
self.flac_tags.set_vorbis("ARTIST", vec![artist]);
Ok(())
}
Ok(())
}
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
self.flac_tags.set_vorbis(
"REPLAYGAIN_TRACK_GAIN",
vec![format!("{:.2} dB", data.track_gain)],
);
self.flac_tags.set_vorbis(
"REPLAYGAIN_TRACK_PEAK",
vec![format!("{:.6}", data.track_peak)],
);
self.flac_tags.set_vorbis(
"REPLAYGAIN_TRACK_GAIN",
vec![format!("{:.2} dB", data.track_gain)],
);
self.flac_tags.set_vorbis(
"REPLAYGAIN_TRACK_PEAK",
vec![format!("{:.6}", data.track_peak)],
);
Ok(())
}
Ok(())
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
self.flac_tags.write_to_path(self.path.as_path())?;
Ok(())
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
self.flac_tags.write_to_path(self.path.as_path())?;
Ok(())
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: Some(FileFormat::FLAC),
})
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: Some(FileFormat::FLAC),
})
}
}
pub fn new_handler(path: &PathBuf) -> Result<FLACAudioFormat, BoxedError> {
Ok(FLACAudioFormat {
flac_tags: metaflac::Tag::read_from_path(path)?,
path: Box::new(path.clone()),
})
Ok(FLACAudioFormat {
flac_tags: metaflac::Tag::read_from_path(path)?,
path: Box::new(path.clone()),
})
}

View file

@ -3,131 +3,131 @@ use std::path::PathBuf;
use id3::TagLike;
use crate::{
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{FormatHandler, AudioFormatError, BoxedError},
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
};
pub struct ID3AudioFormat {
id3_tags: id3::Tag,
path: Box<PathBuf>,
id3_tags: id3::Tag,
path: Box<PathBuf>,
}
impl FormatHandler for ID3AudioFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let title = self.id3_tags.title();
let artist = self.id3_tags.artist();
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let title = self.id3_tags.title();
let artist = self.id3_tags.artist();
if !allow_missing {
if title.is_none() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if artist.is_none() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
if !allow_missing {
if title.is_none() {
return Err(Box::new(AudioFormatError::MissingTitle));
}
if artist.is_none() {
return Err(Box::new(AudioFormatError::MissingArtist));
}
}
Ok(Tags {
title: String::from(title.unwrap()),
artist: String::from(artist.unwrap()),
})
}
Ok(Tags {
title: String::from(title.unwrap()),
artist: String::from(artist.unwrap()),
})
}
fn contains_replaygain_tags(&self) -> bool {
let frames = self.id3_tags.frames();
fn contains_replaygain_tags(&self) -> bool {
let frames = self.id3_tags.frames();
let mut contains_replaygain_tags = false;
let mut contains_replaygain_tags = false;
for frame in frames {
if frame.id() == "TXXX" {
if let Some(extended_text) = frame.content().extended_text() {
match extended_text.description.as_str() {
"REPLAYGAIN_TRACK_GAIN" => {
contains_replaygain_tags = true;
}
"REPLAYGAIN_TRACK_PEAK" => {
contains_replaygain_tags = true;
}
_ => {}
}
}
}
}
for frame in frames {
if frame.id() == "TXXX" {
if let Some(extended_text) = frame.content().extended_text() {
match extended_text.description.as_str() {
"REPLAYGAIN_TRACK_GAIN" => {
contains_replaygain_tags = true;
}
"REPLAYGAIN_TRACK_PEAK" => {
contains_replaygain_tags = true;
}
_ => {}
}
}
}
}
contains_replaygain_tags
}
contains_replaygain_tags
}
fn supports_replaygain(&self) -> bool {
true
}
fn supports_replaygain(&self) -> bool {
true
}
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.id3_tags.set_title(title);
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
self.id3_tags.set_title(title);
Ok(())
}
Ok(())
}
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.id3_tags.set_artist(artist);
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
self.id3_tags.set_artist(artist);
Ok(())
}
Ok(())
}
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
let frames = self.id3_tags.remove("TXXX");
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
let frames = self.id3_tags.remove("TXXX");
for frame in frames {
if let Some(extended_text) = frame.content().extended_text() {
if extended_text.description.starts_with("REPLAYGAIN") {
continue;
}
}
self.id3_tags.add_frame(frame);
}
for frame in frames {
if let Some(extended_text) = frame.content().extended_text() {
if extended_text.description.starts_with("REPLAYGAIN") {
continue;
}
}
self.id3_tags.add_frame(frame);
}
self.id3_tags.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),
}),
));
self.id3_tags.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),
}),
));
self.id3_tags.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),
}),
));
self.id3_tags.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),
}),
));
Ok(())
}
Ok(())
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
self.id3_tags
.write_to_path(self.path.as_path(), id3::Version::Id3v24)?;
Ok(())
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
self.id3_tags
.write_to_path(self.path.as_path(), id3::Version::Id3v24)?;
Ok(())
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
supports_replaygain: self.supports_replaygain(),
format: Some(FileFormat::MP3),
contains_replaygain: self.contains_replaygain_tags(),
})
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
supports_replaygain: self.supports_replaygain(),
format: Some(FileFormat::MP3),
contains_replaygain: self.contains_replaygain_tags(),
})
}
}
pub fn new_handler(path: &PathBuf) -> Result<ID3AudioFormat, BoxedError> {
let id3_tags = id3::Tag::read_from_path(path)?;
let id3_tags = id3::Tag::read_from_path(path)?;
Ok(ID3AudioFormat {
id3_tags,
path: Box::new(path.clone()),
})
Ok(ID3AudioFormat {
id3_tags,
path: Box::new(path.clone()),
})
}

View file

@ -6,136 +6,134 @@ use lazy_static::lazy_static;
use super::{BoxedError, FormatHandler};
#[cfg(feature = "flac_extractor")]
mod flac;
#[cfg(feature = "ffprobe_extractor")]
mod ffprobe;
#[cfg(feature = "taglib_extractor")]
mod taglib;
#[cfg(feature = "flac_extractor")]
mod flac;
#[cfg(feature = "mp3_extractor")]
mod id3;
#[cfg(feature = "taglib_extractor")]
mod taglib;
type NewHandlerFuncReturn = Result<Box<dyn FormatHandler>, BoxedError>;
type NewHandlerFunc = fn(path: &PathBuf, file_format: Option<FileFormat>) -> NewHandlerFuncReturn;
pub struct Handler {
pub supported_extensions: Vec<String>,
pub supported_formats: Vec<FileFormat>,
pub new: NewHandlerFunc
pub supported_extensions: Vec<String>,
pub supported_formats: Vec<FileFormat>,
pub new: NewHandlerFunc,
}
lazy_static! {
pub static ref SUPPORTED_EXTENSIONS: Vec<String> = {
let mut extensions: Vec<String> = Vec::new();
for handler in HANDLERS.iter() {
for extension in handler.supported_extensions.iter() {
if !extensions.contains(extension) {
extensions.push(extension.clone());
}
}
}
pub static ref SUPPORTED_EXTENSIONS: Vec<String> = {
let mut extensions: Vec<String> = Vec::new();
for handler in HANDLERS.iter() {
for extension in handler.supported_extensions.iter() {
if !extensions.contains(extension) {
extensions.push(extension.clone());
}
}
}
extensions
};
extensions
};
pub static ref SUPPORTED_FORMATS: Vec<FileFormat> = {
let mut formats: Vec<FileFormat> = Vec::new();
for handler in HANDLERS.iter() {
for format in handler.supported_formats.iter() {
if !formats.contains(format) {
formats.push(*format);
}
}
}
pub static ref SUPPORTED_FORMATS: Vec<FileFormat> = {
let mut formats: Vec<FileFormat> = Vec::new();
for handler in HANDLERS.iter() {
for format in handler.supported_formats.iter() {
if !formats.contains(format) {
formats.push(*format);
}
}
}
formats
};
pub static ref HANDLERS: Vec<Handler> = {
let mut handlers: Vec<Handler> = Vec::new();
formats
};
#[cfg(feature = "mp3_extractor")]
handlers.push(Handler {
supported_extensions: vec!["mp3".to_string()],
supported_formats: vec![FileFormat::MP3],
new: |path, _file_format| -> NewHandlerFuncReturn {
let handler = id3::new_handler(path)?;
pub static ref HANDLERS: Vec<Handler> = {
let mut handlers: Vec<Handler> = Vec::new();
Ok(Box::from(handler))
},
});
#[cfg(feature = "mp3_extractor")]
handlers.push(Handler {
supported_extensions: vec!["mp3".to_string()],
supported_formats: vec![FileFormat::MP3],
new: |path, _file_format| -> NewHandlerFuncReturn {
let handler = id3::new_handler(path)?;
#[cfg(feature = "flac_extractor")]
handlers.push(Handler {
supported_extensions: vec!["flac".to_string()],
supported_formats: vec![FileFormat::FLAC],
new: |path, _file_format| -> NewHandlerFuncReturn {
let handler = flac::new_handler(path)?;
Ok(Box::from(handler))
},
});
Ok(Box::from(handler))
},
});
#[cfg(feature = "flac_extractor")]
handlers.push(Handler {
supported_extensions: vec!["flac".to_string()],
supported_formats: vec![FileFormat::FLAC],
new: |path, _file_format| -> NewHandlerFuncReturn {
let handler = flac::new_handler(path)?;
#[cfg(feature = "taglib_extractor")]
handlers.push(Handler {
supported_extensions: vec![
"mp3".to_string(),
"flac".to_string(),
"ogg".to_string(),
"opus".to_string(),
"wav".to_string(),
"wv".to_string(),
"aiff".to_string(),
],
supported_formats: vec![
FileFormat::MP3,
FileFormat::FLAC,
FileFormat::OggVorbis,
FileFormat::OggOpus,
FileFormat::OggFLAC,
FileFormat::OggSpeex,
FileFormat::OggTheora,
FileFormat::Wav,
FileFormat::WavPack,
FileFormat::AIFF,
],
new: |path, file_format| -> NewHandlerFuncReturn {
let handler = taglib::new_handler(path, file_format)?;
Ok(Box::from(handler))
},
});
Ok(Box::from(handler))
},
});
#[cfg(feature = "taglib_extractor")]
handlers.push(Handler {
supported_extensions: vec![
"mp3".to_string(),
"flac".to_string(),
"ogg".to_string(),
"opus".to_string(),
"wav".to_string(),
"wv".to_string(),
"aiff".to_string(),
],
supported_formats: vec![
FileFormat::MP3,
FileFormat::FLAC,
FileFormat::OggVorbis,
FileFormat::OggOpus,
FileFormat::OggFLAC,
FileFormat::OggSpeex,
FileFormat::OggTheora,
FileFormat::Wav,
FileFormat::WavPack,
FileFormat::AIFF,
],
new: |path, file_format| -> NewHandlerFuncReturn {
let handler = taglib::new_handler(path, file_format)?;
#[cfg(feature = "ffprobe_extractor")]
handlers.push(Handler {
supported_extensions: vec![
"mp3".to_string(),
"flac".to_string(),
"ogg".to_string(),
"opus".to_string(),
"wav".to_string(),
"wv".to_string(),
"aiff".to_string(),
],
supported_formats: vec![
FileFormat::MP3,
FileFormat::FLAC,
FileFormat::OggVorbis,
FileFormat::OggOpus,
FileFormat::OggFLAC,
FileFormat::OggSpeex,
FileFormat::OggTheora,
FileFormat::Wav,
FileFormat::WavPack,
FileFormat::AIFF,
],
new: |path, file_format| -> NewHandlerFuncReturn {
let handler = ffprobe::new_handler(path, file_format)?;
Ok(Box::from(handler))
},
});
Ok(Box::from(handler))
},
});
#[cfg(feature = "ffprobe_extractor")]
handlers.push(Handler {
supported_extensions: vec![
"mp3".to_string(),
"flac".to_string(),
"ogg".to_string(),
"opus".to_string(),
"wav".to_string(),
"wv".to_string(),
"aiff".to_string(),
],
supported_formats: vec![
FileFormat::MP3,
FileFormat::FLAC,
FileFormat::OggVorbis,
FileFormat::OggOpus,
FileFormat::OggFLAC,
FileFormat::OggSpeex,
FileFormat::OggTheora,
FileFormat::Wav,
FileFormat::WavPack,
FileFormat::AIFF,
],
new: |path, file_format| -> NewHandlerFuncReturn {
let handler = ffprobe::new_handler(path, file_format)?;
Ok(Box::from(handler))
},
});
handlers
};
handlers
};
}

View file

@ -1,165 +1,163 @@
use std::path::Path;
use taglib::{
new_taglib_file,
traits::{File, Tag},
TagLibFileType,
new_taglib_file,
traits::{File, Tag},
TagLibFileType,
};
use crate::{
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{FormatHandler, AudioFormatError, BoxedError}
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
};
pub struct TaglibAudioFormat {
file: taglib::TagLibFile,
file_format: Option<FileFormat>,
file: taglib::TagLibFile,
file_format: Option<FileFormat>,
}
impl FormatHandler for TaglibAudioFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let tags = self.file.tag()?;
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let tags = self.file.tag()?;
let mut title = tags.title();
if title.is_none() {
if !allow_missing {
return Err(Box::new(AudioFormatError::MissingTitle));
} else {
title = Some("".to_string())
}
}
let mut title = tags.title();
if title.is_none() {
if !allow_missing {
return Err(Box::new(AudioFormatError::MissingTitle));
} else {
title = Some("".to_string())
}
}
let mut artist = tags.artist();
if artist.is_none() {
if !allow_missing {
return Err(Box::new(AudioFormatError::MissingArtist));
} else {
artist = Some("".to_string())
}
}
let mut artist = tags.artist();
if artist.is_none() {
if !allow_missing {
return Err(Box::new(AudioFormatError::MissingArtist));
} else {
artist = Some("".to_string())
}
}
Ok(Tags {
title: title.unwrap(),
artist: artist.unwrap(),
})
}
Ok(Tags {
title: title.unwrap(),
artist: artist.unwrap(),
})
}
fn contains_replaygain_tags(&self) -> bool {
if let Some(format) = self.file_format {
if format == FileFormat::OggOpus {
let oggtag = self.file.oggtag().expect("oggtag not available?");
fn contains_replaygain_tags(&self) -> bool {
if let Some(format) = self.file_format {
if format == FileFormat::OggOpus {
let oggtag = self.file.oggtag().expect("oggtag not available?");
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
}
match format {
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
let oggtag = self.file.oggtag().expect("oggtag not available?");
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
return gain.is_some() && peak.is_some();
}
_ => {}
}
}
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
}
match format {
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
let oggtag = self.file.oggtag().expect("oggtag not available?");
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
return gain.is_some() && peak.is_some();
}
_ => {}
}
}
false
}
false
}
fn supports_replaygain(&self) -> bool {
if let Some(format) = self.file_format {
return matches!(
format,
// All Ogg formats support ReplayGain
FileFormat::OggVorbis
| FileFormat::OggOpus
| FileFormat::OggFLAC
| FileFormat::OggSpeex
// Both "support" ReplayGain but not implemented yet
// FileFormat::Wav |
// FileFormat::WavPack
);
}
fn supports_replaygain(&self) -> bool {
if let Some(format) = self.file_format {
return matches!(
format,
// All Ogg formats support ReplayGain
FileFormat::OggVorbis
| FileFormat::OggOpus
| FileFormat::OggFLAC
| FileFormat::OggSpeex // Both "support" ReplayGain but not implemented yet
// FileFormat::Wav |
// FileFormat::WavPack
);
}
false
}
false
}
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?;
tags.set_title(title);
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?;
tags.set_title(title);
Ok(())
}
Ok(())
}
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?;
tags.set_artist(artist);
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?;
tags.set_artist(artist);
Ok(())
}
Ok(())
}
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
if let Some(format) = self.file_format {
let oggtag = self.file.oggtag().expect("oggtag not available?");
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
if let Some(format) = self.file_format {
let oggtag = self.file.oggtag().expect("oggtag not available?");
if format == FileFormat::OggOpus {
let data = data.to_normal(true);
if format == FileFormat::OggOpus {
let data = data.to_normal(true);
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
} else if matches!(
format,
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
) {
let data = data.to_normal(false);
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
} else if matches!(
format,
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
) {
let data = data.to_normal(false);
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain);
oggtag.add_field("REPLAYGAIN_TRACK_PEAK".to_string(), data.track_peak);
}
}
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain);
oggtag.add_field("REPLAYGAIN_TRACK_PEAK".to_string(), data.track_peak);
}
}
Ok(())
}
Ok(())
}
fn save_changes(&mut self) -> Result<(), BoxedError> {
let res = self.file.save();
fn save_changes(&mut self) -> Result<(), BoxedError> {
let res = self.file.save();
match res {
Ok(()) => Ok(()),
Err(e) => Err(Box::new(e)),
}
}
match res {
Ok(()) => Ok(()),
Err(e) => Err(Box::new(e)),
}
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: self.file_format,
})
}
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(),
format: self.file_format,
})
}
}
pub fn new_handler(
path: &Path,
file_format: Option<FileFormat>,
path: &Path,
file_format: Option<FileFormat>,
) -> Result<TaglibAudioFormat, BoxedError> {
let mut taglib_format: Option<TagLibFileType> = None;
if let Some(format) = file_format {
taglib_format = match format {
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
_ => None,
}
}
let mut taglib_format: Option<TagLibFileType> = None;
if let Some(format) = file_format {
taglib_format = match format {
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
_ => None,
}
}
Ok(TaglibAudioFormat {
file_format,
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_format)?,
})
Ok(TaglibAudioFormat {
file_format,
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_format)?,
})
}

View file

@ -7,80 +7,79 @@ use thiserror::Error;
use crate::types::{AudioFileInfo, File, ReplayGainRawData, Tags};
use super::format_detection::detect_format;
type BoxedError = Box<dyn Error>;
pub trait FormatHandler {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
fn contains_replaygain_tags(&self) -> bool;
fn supports_replaygain(&self) -> bool;
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
fn contains_replaygain_tags(&self) -> bool;
fn supports_replaygain(&self) -> bool;
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError>;
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError>;
fn save_changes(&mut self) -> Result<(), BoxedError>;
fn save_changes(&mut self) -> Result<(), BoxedError>;
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError>;
fn get_audio_file_info(
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError>;
}
#[derive(Error, Debug)]
pub enum AudioFormatError {
#[error("title missing from tags")]
MissingTitle,
#[error("artist missing from tags")]
MissingArtist,
#[error("title missing from tags")]
MissingTitle,
#[error("artist missing from tags")]
MissingArtist,
}
pub fn get_format_handler(file: &File) -> Result<Box<dyn FormatHandler>, BoxedError> {
let format = detect_format(&file.join_path_to())?;
let path = file.join_path_to();
let format = detect_format(&file.join_path_to())?;
let path = file.join_path_to();
for handler in handlers::HANDLERS.iter() {
if !handler.supported_extensions.contains(&file.extension) {
continue
}
for handler in handlers::HANDLERS.iter() {
if !handler.supported_extensions.contains(&file.extension) {
continue;
}
if !handler.supported_formats.contains(&format) {
continue
}
if !handler.supported_formats.contains(&format) {
continue;
}
let new = handler.new;
let new = handler.new;
return new(&path, Some(format));
}
return new(&path, Some(format));
}
panic!("no supported handler found");
panic!("no supported handler found");
}
fn is_supported_extension(ext: &str) -> bool {
handlers::SUPPORTED_EXTENSIONS.contains(&ext.to_string())
handlers::SUPPORTED_EXTENSIONS.contains(&ext.to_string())
}
pub fn is_supported_file(file_path: &Path) -> bool {
let ext = file_path.extension();
let ext = file_path.extension();
if ext.is_none() {
return false;
}
if ext.is_none() {
return false;
}
let ext = ext.unwrap().to_str().unwrap();
let ext = ext.unwrap().to_str().unwrap();
if !is_supported_extension(ext) {
return false;
}
if !is_supported_extension(ext) {
return false;
}
let format = detect_format(file_path);
if format.is_err() {
return false;
}
let format = detect_format(file_path);
if format.is_err() {
return false;
}
let format = format.unwrap();
let format = format.unwrap();
handlers::SUPPORTED_FORMATS.contains(&format)
handlers::SUPPORTED_FORMATS.contains(&format)
}

View file

@ -1,11 +1,11 @@
pub mod ascii_reduce;
pub mod ffprobe;
pub mod format_detection;
pub mod replaygain;
pub mod transcoder;
pub mod ffprobe;
pub mod formats;
mod music_scanner;
mod music_scanner;
pub use formats::is_supported_file;
pub use music_scanner::scan_for_music;

View file

@ -6,52 +6,52 @@ use walkdir::WalkDir;
use super::is_supported_file;
pub fn find_extra_files(
src_dir: String,
file: &File,
src_dir: String,
file: &File,
) -> Result<Vec<File>, Box<dyn std::error::Error>> {
let mut extra_files: Vec<File> = Vec::new();
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();
for entry in fs::read_dir(&file.path_to)? {
let entry = entry?;
if !entry.metadata()?.is_file() {
continue;
}
let entry_path = entry.path();
let extension = entry_path.extension();
if extension.is_none() {
continue;
}
let extension = entry_path.extension();
if extension.is_none() {
continue;
}
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
&& extension.unwrap().to_string_lossy() != file.extension
{
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone()));
}
}
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
&& extension.unwrap().to_string_lossy() != file.extension
{
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone()));
}
}
Ok(extra_files)
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();
let mut files: Vec<File> = Vec::new();
for entry in WalkDir::new(src_dir) {
let entry = entry.unwrap();
let entry_path = entry.into_path();
if entry_path.is_dir() {
continue;
}
for entry in WalkDir::new(src_dir) {
let entry = entry.unwrap();
let entry_path = entry.into_path();
if entry_path.is_dir() {
continue;
}
if is_supported_file(&entry_path) {
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
if is_supported_file(&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)?);
file.extra_files
.extend(find_extra_files(src_dir.clone(), &file)?);
files.push(file);
}
}
files.push(file);
}
}
Ok(files)
Ok(files)
}

View file

@ -1,7 +1,7 @@
use std::{
io::{BufRead, BufReader},
path::PathBuf,
process::Command,
io::{BufRead, BufReader},
path::PathBuf,
process::Command,
};
use string_error::static_err;
@ -9,94 +9,94 @@ use string_error::static_err;
use crate::types::ReplayGainRawData;
pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainRawData, 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()?;
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"));
}
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();
// info we need is in stdout
let output_str = String::from_utf8(output.stderr).unwrap();
let mut ebur128_summary = String::new();
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;
// 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 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 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 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>()?;
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;
// 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;
}
track_gain = gain;
}
if line.starts_with("Peak:") {
let mut l = line.split(':');
l.next();
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(ReplayGainRawData {
track_gain,
track_peak,
})
// 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(ReplayGainRawData {
track_gain,
track_peak,
})
}

View file

@ -3,5 +3,5 @@ pub mod progress_monitor;
#[allow(clippy::all)]
mod transcoder;
pub mod types;
use self::progress_monitor::progress_monitor;
use self::progress_monitor::progress_monitor;
pub use self::transcoder::transcode;

View file

@ -6,212 +6,212 @@ use crate::utils::transcoder::types::PresetCategory;
use crate::utils::transcoder::types::TranscodeConfig;
lazy_static! {
#[derive(Debug)]
pub static ref TRANSCODE_CONFIGS: Vec<PresetCategory> = {
let mut preset_categories: Vec<PresetCategory> = Vec::new();
#[derive(Debug)]
pub static ref TRANSCODE_CONFIGS: Vec<PresetCategory> = {
let mut preset_categories: Vec<PresetCategory> = Vec::new();
add_mp3_presets(&mut preset_categories);
add_opus_presets(&mut preset_categories);
add_vorbis_presets(&mut preset_categories);
add_g726_presets(&mut preset_categories);
add_speex_presets(&mut preset_categories);
add_flac_preset(&mut preset_categories);
add_wav_preset(&mut preset_categories);
add_mp3_presets(&mut preset_categories);
add_opus_presets(&mut preset_categories);
add_vorbis_presets(&mut preset_categories);
add_g726_presets(&mut preset_categories);
add_speex_presets(&mut preset_categories);
add_flac_preset(&mut preset_categories);
add_wav_preset(&mut preset_categories);
preset_categories
};
preset_categories
};
}
fn add_mp3_presets(preset_categories: &mut Vec<PresetCategory>) {
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [
8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
] {
presets.push(Preset {
name: format!("mp3-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("mp3".to_string()),
encoder: Some("libmp3lame".to_string()),
container: Some("mp3".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [
8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
] {
presets.push(Preset {
name: format!("mp3-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("mp3".to_string()),
encoder: Some("libmp3lame".to_string()),
container: Some("mp3".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
presets.push(Preset {
name: format!("mp3-v{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("mp3".to_string()),
encoder: Some("libmp3lame".to_string()),
container: Some("mp3".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
presets.push(Preset {
name: format!("mp3-v{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("mp3".to_string()),
encoder: Some("libmp3lame".to_string()),
container: Some("mp3".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
preset_categories.push(PresetCategory {
name: "mp3".to_string(),
presets,
});
preset_categories.push(PresetCategory {
name: "mp3".to_string(),
presets,
});
}
fn add_opus_presets(preset_categories: &mut Vec<PresetCategory>) {
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
presets.push(Preset {
name: format!("opus-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("opus".to_string()),
encoder: Some("libopus".to_string()),
container: Some("ogg".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
presets.push(Preset {
name: format!("opus-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("opus".to_string()),
encoder: Some("libopus".to_string()),
container: Some("ogg".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
preset_categories.push(PresetCategory {
name: "opus".to_string(),
presets,
});
preset_categories.push(PresetCategory {
name: "opus".to_string(),
presets,
});
}
fn add_vorbis_presets(preset_categories: &mut Vec<PresetCategory>) {
let mut presets: Vec<Preset> = Vec::new();
for quality in [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
presets.push(Preset {
name: format!("vorbis-v{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("ogg".to_string()),
encoder: Some("libvorbis".to_string()),
container: Some("ogg".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
let mut presets: Vec<Preset> = Vec::new();
for quality in [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
presets.push(Preset {
name: format!("vorbis-v{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("ogg".to_string()),
encoder: Some("libvorbis".to_string()),
container: Some("ogg".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
preset_categories.push(PresetCategory {
name: "vorbis".to_string(),
presets,
});
preset_categories.push(PresetCategory {
name: "vorbis".to_string(),
presets,
});
}
fn add_g726_presets(preset_categories: &mut Vec<PresetCategory>) {
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
presets.push(Preset {
name: format!("g726-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("mka".to_string()),
encoder: Some("g726".to_string()),
container: Some("matroska".to_string()),
sample_rate: Some("8000".to_string()),
channels: Some("1".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
let mut presets: Vec<Preset> = Vec::new();
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
presets.push(Preset {
name: format!("g726-{}k", bitrate).to_string(),
config: TranscodeConfig {
file_extension: Some("mka".to_string()),
encoder: Some("g726".to_string()),
container: Some("matroska".to_string()),
sample_rate: Some("8000".to_string()),
channels: Some("1".to_string()),
bitrate: Some(format!("{}k", bitrate).to_string()),
..TranscodeConfig::default()
},
})
}
preset_categories.push(PresetCategory {
name: "g726".to_string(),
presets,
});
preset_categories.push(PresetCategory {
name: "g726".to_string(),
presets,
});
}
fn add_speex_presets(preset_categories: &mut Vec<PresetCategory>) {
let mut presets: Vec<Preset> = Vec::new();
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
presets.push(Preset {
name: format!("speex-q{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("ogg".to_string()),
encoder: Some("libspeex".to_string()),
container: Some("ogg".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
let mut presets: Vec<Preset> = Vec::new();
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
presets.push(Preset {
name: format!("speex-q{}", quality).to_string(),
config: TranscodeConfig {
file_extension: Some("ogg".to_string()),
encoder: Some("libspeex".to_string()),
container: Some("ogg".to_string()),
quality: Some(format!("{}", quality).to_string()),
..TranscodeConfig::default()
},
})
}
preset_categories.push(PresetCategory {
name: "speex".to_string(),
presets,
});
preset_categories.push(PresetCategory {
name: "speex".to_string(),
presets,
});
}
fn add_flac_preset(preset_categories: &mut Vec<PresetCategory>) {
preset_categories.push(PresetCategory {
name: "flac".to_string(),
presets: Vec::from([Preset {
name: "flac".to_string(),
config: TranscodeConfig {
encoder: Some("flac".to_string()),
container: Some("flac".to_string()),
file_extension: Some("flac".to_string()),
..TranscodeConfig::default()
},
}]),
})
preset_categories.push(PresetCategory {
name: "flac".to_string(),
presets: Vec::from([Preset {
name: "flac".to_string(),
config: TranscodeConfig {
encoder: Some("flac".to_string()),
container: Some("flac".to_string()),
file_extension: Some("flac".to_string()),
..TranscodeConfig::default()
},
}]),
})
}
fn add_wav_preset(preset_categories: &mut Vec<PresetCategory>) {
preset_categories.push(PresetCategory {
name: "wav".to_string(),
presets: Vec::from([Preset {
name: "wav".to_string(),
config: TranscodeConfig {
container: Some("wav".to_string()),
file_extension: Some("wav".to_string()),
..TranscodeConfig::default()
},
}]),
})
preset_categories.push(PresetCategory {
name: "wav".to_string(),
presets: Vec::from([Preset {
name: "wav".to_string(),
config: TranscodeConfig {
container: Some("wav".to_string()),
file_extension: Some("wav".to_string()),
..TranscodeConfig::default()
},
}]),
})
}
pub fn print_presets() {
for category in TRANSCODE_CONFIGS.iter() {
println!("Category {}:", category.name);
for preset in category.presets.iter() {
println!("- {}", preset.name)
}
}
for category in TRANSCODE_CONFIGS.iter() {
println!("Category {}:", category.name);
for preset in category.presets.iter() {
println!("- {}", preset.name)
}
}
}
pub fn get_preset(name: String) -> Option<TranscodeConfig> {
for category in TRANSCODE_CONFIGS.iter() {
for preset in category.presets.iter() {
if preset.name == name {
return Some(preset.config.clone());
}
}
}
for category in TRANSCODE_CONFIGS.iter() {
for preset in category.presets.iter() {
if preset.name == name {
return Some(preset.config.clone());
}
}
}
None
None
}
pub fn transcode_preset_or_config(
preset_name: Option<&String>,
config_path: Option<&String>,
preset_name: Option<&String>,
config_path: Option<&String>,
) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
if let Some(preset_name) = preset_name {
let preset_config = get_preset(preset_name.to_string());
if let Some(preset_name) = preset_name {
let preset_config = get_preset(preset_name.to_string());
match preset_config {
Some(config) => Ok(config),
None => Err(into_err("invalid preset name".to_string())),
}
} else if let Some(config_path) = config_path {
let config = TranscodeConfig::load(config_path.to_string())?;
match preset_config {
Some(config) => Ok(config),
None => Err(into_err("invalid preset name".to_string())),
}
} else if let Some(config_path) = config_path {
let config = TranscodeConfig::load(config_path.to_string())?;
Ok(config)
} else {
Err(into_err(
"please provide a transcode config or preset".to_string(),
))
}
Ok(config)
} else {
Err(into_err(
"please provide a transcode config or preset".to_string(),
))
}
}

View file

@ -1,11 +1,11 @@
use std::{
fs,
io::{BufRead, BufReader, Seek, SeekFrom},
path::PathBuf,
process::Command,
sync::mpsc::{self, Sender},
thread::{self, JoinHandle},
time::Duration,
fs,
io::{BufRead, BufReader, Seek, SeekFrom},
path::PathBuf,
process::Command,
sync::mpsc::{self, Sender},
thread::{self, JoinHandle},
time::Duration,
};
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
@ -14,127 +14,127 @@ use string_error::static_err;
#[derive(Debug, Clone, Deserialize)]
struct FFProbeOutput {
pub format: FFProbeFormat,
pub format: FFProbeFormat,
}
#[derive(Debug, Clone, Deserialize)]
struct FFProbeFormat {
pub duration: String,
pub duration: String,
}
fn get_file_length_milliseconds(
source_filepath: PathBuf,
source_filepath: PathBuf,
) -> Result<u64, Box<dyn std::error::Error>> {
let output = Command::new(crate::meta::FFPROBE)
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
&source_filepath.to_string_lossy(),
])
.output()?;
let output = Command::new(crate::meta::FFPROBE)
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
&source_filepath.to_string_lossy(),
])
.output()?;
if !output.status.success() {
print!("{:?}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFprobe Crashed"));
}
if !output.status.success() {
print!("{:?}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFprobe Crashed"));
}
let output_str = String::from_utf8(output.stdout).unwrap();
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?;
let output_str = String::from_utf8(output.stdout).unwrap();
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?;
let duration_seconds = ffprobe_out.format.duration.parse::<f32>()?;
let duration_seconds = ffprobe_out.format.duration.parse::<f32>()?;
Ok((duration_seconds * 1000.0).round() as u64)
Ok((duration_seconds * 1000.0).round() as u64)
}
fn ffprobe_duration_to_ms(duration: String) -> Result<u64, Box<dyn std::error::Error>> {
let fields: Vec<&str> = duration.split(':').collect();
let mut duration = Duration::from_nanos(0);
let fields: Vec<&str> = duration.split(':').collect();
let mut duration = Duration::from_nanos(0);
let hours = fields[0].parse::<u64>()?;
duration += Duration::from_secs(hours * 60 * 60);
let hours = fields[0].parse::<u64>()?;
duration += Duration::from_secs(hours * 60 * 60);
let minutes = fields[1].parse::<u64>()?;
duration += Duration::from_secs(minutes * 60);
let minutes = fields[1].parse::<u64>()?;
duration += Duration::from_secs(minutes * 60);
let seconds = fields[1].parse::<f64>()?;
duration += Duration::from_millis((seconds * 1000.0) as u64);
let seconds = fields[1].parse::<f64>()?;
duration += Duration::from_millis((seconds * 1000.0) as u64);
Ok(duration.as_millis() as u64)
Ok(duration.as_millis() as u64)
}
pub fn progress_monitor(
source_filepath: PathBuf,
sender_base: &Sender<String>,
source_filepath: PathBuf,
sender_base: &Sender<String>,
) -> Result<(String, JoinHandle<()>), Box<dyn std::error::Error>> {
let total_length_millis = get_file_length_milliseconds(source_filepath)?;
let total_length_millis = get_file_length_milliseconds(source_filepath)?;
let tempdir = tempfile::tempdir()?;
let file_path = tempdir.path().join("progress.log");
let file_path_string = file_path.to_str().unwrap().to_string();
fs::File::create(&file_path)?;
let tempdir = tempfile::tempdir()?;
let file_path = tempdir.path().join("progress.log");
let file_path_string = file_path.to_str().unwrap().to_string();
fs::File::create(&file_path)?;
let sender = sender_base.clone();
let child = thread::spawn(move || {
let _ = &tempdir;
let sender = sender_base.clone();
let child = thread::spawn(move || {
let _ = &tempdir;
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
tx,
notify::Config::default().with_poll_interval(Duration::from_millis(100)),
)
.expect("could not watch for ffmpeg log progress status");
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
tx,
notify::Config::default().with_poll_interval(Duration::from_millis(100)),
)
.expect("could not watch for ffmpeg log progress status");
watcher
.watch(&file_path, RecursiveMode::NonRecursive)
.unwrap();
watcher
.watch(&file_path, RecursiveMode::NonRecursive)
.unwrap();
let mut pos = 0;
let mut pos = 0;
'outer: for res in rx {
if res.is_err() {
break 'outer;
}
'outer: for res in rx {
if res.is_err() {
break 'outer;
}
let res = res.unwrap();
let res = res.unwrap();
match res.kind {
EventKind::Modify(_) => {
let mut file = fs::File::open(&file_path).unwrap();
file.seek(SeekFrom::Start(pos)).unwrap();
match res.kind {
EventKind::Modify(_) => {
let mut file = fs::File::open(&file_path).unwrap();
file.seek(SeekFrom::Start(pos)).unwrap();
pos = file.metadata().unwrap().len();
pos = file.metadata().unwrap().len();
let reader = BufReader::new(file);
for line in reader.lines() {
let ln = line.unwrap();
let reader = BufReader::new(file);
for line in reader.lines() {
let ln = line.unwrap();
if ln == "progress=end" {
break 'outer;
}
if ln == "progress=end" {
break 'outer;
}
if ln.starts_with("out_time=") {
let out_time = ln.strip_prefix("out_time=").unwrap().to_string();
let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap();
if sender
.send(format!(
"{:.2}%",
((out_time_ms as f64 / total_length_millis as f64) * 100.0)
))
.is_err()
{
break 'outer;
};
}
}
}
EventKind::Remove(_) => break 'outer,
_ => {}
}
}
});
if ln.starts_with("out_time=") {
let out_time = ln.strip_prefix("out_time=").unwrap().to_string();
let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap();
if sender
.send(format!(
"{:.2}%",
((out_time_ms as f64 / total_length_millis as f64) * 100.0)
))
.is_err()
{
break 'outer;
};
}
}
}
EventKind::Remove(_) => break 'outer,
_ => {}
}
}
});
Ok((file_path_string, child))
Ok((file_path_string, child))
}

View file

@ -6,80 +6,80 @@ use string_error::static_err;
use super::{progress_monitor, types::TranscodeConfig};
pub fn transcode(
file: File,
dest: String,
config: &TranscodeConfig,
progress_sender: Option<Sender<String>>,
file: File,
dest: String,
config: &TranscodeConfig,
progress_sender: Option<Sender<String>>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut command_args: Vec<String> = Vec::new();
command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]);
let mut command_args: Vec<String> = Vec::new();
command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]);
command_args.extend(vec![
"-i".to_string(),
file.join_path_to().to_string_lossy().to_string(),
]);
command_args.extend(vec![
"-i".to_string(),
file.join_path_to().to_string_lossy().to_string(),
]);
if let Some(encoder) = &config.encoder {
command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
}
if let Some(encoder) = &config.encoder {
command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
}
if let Some(container) = &config.container {
command_args.extend(vec!["-f".to_string(), container.to_string()]);
}
if let Some(container) = &config.container {
command_args.extend(vec!["-f".to_string(), container.to_string()]);
}
if let Some(sample_rate) = &config.sample_rate {
command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
}
if let Some(sample_rate) = &config.sample_rate {
command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
}
if let Some(channels) = &config.channels {
command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
}
if let Some(channels) = &config.channels {
command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
}
if let Some(quality) = &config.quality {
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
}
if let Some(quality) = &config.quality {
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
}
if let Some(bitrate) = &config.bitrate {
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]);
}
if let Some(bitrate) = &config.bitrate {
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]);
}
command_args.push(dest);
command_args.push(dest);
let mut progress_thread: Option<JoinHandle<()>> = None;
let mut progress_file: Option<String> = None;
let mut progress_thread: Option<JoinHandle<()>> = None;
let mut progress_file: Option<String> = None;
if let Some(sender) = &progress_sender {
let result = progress_monitor(file.join_path_to(), sender);
if let Some(sender) = &progress_sender {
let result = progress_monitor(file.join_path_to(), sender);
if let Ok(result) = result {
progress_thread = Some(result.1);
progress_file = Some(result.0.clone());
command_args.extend(vec![
"-progress".to_string(),
result.0,
"-nostats".to_string(),
]);
}
}
if let Ok(result) = result {
progress_thread = Some(result.1);
progress_file = Some(result.0.clone());
command_args.extend(vec![
"-progress".to_string(),
result.0,
"-nostats".to_string(),
]);
}
}
let output = Command::new(crate::meta::FFMPEG)
.args(command_args)
.output()
.expect("failed to execute process");
let output = Command::new(crate::meta::FFMPEG)
.args(command_args)
.output()
.expect("failed to execute process");
if let Some(sender) = progress_sender {
drop(sender);
}
if let Some(sender) = progress_sender {
drop(sender);
}
if let Some(thread) = progress_thread {
fs::remove_file(progress_file.unwrap())?;
thread.join().expect("thread couldn't join");
}
if let Some(thread) = progress_thread {
fs::remove_file(progress_file.unwrap())?;
thread.join().expect("thread couldn't join");
}
if !output.status.success() {
print!("{}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFmpeg Crashed"));
}
if !output.status.success() {
print!("{}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFmpeg Crashed"));
}
Ok(())
Ok(())
}

View file

@ -5,43 +5,43 @@ use string_error::static_err;
#[derive(Debug, Serialize, Deserialize)]
pub struct Preset {
pub name: String,
pub config: TranscodeConfig,
pub name: String,
pub config: TranscodeConfig,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PresetCategory {
pub name: String,
pub presets: Vec<Preset>,
pub name: String,
pub presets: Vec<Preset>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TranscodeConfig {
pub encoder: Option<String>,
pub file_extension: Option<String>,
pub container: Option<String>,
pub bitrate: Option<String>,
pub quality: Option<String>,
pub sample_rate: Option<String>,
pub channels: Option<String>,
pub encoder: Option<String>,
pub file_extension: Option<String>,
pub container: Option<String>,
pub bitrate: Option<String>,
pub quality: Option<String>,
pub sample_rate: Option<String>,
pub channels: Option<String>,
}
impl TranscodeConfig {
pub fn load(path: String) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
let path_buf = PathBuf::from(&path);
let extension = path_buf.extension().unwrap();
let file = File::open(path)?;
let reader = BufReader::new(file);
pub fn load(path: String) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
let path_buf = PathBuf::from(&path);
let extension = path_buf.extension().unwrap();
let file = File::open(path)?;
let reader = BufReader::new(file);
if extension == "yml" || extension == "yaml" {
let u: TranscodeConfig =
serde_yaml::from_reader(reader).expect("error while reading yaml");
return Ok(u);
} else if extension == "json" {
let u: TranscodeConfig =
serde_json::from_reader(reader).expect("error while reading json");
return Ok(u);
}
Err(static_err("Invalid File Extension"))
}
if extension == "yml" || extension == "yaml" {
let u: TranscodeConfig =
serde_yaml::from_reader(reader).expect("error while reading yaml");
return Ok(u);
} else if extension == "json" {
let u: TranscodeConfig =
serde_json::from_reader(reader).expect("error while reading json");
return Ok(u);
}
Err(static_err("Invalid File Extension"))
}
}