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