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
|
||||
.direnv
|
||||
musicutil_old
|
||||
|
||||
# Added by cargo
|
||||
temp
|
||||
|
||||
/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"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
|
@ -497,7 +491,6 @@ dependencies = [
|
|||
name = "musicutil"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"crossbeam",
|
||||
"html-escape",
|
||||
|
@ -511,6 +504,7 @@ dependencies = [
|
|||
"serde_yaml",
|
||||
"string-error",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
@ -763,6 +757,26 @@ version = "0.15.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.5"
|
||||
|
|
|
@ -29,8 +29,8 @@ metaflac = "0.2.5"
|
|||
html-escape = "0.2.11"
|
||||
|
||||
# error handling
|
||||
thiserror = "1.0"
|
||||
string-error = "0.1.0"
|
||||
anyhow = "1.0.66"
|
||||
|
||||
# temporary file for transcode prefix file
|
||||
tempfile = "3"
|
||||
|
|
102
flake.nix
102
flake.nix
|
@ -10,53 +10,71 @@
|
|||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, ... }:
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
{
|
||||
overlay = final: prev:
|
||||
let system = final.system;
|
||||
in {
|
||||
musicutil = final.rustPlatform.buildRustPackage rec {
|
||||
pname = "musicutil";
|
||||
version = "latest";
|
||||
|
||||
src = ./.;
|
||||
cargoLock = { lockFile = ./Cargo.lock; };
|
||||
|
||||
postPatch = ''
|
||||
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
|
||||
substituteInPlace src/meta.rs --replace 'ffprobe' '${final.ffmpeg}/bin/ffprobe'
|
||||
'';
|
||||
|
||||
doCheck = false;
|
||||
nativeBuildInputs = with final.pkgs; [ pkg-config rustc cargo ];
|
||||
buildInputs = with final; [ ffmpeg ];
|
||||
};
|
||||
};
|
||||
} // utils.lib.eachSystem (utils.lib.defaultSystems) (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlay ];
|
||||
};
|
||||
overlay = final: prev: let
|
||||
system = final.system;
|
||||
in {
|
||||
defaultPackage = self.packages."${system}".musicutil;
|
||||
packages.musicutil = pkgs.musicutil;
|
||||
musicutil = final.rustPlatform.buildRustPackage rec {
|
||||
pname = "musicutil";
|
||||
version = "latest";
|
||||
|
||||
apps = rec {
|
||||
musicutil = {
|
||||
type = "app";
|
||||
program = "${self.defaultPackage.${system}}/bin/musicutil";
|
||||
};
|
||||
default = musicutil;
|
||||
src = ./.;
|
||||
cargoLock = {lockFile = ./Cargo.lock;};
|
||||
|
||||
postPatch = ''
|
||||
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
|
||||
substituteInPlace src/meta.rs --replace 'ffprobe' '${final.ffmpeg}/bin/ffprobe'
|
||||
'';
|
||||
|
||||
doCheck = false;
|
||||
nativeBuildInputs = with final.pkgs; [pkg-config rustc cargo];
|
||||
buildInputs = with final; [ffmpeg];
|
||||
};
|
||||
};
|
||||
}
|
||||
// utils.lib.eachSystem (utils.lib.defaultSystems) (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [self.overlay];
|
||||
};
|
||||
in {
|
||||
defaultPackage = self.packages."${system}".musicutil;
|
||||
packages.musicutil = pkgs.musicutil;
|
||||
|
||||
defaultApp = self.apps."${system}".musicutil;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
||||
buildInputs = with pkgs; [ rustc cargo clippy rust-analyzer rustfmt ];
|
||||
apps = rec {
|
||||
musicutil = {
|
||||
type = "app";
|
||||
program = "${self.defaultPackage.${system}}/bin/musicutil";
|
||||
};
|
||||
default = musicutil;
|
||||
};
|
||||
|
||||
lib = pkgs.musicutil.lib;
|
||||
});
|
||||
defaultApp = self.apps."${system}".musicutil;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ use crate::{
|
|||
args::{CLIArgs, CopyCommandArgs},
|
||||
types::File,
|
||||
utils::{
|
||||
extract_tags_from_file, scan_for_music,
|
||||
formats::get_format_handler,
|
||||
scan_for_music,
|
||||
transcoder::{
|
||||
presets::{print_presets, transcode_preset_or_config},
|
||||
transcode,
|
||||
|
@ -40,12 +41,13 @@ pub fn copy_command(
|
|||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(©_args.source)?;
|
||||
|
||||
println!("Extracting Tags");
|
||||
println!("Analysing Files");
|
||||
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())?;
|
||||
file.tags = tags;
|
||||
let handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(true)?;
|
||||
}
|
||||
|
||||
if copy_args.single_directory {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::args::CLIArgs;
|
||||
use crate::args::GenHTMLCommandArgs;
|
||||
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 std::cmp::Ordering;
|
||||
use std::io::Write;
|
||||
|
@ -41,8 +41,8 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
|
|||
false => "pure-table-even",
|
||||
};
|
||||
|
||||
let data_title = encode_text(&file.tags.title);
|
||||
let data_artist = encode_text(&file.tags.artist);
|
||||
let data_title = encode_text(&file.info.tags.title);
|
||||
let data_artist = encode_text(&file.info.tags.artist);
|
||||
let data_extension = encode_text(&file.extension);
|
||||
|
||||
let mut path_data = String::new();
|
||||
|
@ -85,23 +85,28 @@ pub fn genhtml_command(
|
|||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&genhtml_args.source)?;
|
||||
|
||||
println!("Extracting Tags");
|
||||
println!("Analysing Files");
|
||||
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())?;
|
||||
file.tags = tags;
|
||||
let handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
||||
files.sort_by(|a, b| -> Ordering {
|
||||
if a.path_from_source != b.path_from_source {
|
||||
return a.path_from_source.cmp(&b.path_from_source);
|
||||
}
|
||||
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();
|
||||
|
|
|
@ -6,7 +6,7 @@ use serde::Serialize;
|
|||
use crate::args::CLIArgs;
|
||||
use crate::args::GetTagsCommandArgs;
|
||||
use crate::types::File;
|
||||
use crate::utils::extract_tags_from_file;
|
||||
use crate::utils::formats::get_format_handler;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct Tags {
|
||||
|
@ -32,8 +32,9 @@ pub fn get_tags_command(
|
|||
}
|
||||
|
||||
for file in files.iter_mut() {
|
||||
let tags = extract_tags_from_file(file.clone())?;
|
||||
file.tags = tags;
|
||||
let handler = get_format_handler(file)?;
|
||||
|
||||
file.info.tags = handler.get_tags(true)?;
|
||||
}
|
||||
|
||||
if files.len() == 1 {
|
||||
|
@ -42,17 +43,17 @@ pub fn get_tags_command(
|
|||
if get_tags_args.json {
|
||||
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 {
|
||||
println!("{:#?}", from_main_tags(&file.tags));
|
||||
println!("{:#?}", from_main_tags(&file.info.tags));
|
||||
}
|
||||
} else if get_tags_args.json {
|
||||
let mut result: HashMap<String, Tags> = HashMap::new();
|
||||
for file in files.iter() {
|
||||
result.insert(
|
||||
file.join_path_to().to_string_lossy().to_string(),
|
||||
from_main_tags(&file.tags),
|
||||
from_main_tags(&file.info.tags),
|
||||
);
|
||||
}
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
|
@ -61,7 +62,7 @@ pub fn get_tags_command(
|
|||
println!(
|
||||
"{}: {:#?}",
|
||||
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::types::File;
|
||||
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::tag_replaygain;
|
||||
use crate::utils::scan_for_music;
|
||||
|
||||
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) {
|
||||
let title = &file.tags.title;
|
||||
let artist = &file.tags.artist;
|
||||
let title = &file.info.tags.title;
|
||||
let artist = &file.info.tags.artist;
|
||||
|
||||
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>> {
|
||||
if !file.tags.supports_replaygain {
|
||||
if !file.info.supports_replaygain {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, not supported",
|
||||
file.join_path_from_source()
|
||||
|
@ -109,7 +108,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if file.tags.contains_replaygain_tags && !force {
|
||||
if file.info.replaygain.is_some() && !force {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, contains already",
|
||||
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())?;
|
||||
|
||||
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!(
|
||||
"Applied replaygain tags for {:?}",
|
||||
|
@ -141,12 +143,13 @@ pub fn process_command(
|
|||
println!("Scanning For Music");
|
||||
let mut files = scan_for_music(&process_args.source)?;
|
||||
|
||||
println!("Extracting Tags");
|
||||
println!("Analysing Files");
|
||||
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())?;
|
||||
file.tags = tags;
|
||||
let handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
||||
println!("Renaming Files");
|
||||
|
|
|
@ -1,50 +1,9 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use id3::TagLike;
|
||||
|
||||
use crate::args::CLIArgs;
|
||||
use crate::args::SetTagsCommandArgs;
|
||||
use crate::types::File;
|
||||
|
||||
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(())
|
||||
}
|
||||
use crate::utils::formats::get_format_handler;
|
||||
|
||||
pub fn set_tags_command(
|
||||
_args: &CLIArgs,
|
||||
|
@ -57,14 +16,17 @@ pub fn set_tags_command(
|
|||
}
|
||||
|
||||
for file in files.iter() {
|
||||
match file.extension.as_str() {
|
||||
"mp3" => tag_mp3(file, add_tags_args)?,
|
||||
"flac" => tag_flac(file, add_tags_args)?,
|
||||
_ => panic!(
|
||||
"Invalid File Extension for {}",
|
||||
file.join_path_to().to_string_lossy()
|
||||
),
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
if let Some(title) = &add_tags_args.title {
|
||||
handler.set_title(title.clone())?;
|
||||
}
|
||||
|
||||
if let Some(artist) = &add_tags_args.artist {
|
||||
handler.set_artist(artist.clone())?;
|
||||
}
|
||||
|
||||
handler.save_changes()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
82
src/types.rs
82
src/types.rs
|
@ -1,12 +1,75 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::utils::formats::AudioContainer;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tags {
|
||||
pub title: 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 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)]
|
||||
|
@ -14,11 +77,14 @@ pub struct File {
|
|||
pub filename: String,
|
||||
pub extension: String,
|
||||
|
||||
// relative path to file's folder
|
||||
pub path_to: PathBuf,
|
||||
// path to folder from source
|
||||
pub path_from_source: PathBuf,
|
||||
|
||||
pub extra_files: Vec<File>,
|
||||
|
||||
pub path_to: PathBuf,
|
||||
pub path_from_source: PathBuf,
|
||||
pub tags: Tags,
|
||||
pub info: AudioFileInfo,
|
||||
}
|
||||
|
||||
impl File {
|
||||
|
@ -52,14 +118,10 @@ impl File {
|
|||
path_from_source: folder_path_from_src,
|
||||
path_to,
|
||||
extra_files: Vec::new(),
|
||||
tags: Tags {
|
||||
title: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
supports_replaygain: false,
|
||||
contains_replaygain_tags: false,
|
||||
},
|
||||
info: AudioFileInfo::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_filename(&self) -> String {
|
||||
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(self) mod music_scanner;
|
||||
pub mod replaygain;
|
||||
pub(self) mod tag_extractor;
|
||||
pub mod transcoder;
|
||||
|
||||
pub mod formats;
|
||||
pub(self) mod music_scanner;
|
||||
|
||||
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 {
|
||||
let ext = file_path.extension();
|
||||
if ext.is_none() {
|
||||
return false;
|
||||
}
|
||||
let ext = ext.unwrap().to_str().unwrap();
|
||||
|
||||
matches!(ext, "mp3" | "flac")
|
||||
}
|
||||
pub use formats::is_supported_file_extension;
|
||||
|
|
|
@ -4,16 +4,11 @@ use std::{
|
|||
process::Command,
|
||||
};
|
||||
|
||||
use id3::TagLike;
|
||||
use string_error::static_err;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReplayGainData {
|
||||
pub track_gain: f64,
|
||||
pub track_peak: f64,
|
||||
}
|
||||
use crate::types::ReplayGainRawData;
|
||||
|
||||
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)
|
||||
.args([
|
||||
"-hide_banner",
|
||||
|
@ -100,83 +95,8 @@ pub fn analyze_replaygain(path: PathBuf) -> Result<ReplayGainData, Box<dyn std::
|
|||
break;
|
||||
}
|
||||
}
|
||||
Ok(ReplayGainData {
|
||||
Ok(ReplayGainRawData {
|
||||
track_gain,
|
||||
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