move file format/tag handling to unified trait & handlers
This commit is contained in:
parent
e57ea88b22
commit
9952f20748
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,7 +3,7 @@ build
|
||||||
result
|
result
|
||||||
.direnv
|
.direnv
|
||||||
musicutil_old
|
musicutil_old
|
||||||
|
temp
|
||||||
# Added by cargo
|
|
||||||
|
|
||||||
/target
|
/target
|
||||||
|
/modules/taglib/target
|
||||||
|
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -8,12 +8,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyhow"
|
|
||||||
version = "1.0.66"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
|
@ -497,7 +491,6 @@ dependencies = [
|
||||||
name = "musicutil"
|
name = "musicutil"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"clap",
|
"clap",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
|
@ -511,6 +504,7 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"string-error",
|
"string-error",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -763,6 +757,26 @@ version = "0.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
|
|
@ -29,8 +29,8 @@ metaflac = "0.2.5"
|
||||||
html-escape = "0.2.11"
|
html-escape = "0.2.11"
|
||||||
|
|
||||||
# error handling
|
# error handling
|
||||||
|
thiserror = "1.0"
|
||||||
string-error = "0.1.0"
|
string-error = "0.1.0"
|
||||||
anyhow = "1.0.66"
|
|
||||||
|
|
||||||
# temporary file for transcode prefix file
|
# temporary file for transcode prefix file
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
38
flake.nix
38
flake.nix
|
@ -10,17 +10,22 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, utils, ... }:
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
overlay = final: prev:
|
overlay = final: prev: let
|
||||||
let system = final.system;
|
system = final.system;
|
||||||
in {
|
in {
|
||||||
musicutil = final.rustPlatform.buildRustPackage rec {
|
musicutil = final.rustPlatform.buildRustPackage rec {
|
||||||
pname = "musicutil";
|
pname = "musicutil";
|
||||||
version = "latest";
|
version = "latest";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock = { lockFile = ./Cargo.lock; };
|
cargoLock = {lockFile = ./Cargo.lock;};
|
||||||
|
|
||||||
postPatch = ''
|
postPatch = ''
|
||||||
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
|
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
|
||||||
|
@ -28,15 +33,15 @@
|
||||||
'';
|
'';
|
||||||
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
nativeBuildInputs = with final.pkgs; [ pkg-config rustc cargo ];
|
nativeBuildInputs = with final.pkgs; [pkg-config rustc cargo];
|
||||||
buildInputs = with final; [ ffmpeg ];
|
buildInputs = with final; [ffmpeg];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
} // utils.lib.eachSystem (utils.lib.defaultSystems) (system:
|
}
|
||||||
let
|
// utils.lib.eachSystem (utils.lib.defaultSystems) (system: let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = [ self.overlay ];
|
overlays = [self.overlay];
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
defaultPackage = self.packages."${system}".musicutil;
|
defaultPackage = self.packages."${system}".musicutil;
|
||||||
|
@ -54,7 +59,20 @@
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
||||||
buildInputs = with pkgs; [ rustc cargo clippy rust-analyzer rustfmt ];
|
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
|
buildInputs = with pkgs; [taglib pkg-config rustc cargo clippy rust-analyzer rustfmt];
|
||||||
|
shellHook = let
|
||||||
|
stdenv = pkgs.stdenv;
|
||||||
|
lib = pkgs.lib;
|
||||||
|
in ''
|
||||||
|
export BINDGEN_EXTRA_CLANG_ARGS="$(< ${stdenv.cc}/nix-support/libc-crt1-cflags) \
|
||||||
|
$(< ${stdenv.cc}/nix-support/libc-cflags) \
|
||||||
|
$(< ${stdenv.cc}/nix-support/cc-cflags) \
|
||||||
|
$(< ${stdenv.cc}/nix-support/libcxx-cxxflags) \
|
||||||
|
${lib.optionalString stdenv.cc.isClang "-idirafter ${stdenv.cc.cc}/lib/clang/${lib.getVersion stdenv.cc.cc}/include"} \
|
||||||
|
${lib.optionalString stdenv.cc.isGNU "-isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc} -isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc}/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${lib.getVersion stdenv.cc.cc}/include"} \
|
||||||
|
"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
lib = pkgs.musicutil.lib;
|
lib = pkgs.musicutil.lib;
|
||||||
|
|
|
@ -13,7 +13,8 @@ use crate::{
|
||||||
args::{CLIArgs, CopyCommandArgs},
|
args::{CLIArgs, CopyCommandArgs},
|
||||||
types::File,
|
types::File,
|
||||||
utils::{
|
utils::{
|
||||||
extract_tags_from_file, scan_for_music,
|
formats::get_format_handler,
|
||||||
|
scan_for_music,
|
||||||
transcoder::{
|
transcoder::{
|
||||||
presets::{print_presets, transcode_preset_or_config},
|
presets::{print_presets, transcode_preset_or_config},
|
||||||
transcode,
|
transcode,
|
||||||
|
@ -40,12 +41,13 @@ pub fn copy_command(
|
||||||
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!("Extracting Tags");
|
println!("Analysing Files");
|
||||||
for file in files.iter_mut() {
|
for file in files.iter_mut() {
|
||||||
println!("Extracting tags from: {:?}", file.join_path_from_source());
|
println!("Analysing: {:?}", file.join_path_from_source());
|
||||||
|
|
||||||
let tags = extract_tags_from_file(file.to_owned())?;
|
let handler = get_format_handler(file)?;
|
||||||
file.tags = tags;
|
|
||||||
|
file.info = handler.get_audio_file_info(true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if copy_args.single_directory {
|
if copy_args.single_directory {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::args::CLIArgs;
|
use crate::args::CLIArgs;
|
||||||
use crate::args::GenHTMLCommandArgs;
|
use crate::args::GenHTMLCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use crate::utils::extract_tags_from_file;
|
use crate::utils::formats::get_format_handler;
|
||||||
use crate::utils::scan_for_music;
|
use crate::utils::scan_for_music;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
@ -41,8 +41,8 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
|
||||||
false => "pure-table-even",
|
false => "pure-table-even",
|
||||||
};
|
};
|
||||||
|
|
||||||
let data_title = encode_text(&file.tags.title);
|
let data_title = encode_text(&file.info.tags.title);
|
||||||
let data_artist = encode_text(&file.tags.artist);
|
let data_artist = encode_text(&file.info.tags.artist);
|
||||||
let data_extension = encode_text(&file.extension);
|
let data_extension = encode_text(&file.extension);
|
||||||
|
|
||||||
let mut path_data = String::new();
|
let mut path_data = String::new();
|
||||||
|
@ -85,23 +85,28 @@ pub fn genhtml_command(
|
||||||
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!("Extracting Tags");
|
println!("Analysing Files");
|
||||||
for file in files.iter_mut() {
|
for file in files.iter_mut() {
|
||||||
println!("Extracting tags from: {:?}", file.join_path_from_source());
|
println!("Analysing: {:?}", file.join_path_from_source());
|
||||||
|
|
||||||
let tags = extract_tags_from_file(file.to_owned())?;
|
let handler = get_format_handler(file)?;
|
||||||
file.tags = tags;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
if a.tags.title != b.tags.title {
|
|
||||||
return a.tags.title.cmp(&b.tags.title);
|
let a_tags = &a.info.tags;
|
||||||
|
let b_tags = &b.info.tags;
|
||||||
|
|
||||||
|
if a_tags.title != b_tags.title {
|
||||||
|
return a_tags.title.cmp(&b_tags.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
@ -6,7 +6,7 @@ use serde::Serialize;
|
||||||
use crate::args::CLIArgs;
|
use crate::args::CLIArgs;
|
||||||
use crate::args::GetTagsCommandArgs;
|
use crate::args::GetTagsCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use crate::utils::extract_tags_from_file;
|
use crate::utils::formats::get_format_handler;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct Tags {
|
struct Tags {
|
||||||
|
@ -32,8 +32,9 @@ pub fn get_tags_command(
|
||||||
}
|
}
|
||||||
|
|
||||||
for file in files.iter_mut() {
|
for file in files.iter_mut() {
|
||||||
let tags = extract_tags_from_file(file.clone())?;
|
let handler = get_format_handler(file)?;
|
||||||
file.tags = tags;
|
|
||||||
|
file.info.tags = handler.get_tags(true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if files.len() == 1 {
|
if files.len() == 1 {
|
||||||
|
@ -42,17 +43,17 @@ pub fn get_tags_command(
|
||||||
if get_tags_args.json {
|
if get_tags_args.json {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&from_main_tags(&file.tags)).unwrap()
|
serde_json::to_string_pretty(&from_main_tags(&file.info.tags)).unwrap()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("{:#?}", from_main_tags(&file.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.tags),
|
from_main_tags(&file.info.tags),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||||
|
@ -61,7 +62,7 @@ pub fn get_tags_command(
|
||||||
println!(
|
println!(
|
||||||
"{}: {:#?}",
|
"{}: {:#?}",
|
||||||
file.join_path_to().to_string_lossy(),
|
file.join_path_to().to_string_lossy(),
|
||||||
from_main_tags(&file.tags)
|
from_main_tags(&file.info.tags)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,13 @@ use crate::args::CLIArgs;
|
||||||
use crate::args::ProcessCommandArgs;
|
use crate::args::ProcessCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
use crate::utils::ascii_reduce::reduce_to_ascii;
|
use crate::utils::ascii_reduce::reduce_to_ascii;
|
||||||
use crate::utils::extract_tags_from_file;
|
use crate::utils::formats::get_format_handler;
|
||||||
use crate::utils::replaygain::analyze_replaygain;
|
use crate::utils::replaygain::analyze_replaygain;
|
||||||
use crate::utils::replaygain::tag_replaygain;
|
|
||||||
use crate::utils::scan_for_music;
|
use crate::utils::scan_for_music;
|
||||||
|
|
||||||
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) {
|
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) {
|
||||||
let title = &file.tags.title;
|
let title = &file.info.tags.title;
|
||||||
let artist = &file.tags.artist;
|
let artist = &file.info.tags.artist;
|
||||||
|
|
||||||
let replace_char = "_".to_string();
|
let replace_char = "_".to_string();
|
||||||
|
|
||||||
|
@ -100,7 +99,7 @@ fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut Fi
|
||||||
}
|
}
|
||||||
|
|
||||||
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.tags.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()
|
||||||
|
@ -109,7 +108,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.tags.contains_replaygain_tags && !force {
|
if file.info.replaygain.is_some() && !force {
|
||||||
println!(
|
println!(
|
||||||
"Skipping replaygain for {:?}, contains already",
|
"Skipping replaygain for {:?}, contains already",
|
||||||
file.join_path_from_source()
|
file.join_path_from_source()
|
||||||
|
@ -124,7 +123,10 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
|
||||||
|
|
||||||
let replaygain_data = analyze_replaygain(file.join_path_to())?;
|
let replaygain_data = analyze_replaygain(file.join_path_to())?;
|
||||||
|
|
||||||
tag_replaygain(file.join_path_to(), replaygain_data)?;
|
let mut handler = get_format_handler(file)?;
|
||||||
|
|
||||||
|
handler.set_replaygain_data(replaygain_data)?;
|
||||||
|
handler.save_changes()?;
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Applied replaygain tags for {:?}",
|
"Applied replaygain tags for {:?}",
|
||||||
|
@ -141,12 +143,13 @@ pub fn process_command(
|
||||||
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!("Extracting Tags");
|
println!("Analysing Files");
|
||||||
for file in files.iter_mut() {
|
for file in files.iter_mut() {
|
||||||
println!("Extracting tags from: {:?}", file.join_path_from_source());
|
println!("Analysing: {:?}", file.join_path_from_source());
|
||||||
|
|
||||||
let tags = extract_tags_from_file(file.to_owned())?;
|
let handler = get_format_handler(file)?;
|
||||||
file.tags = tags;
|
|
||||||
|
file.info = handler.get_audio_file_info(false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Renaming Files");
|
println!("Renaming Files");
|
||||||
|
|
|
@ -1,50 +1,9 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use id3::TagLike;
|
|
||||||
|
|
||||||
use crate::args::CLIArgs;
|
use crate::args::CLIArgs;
|
||||||
use crate::args::SetTagsCommandArgs;
|
use crate::args::SetTagsCommandArgs;
|
||||||
use crate::types::File;
|
use crate::types::File;
|
||||||
|
use crate::utils::formats::get_format_handler;
|
||||||
pub fn tag_mp3(
|
|
||||||
file: &File,
|
|
||||||
add_tags_args: &SetTagsCommandArgs,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut tag = id3::Tag::read_from_path(file.join_path_to())?;
|
|
||||||
|
|
||||||
if let Some(title) = &add_tags_args.title {
|
|
||||||
tag.set_title(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(artist) = &add_tags_args.artist {
|
|
||||||
tag.set_artist(artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.write_to_path(file.join_path_to(), id3::Version::Id3v24)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag_flac(
|
|
||||||
file: &File,
|
|
||||||
add_tags_args: &SetTagsCommandArgs,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut tag = metaflac::Tag::read_from_path(file.join_path_to())?;
|
|
||||||
|
|
||||||
if let Some(title) = &add_tags_args.title {
|
|
||||||
tag.remove_vorbis("TITLE");
|
|
||||||
tag.set_vorbis("TITLE", vec![title]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(artist) = &add_tags_args.artist {
|
|
||||||
tag.remove_vorbis("ARTIST");
|
|
||||||
tag.set_vorbis("ARTIST", vec![artist]);
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.write_to_path(file.join_path_to())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_tags_command(
|
pub fn set_tags_command(
|
||||||
_args: &CLIArgs,
|
_args: &CLIArgs,
|
||||||
|
@ -57,14 +16,17 @@ pub fn set_tags_command(
|
||||||
}
|
}
|
||||||
|
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
match file.extension.as_str() {
|
let mut handler = get_format_handler(file)?;
|
||||||
"mp3" => tag_mp3(file, add_tags_args)?,
|
|
||||||
"flac" => tag_flac(file, add_tags_args)?,
|
if let Some(title) = &add_tags_args.title {
|
||||||
_ => panic!(
|
handler.set_title(title.clone())?;
|
||||||
"Invalid File Extension for {}",
|
|
||||||
file.join_path_to().to_string_lossy()
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(artist) = &add_tags_args.artist {
|
||||||
|
handler.set_artist(artist.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.save_changes()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
82
src/types.rs
82
src/types.rs
|
@ -1,12 +1,75 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::utils::formats::AudioContainer;
|
||||||
|
|
||||||
#[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 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Tags {
|
||||||
|
title: "".to_string(),
|
||||||
|
artist: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
|
||||||
|
pub struct ReplayGainData {
|
||||||
|
pub track_gain: String,
|
||||||
|
pub track_peak: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplayGainData {
|
||||||
|
pub fn to_raw(&self) -> ReplayGainRawData {
|
||||||
|
let track_gain = self.track_gain.split(' ').next().unwrap_or("0.0");
|
||||||
|
let track_gain = track_gain.parse::<f64>().unwrap();
|
||||||
|
|
||||||
|
let track_peak = self.track_peak.parse::<f64>().unwrap();
|
||||||
|
|
||||||
|
ReplayGainRawData {
|
||||||
|
track_gain,
|
||||||
|
track_peak,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReplayGainRawData {
|
||||||
|
pub track_gain: f64,
|
||||||
|
pub track_peak: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplayGainRawData {
|
||||||
|
pub fn to_normal(&self) -> ReplayGainData {
|
||||||
|
ReplayGainData {
|
||||||
|
track_gain: format!("{:.2} dB", self.track_gain),
|
||||||
|
track_peak: format!("{:.6}", self.track_peak),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AudioFileInfo {
|
||||||
|
pub tags: Tags,
|
||||||
|
pub replaygain: Option<ReplayGainData>,
|
||||||
pub supports_replaygain: bool,
|
pub supports_replaygain: bool,
|
||||||
pub contains_replaygain_tags: bool,
|
pub container: AudioContainer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioFileInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
AudioFileInfo {
|
||||||
|
tags: Tags::default(),
|
||||||
|
replaygain: None,
|
||||||
|
supports_replaygain: false,
|
||||||
|
container: AudioContainer::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -14,11 +77,14 @@ pub struct File {
|
||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub extension: String,
|
pub extension: String,
|
||||||
|
|
||||||
|
// relative path to file's folder
|
||||||
|
pub path_to: PathBuf,
|
||||||
|
// path to folder from source
|
||||||
|
pub path_from_source: PathBuf,
|
||||||
|
|
||||||
pub extra_files: Vec<File>,
|
pub extra_files: Vec<File>,
|
||||||
|
|
||||||
pub path_to: PathBuf,
|
pub info: AudioFileInfo,
|
||||||
pub path_from_source: PathBuf,
|
|
||||||
pub tags: Tags,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl File {
|
impl File {
|
||||||
|
@ -52,14 +118,10 @@ impl File {
|
||||||
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(),
|
||||||
tags: Tags {
|
info: AudioFileInfo::default(),
|
||||||
title: "".to_string(),
|
|
||||||
artist: "".to_string(),
|
|
||||||
supports_replaygain: false,
|
|
||||||
contains_replaygain_tags: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join_filename(&self) -> String {
|
pub fn join_filename(&self) -> String {
|
||||||
format!("{}.{}", self.filename, self.extension)
|
format!("{}.{}", self.filename, self.extension)
|
||||||
}
|
}
|
||||||
|
|
112
src/utils/formats/flac.rs
Normal file
112
src/utils/formats/flac.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||||
|
|
||||||
|
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||||
|
|
||||||
|
pub struct FLACAudioFormat {
|
||||||
|
flac_tags: metaflac::Tag,
|
||||||
|
path: Box<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flac_get_first(tag: &metaflac::Tag, key: &str) -> Option<String> {
|
||||||
|
if let Some(Some(v)) = tag.vorbis_comments().map(|c| c.get(key)) {
|
||||||
|
if !v.is_empty() {
|
||||||
|
Some(v[0].to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioContainerFormat for FLACAudioFormat {
|
||||||
|
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||||
|
let title = flac_get_first(&self.flac_tags, "TITLE");
|
||||||
|
let artist = flac_get_first(&self.flac_tags, "ARTIST");
|
||||||
|
|
||||||
|
if !allow_missing {
|
||||||
|
if title.is_none() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||||
|
}
|
||||||
|
if artist.is_none() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Tags {
|
||||||
|
title: title.unwrap(),
|
||||||
|
artist: artist.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||||
|
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
|
||||||
|
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
|
||||||
|
|
||||||
|
if track_gain.is_none() || track_peak.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ReplayGainData {
|
||||||
|
track_gain: track_gain.unwrap(),
|
||||||
|
track_peak: track_peak.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_replaygain(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||||
|
self.flac_tags.remove_vorbis("TITLE");
|
||||||
|
self.flac_tags.set_vorbis("TITLE", vec![title]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||||
|
self.flac_tags.remove_vorbis("ARTIST");
|
||||||
|
self.flac_tags.set_vorbis("ARTIST", vec![artist]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||||
|
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
|
||||||
|
self.flac_tags.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
|
||||||
|
|
||||||
|
self.flac_tags.set_vorbis(
|
||||||
|
"REPLAYGAIN_TRACK_GAIN",
|
||||||
|
vec![format!("{:.2} dB", data.track_gain)],
|
||||||
|
);
|
||||||
|
self.flac_tags.set_vorbis(
|
||||||
|
"REPLAYGAIN_TRACK_PEAK",
|
||||||
|
vec![format!("{:.6}", data.track_peak)],
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||||
|
self.flac_tags.write_to_path(self.path.as_path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||||
|
return Ok(AudioFileInfo {
|
||||||
|
tags: self.get_tags(allow_missing_tags)?,
|
||||||
|
replaygain: self.get_replaygain_data(),
|
||||||
|
supports_replaygain: self.supports_replaygain(),
|
||||||
|
container: AudioContainer::FLAC,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_flac_format_handler(path: &PathBuf) -> Result<FLACAudioFormat, BoxedError> {
|
||||||
|
Ok(FLACAudioFormat {
|
||||||
|
flac_tags: metaflac::Tag::read_from_path(path)?,
|
||||||
|
path: Box::new(path.clone()),
|
||||||
|
})
|
||||||
|
}
|
224
src/utils/formats/generic_ffmpeg.rs
Normal file
224
src/utils/formats/generic_ffmpeg.rs
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
use std::{path::{PathBuf, Path}, process::Command};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use string_error::static_err;
|
||||||
|
|
||||||
|
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||||
|
|
||||||
|
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Changes {
|
||||||
|
title: Option<String>,
|
||||||
|
artist: Option<String>,
|
||||||
|
replaygain_data: Option<ReplayGainRawData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ExtractedData {
|
||||||
|
tags: Tags,
|
||||||
|
replaygain_data: Option<ReplayGainData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct FFProbeOutput {
|
||||||
|
pub format: FFProbeFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct FFProbeFormat {
|
||||||
|
pub tags: FFProbeTags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct FFProbeTags {
|
||||||
|
#[serde(alias = "TITLE")]
|
||||||
|
pub title: String,
|
||||||
|
#[serde(default, alias = "ARTIST")]
|
||||||
|
pub artist: String,
|
||||||
|
|
||||||
|
#[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")]
|
||||||
|
pub replaygain_track_peak: Option<String>,
|
||||||
|
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
|
||||||
|
pub replaygain_track_gain: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FFProbeTags {
|
||||||
|
fn default() -> Self {
|
||||||
|
FFProbeTags {
|
||||||
|
title: "".to_string(),
|
||||||
|
artist: "".to_string(),
|
||||||
|
replaygain_track_peak: None,
|
||||||
|
replaygain_track_gain: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GenericFFMpegAudioFormat {
|
||||||
|
container_type: AudioContainer,
|
||||||
|
path: Box<PathBuf>,
|
||||||
|
|
||||||
|
extracted_data: ExtractedData,
|
||||||
|
changes: Changes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericFFMpegAudioFormat {
|
||||||
|
fn analyze(&mut self) -> Result<(), BoxedError> {
|
||||||
|
let output = Command::new(crate::meta::FFPROBE)
|
||||||
|
.args([
|
||||||
|
"-v",
|
||||||
|
"quiet",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
|
"-show_format",
|
||||||
|
&self.path.to_string_lossy(),
|
||||||
|
])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
print!("{:?}", String::from_utf8(output.stderr).unwrap());
|
||||||
|
return Err(static_err("FFprobe Crashed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_str = String::from_utf8(output.stdout).unwrap();
|
||||||
|
let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?;
|
||||||
|
|
||||||
|
let tags = ffprobe_out.format.tags;
|
||||||
|
|
||||||
|
self.extracted_data.tags = Tags {
|
||||||
|
title: tags.title,
|
||||||
|
artist: tags.artist,
|
||||||
|
};
|
||||||
|
|
||||||
|
if tags.replaygain_track_gain.is_some() && tags.replaygain_track_peak.is_some() {
|
||||||
|
self.extracted_data.replaygain_data = Some(ReplayGainData {
|
||||||
|
track_gain: tags.replaygain_track_gain.unwrap(),
|
||||||
|
track_peak: tags.replaygain_track_peak.unwrap(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.extracted_data.replaygain_data = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioContainerFormat for GenericFFMpegAudioFormat {
|
||||||
|
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||||
|
let mut tags = self.extracted_data.tags.clone();
|
||||||
|
|
||||||
|
if let Some(title) = &self.changes.title {
|
||||||
|
tags.title = title.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(artist) = &self.changes.title {
|
||||||
|
tags.artist = artist.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allow_missing {
|
||||||
|
if tags.title.is_empty() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||||
|
}
|
||||||
|
if tags.artist.is_empty() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||||
|
if let Some(data) = &self.changes.replaygain_data {
|
||||||
|
return Some(data.to_normal());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.extracted_data.replaygain_data.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_replaygain(&self) -> bool {
|
||||||
|
match self.container_type {
|
||||||
|
AudioContainer::MP3 => true,
|
||||||
|
AudioContainer::FLAC => true,
|
||||||
|
AudioContainer::WAV => true,
|
||||||
|
AudioContainer::OGG => false, // ffprobe can't do OGG tags
|
||||||
|
AudioContainer::AIFF => true,
|
||||||
|
AudioContainer::Unknown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||||
|
self.changes.title = Some(title);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||||
|
self.changes.artist = Some(artist);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||||
|
self.changes.replaygain_data = Some(data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||||
|
let mut args: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let tempdir = tempfile::tempdir()?;
|
||||||
|
let temp_file = tempdir
|
||||||
|
.path()
|
||||||
|
.join(PathBuf::from(self.path.file_name().unwrap()));
|
||||||
|
|
||||||
|
args.extend(vec![
|
||||||
|
"-i".to_string(),
|
||||||
|
self.path.to_string_lossy().to_string(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
args.extend(vec!["-c".to_string(), "copy".to_string()]);
|
||||||
|
|
||||||
|
if let Some(title) = self.changes.title.clone() {
|
||||||
|
args.extend(vec![
|
||||||
|
"-metadata".to_string(),
|
||||||
|
format!("title=\"{}\"", title),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(artist) = self.changes.artist.clone() {
|
||||||
|
args.extend(vec!["-metadata".to_string(), format!("artist={}", artist)])
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(temp_file.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
let output = Command::new(crate::meta::FFMPEG).args(args).output()?;
|
||||||
|
|
||||||
|
println!("{:?}", String::from_utf8(output.stderr));
|
||||||
|
|
||||||
|
std::fs::copy(temp_file, self.path.to_path_buf())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||||
|
return Ok(AudioFileInfo {
|
||||||
|
tags: self.get_tags(allow_missing_tags)?,
|
||||||
|
replaygain: self.get_replaygain_data(),
|
||||||
|
supports_replaygain: self.supports_replaygain(),
|
||||||
|
container: self.container_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_generic_ffmpeg_format_handler(
|
||||||
|
path: &Path,
|
||||||
|
container_type: AudioContainer,
|
||||||
|
) -> Result<GenericFFMpegAudioFormat, BoxedError> {
|
||||||
|
let mut handler = GenericFFMpegAudioFormat {
|
||||||
|
container_type,
|
||||||
|
path: Box::new(path.to_path_buf()),
|
||||||
|
extracted_data: ExtractedData::default(),
|
||||||
|
changes: Changes::default(),
|
||||||
|
};
|
||||||
|
handler.analyze()?;
|
||||||
|
|
||||||
|
Ok(handler)
|
||||||
|
}
|
149
src/utils/formats/id3.rs
Normal file
149
src/utils/formats/id3.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use id3::TagLike;
|
||||||
|
|
||||||
|
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||||
|
|
||||||
|
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||||
|
|
||||||
|
pub struct ID3AudioFormat {
|
||||||
|
container_type: AudioContainer,
|
||||||
|
id3_tags: id3::Tag,
|
||||||
|
path: Box<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioContainerFormat for ID3AudioFormat {
|
||||||
|
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||||
|
let title = self.id3_tags.title();
|
||||||
|
let artist = self.id3_tags.artist();
|
||||||
|
|
||||||
|
if !allow_missing {
|
||||||
|
if title.is_none() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||||
|
}
|
||||||
|
if artist.is_none() {
|
||||||
|
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Tags {
|
||||||
|
title: String::from(title.unwrap()),
|
||||||
|
artist: String::from(artist.unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||||
|
let frames = self.id3_tags.frames();
|
||||||
|
|
||||||
|
let mut contains_replaygain_tags = false;
|
||||||
|
let mut replaygain_data = ReplayGainData {
|
||||||
|
track_gain: "".to_string(),
|
||||||
|
track_peak: "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for frame in frames {
|
||||||
|
if frame.id() == "TXXX" {
|
||||||
|
if let Some(extended_text) = frame.content().extended_text() {
|
||||||
|
match extended_text.description.as_str() {
|
||||||
|
"REPLAYGAIN_TRACK_GAIN" => {
|
||||||
|
contains_replaygain_tags = true;
|
||||||
|
replaygain_data.track_gain = extended_text.value.clone()
|
||||||
|
}
|
||||||
|
"REPLAYGAIN_TRACK_PEAK" => {
|
||||||
|
contains_replaygain_tags = true;
|
||||||
|
replaygain_data.track_peak = extended_text.value.clone()
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains_replaygain_tags {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(replaygain_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_replaygain(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||||
|
self.id3_tags.set_title(title);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||||
|
self.id3_tags.set_artist(artist);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||||
|
let frames = self.id3_tags.remove("TXXX");
|
||||||
|
|
||||||
|
for frame in frames {
|
||||||
|
if let Some(extended_text) = frame.content().extended_text() {
|
||||||
|
if extended_text.description.starts_with("REPLAYGAIN") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.id3_tags.add_frame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||||
|
"TXXX",
|
||||||
|
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||||
|
description: "REPLAYGAIN_TRACK_GAIN".to_string(),
|
||||||
|
value: format!("{:.2} dB", data.track_gain),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
self.id3_tags.add_frame(id3::Frame::with_content(
|
||||||
|
"TXXX",
|
||||||
|
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
||||||
|
description: "REPLAYGAIN_TRACK_PEAK".to_string(),
|
||||||
|
value: format!("{:.6}", data.track_peak),
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||||
|
self.id3_tags
|
||||||
|
.write_to_path(self.path.as_path(), id3::Version::Id3v24)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||||
|
return Ok(AudioFileInfo {
|
||||||
|
tags: self.get_tags(allow_missing_tags)?,
|
||||||
|
replaygain: self.get_replaygain_data(),
|
||||||
|
supports_replaygain: self.supports_replaygain(),
|
||||||
|
container: self.container_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_id3_format_handler(
|
||||||
|
path: &PathBuf,
|
||||||
|
container_type: AudioContainer,
|
||||||
|
) -> Result<ID3AudioFormat, BoxedError> {
|
||||||
|
let id3_tags = match container_type {
|
||||||
|
// Only works on ID3 containing WAV files, but doesn't seem very widespread
|
||||||
|
AudioContainer::WAV => id3::Tag::read_from_wav_path(path)?,
|
||||||
|
AudioContainer::AIFF => id3::Tag::read_from_aiff_path(path)?,
|
||||||
|
|
||||||
|
_ => id3::Tag::read_from_path(path)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ID3AudioFormat {
|
||||||
|
container_type,
|
||||||
|
id3_tags,
|
||||||
|
path: Box::new(path.clone()),
|
||||||
|
})
|
||||||
|
}
|
76
src/utils/formats/mod.rs
Normal file
76
src/utils/formats/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
pub mod flac;
|
||||||
|
pub mod generic_ffmpeg;
|
||||||
|
pub mod id3;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::types::{AudioFileInfo, File, ReplayGainData, ReplayGainRawData, Tags};
|
||||||
|
|
||||||
|
use self::flac::new_flac_format_handler;
|
||||||
|
use self::id3::new_id3_format_handler;
|
||||||
|
|
||||||
|
type BoxedError = Box<dyn Error>;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum AudioContainer {
|
||||||
|
MP3,
|
||||||
|
FLAC,
|
||||||
|
WAV,
|
||||||
|
OGG,
|
||||||
|
AIFF,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AudioContainerFormat {
|
||||||
|
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
|
||||||
|
fn get_replaygain_data(&self) -> Option<ReplayGainData>;
|
||||||
|
fn supports_replaygain(&self) -> bool;
|
||||||
|
|
||||||
|
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
|
||||||
|
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError>;
|
||||||
|
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError>;
|
||||||
|
|
||||||
|
fn save_changes(&mut self) -> Result<(), BoxedError>;
|
||||||
|
|
||||||
|
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AudioFormatError {
|
||||||
|
#[error("title missing from tags")]
|
||||||
|
MissingTitle,
|
||||||
|
#[error("artist missing from tags")]
|
||||||
|
MissingArtist,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_format_handler(file: &File) -> Result<Box<dyn AudioContainerFormat>, BoxedError> {
|
||||||
|
// TODO: detect format from magic bytes rather than extension
|
||||||
|
match file.extension.as_str() {
|
||||||
|
"mp3" => {
|
||||||
|
return Ok(Box::new(new_id3_format_handler(
|
||||||
|
&file.join_path_to(),
|
||||||
|
AudioContainer::MP3,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
// "aiff" => return Ok(Box::new(new_generic_ffmpeg_format_handler(&file.join_path_to(), AudioContainer::AIFF)?)),
|
||||||
|
// "wav" => return Ok(Box::new(new_generic_ffmpeg_format_handler(&file.join_path_to(), AudioContainer::WAV)?)),
|
||||||
|
"flac" => return Ok(Box::new(new_flac_format_handler(&file.join_path_to())?)),
|
||||||
|
_ => {
|
||||||
|
panic!("unsupported filetype");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
||||||
|
let ext = file_path.extension();
|
||||||
|
if ext.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = ext.unwrap().to_str().unwrap();
|
||||||
|
|
||||||
|
matches!(ext, "mp3" | "flac")
|
||||||
|
}
|
|
@ -1,20 +1,10 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub mod ascii_reduce;
|
pub mod ascii_reduce;
|
||||||
pub(self) mod music_scanner;
|
|
||||||
pub mod replaygain;
|
pub mod replaygain;
|
||||||
pub(self) mod tag_extractor;
|
|
||||||
pub mod transcoder;
|
pub mod transcoder;
|
||||||
|
|
||||||
|
pub mod formats;
|
||||||
|
pub(self) mod music_scanner;
|
||||||
|
|
||||||
pub use music_scanner::scan_for_music;
|
pub use music_scanner::scan_for_music;
|
||||||
pub use tag_extractor::extract_tags_from_file;
|
|
||||||
|
|
||||||
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
pub use formats::is_supported_file_extension;
|
||||||
let ext = file_path.extension();
|
|
||||||
if ext.is_none() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let ext = ext.unwrap().to_str().unwrap();
|
|
||||||
|
|
||||||
matches!(ext, "mp3" | "flac")
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,16 +4,11 @@ use std::{
|
||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use id3::TagLike;
|
|
||||||
use string_error::static_err;
|
use string_error::static_err;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
use crate::types::ReplayGainRawData;
|
||||||
pub struct ReplayGainData {
|
|
||||||
pub track_gain: f64,
|
|
||||||
pub track_peak: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainData, 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",
|
||||||
|
@ -100,83 +95,8 @@ pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainData, Box<dyn std::
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ReplayGainData {
|
Ok(ReplayGainRawData {
|
||||||
track_gain,
|
track_gain,
|
||||||
track_peak,
|
track_peak,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tag_replaygain_mp3(
|
|
||||||
path: PathBuf,
|
|
||||||
data: ReplayGainData,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut tag = id3::Tag::read_from_path(&path)?;
|
|
||||||
|
|
||||||
let frames = tag.remove("TXXX");
|
|
||||||
|
|
||||||
for frame in frames {
|
|
||||||
if let Some(extended_text) = frame.content().extended_text() {
|
|
||||||
if extended_text.description.starts_with("REPLAYGAIN") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag.add_frame(frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.add_frame(id3::Frame::with_content(
|
|
||||||
"TXXX",
|
|
||||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
|
||||||
description: "REPLAYGAIN_TRACK_GAIN".to_string(),
|
|
||||||
value: format!("{:.2} dB", data.track_gain),
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
tag.add_frame(id3::Frame::with_content(
|
|
||||||
"TXXX",
|
|
||||||
id3::Content::ExtendedText(id3::frame::ExtendedText {
|
|
||||||
description: "REPLAYGAIN_TRACK_PEAK".to_string(),
|
|
||||||
value: format!("{:.6}", data.track_peak),
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
tag.write_to_path(path, id3::Version::Id3v24)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag_replaygain_flac(
|
|
||||||
path: PathBuf,
|
|
||||||
data: ReplayGainData,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut tag = metaflac::Tag::read_from_path(&path)?;
|
|
||||||
tag.remove_vorbis("REPLAYGAIN_TRACK_GAIN");
|
|
||||||
tag.remove_vorbis("REPLAYGAIN_TRACK_PEAK");
|
|
||||||
|
|
||||||
tag.set_vorbis(
|
|
||||||
"REPLAYGAIN_TRACK_GAIN",
|
|
||||||
vec![format!("{:.2} dB", data.track_gain)],
|
|
||||||
);
|
|
||||||
tag.set_vorbis(
|
|
||||||
"REPLAYGAIN_TRACK_PEAK",
|
|
||||||
vec![format!("{:.6}", data.track_peak)],
|
|
||||||
);
|
|
||||||
|
|
||||||
tag.write_to_path(path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tag_replaygain(
|
|
||||||
path: PathBuf,
|
|
||||||
data: ReplayGainData,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let extension = path.extension().unwrap().to_string_lossy().to_string();
|
|
||||||
|
|
||||||
if extension == "mp3" {
|
|
||||||
return tag_replaygain_mp3(path, data);
|
|
||||||
} else if extension == "flac" {
|
|
||||||
return tag_replaygain_flac(path, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
panic!("extension not tagable");
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
use id3::TagLike;
|
|
||||||
|
|
||||||
use crate::types::{File, Tags};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn extract_tags(filepath: PathBuf) -> Result<Tags, Box<dyn std::error::Error>> {
|
|
||||||
let file_extension = filepath.extension().unwrap();
|
|
||||||
|
|
||||||
if file_extension == "mp3" {
|
|
||||||
let tag = id3::Tag::read_from_path(&filepath)?;
|
|
||||||
|
|
||||||
let mut contains_replaygain_tags = false;
|
|
||||||
|
|
||||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#ID3v2
|
|
||||||
let frames = tag.frames();
|
|
||||||
for frame in frames {
|
|
||||||
if frame.id() == "TXXX" {
|
|
||||||
if let Some(extended_text) = frame.content().extended_text() {
|
|
||||||
if extended_text.value == "REPLAYGAIN_TRACK_GAIN" {
|
|
||||||
contains_replaygain_tags = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(Tags {
|
|
||||||
title: String::from(tag.title().unwrap()),
|
|
||||||
artist: String::from(tag.artist().unwrap()),
|
|
||||||
supports_replaygain: true,
|
|
||||||
contains_replaygain_tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if file_extension == "flac" {
|
|
||||||
let tag = metaflac::Tag::read_from_path(filepath)?;
|
|
||||||
|
|
||||||
fn get_first(tag: &metaflac::Tag, key: &str) -> Option<String> {
|
|
||||||
if let Some(Some(v)) = tag.vorbis_comments().map(|c| c.get(key)) {
|
|
||||||
if !v.is_empty() {
|
|
||||||
Some(v[0].to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rpg = get_first(&tag, "REPLAYGAIN_TRACK_GAIN");
|
|
||||||
|
|
||||||
return Ok(Tags {
|
|
||||||
title: get_first(&tag, "TITLE").unwrap(),
|
|
||||||
artist: get_first(&tag, "ARTIST").unwrap(),
|
|
||||||
supports_replaygain: true,
|
|
||||||
contains_replaygain_tags: rpg.is_some(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
panic!("wrong extension")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_tags_from_file(file: File) -> Result<Tags, Box<dyn std::error::Error>> {
|
|
||||||
extract_tags(file.join_path_to())
|
|
||||||
}
|
|
Loading…
Reference in a new issue