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; use std::path::PathBuf;
fn main() { fn main() {
println!("cargo:rerun-if-changed=src/wrapper.h"); println!("cargo:rerun-if-changed=src/wrapper.h");
println!("cargo:rerun-if-changed=src/wrapper.cxx"); 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 cc_builder
.file("src/wrapper.cxx") .file("src/wrapper.cxx")
.flag("-std=c++2b") .flag("-std=c++2b")
.flag("-Og") .flag("-Og")
.include("src") .include("src")
.cpp(true); .cpp(true);
for include in taglib.include_paths { for include in taglib.include_paths {
cc_builder.include(include); cc_builder.include(include);
} }
cc_builder.compile("wrapper"); cc_builder.compile("wrapper");
let bindings = bindgen::Builder::default() let bindings = bindgen::Builder::default()
.header("src/wrapper.h") .header("src/wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks)) .parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate() .generate()
.expect("Unable to generate bindings"); .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 bindings
.write_to_file(out_path.join("bindings.rs")) .write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!"); .expect("Couldn't write bindings!");
} }

View file

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

View file

@ -5,83 +5,83 @@ use crate::{bindings, errors::TagLibError, traits::File, TagLibFileType};
use super::{oggtag::TagLibOggTag, tag::TagLibTag}; use super::{oggtag::TagLibOggTag, tag::TagLibTag};
pub struct TagLibFile { pub struct TagLibFile {
ctx: *mut bindings::TagLib_File, ctx: *mut bindings::TagLib_File,
taglib_type: Option<TagLibFileType>, taglib_type: Option<TagLibFileType>,
} }
pub fn new_taglib_file( pub fn new_taglib_file(
filepath: String, filepath: String,
taglib_type: Option<TagLibFileType>, taglib_type: Option<TagLibFileType>,
) -> Result<TagLibFile, TagLibError> { ) -> Result<TagLibFile, TagLibError> {
let filename_c = CString::new(filepath).unwrap(); let filename_c = CString::new(filepath).unwrap();
let filename_c_ptr = filename_c.as_ptr(); let filename_c_ptr = filename_c.as_ptr();
let file = unsafe { let file = unsafe {
if let Some(taglib_type) = taglib_type { if let Some(taglib_type) = taglib_type {
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into()) bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
} else { } else {
bindings::wrap_taglib_file_new(filename_c_ptr) bindings::wrap_taglib_file_new(filename_c_ptr)
} }
}; };
if file.is_null() { if file.is_null() {
return Err(TagLibError::InvalidFile); return Err(TagLibError::InvalidFile);
} }
Ok(TagLibFile { Ok(TagLibFile {
ctx: file, ctx: file,
taglib_type, taglib_type,
}) })
} }
impl TagLibFile { impl TagLibFile {
pub fn tag(&self) -> Result<TagLibTag, TagLibError> { pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) }; let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() { if tag.is_null() {
return Err(TagLibError::MetadataUnavailable); return Err(TagLibError::MetadataUnavailable);
} }
Ok(TagLibTag { ctx: tag }) Ok(TagLibTag { ctx: tag })
} }
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> { pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
if let Some(taglib_type) = &self.taglib_type { if let Some(taglib_type) = &self.taglib_type {
let supported = match taglib_type { let supported = match taglib_type {
TagLibFileType::OggFLAC TagLibFileType::OggFLAC
| TagLibFileType::OggOpus | TagLibFileType::OggOpus
| TagLibFileType::OggSpeex | TagLibFileType::OggSpeex
| TagLibFileType::OggVorbis => true, | TagLibFileType::OggVorbis => true,
}; };
if !supported { if !supported {
panic!("ogg tag not supported") panic!("ogg tag not supported")
} }
} }
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) }; let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
if tag.is_null() { if tag.is_null() {
return Err(TagLibError::MetadataUnavailable); return Err(TagLibError::MetadataUnavailable);
} }
Ok(TagLibOggTag { ctx: tag }) Ok(TagLibOggTag { ctx: tag })
} }
} }
impl File for TagLibFile { impl File for TagLibFile {
fn save(&mut self) -> Result<(), TagLibError> { fn save(&mut self) -> Result<(), TagLibError> {
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) }; let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
match result { match result {
true => Ok(()), true => Ok(()),
false => Err(TagLibError::SaveError), false => Err(TagLibError::SaveError),
} }
} }
} }
impl Drop for TagLibFile { impl Drop for TagLibFile {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
bindings::wrap_taglib_file_free(self.ctx); 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}; use crate::{bindings, utils::c_str_to_str};
pub struct TagLibOggTag { pub struct TagLibOggTag {
pub ctx: *mut bindings::TagLib_Tag, pub ctx: *mut bindings::TagLib_Tag,
} }
impl TagLibOggTag { impl TagLibOggTag {
pub fn get_field(&self, key: String) -> Option<String> { pub fn get_field(&self, key: String) -> Option<String> {
let key = CString::new(key).unwrap(); let key = CString::new(key).unwrap();
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) }; let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
if value.is_null() { if value.is_null() {
None None
} else { } else {
c_str_to_str(value) c_str_to_str(value)
} }
} }
pub fn add_field(&self, key: String, value: String) { pub fn add_field(&self, key: String, value: String) {
let key = CString::new(key).unwrap(); let key = CString::new(key).unwrap();
let value = CString::new(value).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) { pub fn remove_fields(&self, key: String) {
let key = CString::new(key).unwrap(); 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}; use crate::{bindings, traits::Tag, utils::c_str_to_str};
pub struct TagLibTag { pub struct TagLibTag {
pub ctx: *mut bindings::TagLib_Tag, pub ctx: *mut bindings::TagLib_Tag,
} }
impl Tag for TagLibTag { impl Tag for TagLibTag {
fn title(&self) -> Option<String> { fn title(&self) -> Option<String> {
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) }; let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
c_str_to_str(title_ref) c_str_to_str(title_ref)
} }
fn set_title(&mut self, title: String) { fn set_title(&mut self, title: String) {
let title = CString::new(title).unwrap(); let title = CString::new(title).unwrap();
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) }; unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
} }
fn artist(&self) -> Option<String> { fn artist(&self) -> Option<String> {
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) }; let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
c_str_to_str(artist_ref) c_str_to_str(artist_ref)
} }
fn set_artist(&mut self, artist: String) { fn set_artist(&mut self, artist: String) {
let artist = CString::new(artist).unwrap(); 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 utils;
pub(crate) mod bindings { pub(crate) mod bindings {
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![allow(dead_code)] #![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs")); include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum TagLibFileType { pub enum TagLibFileType {
OggFLAC = 1, OggFLAC = 1,
OggOpus = 2, OggOpus = 2,
OggSpeex = 3, OggSpeex = 3,
OggVorbis = 4, OggVorbis = 4,
} }
pub use impls::file::*; pub use impls::file::*;

View file

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

View file

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

View file

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

View file

@ -1,243 +1,243 @@
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::{hash_map::Entry, HashMap},
fs, fs,
path::PathBuf, path::PathBuf,
process::exit, process::exit,
str::FromStr, str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::scope, thread::scope,
}; };
use crate::{ use crate::{
args::{CLIArgs, CopyCommandArgs}, args::{CLIArgs, CopyCommandArgs},
types::File, types::File,
utils::{ utils::{
formats::get_format_handler, formats::get_format_handler,
scan_for_music, scan_for_music,
transcoder::{ transcoder::{
presets::{print_presets, transcode_preset_or_config}, presets::{print_presets, transcode_preset_or_config},
transcode, transcode,
types::TranscodeConfig, types::TranscodeConfig,
}, },
}, },
}; };
pub fn copy_command( pub fn copy_command(
_args: CLIArgs, _args: CLIArgs,
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() { if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
panic!("Please provide Transcode Preset/Config"); panic!("Please provide Transcode Preset/Config");
} }
if let Some(preset) = &copy_args.transcode_preset { if let Some(preset) = &copy_args.transcode_preset {
if preset == "list" { if preset == "list" {
print_presets(); print_presets();
exit(0); exit(0);
} }
} }
println!("Scanning For Music"); println!("Scanning For Music");
let mut files = scan_for_music(&copy_args.source)?; let mut files = scan_for_music(&copy_args.source)?;
println!("Analysing Files"); println!("Analysing Files");
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); 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 { if copy_args.single_directory {
println!("Checking for Duplicates"); println!("Checking for Duplicates");
let mut seen: HashMap<String, bool> = HashMap::new(); let mut seen: HashMap<String, bool> = HashMap::new();
let mut dupes: Vec<String> = Vec::new(); let mut dupes: Vec<String> = Vec::new();
for file in files.iter() { for file in files.iter() {
let filename = file.join_filename(); let filename = file.join_filename();
if let Entry::Vacant(entry) = seen.entry(filename.clone()) { if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
entry.insert(true); entry.insert(true);
} else { } else {
dupes.push(filename); dupes.push(filename);
} }
} }
if !dupes.is_empty() { if !dupes.is_empty() {
panic!("Duplicates Found: {}", dupes.join(",")) panic!("Duplicates Found: {}", dupes.join(","))
} }
} }
if !copy_args.single_directory { if !copy_args.single_directory {
println!("Creating Directories"); println!("Creating Directories");
let mut directories: Vec<String> = Vec::new(); let mut directories: Vec<String> = Vec::new();
for file in files.iter() { for file in files.iter() {
let file_directory = file.path_from_source.to_string_lossy().to_string(); let file_directory = file.path_from_source.to_string_lossy().to_string();
if !directories.contains(&file_directory) { if !directories.contains(&file_directory) {
directories.push(file_directory.clone()); directories.push(file_directory.clone());
} }
} }
for directory in directories.iter() { for directory in directories.iter() {
fs::create_dir_all( fs::create_dir_all(
PathBuf::from_str(copy_args.dest.as_str()) PathBuf::from_str(copy_args.dest.as_str())
.expect("invalid destination") .expect("invalid destination")
.join(directory), .join(directory),
)?; )?;
} }
} }
if copy_args if copy_args
.transcode_preset .transcode_preset
.as_ref() .as_ref()
.unwrap_or(&"".to_string()) .unwrap_or(&"".to_string())
== "copy" == "copy"
{ {
println!("Copying Files Into Dest"); println!("Copying Files Into Dest");
copy_files(&files, copy_args)?; copy_files(&files, copy_args)?;
} else { } else {
println!("Transcoding Files"); println!("Transcoding Files");
transcode_files(&files, copy_args)?; transcode_files(&files, copy_args)?;
} }
Ok(()) Ok(())
} }
fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> { 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_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory { let to_path = match copy_args.single_directory {
true => to_path_dest.join(file.join_filename()), true => to_path_dest.join(file.join_filename()),
false => to_path_dest false => to_path_dest
.join(file.path_from_source.clone()) .join(file.path_from_source.clone())
.join(file.join_filename()), .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() { if !copy_args.no_skip_existing && to_path.exists() {
println!( println!(
"Skipping {} as already exists in destination", "Skipping {} as already exists in destination",
to_path_string to_path_string
); );
} else { } else {
println!("Copying {:?} to {}", from_path, to_path_string); println!("Copying {:?} to {}", from_path, to_path_string);
fs::copy(from_path, to_path)?; fs::copy(from_path, to_path)?;
} }
Ok(()) Ok(())
} }
fn copy_files( fn copy_files(
files: &[File], files: &[File],
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
for file in files.iter() { for file in files.iter() {
copy_file(file, copy_args)?; copy_file(file, copy_args)?;
if !file.extra_files.is_empty() { if !file.extra_files.is_empty() {
for extra_file in file.extra_files.iter() { for extra_file in file.extra_files.iter() {
copy_file(extra_file, copy_args)?; copy_file(extra_file, copy_args)?;
} }
} }
} }
Ok(()) Ok(())
} }
fn transcode_file( fn transcode_file(
file: &File, file: &File,
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
config: &TranscodeConfig, config: &TranscodeConfig,
is_threaded: bool, is_threaded: bool,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let new_filename_full: String = match config.file_extension.clone() { let new_filename_full: String = match config.file_extension.clone() {
Some(ext) => format!("{}.{}", file.filename, ext), Some(ext) => format!("{}.{}", file.filename, ext),
None => { None => {
panic!("file_extension is required in custom transcode configs"); 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_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory { let to_path = match copy_args.single_directory {
true => to_path_dest.join(new_filename_full), true => to_path_dest.join(new_filename_full),
false => to_path_dest false => to_path_dest
.join(file.path_from_source.clone()) .join(file.path_from_source.clone())
.join(new_filename_full), .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() { if !file.extra_files.is_empty() {
for extra_file in file.extra_files.iter() { for extra_file in file.extra_files.iter() {
copy_file(extra_file, copy_args)?; copy_file(extra_file, copy_args)?;
} }
} }
if !copy_args.no_skip_existing && to_path.exists() { if !copy_args.no_skip_existing && to_path.exists() {
println!( println!(
"Skipping transcode for {} as file already exists", "Skipping transcode for {} as file already exists",
to_path_string 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 { if is_threaded {
println!("Finished Transcoding {}", to_path_string); println!("Finished Transcoding {}", to_path_string);
} }
Ok(()) Ok(())
} }
fn transcode_files( fn transcode_files(
files: &[File], files: &[File],
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let transcode_config = transcode_preset_or_config( let transcode_config = transcode_preset_or_config(
copy_args.transcode_preset.as_ref(), copy_args.transcode_preset.as_ref(),
copy_args.transcode_config.as_ref(), copy_args.transcode_config.as_ref(),
) )
.expect("transcode config error"); .expect("transcode config error");
let threads = copy_args.threads.unwrap_or(1); let threads = copy_args.threads.unwrap_or(1);
if threads > 1 { if threads > 1 {
let files_copy = files.to_vec(); 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 copy_args = Arc::new(copy_args);
let transcode_config = Arc::new(transcode_config); let transcode_config = Arc::new(transcode_config);
scope(|s| { scope(|s| {
for _ in 0..threads { for _ in 0..threads {
s.spawn(|| loop { s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap(); let mut jobs = jobs.lock().unwrap();
let job = jobs.pop(); let job = jobs.pop();
if let Some(job) = job { if let Some(job) = job {
let result = transcode_file(&job, &copy_args, &transcode_config, true); let result = transcode_file(&job, &copy_args, &transcode_config, true);
if let Err(err) = result { if let Err(err) = result {
panic!("Error Transcoding: {}", err) panic!("Error Transcoding: {}", err)
} }
} else { } else {
break; break;
} }
}); });
} }
}); });
} else { } else {
for file in files.iter() { for file in files.iter() {
transcode_file(file, copy_args, &transcode_config, false)?; 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; use urlencoding::encode as url_encode;
fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<String>) -> String { 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(); let mut path_head = String::new();
if includes_path { if includes_path {
path_head.push_str("<th>Path</th>") path_head.push_str("<th>Path</th>")
} }
let mut link_head = String::new(); let mut link_head = String::new();
if link_base.is_some() { if link_base.is_some() {
link_head.push_str("<th>Link</th>") link_head.push_str("<th>Link</th>")
} }
html_content.push_str( html_content.push_str(
format!( format!(
" "
<table class=\"pure-table pure-table-horizontal\"> <table class=\"pure-table pure-table-horizontal\">
<thead> <thead>
<tr> <tr>
@ -38,69 +38,69 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
</thead> </thead>
<tbody> <tbody>
", ",
link_head, path_head link_head, path_head
) )
.as_str(), .as_str(),
); );
let mut is_odd = true; let mut is_odd = true;
for file in files.iter() { for file in files.iter() {
let td_class = match is_odd { let td_class = match is_odd {
true => "pure-table-odd", true => "pure-table-odd",
false => "pure-table-even", false => "pure-table-even",
}; };
let data_title = encode_text(&file.info.tags.title); let data_title = encode_text(&file.info.tags.title);
let data_artist = encode_text(&file.info.tags.artist); let data_artist = encode_text(&file.info.tags.artist);
let format = if let Some(format) = &file.info.format { let format = if let Some(format) = &file.info.format {
format.to_string() format.to_string()
} else { } else {
"unknown".to_string() "unknown".to_string()
}; };
let data_format = encode_text(&format); let data_format = encode_text(&format);
let mut path_data = String::new(); let mut path_data = String::new();
if includes_path { if includes_path {
let file_directory = file.path_from_source.to_string_lossy().to_string(); 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(); let mut url_data = String::new();
if let Some(link_base) = &link_base { if let Some(link_base) = &link_base {
let mut url = String::new(); let mut url = String::new();
url.push_str(link_base.as_str()); url.push_str(link_base.as_str());
url.push('/'); url.push('/');
let file_path = file.join_path_from_source(); let file_path = file.join_path_from_source();
let file_path: Vec<&OsStr> = file_path.iter().collect(); let file_path: Vec<&OsStr> = file_path.iter().collect();
for i in 0..(file_path.len()) { for i in 0..(file_path.len()) {
let file_path_element = file_path.get(i).unwrap(); let file_path_element = file_path.get(i).unwrap();
url.push_str( url.push_str(
url_encode( url_encode(
file_path_element file_path_element
.to_str() .to_str()
.expect("invalid character in filename"), .expect("invalid character in filename"),
) )
.to_string() .to_string()
.as_str(), .as_str(),
); );
if i != file_path.len() - 1 { if i != file_path.len() - 1 {
url.push('/'); 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( html_content.push_str(
format!( format!(
" "
<tr class=\"{}\"> <tr class=\"{}\">
{} {}
{} {}
@ -109,58 +109,58 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
<td>{}</td> <td>{}</td>
</tr> </tr>
", ",
td_class, url_data, path_data, data_title, data_artist, data_format td_class, url_data, path_data, data_title, data_artist, data_format
) )
.as_str(), .as_str(),
); );
is_odd = !is_odd; is_odd = !is_odd;
} }
html_content.push_str( html_content.push_str(
" "
</tbody> </tbody>
</table> </table>
", ",
); );
html_content html_content
} }
pub fn genhtml_command( pub fn genhtml_command(
_args: CLIArgs, _args: CLIArgs,
genhtml_args: &GenHTMLCommandArgs, genhtml_args: &GenHTMLCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Scanning For Music"); println!("Scanning For Music");
let mut files = scan_for_music(&genhtml_args.source)?; let mut files = scan_for_music(&genhtml_args.source)?;
println!("Analysing Files"); println!("Analysing Files");
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); 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 { files.sort_by(|a, b| -> Ordering {
if a.path_from_source != b.path_from_source { if a.path_from_source != b.path_from_source {
return a.path_from_source.cmp(&b.path_from_source); return a.path_from_source.cmp(&b.path_from_source);
} }
let a_tags = &a.info.tags; let a_tags = &a.info.tags;
let b_tags = &b.info.tags; let b_tags = &b.info.tags;
if a_tags.title != b_tags.title { if a_tags.title != b_tags.title {
return a_tags.title.cmp(&b_tags.title); return a_tags.title.cmp(&b_tags.title);
} }
a_tags.artist.cmp(&b_tags.artist) a_tags.artist.cmp(&b_tags.artist)
}); });
let mut html_content = String::new(); let mut html_content = String::new();
html_content.push_str( html_content.push_str(
format!( format!(
" "
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html> <html>
<head> <head>
@ -173,30 +173,30 @@ pub fn genhtml_command(
</head> </head>
<body> <body>
", ",
encode_text(&genhtml_args.title), encode_text(&genhtml_args.title),
encode_text(&genhtml_args.title), encode_text(&genhtml_args.title),
encode_text(&genhtml_args.description) encode_text(&genhtml_args.description)
) )
.as_str(), .as_str(),
); );
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base)); html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
html_content.push_str("</body></html>"); html_content.push_str("</body></html>");
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.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 html_index_file = std::fs::File::create(file_path);
match html_index_file { match html_index_file {
Ok(mut file) => match file.write_all(html_content.as_bytes()) { Ok(mut file) => match file.write_all(html_content.as_bytes()) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("Could not write HTML file: {}", e); panic!("Could not write HTML file: {}", e);
} }
}, },
Err(e) => { Err(e) => {
panic!("Could not create HTML file: {}", 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)] #[derive(Debug, Clone, Serialize)]
struct Tags { struct Tags {
title: String, title: String,
artist: String, artist: String,
} }
fn from_main_tags(tags: &crate::types::Tags) -> Tags { fn from_main_tags(tags: &crate::types::Tags) -> Tags {
Tags { Tags {
title: tags.title.clone(), title: tags.title.clone(),
artist: tags.artist.clone(), artist: tags.artist.clone(),
} }
} }
pub fn get_tags_command( pub fn get_tags_command(
_args: CLIArgs, _args: CLIArgs,
get_tags_args: &GetTagsCommandArgs, get_tags_args: &GetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> 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() { for file in get_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file))); files.push(File::from_path("".to_string(), PathBuf::from(file)));
} }
for file in files.iter_mut() { for file in files.iter_mut() {
let handler = get_format_handler(file)?; let handler = get_format_handler(file)?;
file.info.tags = handler.get_tags(true)?; file.info.tags = handler.get_tags(true)?;
} }
if files.len() == 1 { if files.len() == 1 {
let file = files.first().unwrap(); let file = files.first().unwrap();
if get_tags_args.json { if get_tags_args.json {
println!( println!(
"{}", "{}",
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap() serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
); );
} else { } else {
println!("{:#?}", from_main_tags(&file.info.tags)); println!("{:#?}", from_main_tags(&file.info.tags));
} }
} else if get_tags_args.json { } else if get_tags_args.json {
let mut result: HashMap<String, Tags> = HashMap::new(); let mut result: HashMap<String, Tags> = HashMap::new();
for file in files.iter() { for file in files.iter() {
result.insert( result.insert(
file.join_path_to().to_string_lossy().to_string(), file.join_path_to().to_string_lossy().to_string(),
from_main_tags(&file.info.tags), from_main_tags(&file.info.tags),
); );
} }
println!("{}", serde_json::to_string_pretty(&result)?); println!("{}", serde_json::to_string_pretty(&result)?);
} else { } else {
for file in files.iter() { for file in files.iter() {
println!( println!(
"{}: {:#?}", "{}: {:#?}",
file.join_path_to().to_string_lossy(), file.join_path_to().to_string_lossy(),
from_main_tags(&file.info.tags) 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; use crate::utils::scan_for_music;
fn rename_file(process_args: &ProcessCommandArgs, file: &mut File) { fn rename_file(process_args: &ProcessCommandArgs, file: &mut File) {
let title = &file.info.tags.title; let title = &file.info.tags.title;
let artist = &file.info.tags.artist; let artist = &file.info.tags.artist;
let replace_char = "_".to_string(); let replace_char = "_".to_string();
// Step 1: Remove Newlines // Step 1: Remove Newlines
let title = title.replace('\n', ""); let title = title.replace('\n', "");
let artist = artist.replace('\n', ""); let artist = artist.replace('\n', "");
// Step 2: Strip ASCII // Step 2: Strip ASCII
let title = reduce_to_ascii(title); let title = reduce_to_ascii(title);
let artist = reduce_to_ascii(artist); let artist = reduce_to_ascii(artist);
// Step 3: Remove File Seperators // Step 3: Remove File Seperators
let title = title.replace('\\', &replace_char); let title = title.replace('\\', &replace_char);
let title = title.replace('/', &replace_char); let title = title.replace('/', &replace_char);
let artist = artist.replace('\\', &replace_char); let artist = artist.replace('\\', &replace_char);
let artist = artist.replace('/', &replace_char); let artist = artist.replace('/', &replace_char);
// Step 4: Join Filename // Step 4: Join Filename
let filename = format!("{} - {}", artist, title); let filename = format!("{} - {}", artist, title);
if filename == file.filename { if filename == file.filename {
return; return;
} }
// Step 5: Make new File with filename set // Step 5: Make new File with filename set
let mut new_file = file.clone(); let mut new_file = file.clone();
new_file.filename = filename.clone(); new_file.filename = filename.clone();
let extension = &file.extension; let extension = &file.extension;
// Step 6: Rename File // Step 6: Rename File
println!( println!(
"Renaming File from {}.{extension} to {}.{extension}", "Renaming File from {}.{extension} to {}.{extension}",
file.filename, filename file.filename, filename
); );
if !process_args.dry_run { if !process_args.dry_run {
if std::path::Path::new(&new_file.join_path_to()).exists() { if std::path::Path::new(&new_file.join_path_to()).exists() {
panic!( panic!(
"Refusing to rename {} to {}, please retag to be distinct", "Refusing to rename {} to {}, please retag to be distinct",
&file.join_filename(), &file.join_filename(),
&new_file.join_filename() &new_file.join_filename()
); );
} else { } else {
let err = std::fs::rename(&file.join_path_to(), &new_file.join_path_to()); let err = std::fs::rename(&file.join_path_to(), &new_file.join_path_to());
if err.is_err() { if err.is_err() {
panic!("Could not rename {:?}", err) panic!("Could not rename {:?}", err)
} }
} }
} }
if !file.extra_files.is_empty() { if !file.extra_files.is_empty() {
let mut new_extra_files: Vec<File> = Vec::new(); let mut new_extra_files: Vec<File> = Vec::new();
for extra_file in file.extra_files.iter() { for extra_file in file.extra_files.iter() {
let mut new_extra_file = extra_file.clone(); let mut new_extra_file = extra_file.clone();
new_extra_file.filename = filename.clone(); new_extra_file.filename = filename.clone();
let extra_extension = &extra_file.extension; let extra_extension = &extra_file.extension;
println!( println!(
"Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}", "Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}",
file.filename, filename file.filename, filename
); );
if !process_args.dry_run { if !process_args.dry_run {
let err = let err =
std::fs::rename(&extra_file.join_path_to(), &new_extra_file.join_path_to()); std::fs::rename(&extra_file.join_path_to(), &new_extra_file.join_path_to());
if err.is_err() { if err.is_err() {
panic!("Could not rename {:?}", err) panic!("Could not rename {:?}", err)
} }
new_extra_files.push(new_extra_file); new_extra_files.push(new_extra_file);
} }
} }
if !process_args.dry_run { if !process_args.dry_run {
file.extra_files.clear(); file.extra_files.clear();
file.extra_files.extend(new_extra_files); file.extra_files.extend(new_extra_files);
} }
} }
if !process_args.dry_run { if !process_args.dry_run {
file.filename = filename; file.filename = filename;
} }
} }
pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::error::Error>> { pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::error::Error>> {
if !file.info.supports_replaygain { if !file.info.supports_replaygain {
println!( println!(
"Skipping replaygain for {:?}, not supported", "Skipping replaygain for {:?}, not supported",
file.join_path_from_source() file.join_path_from_source()
); );
return Ok(()); return Ok(());
} }
if file.info.contains_replaygain && !force { if file.info.contains_replaygain && !force {
println!( println!(
"Skipping replaygain for {:?}, contains already", "Skipping replaygain for {:?}, contains already",
file.join_path_from_source() file.join_path_from_source()
); );
return Ok(()); return Ok(());
} }
println!( println!(
"Analyzing replaygain for {:?}", "Analyzing replaygain for {:?}",
file.join_path_from_source() 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.set_replaygain_data(replaygain_data)?;
handler.save_changes()?; handler.save_changes()?;
println!( println!(
"Applied replaygain tags for {:?}", "Applied replaygain tags for {:?}",
file.join_path_from_source() file.join_path_from_source()
); );
Ok(()) Ok(())
} }
pub fn process_command( pub fn process_command(
_args: CLIArgs, _args: CLIArgs,
process_args: &ProcessCommandArgs, process_args: &ProcessCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Scanning For Music"); println!("Scanning For Music");
let mut files = scan_for_music(&process_args.source)?; let mut files = scan_for_music(&process_args.source)?;
println!("Analysing Files"); println!("Analysing Files");
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); 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"); println!("Renaming Files");
for file in files.iter_mut() { for file in files.iter_mut() {
rename_file(process_args, file); rename_file(process_args, file);
} }
if !process_args.skip_replaygain && !process_args.dry_run { if !process_args.skip_replaygain && !process_args.dry_run {
println!("Adding ReplayGain Tags to Files"); 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 { if threads <= 1 {
for file in files.iter_mut() { for file in files.iter_mut() {
add_replaygain_tags(file, process_args.force_replaygain)?; add_replaygain_tags(file, process_args.force_replaygain)?;
} }
return Ok(()); return Ok(());
} else { } else {
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files)); let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files));
scope(|s| { scope(|s| {
for _ in 0..threads { for _ in 0..threads {
s.spawn(|| loop { s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap(); let mut jobs = jobs.lock().unwrap();
let job = jobs.pop(); let job = jobs.pop();
if let Some(job) = job { if let Some(job) = job {
let result = add_replaygain_tags(&job, process_args.force_replaygain); let result = add_replaygain_tags(&job, process_args.force_replaygain);
if let Err(err) = result { if let Err(err) = result {
panic!("Error doing replaygain: {}", err) panic!("Error doing replaygain: {}", err)
} }
} else { } else {
break; break;
} }
}); });
} }
}); });
} }
} }
Ok(()) Ok(())
} }

View file

@ -6,28 +6,28 @@ use crate::types::File;
use crate::utils::formats::get_format_handler; use crate::utils::formats::get_format_handler;
pub fn set_tags_command( pub fn set_tags_command(
_args: CLIArgs, _args: CLIArgs,
add_tags_args: &SetTagsCommandArgs, add_tags_args: &SetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> 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() { for file in add_tags_args.files.iter() {
files.push(File::from_path("".to_string(), PathBuf::from(file))); files.push(File::from_path("".to_string(), PathBuf::from(file)));
} }
for file in files.iter() { for file in files.iter() {
let mut handler = get_format_handler(file)?; let mut handler = get_format_handler(file)?;
if let Some(title) = &add_tags_args.title { if let Some(title) = &add_tags_args.title {
handler.set_title(title.clone())?; handler.set_title(title.clone())?;
} }
if let Some(artist) = &add_tags_args.artist { if let Some(artist) = &add_tags_args.artist {
handler.set_artist(artist.clone())?; 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; use crate::utils::transcoder::transcode;
pub fn transcode_command( pub fn transcode_command(
_args: CLIArgs, _args: CLIArgs,
transcode_args: &TranscodeCommandArgs, transcode_args: &TranscodeCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
if let Some(preset) = &transcode_args.transcode_preset { if let Some(preset) = &transcode_args.transcode_preset {
if preset == "list" { if preset == "list" {
print_presets(); print_presets();
exit(0); exit(0);
} }
} }
let transcode_config = transcode_preset_or_config( let transcode_config = transcode_preset_or_config(
transcode_args.transcode_preset.as_ref(), transcode_args.transcode_preset.as_ref(),
transcode_args.transcode_config.as_ref(), transcode_args.transcode_config.as_ref(),
) )
.expect("transcode config error"); .expect("transcode config error");
println!("Transcoding"); println!("Transcoding");
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source)); 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 output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
if !transcode_args.ignore_extension { if !transcode_args.ignore_extension {
if let Some(ref file_extension) = transcode_config.file_extension { if let Some(ref file_extension) = transcode_config.file_extension {
if file_extension != &output_file.extension { if file_extension != &output_file.extension {
panic!( panic!(
concat!( concat!(
"{} is not the recommended ", "{} is not the recommended ",
"extension for specified transcode config ", "extension for specified transcode config ",
"please change it to {} ", "please change it to {} ",
"or run with --ignore-extension" "or run with --ignore-extension"
), ),
output_file.extension, file_extension output_file.extension, file_extension
); );
} }
} }
} }
let (tx, rx) = mpsc::channel::<String>(); let (tx, rx) = mpsc::channel::<String>();
let mut child: Option<JoinHandle<()>> = None; let mut child: Option<JoinHandle<()>> = None;
if !transcode_args.hide_progress { if !transcode_args.hide_progress {
child = Some(thread::spawn(move || loop { child = Some(thread::spawn(move || loop {
let progress = rx.recv(); let progress = rx.recv();
if let Ok(progress_str) = progress { if let Ok(progress_str) = progress {
println!("Transcode Progress: {}", progress_str); println!("Transcode Progress: {}", progress_str);
} else { } else {
break; break;
} }
})); }));
} }
transcode( transcode(
input_file, input_file,
transcode_args.dest.clone(), transcode_args.dest.clone(),
&transcode_config, &transcode_config,
match transcode_args.hide_progress { match transcode_args.hide_progress {
true => None, true => None,
false => Some(tx), false => Some(tx),
}, },
)?; )?;
if let Some(child) = child { if let Some(child) = child {
child.join().expect("oops! the child thread panicked"); 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; use commands::transcode::transcode_command;
fn main() -> Result<(), Box<dyn std::error::Error>> { 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 { match command {
Commands::Process(process_args) => { Commands::Process(process_args) => {
return process_command(cli, &process_args); return process_command(cli, &process_args);
} }
Commands::Genhtml(genhtml_args) => { Commands::Genhtml(genhtml_args) => {
return genhtml_command(cli, &genhtml_args); return genhtml_command(cli, &genhtml_args);
} }
Commands::Transcode(transcode_args) => { Commands::Transcode(transcode_args) => {
return transcode_command(cli, &transcode_args); return transcode_command(cli, &transcode_args);
} }
Commands::Copy(copy_args) => { Commands::Copy(copy_args) => {
return copy_command(cli, &copy_args); return copy_command(cli, &copy_args);
} }
Commands::SetTags(set_tags_args) => { Commands::SetTags(set_tags_args) => {
return set_tags_command(cli, &set_tags_args); return set_tags_command(cli, &set_tags_args);
} }
Commands::GetTags(get_tags_args) => { Commands::GetTags(get_tags_args) => {
return get_tags_command(cli, &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)] #[derive(Debug, Clone)]
pub struct Tags { pub struct Tags {
pub title: String, pub title: String,
pub artist: String, pub artist: String,
} }
impl Default for Tags { impl Default for Tags {
fn default() -> Self { fn default() -> Self {
Tags { Tags {
title: "".to_string(), title: "".to_string(),
artist: "".to_string(), artist: "".to_string(),
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ReplayGainData { pub struct ReplayGainData {
pub track_gain: String, pub track_gain: String,
pub track_peak: String, pub track_peak: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ReplayGainRawData { pub struct ReplayGainRawData {
pub track_gain: f64, pub track_gain: f64,
pub track_peak: f64, pub track_peak: f64,
} }
impl ReplayGainRawData { impl ReplayGainRawData {
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData { pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
if is_ogg_opus { if is_ogg_opus {
ReplayGainData { ReplayGainData {
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()), track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
track_peak: "".to_string(), // Not Required track_peak: "".to_string(), // Not Required
} }
} else { } else {
ReplayGainData { ReplayGainData {
track_gain: format!("{:.2} dB", self.track_gain), track_gain: format!("{:.2} dB", self.track_gain),
track_peak: format!("{:.6}", self.track_peak), track_peak: format!("{:.6}", self.track_peak),
} }
} }
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct AudioFileInfo { pub struct AudioFileInfo {
pub tags: Tags, pub tags: Tags,
pub contains_replaygain: bool, pub contains_replaygain: bool,
pub supports_replaygain: bool, pub supports_replaygain: bool,
pub format: Option<FileFormat>, pub format: Option<FileFormat>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct File { pub struct File {
pub filename: String, pub filename: String,
pub extension: String, pub extension: String,
// relative path to file's folder // relative path to file's folder
pub path_to: PathBuf, pub path_to: PathBuf,
// path to folder from source // path to folder from source
pub path_from_source: PathBuf, pub path_from_source: PathBuf,
pub extra_files: Vec<File>, pub extra_files: Vec<File>,
pub info: AudioFileInfo, pub info: AudioFileInfo,
} }
impl File { impl File {
pub fn from_path(source_dir: String, full_path: PathBuf) -> File { pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
let full_file_path = PathBuf::from(&source_dir).join(full_path); let full_file_path = PathBuf::from(&source_dir).join(full_path);
let filename_without_extension = full_file_path let filename_without_extension = full_file_path
.file_stem() .file_stem()
.expect("filename invalid") .expect("filename invalid")
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let extension = full_file_path.extension(); let extension = full_file_path.extension();
let extension = if let Some(extension) = extension { let extension = if let Some(extension) = extension {
extension.to_string_lossy().to_string() extension.to_string_lossy().to_string()
} else { } else {
"".to_string() "".to_string()
}; };
let path_from_src = full_file_path let path_from_src = full_file_path
.strip_prefix(&source_dir) .strip_prefix(&source_dir)
.expect("couldn't get path relative to source"); .expect("couldn't get path relative to source");
let mut folder_path_from_src = path_from_src.to_path_buf(); let mut folder_path_from_src = path_from_src.to_path_buf();
folder_path_from_src.pop(); 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 { File {
filename: filename_without_extension, filename: filename_without_extension,
extension, extension,
path_from_source: folder_path_from_src, path_from_source: folder_path_from_src,
path_to, path_to,
extra_files: Vec::new(), extra_files: Vec::new(),
info: AudioFileInfo::default(), info: AudioFileInfo::default(),
} }
} }
pub fn join_filename(&self) -> String { pub fn join_filename(&self) -> String {
format!("{}.{}", self.filename, self.extension) format!("{}.{}", self.filename, self.extension)
} }
pub fn join_path_to(&self) -> PathBuf { pub fn join_path_to(&self) -> PathBuf {
PathBuf::from(&self.path_to).join(self.join_filename()) PathBuf::from(&self.path_to).join(self.join_filename())
} }
pub fn join_path_from_source(&self) -> PathBuf { pub fn join_path_from_source(&self) -> PathBuf {
PathBuf::from(&self.path_from_source).join(self.join_filename()) 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"); const MAPPINGS_DATA: &str = include_str!("mappings.json");
lazy_static! { lazy_static! {
static ref MAPPINGS: HashMap<char, String> = { static ref MAPPINGS: HashMap<char, String> = {
let data: HashMap<String, String> = let data: HashMap<String, String> =
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid"); serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
let mut replacement_map: HashMap<char, String> = HashMap::new(); let mut replacement_map: HashMap<char, String> = HashMap::new();
for (chr, repl) in &data { for (chr, repl) in &data {
match chr.parse::<u32>() { match chr.parse::<u32>() {
Ok(n) => { Ok(n) => {
let b = char::from_u32(n).expect("invalid char in string"); let b = char::from_u32(n).expect("invalid char in string");
replacement_map.insert(b, repl.to_string()); replacement_map.insert(b, repl.to_string());
} }
Err(e) => { Err(e) => {
panic!( panic!(
"mapping data broken, could not parse char {} with error {}", "mapping data broken, could not parse char {} with error {}",
chr, e chr, e
); );
} }
} }
} }
replacement_map replacement_map
}; };
} }
pub fn reduce_to_ascii(input: String) -> String { pub fn reduce_to_ascii(input: String) -> String {
if input.is_ascii() { if input.is_ascii() {
return input; return input;
} }
let mut output = String::with_capacity(input.len()); let mut output = String::with_capacity(input.len());
for c in input.chars() { for c in input.chars() {
if c.is_ascii() { if c.is_ascii() {
output.push(c); output.push(c);
continue; continue;
} }
if let Some(replacement) = MAPPINGS.get(&c) { if let Some(replacement) = MAPPINGS.get(&c) {
output.push_str(replacement); output.push_str(replacement);
} }
} }
output output
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,165 +1,163 @@
use std::path::Path; use std::path::Path;
use taglib::{ use taglib::{
new_taglib_file, new_taglib_file,
traits::{File, Tag}, traits::{File, Tag},
TagLibFileType, TagLibFileType,
}; };
use crate::{ use crate::{
types::{AudioFileInfo, ReplayGainRawData, Tags}, types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat, utils::format_detection::FileFormat,
utils::formats::{FormatHandler, AudioFormatError, BoxedError} utils::formats::{AudioFormatError, BoxedError, FormatHandler},
}; };
pub struct TaglibAudioFormat { pub struct TaglibAudioFormat {
file: taglib::TagLibFile, file: taglib::TagLibFile,
file_format: Option<FileFormat>, file_format: Option<FileFormat>,
} }
impl FormatHandler for TaglibAudioFormat { impl FormatHandler for TaglibAudioFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> { fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
let tags = self.file.tag()?; let tags = self.file.tag()?;
let mut title = tags.title(); let mut title = tags.title();
if title.is_none() { if title.is_none() {
if !allow_missing { if !allow_missing {
return Err(Box::new(AudioFormatError::MissingTitle)); return Err(Box::new(AudioFormatError::MissingTitle));
} else { } else {
title = Some("".to_string()) title = Some("".to_string())
} }
} }
let mut artist = tags.artist(); let mut artist = tags.artist();
if artist.is_none() { if artist.is_none() {
if !allow_missing { if !allow_missing {
return Err(Box::new(AudioFormatError::MissingArtist)); return Err(Box::new(AudioFormatError::MissingArtist));
} else { } else {
artist = Some("".to_string()) artist = Some("".to_string())
} }
} }
Ok(Tags { Ok(Tags {
title: title.unwrap(), title: title.unwrap(),
artist: artist.unwrap(), artist: artist.unwrap(),
}) })
} }
fn contains_replaygain_tags(&self) -> bool { fn contains_replaygain_tags(&self) -> bool {
if let Some(format) = self.file_format { if let Some(format) = self.file_format {
if format == FileFormat::OggOpus { if format == FileFormat::OggOpus {
let oggtag = self.file.oggtag().expect("oggtag not available?"); let oggtag = self.file.oggtag().expect("oggtag not available?");
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some(); return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
} }
match format { match format {
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => { FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
let oggtag = self.file.oggtag().expect("oggtag not available?"); let oggtag = self.file.oggtag().expect("oggtag not available?");
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string()); let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string()); let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
return gain.is_some() && peak.is_some(); return gain.is_some() && peak.is_some();
} }
_ => {} _ => {}
} }
} }
false false
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
if let Some(format) = self.file_format { if let Some(format) = self.file_format {
return matches!( return matches!(
format, format,
// All Ogg formats support ReplayGain // All Ogg formats support ReplayGain
FileFormat::OggVorbis FileFormat::OggVorbis
| FileFormat::OggOpus | FileFormat::OggOpus
| FileFormat::OggFLAC | FileFormat::OggFLAC
| FileFormat::OggSpeex | FileFormat::OggSpeex // Both "support" ReplayGain but not implemented yet
// Both "support" ReplayGain but not implemented yet // FileFormat::Wav |
// FileFormat::Wav | // FileFormat::WavPack
// FileFormat::WavPack );
); }
}
false false
} }
fn set_title(&mut self, title: String) -> Result<(), BoxedError> { fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?; let mut tags = self.file.tag()?;
tags.set_title(title); tags.set_title(title);
Ok(()) Ok(())
} }
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> { fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
let mut tags = self.file.tag()?; let mut tags = self.file.tag()?;
tags.set_artist(artist); tags.set_artist(artist);
Ok(()) Ok(())
} }
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> { fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
if let Some(format) = self.file_format { if let Some(format) = self.file_format {
let oggtag = self.file.oggtag().expect("oggtag not available?"); let oggtag = self.file.oggtag().expect("oggtag not available?");
if format == FileFormat::OggOpus { if format == FileFormat::OggOpus {
let data = data.to_normal(true); let data = data.to_normal(true);
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain); oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
} else if matches!( } else if matches!(
format, format,
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
) { ) {
let data = data.to_normal(false); let data = data.to_normal(false);
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain); 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_PEAK".to_string(), data.track_peak);
} }
} }
Ok(()) Ok(())
} }
fn save_changes(&mut self) -> Result<(), BoxedError> { fn save_changes(&mut self) -> Result<(), BoxedError> {
let res = self.file.save(); let res = self.file.save();
match res { match res {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(e) => Err(Box::new(e)), Err(e) => Err(Box::new(e)),
} }
} }
fn get_audio_file_info( fn get_audio_file_info(
&mut self, &mut self,
allow_missing_tags: bool, allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> { ) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo { Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?, tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(), contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(), supports_replaygain: self.supports_replaygain(),
format: self.file_format, format: self.file_format,
}) })
} }
} }
pub fn new_handler( pub fn new_handler(
path: &Path, path: &Path,
file_format: Option<FileFormat>, file_format: Option<FileFormat>,
) -> Result<TaglibAudioFormat, BoxedError> { ) -> Result<TaglibAudioFormat, BoxedError> {
let mut taglib_format: Option<TagLibFileType> = None; let mut taglib_format: Option<TagLibFileType> = None;
if let Some(format) = file_format { if let Some(format) = file_format {
taglib_format = match format { taglib_format = match format {
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis), FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
FileFormat::OggOpus => Some(TagLibFileType::OggOpus), FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC), FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex), FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
_ => None, _ => None,
} }
} }
Ok(TaglibAudioFormat { Ok(TaglibAudioFormat {
file_format, file_format,
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_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 crate::types::{AudioFileInfo, File, ReplayGainRawData, Tags};
use super::format_detection::detect_format; use super::format_detection::detect_format;
type BoxedError = Box<dyn Error>; type BoxedError = Box<dyn Error>;
pub trait FormatHandler { pub trait FormatHandler {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>; fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
fn contains_replaygain_tags(&self) -> bool; fn contains_replaygain_tags(&self) -> bool;
fn supports_replaygain(&self) -> bool; fn supports_replaygain(&self) -> bool;
fn set_title(&mut self, title: String) -> Result<(), BoxedError>; fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>; fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> 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( fn get_audio_file_info(
&mut self, &mut self,
allow_missing_tags: bool, allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError>; ) -> Result<AudioFileInfo, BoxedError>;
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AudioFormatError { pub enum AudioFormatError {
#[error("title missing from tags")] #[error("title missing from tags")]
MissingTitle, MissingTitle,
#[error("artist missing from tags")] #[error("artist missing from tags")]
MissingArtist, MissingArtist,
} }
pub fn get_format_handler(file: &File) -> Result<Box<dyn FormatHandler>, BoxedError> { pub fn get_format_handler(file: &File) -> Result<Box<dyn FormatHandler>, BoxedError> {
let format = detect_format(&file.join_path_to())?; let format = detect_format(&file.join_path_to())?;
let path = file.join_path_to(); let path = file.join_path_to();
for handler in handlers::HANDLERS.iter() { for handler in handlers::HANDLERS.iter() {
if !handler.supported_extensions.contains(&file.extension) { if !handler.supported_extensions.contains(&file.extension) {
continue continue;
} }
if !handler.supported_formats.contains(&format) { if !handler.supported_formats.contains(&format) {
continue 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 { 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 { pub fn is_supported_file(file_path: &Path) -> bool {
let ext = file_path.extension(); let ext = file_path.extension();
if ext.is_none() { if ext.is_none() {
return false; return false;
} }
let ext = ext.unwrap().to_str().unwrap(); let ext = ext.unwrap().to_str().unwrap();
if !is_supported_extension(ext) { if !is_supported_extension(ext) {
return false; return false;
} }
let format = detect_format(file_path); let format = detect_format(file_path);
if format.is_err() { if format.is_err() {
return false; 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 ascii_reduce;
pub mod ffprobe;
pub mod format_detection; pub mod format_detection;
pub mod replaygain; pub mod replaygain;
pub mod transcoder; pub mod transcoder;
pub mod ffprobe;
pub mod formats; pub mod formats;
mod music_scanner; mod music_scanner;
pub use formats::is_supported_file; pub use formats::is_supported_file;
pub use music_scanner::scan_for_music; pub use music_scanner::scan_for_music;

View file

@ -6,52 +6,52 @@ use walkdir::WalkDir;
use super::is_supported_file; use super::is_supported_file;
pub fn find_extra_files( pub fn find_extra_files(
src_dir: String, src_dir: String,
file: &File, file: &File,
) -> Result<Vec<File>, Box<dyn std::error::Error>> { ) -> 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)? { for entry in fs::read_dir(&file.path_to)? {
let entry = entry?; let entry = entry?;
if !entry.metadata()?.is_file() { if !entry.metadata()?.is_file() {
continue; continue;
} }
let entry_path = entry.path(); let entry_path = entry.path();
let extension = entry_path.extension(); let extension = entry_path.extension();
if extension.is_none() { if extension.is_none() {
continue; continue;
} }
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
&& extension.unwrap().to_string_lossy() != file.extension && extension.unwrap().to_string_lossy() != file.extension
{ {
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone())); 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>> { 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) { for entry in WalkDir::new(src_dir) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let entry_path = entry.into_path(); let entry_path = entry.into_path();
if entry_path.is_dir() { if entry_path.is_dir() {
continue; continue;
} }
if is_supported_file(&entry_path) { if is_supported_file(&entry_path) {
let mut file = File::from_path(src_dir.clone(), entry_path.clone()); let mut file = File::from_path(src_dir.clone(), entry_path.clone());
file.extra_files file.extra_files
.extend(find_extra_files(src_dir.clone(), &file)?); .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::{ use std::{
io::{BufRead, BufReader}, io::{BufRead, BufReader},
path::PathBuf, path::PathBuf,
process::Command, process::Command,
}; };
use string_error::static_err; use string_error::static_err;
@ -9,94 +9,94 @@ use string_error::static_err;
use crate::types::ReplayGainRawData; use crate::types::ReplayGainRawData;
pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainRawData, Box<dyn std::error::Error>> { pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainRawData, Box<dyn std::error::Error>> {
let output = Command::new(crate::meta::FFMPEG) let output = Command::new(crate::meta::FFMPEG)
.args([ .args([
"-hide_banner", "-hide_banner",
"-nostats", "-nostats",
"-v", "-v",
"info", "info",
"-i", "-i",
&path.to_string_lossy(), &path.to_string_lossy(),
"-filter_complex", "-filter_complex",
"[0:a]ebur128=framelog=verbose:peak=sample:dualmono=true[s6]", "[0:a]ebur128=framelog=verbose:peak=sample:dualmono=true[s6]",
"-map", "-map",
"[s6]", "[s6]",
"-f", "-f",
"null", "null",
"/dev/null", "/dev/null",
]) ])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
print!("{:?}", String::from_utf8(output.stderr).unwrap()); print!("{:?}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFmpeg Crashed")); return Err(static_err("FFmpeg Crashed"));
} }
// info we need is in stdout // info we need is in stdout
let output_str = String::from_utf8(output.stderr).unwrap(); 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, // for some reason ffmpeg outputs the summary twice,
// the first time is garbage data // the first time is garbage data
let mut has_seen_first_summary = false; let mut has_seen_first_summary = false;
let mut should_start_reading_lines = false; let mut should_start_reading_lines = false;
let output_reader = BufReader::new(output_str.as_bytes()); let output_reader = BufReader::new(output_str.as_bytes());
for line in output_reader.lines() { for line in output_reader.lines() {
if let Ok(line) = line { if let Ok(line) = line {
if line.starts_with("[Parsed_ebur128_0") { if line.starts_with("[Parsed_ebur128_0") {
if has_seen_first_summary { if has_seen_first_summary {
should_start_reading_lines = true; should_start_reading_lines = true;
ebur128_summary.push_str("Summary:\n") ebur128_summary.push_str("Summary:\n")
} else { } else {
has_seen_first_summary = true; has_seen_first_summary = true;
} }
} else if should_start_reading_lines { } else if should_start_reading_lines {
ebur128_summary.push_str(line.trim()); ebur128_summary.push_str(line.trim());
ebur128_summary.push('\n') ebur128_summary.push('\n')
} }
} else { } else {
break; break;
} }
} }
let mut track_gain: f64 = 0.0; let mut track_gain: f64 = 0.0;
let mut track_peak: f64 = 0.0; let mut track_peak: f64 = 0.0;
let summary_reader = BufReader::new(ebur128_summary.as_bytes()); let summary_reader = BufReader::new(ebur128_summary.as_bytes());
for line in summary_reader.lines() { for line in summary_reader.lines() {
if let Ok(line) = line { if let Ok(line) = line {
if line.starts_with("I:") { if line.starts_with("I:") {
let mut l = line.split(':'); let mut l = line.split(':');
l.next(); l.next();
let gain = l.next().unwrap().trim().trim_end_matches(" LUFS"); let gain = l.next().unwrap().trim().trim_end_matches(" LUFS");
let gain = gain.parse::<f64>()?; let gain = gain.parse::<f64>()?;
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Gain_calculation // 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." // "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; let gain = -18_f64 - gain;
track_gain = gain; track_gain = gain;
} }
if line.starts_with("Peak:") { if line.starts_with("Peak:") {
let mut l = line.split(':'); let mut l = line.split(':');
l.next(); l.next();
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Loudness_normalization // 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 = l.next().unwrap().trim().trim_end_matches(" dBFS");
let peak = peak.parse::<f64>()?; let peak = peak.parse::<f64>()?;
let peak = f64::powf(10_f64, peak / 20.0_f64); let peak = f64::powf(10_f64, peak / 20.0_f64);
track_peak = peak; track_peak = peak;
} }
} else { } else {
break; break;
} }
} }
Ok(ReplayGainRawData { Ok(ReplayGainRawData {
track_gain, track_gain,
track_peak, track_peak,
}) })
} }

View file

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

View file

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

View file

@ -1,11 +1,11 @@
use std::{ use std::{
fs, fs,
io::{BufRead, BufReader, Seek, SeekFrom}, io::{BufRead, BufReader, Seek, SeekFrom},
path::PathBuf, path::PathBuf,
process::Command, process::Command,
sync::mpsc::{self, Sender}, sync::mpsc::{self, Sender},
thread::{self, JoinHandle}, thread::{self, JoinHandle},
time::Duration, time::Duration,
}; };
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
@ -14,127 +14,127 @@ use string_error::static_err;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct FFProbeOutput { struct FFProbeOutput {
pub format: FFProbeFormat, pub format: FFProbeFormat,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct FFProbeFormat { struct FFProbeFormat {
pub duration: String, pub duration: String,
} }
fn get_file_length_milliseconds( fn get_file_length_milliseconds(
source_filepath: PathBuf, source_filepath: PathBuf,
) -> Result<u64, Box<dyn std::error::Error>> { ) -> Result<u64, Box<dyn std::error::Error>> {
let output = Command::new(crate::meta::FFPROBE) let output = Command::new(crate::meta::FFPROBE)
.args([ .args([
"-v", "-v",
"quiet", "quiet",
"-print_format", "-print_format",
"json", "json",
"-show_format", "-show_format",
&source_filepath.to_string_lossy(), &source_filepath.to_string_lossy(),
]) ])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
print!("{:?}", String::from_utf8(output.stderr).unwrap()); print!("{:?}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFprobe Crashed")); return Err(static_err("FFprobe Crashed"));
} }
let output_str = String::from_utf8(output.stdout).unwrap(); let output_str = String::from_utf8(output.stdout).unwrap();
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?; 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>> { fn ffprobe_duration_to_ms(duration: String) -> Result<u64, Box<dyn std::error::Error>> {
let fields: Vec<&str> = duration.split(':').collect(); let fields: Vec<&str> = duration.split(':').collect();
let mut duration = Duration::from_nanos(0); let mut duration = Duration::from_nanos(0);
let hours = fields[0].parse::<u64>()?; let hours = fields[0].parse::<u64>()?;
duration += Duration::from_secs(hours * 60 * 60); duration += Duration::from_secs(hours * 60 * 60);
let minutes = fields[1].parse::<u64>()?; let minutes = fields[1].parse::<u64>()?;
duration += Duration::from_secs(minutes * 60); duration += Duration::from_secs(minutes * 60);
let seconds = fields[1].parse::<f64>()?; let seconds = fields[1].parse::<f64>()?;
duration += Duration::from_millis((seconds * 1000.0) as u64); 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( pub fn progress_monitor(
source_filepath: PathBuf, source_filepath: PathBuf,
sender_base: &Sender<String>, sender_base: &Sender<String>,
) -> Result<(String, JoinHandle<()>), Box<dyn std::error::Error>> { ) -> 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 tempdir = tempfile::tempdir()?;
let file_path = tempdir.path().join("progress.log"); let file_path = tempdir.path().join("progress.log");
let file_path_string = file_path.to_str().unwrap().to_string(); let file_path_string = file_path.to_str().unwrap().to_string();
fs::File::create(&file_path)?; fs::File::create(&file_path)?;
let sender = sender_base.clone(); let sender = sender_base.clone();
let child = thread::spawn(move || { let child = thread::spawn(move || {
let _ = &tempdir; let _ = &tempdir;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new( let mut watcher = RecommendedWatcher::new(
tx, tx,
notify::Config::default().with_poll_interval(Duration::from_millis(100)), notify::Config::default().with_poll_interval(Duration::from_millis(100)),
) )
.expect("could not watch for ffmpeg log progress status"); .expect("could not watch for ffmpeg log progress status");
watcher watcher
.watch(&file_path, RecursiveMode::NonRecursive) .watch(&file_path, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
let mut pos = 0; let mut pos = 0;
'outer: for res in rx { 'outer: for res in rx {
if res.is_err() { if res.is_err() {
break 'outer; break 'outer;
} }
let res = res.unwrap(); let res = res.unwrap();
match res.kind { match res.kind {
EventKind::Modify(_) => { EventKind::Modify(_) => {
let mut file = fs::File::open(&file_path).unwrap(); let mut file = fs::File::open(&file_path).unwrap();
file.seek(SeekFrom::Start(pos)).unwrap(); file.seek(SeekFrom::Start(pos)).unwrap();
pos = file.metadata().unwrap().len(); pos = file.metadata().unwrap().len();
let reader = BufReader::new(file); let reader = BufReader::new(file);
for line in reader.lines() { for line in reader.lines() {
let ln = line.unwrap(); let ln = line.unwrap();
if ln == "progress=end" { if ln == "progress=end" {
break 'outer; break 'outer;
} }
if ln.starts_with("out_time=") { if ln.starts_with("out_time=") {
let out_time = ln.strip_prefix("out_time=").unwrap().to_string(); let out_time = ln.strip_prefix("out_time=").unwrap().to_string();
let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap(); let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap();
if sender if sender
.send(format!( .send(format!(
"{:.2}%", "{:.2}%",
((out_time_ms as f64 / total_length_millis as f64) * 100.0) ((out_time_ms as f64 / total_length_millis as f64) * 100.0)
)) ))
.is_err() .is_err()
{ {
break 'outer; break 'outer;
}; };
} }
} }
} }
EventKind::Remove(_) => 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}; use super::{progress_monitor, types::TranscodeConfig};
pub fn transcode( pub fn transcode(
file: File, file: File,
dest: String, dest: String,
config: &TranscodeConfig, config: &TranscodeConfig,
progress_sender: Option<Sender<String>>, progress_sender: Option<Sender<String>>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut command_args: Vec<String> = Vec::new(); let mut command_args: Vec<String> = Vec::new();
command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]); command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]);
command_args.extend(vec![ command_args.extend(vec![
"-i".to_string(), "-i".to_string(),
file.join_path_to().to_string_lossy().to_string(), file.join_path_to().to_string_lossy().to_string(),
]); ]);
if let Some(encoder) = &config.encoder { if let Some(encoder) = &config.encoder {
command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]); command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
} }
if let Some(container) = &config.container { if let Some(container) = &config.container {
command_args.extend(vec!["-f".to_string(), container.to_string()]); command_args.extend(vec!["-f".to_string(), container.to_string()]);
} }
if let Some(sample_rate) = &config.sample_rate { if let Some(sample_rate) = &config.sample_rate {
command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]); command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
} }
if let Some(channels) = &config.channels { if let Some(channels) = &config.channels {
command_args.extend(vec!["-ac".to_string(), channels.to_string()]); command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
} }
if let Some(quality) = &config.quality { if let Some(quality) = &config.quality {
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]); command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
} }
if let Some(bitrate) = &config.bitrate { if let Some(bitrate) = &config.bitrate {
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]); 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_thread: Option<JoinHandle<()>> = None;
let mut progress_file: Option<String> = None; let mut progress_file: Option<String> = None;
if let Some(sender) = &progress_sender { if let Some(sender) = &progress_sender {
let result = progress_monitor(file.join_path_to(), sender); let result = progress_monitor(file.join_path_to(), sender);
if let Ok(result) = result { if let Ok(result) = result {
progress_thread = Some(result.1); progress_thread = Some(result.1);
progress_file = Some(result.0.clone()); progress_file = Some(result.0.clone());
command_args.extend(vec![ command_args.extend(vec![
"-progress".to_string(), "-progress".to_string(),
result.0, result.0,
"-nostats".to_string(), "-nostats".to_string(),
]); ]);
} }
} }
let output = Command::new(crate::meta::FFMPEG) let output = Command::new(crate::meta::FFMPEG)
.args(command_args) .args(command_args)
.output() .output()
.expect("failed to execute process"); .expect("failed to execute process");
if let Some(sender) = progress_sender { if let Some(sender) = progress_sender {
drop(sender); drop(sender);
} }
if let Some(thread) = progress_thread { if let Some(thread) = progress_thread {
fs::remove_file(progress_file.unwrap())?; fs::remove_file(progress_file.unwrap())?;
thread.join().expect("thread couldn't join"); thread.join().expect("thread couldn't join");
} }
if !output.status.success() { if !output.status.success() {
print!("{}", String::from_utf8(output.stderr).unwrap()); print!("{}", String::from_utf8(output.stderr).unwrap());
return Err(static_err("FFmpeg Crashed")); return Err(static_err("FFmpeg Crashed"));
} }
Ok(()) Ok(())
} }

View file

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