change default formatting to use tabs instead of spaces
This commit is contained in:
parent
1dc74c2cb0
commit
2f5f493f9b
2
.rustfmt.toml
Normal file
2
.rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
hard_tabs = true
|
||||
use_field_init_shorthand = true
|
|
@ -2,35 +2,35 @@ use std::env;
|
|||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/wrapper.h");
|
||||
println!("cargo:rerun-if-changed=src/wrapper.cxx");
|
||||
println!("cargo:rerun-if-changed=src/wrapper.h");
|
||||
println!("cargo:rerun-if-changed=src/wrapper.cxx");
|
||||
|
||||
let taglib = pkg_config::Config::new().probe("taglib").unwrap();
|
||||
let taglib = pkg_config::Config::new().probe("taglib").unwrap();
|
||||
|
||||
let mut cc_builder = cc::Build::new();
|
||||
let mut cc_builder = cc::Build::new();
|
||||
|
||||
cc_builder
|
||||
.file("src/wrapper.cxx")
|
||||
.flag("-std=c++2b")
|
||||
.flag("-Og")
|
||||
.include("src")
|
||||
.cpp(true);
|
||||
cc_builder
|
||||
.file("src/wrapper.cxx")
|
||||
.flag("-std=c++2b")
|
||||
.flag("-Og")
|
||||
.include("src")
|
||||
.cpp(true);
|
||||
|
||||
for include in taglib.include_paths {
|
||||
cc_builder.include(include);
|
||||
}
|
||||
for include in taglib.include_paths {
|
||||
cc_builder.include(include);
|
||||
}
|
||||
|
||||
cc_builder.compile("wrapper");
|
||||
cc_builder.compile("wrapper");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/wrapper.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/wrapper.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ use thiserror::Error;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TagLibError {
|
||||
#[error("could not save file")]
|
||||
SaveError,
|
||||
#[error("invalid file")]
|
||||
InvalidFile,
|
||||
#[error("metadata unavailable")]
|
||||
MetadataUnavailable,
|
||||
#[error("could not save file")]
|
||||
SaveError,
|
||||
#[error("invalid file")]
|
||||
InvalidFile,
|
||||
#[error("metadata unavailable")]
|
||||
MetadataUnavailable,
|
||||
}
|
||||
|
|
|
@ -5,83 +5,83 @@ use crate::{bindings, errors::TagLibError, traits::File, TagLibFileType};
|
|||
use super::{oggtag::TagLibOggTag, tag::TagLibTag};
|
||||
|
||||
pub struct TagLibFile {
|
||||
ctx: *mut bindings::TagLib_File,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
ctx: *mut bindings::TagLib_File,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
}
|
||||
|
||||
pub fn new_taglib_file(
|
||||
filepath: String,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
filepath: String,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
) -> Result<TagLibFile, TagLibError> {
|
||||
let filename_c = CString::new(filepath).unwrap();
|
||||
let filename_c_ptr = filename_c.as_ptr();
|
||||
let filename_c = CString::new(filepath).unwrap();
|
||||
let filename_c_ptr = filename_c.as_ptr();
|
||||
|
||||
let file = unsafe {
|
||||
if let Some(taglib_type) = taglib_type {
|
||||
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
|
||||
} else {
|
||||
bindings::wrap_taglib_file_new(filename_c_ptr)
|
||||
}
|
||||
};
|
||||
let file = unsafe {
|
||||
if let Some(taglib_type) = taglib_type {
|
||||
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
|
||||
} else {
|
||||
bindings::wrap_taglib_file_new(filename_c_ptr)
|
||||
}
|
||||
};
|
||||
|
||||
if file.is_null() {
|
||||
return Err(TagLibError::InvalidFile);
|
||||
}
|
||||
if file.is_null() {
|
||||
return Err(TagLibError::InvalidFile);
|
||||
}
|
||||
|
||||
Ok(TagLibFile {
|
||||
ctx: file,
|
||||
taglib_type,
|
||||
})
|
||||
Ok(TagLibFile {
|
||||
ctx: file,
|
||||
taglib_type,
|
||||
})
|
||||
}
|
||||
|
||||
impl TagLibFile {
|
||||
pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
|
||||
Ok(TagLibTag { ctx: tag })
|
||||
}
|
||||
Ok(TagLibTag { ctx: tag })
|
||||
}
|
||||
|
||||
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
|
||||
if let Some(taglib_type) = &self.taglib_type {
|
||||
let supported = match taglib_type {
|
||||
TagLibFileType::OggFLAC
|
||||
| TagLibFileType::OggOpus
|
||||
| TagLibFileType::OggSpeex
|
||||
| TagLibFileType::OggVorbis => true,
|
||||
};
|
||||
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
|
||||
if let Some(taglib_type) = &self.taglib_type {
|
||||
let supported = match taglib_type {
|
||||
TagLibFileType::OggFLAC
|
||||
| TagLibFileType::OggOpus
|
||||
| TagLibFileType::OggSpeex
|
||||
| TagLibFileType::OggVorbis => true,
|
||||
};
|
||||
|
||||
if !supported {
|
||||
panic!("ogg tag not supported")
|
||||
}
|
||||
}
|
||||
if !supported {
|
||||
panic!("ogg tag not supported")
|
||||
}
|
||||
}
|
||||
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
|
||||
Ok(TagLibOggTag { ctx: tag })
|
||||
}
|
||||
Ok(TagLibOggTag { ctx: tag })
|
||||
}
|
||||
}
|
||||
|
||||
impl File for TagLibFile {
|
||||
fn save(&mut self) -> Result<(), TagLibError> {
|
||||
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
|
||||
fn save(&mut self) -> Result<(), TagLibError> {
|
||||
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
|
||||
|
||||
match result {
|
||||
true => Ok(()),
|
||||
false => Err(TagLibError::SaveError),
|
||||
}
|
||||
}
|
||||
match result {
|
||||
true => Ok(()),
|
||||
false => Err(TagLibError::SaveError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TagLibFile {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
bindings::wrap_taglib_file_free(self.ctx);
|
||||
}
|
||||
}
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
bindings::wrap_taglib_file_free(self.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,31 +3,31 @@ use std::ffi::CString;
|
|||
use crate::{bindings, utils::c_str_to_str};
|
||||
|
||||
pub struct TagLibOggTag {
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
}
|
||||
|
||||
impl TagLibOggTag {
|
||||
pub fn get_field(&self, key: String) -> Option<String> {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
|
||||
pub fn get_field(&self, key: String) -> Option<String> {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
|
||||
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
c_str_to_str(value)
|
||||
}
|
||||
}
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
c_str_to_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_field(&self, key: String, value: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = CString::new(value).unwrap();
|
||||
pub fn add_field(&self, key: String, value: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = CString::new(value).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_opustag_add_field(self.ctx, key.as_ptr(), value.as_ptr()) };
|
||||
}
|
||||
unsafe { bindings::wrap_taglib_opustag_add_field(self.ctx, key.as_ptr(), value.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn remove_fields(&self, key: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
pub fn remove_fields(&self, key: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_opustag_remove_fields(self.ctx, key.as_ptr()) };
|
||||
}
|
||||
unsafe { bindings::wrap_taglib_opustag_remove_fields(self.ctx, key.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,28 +3,28 @@ use std::ffi::CString;
|
|||
use crate::{bindings, traits::Tag, utils::c_str_to_str};
|
||||
|
||||
pub struct TagLibTag {
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
}
|
||||
|
||||
impl Tag for TagLibTag {
|
||||
fn title(&self) -> Option<String> {
|
||||
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
|
||||
fn title(&self) -> Option<String> {
|
||||
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
|
||||
|
||||
c_str_to_str(title_ref)
|
||||
}
|
||||
fn set_title(&mut self, title: String) {
|
||||
let title = CString::new(title).unwrap();
|
||||
c_str_to_str(title_ref)
|
||||
}
|
||||
fn set_title(&mut self, title: String) {
|
||||
let title = CString::new(title).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
|
||||
}
|
||||
fn artist(&self) -> Option<String> {
|
||||
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
|
||||
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
|
||||
}
|
||||
fn artist(&self) -> Option<String> {
|
||||
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
|
||||
|
||||
c_str_to_str(artist_ref)
|
||||
}
|
||||
fn set_artist(&mut self, artist: String) {
|
||||
let artist = CString::new(artist).unwrap();
|
||||
c_str_to_str(artist_ref)
|
||||
}
|
||||
fn set_artist(&mut self, artist: String) {
|
||||
let artist = CString::new(artist).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_tag_set_artist(self.ctx, artist.as_ptr()) };
|
||||
}
|
||||
unsafe { bindings::wrap_taglib_tag_set_artist(self.ctx, artist.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,17 @@ pub mod traits;
|
|||
pub(crate) mod utils;
|
||||
|
||||
pub(crate) mod bindings {
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TagLibFileType {
|
||||
OggFLAC = 1,
|
||||
OggOpus = 2,
|
||||
OggSpeex = 3,
|
||||
OggVorbis = 4,
|
||||
OggFLAC = 1,
|
||||
OggOpus = 2,
|
||||
OggSpeex = 3,
|
||||
OggVorbis = 4,
|
||||
}
|
||||
|
||||
pub use impls::file::*;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use crate::errors::TagLibError;
|
||||
|
||||
pub trait File {
|
||||
fn save(&mut self) -> Result<(), TagLibError>;
|
||||
fn save(&mut self) -> Result<(), TagLibError>;
|
||||
}
|
||||
|
||||
pub trait Tag {
|
||||
fn title(&self) -> Option<String>;
|
||||
fn artist(&self) -> Option<String>;
|
||||
fn set_title(&mut self, title: String);
|
||||
fn set_artist(&mut self, artist: String);
|
||||
fn title(&self) -> Option<String>;
|
||||
fn artist(&self) -> Option<String>;
|
||||
fn set_title(&mut self, title: String);
|
||||
fn set_artist(&mut self, artist: String);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
pub fn c_str_to_str(c_str: *const c_char) -> Option<String> {
|
||||
if c_str.is_null() {
|
||||
None
|
||||
} else {
|
||||
let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() };
|
||||
if c_str.is_null() {
|
||||
None
|
||||
} else {
|
||||
let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() };
|
||||
|
||||
if bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(bytes).to_string())
|
||||
}
|
||||
}
|
||||
if bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(bytes).to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
110
src/args.rs
110
src/args.rs
|
@ -3,87 +3,87 @@ use clap::{Args, Parser, Subcommand};
|
|||
#[derive(Debug, Parser)]
|
||||
#[clap()]
|
||||
pub struct CLIArgs {
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands,
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Commands {
|
||||
Process(ProcessCommandArgs),
|
||||
Genhtml(GenHTMLCommandArgs),
|
||||
Transcode(TranscodeCommandArgs),
|
||||
Copy(CopyCommandArgs),
|
||||
SetTags(SetTagsCommandArgs),
|
||||
GetTags(GetTagsCommandArgs),
|
||||
Process(ProcessCommandArgs),
|
||||
Genhtml(GenHTMLCommandArgs),
|
||||
Transcode(TranscodeCommandArgs),
|
||||
Copy(CopyCommandArgs),
|
||||
SetTags(SetTagsCommandArgs),
|
||||
GetTags(GetTagsCommandArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ProcessCommandArgs {
|
||||
pub source: String,
|
||||
#[clap(long)]
|
||||
pub dry_run: bool,
|
||||
#[clap(long)]
|
||||
pub skip_replaygain: bool,
|
||||
#[clap(long)]
|
||||
pub force_replaygain: bool,
|
||||
#[clap(long)]
|
||||
pub replaygain_threads: Option<u32>,
|
||||
pub source: String,
|
||||
#[clap(long)]
|
||||
pub dry_run: bool,
|
||||
#[clap(long)]
|
||||
pub skip_replaygain: bool,
|
||||
#[clap(long)]
|
||||
pub force_replaygain: bool,
|
||||
#[clap(long)]
|
||||
pub replaygain_threads: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct GenHTMLCommandArgs {
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long, default_value = "musicutil")]
|
||||
pub title: String,
|
||||
#[clap(long, default_value = "generated by musicutil")]
|
||||
pub description: String,
|
||||
#[clap(long)]
|
||||
pub link_base: Option<String>,
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long, default_value = "musicutil")]
|
||||
pub title: String,
|
||||
#[clap(long, default_value = "generated by musicutil")]
|
||||
pub description: String,
|
||||
#[clap(long)]
|
||||
pub link_base: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct TranscodeCommandArgs {
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long)]
|
||||
pub ignore_extension: bool,
|
||||
#[clap(long)]
|
||||
pub hide_progress: bool,
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long)]
|
||||
pub ignore_extension: bool,
|
||||
#[clap(long)]
|
||||
pub hide_progress: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CopyCommandArgs {
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long)]
|
||||
pub threads: Option<u32>,
|
||||
#[clap(long)]
|
||||
pub no_skip_existing: bool,
|
||||
#[clap(long)]
|
||||
pub single_directory: bool,
|
||||
pub source: String,
|
||||
pub dest: String,
|
||||
#[clap(long)]
|
||||
pub transcode_preset: Option<String>,
|
||||
#[clap(long)]
|
||||
pub transcode_config: Option<String>,
|
||||
#[clap(long)]
|
||||
pub threads: Option<u32>,
|
||||
#[clap(long)]
|
||||
pub no_skip_existing: bool,
|
||||
#[clap(long)]
|
||||
pub single_directory: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct SetTagsCommandArgs {
|
||||
pub files: Vec<String>,
|
||||
#[clap(long)]
|
||||
pub title: Option<String>,
|
||||
#[clap(long)]
|
||||
pub artist: Option<String>,
|
||||
pub files: Vec<String>,
|
||||
#[clap(long)]
|
||||
pub title: Option<String>,
|
||||
#[clap(long)]
|
||||
pub artist: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct GetTagsCommandArgs {
|
||||
pub files: Vec<String>,
|
||||
#[clap(long)]
|
||||
pub json: bool,
|
||||
pub files: Vec<String>,
|
||||
#[clap(long)]
|
||||
pub json: bool,
|
||||
}
|
||||
|
|
|
@ -1,243 +1,243 @@
|
|||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
thread::scope,
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
thread::scope,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
args::{CLIArgs, CopyCommandArgs},
|
||||
types::File,
|
||||
utils::{
|
||||
formats::get_format_handler,
|
||||
scan_for_music,
|
||||
transcoder::{
|
||||
presets::{print_presets, transcode_preset_or_config},
|
||||
transcode,
|
||||
types::TranscodeConfig,
|
||||
},
|
||||
},
|
||||
args::{CLIArgs, CopyCommandArgs},
|
||||
types::File,
|
||||
utils::{
|
||||
formats::get_format_handler,
|
||||
scan_for_music,
|
||||
transcoder::{
|
||||
presets::{print_presets, transcode_preset_or_config},
|
||||
transcode,
|
||||
types::TranscodeConfig,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn copy_command(
|
||||
_args: CLIArgs,
|
||||
copy_args: &CopyCommandArgs,
|
||||
_args: CLIArgs,
|
||||
copy_args: &CopyCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
|
||||
panic!("Please provide Transcode Preset/Config");
|
||||
}
|
||||
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
|
||||
panic!("Please provide Transcode Preset/Config");
|
||||
}
|
||||
|
||||
if let Some(preset) = ©_args.transcode_preset {
|
||||
if preset == "list" {
|
||||
print_presets();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
if let Some(preset) = ©_args.transcode_preset {
|
||||
if preset == "list" {
|
||||
print_presets();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(©_args.source)?;
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(©_args.source)?;
|
||||
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let mut handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(true)?;
|
||||
}
|
||||
file.info = handler.get_audio_file_info(true)?;
|
||||
}
|
||||
|
||||
if copy_args.single_directory {
|
||||
println!("Checking for Duplicates");
|
||||
let mut seen: HashMap<String, bool> = HashMap::new();
|
||||
let mut dupes: Vec<String> = Vec::new();
|
||||
if copy_args.single_directory {
|
||||
println!("Checking for Duplicates");
|
||||
let mut seen: HashMap<String, bool> = HashMap::new();
|
||||
let mut dupes: Vec<String> = Vec::new();
|
||||
|
||||
for file in files.iter() {
|
||||
let filename = file.join_filename();
|
||||
for file in files.iter() {
|
||||
let filename = file.join_filename();
|
||||
|
||||
if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
|
||||
entry.insert(true);
|
||||
} else {
|
||||
dupes.push(filename);
|
||||
}
|
||||
}
|
||||
if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
|
||||
entry.insert(true);
|
||||
} else {
|
||||
dupes.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if !dupes.is_empty() {
|
||||
panic!("Duplicates Found: {}", dupes.join(","))
|
||||
}
|
||||
}
|
||||
if !dupes.is_empty() {
|
||||
panic!("Duplicates Found: {}", dupes.join(","))
|
||||
}
|
||||
}
|
||||
|
||||
if !copy_args.single_directory {
|
||||
println!("Creating Directories");
|
||||
if !copy_args.single_directory {
|
||||
println!("Creating Directories");
|
||||
|
||||
let mut directories: Vec<String> = Vec::new();
|
||||
for file in files.iter() {
|
||||
let file_directory = file.path_from_source.to_string_lossy().to_string();
|
||||
let mut directories: Vec<String> = Vec::new();
|
||||
for file in files.iter() {
|
||||
let file_directory = file.path_from_source.to_string_lossy().to_string();
|
||||
|
||||
if !directories.contains(&file_directory) {
|
||||
directories.push(file_directory.clone());
|
||||
}
|
||||
}
|
||||
for directory in directories.iter() {
|
||||
fs::create_dir_all(
|
||||
PathBuf::from_str(copy_args.dest.as_str())
|
||||
.expect("invalid destination")
|
||||
.join(directory),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if !directories.contains(&file_directory) {
|
||||
directories.push(file_directory.clone());
|
||||
}
|
||||
}
|
||||
for directory in directories.iter() {
|
||||
fs::create_dir_all(
|
||||
PathBuf::from_str(copy_args.dest.as_str())
|
||||
.expect("invalid destination")
|
||||
.join(directory),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
if copy_args
|
||||
.transcode_preset
|
||||
.as_ref()
|
||||
.unwrap_or(&"".to_string())
|
||||
== "copy"
|
||||
{
|
||||
println!("Copying Files Into Dest");
|
||||
copy_files(&files, copy_args)?;
|
||||
} else {
|
||||
println!("Transcoding Files");
|
||||
transcode_files(&files, copy_args)?;
|
||||
}
|
||||
if copy_args
|
||||
.transcode_preset
|
||||
.as_ref()
|
||||
.unwrap_or(&"".to_string())
|
||||
== "copy"
|
||||
{
|
||||
println!("Copying Files Into Dest");
|
||||
copy_files(&files, copy_args)?;
|
||||
} else {
|
||||
println!("Transcoding Files");
|
||||
transcode_files(&files, copy_args)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let from_path = file.join_path_to();
|
||||
let from_path = file.join_path_to();
|
||||
|
||||
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
|
||||
let to_path = match copy_args.single_directory {
|
||||
true => to_path_dest.join(file.join_filename()),
|
||||
false => to_path_dest
|
||||
.join(file.path_from_source.clone())
|
||||
.join(file.join_filename()),
|
||||
};
|
||||
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
|
||||
let to_path = match copy_args.single_directory {
|
||||
true => to_path_dest.join(file.join_filename()),
|
||||
false => to_path_dest
|
||||
.join(file.path_from_source.clone())
|
||||
.join(file.join_filename()),
|
||||
};
|
||||
|
||||
let to_path_string = to_path.to_string_lossy();
|
||||
let to_path_string = to_path.to_string_lossy();
|
||||
|
||||
if !copy_args.no_skip_existing && to_path.exists() {
|
||||
println!(
|
||||
"Skipping {} as already exists in destination",
|
||||
to_path_string
|
||||
);
|
||||
} else {
|
||||
println!("Copying {:?} to {}", from_path, to_path_string);
|
||||
fs::copy(from_path, to_path)?;
|
||||
}
|
||||
Ok(())
|
||||
if !copy_args.no_skip_existing && to_path.exists() {
|
||||
println!(
|
||||
"Skipping {} as already exists in destination",
|
||||
to_path_string
|
||||
);
|
||||
} else {
|
||||
println!("Copying {:?} to {}", from_path, to_path_string);
|
||||
fs::copy(from_path, to_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_files(
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for file in files.iter() {
|
||||
copy_file(file, copy_args)?;
|
||||
for file in files.iter() {
|
||||
copy_file(file, copy_args)?;
|
||||
|
||||
if !file.extra_files.is_empty() {
|
||||
for extra_file in file.extra_files.iter() {
|
||||
copy_file(extra_file, copy_args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !file.extra_files.is_empty() {
|
||||
for extra_file in file.extra_files.iter() {
|
||||
copy_file(extra_file, copy_args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transcode_file(
|
||||
file: &File,
|
||||
copy_args: &CopyCommandArgs,
|
||||
config: &TranscodeConfig,
|
||||
is_threaded: bool,
|
||||
file: &File,
|
||||
copy_args: &CopyCommandArgs,
|
||||
config: &TranscodeConfig,
|
||||
is_threaded: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let new_filename_full: String = match config.file_extension.clone() {
|
||||
Some(ext) => format!("{}.{}", file.filename, ext),
|
||||
None => {
|
||||
panic!("file_extension is required in custom transcode configs");
|
||||
}
|
||||
};
|
||||
let new_filename_full: String = match config.file_extension.clone() {
|
||||
Some(ext) => format!("{}.{}", file.filename, ext),
|
||||
None => {
|
||||
panic!("file_extension is required in custom transcode configs");
|
||||
}
|
||||
};
|
||||
|
||||
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
|
||||
let to_path = match copy_args.single_directory {
|
||||
true => to_path_dest.join(new_filename_full),
|
||||
false => to_path_dest
|
||||
.join(file.path_from_source.clone())
|
||||
.join(new_filename_full),
|
||||
};
|
||||
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
|
||||
let to_path = match copy_args.single_directory {
|
||||
true => to_path_dest.join(new_filename_full),
|
||||
false => to_path_dest
|
||||
.join(file.path_from_source.clone())
|
||||
.join(new_filename_full),
|
||||
};
|
||||
|
||||
let to_path_string = to_path.to_string_lossy();
|
||||
let to_path_string = to_path.to_string_lossy();
|
||||
|
||||
if !file.extra_files.is_empty() {
|
||||
for extra_file in file.extra_files.iter() {
|
||||
copy_file(extra_file, copy_args)?;
|
||||
}
|
||||
}
|
||||
if !file.extra_files.is_empty() {
|
||||
for extra_file in file.extra_files.iter() {
|
||||
copy_file(extra_file, copy_args)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !copy_args.no_skip_existing && to_path.exists() {
|
||||
println!(
|
||||
"Skipping transcode for {} as file already exists",
|
||||
to_path_string
|
||||
);
|
||||
if !copy_args.no_skip_existing && to_path.exists() {
|
||||
println!(
|
||||
"Skipping transcode for {} as file already exists",
|
||||
to_path_string
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Transcoding {}", to_path_string);
|
||||
println!("Transcoding {}", to_path_string);
|
||||
|
||||
transcode(file.to_owned(), to_path_string.to_string(), config, None)?;
|
||||
transcode(file.to_owned(), to_path_string.to_string(), config, None)?;
|
||||
|
||||
if is_threaded {
|
||||
println!("Finished Transcoding {}", to_path_string);
|
||||
}
|
||||
if is_threaded {
|
||||
println!("Finished Transcoding {}", to_path_string);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transcode_files(
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
files: &[File],
|
||||
copy_args: &CopyCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let transcode_config = transcode_preset_or_config(
|
||||
copy_args.transcode_preset.as_ref(),
|
||||
copy_args.transcode_config.as_ref(),
|
||||
)
|
||||
.expect("transcode config error");
|
||||
let transcode_config = transcode_preset_or_config(
|
||||
copy_args.transcode_preset.as_ref(),
|
||||
copy_args.transcode_config.as_ref(),
|
||||
)
|
||||
.expect("transcode config error");
|
||||
|
||||
let threads = copy_args.threads.unwrap_or(1);
|
||||
let threads = copy_args.threads.unwrap_or(1);
|
||||
|
||||
if threads > 1 {
|
||||
let files_copy = files.to_vec();
|
||||
if threads > 1 {
|
||||
let files_copy = files.to_vec();
|
||||
|
||||
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
|
||||
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
|
||||
|
||||
let copy_args = Arc::new(copy_args);
|
||||
let transcode_config = Arc::new(transcode_config);
|
||||
let copy_args = Arc::new(copy_args);
|
||||
let transcode_config = Arc::new(transcode_config);
|
||||
|
||||
scope(|s| {
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| loop {
|
||||
let mut jobs = jobs.lock().unwrap();
|
||||
scope(|s| {
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| loop {
|
||||
let mut jobs = jobs.lock().unwrap();
|
||||
|
||||
let job = jobs.pop();
|
||||
if let Some(job) = job {
|
||||
let result = transcode_file(&job, ©_args, &transcode_config, true);
|
||||
if let Err(err) = result {
|
||||
panic!("Error Transcoding: {}", err)
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for file in files.iter() {
|
||||
transcode_file(file, copy_args, &transcode_config, false)?;
|
||||
}
|
||||
}
|
||||
let job = jobs.pop();
|
||||
if let Some(job) = job {
|
||||
let result = transcode_file(&job, ©_args, &transcode_config, true);
|
||||
if let Err(err) = result {
|
||||
panic!("Error Transcoding: {}", err)
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for file in files.iter() {
|
||||
transcode_file(file, copy_args, &transcode_config, false)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -11,21 +11,21 @@ use html_escape::encode_text;
|
|||
use urlencoding::encode as url_encode;
|
||||
|
||||
fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<String>) -> String {
|
||||
let mut html_content = String::new();
|
||||
let mut html_content = String::new();
|
||||
|
||||
let mut path_head = String::new();
|
||||
if includes_path {
|
||||
path_head.push_str("<th>Path</th>")
|
||||
}
|
||||
let mut path_head = String::new();
|
||||
if includes_path {
|
||||
path_head.push_str("<th>Path</th>")
|
||||
}
|
||||
|
||||
let mut link_head = String::new();
|
||||
if link_base.is_some() {
|
||||
link_head.push_str("<th>Link</th>")
|
||||
}
|
||||
let mut link_head = String::new();
|
||||
if link_base.is_some() {
|
||||
link_head.push_str("<th>Link</th>")
|
||||
}
|
||||
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
<table class=\"pure-table pure-table-horizontal\">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -38,69 +38,69 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
|
|||
</thead>
|
||||
<tbody>
|
||||
",
|
||||
link_head, path_head
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
link_head, path_head
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let mut is_odd = true;
|
||||
for file in files.iter() {
|
||||
let td_class = match is_odd {
|
||||
true => "pure-table-odd",
|
||||
false => "pure-table-even",
|
||||
};
|
||||
let mut is_odd = true;
|
||||
for file in files.iter() {
|
||||
let td_class = match is_odd {
|
||||
true => "pure-table-odd",
|
||||
false => "pure-table-even",
|
||||
};
|
||||
|
||||
let data_title = encode_text(&file.info.tags.title);
|
||||
let data_artist = encode_text(&file.info.tags.artist);
|
||||
let data_title = encode_text(&file.info.tags.title);
|
||||
let data_artist = encode_text(&file.info.tags.artist);
|
||||
|
||||
let format = if let Some(format) = &file.info.format {
|
||||
format.to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
let format = if let Some(format) = &file.info.format {
|
||||
format.to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
|
||||
let data_format = encode_text(&format);
|
||||
let data_format = encode_text(&format);
|
||||
|
||||
let mut path_data = String::new();
|
||||
if includes_path {
|
||||
let file_directory = file.path_from_source.to_string_lossy().to_string();
|
||||
let mut path_data = String::new();
|
||||
if includes_path {
|
||||
let file_directory = file.path_from_source.to_string_lossy().to_string();
|
||||
|
||||
path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str());
|
||||
}
|
||||
path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str());
|
||||
}
|
||||
|
||||
let mut url_data = String::new();
|
||||
if let Some(link_base) = &link_base {
|
||||
let mut url = String::new();
|
||||
let mut url_data = String::new();
|
||||
if let Some(link_base) = &link_base {
|
||||
let mut url = String::new();
|
||||
|
||||
url.push_str(link_base.as_str());
|
||||
url.push('/');
|
||||
url.push_str(link_base.as_str());
|
||||
url.push('/');
|
||||
|
||||
let file_path = file.join_path_from_source();
|
||||
let file_path: Vec<&OsStr> = file_path.iter().collect();
|
||||
let file_path = file.join_path_from_source();
|
||||
let file_path: Vec<&OsStr> = file_path.iter().collect();
|
||||
|
||||
for i in 0..(file_path.len()) {
|
||||
let file_path_element = file_path.get(i).unwrap();
|
||||
for i in 0..(file_path.len()) {
|
||||
let file_path_element = file_path.get(i).unwrap();
|
||||
|
||||
url.push_str(
|
||||
url_encode(
|
||||
file_path_element
|
||||
.to_str()
|
||||
.expect("invalid character in filename"),
|
||||
)
|
||||
.to_string()
|
||||
.as_str(),
|
||||
);
|
||||
if i != file_path.len() - 1 {
|
||||
url.push('/');
|
||||
}
|
||||
}
|
||||
url.push_str(
|
||||
url_encode(
|
||||
file_path_element
|
||||
.to_str()
|
||||
.expect("invalid character in filename"),
|
||||
)
|
||||
.to_string()
|
||||
.as_str(),
|
||||
);
|
||||
if i != file_path.len() - 1 {
|
||||
url.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
url_data.push_str(format!("<td><a href=\"{}\">🔗</a></td>", url).as_str());
|
||||
}
|
||||
url_data.push_str(format!("<td><a href=\"{}\">🔗</a></td>", url).as_str());
|
||||
}
|
||||
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
<tr class=\"{}\">
|
||||
{}
|
||||
{}
|
||||
|
@ -109,58 +109,58 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
|
|||
<td>{}</td>
|
||||
</tr>
|
||||
",
|
||||
td_class, url_data, path_data, data_title, data_artist, data_format
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
is_odd = !is_odd;
|
||||
}
|
||||
td_class, url_data, path_data, data_title, data_artist, data_format
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
is_odd = !is_odd;
|
||||
}
|
||||
|
||||
html_content.push_str(
|
||||
"
|
||||
html_content.push_str(
|
||||
"
|
||||
</tbody>
|
||||
</table>
|
||||
",
|
||||
);
|
||||
);
|
||||
|
||||
html_content
|
||||
html_content
|
||||
}
|
||||
|
||||
pub fn genhtml_command(
|
||||
_args: CLIArgs,
|
||||
genhtml_args: &GenHTMLCommandArgs,
|
||||
_args: CLIArgs,
|
||||
genhtml_args: &GenHTMLCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&genhtml_args.source)?;
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&genhtml_args.source)?;
|
||||
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let mut handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
||||
files.sort_by(|a, b| -> Ordering {
|
||||
if a.path_from_source != b.path_from_source {
|
||||
return a.path_from_source.cmp(&b.path_from_source);
|
||||
}
|
||||
files.sort_by(|a, b| -> Ordering {
|
||||
if a.path_from_source != b.path_from_source {
|
||||
return a.path_from_source.cmp(&b.path_from_source);
|
||||
}
|
||||
|
||||
let a_tags = &a.info.tags;
|
||||
let b_tags = &b.info.tags;
|
||||
let a_tags = &a.info.tags;
|
||||
let b_tags = &b.info.tags;
|
||||
|
||||
if a_tags.title != b_tags.title {
|
||||
return a_tags.title.cmp(&b_tags.title);
|
||||
}
|
||||
if a_tags.title != b_tags.title {
|
||||
return a_tags.title.cmp(&b_tags.title);
|
||||
}
|
||||
|
||||
a_tags.artist.cmp(&b_tags.artist)
|
||||
});
|
||||
a_tags.artist.cmp(&b_tags.artist)
|
||||
});
|
||||
|
||||
let mut html_content = String::new();
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
let mut html_content = String::new();
|
||||
html_content.push_str(
|
||||
format!(
|
||||
"
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
|
@ -173,30 +173,30 @@ pub fn genhtml_command(
|
|||
</head>
|
||||
<body>
|
||||
",
|
||||
encode_text(&genhtml_args.title),
|
||||
encode_text(&genhtml_args.title),
|
||||
encode_text(&genhtml_args.description)
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
encode_text(&genhtml_args.title),
|
||||
encode_text(&genhtml_args.title),
|
||||
encode_text(&genhtml_args.description)
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
|
||||
html_content.push_str("</body></html>");
|
||||
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
|
||||
html_content.push_str("</body></html>");
|
||||
|
||||
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html");
|
||||
let html_index_file = std::fs::File::create(file_path);
|
||||
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html");
|
||||
let html_index_file = std::fs::File::create(file_path);
|
||||
|
||||
match html_index_file {
|
||||
Ok(mut file) => match file.write_all(html_content.as_bytes()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic!("Could not write HTML file: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("Could not create HTML file: {}", e);
|
||||
}
|
||||
}
|
||||
match html_index_file {
|
||||
Ok(mut file) => match file.write_all(html_content.as_bytes()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
panic!("Could not write HTML file: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("Could not create HTML file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,62 +10,62 @@ use crate::utils::formats::get_format_handler;
|
|||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct Tags {
|
||||
title: String,
|
||||
artist: String,
|
||||
title: String,
|
||||
artist: String,
|
||||
}
|
||||
|
||||
fn from_main_tags(tags: &crate::types::Tags) -> Tags {
|
||||
Tags {
|
||||
title: tags.title.clone(),
|
||||
artist: tags.artist.clone(),
|
||||
}
|
||||
Tags {
|
||||
title: tags.title.clone(),
|
||||
artist: tags.artist.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tags_command(
|
||||
_args: CLIArgs,
|
||||
get_tags_args: &GetTagsCommandArgs,
|
||||
_args: CLIArgs,
|
||||
get_tags_args: &GetTagsCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
|
||||
for file in get_tags_args.files.iter() {
|
||||
files.push(File::from_path("".to_string(), PathBuf::from(file)));
|
||||
}
|
||||
for file in get_tags_args.files.iter() {
|
||||
files.push(File::from_path("".to_string(), PathBuf::from(file)));
|
||||
}
|
||||
|
||||
for file in files.iter_mut() {
|
||||
let handler = get_format_handler(file)?;
|
||||
for file in files.iter_mut() {
|
||||
let handler = get_format_handler(file)?;
|
||||
|
||||
file.info.tags = handler.get_tags(true)?;
|
||||
}
|
||||
file.info.tags = handler.get_tags(true)?;
|
||||
}
|
||||
|
||||
if files.len() == 1 {
|
||||
let file = files.first().unwrap();
|
||||
if files.len() == 1 {
|
||||
let file = files.first().unwrap();
|
||||
|
||||
if get_tags_args.json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
|
||||
);
|
||||
} else {
|
||||
println!("{:#?}", from_main_tags(&file.info.tags));
|
||||
}
|
||||
} else if get_tags_args.json {
|
||||
let mut result: HashMap<String, Tags> = HashMap::new();
|
||||
for file in files.iter() {
|
||||
result.insert(
|
||||
file.join_path_to().to_string_lossy().to_string(),
|
||||
from_main_tags(&file.info.tags),
|
||||
);
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
} else {
|
||||
for file in files.iter() {
|
||||
println!(
|
||||
"{}: {:#?}",
|
||||
file.join_path_to().to_string_lossy(),
|
||||
from_main_tags(&file.info.tags)
|
||||
);
|
||||
}
|
||||
}
|
||||
if get_tags_args.json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
|
||||
);
|
||||
} else {
|
||||
println!("{:#?}", from_main_tags(&file.info.tags));
|
||||
}
|
||||
} else if get_tags_args.json {
|
||||
let mut result: HashMap<String, Tags> = HashMap::new();
|
||||
for file in files.iter() {
|
||||
result.insert(
|
||||
file.join_path_to().to_string_lossy().to_string(),
|
||||
from_main_tags(&file.info.tags),
|
||||
);
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
} else {
|
||||
for file in files.iter() {
|
||||
println!(
|
||||
"{}: {:#?}",
|
||||
file.join_path_to().to_string_lossy(),
|
||||
from_main_tags(&file.info.tags)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -11,184 +11,184 @@ use crate::utils::replaygain::analyze_replaygain;
|
|||
use crate::utils::scan_for_music;
|
||||
|
||||
fn rename_file(process_args: &ProcessCommandArgs, file: &mut File) {
|
||||
let title = &file.info.tags.title;
|
||||
let artist = &file.info.tags.artist;
|
||||
let title = &file.info.tags.title;
|
||||
let artist = &file.info.tags.artist;
|
||||
|
||||
let replace_char = "_".to_string();
|
||||
let replace_char = "_".to_string();
|
||||
|
||||
// Step 1: Remove Newlines
|
||||
let title = title.replace('\n', "");
|
||||
let artist = artist.replace('\n', "");
|
||||
// Step 1: Remove Newlines
|
||||
let title = title.replace('\n', "");
|
||||
let artist = artist.replace('\n', "");
|
||||
|
||||
// Step 2: Strip ASCII
|
||||
let title = reduce_to_ascii(title);
|
||||
let artist = reduce_to_ascii(artist);
|
||||
// Step 2: Strip ASCII
|
||||
let title = reduce_to_ascii(title);
|
||||
let artist = reduce_to_ascii(artist);
|
||||
|
||||
// Step 3: Remove File Seperators
|
||||
let title = title.replace('\\', &replace_char);
|
||||
let title = title.replace('/', &replace_char);
|
||||
let artist = artist.replace('\\', &replace_char);
|
||||
let artist = artist.replace('/', &replace_char);
|
||||
// Step 3: Remove File Seperators
|
||||
let title = title.replace('\\', &replace_char);
|
||||
let title = title.replace('/', &replace_char);
|
||||
let artist = artist.replace('\\', &replace_char);
|
||||
let artist = artist.replace('/', &replace_char);
|
||||
|
||||
// Step 4: Join Filename
|
||||
let filename = format!("{} - {}", artist, title);
|
||||
// Step 4: Join Filename
|
||||
let filename = format!("{} - {}", artist, title);
|
||||
|
||||
if filename == file.filename {
|
||||
return;
|
||||
}
|
||||
if filename == file.filename {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Make new File with filename set
|
||||
let mut new_file = file.clone();
|
||||
new_file.filename = filename.clone();
|
||||
// Step 5: Make new File with filename set
|
||||
let mut new_file = file.clone();
|
||||
new_file.filename = filename.clone();
|
||||
|
||||
let extension = &file.extension;
|
||||
let extension = &file.extension;
|
||||
|
||||
// Step 6: Rename File
|
||||
println!(
|
||||
"Renaming File from {}.{extension} to {}.{extension}",
|
||||
file.filename, filename
|
||||
);
|
||||
if !process_args.dry_run {
|
||||
if std::path::Path::new(&new_file.join_path_to()).exists() {
|
||||
panic!(
|
||||
"Refusing to rename {} to {}, please retag to be distinct",
|
||||
&file.join_filename(),
|
||||
&new_file.join_filename()
|
||||
);
|
||||
} else {
|
||||
let err = std::fs::rename(&file.join_path_to(), &new_file.join_path_to());
|
||||
if err.is_err() {
|
||||
panic!("Could not rename {:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 6: Rename File
|
||||
println!(
|
||||
"Renaming File from {}.{extension} to {}.{extension}",
|
||||
file.filename, filename
|
||||
);
|
||||
if !process_args.dry_run {
|
||||
if std::path::Path::new(&new_file.join_path_to()).exists() {
|
||||
panic!(
|
||||
"Refusing to rename {} to {}, please retag to be distinct",
|
||||
&file.join_filename(),
|
||||
&new_file.join_filename()
|
||||
);
|
||||
} else {
|
||||
let err = std::fs::rename(&file.join_path_to(), &new_file.join_path_to());
|
||||
if err.is_err() {
|
||||
panic!("Could not rename {:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !file.extra_files.is_empty() {
|
||||
let mut new_extra_files: Vec<File> = Vec::new();
|
||||
if !file.extra_files.is_empty() {
|
||||
let mut new_extra_files: Vec<File> = Vec::new();
|
||||
|
||||
for extra_file in file.extra_files.iter() {
|
||||
let mut new_extra_file = extra_file.clone();
|
||||
new_extra_file.filename = filename.clone();
|
||||
for extra_file in file.extra_files.iter() {
|
||||
let mut new_extra_file = extra_file.clone();
|
||||
new_extra_file.filename = filename.clone();
|
||||
|
||||
let extra_extension = &extra_file.extension;
|
||||
let extra_extension = &extra_file.extension;
|
||||
|
||||
println!(
|
||||
"Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}",
|
||||
file.filename, filename
|
||||
);
|
||||
println!(
|
||||
"Renaming Extra File from {}.{extra_extension} to {}.{extra_extension}",
|
||||
file.filename, filename
|
||||
);
|
||||
|
||||
if !process_args.dry_run {
|
||||
let err =
|
||||
std::fs::rename(&extra_file.join_path_to(), &new_extra_file.join_path_to());
|
||||
if err.is_err() {
|
||||
panic!("Could not rename {:?}", err)
|
||||
}
|
||||
new_extra_files.push(new_extra_file);
|
||||
}
|
||||
}
|
||||
if !process_args.dry_run {
|
||||
file.extra_files.clear();
|
||||
file.extra_files.extend(new_extra_files);
|
||||
}
|
||||
}
|
||||
if !process_args.dry_run {
|
||||
let err =
|
||||
std::fs::rename(&extra_file.join_path_to(), &new_extra_file.join_path_to());
|
||||
if err.is_err() {
|
||||
panic!("Could not rename {:?}", err)
|
||||
}
|
||||
new_extra_files.push(new_extra_file);
|
||||
}
|
||||
}
|
||||
if !process_args.dry_run {
|
||||
file.extra_files.clear();
|
||||
file.extra_files.extend(new_extra_files);
|
||||
}
|
||||
}
|
||||
|
||||
if !process_args.dry_run {
|
||||
file.filename = filename;
|
||||
}
|
||||
if !process_args.dry_run {
|
||||
file.filename = filename;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !file.info.supports_replaygain {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, not supported",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
if !file.info.supports_replaygain {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, not supported",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if file.info.contains_replaygain && !force {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, contains already",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if file.info.contains_replaygain && !force {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, contains already",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!(
|
||||
"Analyzing replaygain for {:?}",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
println!(
|
||||
"Analyzing replaygain for {:?}",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
|
||||
let replaygain_data = analyze_replaygain(file.join_path_to())?;
|
||||
let replaygain_data = analyze_replaygain(file.join_path_to())?;
|
||||
|
||||
let mut handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
handler.set_replaygain_data(replaygain_data)?;
|
||||
handler.save_changes()?;
|
||||
handler.set_replaygain_data(replaygain_data)?;
|
||||
handler.save_changes()?;
|
||||
|
||||
println!(
|
||||
"Applied replaygain tags for {:?}",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
println!(
|
||||
"Applied replaygain tags for {:?}",
|
||||
file.join_path_from_source()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_command(
|
||||
_args: CLIArgs,
|
||||
process_args: &ProcessCommandArgs,
|
||||
_args: CLIArgs,
|
||||
process_args: &ProcessCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&process_args.source)?;
|
||||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&process_args.source)?;
|
||||
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
println!("Analysing Files");
|
||||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let mut handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
||||
println!("Renaming Files");
|
||||
for file in files.iter_mut() {
|
||||
rename_file(process_args, file);
|
||||
}
|
||||
println!("Renaming Files");
|
||||
for file in files.iter_mut() {
|
||||
rename_file(process_args, file);
|
||||
}
|
||||
|
||||
if !process_args.skip_replaygain && !process_args.dry_run {
|
||||
println!("Adding ReplayGain Tags to Files");
|
||||
if !process_args.skip_replaygain && !process_args.dry_run {
|
||||
println!("Adding ReplayGain Tags to Files");
|
||||
|
||||
let threads = process_args.replaygain_threads.unwrap_or(0);
|
||||
let threads = process_args.replaygain_threads.unwrap_or(0);
|
||||
|
||||
if threads <= 1 {
|
||||
for file in files.iter_mut() {
|
||||
add_replaygain_tags(file, process_args.force_replaygain)?;
|
||||
}
|
||||
if threads <= 1 {
|
||||
for file in files.iter_mut() {
|
||||
add_replaygain_tags(file, process_args.force_replaygain)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
} else {
|
||||
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files));
|
||||
return Ok(());
|
||||
} else {
|
||||
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files));
|
||||
|
||||
scope(|s| {
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| loop {
|
||||
let mut jobs = jobs.lock().unwrap();
|
||||
scope(|s| {
|
||||
for _ in 0..threads {
|
||||
s.spawn(|| loop {
|
||||
let mut jobs = jobs.lock().unwrap();
|
||||
|
||||
let job = jobs.pop();
|
||||
if let Some(job) = job {
|
||||
let result = add_replaygain_tags(&job, process_args.force_replaygain);
|
||||
if let Err(err) = result {
|
||||
panic!("Error doing replaygain: {}", err)
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let job = jobs.pop();
|
||||
if let Some(job) = job {
|
||||
let result = add_replaygain_tags(&job, process_args.force_replaygain);
|
||||
if let Err(err) = result {
|
||||
panic!("Error doing replaygain: {}", err)
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,28 +6,28 @@ use crate::types::File;
|
|||
use crate::utils::formats::get_format_handler;
|
||||
|
||||
pub fn set_tags_command(
|
||||
_args: CLIArgs,
|
||||
add_tags_args: &SetTagsCommandArgs,
|
||||
_args: CLIArgs,
|
||||
add_tags_args: &SetTagsCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
|
||||
for file in add_tags_args.files.iter() {
|
||||
files.push(File::from_path("".to_string(), PathBuf::from(file)));
|
||||
}
|
||||
for file in add_tags_args.files.iter() {
|
||||
files.push(File::from_path("".to_string(), PathBuf::from(file)));
|
||||
}
|
||||
|
||||
for file in files.iter() {
|
||||
let mut handler = get_format_handler(file)?;
|
||||
for file in files.iter() {
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
if let Some(title) = &add_tags_args.title {
|
||||
handler.set_title(title.clone())?;
|
||||
}
|
||||
if let Some(title) = &add_tags_args.title {
|
||||
handler.set_title(title.clone())?;
|
||||
}
|
||||
|
||||
if let Some(artist) = &add_tags_args.artist {
|
||||
handler.set_artist(artist.clone())?;
|
||||
}
|
||||
if let Some(artist) = &add_tags_args.artist {
|
||||
handler.set_artist(artist.clone())?;
|
||||
}
|
||||
|
||||
handler.save_changes()?;
|
||||
}
|
||||
handler.save_changes()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -12,73 +12,73 @@ use crate::utils::transcoder::presets::transcode_preset_or_config;
|
|||
use crate::utils::transcoder::transcode;
|
||||
|
||||
pub fn transcode_command(
|
||||
_args: CLIArgs,
|
||||
transcode_args: &TranscodeCommandArgs,
|
||||
_args: CLIArgs,
|
||||
transcode_args: &TranscodeCommandArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(preset) = &transcode_args.transcode_preset {
|
||||
if preset == "list" {
|
||||
print_presets();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
if let Some(preset) = &transcode_args.transcode_preset {
|
||||
if preset == "list" {
|
||||
print_presets();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
let transcode_config = transcode_preset_or_config(
|
||||
transcode_args.transcode_preset.as_ref(),
|
||||
transcode_args.transcode_config.as_ref(),
|
||||
)
|
||||
.expect("transcode config error");
|
||||
let transcode_config = transcode_preset_or_config(
|
||||
transcode_args.transcode_preset.as_ref(),
|
||||
transcode_args.transcode_config.as_ref(),
|
||||
)
|
||||
.expect("transcode config error");
|
||||
|
||||
println!("Transcoding");
|
||||
println!("Transcoding");
|
||||
|
||||
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
|
||||
let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
|
||||
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
|
||||
let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
|
||||
|
||||
if !transcode_args.ignore_extension {
|
||||
if let Some(ref file_extension) = transcode_config.file_extension {
|
||||
if file_extension != &output_file.extension {
|
||||
panic!(
|
||||
concat!(
|
||||
"{} is not the recommended ",
|
||||
"extension for specified transcode config ",
|
||||
"please change it to {} ",
|
||||
"or run with --ignore-extension"
|
||||
),
|
||||
output_file.extension, file_extension
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !transcode_args.ignore_extension {
|
||||
if let Some(ref file_extension) = transcode_config.file_extension {
|
||||
if file_extension != &output_file.extension {
|
||||
panic!(
|
||||
concat!(
|
||||
"{} is not the recommended ",
|
||||
"extension for specified transcode config ",
|
||||
"please change it to {} ",
|
||||
"or run with --ignore-extension"
|
||||
),
|
||||
output_file.extension, file_extension
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel::<String>();
|
||||
let mut child: Option<JoinHandle<()>> = None;
|
||||
let (tx, rx) = mpsc::channel::<String>();
|
||||
let mut child: Option<JoinHandle<()>> = None;
|
||||
|
||||
if !transcode_args.hide_progress {
|
||||
child = Some(thread::spawn(move || loop {
|
||||
let progress = rx.recv();
|
||||
if !transcode_args.hide_progress {
|
||||
child = Some(thread::spawn(move || loop {
|
||||
let progress = rx.recv();
|
||||
|
||||
if let Ok(progress_str) = progress {
|
||||
println!("Transcode Progress: {}", progress_str);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
if let Ok(progress_str) = progress {
|
||||
println!("Transcode Progress: {}", progress_str);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
transcode(
|
||||
input_file,
|
||||
transcode_args.dest.clone(),
|
||||
&transcode_config,
|
||||
match transcode_args.hide_progress {
|
||||
true => None,
|
||||
false => Some(tx),
|
||||
},
|
||||
)?;
|
||||
transcode(
|
||||
input_file,
|
||||
transcode_args.dest.clone(),
|
||||
&transcode_config,
|
||||
match transcode_args.hide_progress {
|
||||
true => None,
|
||||
false => Some(tx),
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some(child) = child {
|
||||
child.join().expect("oops! the child thread panicked");
|
||||
}
|
||||
if let Some(child) = child {
|
||||
child.join().expect("oops! the child thread panicked");
|
||||
}
|
||||
|
||||
println!("Transcode Finished");
|
||||
println!("Transcode Finished");
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
44
src/main.rs
44
src/main.rs
|
@ -15,28 +15,28 @@ use commands::set_tags::set_tags_command;
|
|||
use commands::transcode::transcode_command;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = CLIArgs::parse();
|
||||
let cli = CLIArgs::parse();
|
||||
|
||||
let command = cli.command.to_owned();
|
||||
let command = cli.command.to_owned();
|
||||
|
||||
match command {
|
||||
Commands::Process(process_args) => {
|
||||
return process_command(cli, &process_args);
|
||||
}
|
||||
Commands::Genhtml(genhtml_args) => {
|
||||
return genhtml_command(cli, &genhtml_args);
|
||||
}
|
||||
Commands::Transcode(transcode_args) => {
|
||||
return transcode_command(cli, &transcode_args);
|
||||
}
|
||||
Commands::Copy(copy_args) => {
|
||||
return copy_command(cli, ©_args);
|
||||
}
|
||||
Commands::SetTags(set_tags_args) => {
|
||||
return set_tags_command(cli, &set_tags_args);
|
||||
}
|
||||
Commands::GetTags(get_tags_args) => {
|
||||
return get_tags_command(cli, &get_tags_args);
|
||||
}
|
||||
}
|
||||
match command {
|
||||
Commands::Process(process_args) => {
|
||||
return process_command(cli, &process_args);
|
||||
}
|
||||
Commands::Genhtml(genhtml_args) => {
|
||||
return genhtml_command(cli, &genhtml_args);
|
||||
}
|
||||
Commands::Transcode(transcode_args) => {
|
||||
return transcode_command(cli, &transcode_args);
|
||||
}
|
||||
Commands::Copy(copy_args) => {
|
||||
return copy_command(cli, ©_args);
|
||||
}
|
||||
Commands::SetTags(set_tags_args) => {
|
||||
return set_tags_command(cli, &set_tags_args);
|
||||
}
|
||||
Commands::GetTags(get_tags_args) => {
|
||||
return get_tags_command(cli, &get_tags_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
148
src/types.rs
148
src/types.rs
|
@ -4,115 +4,115 @@ use crate::utils::format_detection::FileFormat;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tags {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
}
|
||||
|
||||
impl Default for Tags {
|
||||
fn default() -> Self {
|
||||
Tags {
|
||||
title: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
Tags {
|
||||
title: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
pub struct ReplayGainData {
|
||||
pub track_gain: String,
|
||||
pub track_peak: String,
|
||||
pub track_gain: String,
|
||||
pub track_peak: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplayGainRawData {
|
||||
pub track_gain: f64,
|
||||
pub track_peak: f64,
|
||||
pub track_gain: f64,
|
||||
pub track_peak: f64,
|
||||
}
|
||||
|
||||
impl ReplayGainRawData {
|
||||
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
|
||||
if is_ogg_opus {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
|
||||
track_peak: "".to_string(), // Not Required
|
||||
}
|
||||
} else {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.2} dB", self.track_gain),
|
||||
track_peak: format!("{:.6}", self.track_peak),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
|
||||
if is_ogg_opus {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
|
||||
track_peak: "".to_string(), // Not Required
|
||||
}
|
||||
} else {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.2} dB", self.track_gain),
|
||||
track_peak: format!("{:.6}", self.track_peak),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct AudioFileInfo {
|
||||
pub tags: Tags,
|
||||
pub contains_replaygain: bool,
|
||||
pub supports_replaygain: bool,
|
||||
pub format: Option<FileFormat>,
|
||||
pub tags: Tags,
|
||||
pub contains_replaygain: bool,
|
||||
pub supports_replaygain: bool,
|
||||
pub format: Option<FileFormat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct File {
|
||||
pub filename: String,
|
||||
pub extension: String,
|
||||
pub filename: String,
|
||||
pub extension: String,
|
||||
|
||||
// relative path to file's folder
|
||||
pub path_to: PathBuf,
|
||||
// path to folder from source
|
||||
pub path_from_source: PathBuf,
|
||||
// relative path to file's folder
|
||||
pub path_to: PathBuf,
|
||||
// path to folder from source
|
||||
pub path_from_source: PathBuf,
|
||||
|
||||
pub extra_files: Vec<File>,
|
||||
pub extra_files: Vec<File>,
|
||||
|
||||
pub info: AudioFileInfo,
|
||||
pub info: AudioFileInfo,
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
|
||||
let full_file_path = PathBuf::from(&source_dir).join(full_path);
|
||||
pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
|
||||
let full_file_path = PathBuf::from(&source_dir).join(full_path);
|
||||
|
||||
let filename_without_extension = full_file_path
|
||||
.file_stem()
|
||||
.expect("filename invalid")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let filename_without_extension = full_file_path
|
||||
.file_stem()
|
||||
.expect("filename invalid")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let extension = full_file_path.extension();
|
||||
let extension = full_file_path.extension();
|
||||
|
||||
let extension = if let Some(extension) = extension {
|
||||
extension.to_string_lossy().to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let extension = if let Some(extension) = extension {
|
||||
extension.to_string_lossy().to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let path_from_src = full_file_path
|
||||
.strip_prefix(&source_dir)
|
||||
.expect("couldn't get path relative to source");
|
||||
let path_from_src = full_file_path
|
||||
.strip_prefix(&source_dir)
|
||||
.expect("couldn't get path relative to source");
|
||||
|
||||
let mut folder_path_from_src = path_from_src.to_path_buf();
|
||||
folder_path_from_src.pop();
|
||||
let mut folder_path_from_src = path_from_src.to_path_buf();
|
||||
folder_path_from_src.pop();
|
||||
|
||||
let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src);
|
||||
let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src);
|
||||
|
||||
File {
|
||||
filename: filename_without_extension,
|
||||
extension,
|
||||
path_from_source: folder_path_from_src,
|
||||
path_to,
|
||||
extra_files: Vec::new(),
|
||||
info: AudioFileInfo::default(),
|
||||
}
|
||||
}
|
||||
File {
|
||||
filename: filename_without_extension,
|
||||
extension,
|
||||
path_from_source: folder_path_from_src,
|
||||
path_to,
|
||||
extra_files: Vec::new(),
|
||||
info: AudioFileInfo::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_filename(&self) -> String {
|
||||
format!("{}.{}", self.filename, self.extension)
|
||||
}
|
||||
pub fn join_path_to(&self) -> PathBuf {
|
||||
PathBuf::from(&self.path_to).join(self.join_filename())
|
||||
}
|
||||
pub fn join_path_from_source(&self) -> PathBuf {
|
||||
PathBuf::from(&self.path_from_source).join(self.join_filename())
|
||||
}
|
||||
pub fn join_filename(&self) -> String {
|
||||
format!("{}.{}", self.filename, self.extension)
|
||||
}
|
||||
pub fn join_path_to(&self) -> PathBuf {
|
||||
PathBuf::from(&self.path_to).join(self.join_filename())
|
||||
}
|
||||
pub fn join_path_from_source(&self) -> PathBuf {
|
||||
PathBuf::from(&self.path_from_source).join(self.join_filename())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,47 +4,47 @@ use std::collections::HashMap;
|
|||
const MAPPINGS_DATA: &str = include_str!("mappings.json");
|
||||
|
||||
lazy_static! {
|
||||
static ref MAPPINGS: HashMap<char, String> = {
|
||||
let data: HashMap<String, String> =
|
||||
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
|
||||
static ref MAPPINGS: HashMap<char, String> = {
|
||||
let data: HashMap<String, String> =
|
||||
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
|
||||
|
||||
let mut replacement_map: HashMap<char, String> = HashMap::new();
|
||||
for (chr, repl) in &data {
|
||||
match chr.parse::<u32>() {
|
||||
Ok(n) => {
|
||||
let b = char::from_u32(n).expect("invalid char in string");
|
||||
let mut replacement_map: HashMap<char, String> = HashMap::new();
|
||||
for (chr, repl) in &data {
|
||||
match chr.parse::<u32>() {
|
||||
Ok(n) => {
|
||||
let b = char::from_u32(n).expect("invalid char in string");
|
||||
|
||||
replacement_map.insert(b, repl.to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"mapping data broken, could not parse char {} with error {}",
|
||||
chr, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
replacement_map.insert(b, repl.to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"mapping data broken, could not parse char {} with error {}",
|
||||
chr, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replacement_map
|
||||
};
|
||||
replacement_map
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reduce_to_ascii(input: String) -> String {
|
||||
if input.is_ascii() {
|
||||
return input;
|
||||
}
|
||||
if input.is_ascii() {
|
||||
return input;
|
||||
}
|
||||
|
||||
let mut output = String::with_capacity(input.len());
|
||||
for c in input.chars() {
|
||||
if c.is_ascii() {
|
||||
output.push(c);
|
||||
continue;
|
||||
}
|
||||
let mut output = String::with_capacity(input.len());
|
||||
for c in input.chars() {
|
||||
if c.is_ascii() {
|
||||
output.push(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(replacement) = MAPPINGS.get(&c) {
|
||||
output.push_str(replacement);
|
||||
}
|
||||
}
|
||||
if let Some(replacement) = MAPPINGS.get(&c) {
|
||||
output.push_str(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
output
|
||||
}
|
||||
|
|
|
@ -2,34 +2,34 @@ use std::{fmt, io, process};
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum AnalyzeError {
|
||||
FFProbeError(FFProbeError),
|
||||
IOError(io::Error),
|
||||
ParseError(serde_json::Error),
|
||||
FFProbeError(FFProbeError),
|
||||
IOError(io::Error),
|
||||
ParseError(serde_json::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for AnalyzeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AnalyzeError::FFProbeError(err) => write!(f, "{}", err),
|
||||
AnalyzeError::IOError(err) => write!(f, "{}", err),
|
||||
AnalyzeError::ParseError(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AnalyzeError::FFProbeError(err) => write!(f, "{}", err),
|
||||
AnalyzeError::IOError(err) => write!(f, "{}", err),
|
||||
AnalyzeError::ParseError(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FFProbeError {
|
||||
pub exit_status: process::ExitStatus,
|
||||
pub stderr: String,
|
||||
pub exit_status: process::ExitStatus,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for FFProbeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ffprobe exited with error code {}, stderr: {}",
|
||||
self.exit_status.code().unwrap(),
|
||||
self.stderr
|
||||
)
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ffprobe exited with error code {}, stderr: {}",
|
||||
self.exit_status.code().unwrap(),
|
||||
self.stderr
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,34 +2,34 @@ use serde::Deserialize;
|
|||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct FFProbeOutput {
|
||||
pub format: FFProbeOutputFormat,
|
||||
pub format: FFProbeOutputFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct FFProbeOutputFormat {
|
||||
pub tags: FFProbeOutputTags,
|
||||
pub tags: FFProbeOutputTags,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct FFProbeOutputTags {
|
||||
#[serde(alias = "TITLE")]
|
||||
pub title: String,
|
||||
#[serde(default, alias = "ARTIST")]
|
||||
pub artist: String,
|
||||
#[serde(alias = "TITLE")]
|
||||
pub title: String,
|
||||
#[serde(default, alias = "ARTIST")]
|
||||
pub artist: String,
|
||||
|
||||
#[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")]
|
||||
pub replaygain_track_peak: Option<String>,
|
||||
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
|
||||
pub replaygain_track_gain: Option<String>,
|
||||
#[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")]
|
||||
pub replaygain_track_peak: Option<String>,
|
||||
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
|
||||
pub replaygain_track_gain: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for FFProbeOutputTags {
|
||||
fn default() -> Self {
|
||||
FFProbeOutputTags {
|
||||
title: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
replaygain_track_peak: None,
|
||||
replaygain_track_gain: None,
|
||||
}
|
||||
}
|
||||
fn default() -> Self {
|
||||
FFProbeOutputTags {
|
||||
title: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
replaygain_track_peak: None,
|
||||
replaygain_track_gain: None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +1,43 @@
|
|||
mod ffprobe_output;
|
||||
pub mod errors;
|
||||
mod ffprobe_output;
|
||||
pub mod types;
|
||||
|
||||
use std::{
|
||||
convert::Into,
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
use std::{convert::Into, path::Path, process::Command};
|
||||
|
||||
use self::errors::{AnalyzeError, FFProbeError};
|
||||
|
||||
pub fn analyze(path: &Path) -> Result<types::FFProbeData, AnalyzeError> {
|
||||
let output = Command::new(crate::meta::FFPROBE)
|
||||
.args([
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
path.to_str().unwrap(),
|
||||
])
|
||||
.output();
|
||||
let output = Command::new(crate::meta::FFPROBE)
|
||||
.args([
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
path.to_str().unwrap(),
|
||||
])
|
||||
.output();
|
||||
|
||||
if let Err(err) = output {
|
||||
return Err(AnalyzeError::IOError(err));
|
||||
}
|
||||
if let Err(err) = output {
|
||||
return Err(AnalyzeError::IOError(err));
|
||||
}
|
||||
|
||||
let output = output.unwrap();
|
||||
let output = output.unwrap();
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AnalyzeError::FFProbeError(FFProbeError {
|
||||
exit_status: output.status,
|
||||
stderr: String::from_utf8(output.stderr).unwrap(),
|
||||
}));
|
||||
}
|
||||
if !output.status.success() {
|
||||
return Err(AnalyzeError::FFProbeError(FFProbeError {
|
||||
exit_status: output.status,
|
||||
stderr: String::from_utf8(output.stderr).unwrap(),
|
||||
}));
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let ffprobe_out: serde_json::Result<ffprobe_output::FFProbeOutput> =
|
||||
serde_json::from_str(output.as_str());
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let ffprobe_out: serde_json::Result<ffprobe_output::FFProbeOutput> =
|
||||
serde_json::from_str(output.as_str());
|
||||
|
||||
let ffprobe_out = ffprobe_out.unwrap();
|
||||
let ffprobe_out = ffprobe_out.unwrap();
|
||||
|
||||
Ok(types::FFProbeData {
|
||||
tags: ffprobe_out.format.tags.into(),
|
||||
})
|
||||
Ok(types::FFProbeData {
|
||||
tags: ffprobe_out.format.tags.into(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,24 +4,24 @@ use super::ffprobe_output;
|
|||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FFProbeTags {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub replaygain_track_peak: Option<String>,
|
||||
pub replaygain_track_gain: Option<String>,
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub replaygain_track_peak: Option<String>,
|
||||
pub replaygain_track_gain: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ffprobe_output::FFProbeOutputTags> for FFProbeTags {
|
||||
fn from(val: ffprobe_output::FFProbeOutputTags) -> Self {
|
||||
FFProbeTags {
|
||||
title: val.title,
|
||||
artist: val.artist,
|
||||
replaygain_track_peak: val.replaygain_track_peak,
|
||||
replaygain_track_gain: val.replaygain_track_gain,
|
||||
}
|
||||
}
|
||||
fn from(val: ffprobe_output::FFProbeOutputTags) -> Self {
|
||||
FFProbeTags {
|
||||
title: val.title,
|
||||
artist: val.artist,
|
||||
replaygain_track_peak: val.replaygain_track_peak,
|
||||
replaygain_track_gain: val.replaygain_track_gain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FFProbeData {
|
||||
pub tags: FFProbeTags,
|
||||
pub tags: FFProbeTags,
|
||||
}
|
|
@ -4,126 +4,126 @@ use thiserror::Error;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FormatDetectionError {
|
||||
#[error("could not read file")]
|
||||
FileReadError,
|
||||
#[error("file format unrecognised")]
|
||||
UnrecognisedFormat,
|
||||
#[error("could not read file")]
|
||||
FileReadError,
|
||||
#[error("file format unrecognised")]
|
||||
UnrecognisedFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FileFormat {
|
||||
FLAC,
|
||||
MP3,
|
||||
OggVorbis,
|
||||
OggOpus,
|
||||
OggFLAC,
|
||||
OggSpeex,
|
||||
OggTheora,
|
||||
AIFF,
|
||||
Wav,
|
||||
WavPack,
|
||||
FLAC,
|
||||
MP3,
|
||||
OggVorbis,
|
||||
OggOpus,
|
||||
OggFLAC,
|
||||
OggSpeex,
|
||||
OggTheora,
|
||||
AIFF,
|
||||
Wav,
|
||||
WavPack,
|
||||
}
|
||||
|
||||
impl ToString for FileFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
FileFormat::FLAC => "FLAC".to_string(),
|
||||
FileFormat::MP3 => "MP3".to_string(),
|
||||
FileFormat::OggVorbis => "Ogg (Vorbis)".to_string(),
|
||||
FileFormat::OggOpus => "Ogg (Opus)".to_string(),
|
||||
FileFormat::OggFLAC => "Ogg (FLAC)".to_string(),
|
||||
FileFormat::OggSpeex => "Ogg (Speex)".to_string(),
|
||||
FileFormat::OggTheora => "Ogg (Theora)".to_string(),
|
||||
FileFormat::AIFF => "AIFF".to_string(),
|
||||
FileFormat::Wav => "Wav".to_string(),
|
||||
FileFormat::WavPack => "WavPack".to_string(),
|
||||
}
|
||||
}
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
FileFormat::FLAC => "FLAC".to_string(),
|
||||
FileFormat::MP3 => "MP3".to_string(),
|
||||
FileFormat::OggVorbis => "Ogg (Vorbis)".to_string(),
|
||||
FileFormat::OggOpus => "Ogg (Opus)".to_string(),
|
||||
FileFormat::OggFLAC => "Ogg (FLAC)".to_string(),
|
||||
FileFormat::OggSpeex => "Ogg (Speex)".to_string(),
|
||||
FileFormat::OggTheora => "Ogg (Theora)".to_string(),
|
||||
FileFormat::AIFF => "AIFF".to_string(),
|
||||
FileFormat::Wav => "Wav".to_string(),
|
||||
FileFormat::WavPack => "WavPack".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_ogg_subformat(path: &Path) -> Result<FileFormat, FormatDetectionError> {
|
||||
let file = File::open(path);
|
||||
let file = File::open(path);
|
||||
|
||||
if let Err(_error) = file {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
if let Err(_error) = file {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let file = file.unwrap();
|
||||
let file = file.unwrap();
|
||||
|
||||
let limit = file
|
||||
.metadata()
|
||||
.map(|m| std::cmp::min(m.len(), 128) as usize + 1)
|
||||
.unwrap_or(0);
|
||||
let limit = file
|
||||
.metadata()
|
||||
.map(|m| std::cmp::min(m.len(), 128) as usize + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut bytes = Vec::with_capacity(limit);
|
||||
if let Err(_err) = file.take(128).read_to_end(&mut bytes) {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(limit);
|
||||
if let Err(_err) = file.take(128).read_to_end(&mut bytes) {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let mut mem = Bytes::from(bytes);
|
||||
mem.advance(28);
|
||||
let mut mem = Bytes::from(bytes);
|
||||
mem.advance(28);
|
||||
|
||||
let vorbis_type = mem.get_u8();
|
||||
let vorbis_type = mem.get_u8();
|
||||
|
||||
match vorbis_type {
|
||||
0x01 => return Ok(FileFormat::OggVorbis),
|
||||
0x7f => return Ok(FileFormat::OggFLAC),
|
||||
0x80 => return Ok(FileFormat::OggTheora),
|
||||
// S for speex
|
||||
0x53 => return Ok(FileFormat::OggSpeex),
|
||||
_ => {}
|
||||
}
|
||||
match vorbis_type {
|
||||
0x01 => return Ok(FileFormat::OggVorbis),
|
||||
0x7f => return Ok(FileFormat::OggFLAC),
|
||||
0x80 => return Ok(FileFormat::OggTheora),
|
||||
// S for speex
|
||||
0x53 => return Ok(FileFormat::OggSpeex),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
}
|
||||
|
||||
fn wavpack_matcher(buf: &[u8]) -> bool {
|
||||
// 77 76 70 6b
|
||||
buf.len() >= 4 && buf[0] == 0x77 && buf[1] == 0x76 && buf[2] == 0x70 && buf[3] == 0x6b
|
||||
// 77 76 70 6b
|
||||
buf.len() >= 4 && buf[0] == 0x77 && buf[1] == 0x76 && buf[2] == 0x70 && buf[3] == 0x6b
|
||||
}
|
||||
|
||||
pub fn detect_format(path: &Path) -> Result<FileFormat, FormatDetectionError> {
|
||||
let mut info = infer::Infer::new();
|
||||
info.add("custom/wavpack", "wv", wavpack_matcher);
|
||||
let mut info = infer::Infer::new();
|
||||
info.add("custom/wavpack", "wv", wavpack_matcher);
|
||||
|
||||
let kind = info.get_from_path(path);
|
||||
let kind = info.get_from_path(path);
|
||||
|
||||
if let Err(_error) = kind {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
if let Err(_error) = kind {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let kind = kind.unwrap();
|
||||
let kind = kind.unwrap();
|
||||
|
||||
if kind.is_none() {
|
||||
return Err(FormatDetectionError::UnrecognisedFormat);
|
||||
}
|
||||
if kind.is_none() {
|
||||
return Err(FormatDetectionError::UnrecognisedFormat);
|
||||
}
|
||||
|
||||
let kind = kind.unwrap();
|
||||
let kind = kind.unwrap();
|
||||
|
||||
match kind.mime_type() {
|
||||
"audio/mpeg" => {
|
||||
return Ok(FileFormat::MP3);
|
||||
}
|
||||
"audio/x-wav" => {
|
||||
return Ok(FileFormat::Wav);
|
||||
}
|
||||
"custom/wavpack" => {
|
||||
return Ok(FileFormat::WavPack);
|
||||
}
|
||||
"audio/ogg" => {
|
||||
return detect_ogg_subformat(path);
|
||||
}
|
||||
"audio/x-flac" => {
|
||||
return Ok(FileFormat::FLAC);
|
||||
}
|
||||
"audio/x-aiff" => {
|
||||
return Ok(FileFormat::AIFF);
|
||||
}
|
||||
"audio/opus" => {
|
||||
return Ok(FileFormat::OggOpus);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match kind.mime_type() {
|
||||
"audio/mpeg" => {
|
||||
return Ok(FileFormat::MP3);
|
||||
}
|
||||
"audio/x-wav" => {
|
||||
return Ok(FileFormat::Wav);
|
||||
}
|
||||
"custom/wavpack" => {
|
||||
return Ok(FileFormat::WavPack);
|
||||
}
|
||||
"audio/ogg" => {
|
||||
return detect_ogg_subformat(path);
|
||||
}
|
||||
"audio/x-flac" => {
|
||||
return Ok(FileFormat::FLAC);
|
||||
}
|
||||
"audio/x-aiff" => {
|
||||
return Ok(FileFormat::AIFF);
|
||||
}
|
||||
"audio/opus" => {
|
||||
return Ok(FileFormat::OggOpus);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
}
|
||||
|
|
|
@ -1,189 +1,189 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use string_error::into_err;
|
||||
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags},
|
||||
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
|
||||
utils::{
|
||||
ffprobe,
|
||||
format_detection::{detect_format, FileFormat},
|
||||
},
|
||||
types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags},
|
||||
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
|
||||
utils::{
|
||||
ffprobe,
|
||||
format_detection::{detect_format, FileFormat},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Changes {
|
||||
title: Option<String>,
|
||||
artist: Option<String>,
|
||||
title: Option<String>,
|
||||
artist: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExtractedData {
|
||||
tags: Tags,
|
||||
replaygain_data: Option<ReplayGainData>,
|
||||
tags: Tags,
|
||||
replaygain_data: Option<ReplayGainData>,
|
||||
}
|
||||
|
||||
pub struct GenericFFMpegAudioFormat {
|
||||
file_format: FileFormat,
|
||||
path: Box<PathBuf>,
|
||||
file_format: FileFormat,
|
||||
path: Box<PathBuf>,
|
||||
|
||||
extracted_data: ExtractedData,
|
||||
changes: Changes,
|
||||
extracted_data: ExtractedData,
|
||||
changes: Changes,
|
||||
}
|
||||
|
||||
impl GenericFFMpegAudioFormat {
|
||||
fn analyze(&mut self) -> Result<(), BoxedError> {
|
||||
let output = ffprobe::analyze(&self.path);
|
||||
fn analyze(&mut self) -> Result<(), BoxedError> {
|
||||
let output = ffprobe::analyze(&self.path);
|
||||
|
||||
if let Err(err) = output {
|
||||
return Err(into_err(format!("{}", err)));
|
||||
}
|
||||
if let Err(err) = output {
|
||||
return Err(into_err(format!("{}", err)));
|
||||
}
|
||||
|
||||
let output = output.unwrap();
|
||||
let output = output.unwrap();
|
||||
|
||||
self.extracted_data.tags = Tags {
|
||||
title: output.tags.title,
|
||||
artist: output.tags.artist,
|
||||
};
|
||||
self.extracted_data.tags = Tags {
|
||||
title: output.tags.title,
|
||||
artist: output.tags.artist,
|
||||
};
|
||||
|
||||
if output.tags.replaygain_track_gain.is_some()
|
||||
&& output.tags.replaygain_track_peak.is_some()
|
||||
{
|
||||
self.extracted_data.replaygain_data = Some(ReplayGainData {
|
||||
track_gain: output.tags.replaygain_track_gain.unwrap(),
|
||||
track_peak: output.tags.replaygain_track_peak.unwrap(),
|
||||
});
|
||||
} else {
|
||||
self.extracted_data.replaygain_data = None;
|
||||
}
|
||||
if output.tags.replaygain_track_gain.is_some()
|
||||
&& output.tags.replaygain_track_peak.is_some()
|
||||
{
|
||||
self.extracted_data.replaygain_data = Some(ReplayGainData {
|
||||
track_gain: output.tags.replaygain_track_gain.unwrap(),
|
||||
track_peak: output.tags.replaygain_track_peak.unwrap(),
|
||||
});
|
||||
} else {
|
||||
self.extracted_data.replaygain_data = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatHandler for GenericFFMpegAudioFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let mut tags = self.extracted_data.tags.clone();
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let mut tags = self.extracted_data.tags.clone();
|
||||
|
||||
if let Some(title) = &self.changes.title {
|
||||
tags.title = title.clone();
|
||||
}
|
||||
if let Some(title) = &self.changes.title {
|
||||
tags.title = title.clone();
|
||||
}
|
||||
|
||||
if let Some(artist) = &self.changes.title {
|
||||
tags.artist = artist.clone();
|
||||
}
|
||||
if let Some(artist) = &self.changes.title {
|
||||
tags.artist = artist.clone();
|
||||
}
|
||||
|
||||
if !allow_missing {
|
||||
if tags.title.is_empty() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if tags.artist.is_empty() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
if !allow_missing {
|
||||
if tags.title.is_empty() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if tags.artist.is_empty() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.changes.title = Some(title);
|
||||
Ok(())
|
||||
}
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.changes.title = Some(title);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.changes.artist = Some(artist);
|
||||
Ok(())
|
||||
}
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.changes.artist = Some(artist);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_replaygain_data(&mut self, _data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
panic!("ffprobe doesn't support setting replaygain data, check supports_replaygain() f")
|
||||
}
|
||||
fn set_replaygain_data(&mut self, _data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
panic!("ffprobe doesn't support setting replaygain data, check supports_replaygain() f")
|
||||
}
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
if self.changes.title.is_none() && self.changes.artist.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
if self.changes.title.is_none() && self.changes.artist.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let temp_file = tempdir
|
||||
.path()
|
||||
.join(PathBuf::from(self.path.file_name().unwrap()));
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let temp_file = tempdir
|
||||
.path()
|
||||
.join(PathBuf::from(self.path.file_name().unwrap()));
|
||||
|
||||
args.extend(vec![
|
||||
"-i".to_string(),
|
||||
self.path.to_string_lossy().to_string(),
|
||||
]);
|
||||
args.extend(vec![
|
||||
"-i".to_string(),
|
||||
self.path.to_string_lossy().to_string(),
|
||||
]);
|
||||
|
||||
args.extend(vec!["-c".to_string(), "copy".to_string()]);
|
||||
args.extend(vec!["-c".to_string(), "copy".to_string()]);
|
||||
|
||||
if let Some(title) = &self.changes.title {
|
||||
args.extend(vec![
|
||||
"-metadata".to_string(),
|
||||
format!("title=\"{}\"", title.as_str()),
|
||||
])
|
||||
}
|
||||
if let Some(title) = &self.changes.title {
|
||||
args.extend(vec![
|
||||
"-metadata".to_string(),
|
||||
format!("title=\"{}\"", title.as_str()),
|
||||
])
|
||||
}
|
||||
|
||||
if let Some(artist) = &self.changes.artist {
|
||||
args.extend(vec![
|
||||
"-metadata".to_string(),
|
||||
format!("artist={}", artist.as_str()),
|
||||
])
|
||||
}
|
||||
if let Some(artist) = &self.changes.artist {
|
||||
args.extend(vec![
|
||||
"-metadata".to_string(),
|
||||
format!("artist={}", artist.as_str()),
|
||||
])
|
||||
}
|
||||
|
||||
args.push(temp_file.to_string_lossy().to_string());
|
||||
args.push(temp_file.to_string_lossy().to_string());
|
||||
|
||||
let output = Command::new(crate::meta::FFMPEG).args(args).output()?;
|
||||
let output = Command::new(crate::meta::FFMPEG).args(args).output()?;
|
||||
|
||||
println!("{:?}", String::from_utf8(output.stderr));
|
||||
println!("{:?}", String::from_utf8(output.stderr));
|
||||
|
||||
std::fs::copy(temp_file, self.path.to_path_buf())?;
|
||||
std::fs::copy(temp_file, self.path.to_path_buf())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(self.file_format),
|
||||
})
|
||||
}
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(self.file_format),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_handler(
|
||||
path: &Path,
|
||||
file_format: Option<FileFormat>,
|
||||
path: &Path,
|
||||
file_format: Option<FileFormat>,
|
||||
) -> Result<GenericFFMpegAudioFormat, BoxedError> {
|
||||
let mut file_format = file_format;
|
||||
if file_format.is_none() {
|
||||
file_format = Some(detect_format(path)?);
|
||||
}
|
||||
let mut file_format = file_format;
|
||||
if file_format.is_none() {
|
||||
file_format = Some(detect_format(path)?);
|
||||
}
|
||||
|
||||
let mut handler = GenericFFMpegAudioFormat {
|
||||
file_format: file_format.unwrap(),
|
||||
path: Box::new(path.to_path_buf()),
|
||||
extracted_data: ExtractedData::default(),
|
||||
changes: Changes::default(),
|
||||
};
|
||||
let mut handler = GenericFFMpegAudioFormat {
|
||||
file_format: file_format.unwrap(),
|
||||
path: Box::new(path.to_path_buf()),
|
||||
extracted_data: ExtractedData::default(),
|
||||
changes: Changes::default(),
|
||||
};
|
||||
|
||||
handler.analyze()?;
|
||||
handler.analyze()?;
|
||||
|
||||
Ok(handler)
|
||||
Ok(handler)
|
||||
}
|
||||
|
|
|
@ -1,115 +1,114 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{FormatHandler, AudioFormatError, BoxedError}
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
|
||||
};
|
||||
|
||||
|
||||
pub struct FLACAudioFormat {
|
||||
flac_tags: metaflac::Tag,
|
||||
path: Box<PathBuf>,
|
||||
flac_tags: metaflac::Tag,
|
||||
path: Box<PathBuf>,
|
||||
}
|
||||
|
||||
fn flac_get_first(tag: &metaflac::Tag, key: &str) -> Option<String> {
|
||||
if let Some(Some(v)) = tag.vorbis_comments().map(|c| c.get(key)) {
|
||||
if !v.is_empty() {
|
||||
Some(v[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if let Some(Some(v)) = tag.vorbis_comments().map(|c| c.get(key)) {
|
||||
if !v.is_empty() {
|
||||
Some(v[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatHandler for FLACAudioFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let title = flac_get_first(&self.flac_tags, "TITLE");
|
||||
let artist = flac_get_first(&self.flac_tags, "ARTIST");
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let title = flac_get_first(&self.flac_tags, "TITLE");
|
||||
let artist = flac_get_first(&self.flac_tags, "ARTIST");
|
||||
|
||||
if !allow_missing {
|
||||
if title.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if artist.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
if !allow_missing {
|
||||
if title.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if artist.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Tags {
|
||||
title: title.unwrap(),
|
||||
artist: artist.unwrap(),
|
||||
})
|
||||
}
|
||||
Ok(Tags {
|
||||
title: title.unwrap(),
|
||||
artist: artist.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
|
||||
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
|
||||
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
|
||||
|
||||
if track_gain.is_none() || track_peak.is_none() {
|
||||
return false;
|
||||
}
|
||||
if track_gain.is_none() || track_peak.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("TITLE");
|
||||
self.flac_tags.set_vorbis("TITLE", vec![title]);
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("TITLE");
|
||||
self.flac_tags.set_vorbis("TITLE", vec![title]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("ARTIST");
|
||||
self.flac_tags.set_vorbis("ARTIST", vec![artist]);
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("ARTIST");
|
||||
self.flac_tags.set_vorbis("ARTIST", vec![artist]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
|
||||
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
|
||||
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
|
||||
|
||||
self.flac_tags.set_vorbis(
|
||||
"REPLAYGAIN_TRACK_GAIN",
|
||||
vec![format!("{:.2} dB", data.track_gain)],
|
||||
);
|
||||
self.flac_tags.set_vorbis(
|
||||
"REPLAYGAIN_TRACK_PEAK",
|
||||
vec![format!("{:.6}", data.track_peak)],
|
||||
);
|
||||
self.flac_tags.set_vorbis(
|
||||
"REPLAYGAIN_TRACK_GAIN",
|
||||
vec![format!("{:.2} dB", data.track_gain)],
|
||||
);
|
||||
self.flac_tags.set_vorbis(
|
||||
"REPLAYGAIN_TRACK_PEAK",
|
||||
vec![format!("{:.6}", data.track_peak)],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
self.flac_tags.write_to_path(self.path.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
self.flac_tags.write_to_path(self.path.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(FileFormat::FLAC),
|
||||
})
|
||||
}
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(FileFormat::FLAC),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_handler(path: &PathBuf) -> Result<FLACAudioFormat, BoxedError> {
|
||||
Ok(FLACAudioFormat {
|
||||
flac_tags: metaflac::Tag::read_from_path(path)?,
|
||||
path: Box::new(path.clone()),
|
||||
})
|
||||
Ok(FLACAudioFormat {
|
||||
flac_tags: metaflac::Tag::read_from_path(path)?,
|
||||
path: Box::new(path.clone()),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,131 +3,131 @@ use std::path::PathBuf;
|
|||
use id3::TagLike;
|
||||
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{FormatHandler, AudioFormatError, BoxedError},
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
|
||||
};
|
||||
|
||||
pub struct ID3AudioFormat {
|
||||
id3_tags: id3::Tag,
|
||||
path: Box<PathBuf>,
|
||||
id3_tags: id3::Tag,
|
||||
path: Box<PathBuf>,
|
||||
}
|
||||
|
||||
impl FormatHandler for ID3AudioFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let title = self.id3_tags.title();
|
||||
let artist = self.id3_tags.artist();
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let title = self.id3_tags.title();
|
||||
let artist = self.id3_tags.artist();
|
||||
|
||||
if !allow_missing {
|
||||
if title.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if artist.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
if !allow_missing {
|
||||
if title.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
}
|
||||
if artist.is_none() {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Tags {
|
||||
title: String::from(title.unwrap()),
|
||||
artist: String::from(artist.unwrap()),
|
||||
})
|
||||
}
|
||||
Ok(Tags {
|
||||
title: String::from(title.unwrap()),
|
||||
artist: String::from(artist.unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
let frames = self.id3_tags.frames();
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
let frames = self.id3_tags.frames();
|
||||
|
||||
let mut contains_replaygain_tags = false;
|
||||
let mut contains_replaygain_tags = false;
|
||||
|
||||
for frame in frames {
|
||||
if frame.id() == "TXXX" {
|
||||
if let Some(extended_text) = frame.content().extended_text() {
|
||||
match extended_text.description.as_str() {
|
||||
"REPLAYGAIN_TRACK_GAIN" => {
|
||||
contains_replaygain_tags = true;
|
||||
}
|
||||
"REPLAYGAIN_TRACK_PEAK" => {
|
||||
contains_replaygain_tags = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for frame in frames {
|
||||
if frame.id() == "TXXX" {
|
||||
if let Some(extended_text) = frame.content().extended_text() {
|
||||
match extended_text.description.as_str() {
|
||||
"REPLAYGAIN_TRACK_GAIN" => {
|
||||
contains_replaygain_tags = true;
|
||||
}
|
||||
"REPLAYGAIN_TRACK_PEAK" => {
|
||||
contains_replaygain_tags = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contains_replaygain_tags
|
||||
}
|
||||
contains_replaygain_tags
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.id3_tags.set_title(title);
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
self.id3_tags.set_title(title);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.id3_tags.set_artist(artist);
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
self.id3_tags.set_artist(artist);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
let frames = self.id3_tags.remove("TXXX");
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
let frames = self.id3_tags.remove("TXXX");
|
||||
|
||||
for frame in frames {
|
||||
if let Some(extended_text) = frame.content().extended_text() {
|
||||
if extended_text.description.starts_with("REPLAYGAIN") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.id3_tags.add_frame(frame);
|
||||
}
|
||||
for frame in frames {
|
||||
if let Some(extended_text) = frame.content().extended_text() {
|
||||
if extended_text.description.starts_with("REPLAYGAIN") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.id3_tags.add_frame(frame);
|
||||
}
|
||||
|
||||
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||
"TXXX",
|
||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||
description: "REPLAYGAIN_TRACK_GAIN".to_string(),
|
||||
value: format!("{:.2} dB", data.track_gain),
|
||||
}),
|
||||
));
|
||||
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||
"TXXX",
|
||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||
description: "REPLAYGAIN_TRACK_GAIN".to_string(),
|
||||
value: format!("{:.2} dB", data.track_gain),
|
||||
}),
|
||||
));
|
||||
|
||||
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||
"TXXX",
|
||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||
description: "REPLAYGAIN_TRACK_PEAK".to_string(),
|
||||
value: format!("{:.6}", data.track_peak),
|
||||
}),
|
||||
));
|
||||
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||
"TXXX",
|
||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||
description: "REPLAYGAIN_TRACK_PEAK".to_string(),
|
||||
value: format!("{:.6}", data.track_peak),
|
||||
}),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
self.id3_tags
|
||||
.write_to_path(self.path.as_path(), id3::Version::Id3v24)?;
|
||||
Ok(())
|
||||
}
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
self.id3_tags
|
||||
.write_to_path(self.path.as_path(), id3::Version::Id3v24)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(FileFormat::MP3),
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
})
|
||||
}
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: Some(FileFormat::MP3),
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_handler(path: &PathBuf) -> Result<ID3AudioFormat, BoxedError> {
|
||||
let id3_tags = id3::Tag::read_from_path(path)?;
|
||||
let id3_tags = id3::Tag::read_from_path(path)?;
|
||||
|
||||
Ok(ID3AudioFormat {
|
||||
id3_tags,
|
||||
path: Box::new(path.clone()),
|
||||
})
|
||||
Ok(ID3AudioFormat {
|
||||
id3_tags,
|
||||
path: Box::new(path.clone()),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,136 +6,134 @@ use lazy_static::lazy_static;
|
|||
|
||||
use super::{BoxedError, FormatHandler};
|
||||
|
||||
#[cfg(feature = "flac_extractor")]
|
||||
mod flac;
|
||||
#[cfg(feature = "ffprobe_extractor")]
|
||||
mod ffprobe;
|
||||
#[cfg(feature = "taglib_extractor")]
|
||||
mod taglib;
|
||||
#[cfg(feature = "flac_extractor")]
|
||||
mod flac;
|
||||
#[cfg(feature = "mp3_extractor")]
|
||||
mod id3;
|
||||
#[cfg(feature = "taglib_extractor")]
|
||||
mod taglib;
|
||||
|
||||
type NewHandlerFuncReturn = Result<Box<dyn FormatHandler>, BoxedError>;
|
||||
type NewHandlerFunc = fn(path: &PathBuf, file_format: Option<FileFormat>) -> NewHandlerFuncReturn;
|
||||
|
||||
pub struct Handler {
|
||||
pub supported_extensions: Vec<String>,
|
||||
pub supported_formats: Vec<FileFormat>,
|
||||
pub new: NewHandlerFunc
|
||||
pub supported_extensions: Vec<String>,
|
||||
pub supported_formats: Vec<FileFormat>,
|
||||
pub new: NewHandlerFunc,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SUPPORTED_EXTENSIONS: Vec<String> = {
|
||||
let mut extensions: Vec<String> = Vec::new();
|
||||
for handler in HANDLERS.iter() {
|
||||
for extension in handler.supported_extensions.iter() {
|
||||
if !extensions.contains(extension) {
|
||||
extensions.push(extension.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
pub static ref SUPPORTED_EXTENSIONS: Vec<String> = {
|
||||
let mut extensions: Vec<String> = Vec::new();
|
||||
for handler in HANDLERS.iter() {
|
||||
for extension in handler.supported_extensions.iter() {
|
||||
if !extensions.contains(extension) {
|
||||
extensions.push(extension.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extensions
|
||||
};
|
||||
extensions
|
||||
};
|
||||
pub static ref SUPPORTED_FORMATS: Vec<FileFormat> = {
|
||||
let mut formats: Vec<FileFormat> = Vec::new();
|
||||
for handler in HANDLERS.iter() {
|
||||
for format in handler.supported_formats.iter() {
|
||||
if !formats.contains(format) {
|
||||
formats.push(*format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static ref SUPPORTED_FORMATS: Vec<FileFormat> = {
|
||||
let mut formats: Vec<FileFormat> = Vec::new();
|
||||
for handler in HANDLERS.iter() {
|
||||
for format in handler.supported_formats.iter() {
|
||||
if !formats.contains(format) {
|
||||
formats.push(*format);
|
||||
}
|
||||
}
|
||||
}
|
||||
formats
|
||||
};
|
||||
pub static ref HANDLERS: Vec<Handler> = {
|
||||
let mut handlers: Vec<Handler> = Vec::new();
|
||||
|
||||
formats
|
||||
};
|
||||
#[cfg(feature = "mp3_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec!["mp3".to_string()],
|
||||
supported_formats: vec![FileFormat::MP3],
|
||||
new: |path, _file_format| -> NewHandlerFuncReturn {
|
||||
let handler = id3::new_handler(path)?;
|
||||
|
||||
pub static ref HANDLERS: Vec<Handler> = {
|
||||
let mut handlers: Vec<Handler> = Vec::new();
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
|
||||
#[cfg(feature = "mp3_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec!["mp3".to_string()],
|
||||
supported_formats: vec![FileFormat::MP3],
|
||||
new: |path, _file_format| -> NewHandlerFuncReturn {
|
||||
let handler = id3::new_handler(path)?;
|
||||
#[cfg(feature = "flac_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec!["flac".to_string()],
|
||||
supported_formats: vec![FileFormat::FLAC],
|
||||
new: |path, _file_format| -> NewHandlerFuncReturn {
|
||||
let handler = flac::new_handler(path)?;
|
||||
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
|
||||
#[cfg(feature = "flac_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec!["flac".to_string()],
|
||||
supported_formats: vec![FileFormat::FLAC],
|
||||
new: |path, _file_format| -> NewHandlerFuncReturn {
|
||||
let handler = flac::new_handler(path)?;
|
||||
#[cfg(feature = "taglib_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec![
|
||||
"mp3".to_string(),
|
||||
"flac".to_string(),
|
||||
"ogg".to_string(),
|
||||
"opus".to_string(),
|
||||
"wav".to_string(),
|
||||
"wv".to_string(),
|
||||
"aiff".to_string(),
|
||||
],
|
||||
supported_formats: vec![
|
||||
FileFormat::MP3,
|
||||
FileFormat::FLAC,
|
||||
FileFormat::OggVorbis,
|
||||
FileFormat::OggOpus,
|
||||
FileFormat::OggFLAC,
|
||||
FileFormat::OggSpeex,
|
||||
FileFormat::OggTheora,
|
||||
FileFormat::Wav,
|
||||
FileFormat::WavPack,
|
||||
FileFormat::AIFF,
|
||||
],
|
||||
new: |path, file_format| -> NewHandlerFuncReturn {
|
||||
let handler = taglib::new_handler(path, file_format)?;
|
||||
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
|
||||
#[cfg(feature = "taglib_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec![
|
||||
"mp3".to_string(),
|
||||
"flac".to_string(),
|
||||
"ogg".to_string(),
|
||||
"opus".to_string(),
|
||||
"wav".to_string(),
|
||||
"wv".to_string(),
|
||||
"aiff".to_string(),
|
||||
],
|
||||
supported_formats: vec![
|
||||
FileFormat::MP3,
|
||||
FileFormat::FLAC,
|
||||
FileFormat::OggVorbis,
|
||||
FileFormat::OggOpus,
|
||||
FileFormat::OggFLAC,
|
||||
FileFormat::OggSpeex,
|
||||
FileFormat::OggTheora,
|
||||
FileFormat::Wav,
|
||||
FileFormat::WavPack,
|
||||
FileFormat::AIFF,
|
||||
],
|
||||
new: |path, file_format| -> NewHandlerFuncReturn {
|
||||
let handler = taglib::new_handler(path, file_format)?;
|
||||
#[cfg(feature = "ffprobe_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec![
|
||||
"mp3".to_string(),
|
||||
"flac".to_string(),
|
||||
"ogg".to_string(),
|
||||
"opus".to_string(),
|
||||
"wav".to_string(),
|
||||
"wv".to_string(),
|
||||
"aiff".to_string(),
|
||||
],
|
||||
supported_formats: vec![
|
||||
FileFormat::MP3,
|
||||
FileFormat::FLAC,
|
||||
FileFormat::OggVorbis,
|
||||
FileFormat::OggOpus,
|
||||
FileFormat::OggFLAC,
|
||||
FileFormat::OggSpeex,
|
||||
FileFormat::OggTheora,
|
||||
FileFormat::Wav,
|
||||
FileFormat::WavPack,
|
||||
FileFormat::AIFF,
|
||||
],
|
||||
new: |path, file_format| -> NewHandlerFuncReturn {
|
||||
let handler = ffprobe::new_handler(path, file_format)?;
|
||||
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
|
||||
#[cfg(feature = "ffprobe_extractor")]
|
||||
handlers.push(Handler {
|
||||
supported_extensions: vec![
|
||||
"mp3".to_string(),
|
||||
"flac".to_string(),
|
||||
"ogg".to_string(),
|
||||
"opus".to_string(),
|
||||
"wav".to_string(),
|
||||
"wv".to_string(),
|
||||
"aiff".to_string(),
|
||||
],
|
||||
supported_formats: vec![
|
||||
FileFormat::MP3,
|
||||
FileFormat::FLAC,
|
||||
FileFormat::OggVorbis,
|
||||
FileFormat::OggOpus,
|
||||
FileFormat::OggFLAC,
|
||||
FileFormat::OggSpeex,
|
||||
FileFormat::OggTheora,
|
||||
FileFormat::Wav,
|
||||
FileFormat::WavPack,
|
||||
FileFormat::AIFF,
|
||||
],
|
||||
new: |path, file_format| -> NewHandlerFuncReturn {
|
||||
let handler = ffprobe::new_handler(path, file_format)?;
|
||||
|
||||
Ok(Box::from(handler))
|
||||
},
|
||||
});
|
||||
|
||||
handlers
|
||||
};
|
||||
handlers
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,165 +1,163 @@
|
|||
use std::path::Path;
|
||||
|
||||
use taglib::{
|
||||
new_taglib_file,
|
||||
traits::{File, Tag},
|
||||
TagLibFileType,
|
||||
new_taglib_file,
|
||||
traits::{File, Tag},
|
||||
TagLibFileType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{FormatHandler, AudioFormatError, BoxedError}
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
utils::formats::{AudioFormatError, BoxedError, FormatHandler},
|
||||
};
|
||||
|
||||
|
||||
pub struct TaglibAudioFormat {
|
||||
file: taglib::TagLibFile,
|
||||
file_format: Option<FileFormat>,
|
||||
file: taglib::TagLibFile,
|
||||
file_format: Option<FileFormat>,
|
||||
}
|
||||
|
||||
impl FormatHandler for TaglibAudioFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let tags = self.file.tag()?;
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let tags = self.file.tag()?;
|
||||
|
||||
let mut title = tags.title();
|
||||
if title.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
} else {
|
||||
title = Some("".to_string())
|
||||
}
|
||||
}
|
||||
let mut title = tags.title();
|
||||
if title.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
} else {
|
||||
title = Some("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
let mut artist = tags.artist();
|
||||
if artist.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
} else {
|
||||
artist = Some("".to_string())
|
||||
}
|
||||
}
|
||||
let mut artist = tags.artist();
|
||||
if artist.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
} else {
|
||||
artist = Some("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Tags {
|
||||
title: title.unwrap(),
|
||||
artist: artist.unwrap(),
|
||||
})
|
||||
}
|
||||
Ok(Tags {
|
||||
title: title.unwrap(),
|
||||
artist: artist.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
if format == FileFormat::OggOpus {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
if format == FileFormat::OggOpus {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
|
||||
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
|
||||
}
|
||||
match format {
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
|
||||
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
|
||||
return gain.is_some() && peak.is_some();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
|
||||
}
|
||||
match format {
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
|
||||
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
|
||||
return gain.is_some() && peak.is_some();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
return matches!(
|
||||
format,
|
||||
// All Ogg formats support ReplayGain
|
||||
FileFormat::OggVorbis
|
||||
| FileFormat::OggOpus
|
||||
| FileFormat::OggFLAC
|
||||
| FileFormat::OggSpeex
|
||||
// Both "support" ReplayGain but not implemented yet
|
||||
// FileFormat::Wav |
|
||||
// FileFormat::WavPack
|
||||
);
|
||||
}
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
return matches!(
|
||||
format,
|
||||
// All Ogg formats support ReplayGain
|
||||
FileFormat::OggVorbis
|
||||
| FileFormat::OggOpus
|
||||
| FileFormat::OggFLAC
|
||||
| FileFormat::OggSpeex // Both "support" ReplayGain but not implemented yet
|
||||
// FileFormat::Wav |
|
||||
// FileFormat::WavPack
|
||||
);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_title(title);
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_title(title);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_artist(artist);
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_artist(artist);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
if let Some(format) = self.file_format {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
if let Some(format) = self.file_format {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
|
||||
if format == FileFormat::OggOpus {
|
||||
let data = data.to_normal(true);
|
||||
if format == FileFormat::OggOpus {
|
||||
let data = data.to_normal(true);
|
||||
|
||||
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
|
||||
} else if matches!(
|
||||
format,
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
|
||||
) {
|
||||
let data = data.to_normal(false);
|
||||
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
|
||||
} else if matches!(
|
||||
format,
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
|
||||
) {
|
||||
let data = data.to_normal(false);
|
||||
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain);
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_PEAK".to_string(), data.track_peak);
|
||||
}
|
||||
}
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain);
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_PEAK".to_string(), data.track_peak);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
let res = self.file.save();
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
let res = self.file.save();
|
||||
|
||||
match res {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
match res {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: self.file_format,
|
||||
})
|
||||
}
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: self.file_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_handler(
|
||||
path: &Path,
|
||||
file_format: Option<FileFormat>,
|
||||
path: &Path,
|
||||
file_format: Option<FileFormat>,
|
||||
) -> Result<TaglibAudioFormat, BoxedError> {
|
||||
let mut taglib_format: Option<TagLibFileType> = None;
|
||||
if let Some(format) = file_format {
|
||||
taglib_format = match format {
|
||||
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
|
||||
FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
|
||||
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
|
||||
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
let mut taglib_format: Option<TagLibFileType> = None;
|
||||
if let Some(format) = file_format {
|
||||
taglib_format = match format {
|
||||
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
|
||||
FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
|
||||
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
|
||||
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaglibAudioFormat {
|
||||
file_format,
|
||||
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_format)?,
|
||||
})
|
||||
Ok(TaglibAudioFormat {
|
||||
file_format,
|
||||
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_format)?,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,80 +7,79 @@ use thiserror::Error;
|
|||
|
||||
use crate::types::{AudioFileInfo, File, ReplayGainRawData, Tags};
|
||||
|
||||
|
||||
use super::format_detection::detect_format;
|
||||
|
||||
type BoxedError = Box<dyn Error>;
|
||||
|
||||
pub trait FormatHandler {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
|
||||
fn contains_replaygain_tags(&self) -> bool;
|
||||
fn supports_replaygain(&self) -> bool;
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
|
||||
fn contains_replaygain_tags(&self) -> bool;
|
||||
fn supports_replaygain(&self) -> bool;
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError>;
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError>;
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError>;
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError>;
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError>;
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AudioFormatError {
|
||||
#[error("title missing from tags")]
|
||||
MissingTitle,
|
||||
#[error("artist missing from tags")]
|
||||
MissingArtist,
|
||||
#[error("title missing from tags")]
|
||||
MissingTitle,
|
||||
#[error("artist missing from tags")]
|
||||
MissingArtist,
|
||||
}
|
||||
|
||||
pub fn get_format_handler(file: &File) -> Result<Box<dyn FormatHandler>, BoxedError> {
|
||||
let format = detect_format(&file.join_path_to())?;
|
||||
let path = file.join_path_to();
|
||||
let format = detect_format(&file.join_path_to())?;
|
||||
let path = file.join_path_to();
|
||||
|
||||
for handler in handlers::HANDLERS.iter() {
|
||||
if !handler.supported_extensions.contains(&file.extension) {
|
||||
continue
|
||||
}
|
||||
for handler in handlers::HANDLERS.iter() {
|
||||
if !handler.supported_extensions.contains(&file.extension) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !handler.supported_formats.contains(&format) {
|
||||
continue
|
||||
}
|
||||
if !handler.supported_formats.contains(&format) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new = handler.new;
|
||||
let new = handler.new;
|
||||
|
||||
return new(&path, Some(format));
|
||||
}
|
||||
return new(&path, Some(format));
|
||||
}
|
||||
|
||||
panic!("no supported handler found");
|
||||
panic!("no supported handler found");
|
||||
}
|
||||
|
||||
fn is_supported_extension(ext: &str) -> bool {
|
||||
handlers::SUPPORTED_EXTENSIONS.contains(&ext.to_string())
|
||||
handlers::SUPPORTED_EXTENSIONS.contains(&ext.to_string())
|
||||
}
|
||||
|
||||
pub fn is_supported_file(file_path: &Path) -> bool {
|
||||
let ext = file_path.extension();
|
||||
let ext = file_path.extension();
|
||||
|
||||
if ext.is_none() {
|
||||
return false;
|
||||
}
|
||||
if ext.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ext = ext.unwrap().to_str().unwrap();
|
||||
let ext = ext.unwrap().to_str().unwrap();
|
||||
|
||||
if !is_supported_extension(ext) {
|
||||
return false;
|
||||
}
|
||||
if !is_supported_extension(ext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let format = detect_format(file_path);
|
||||
if format.is_err() {
|
||||
return false;
|
||||
}
|
||||
let format = detect_format(file_path);
|
||||
if format.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let format = format.unwrap();
|
||||
let format = format.unwrap();
|
||||
|
||||
handlers::SUPPORTED_FORMATS.contains(&format)
|
||||
handlers::SUPPORTED_FORMATS.contains(&format)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
pub mod ascii_reduce;
|
||||
pub mod ffprobe;
|
||||
pub mod format_detection;
|
||||
pub mod replaygain;
|
||||
pub mod transcoder;
|
||||
pub mod ffprobe;
|
||||
|
||||
pub mod formats;
|
||||
mod music_scanner;
|
||||
mod music_scanner;
|
||||
|
||||
pub use formats::is_supported_file;
|
||||
pub use music_scanner::scan_for_music;
|
||||
|
|
|
@ -6,52 +6,52 @@ use walkdir::WalkDir;
|
|||
use super::is_supported_file;
|
||||
|
||||
pub fn find_extra_files(
|
||||
src_dir: String,
|
||||
file: &File,
|
||||
src_dir: String,
|
||||
file: &File,
|
||||
) -> Result<Vec<File>, Box<dyn std::error::Error>> {
|
||||
let mut extra_files: Vec<File> = Vec::new();
|
||||
let mut extra_files: Vec<File> = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&file.path_to)? {
|
||||
let entry = entry?;
|
||||
if !entry.metadata()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
let entry_path = entry.path();
|
||||
for entry in fs::read_dir(&file.path_to)? {
|
||||
let entry = entry?;
|
||||
if !entry.metadata()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
let entry_path = entry.path();
|
||||
|
||||
let extension = entry_path.extension();
|
||||
if extension.is_none() {
|
||||
continue;
|
||||
}
|
||||
let extension = entry_path.extension();
|
||||
if extension.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
|
||||
&& extension.unwrap().to_string_lossy() != file.extension
|
||||
{
|
||||
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone()));
|
||||
}
|
||||
}
|
||||
if entry_path.file_stem().unwrap().to_string_lossy() == file.filename
|
||||
&& extension.unwrap().to_string_lossy() != file.extension
|
||||
{
|
||||
extra_files.push(File::from_path(src_dir.clone(), entry_path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extra_files)
|
||||
Ok(extra_files)
|
||||
}
|
||||
|
||||
pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error::Error>> {
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
let mut files: Vec<File> = Vec::new();
|
||||
|
||||
for entry in WalkDir::new(src_dir) {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.into_path();
|
||||
if entry_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
for entry in WalkDir::new(src_dir) {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.into_path();
|
||||
if entry_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_supported_file(&entry_path) {
|
||||
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
|
||||
if is_supported_file(&entry_path) {
|
||||
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
|
||||
|
||||
file.extra_files
|
||||
.extend(find_extra_files(src_dir.clone(), &file)?);
|
||||
file.extra_files
|
||||
.extend(find_extra_files(src_dir.clone(), &file)?);
|
||||
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
Ok(files)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
io::{BufRead, BufReader},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use string_error::static_err;
|
||||
|
@ -9,94 +9,94 @@ use string_error::static_err;
|
|||
use crate::types::ReplayGainRawData;
|
||||
|
||||
pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainRawData, Box<dyn std::error::Error>> {
|
||||
let output = Command::new(crate::meta::FFMPEG)
|
||||
.args([
|
||||
"-hide_banner",
|
||||
"-nostats",
|
||||
"-v",
|
||||
"info",
|
||||
"-i",
|
||||
&path.to_string_lossy(),
|
||||
"-filter_complex",
|
||||
"[0:a]ebur128=framelog=verbose:peak=sample:dualmono=true[s6]",
|
||||
"-map",
|
||||
"[s6]",
|
||||
"-f",
|
||||
"null",
|
||||
"/dev/null",
|
||||
])
|
||||
.output()?;
|
||||
let output = Command::new(crate::meta::FFMPEG)
|
||||
.args([
|
||||
"-hide_banner",
|
||||
"-nostats",
|
||||
"-v",
|
||||
"info",
|
||||
"-i",
|
||||
&path.to_string_lossy(),
|
||||
"-filter_complex",
|
||||
"[0:a]ebur128=framelog=verbose:peak=sample:dualmono=true[s6]",
|
||||
"-map",
|
||||
"[s6]",
|
||||
"-f",
|
||||
"null",
|
||||
"/dev/null",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFmpeg Crashed"));
|
||||
}
|
||||
if !output.status.success() {
|
||||
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFmpeg Crashed"));
|
||||
}
|
||||
|
||||
// info we need is in stdout
|
||||
let output_str = String::from_utf8(output.stderr).unwrap();
|
||||
// info we need is in stdout
|
||||
let output_str = String::from_utf8(output.stderr).unwrap();
|
||||
|
||||
let mut ebur128_summary = String::new();
|
||||
let mut ebur128_summary = String::new();
|
||||
|
||||
// for some reason ffmpeg outputs the summary twice,
|
||||
// the first time is garbage data
|
||||
let mut has_seen_first_summary = false;
|
||||
let mut should_start_reading_lines = false;
|
||||
// for some reason ffmpeg outputs the summary twice,
|
||||
// the first time is garbage data
|
||||
let mut has_seen_first_summary = false;
|
||||
let mut should_start_reading_lines = false;
|
||||
|
||||
let output_reader = BufReader::new(output_str.as_bytes());
|
||||
for line in output_reader.lines() {
|
||||
if let Ok(line) = line {
|
||||
if line.starts_with("[Parsed_ebur128_0") {
|
||||
if has_seen_first_summary {
|
||||
should_start_reading_lines = true;
|
||||
ebur128_summary.push_str("Summary:\n")
|
||||
} else {
|
||||
has_seen_first_summary = true;
|
||||
}
|
||||
} else if should_start_reading_lines {
|
||||
ebur128_summary.push_str(line.trim());
|
||||
ebur128_summary.push('\n')
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let output_reader = BufReader::new(output_str.as_bytes());
|
||||
for line in output_reader.lines() {
|
||||
if let Ok(line) = line {
|
||||
if line.starts_with("[Parsed_ebur128_0") {
|
||||
if has_seen_first_summary {
|
||||
should_start_reading_lines = true;
|
||||
ebur128_summary.push_str("Summary:\n")
|
||||
} else {
|
||||
has_seen_first_summary = true;
|
||||
}
|
||||
} else if should_start_reading_lines {
|
||||
ebur128_summary.push_str(line.trim());
|
||||
ebur128_summary.push('\n')
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut track_gain: f64 = 0.0;
|
||||
let mut track_peak: f64 = 0.0;
|
||||
let mut track_gain: f64 = 0.0;
|
||||
let mut track_peak: f64 = 0.0;
|
||||
|
||||
let summary_reader = BufReader::new(ebur128_summary.as_bytes());
|
||||
for line in summary_reader.lines() {
|
||||
if let Ok(line) = line {
|
||||
if line.starts_with("I:") {
|
||||
let mut l = line.split(':');
|
||||
l.next();
|
||||
let summary_reader = BufReader::new(ebur128_summary.as_bytes());
|
||||
for line in summary_reader.lines() {
|
||||
if let Ok(line) = line {
|
||||
if line.starts_with("I:") {
|
||||
let mut l = line.split(':');
|
||||
l.next();
|
||||
|
||||
let gain = l.next().unwrap().trim().trim_end_matches(" LUFS");
|
||||
let gain = gain.parse::<f64>()?;
|
||||
let gain = l.next().unwrap().trim().trim_end_matches(" LUFS");
|
||||
let gain = gain.parse::<f64>()?;
|
||||
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Gain_calculation
|
||||
// "In order to maintain backwards compatibility with RG1, RG2 uses a -18 LUFS reference, which based on lots of music, can give similar loudness compared to RG1."
|
||||
let gain = -18_f64 - gain;
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Gain_calculation
|
||||
// "In order to maintain backwards compatibility with RG1, RG2 uses a -18 LUFS reference, which based on lots of music, can give similar loudness compared to RG1."
|
||||
let gain = -18_f64 - gain;
|
||||
|
||||
track_gain = gain;
|
||||
}
|
||||
track_gain = gain;
|
||||
}
|
||||
|
||||
if line.starts_with("Peak:") {
|
||||
let mut l = line.split(':');
|
||||
l.next();
|
||||
if line.starts_with("Peak:") {
|
||||
let mut l = line.split(':');
|
||||
l.next();
|
||||
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Loudness_normalization
|
||||
let peak = l.next().unwrap().trim().trim_end_matches(" dBFS");
|
||||
let peak = peak.parse::<f64>()?;
|
||||
let peak = f64::powf(10_f64, peak / 20.0_f64);
|
||||
track_peak = peak;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(ReplayGainRawData {
|
||||
track_gain,
|
||||
track_peak,
|
||||
})
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Loudness_normalization
|
||||
let peak = l.next().unwrap().trim().trim_end_matches(" dBFS");
|
||||
let peak = peak.parse::<f64>()?;
|
||||
let peak = f64::powf(10_f64, peak / 20.0_f64);
|
||||
track_peak = peak;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(ReplayGainRawData {
|
||||
track_gain,
|
||||
track_peak,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,5 +3,5 @@ pub mod progress_monitor;
|
|||
#[allow(clippy::all)]
|
||||
mod transcoder;
|
||||
pub mod types;
|
||||
use self::progress_monitor::progress_monitor;
|
||||
use self::progress_monitor::progress_monitor;
|
||||
pub use self::transcoder::transcode;
|
||||
|
|
|
@ -6,212 +6,212 @@ use crate::utils::transcoder::types::PresetCategory;
|
|||
use crate::utils::transcoder::types::TranscodeConfig;
|
||||
|
||||
lazy_static! {
|
||||
#[derive(Debug)]
|
||||
pub static ref TRANSCODE_CONFIGS: Vec<PresetCategory> = {
|
||||
let mut preset_categories: Vec<PresetCategory> = Vec::new();
|
||||
#[derive(Debug)]
|
||||
pub static ref TRANSCODE_CONFIGS: Vec<PresetCategory> = {
|
||||
let mut preset_categories: Vec<PresetCategory> = Vec::new();
|
||||
|
||||
add_mp3_presets(&mut preset_categories);
|
||||
add_opus_presets(&mut preset_categories);
|
||||
add_vorbis_presets(&mut preset_categories);
|
||||
add_g726_presets(&mut preset_categories);
|
||||
add_speex_presets(&mut preset_categories);
|
||||
add_flac_preset(&mut preset_categories);
|
||||
add_wav_preset(&mut preset_categories);
|
||||
add_mp3_presets(&mut preset_categories);
|
||||
add_opus_presets(&mut preset_categories);
|
||||
add_vorbis_presets(&mut preset_categories);
|
||||
add_g726_presets(&mut preset_categories);
|
||||
add_speex_presets(&mut preset_categories);
|
||||
add_flac_preset(&mut preset_categories);
|
||||
add_wav_preset(&mut preset_categories);
|
||||
|
||||
preset_categories
|
||||
};
|
||||
preset_categories
|
||||
};
|
||||
}
|
||||
|
||||
fn add_mp3_presets(preset_categories: &mut Vec<PresetCategory>) {
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [
|
||||
8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
|
||||
] {
|
||||
presets.push(Preset {
|
||||
name: format!("mp3-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mp3".to_string()),
|
||||
encoder: Some("libmp3lame".to_string()),
|
||||
container: Some("mp3".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [
|
||||
8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
|
||||
] {
|
||||
presets.push(Preset {
|
||||
name: format!("mp3-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mp3".to_string()),
|
||||
encoder: Some("libmp3lame".to_string()),
|
||||
container: Some("mp3".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
|
||||
presets.push(Preset {
|
||||
name: format!("mp3-v{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mp3".to_string()),
|
||||
encoder: Some("libmp3lame".to_string()),
|
||||
container: Some("mp3".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
|
||||
presets.push(Preset {
|
||||
name: format!("mp3-v{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mp3".to_string()),
|
||||
encoder: Some("libmp3lame".to_string()),
|
||||
container: Some("mp3".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "mp3".to_string(),
|
||||
presets,
|
||||
});
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "mp3".to_string(),
|
||||
presets,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_opus_presets(preset_categories: &mut Vec<PresetCategory>) {
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
|
||||
presets.push(Preset {
|
||||
name: format!("opus-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("opus".to_string()),
|
||||
encoder: Some("libopus".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
|
||||
presets.push(Preset {
|
||||
name: format!("opus-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("opus".to_string()),
|
||||
encoder: Some("libopus".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "opus".to_string(),
|
||||
presets,
|
||||
});
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "opus".to_string(),
|
||||
presets,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_vorbis_presets(preset_categories: &mut Vec<PresetCategory>) {
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for quality in [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
|
||||
presets.push(Preset {
|
||||
name: format!("vorbis-v{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("ogg".to_string()),
|
||||
encoder: Some("libvorbis".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for quality in [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
|
||||
presets.push(Preset {
|
||||
name: format!("vorbis-v{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("ogg".to_string()),
|
||||
encoder: Some("libvorbis".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "vorbis".to_string(),
|
||||
presets,
|
||||
});
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "vorbis".to_string(),
|
||||
presets,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_g726_presets(preset_categories: &mut Vec<PresetCategory>) {
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
|
||||
presets.push(Preset {
|
||||
name: format!("g726-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mka".to_string()),
|
||||
encoder: Some("g726".to_string()),
|
||||
container: Some("matroska".to_string()),
|
||||
sample_rate: Some("8000".to_string()),
|
||||
channels: Some("1".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for bitrate in [16, 24, 32, 64, 96, 128, 256] {
|
||||
presets.push(Preset {
|
||||
name: format!("g726-{}k", bitrate).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("mka".to_string()),
|
||||
encoder: Some("g726".to_string()),
|
||||
container: Some("matroska".to_string()),
|
||||
sample_rate: Some("8000".to_string()),
|
||||
channels: Some("1".to_string()),
|
||||
bitrate: Some(format!("{}k", bitrate).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "g726".to_string(),
|
||||
presets,
|
||||
});
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "g726".to_string(),
|
||||
presets,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_speex_presets(preset_categories: &mut Vec<PresetCategory>) {
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
|
||||
presets.push(Preset {
|
||||
name: format!("speex-q{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("ogg".to_string()),
|
||||
encoder: Some("libspeex".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
let mut presets: Vec<Preset> = Vec::new();
|
||||
for quality in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] {
|
||||
presets.push(Preset {
|
||||
name: format!("speex-q{}", quality).to_string(),
|
||||
config: TranscodeConfig {
|
||||
file_extension: Some("ogg".to_string()),
|
||||
encoder: Some("libspeex".to_string()),
|
||||
container: Some("ogg".to_string()),
|
||||
quality: Some(format!("{}", quality).to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "speex".to_string(),
|
||||
presets,
|
||||
});
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "speex".to_string(),
|
||||
presets,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_flac_preset(preset_categories: &mut Vec<PresetCategory>) {
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "flac".to_string(),
|
||||
presets: Vec::from([Preset {
|
||||
name: "flac".to_string(),
|
||||
config: TranscodeConfig {
|
||||
encoder: Some("flac".to_string()),
|
||||
container: Some("flac".to_string()),
|
||||
file_extension: Some("flac".to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
}]),
|
||||
})
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "flac".to_string(),
|
||||
presets: Vec::from([Preset {
|
||||
name: "flac".to_string(),
|
||||
config: TranscodeConfig {
|
||||
encoder: Some("flac".to_string()),
|
||||
container: Some("flac".to_string()),
|
||||
file_extension: Some("flac".to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
}]),
|
||||
})
|
||||
}
|
||||
|
||||
fn add_wav_preset(preset_categories: &mut Vec<PresetCategory>) {
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "wav".to_string(),
|
||||
presets: Vec::from([Preset {
|
||||
name: "wav".to_string(),
|
||||
config: TranscodeConfig {
|
||||
container: Some("wav".to_string()),
|
||||
file_extension: Some("wav".to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
}]),
|
||||
})
|
||||
preset_categories.push(PresetCategory {
|
||||
name: "wav".to_string(),
|
||||
presets: Vec::from([Preset {
|
||||
name: "wav".to_string(),
|
||||
config: TranscodeConfig {
|
||||
container: Some("wav".to_string()),
|
||||
file_extension: Some("wav".to_string()),
|
||||
..TranscodeConfig::default()
|
||||
},
|
||||
}]),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn print_presets() {
|
||||
for category in TRANSCODE_CONFIGS.iter() {
|
||||
println!("Category {}:", category.name);
|
||||
for preset in category.presets.iter() {
|
||||
println!("- {}", preset.name)
|
||||
}
|
||||
}
|
||||
for category in TRANSCODE_CONFIGS.iter() {
|
||||
println!("Category {}:", category.name);
|
||||
for preset in category.presets.iter() {
|
||||
println!("- {}", preset.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_preset(name: String) -> Option<TranscodeConfig> {
|
||||
for category in TRANSCODE_CONFIGS.iter() {
|
||||
for preset in category.presets.iter() {
|
||||
if preset.name == name {
|
||||
return Some(preset.config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for category in TRANSCODE_CONFIGS.iter() {
|
||||
for preset in category.presets.iter() {
|
||||
if preset.name == name {
|
||||
return Some(preset.config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
None
|
||||
}
|
||||
|
||||
pub fn transcode_preset_or_config(
|
||||
preset_name: Option<&String>,
|
||||
config_path: Option<&String>,
|
||||
preset_name: Option<&String>,
|
||||
config_path: Option<&String>,
|
||||
) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
|
||||
if let Some(preset_name) = preset_name {
|
||||
let preset_config = get_preset(preset_name.to_string());
|
||||
if let Some(preset_name) = preset_name {
|
||||
let preset_config = get_preset(preset_name.to_string());
|
||||
|
||||
match preset_config {
|
||||
Some(config) => Ok(config),
|
||||
None => Err(into_err("invalid preset name".to_string())),
|
||||
}
|
||||
} else if let Some(config_path) = config_path {
|
||||
let config = TranscodeConfig::load(config_path.to_string())?;
|
||||
match preset_config {
|
||||
Some(config) => Ok(config),
|
||||
None => Err(into_err("invalid preset name".to_string())),
|
||||
}
|
||||
} else if let Some(config_path) = config_path {
|
||||
let config = TranscodeConfig::load(config_path.to_string())?;
|
||||
|
||||
Ok(config)
|
||||
} else {
|
||||
Err(into_err(
|
||||
"please provide a transcode config or preset".to_string(),
|
||||
))
|
||||
}
|
||||
Ok(config)
|
||||
} else {
|
||||
Err(into_err(
|
||||
"please provide a transcode config or preset".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader, Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::mpsc::{self, Sender},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
fs,
|
||||
io::{BufRead, BufReader, Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::mpsc::{self, Sender},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
@ -14,127 +14,127 @@ use string_error::static_err;
|
|||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct FFProbeOutput {
|
||||
pub format: FFProbeFormat,
|
||||
pub format: FFProbeFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct FFProbeFormat {
|
||||
pub duration: String,
|
||||
pub duration: String,
|
||||
}
|
||||
|
||||
fn get_file_length_milliseconds(
|
||||
source_filepath: PathBuf,
|
||||
source_filepath: PathBuf,
|
||||
) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let output = Command::new(crate::meta::FFPROBE)
|
||||
.args([
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
&source_filepath.to_string_lossy(),
|
||||
])
|
||||
.output()?;
|
||||
let output = Command::new(crate::meta::FFPROBE)
|
||||
.args([
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
&source_filepath.to_string_lossy(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFprobe Crashed"));
|
||||
}
|
||||
if !output.status.success() {
|
||||
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFprobe Crashed"));
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout).unwrap();
|
||||
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?;
|
||||
let output_str = String::from_utf8(output.stdout).unwrap();
|
||||
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?;
|
||||
|
||||
let duration_seconds = ffprobe_out.format.duration.parse::<f32>()?;
|
||||
let duration_seconds = ffprobe_out.format.duration.parse::<f32>()?;
|
||||
|
||||
Ok((duration_seconds * 1000.0).round() as u64)
|
||||
Ok((duration_seconds * 1000.0).round() as u64)
|
||||
}
|
||||
|
||||
fn ffprobe_duration_to_ms(duration: String) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let fields: Vec<&str> = duration.split(':').collect();
|
||||
let mut duration = Duration::from_nanos(0);
|
||||
let fields: Vec<&str> = duration.split(':').collect();
|
||||
let mut duration = Duration::from_nanos(0);
|
||||
|
||||
let hours = fields[0].parse::<u64>()?;
|
||||
duration += Duration::from_secs(hours * 60 * 60);
|
||||
let hours = fields[0].parse::<u64>()?;
|
||||
duration += Duration::from_secs(hours * 60 * 60);
|
||||
|
||||
let minutes = fields[1].parse::<u64>()?;
|
||||
duration += Duration::from_secs(minutes * 60);
|
||||
let minutes = fields[1].parse::<u64>()?;
|
||||
duration += Duration::from_secs(minutes * 60);
|
||||
|
||||
let seconds = fields[1].parse::<f64>()?;
|
||||
duration += Duration::from_millis((seconds * 1000.0) as u64);
|
||||
let seconds = fields[1].parse::<f64>()?;
|
||||
duration += Duration::from_millis((seconds * 1000.0) as u64);
|
||||
|
||||
Ok(duration.as_millis() as u64)
|
||||
Ok(duration.as_millis() as u64)
|
||||
}
|
||||
|
||||
pub fn progress_monitor(
|
||||
source_filepath: PathBuf,
|
||||
sender_base: &Sender<String>,
|
||||
source_filepath: PathBuf,
|
||||
sender_base: &Sender<String>,
|
||||
) -> Result<(String, JoinHandle<()>), Box<dyn std::error::Error>> {
|
||||
let total_length_millis = get_file_length_milliseconds(source_filepath)?;
|
||||
let total_length_millis = get_file_length_milliseconds(source_filepath)?;
|
||||
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let file_path = tempdir.path().join("progress.log");
|
||||
let file_path_string = file_path.to_str().unwrap().to_string();
|
||||
fs::File::create(&file_path)?;
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
let file_path = tempdir.path().join("progress.log");
|
||||
let file_path_string = file_path.to_str().unwrap().to_string();
|
||||
fs::File::create(&file_path)?;
|
||||
|
||||
let sender = sender_base.clone();
|
||||
let child = thread::spawn(move || {
|
||||
let _ = &tempdir;
|
||||
let sender = sender_base.clone();
|
||||
let child = thread::spawn(move || {
|
||||
let _ = &tempdir;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
tx,
|
||||
notify::Config::default().with_poll_interval(Duration::from_millis(100)),
|
||||
)
|
||||
.expect("could not watch for ffmpeg log progress status");
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
tx,
|
||||
notify::Config::default().with_poll_interval(Duration::from_millis(100)),
|
||||
)
|
||||
.expect("could not watch for ffmpeg log progress status");
|
||||
|
||||
watcher
|
||||
.watch(&file_path, RecursiveMode::NonRecursive)
|
||||
.unwrap();
|
||||
watcher
|
||||
.watch(&file_path, RecursiveMode::NonRecursive)
|
||||
.unwrap();
|
||||
|
||||
let mut pos = 0;
|
||||
let mut pos = 0;
|
||||
|
||||
'outer: for res in rx {
|
||||
if res.is_err() {
|
||||
break 'outer;
|
||||
}
|
||||
'outer: for res in rx {
|
||||
if res.is_err() {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
let res = res.unwrap();
|
||||
let res = res.unwrap();
|
||||
|
||||
match res.kind {
|
||||
EventKind::Modify(_) => {
|
||||
let mut file = fs::File::open(&file_path).unwrap();
|
||||
file.seek(SeekFrom::Start(pos)).unwrap();
|
||||
match res.kind {
|
||||
EventKind::Modify(_) => {
|
||||
let mut file = fs::File::open(&file_path).unwrap();
|
||||
file.seek(SeekFrom::Start(pos)).unwrap();
|
||||
|
||||
pos = file.metadata().unwrap().len();
|
||||
pos = file.metadata().unwrap().len();
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let ln = line.unwrap();
|
||||
let reader = BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let ln = line.unwrap();
|
||||
|
||||
if ln == "progress=end" {
|
||||
break 'outer;
|
||||
}
|
||||
if ln == "progress=end" {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
if ln.starts_with("out_time=") {
|
||||
let out_time = ln.strip_prefix("out_time=").unwrap().to_string();
|
||||
let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap();
|
||||
if sender
|
||||
.send(format!(
|
||||
"{:.2}%",
|
||||
((out_time_ms as f64 / total_length_millis as f64) * 100.0)
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
break 'outer;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
EventKind::Remove(_) => break 'outer,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
if ln.starts_with("out_time=") {
|
||||
let out_time = ln.strip_prefix("out_time=").unwrap().to_string();
|
||||
let out_time_ms = ffprobe_duration_to_ms(out_time).unwrap();
|
||||
if sender
|
||||
.send(format!(
|
||||
"{:.2}%",
|
||||
((out_time_ms as f64 / total_length_millis as f64) * 100.0)
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
break 'outer;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
EventKind::Remove(_) => break 'outer,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((file_path_string, child))
|
||||
Ok((file_path_string, child))
|
||||
}
|
||||
|
|
|
@ -6,80 +6,80 @@ use string_error::static_err;
|
|||
use super::{progress_monitor, types::TranscodeConfig};
|
||||
|
||||
pub fn transcode(
|
||||
file: File,
|
||||
dest: String,
|
||||
config: &TranscodeConfig,
|
||||
progress_sender: Option<Sender<String>>,
|
||||
file: File,
|
||||
dest: String,
|
||||
config: &TranscodeConfig,
|
||||
progress_sender: Option<Sender<String>>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut command_args: Vec<String> = Vec::new();
|
||||
command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]);
|
||||
let mut command_args: Vec<String> = Vec::new();
|
||||
command_args.extend(vec!["-y".to_string(), "-hide_banner".to_string()]);
|
||||
|
||||
command_args.extend(vec![
|
||||
"-i".to_string(),
|
||||
file.join_path_to().to_string_lossy().to_string(),
|
||||
]);
|
||||
command_args.extend(vec![
|
||||
"-i".to_string(),
|
||||
file.join_path_to().to_string_lossy().to_string(),
|
||||
]);
|
||||
|
||||
if let Some(encoder) = &config.encoder {
|
||||
command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
|
||||
}
|
||||
if let Some(encoder) = &config.encoder {
|
||||
command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(container) = &config.container {
|
||||
command_args.extend(vec!["-f".to_string(), container.to_string()]);
|
||||
}
|
||||
if let Some(container) = &config.container {
|
||||
command_args.extend(vec!["-f".to_string(), container.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(sample_rate) = &config.sample_rate {
|
||||
command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
|
||||
}
|
||||
if let Some(sample_rate) = &config.sample_rate {
|
||||
command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(channels) = &config.channels {
|
||||
command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
|
||||
}
|
||||
if let Some(channels) = &config.channels {
|
||||
command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(quality) = &config.quality {
|
||||
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
|
||||
}
|
||||
if let Some(quality) = &config.quality {
|
||||
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(bitrate) = &config.bitrate {
|
||||
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]);
|
||||
}
|
||||
if let Some(bitrate) = &config.bitrate {
|
||||
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]);
|
||||
}
|
||||
|
||||
command_args.push(dest);
|
||||
command_args.push(dest);
|
||||
|
||||
let mut progress_thread: Option<JoinHandle<()>> = None;
|
||||
let mut progress_file: Option<String> = None;
|
||||
let mut progress_thread: Option<JoinHandle<()>> = None;
|
||||
let mut progress_file: Option<String> = None;
|
||||
|
||||
if let Some(sender) = &progress_sender {
|
||||
let result = progress_monitor(file.join_path_to(), sender);
|
||||
if let Some(sender) = &progress_sender {
|
||||
let result = progress_monitor(file.join_path_to(), sender);
|
||||
|
||||
if let Ok(result) = result {
|
||||
progress_thread = Some(result.1);
|
||||
progress_file = Some(result.0.clone());
|
||||
command_args.extend(vec![
|
||||
"-progress".to_string(),
|
||||
result.0,
|
||||
"-nostats".to_string(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
if let Ok(result) = result {
|
||||
progress_thread = Some(result.1);
|
||||
progress_file = Some(result.0.clone());
|
||||
command_args.extend(vec![
|
||||
"-progress".to_string(),
|
||||
result.0,
|
||||
"-nostats".to_string(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
let output = Command::new(crate::meta::FFMPEG)
|
||||
.args(command_args)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
let output = Command::new(crate::meta::FFMPEG)
|
||||
.args(command_args)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
if let Some(sender) = progress_sender {
|
||||
drop(sender);
|
||||
}
|
||||
if let Some(sender) = progress_sender {
|
||||
drop(sender);
|
||||
}
|
||||
|
||||
if let Some(thread) = progress_thread {
|
||||
fs::remove_file(progress_file.unwrap())?;
|
||||
thread.join().expect("thread couldn't join");
|
||||
}
|
||||
if let Some(thread) = progress_thread {
|
||||
fs::remove_file(progress_file.unwrap())?;
|
||||
thread.join().expect("thread couldn't join");
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
print!("{}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFmpeg Crashed"));
|
||||
}
|
||||
if !output.status.success() {
|
||||
print!("{}", String::from_utf8(output.stderr).unwrap());
|
||||
return Err(static_err("FFmpeg Crashed"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -5,43 +5,43 @@ use string_error::static_err;
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Preset {
|
||||
pub name: String,
|
||||
pub config: TranscodeConfig,
|
||||
pub name: String,
|
||||
pub config: TranscodeConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PresetCategory {
|
||||
pub name: String,
|
||||
pub presets: Vec<Preset>,
|
||||
pub name: String,
|
||||
pub presets: Vec<Preset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TranscodeConfig {
|
||||
pub encoder: Option<String>,
|
||||
pub file_extension: Option<String>,
|
||||
pub container: Option<String>,
|
||||
pub bitrate: Option<String>,
|
||||
pub quality: Option<String>,
|
||||
pub sample_rate: Option<String>,
|
||||
pub channels: Option<String>,
|
||||
pub encoder: Option<String>,
|
||||
pub file_extension: Option<String>,
|
||||
pub container: Option<String>,
|
||||
pub bitrate: Option<String>,
|
||||
pub quality: Option<String>,
|
||||
pub sample_rate: Option<String>,
|
||||
pub channels: Option<String>,
|
||||
}
|
||||
|
||||
impl TranscodeConfig {
|
||||
pub fn load(path: String) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
|
||||
let path_buf = PathBuf::from(&path);
|
||||
let extension = path_buf.extension().unwrap();
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
pub fn load(path: String) -> Result<TranscodeConfig, Box<dyn std::error::Error>> {
|
||||
let path_buf = PathBuf::from(&path);
|
||||
let extension = path_buf.extension().unwrap();
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
if extension == "yml" || extension == "yaml" {
|
||||
let u: TranscodeConfig =
|
||||
serde_yaml::from_reader(reader).expect("error while reading yaml");
|
||||
return Ok(u);
|
||||
} else if extension == "json" {
|
||||
let u: TranscodeConfig =
|
||||
serde_json::from_reader(reader).expect("error while reading json");
|
||||
return Ok(u);
|
||||
}
|
||||
Err(static_err("Invalid File Extension"))
|
||||
}
|
||||
if extension == "yml" || extension == "yaml" {
|
||||
let u: TranscodeConfig =
|
||||
serde_yaml::from_reader(reader).expect("error while reading yaml");
|
||||
return Ok(u);
|
||||
} else if extension == "json" {
|
||||
let u: TranscodeConfig =
|
||||
serde_json::from_reader(reader).expect("error while reading json");
|
||||
return Ok(u);
|
||||
}
|
||||
Err(static_err("Invalid File Extension"))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue