diff --git a/Cargo.lock b/Cargo.lock index 5d75a1f..1f75c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 013a26e..e1f0a5d 100644 --- a/Cargo.toml +++ b/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" \ No newline at end of file +crossbeam = "0.8" + +[features] +default = ["taglib"] +taglib = ["dep:taglib"] \ No newline at end of file diff --git a/flake.nix b/flake.nix index abbb43f..da156b5 100644 --- a/flake.nix +++ b/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; diff --git a/modules/taglib/Cargo.toml b/modules/taglib/Cargo.toml new file mode 100644 index 0000000..e871c6d --- /dev/null +++ b/modules/taglib/Cargo.toml @@ -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" \ No newline at end of file diff --git a/modules/taglib/build.rs b/modules/taglib/build.rs new file mode 100644 index 0000000..076d254 --- /dev/null +++ b/modules/taglib/build.rs @@ -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!"); +} diff --git a/modules/taglib/src/errors.rs b/modules/taglib/src/errors.rs new file mode 100644 index 0000000..2146814 --- /dev/null +++ b/modules/taglib/src/errors.rs @@ -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, +} diff --git a/modules/taglib/src/impls/file.rs b/modules/taglib/src/impls/file.rs new file mode 100644 index 0000000..b7ecbc4 --- /dev/null +++ b/modules/taglib/src/impls/file.rs @@ -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, +} + +pub fn new_taglib_file( + filepath: String, + taglib_type: Option, +) -> Result { + 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 { + 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 { + 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); + } + } +} diff --git a/modules/taglib/src/impls/mod.rs b/modules/taglib/src/impls/mod.rs new file mode 100644 index 0000000..9fc8259 --- /dev/null +++ b/modules/taglib/src/impls/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod file; +pub(crate) mod oggtag; +pub(crate) mod tag; diff --git a/modules/taglib/src/impls/oggtag.rs b/modules/taglib/src/impls/oggtag.rs new file mode 100644 index 0000000..1118b20 --- /dev/null +++ b/modules/taglib/src/impls/oggtag.rs @@ -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 { + 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()) }; + } +} diff --git a/modules/taglib/src/impls/tag.rs b/modules/taglib/src/impls/tag.rs new file mode 100644 index 0000000..d7a829f --- /dev/null +++ b/modules/taglib/src/impls/tag.rs @@ -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 { + 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 { + 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()) }; + } +} diff --git a/modules/taglib/src/lib.rs b/modules/taglib/src/lib.rs new file mode 100644 index 0000000..b71dc31 --- /dev/null +++ b/modules/taglib/src/lib.rs @@ -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::*; diff --git a/modules/taglib/src/traits.rs b/modules/taglib/src/traits.rs new file mode 100644 index 0000000..88115ce --- /dev/null +++ b/modules/taglib/src/traits.rs @@ -0,0 +1,12 @@ +use crate::errors::TagLibError; + +pub trait File { + fn save(&mut self) -> Result<(), TagLibError>; +} + +pub trait Tag { + fn title(&self) -> Option; + fn artist(&self) -> Option; + fn set_title(&mut self, title: String); + fn set_artist(&mut self, artist: String); +} diff --git a/modules/taglib/src/utils.rs b/modules/taglib/src/utils.rs new file mode 100644 index 0000000..1ee24e5 --- /dev/null +++ b/modules/taglib/src/utils.rs @@ -0,0 +1,15 @@ +use std::{ffi::CStr, os::raw::c_char}; + +pub fn c_str_to_str(c_str: *const c_char) -> Option { + 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()) + } + } +} diff --git a/modules/taglib/src/wrapper.cxx b/modules/taglib/src/wrapper.cxx new file mode 100644 index 0000000..e990124 --- /dev/null +++ b/modules/taglib/src/wrapper.cxx @@ -0,0 +1,98 @@ +#include "wrapper.h" + +#include +#include +#include +#include +#include +#include + +#include + +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::FileRef::create(filename)); +} + +TagLib_File *wrap_taglib_file_new_with_type(const char *filename, short taglib_type) { + if (taglib_type == 1) { + return reinterpret_cast(new TagLib::Ogg::FLAC::File(filename)); + } + if (taglib_type == 2) { + return reinterpret_cast(new TagLib::Ogg::Opus::File(filename)); + } + if (taglib_type == 3) { + return reinterpret_cast(new TagLib::Ogg::Speex::File(filename)); + } + if (taglib_type == 4) { + return reinterpret_cast(new TagLib::Ogg::Vorbis::File(filename)); + } + return reinterpret_cast(TagLib::FileRef::create(filename)); +} + + +void wrap_taglib_file_free(TagLib_File *file) { + delete reinterpret_cast(file); +} + +bool wrap_taglib_file_save(TagLib_File *file) { + return reinterpret_cast(file)->save(); +} + +TagLib_Tag *wrap_taglib_file_tag(TagLib_File *file) { + const TagLib::File *f = reinterpret_cast(file); + return reinterpret_cast(f->tag()); +} + +char* wrap_taglib_tag_title(TagLib_Tag *tag) { + const TagLib::Tag *t = reinterpret_cast(tag); + return stringToCharArray(t->title()); +} + +char* wrap_taglib_tag_artist(TagLib_Tag *tag) { + const TagLib::Tag *t = reinterpret_cast(tag); + return stringToCharArray(t->artist()); +} + +void wrap_taglib_tag_set_title(TagLib_Tag *tag, const char *title) { + TagLib::Tag *t = reinterpret_cast(tag); + t->setTitle(charArrayToString(title)); +} + +void wrap_taglib_tag_set_artist(TagLib_Tag *tag, const char *artist) { + TagLib::Tag *t = reinterpret_cast(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(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(tag); + t->removeFields(charArrayToString(key)); +} + +char *wrap_taglib_opustag_get_field(TagLib_Tag *tag, const char *key) { + TagLib::Ogg::XiphComment *t = reinterpret_cast(tag); + TagLib::Ogg::FieldListMap map = t->fieldListMap(); + if (map[charArrayToString(key)].isEmpty()) { + return NULL; + } else { + auto first = map[charArrayToString(key)].front(); + return stringToCharArray(first); + } +} \ No newline at end of file diff --git a/modules/taglib/src/wrapper.h b/modules/taglib/src/wrapper.h new file mode 100644 index 0000000..a19bc1f --- /dev/null +++ b/modules/taglib/src/wrapper.h @@ -0,0 +1,30 @@ +#include +#include + +#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 \ No newline at end of file diff --git a/src/commands/copy.rs b/src/commands/copy.rs index ab9a45f..dc4b83c 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -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)?; } diff --git a/src/commands/genhtml.rs b/src/commands/genhtml.rs index a378111..0cb3a0e 100644 --- a/src/commands/genhtml.rs +++ b/src/commands/genhtml.rs @@ -43,7 +43,13 @@ fn table_for_files(files: Vec, 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, includes_path: bool) -> String { {} ", - 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)?; } diff --git a/src/commands/process.rs b/src/commands/process.rs index a54f6fb..fad522a 100644 --- a/src/commands/process.rs +++ b/src/commands/process.rs @@ -108,7 +108,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box ReplayGainRawData { - let track_gain = self.track_gain.split(' ').next().unwrap_or("0.0"); - let track_gain = track_gain.parse::().unwrap(); - - let track_peak = self.track_peak.parse::().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, + 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, } #[derive(Debug, Clone)] diff --git a/src/utils/format_detection.rs b/src/utils/format_detection.rs new file mode 100644 index 0000000..e3fab2c --- /dev/null +++ b/src/utils/format_detection.rs @@ -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 { + 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 { + 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) +} diff --git a/src/utils/formats/flac.rs b/src/utils/formats/flac.rs index fdf0547..5fe6883 100644 --- a/src/utils/formats/flac.rs +++ b/src/utils/formats/flac.rs @@ -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 { + 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 { + fn get_audio_file_info( + &mut self, + allow_missing_tags: bool, + ) -> Result { 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), }); } } diff --git a/src/utils/formats/generic_ffmpeg.rs b/src/utils/formats/generic_ffmpeg.rs index 3479e16..f4de27c 100644 --- a/src/utils/formats/generic_ffmpeg.rs +++ b/src/utils/formats/generic_ffmpeg.rs @@ -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, extracted_data: ExtractedData, @@ -127,23 +133,20 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat { Ok(tags) } - fn get_replaygain_data(&self) -> Option { - 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 { + fn get_audio_file_info( + &mut self, + allow_missing_tags: bool, + ) -> Result { 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 { let mut handler = GenericFFMpegAudioFormat { - container_type, + format_type, path: Box::new(path.to_path_buf()), extracted_data: ExtractedData::default(), changes: Changes::default(), diff --git a/src/utils/formats/generic_taglib.rs b/src/utils/formats/generic_taglib.rs new file mode 100644 index 0000000..6066265 --- /dev/null +++ b/src/utils/formats/generic_taglib.rs @@ -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, +} + +impl AudioContainerFormat for TaglibAudioFormat { + fn get_tags(&self, allow_missing: bool) -> Result { + 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 { + 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, +) -> Result { + let mut taglib_format: Option = 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)?, + }) +} diff --git a/src/utils/formats/id3.rs b/src/utils/formats/id3.rs index bf9ec42..ecc81b3 100644 --- a/src/utils/formats/id3.rs +++ b/src/utils/formats/id3.rs @@ -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, } @@ -32,14 +34,10 @@ impl AudioContainerFormat for ID3AudioFormat { }) } - fn get_replaygain_data(&self) -> Option { + 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 { + fn get_audio_file_info( + &mut self, + allow_missing_tags: bool, + ) -> Result { 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 { - 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 { + let id3_tags = id3::Tag::read_from_path(path)?; Ok(ID3AudioFormat { - container_type, id3_tags, path: Box::new(path.clone()), }) diff --git a/src/utils/formats/mod.rs b/src/utils/formats/mod.rs index 5fc6bbb..cdbcd6c 100644 --- a/src/utils/formats/mod.rs +++ b/src/utils/formats/mod.rs @@ -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; +#[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; pub trait AudioContainerFormat { fn get_tags(&self, allow_missing: bool) -> Result; - fn get_replaygain_data(&self) -> Option; + 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; + fn get_audio_file_info( + &mut self, + allow_missing_tags: bool, + ) -> Result; } #[derive(Error, Debug)] @@ -47,24 +48,41 @@ pub enum AudioFormatError { } pub fn get_format_handler(file: &File) -> Result, 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, + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 91e3a02..b5ded79 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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; diff --git a/src/utils/music_scanner.rs b/src/utils/music_scanner.rs index ff826cc..3872adf 100644 --- a/src/utils/music_scanner.rs +++ b/src/utils/music_scanner.rs @@ -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, Box