improve format handling & add taglib wrapper & support for more formats
This commit is contained in:
parent
da77dc89ac
commit
aadb338d75
187
Cargo.lock
generated
187
Cargo.lock
generated
|
@ -25,6 +25,28 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.63.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -37,6 +59,38 @@ version = "1.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfb"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
|
@ -49,6 +103,17 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.22"
|
||||
|
@ -199,6 +264,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.8.0"
|
||||
|
@ -271,6 +342,12 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
@ -334,6 +411,15 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "infer"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a898e4b7951673fce96614ce5751d13c40fc5674bc2d759288e46c3ab62598b3"
|
||||
dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.7.1"
|
||||
|
@ -406,6 +492,16 @@ version = "0.2.135"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.17"
|
||||
|
@ -415,6 +511,12 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
|
@ -435,6 +537,12 @@ dependencies = [
|
|||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.4"
|
||||
|
@ -491,10 +599,12 @@ dependencies = [
|
|||
name = "musicutil"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"clap",
|
||||
"crossbeam",
|
||||
"html-escape",
|
||||
"id3",
|
||||
"infer",
|
||||
"lazy_static",
|
||||
"metaflac",
|
||||
"notify",
|
||||
|
@ -503,6 +613,7 @@ dependencies = [
|
|||
"serde_with",
|
||||
"serde_yaml",
|
||||
"string-error",
|
||||
"taglib",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
|
@ -519,6 +630,16 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5507769c4919c998e69e49c839d9dc6e693ede4cc4290d6ad8b41d4f09c548c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "4.0.17"
|
||||
|
@ -549,6 +670,18 @@ version = "6.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
|
@ -600,6 +733,21 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
|
@ -609,6 +757,12 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
|
@ -696,6 +850,12 @@ dependencies = [
|
|||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.7"
|
||||
|
@ -728,6 +888,16 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taglib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
|
@ -795,6 +965,12 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
@ -812,6 +988,17 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
|
||||
dependencies = [
|
||||
"either",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
|
|
17
Cargo.toml
17
Cargo.toml
|
@ -3,6 +3,12 @@ name = "musicutil"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"modules/taglib",
|
||||
]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
@ -21,9 +27,14 @@ lazy_static = "1.4.0"
|
|||
# for scan_for_music
|
||||
walkdir = "2.3.2"
|
||||
|
||||
# format detection
|
||||
infer = "0.12.0"
|
||||
bytes = "1.3.0"
|
||||
|
||||
# tag reading
|
||||
id3 = "1.3.0"
|
||||
metaflac = "0.2.5"
|
||||
taglib = { path = "./modules/taglib", optional = true }
|
||||
|
||||
# for genhtml command
|
||||
html-escape = "0.2.11"
|
||||
|
@ -39,4 +50,8 @@ tempfile = "3"
|
|||
notify = "4.0.17"
|
||||
|
||||
# scoped threads
|
||||
crossbeam = "0.8"
|
||||
crossbeam = "0.8"
|
||||
|
||||
[features]
|
||||
default = ["taglib"]
|
||||
taglib = ["dep:taglib"]
|
27
flake.nix
27
flake.nix
|
@ -19,22 +19,37 @@
|
|||
{
|
||||
overlay = final: prev: let
|
||||
system = final.system;
|
||||
pkgs = final.pkgs;
|
||||
lib = pkgs.lib;
|
||||
stdenv = pkgs.stdenv;
|
||||
in {
|
||||
musicutil = final.rustPlatform.buildRustPackage rec {
|
||||
musicutil = pkgs.rustPlatform.buildRustPackage rec {
|
||||
pname = "musicutil";
|
||||
version = "latest";
|
||||
|
||||
src = ./.;
|
||||
cargoLock = {lockFile = ./Cargo.lock;};
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
|
||||
preBuild = ''
|
||||
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"} \
|
||||
"
|
||||
'';
|
||||
|
||||
postPatch = ''
|
||||
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
|
||||
substituteInPlace src/meta.rs --replace 'ffprobe' '${final.ffmpeg}/bin/ffprobe'
|
||||
substituteInPlace src/meta.rs --replace 'ffmpeg' '${pkgs.ffmpeg}/bin/ffmpeg'
|
||||
substituteInPlace src/meta.rs --replace 'ffprobe' '${pkgs.ffmpeg}/bin/ffprobe'
|
||||
'';
|
||||
|
||||
doCheck = false;
|
||||
nativeBuildInputs = with final.pkgs; [pkg-config rustc cargo];
|
||||
buildInputs = with final; [ffmpeg];
|
||||
nativeBuildInputs = with pkgs; [pkg-config rustc cargo];
|
||||
buildInputs = with pkgs; [ffmpeg zlib taglib];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -60,7 +75,7 @@
|
|||
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];
|
||||
buildInputs = with pkgs; [zlib taglib pkg-config rustc cargo clippy rust-analyzer rustfmt];
|
||||
shellHook = let
|
||||
stdenv = pkgs.stdenv;
|
||||
lib = pkgs.lib;
|
||||
|
|
14
modules/taglib/Cargo.toml
Normal file
14
modules/taglib/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "taglib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
pkg-config = "0.3.26"
|
||||
bindgen = "0.63.0"
|
36
modules/taglib/build.rs
Normal file
36
modules/taglib/build.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/wrapper.h");
|
||||
println!("cargo:rerun-if-changed=src/wrapper.cxx");
|
||||
|
||||
let taglib = pkg_config::Config::new().probe("taglib").unwrap();
|
||||
|
||||
let mut cc_builder = cc::Build::new();
|
||||
|
||||
cc_builder
|
||||
.file("src/wrapper.cxx")
|
||||
.flag("-std=c++2b")
|
||||
.flag("-Og")
|
||||
.include("src")
|
||||
.cpp(true);
|
||||
|
||||
for include in taglib.include_paths {
|
||||
cc_builder.include(include);
|
||||
}
|
||||
|
||||
cc_builder.compile("wrapper");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("src/wrapper.h")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings");
|
||||
|
||||
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
bindings
|
||||
.write_to_file(out_path.join("bindings.rs"))
|
||||
.expect("Couldn't write bindings!");
|
||||
}
|
11
modules/taglib/src/errors.rs
Normal file
11
modules/taglib/src/errors.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TagLibError {
|
||||
#[error("could not save file")]
|
||||
SaveError,
|
||||
#[error("invalid file")]
|
||||
InvalidFile,
|
||||
#[error("metadata unavailable")]
|
||||
MetadataUnavailable,
|
||||
}
|
86
modules/taglib/src/impls/file.rs
Normal file
86
modules/taglib/src/impls/file.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::ffi::CString;
|
||||
|
||||
use crate::{bindings, errors::TagLibError, traits::File, TagLibFileType};
|
||||
|
||||
use super::{oggtag::TagLibOggTag, tag::TagLibTag};
|
||||
|
||||
pub struct TagLibFile {
|
||||
ctx: *mut bindings::TagLib_File,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
}
|
||||
|
||||
pub fn new_taglib_file(
|
||||
filepath: String,
|
||||
taglib_type: Option<TagLibFileType>,
|
||||
) -> Result<TagLibFile, TagLibError> {
|
||||
let filename_c = CString::new(filepath.clone()).unwrap();
|
||||
let filename_c_ptr = filename_c.as_ptr();
|
||||
|
||||
let file = unsafe {
|
||||
if let Some(taglib_type) = taglib_type {
|
||||
bindings::wrap_taglib_file_new_with_type(filename_c_ptr, (taglib_type as u8).into())
|
||||
} else {
|
||||
bindings::wrap_taglib_file_new(filename_c_ptr)
|
||||
}
|
||||
};
|
||||
|
||||
if file.is_null() {
|
||||
return Err(TagLibError::InvalidFile);
|
||||
}
|
||||
|
||||
Ok(TagLibFile {
|
||||
ctx: file,
|
||||
taglib_type,
|
||||
})
|
||||
}
|
||||
|
||||
impl TagLibFile {
|
||||
pub fn tag(&self) -> Result<TagLibTag, TagLibError> {
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
|
||||
Ok(TagLibTag { ctx: tag })
|
||||
}
|
||||
|
||||
pub fn oggtag(&self) -> Result<TagLibOggTag, TagLibError> {
|
||||
if let Some(taglib_type) = &self.taglib_type {
|
||||
let supported = match taglib_type {
|
||||
TagLibFileType::OggFLAC
|
||||
| TagLibFileType::OggOpus
|
||||
| TagLibFileType::OggSpeex
|
||||
| TagLibFileType::OggVorbis => true,
|
||||
};
|
||||
|
||||
if !supported {
|
||||
panic!("ogg tag not supported")
|
||||
}
|
||||
}
|
||||
|
||||
let tag = unsafe { bindings::wrap_taglib_file_tag(self.ctx) };
|
||||
if tag.is_null() {
|
||||
return Err(TagLibError::MetadataUnavailable);
|
||||
}
|
||||
|
||||
Ok(TagLibOggTag { ctx: tag })
|
||||
}
|
||||
}
|
||||
|
||||
impl File for TagLibFile {
|
||||
fn save(&mut self) -> Result<(), TagLibError> {
|
||||
let result = unsafe { bindings::wrap_taglib_file_save(self.ctx) };
|
||||
return match result {
|
||||
true => Ok(()),
|
||||
false => Err(TagLibError::SaveError),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TagLibFile {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
bindings::wrap_taglib_file_free(self.ctx);
|
||||
}
|
||||
}
|
||||
}
|
3
modules/taglib/src/impls/mod.rs
Normal file
3
modules/taglib/src/impls/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) mod file;
|
||||
pub(crate) mod oggtag;
|
||||
pub(crate) mod tag;
|
33
modules/taglib/src/impls/oggtag.rs
Normal file
33
modules/taglib/src/impls/oggtag.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use std::ffi::CString;
|
||||
|
||||
use crate::{bindings, utils::c_str_to_str};
|
||||
|
||||
pub struct TagLibOggTag {
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
}
|
||||
|
||||
impl TagLibOggTag {
|
||||
pub fn get_field(&self, key: String) -> Option<String> {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = unsafe { bindings::wrap_taglib_opustag_get_field(self.ctx, key.as_ptr()) };
|
||||
|
||||
if value.is_null() {
|
||||
return None;
|
||||
} else {
|
||||
return c_str_to_str(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_field(&self, key: String, value: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
let value = CString::new(value).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_opustag_add_field(self.ctx, key.as_ptr(), value.as_ptr()) };
|
||||
}
|
||||
|
||||
pub fn remove_fields(&self, key: String) {
|
||||
let key = CString::new(key).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_opustag_remove_fields(self.ctx, key.as_ptr()) };
|
||||
}
|
||||
}
|
28
modules/taglib/src/impls/tag.rs
Normal file
28
modules/taglib/src/impls/tag.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use std::ffi::CString;
|
||||
|
||||
use crate::{bindings, traits::Tag, utils::c_str_to_str};
|
||||
|
||||
pub struct TagLibTag {
|
||||
pub ctx: *mut bindings::TagLib_Tag,
|
||||
}
|
||||
|
||||
impl Tag for TagLibTag {
|
||||
fn title(&self) -> Option<String> {
|
||||
let title_ref = unsafe { bindings::wrap_taglib_tag_title(self.ctx) };
|
||||
return c_str_to_str(title_ref);
|
||||
}
|
||||
fn set_title(&mut self, title: String) {
|
||||
let title = CString::new(title).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_tag_set_title(self.ctx, title.as_ptr()) };
|
||||
}
|
||||
fn artist(&self) -> Option<String> {
|
||||
let artist_ref = unsafe { bindings::wrap_taglib_tag_artist(self.ctx) };
|
||||
return c_str_to_str(artist_ref);
|
||||
}
|
||||
fn set_artist(&mut self, artist: String) {
|
||||
let artist = CString::new(artist).unwrap();
|
||||
|
||||
unsafe { bindings::wrap_taglib_tag_set_artist(self.ctx, artist.as_ptr()) };
|
||||
}
|
||||
}
|
22
modules/taglib/src/lib.rs
Normal file
22
modules/taglib/src/lib.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
pub mod errors;
|
||||
pub(crate) mod impls;
|
||||
pub mod traits;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub(crate) mod bindings {
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TagLibFileType {
|
||||
OggFLAC = 1,
|
||||
OggOpus = 2,
|
||||
OggSpeex = 3,
|
||||
OggVorbis = 4,
|
||||
}
|
||||
|
||||
pub use impls::file::*;
|
12
modules/taglib/src/traits.rs
Normal file
12
modules/taglib/src/traits.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use crate::errors::TagLibError;
|
||||
|
||||
pub trait File {
|
||||
fn save(&mut self) -> Result<(), TagLibError>;
|
||||
}
|
||||
|
||||
pub trait Tag {
|
||||
fn title(&self) -> Option<String>;
|
||||
fn artist(&self) -> Option<String>;
|
||||
fn set_title(&mut self, title: String);
|
||||
fn set_artist(&mut self, artist: String);
|
||||
}
|
15
modules/taglib/src/utils.rs
Normal file
15
modules/taglib/src/utils.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::{ffi::CStr, os::raw::c_char};
|
||||
|
||||
pub fn c_str_to_str(c_str: *const c_char) -> Option<String> {
|
||||
if c_str.is_null() {
|
||||
None
|
||||
} else {
|
||||
let bytes = unsafe { CStr::from_ptr(c_str).to_bytes() };
|
||||
|
||||
if bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(bytes).to_string())
|
||||
}
|
||||
}
|
||||
}
|
98
modules/taglib/src/wrapper.cxx
Normal file
98
modules/taglib/src/wrapper.cxx
Normal file
|
@ -0,0 +1,98 @@
|
|||
#include "wrapper.h"
|
||||
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/oggflacfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/speexfile.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
char *stringToCharArray(const TagLib::String &s) {
|
||||
const std::string str = s.to8Bit(true);
|
||||
return strdup(str.c_str());
|
||||
}
|
||||
|
||||
TagLib::String charArrayToString(const char *s) {
|
||||
return TagLib::String(s, TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
void wrap_taglib_free(void* pointer) {
|
||||
free(pointer);
|
||||
}
|
||||
|
||||
TagLib_File *wrap_taglib_file_new(const char *filename) {
|
||||
return reinterpret_cast<TagLib_File *>(TagLib::FileRef::create(filename));
|
||||
}
|
||||
|
||||
TagLib_File *wrap_taglib_file_new_with_type(const char *filename, short taglib_type) {
|
||||
if (taglib_type == 1) {
|
||||
return reinterpret_cast<TagLib_File *>(new TagLib::Ogg::FLAC::File(filename));
|
||||
}
|
||||
if (taglib_type == 2) {
|
||||
return reinterpret_cast<TagLib_File *>(new TagLib::Ogg::Opus::File(filename));
|
||||
}
|
||||
if (taglib_type == 3) {
|
||||
return reinterpret_cast<TagLib_File *>(new TagLib::Ogg::Speex::File(filename));
|
||||
}
|
||||
if (taglib_type == 4) {
|
||||
return reinterpret_cast<TagLib_File *>(new TagLib::Ogg::Vorbis::File(filename));
|
||||
}
|
||||
return reinterpret_cast<TagLib_File *>(TagLib::FileRef::create(filename));
|
||||
}
|
||||
|
||||
|
||||
void wrap_taglib_file_free(TagLib_File *file) {
|
||||
delete reinterpret_cast<TagLib::File *>(file);
|
||||
}
|
||||
|
||||
bool wrap_taglib_file_save(TagLib_File *file) {
|
||||
return reinterpret_cast<TagLib::File *>(file)->save();
|
||||
}
|
||||
|
||||
TagLib_Tag *wrap_taglib_file_tag(TagLib_File *file) {
|
||||
const TagLib::File *f = reinterpret_cast<const TagLib::File *>(file);
|
||||
return reinterpret_cast<TagLib_Tag *>(f->tag());
|
||||
}
|
||||
|
||||
char* wrap_taglib_tag_title(TagLib_Tag *tag) {
|
||||
const TagLib::Tag *t = reinterpret_cast<const TagLib::Tag *>(tag);
|
||||
return stringToCharArray(t->title());
|
||||
}
|
||||
|
||||
char* wrap_taglib_tag_artist(TagLib_Tag *tag) {
|
||||
const TagLib::Tag *t = reinterpret_cast<const TagLib::Tag *>(tag);
|
||||
return stringToCharArray(t->artist());
|
||||
}
|
||||
|
||||
void wrap_taglib_tag_set_title(TagLib_Tag *tag, const char *title) {
|
||||
TagLib::Tag *t = reinterpret_cast<TagLib::Tag *>(tag);
|
||||
t->setTitle(charArrayToString(title));
|
||||
}
|
||||
|
||||
void wrap_taglib_tag_set_artist(TagLib_Tag *tag, const char *artist) {
|
||||
TagLib::Tag *t = reinterpret_cast<TagLib::Tag *>(tag);
|
||||
t->setArtist(charArrayToString(artist));
|
||||
}
|
||||
|
||||
void wrap_taglib_opustag_add_field(TagLib_Tag *tag, const char *key, const char *value) {
|
||||
TagLib::Ogg::XiphComment *t = reinterpret_cast<TagLib::Ogg::XiphComment *>(tag);
|
||||
t->addField(charArrayToString(key), charArrayToString(value));
|
||||
}
|
||||
|
||||
void wrap_taglib_opustag_remove_fields(TagLib_Tag *tag, const char *key) {
|
||||
TagLib::Ogg::XiphComment *t = reinterpret_cast<TagLib::Ogg::XiphComment *>(tag);
|
||||
t->removeFields(charArrayToString(key));
|
||||
}
|
||||
|
||||
char *wrap_taglib_opustag_get_field(TagLib_Tag *tag, const char *key) {
|
||||
TagLib::Ogg::XiphComment *t = reinterpret_cast<TagLib::Ogg::XiphComment *>(tag);
|
||||
TagLib::Ogg::FieldListMap map = t->fieldListMap();
|
||||
if (map[charArrayToString(key)].isEmpty()) {
|
||||
return NULL;
|
||||
} else {
|
||||
auto first = map[charArrayToString(key)].front();
|
||||
return stringToCharArray(first);
|
||||
}
|
||||
}
|
30
modules/taglib/src/wrapper.h
Normal file
30
modules/taglib/src/wrapper.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* TagLib_File;
|
||||
typedef void* TagLib_Tag;
|
||||
|
||||
TagLib_File *wrap_taglib_file_new(const char *filename);
|
||||
TagLib_File *wrap_taglib_file_new_with_type(const char *filename, short taglib_type);
|
||||
|
||||
TagLib_Tag *wrap_taglib_file_tag(TagLib_File *file);
|
||||
void wrap_taglib_file_free(TagLib_File *file);
|
||||
bool wrap_taglib_file_save(TagLib_File *file);
|
||||
|
||||
char* wrap_taglib_tag_title(TagLib_Tag *tag);
|
||||
char* wrap_taglib_tag_artist(TagLib_Tag *tag);
|
||||
void wrap_taglib_tag_set_title(TagLib_Tag *tag, const char *title);
|
||||
void wrap_taglib_tag_set_artist(TagLib_Tag *tag, const char *artist);
|
||||
|
||||
void wrap_taglib_opustag_add_field(TagLib_Tag *tag, const char *key, const char *value);
|
||||
void wrap_taglib_opustag_remove_fields(TagLib_Tag *tag, const char *key);
|
||||
char* wrap_taglib_opustag_get_field(TagLib_Tag *tag, const char *key);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -45,7 +45,7 @@ pub fn copy_command(
|
|||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(true)?;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,13 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
|
|||
|
||||
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 format = if let Some(format) = &file.info.format {
|
||||
format.to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
|
||||
let data_format = encode_text(&format);
|
||||
|
||||
let mut path_data = String::new();
|
||||
if includes_path {
|
||||
|
@ -61,7 +67,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
|
|||
<td>{}</td>
|
||||
</tr>
|
||||
",
|
||||
td_class, path_data, data_title, data_artist, data_extension
|
||||
td_class, path_data, data_title, data_artist, data_format
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
@ -89,7 +95,7 @@ pub fn genhtml_command(
|
|||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if file.info.replaygain.is_some() && !force {
|
||||
if file.info.contains_replaygain && !force {
|
||||
println!(
|
||||
"Skipping replaygain for {:?}, contains already",
|
||||
file.join_path_from_source()
|
||||
|
@ -147,7 +147,7 @@ pub fn process_command(
|
|||
for file in files.iter_mut() {
|
||||
println!("Analysing: {:?}", file.join_path_from_source());
|
||||
|
||||
let handler = get_format_handler(file)?;
|
||||
let mut handler = get_format_handler(file)?;
|
||||
|
||||
file.info = handler.get_audio_file_info(false)?;
|
||||
}
|
||||
|
|
48
src/types.rs
48
src/types.rs
|
@ -1,6 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::utils::formats::AudioContainer;
|
||||
use crate::utils::format_detection::FileFormat;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tags {
|
||||
|
@ -24,20 +24,6 @@ pub struct ReplayGainData {
|
|||
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,
|
||||
|
@ -45,31 +31,27 @@ pub struct ReplayGainRawData {
|
|||
}
|
||||
|
||||
impl ReplayGainRawData {
|
||||
pub fn to_normal(&self) -> ReplayGainData {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.2} dB", self.track_gain),
|
||||
track_peak: format!("{:.6}", self.track_peak),
|
||||
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData {
|
||||
if is_ogg_opus {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.6}", (self.track_gain * 256.0).ceil()),
|
||||
track_peak: "".to_string(), // Not Required
|
||||
}
|
||||
} else {
|
||||
ReplayGainData {
|
||||
track_gain: format!("{:.2} dB", self.track_gain),
|
||||
track_peak: format!("{:.6}", self.track_peak),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct AudioFileInfo {
|
||||
pub tags: Tags,
|
||||
pub replaygain: Option<ReplayGainData>,
|
||||
pub contains_replaygain: bool,
|
||||
pub supports_replaygain: bool,
|
||||
pub container: AudioContainer,
|
||||
}
|
||||
|
||||
impl Default for AudioFileInfo {
|
||||
fn default() -> Self {
|
||||
AudioFileInfo {
|
||||
tags: Tags::default(),
|
||||
replaygain: None,
|
||||
supports_replaygain: false,
|
||||
container: AudioContainer::Unknown,
|
||||
}
|
||||
}
|
||||
pub format: Option<FileFormat>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
128
src/utils/format_detection.rs
Normal file
128
src/utils/format_detection.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use bytes::{Buf, Bytes};
|
||||
use std::{fs::File, io::Read, path::Path};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FormatDetectionError {
|
||||
#[error("could not read file")]
|
||||
FileReadError,
|
||||
#[error("file format unrecognised")]
|
||||
UnrecognisedFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FileFormat {
|
||||
FLAC,
|
||||
MP3,
|
||||
OggVorbis,
|
||||
OggOpus,
|
||||
OggFLAC,
|
||||
OggSpeex,
|
||||
OggTheora,
|
||||
AIFF,
|
||||
Wav,
|
||||
WavPack,
|
||||
}
|
||||
|
||||
impl ToString for FileFormat {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
FileFormat::FLAC => "FLAC".to_string(),
|
||||
FileFormat::MP3 => "MP3".to_string(),
|
||||
FileFormat::OggVorbis => "Ogg (Vorbis)".to_string(),
|
||||
FileFormat::OggOpus => "Ogg (Opus)".to_string(),
|
||||
FileFormat::OggFLAC => "Ogg (FLAC)".to_string(),
|
||||
FileFormat::OggSpeex => "Ogg (Speex)".to_string(),
|
||||
FileFormat::OggTheora => "Ogg (Theora)".to_string(),
|
||||
FileFormat::AIFF => "AIFF".to_string(),
|
||||
FileFormat::Wav => "Wav".to_string(),
|
||||
FileFormat::WavPack => "WavPack".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_ogg_subformat(path: &Path) -> Result<FileFormat, FormatDetectionError> {
|
||||
let file = File::open(path);
|
||||
if let Err(_error) = file {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let file = file.unwrap();
|
||||
|
||||
let limit = file
|
||||
.metadata()
|
||||
.map(|m| std::cmp::min(m.len(), 128) as usize + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut bytes = Vec::with_capacity(limit);
|
||||
if let Err(_err) = file.take(128).read_to_end(&mut bytes) {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let mut mem = Bytes::from(bytes);
|
||||
mem.advance(28);
|
||||
|
||||
let vorbis_type = mem.get_u8();
|
||||
|
||||
match vorbis_type {
|
||||
0x01 => return Ok(FileFormat::OggVorbis),
|
||||
0x7f => return Ok(FileFormat::OggFLAC),
|
||||
0x80 => return Ok(FileFormat::OggTheora),
|
||||
// S for speex
|
||||
0x53 => return Ok(FileFormat::OggSpeex),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
}
|
||||
|
||||
fn wavpack_matcher(buf: &[u8]) -> bool {
|
||||
// 77 76 70 6b
|
||||
buf.len() >= 4 && buf[0] == 0x77 && buf[1] == 0x76 && buf[2] == 0x70 && buf[3] == 0x6b
|
||||
}
|
||||
|
||||
pub fn detect_format(path: &Path) -> Result<FileFormat, FormatDetectionError> {
|
||||
let mut info = infer::Infer::new();
|
||||
info.add("custom/wavpack", "wv", wavpack_matcher);
|
||||
|
||||
let kind = info.get_from_path(path);
|
||||
|
||||
if let Err(_error) = kind {
|
||||
return Err(FormatDetectionError::FileReadError);
|
||||
}
|
||||
|
||||
let kind = kind.unwrap();
|
||||
|
||||
if kind.is_none() {
|
||||
return Err(FormatDetectionError::UnrecognisedFormat);
|
||||
}
|
||||
|
||||
let kind = kind.unwrap();
|
||||
|
||||
match kind.mime_type() {
|
||||
"audio/mpeg" => {
|
||||
return Ok(FileFormat::MP3);
|
||||
}
|
||||
"audio/x-wav" => {
|
||||
return Ok(FileFormat::Wav);
|
||||
}
|
||||
"custom/wavpack" => {
|
||||
return Ok(FileFormat::WavPack);
|
||||
}
|
||||
"audio/ogg" => {
|
||||
return detect_ogg_subformat(path);
|
||||
}
|
||||
"audio/x-flac" => {
|
||||
return Ok(FileFormat::FLAC);
|
||||
}
|
||||
"audio/x-aiff" => {
|
||||
return Ok(FileFormat::AIFF);
|
||||
}
|
||||
"audio/opus" => {
|
||||
return Ok(FileFormat::OggOpus);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(FormatDetectionError::UnrecognisedFormat)
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
};
|
||||
|
||||
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
use super::{AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
|
||||
pub struct FLACAudioFormat {
|
||||
flac_tags: metaflac::Tag,
|
||||
|
@ -41,18 +44,15 @@ impl AudioContainerFormat for FLACAudioFormat {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
let track_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
|
||||
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
|
||||
|
||||
if track_gain.is_none() || track_peak.is_none() {
|
||||
return None;
|
||||
return false;
|
||||
}
|
||||
|
||||
Some(ReplayGainData {
|
||||
track_gain: track_gain.unwrap(),
|
||||
track_peak: track_peak.unwrap(),
|
||||
})
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
|
@ -94,12 +94,15 @@ impl AudioContainerFormat for FLACAudioFormat {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
return Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
replaygain: self.get_replaygain_data(),
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
container: AudioContainer::FLAC,
|
||||
format: Some(FileFormat::FLAC),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
use std::{path::{PathBuf, Path}, process::Command};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use string_error::static_err;
|
||||
|
||||
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
};
|
||||
|
||||
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
use super::{AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Changes {
|
||||
|
@ -55,7 +61,7 @@ impl Default for FFProbeTags {
|
|||
}
|
||||
|
||||
pub struct GenericFFMpegAudioFormat {
|
||||
container_type: AudioContainer,
|
||||
format_type: FileFormat,
|
||||
path: Box<PathBuf>,
|
||||
|
||||
extracted_data: ExtractedData,
|
||||
|
@ -127,23 +133,20 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
|
|||
Ok(tags)
|
||||
}
|
||||
|
||||
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||
if let Some(data) = &self.changes.replaygain_data {
|
||||
return Some(data.to_normal());
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
if self.changes.replaygain_data.is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.extracted_data.replaygain_data.clone()
|
||||
if self.extracted_data.replaygain_data.is_some() {
|
||||
return true;
|
||||
};
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
|
@ -198,22 +201,25 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
return Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
replaygain: self.get_replaygain_data(),
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
container: self.container_type,
|
||||
format: Some(self.format_type),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_generic_ffmpeg_format_handler(
|
||||
path: &Path,
|
||||
container_type: AudioContainer,
|
||||
format_type: FileFormat,
|
||||
) -> Result<GenericFFMpegAudioFormat, BoxedError> {
|
||||
let mut handler = GenericFFMpegAudioFormat {
|
||||
container_type,
|
||||
format_type,
|
||||
path: Box::new(path.to_path_buf()),
|
||||
extracted_data: ExtractedData::default(),
|
||||
changes: Changes::default(),
|
||||
|
|
166
src/utils/formats/generic_taglib.rs
Normal file
166
src/utils/formats/generic_taglib.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use std::path::Path;
|
||||
|
||||
use taglib::{
|
||||
new_taglib_file,
|
||||
traits::{File, Tag},
|
||||
TagLibFileType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
};
|
||||
|
||||
use super::{AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
|
||||
pub struct TaglibAudioFormat {
|
||||
file: taglib::TagLibFile,
|
||||
file_format: Option<FileFormat>,
|
||||
}
|
||||
|
||||
impl AudioContainerFormat for TaglibAudioFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError> {
|
||||
let tags = self.file.tag()?;
|
||||
|
||||
let mut title = tags.title();
|
||||
if title.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingTitle));
|
||||
} else {
|
||||
title = Some("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
let mut artist = tags.artist();
|
||||
if artist.is_none() {
|
||||
if !allow_missing {
|
||||
return Err(Box::new(AudioFormatError::MissingArtist));
|
||||
} else {
|
||||
artist = Some("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Tags {
|
||||
title: title.unwrap(),
|
||||
artist: artist.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
if format == FileFormat::OggOpus {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
|
||||
return oggtag.get_field("R128_TRACK_GAIN".to_string()).is_some();
|
||||
}
|
||||
match format {
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex => {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
let gain = oggtag.get_field("REPLAYGAIN_TRACK_GAIN".to_string());
|
||||
let peak = oggtag.get_field("REPLAYGAIN_TRACK_PEAK".to_string());
|
||||
return gain.is_some() && peak.is_some();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
if let Some(format) = self.file_format {
|
||||
return matches!(
|
||||
format,
|
||||
// All Ogg formats support ReplayGain
|
||||
FileFormat::OggVorbis
|
||||
| FileFormat::OggOpus
|
||||
| FileFormat::OggFLAC
|
||||
| FileFormat::OggSpeex // Both "support" ReplayGain but not implemented yet
|
||||
|
||||
// FileFormat::Wav |
|
||||
// FileFormat::WavPack
|
||||
);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_title(title);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> {
|
||||
let mut tags = self.file.tag()?;
|
||||
tags.set_artist(artist);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> {
|
||||
if let Some(format) = self.file_format {
|
||||
let oggtag = self.file.oggtag().expect("oggtag not available?");
|
||||
|
||||
if format == FileFormat::OggOpus {
|
||||
let data = data.to_normal(true);
|
||||
|
||||
oggtag.add_field("R128_TRACK_GAIN".to_string(), data.track_gain);
|
||||
} else if matches!(
|
||||
format,
|
||||
FileFormat::OggVorbis | FileFormat::OggFLAC | FileFormat::OggSpeex
|
||||
) {
|
||||
let data = data.to_normal(false);
|
||||
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_GAIN".to_string(), data.track_gain);
|
||||
oggtag.add_field("REPLAYGAIN_TRACK_PEAK".to_string(), data.track_peak);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError> {
|
||||
let res = self.file.save();
|
||||
|
||||
match res {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError> {
|
||||
return Ok(AudioFileInfo {
|
||||
tags: self.get_tags(allow_missing_tags)?,
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
supports_replaygain: self.supports_replaygain(),
|
||||
format: self.file_format,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_taglib_format_handler(
|
||||
path: &Path,
|
||||
file_format: Option<FileFormat>,
|
||||
) -> Result<TaglibAudioFormat, BoxedError> {
|
||||
let mut taglib_format: Option<TagLibFileType> = None;
|
||||
if let Some(format) = file_format {
|
||||
taglib_format = match format {
|
||||
FileFormat::OggVorbis => Some(TagLibFileType::OggVorbis),
|
||||
FileFormat::OggOpus => Some(TagLibFileType::OggOpus),
|
||||
FileFormat::OggFLAC => Some(TagLibFileType::OggFLAC),
|
||||
FileFormat::OggSpeex => Some(TagLibFileType::OggSpeex),
|
||||
FileFormat::OggTheora => panic!("Ogg Theora is not supported by taglib"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TaglibAudioFormat {
|
||||
file_format,
|
||||
file: new_taglib_file(path.to_string_lossy().to_string(), taglib_format)?,
|
||||
})
|
||||
}
|
|
@ -2,12 +2,14 @@ use std::path::PathBuf;
|
|||
|
||||
use id3::TagLike;
|
||||
|
||||
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||||
use crate::{
|
||||
types::{AudioFileInfo, ReplayGainRawData, Tags},
|
||||
utils::format_detection::FileFormat,
|
||||
};
|
||||
|
||||
use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
use super::{AudioContainerFormat, AudioFormatError, BoxedError};
|
||||
|
||||
pub struct ID3AudioFormat {
|
||||
container_type: AudioContainer,
|
||||
id3_tags: id3::Tag,
|
||||
path: Box<PathBuf>,
|
||||
}
|
||||
|
@ -32,14 +34,10 @@ impl AudioContainerFormat for ID3AudioFormat {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||||
fn contains_replaygain_tags(&self) -> bool {
|
||||
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" {
|
||||
|
@ -47,11 +45,9 @@ impl AudioContainerFormat for ID3AudioFormat {
|
|||
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()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -59,11 +55,7 @@ impl AudioContainerFormat for ID3AudioFormat {
|
|||
}
|
||||
}
|
||||
|
||||
if !contains_replaygain_tags {
|
||||
None
|
||||
} else {
|
||||
Some(replaygain_data)
|
||||
}
|
||||
contains_replaygain_tags
|
||||
}
|
||||
|
||||
fn supports_replaygain(&self) -> bool {
|
||||
|
@ -119,30 +111,23 @@ impl AudioContainerFormat for ID3AudioFormat {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||||
fn get_audio_file_info(
|
||||
&mut 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,
|
||||
format: Some(FileFormat::MP3),
|
||||
contains_replaygain: self.contains_replaygain_tags(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)?,
|
||||
};
|
||||
pub fn new_id3_format_handler(path: &PathBuf) -> Result<ID3AudioFormat, BoxedError> {
|
||||
let id3_tags = id3::Tag::read_from_path(path)?;
|
||||
|
||||
Ok(ID3AudioFormat {
|
||||
container_type,
|
||||
id3_tags,
|
||||
path: Box::new(path.clone()),
|
||||
})
|
||||
|
|
|
@ -2,31 +2,29 @@ pub mod flac;
|
|||
pub mod generic_ffmpeg;
|
||||
pub mod id3;
|
||||
|
||||
#[cfg(feature = "taglib")]
|
||||
pub mod generic_taglib;
|
||||
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::types::{AudioFileInfo, File, ReplayGainData, ReplayGainRawData, Tags};
|
||||
use crate::types::{AudioFileInfo, File, ReplayGainRawData, Tags};
|
||||
|
||||
use self::flac::new_flac_format_handler;
|
||||
use self::id3::new_id3_format_handler;
|
||||
|
||||
type BoxedError = Box<dyn Error>;
|
||||
#[cfg(feature = "taglib")]
|
||||
use self::generic_taglib::new_taglib_format_handler;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AudioContainer {
|
||||
MP3,
|
||||
FLAC,
|
||||
WAV,
|
||||
OGG,
|
||||
AIFF,
|
||||
Unknown,
|
||||
}
|
||||
use super::format_detection::{detect_format, FileFormat};
|
||||
|
||||
type BoxedError = Box<dyn Error>;
|
||||
|
||||
pub trait AudioContainerFormat {
|
||||
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>;
|
||||
fn get_replaygain_data(&self) -> Option<ReplayGainData>;
|
||||
fn contains_replaygain_tags(&self) -> bool;
|
||||
fn supports_replaygain(&self) -> bool;
|
||||
|
||||
fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
|
||||
|
@ -35,7 +33,10 @@ pub trait AudioContainerFormat {
|
|||
|
||||
fn save_changes(&mut self) -> Result<(), BoxedError>;
|
||||
|
||||
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError>;
|
||||
fn get_audio_file_info(
|
||||
&mut self,
|
||||
allow_missing_tags: bool,
|
||||
) -> Result<AudioFileInfo, BoxedError>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
@ -47,24 +48,41 @@ pub enum AudioFormatError {
|
|||
}
|
||||
|
||||
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");
|
||||
let format = detect_format(&file.join_path_to())?;
|
||||
let path = file.join_path_to();
|
||||
|
||||
#[cfg(feature = "taglib")]
|
||||
{
|
||||
match format {
|
||||
FileFormat::OggFLAC
|
||||
| FileFormat::OggSpeex
|
||||
| FileFormat::OggVorbis
|
||||
| FileFormat::OggOpus
|
||||
| FileFormat::Wav
|
||||
| FileFormat::WavPack
|
||||
| FileFormat::AIFF => {
|
||||
return Ok(Box::new(new_taglib_format_handler(&path, Some(format))?));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match format {
|
||||
FileFormat::FLAC => {
|
||||
// Native FLAC support
|
||||
return Ok(Box::new(new_flac_format_handler(&path)?));
|
||||
}
|
||||
FileFormat::MP3 => {
|
||||
// Native MP3 support
|
||||
return Ok(Box::new(new_id3_format_handler(&path)?));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
panic!("no supported handler found");
|
||||
}
|
||||
|
||||
pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
||||
fn is_supported_extension(file_path: &Path) -> bool {
|
||||
let ext = file_path.extension();
|
||||
if ext.is_none() {
|
||||
return false;
|
||||
|
@ -72,5 +90,49 @@ pub fn is_supported_file_extension(file_path: &Path) -> bool {
|
|||
|
||||
let ext = ext.unwrap().to_str().unwrap();
|
||||
|
||||
#[cfg(feature = "taglib")]
|
||||
{
|
||||
if matches!(ext, "ogg" | "opus" | "wav" | "wv" | "aiff") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
matches!(ext, "mp3" | "flac")
|
||||
}
|
||||
|
||||
pub fn is_supported_file(file_path: &Path) -> bool {
|
||||
if !is_supported_extension(file_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let format = detect_format(file_path);
|
||||
if format.is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let format = format.unwrap();
|
||||
|
||||
#[cfg(feature = "taglib")]
|
||||
{
|
||||
match format {
|
||||
FileFormat::OggVorbis
|
||||
| FileFormat::OggOpus
|
||||
| FileFormat::OggFLAC
|
||||
| FileFormat::OggSpeex
|
||||
| FileFormat::Wav
|
||||
| FileFormat::WavPack
|
||||
| FileFormat::AIFF => {
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match format {
|
||||
// Not supported yet
|
||||
FileFormat::OggTheora => false,
|
||||
FileFormat::FLAC | FileFormat::MP3 => true,
|
||||
// Rest not supported
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
pub mod ascii_reduce;
|
||||
pub mod format_detection;
|
||||
pub mod replaygain;
|
||||
pub mod transcoder;
|
||||
|
||||
pub mod formats;
|
||||
pub(self) mod music_scanner;
|
||||
|
||||
pub use formats::is_supported_file;
|
||||
pub use music_scanner::scan_for_music;
|
||||
|
||||
pub use formats::is_supported_file_extension;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::fs;
|
|||
use crate::types::File;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::is_supported_file_extension;
|
||||
use super::is_supported_file;
|
||||
|
||||
pub fn find_extra_files(
|
||||
src_dir: String,
|
||||
|
@ -43,7 +43,7 @@ pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error:
|
|||
continue;
|
||||
}
|
||||
|
||||
if is_supported_file_extension(&entry_path) {
|
||||
if is_supported_file(&entry_path) {
|
||||
let mut file = File::from_path(src_dir.clone(), entry_path.clone());
|
||||
|
||||
file.extra_files
|
||||
|
|
Loading…
Reference in a new issue