Compare commits

..

10 commits

Author SHA1 Message Date
chaos d5f3cdde3a
delete readme 2023-09-30 13:30:11 +01:00
chaos 0e9aceb2b9
update flake 2023-09-27 17:20:28 +01:00
chaos 7580e1fd01
update flake to be more compliant with schema 2023-09-08 10:55:10 +01:00
chaos 190f47d6ef
format code 2023-08-31 23:52:56 +01:00
chaos 8f03cf4f17
code tidying 2023-08-31 23:50:30 +01:00
chaos 253db1cbd5
update to latest nixpkgs/rustc 2023-08-31 22:08:21 +01:00
chaos 7ef7093eb7
fix transcode command 2023-08-09 22:35:12 +01:00
Chaos 5eca86a470
update flake but not cargo.lock/toml as not new enough rustc 2023-07-28 20:29:08 +00:00
Chaos f67a9ebc3b
cursed broken code for links on genhtml 2023-01-18 10:38:20 +00:00
Chaos aadb338d75
improve format handling & add taglib wrapper & support for more formats 2023-01-14 16:53:53 +00:00
38 changed files with 1830 additions and 698 deletions

1023
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,40 +3,54 @@ name = "musicutil"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[workspace]
members = [
"modules/taglib",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
# for decode/encoding yaml/json for transcode config & ffprobe output # for decode/encoding yaml/json for transcode config & ffprobe output
serde = { version = "1.0.143", features = ["derive"] } serde = { version = "1.0.0", features = ["derive"] }
serde_yaml = "0.9.9" serde_yaml = "0.9"
serde_json = "1.0" serde_json = "1.0"
serde_with = "1.3.1" serde_with = "3"
# argument parsing # argument parsing
clap = { version = "3.2.17", features = ["derive"] } clap = { version = "4", features = ["derive"] }
# ascii_reduce constants & transcode presets # ascii_reduce constants & transcode presets
lazy_static = "1.4.0" lazy_static = "1"
# for scan_for_music # for scan_for_music
walkdir = "2.3.2" walkdir = "2"
# format detection
infer = "0.15"
bytes = "1"
# tag reading # tag reading
id3 = "1.3.0" id3 = "1"
metaflac = "0.2.5" metaflac = "0.2"
taglib = { path = "./modules/taglib", optional = true }
# for genhtml command # for genhtml command
html-escape = "0.2.11" html-escape = "0.2"
urlencoding = "2"
# error handling # error handling
thiserror = "1.0" thiserror = "1"
string-error = "0.1.0" string-error = "0.1"
# temporary file for transcode prefix file # temporary file for transcode prefix file
tempfile = "3" tempfile = "3"
# for reading ffmpeg progress output file # for reading ffmpeg progress output file
notify = "4.0.17" notify = "6"
# scoped threads [features]
crossbeam = "0.8" default = ["taglib"]
taglib = ["dep:taglib"]

View file

@ -1,3 +0,0 @@
# musicutil
My tool for managing a local music library.

View file

@ -1,6 +1,8 @@
(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); (import (let
in fetchTarball { lock = builtins.fromJSON (builtins.readFile ./flake.lock);
url = in
"https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash; sha256 = lock.nodes.flake-compat.locked.narHash;
}) { src = ./.; }).defaultNix }) {src = ./.;})
.defaultNix

View file

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1668681692, "lastModified": 1673956053,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1", "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,16 +18,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1670980281, "lastModified": 1695644571,
"narHash": "sha256-g0t/SmQca/JBEd+3Ry1qFgDfDK8ME9AM6EP4YUl8/lo=", "narHash": "sha256-asS9dCCdlt1lPq0DLwkVBbVoEKuEuz+Zi3DG7pR/RxA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5cb48ea3c19ce2e5746a44d6b91847396bd28c1f", "rev": "6500b4580c2a1f3d0f980d32d285739d8e156d92",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "release-22.05", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -39,13 +39,31 @@
"utils": "utils" "utils": "utils"
} }
}, },
"utils": { "systems": {
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1681028828,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -2,7 +2,7 @@
description = "A tool for organising a music library"; description = "A tool for organising a music library";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/release-22.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils"; utils.url = "github:numtide/flake-utils";
flake-compat = { flake-compat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";
@ -17,31 +17,47 @@
... ...
}: }:
{ {
overlay = final: prev: let overlays.musicutil = final: prev: let
system = final.system; system = final.system;
pkgs = final.pkgs;
lib = pkgs.lib;
stdenv = pkgs.stdenv;
in { in {
musicutil = final.rustPlatform.buildRustPackage rec { musicutil = pkgs.rustPlatform.buildRustPackage rec {
pname = "musicutil"; pname = "musicutil";
version = "latest"; version = "latest";
src = ./.; src = ./.;
cargoLock = {lockFile = ./Cargo.lock;}; 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 = '' postPatch = ''
substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg' substituteInPlace src/meta.rs --replace 'ffmpeg' '${pkgs.ffmpeg}/bin/ffmpeg'
substituteInPlace src/meta.rs --replace 'ffprobe' '${final.ffmpeg}/bin/ffprobe' substituteInPlace src/meta.rs --replace 'ffprobe' '${pkgs.ffmpeg}/bin/ffprobe'
''; '';
doCheck = false; doCheck = false;
nativeBuildInputs = with final.pkgs; [pkg-config rustc cargo]; nativeBuildInputs = with pkgs; [pkg-config rustc cargo];
buildInputs = with final; [ffmpeg]; buildInputs = with pkgs; [ffmpeg zlib taglib];
}; };
}; };
overlays.default = self.overlays.musicutil;
} }
// utils.lib.eachSystem (utils.lib.defaultSystems) (system: let // utils.lib.eachSystem (utils.lib.defaultSystems) (system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
overlays = [self.overlay]; overlays = [self.overlays.default];
}; };
in { in {
defaultPackage = self.packages."${system}".musicutil; defaultPackage = self.packages."${system}".musicutil;
@ -60,7 +76,7 @@
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc; RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; 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 shellHook = let
stdenv = pkgs.stdenv; stdenv = pkgs.stdenv;
lib = pkgs.lib; lib = pkgs.lib;

14
modules/taglib/Cargo.toml Normal file
View 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"
pkg-config = "0.3"
bindgen = "0.66"

36
modules/taglib/build.rs Normal file
View 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!");
}

View 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,
}

View file

@ -0,0 +1,87 @@
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).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) };
match result {
true => Ok(()),
false => Err(TagLibError::SaveError),
}
}
}
impl Drop for TagLibFile {
fn drop(&mut self) {
unsafe {
bindings::wrap_taglib_file_free(self.ctx);
}
}
}

View file

@ -0,0 +1,3 @@
pub(crate) mod file;
pub(crate) mod oggtag;
pub(crate) mod tag;

View 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() {
None
} else {
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()) };
}
}

View file

@ -0,0 +1,30 @@
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) };
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) };
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
View 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::*;

View 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);
}

View 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())
}
}
}

View 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);
}
}

View 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

View file

@ -4,10 +4,10 @@ use clap::{Args, Parser, Subcommand};
#[clap()] #[clap()]
pub struct CLIArgs { pub struct CLIArgs {
#[clap(subcommand)] #[clap(subcommand)]
pub command: Option<Commands>, pub command: Commands,
} }
#[derive(Debug, Subcommand)] #[derive(Debug, Clone, Subcommand)]
pub enum Commands { pub enum Commands {
Process(ProcessCommandArgs), Process(ProcessCommandArgs),
Genhtml(GenHTMLCommandArgs), Genhtml(GenHTMLCommandArgs),
@ -17,7 +17,7 @@ pub enum Commands {
GetTags(GetTagsCommandArgs), GetTags(GetTagsCommandArgs),
} }
#[derive(Debug, Args)] #[derive(Debug, Clone, Args)]
pub struct ProcessCommandArgs { pub struct ProcessCommandArgs {
pub source: String, pub source: String,
#[clap(long)] #[clap(long)]
@ -30,7 +30,7 @@ pub struct ProcessCommandArgs {
pub replaygain_threads: Option<u32>, pub replaygain_threads: Option<u32>,
} }
#[derive(Debug, Args)] #[derive(Debug, Clone, Args)]
pub struct GenHTMLCommandArgs { pub struct GenHTMLCommandArgs {
pub source: String, pub source: String,
pub dest: String, pub dest: String,
@ -38,9 +38,11 @@ pub struct GenHTMLCommandArgs {
pub title: String, pub title: String,
#[clap(long, default_value = "generated by musicutil")] #[clap(long, default_value = "generated by musicutil")]
pub description: String, pub description: String,
#[clap(long)]
pub link_base: Option<String>,
} }
#[derive(Debug, Args)] #[derive(Debug, Clone, Args)]
pub struct TranscodeCommandArgs { pub struct TranscodeCommandArgs {
pub source: String, pub source: String,
pub dest: String, pub dest: String,

View file

@ -5,10 +5,9 @@ use std::{
process::exit, process::exit,
str::FromStr, str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::scope,
}; };
use crossbeam::scope;
use crate::{ use crate::{
args::{CLIArgs, CopyCommandArgs}, args::{CLIArgs, CopyCommandArgs},
types::File, types::File,
@ -24,7 +23,7 @@ use crate::{
}; };
pub fn copy_command( pub fn copy_command(
_args: &CLIArgs, _args: CLIArgs,
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() { if copy_args.transcode_config.is_none() && copy_args.transcode_preset.is_none() {
@ -45,7 +44,7 @@ pub fn copy_command(
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); println!("Analysing: {:?}", file.join_path_from_source());
let handler = get_format_handler(file)?; let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(true)?; file.info = handler.get_audio_file_info(true)?;
} }
@ -57,6 +56,7 @@ pub fn copy_command(
for file in files.iter() { for file in files.iter() {
let filename = file.join_filename(); let filename = file.join_filename();
if let Entry::Vacant(entry) = seen.entry(filename.clone()) { if let Entry::Vacant(entry) = seen.entry(filename.clone()) {
entry.insert(true); entry.insert(true);
} else { } else {
@ -83,7 +83,7 @@ pub fn copy_command(
for directory in directories.iter() { for directory in directories.iter() {
fs::create_dir_all( fs::create_dir_all(
PathBuf::from_str(copy_args.dest.as_str()) PathBuf::from_str(copy_args.dest.as_str())
.unwrap() .expect("invalid destination")
.join(directory), .join(directory),
)?; )?;
} }
@ -108,14 +108,15 @@ pub fn copy_command(
fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> { fn copy_file(file: &File, copy_args: &CopyCommandArgs) -> Result<(), Box<dyn std::error::Error>> {
let from_path = file.join_path_to(); let from_path = file.join_path_to();
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap(); let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory { let to_path = match copy_args.single_directory {
true => to_path_dest.join(file.join_filename()), true => to_path_dest.join(file.join_filename()),
false => to_path_dest false => to_path_dest
.join(file.path_from_source.clone()) .join(file.path_from_source.clone())
.join(file.join_filename()), .join(file.join_filename()),
}; };
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
let to_path_string = to_path.to_string_lossy();
if !copy_args.no_skip_existing && to_path.exists() { if !copy_args.no_skip_existing && to_path.exists() {
println!( println!(
@ -149,7 +150,7 @@ fn copy_files(
fn transcode_file( fn transcode_file(
file: &File, file: &File,
copy_args: &CopyCommandArgs, copy_args: &CopyCommandArgs,
config: TranscodeConfig, config: &TranscodeConfig,
is_threaded: bool, is_threaded: bool,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let new_filename_full: String = match config.file_extension.clone() { let new_filename_full: String = match config.file_extension.clone() {
@ -159,14 +160,15 @@ fn transcode_file(
} }
}; };
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap(); let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination");
let to_path = match copy_args.single_directory { let to_path = match copy_args.single_directory {
true => to_path_dest.join(new_filename_full), true => to_path_dest.join(new_filename_full),
false => to_path_dest false => to_path_dest
.join(file.path_from_source.clone()) .join(file.path_from_source.clone())
.join(new_filename_full), .join(new_filename_full),
}; };
let to_path_string = to_path.as_os_str().to_str().unwrap().to_string();
let to_path_string = to_path.to_string_lossy();
if !file.extra_files.is_empty() { if !file.extra_files.is_empty() {
for extra_file in file.extra_files.iter() { for extra_file in file.extra_files.iter() {
@ -179,11 +181,14 @@ fn transcode_file(
"Skipping transcode for {} as file already exists", "Skipping transcode for {} as file already exists",
to_path_string to_path_string
); );
return Ok(()); return Ok(());
} }
println!("Transcoding {}", to_path_string); println!("Transcoding {}", to_path_string);
transcode(file.to_owned(), to_path_string.clone(), config, None)?;
transcode(file.to_owned(), to_path_string.to_string(), config, None)?;
if is_threaded { if is_threaded {
println!("Finished Transcoding {}", to_path_string); println!("Finished Transcoding {}", to_path_string);
} }
@ -199,42 +204,38 @@ fn transcode_files(
copy_args.transcode_preset.as_ref(), copy_args.transcode_preset.as_ref(),
copy_args.transcode_config.as_ref(), copy_args.transcode_config.as_ref(),
) )
.unwrap(); .expect("transcode config error");
if copy_args.threads.is_some() && copy_args.threads.unwrap() > 1 { let threads = copy_args.threads.unwrap_or(1);
if threads > 1 {
let files_copy = files.to_vec(); let files_copy = files.to_vec();
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy)); let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
let copy_args_arc = Arc::new(copy_args);
let transcode_config_arc = Arc::new(transcode_config); let copy_args = Arc::new(copy_args);
let transcode_config = Arc::new(transcode_config);
scope(|s| { scope(|s| {
for _ in 0..copy_args.threads.unwrap() { for _ in 0..threads {
s.spawn(|_| loop { s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap(); let mut jobs = jobs.lock().unwrap();
let job = jobs.pop();
if job.is_none() {
break;
}
let job = job.unwrap().clone();
drop(jobs);
let result = transcode_file( let job = jobs.pop();
&job, if let Some(job) = job {
&copy_args_arc, let result = transcode_file(&job, &copy_args, &transcode_config, true);
transcode_config_arc.as_ref().clone(), if let Err(err) = result {
true, panic!("Error Transcoding: {}", err)
); }
if result.is_err() { } else {
panic!("Error Transcoding: {}", result.unwrap_err()) break;
} }
}); });
} }
}) });
.expect("threads haunted");
} else { } else {
for file in files.iter() { for file in files.iter() {
transcode_file(file, copy_args, transcode_config.clone(), false)?; transcode_file(file, copy_args, &transcode_config, false)?;
} }
} }

View file

@ -4,23 +4,32 @@ use crate::types::File;
use crate::utils::formats::get_format_handler; use crate::utils::formats::get_format_handler;
use crate::utils::scan_for_music; use crate::utils::scan_for_music;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::ffi::OsStr;
use std::io::Write; use std::io::Write;
use html_escape::encode_text; use html_escape::encode_text;
use urlencoding::encode as url_encode;
fn table_for_files(files: Vec<File>, includes_path: bool) -> String { fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<String>) -> String {
let mut html_content = String::new(); let mut html_content = String::new();
let mut path_head = String::new(); let mut path_head = String::new();
if includes_path { if includes_path {
path_head.push_str("<th>Path</th>") path_head.push_str("<th>Path</th>")
} }
let mut link_head = String::new();
if link_base.is_some() {
link_head.push_str("<th>Link</th>")
}
html_content.push_str( html_content.push_str(
format!( format!(
" "
<table class=\"pure-table pure-table-horizontal\"> <table class=\"pure-table pure-table-horizontal\">
<thead> <thead>
<tr> <tr>
{}
{} {}
<th>Title</th> <th>Title</th>
<th>Artist</th> <th>Artist</th>
@ -29,7 +38,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
</thead> </thead>
<tbody> <tbody>
", ",
path_head link_head, path_head
) )
.as_str(), .as_str(),
); );
@ -43,7 +52,14 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
let data_title = encode_text(&file.info.tags.title); let data_title = encode_text(&file.info.tags.title);
let data_artist = encode_text(&file.info.tags.artist); let data_artist = encode_text(&file.info.tags.artist);
let 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(); let mut path_data = String::new();
if includes_path { if includes_path {
@ -51,17 +67,49 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str()); path_data.push_str(format!("<td>{}</td>", encode_text(&file_directory)).as_str());
} }
let mut url_data = String::new();
if let Some(link_base) = &link_base {
let mut url = String::new();
url.push_str(link_base.as_str());
url.push('/');
let file_path = file.join_path_from_source();
let file_path: Vec<&OsStr> = file_path.iter().collect();
for i in 0..(file_path.len()) {
let file_path_element = file_path.get(i).unwrap();
url.push_str(
url_encode(
file_path_element
.to_str()
.expect("invalid character in filename"),
)
.to_string()
.as_str(),
);
if i != file_path.len() - 1 {
url.push('/');
}
}
url_data.push_str(format!("<td><a href=\"{}\">🔗</a></td>", url).as_str());
}
html_content.push_str( html_content.push_str(
format!( format!(
" "
<tr class=\"{}\"> <tr class=\"{}\">
{} {}
{}
<td>{}</td> <td>{}</td>
<td>{}</td> <td>{}</td>
<td>{}</td> <td>{}</td>
</tr> </tr>
", ",
td_class, path_data, data_title, data_artist, data_extension td_class, url_data, path_data, data_title, data_artist, data_format
) )
.as_str(), .as_str(),
); );
@ -79,7 +127,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool) -> String {
} }
pub fn genhtml_command( pub fn genhtml_command(
_args: &CLIArgs, _args: CLIArgs,
genhtml_args: &GenHTMLCommandArgs, genhtml_args: &GenHTMLCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Scanning For Music"); println!("Scanning For Music");
@ -89,7 +137,7 @@ pub fn genhtml_command(
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); println!("Analysing: {:?}", file.join_path_from_source());
let handler = get_format_handler(file)?; let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(false)?; file.info = handler.get_audio_file_info(false)?;
} }
@ -132,16 +180,17 @@ pub fn genhtml_command(
.as_str(), .as_str(),
); );
html_content.push_str(&table_for_files(files, true)); html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base));
html_content.push_str("</body></html>"); html_content.push_str("</body></html>");
let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html"); let file_path = std::path::PathBuf::from(genhtml_args.dest.as_str()).join("index.html");
let html_index_file = std::fs::File::create(file_path); let html_index_file = std::fs::File::create(file_path);
match html_index_file { match html_index_file {
Ok(mut file) => match file.write_all(html_content.as_bytes()) { Ok(mut file) => match file.write_all(html_content.as_bytes()) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
panic!("Could not write html file: {}", e); panic!("Could not write HTML file: {}", e);
} }
}, },
Err(e) => { Err(e) => {

View file

@ -22,7 +22,7 @@ fn from_main_tags(tags: &crate::types::Tags) -> Tags {
} }
pub fn get_tags_command( pub fn get_tags_command(
_args: &CLIArgs, _args: CLIArgs,
get_tags_args: &GetTagsCommandArgs, get_tags_args: &GetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut files: Vec<File> = Vec::new(); let mut files: Vec<File> = Vec::new();

View file

@ -1,7 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::thread::scope;
use crossbeam::scope;
use crate::args::CLIArgs; use crate::args::CLIArgs;
use crate::args::ProcessCommandArgs; use crate::args::ProcessCommandArgs;
@ -11,7 +10,7 @@ use crate::utils::formats::get_format_handler;
use crate::utils::replaygain::analyze_replaygain; use crate::utils::replaygain::analyze_replaygain;
use crate::utils::scan_for_music; use crate::utils::scan_for_music;
fn rename_file(_args: &CLIArgs, process_args: &ProcessCommandArgs, file: &mut File) { fn rename_file(process_args: &ProcessCommandArgs, file: &mut File) {
let title = &file.info.tags.title; let title = &file.info.tags.title;
let artist = &file.info.tags.artist; let artist = &file.info.tags.artist;
@ -108,7 +107,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
return Ok(()); return Ok(());
} }
if file.info.replaygain.is_some() && !force { if file.info.contains_replaygain && !force {
println!( println!(
"Skipping replaygain for {:?}, contains already", "Skipping replaygain for {:?}, contains already",
file.join_path_from_source() file.join_path_from_source()
@ -137,7 +136,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
} }
pub fn process_command( pub fn process_command(
args: &CLIArgs, _args: CLIArgs,
process_args: &ProcessCommandArgs, process_args: &ProcessCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
println!("Scanning For Music"); println!("Scanning For Music");
@ -147,48 +146,47 @@ pub fn process_command(
for file in files.iter_mut() { for file in files.iter_mut() {
println!("Analysing: {:?}", file.join_path_from_source()); println!("Analysing: {:?}", file.join_path_from_source());
let handler = get_format_handler(file)?; let mut handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(false)?; file.info = handler.get_audio_file_info(false)?;
} }
println!("Renaming Files"); println!("Renaming Files");
for file in files.iter_mut() { for file in files.iter_mut() {
rename_file(args, process_args, file); rename_file(process_args, file);
} }
if !process_args.skip_replaygain && !process_args.dry_run { if !process_args.skip_replaygain && !process_args.dry_run {
println!("Adding ReplayGain Tags to Files"); println!("Adding ReplayGain Tags to Files");
if process_args.replaygain_threads.is_some() && process_args.replaygain_threads.unwrap() > 1 let threads = process_args.replaygain_threads.unwrap_or(0);
{
let files_copy = files.to_vec();
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy)); if threads <= 1 {
scope(|s| {
for _ in 0..process_args.replaygain_threads.unwrap() {
s.spawn(|_| loop {
let mut jobs = jobs.lock().unwrap();
let job = jobs.pop();
if job.is_none() {
break;
}
let job = job.unwrap().clone();
drop(jobs);
let result = add_replaygain_tags(&job, process_args.force_replaygain);
if result.is_err() {
panic!("Error doing replaygain: {}", result.unwrap_err())
}
});
}
})
.expect("threads haunted");
} else {
for file in files.iter_mut() { for file in files.iter_mut() {
add_replaygain_tags(file, process_args.force_replaygain)?; add_replaygain_tags(file, process_args.force_replaygain)?;
} }
return Ok(());
} else {
let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files));
scope(|s| {
for _ in 0..threads {
s.spawn(|| loop {
let mut jobs = jobs.lock().unwrap();
let job = jobs.pop();
if let Some(job) = job {
let result = add_replaygain_tags(&job, process_args.force_replaygain);
if let Err(err) = result {
panic!("Error doing replaygain: {}", err)
}
} else {
break;
}
});
}
});
} }
} }

View file

@ -6,7 +6,7 @@ use crate::types::File;
use crate::utils::formats::get_format_handler; use crate::utils::formats::get_format_handler;
pub fn set_tags_command( pub fn set_tags_command(
_args: &CLIArgs, _args: CLIArgs,
add_tags_args: &SetTagsCommandArgs, add_tags_args: &SetTagsCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut files: Vec<File> = Vec::new(); let mut files: Vec<File> = Vec::new();

View file

@ -11,13 +11,9 @@ use crate::utils::transcoder::presets::transcode_preset_or_config;
use crate::utils::transcoder::transcode; use crate::utils::transcoder::transcode;
pub fn transcode_command( pub fn transcode_command(
_args: &CLIArgs, _args: CLIArgs,
transcode_args: &TranscodeCommandArgs, transcode_args: &TranscodeCommandArgs,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
if transcode_args.transcode_config.is_none() && transcode_args.transcode_preset.is_none() {
panic!("Please provide Transcode Preset/Config");
}
if let Some(preset) = &transcode_args.transcode_preset { if let Some(preset) = &transcode_args.transcode_preset {
if preset == "list" { if preset == "list" {
print_presets(); print_presets();
@ -25,19 +21,16 @@ pub fn transcode_command(
} }
} }
println!("Transcoding");
let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
let transcode_config = transcode_preset_or_config( let transcode_config = transcode_preset_or_config(
transcode_args.transcode_preset.as_ref(), transcode_args.transcode_preset.as_ref(),
transcode_args.transcode_config.as_ref(), transcode_args.transcode_config.as_ref(),
); )
.expect("transcode config error");
if transcode_config.is_err() { println!("Transcoding");
panic!("Please provide Transcode Preset/Config");
} let input_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.source));
let transcode_config = transcode_config.unwrap(); let output_file = File::from_path("".to_string(), PathBuf::from(&transcode_args.dest));
if !transcode_args.ignore_extension { if !transcode_args.ignore_extension {
if let Some(ref file_extension) = transcode_config.file_extension { if let Some(ref file_extension) = transcode_config.file_extension {
@ -69,7 +62,7 @@ pub fn transcode_command(
transcode( transcode(
input_file, input_file,
transcode_args.dest.clone(), transcode_args.dest.clone(),
transcode_config, &transcode_config,
Some(tx), Some(tx),
)?; )?;
child.join().expect("oops! the child thread panicked"); child.join().expect("oops! the child thread panicked");

View file

@ -17,29 +17,26 @@ use commands::transcode::transcode_command;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = CLIArgs::parse(); let cli = CLIArgs::parse();
match cli.command.as_ref() { let command = cli.command.to_owned();
Some(Commands::Process(process_args)) => {
process_command(&cli, process_args)?;
}
Some(Commands::Genhtml(genhtml_args)) => {
genhtml_command(&cli, genhtml_args)?;
}
Some(Commands::Transcode(transcode_args)) => {
transcode_command(&cli, transcode_args)?;
}
Some(Commands::Copy(copy_args)) => {
copy_command(&cli, copy_args)?;
}
Some(Commands::SetTags(set_tags_args)) => {
set_tags_command(&cli, set_tags_args)?;
}
Some(Commands::GetTags(get_tags_args)) => {
get_tags_command(&cli, get_tags_args)?;
}
None => {
panic!("please provide a subcommand");
}
}
Ok(()) match command {
Commands::Process(process_args) => {
return process_command(cli, &process_args);
}
Commands::Genhtml(genhtml_args) => {
return genhtml_command(cli, &genhtml_args);
}
Commands::Transcode(transcode_args) => {
return transcode_command(cli, &transcode_args);
}
Commands::Copy(copy_args) => {
return copy_command(cli, &copy_args);
}
Commands::SetTags(set_tags_args) => {
return set_tags_command(cli, &set_tags_args);
}
Commands::GetTags(get_tags_args) => {
return get_tags_command(cli, &get_tags_args);
}
}
} }

View file

@ -1,6 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::utils::formats::AudioContainer; use crate::utils::format_detection::FileFormat;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tags { pub struct Tags {
@ -24,20 +24,6 @@ pub struct ReplayGainData {
pub track_peak: String, pub track_peak: String,
} }
impl ReplayGainData {
pub fn to_raw(&self) -> ReplayGainRawData {
let track_gain = self.track_gain.split(' ').next().unwrap_or("0.0");
let track_gain = track_gain.parse::<f64>().unwrap();
let track_peak = self.track_peak.parse::<f64>().unwrap();
ReplayGainRawData {
track_gain,
track_peak,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ReplayGainRawData { pub struct ReplayGainRawData {
pub track_gain: f64, pub track_gain: f64,
@ -45,31 +31,27 @@ pub struct ReplayGainRawData {
} }
impl ReplayGainRawData { impl ReplayGainRawData {
pub fn to_normal(&self) -> ReplayGainData { 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 { ReplayGainData {
track_gain: format!("{:.2} dB", self.track_gain), track_gain: format!("{:.2} dB", self.track_gain),
track_peak: format!("{:.6}", self.track_peak), track_peak: format!("{:.6}", self.track_peak),
} }
} }
}
} }
#[derive(Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct AudioFileInfo { pub struct AudioFileInfo {
pub tags: Tags, pub tags: Tags,
pub replaygain: Option<ReplayGainData>, pub contains_replaygain: bool,
pub supports_replaygain: bool, pub supports_replaygain: bool,
pub container: AudioContainer, pub format: Option<FileFormat>,
}
impl Default for AudioFileInfo {
fn default() -> Self {
AudioFileInfo {
tags: Tags::default(),
replaygain: None,
supports_replaygain: false,
container: AudioContainer::Unknown,
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -90,22 +72,24 @@ pub struct File {
impl File { impl File {
pub fn from_path(source_dir: String, full_path: PathBuf) -> File { pub fn from_path(source_dir: String, full_path: PathBuf) -> File {
let full_file_path = PathBuf::from(&source_dir).join(full_path); let full_file_path = PathBuf::from(&source_dir).join(full_path);
let filename = full_file_path
let filename_without_extension = full_file_path
.file_stem() .file_stem()
.unwrap() .expect("filename invalid")
.to_str() .to_string_lossy()
.unwrap()
.to_string(); .to_string();
let extension = full_file_path.extension(); let extension = full_file_path.extension();
let extension = if let Some(extension) = extension { let extension = if let Some(extension) = extension {
extension.to_str().unwrap().to_string() extension.to_string_lossy().to_string()
} else { } else {
"".to_string() "".to_string()
}; };
let path_from_src = full_file_path.strip_prefix(&source_dir).unwrap(); let path_from_src = full_file_path
.strip_prefix(&source_dir)
.expect("couldn't get path relative to source");
let mut folder_path_from_src = path_from_src.to_path_buf(); let mut folder_path_from_src = path_from_src.to_path_buf();
folder_path_from_src.pop(); folder_path_from_src.pop();
@ -113,7 +97,7 @@ impl File {
let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src); let path_to = PathBuf::from(&source_dir).join(&folder_path_from_src);
File { File {
filename, filename: filename_without_extension,
extension, extension,
path_from_source: folder_path_from_src, path_from_source: folder_path_from_src,
path_to, path_to,

View file

@ -5,16 +5,16 @@ const MAPPINGS_DATA: &str = include_str!("mappings.json");
lazy_static! { lazy_static! {
static ref MAPPINGS: HashMap<char, String> = { static ref MAPPINGS: HashMap<char, String> = {
let data: HashMap<String, String> = serde_json::from_str(MAPPINGS_DATA).unwrap(); let data: HashMap<String, String> =
serde_json::from_str(MAPPINGS_DATA).expect("mapping data invalid");
let mut replacement_map: HashMap<char, String> = HashMap::new(); let mut replacement_map: HashMap<char, String> = HashMap::new();
for (chr, repl) in &data { for (chr, repl) in &data {
match chr.parse::<u32>() { match chr.parse::<u32>() {
Ok(n) => { Ok(n) => {
let b = char::from_u32(n); let b = char::from_u32(n).expect("invalid char in string");
if b.is_some() {
replacement_map.insert(b.unwrap(), repl.to_string()); replacement_map.insert(b, repl.to_string());
}
} }
Err(e) => { Err(e) => {
panic!( panic!(
@ -41,14 +41,9 @@ pub fn reduce_to_ascii(input: String) -> String {
continue; continue;
} }
match MAPPINGS.get(&c) { if let Some(replacement) = MAPPINGS.get(&c) {
Some(replacement) => {
output.push_str(replacement); output.push_str(replacement);
} }
None => {
output.push_str("");
}
}
} }
output output

View file

@ -0,0 +1,129 @@
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)
}

View file

@ -1,8 +1,11 @@
use std::path::PathBuf; 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 { pub struct FLACAudioFormat {
flac_tags: metaflac::Tag, 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_gain = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_GAIN");
let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK"); let track_peak = flac_get_first(&self.flac_tags, "REPLAYGAIN_TRACK_PEAK");
if track_gain.is_none() || track_peak.is_none() { if track_gain.is_none() || track_peak.is_none() {
return None; return false;
} }
Some(ReplayGainData { true
track_gain: track_gain.unwrap(),
track_peak: track_peak.unwrap(),
})
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
@ -94,13 +94,16 @@ impl AudioContainerFormat for FLACAudioFormat {
Ok(()) Ok(())
} }
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> { fn get_audio_file_info(
return Ok(AudioFileInfo { &mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?, tags: self.get_tags(allow_missing_tags)?,
replaygain: self.get_replaygain_data(), contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(), supports_replaygain: self.supports_replaygain(),
container: AudioContainer::FLAC, format: Some(FileFormat::FLAC),
}); })
} }
} }

View file

@ -1,11 +1,17 @@
use std::{path::{PathBuf, Path}, process::Command}; use std::{
path::{Path, PathBuf},
process::Command,
};
use serde::Deserialize; use serde::Deserialize;
use string_error::static_err; 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)] #[derive(Default)]
struct Changes { struct Changes {
@ -55,7 +61,7 @@ impl Default for FFProbeTags {
} }
pub struct GenericFFMpegAudioFormat { pub struct GenericFFMpegAudioFormat {
container_type: AudioContainer, format_type: FileFormat,
path: Box<PathBuf>, path: Box<PathBuf>,
extracted_data: ExtractedData, extracted_data: ExtractedData,
@ -127,23 +133,20 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
Ok(tags) Ok(tags)
} }
fn get_replaygain_data(&self) -> Option<ReplayGainData> { fn contains_replaygain_tags(&self) -> bool {
if let Some(data) = &self.changes.replaygain_data { if self.changes.replaygain_data.is_some() {
return Some(data.to_normal()); return true;
} }
self.extracted_data.replaygain_data.clone() if self.extracted_data.replaygain_data.is_some() {
return true;
};
false
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
match self.container_type { false
AudioContainer::MP3 => true,
AudioContainer::FLAC => true,
AudioContainer::WAV => true,
AudioContainer::OGG => false, // ffprobe can't do OGG tags
AudioContainer::AIFF => true,
AudioContainer::Unknown => false,
}
} }
fn set_title(&mut self, title: String) -> Result<(), BoxedError> { fn set_title(&mut self, title: String) -> Result<(), BoxedError> {
@ -162,6 +165,13 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
} }
fn save_changes(&mut self) -> Result<(), BoxedError> { fn save_changes(&mut self) -> Result<(), BoxedError> {
if self.changes.title.is_none()
&& self.changes.artist.is_none()
&& self.changes.replaygain_data.is_none()
{
return Ok(());
}
let mut args: Vec<String> = Vec::new(); let mut args: Vec<String> = Vec::new();
let tempdir = tempfile::tempdir()?; let tempdir = tempfile::tempdir()?;
@ -176,15 +186,18 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
args.extend(vec!["-c".to_string(), "copy".to_string()]); args.extend(vec!["-c".to_string(), "copy".to_string()]);
if let Some(title) = self.changes.title.clone() { if let Some(title) = &self.changes.title {
args.extend(vec![ args.extend(vec![
"-metadata".to_string(), "-metadata".to_string(),
format!("title=\"{}\"", title), format!("title=\"{}\"", title.as_str()),
]) ])
} }
if let Some(artist) = self.changes.artist.clone() { if let Some(artist) = &self.changes.artist {
args.extend(vec!["-metadata".to_string(), format!("artist={}", artist)]) args.extend(vec![
"-metadata".to_string(),
format!("artist={}", artist.as_str()),
])
} }
args.push(temp_file.to_string_lossy().to_string()); args.push(temp_file.to_string_lossy().to_string());
@ -198,22 +211,25 @@ impl AudioContainerFormat for GenericFFMpegAudioFormat {
Ok(()) Ok(())
} }
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> { fn get_audio_file_info(
return Ok(AudioFileInfo { &mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?, tags: self.get_tags(allow_missing_tags)?,
replaygain: self.get_replaygain_data(), contains_replaygain: self.contains_replaygain_tags(),
supports_replaygain: self.supports_replaygain(), supports_replaygain: self.supports_replaygain(),
container: self.container_type, format: Some(self.format_type),
}); })
} }
} }
pub fn new_generic_ffmpeg_format_handler( pub fn new_generic_ffmpeg_format_handler(
path: &Path, path: &Path,
container_type: AudioContainer, format_type: FileFormat,
) -> Result<GenericFFMpegAudioFormat, BoxedError> { ) -> Result<GenericFFMpegAudioFormat, BoxedError> {
let mut handler = GenericFFMpegAudioFormat { let mut handler = GenericFFMpegAudioFormat {
container_type, format_type,
path: Box::new(path.to_path_buf()), path: Box::new(path.to_path_buf()),
extracted_data: ExtractedData::default(), extracted_data: ExtractedData::default(),
changes: Changes::default(), changes: Changes::default(),

View 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> {
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)?,
})
}

View file

@ -2,12 +2,14 @@ use std::path::PathBuf;
use id3::TagLike; 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 { pub struct ID3AudioFormat {
container_type: AudioContainer,
id3_tags: id3::Tag, id3_tags: id3::Tag,
path: Box<PathBuf>, 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 frames = self.id3_tags.frames();
let mut contains_replaygain_tags = false; let mut contains_replaygain_tags = false;
let mut replaygain_data = ReplayGainData {
track_gain: "".to_string(),
track_peak: "".to_string(),
};
for frame in frames { for frame in frames {
if frame.id() == "TXXX" { if frame.id() == "TXXX" {
@ -47,11 +45,9 @@ impl AudioContainerFormat for ID3AudioFormat {
match extended_text.description.as_str() { match extended_text.description.as_str() {
"REPLAYGAIN_TRACK_GAIN" => { "REPLAYGAIN_TRACK_GAIN" => {
contains_replaygain_tags = true; contains_replaygain_tags = true;
replaygain_data.track_gain = extended_text.value.clone()
} }
"REPLAYGAIN_TRACK_PEAK" => { "REPLAYGAIN_TRACK_PEAK" => {
contains_replaygain_tags = true; contains_replaygain_tags = true;
replaygain_data.track_peak = extended_text.value.clone()
} }
_ => {} _ => {}
} }
@ -59,11 +55,7 @@ impl AudioContainerFormat for ID3AudioFormat {
} }
} }
if !contains_replaygain_tags { contains_replaygain_tags
None
} else {
Some(replaygain_data)
}
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
@ -119,30 +111,23 @@ impl AudioContainerFormat for ID3AudioFormat {
Ok(()) Ok(())
} }
fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> { fn get_audio_file_info(
return Ok(AudioFileInfo { &mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?, tags: self.get_tags(allow_missing_tags)?,
replaygain: self.get_replaygain_data(),
supports_replaygain: self.supports_replaygain(), 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( pub fn new_id3_format_handler(path: &PathBuf) -> Result<ID3AudioFormat, BoxedError> {
path: &PathBuf, let id3_tags = id3::Tag::read_from_path(path)?;
container_type: AudioContainer,
) -> Result<ID3AudioFormat, BoxedError> {
let id3_tags = match container_type {
// Only works on ID3 containing WAV files, but doesn't seem very widespread
AudioContainer::WAV => id3::Tag::read_from_wav_path(path)?,
AudioContainer::AIFF => id3::Tag::read_from_aiff_path(path)?,
_ => id3::Tag::read_from_path(path)?,
};
Ok(ID3AudioFormat { Ok(ID3AudioFormat {
container_type,
id3_tags, id3_tags,
path: Box::new(path.clone()), path: Box::new(path.clone()),
}) })

View file

@ -2,31 +2,29 @@ pub mod flac;
pub mod generic_ffmpeg; pub mod generic_ffmpeg;
pub mod id3; pub mod id3;
#[cfg(feature = "taglib")]
pub mod generic_taglib;
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
use thiserror::Error; 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::flac::new_flac_format_handler;
use self::id3::new_id3_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)] use super::format_detection::{detect_format, FileFormat};
pub enum AudioContainer {
MP3, type BoxedError = Box<dyn Error>;
FLAC,
WAV,
OGG,
AIFF,
Unknown,
}
pub trait AudioContainerFormat { pub trait AudioContainerFormat {
fn get_tags(&self, allow_missing: bool) -> Result<Tags, BoxedError>; 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 supports_replaygain(&self) -> bool;
fn set_title(&mut self, title: String) -> Result<(), BoxedError>; fn set_title(&mut self, title: String) -> Result<(), BoxedError>;
@ -35,7 +33,10 @@ pub trait AudioContainerFormat {
fn save_changes(&mut self) -> Result<(), BoxedError>; 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)] #[derive(Error, Debug)]
@ -47,30 +48,92 @@ pub enum AudioFormatError {
} }
pub fn get_format_handler(file: &File) -> Result<Box<dyn AudioContainerFormat>, BoxedError> { pub fn get_format_handler(file: &File) -> Result<Box<dyn AudioContainerFormat>, BoxedError> {
// TODO: detect format from magic bytes rather than extension let format = detect_format(&file.join_path_to())?;
match file.extension.as_str() { let path = file.join_path_to();
"mp3" => {
return Ok(Box::new(new_id3_format_handler( #[cfg(feature = "taglib")]
&file.join_path_to(), {
AudioContainer::MP3, 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))?));
} }
// "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");
} }
} }
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(); let ext = file_path.extension();
if ext.is_none() { if ext.is_none() {
return false; return false;
} }
let ext = ext.unwrap().to_str().unwrap(); let ext = ext.unwrap().to_str().unwrap();
#[cfg(feature = "taglib")]
{
if matches!(ext, "ogg" | "opus" | "wav" | "wv" | "aiff") {
return true;
}
}
matches!(ext, "mp3" | "flac") 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,
}
}

View file

@ -1,10 +1,10 @@
pub mod ascii_reduce; pub mod ascii_reduce;
pub mod format_detection;
pub mod replaygain; pub mod replaygain;
pub mod transcoder; pub mod transcoder;
pub mod formats; pub mod formats;
pub(self) mod music_scanner; pub(self) mod music_scanner;
pub use formats::is_supported_file;
pub use music_scanner::scan_for_music; pub use music_scanner::scan_for_music;
pub use formats::is_supported_file_extension;

View file

@ -3,7 +3,7 @@ use std::fs;
use crate::types::File; use crate::types::File;
use walkdir::WalkDir; use walkdir::WalkDir;
use super::is_supported_file_extension; use super::is_supported_file;
pub fn find_extra_files( pub fn find_extra_files(
src_dir: String, src_dir: String,
@ -36,14 +36,14 @@ pub fn find_extra_files(
pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error::Error>> { pub fn scan_for_music(src_dir: &String) -> Result<Vec<File>, Box<dyn std::error::Error>> {
let mut files: Vec<File> = Vec::new(); let mut files: Vec<File> = Vec::new();
for entry in WalkDir::new(&src_dir) { for entry in WalkDir::new(src_dir) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let entry_path = entry.into_path(); let entry_path = entry.into_path();
if entry_path.is_dir() { if entry_path.is_dir() {
continue; continue;
} }
if is_supported_file_extension(&entry_path) { if is_supported_file(&entry_path) {
let mut file = File::from_path(src_dir.clone(), entry_path.clone()); let mut file = File::from_path(src_dir.clone(), entry_path.clone());
file.extra_files file.extra_files

View file

@ -8,7 +8,7 @@ use std::{
time::Duration, time::Duration,
}; };
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use serde::Deserialize; use serde::Deserialize;
use string_error::static_err; use string_error::static_err;
@ -81,16 +81,27 @@ pub fn progress_monitor(
let _ = &tempdir; let _ = &tempdir;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap(); let mut watcher = RecommendedWatcher::new(
tx,
notify::Config::default().with_poll_interval(Duration::from_millis(100)),
)
.expect("could not watch for ffmpeg log progress status");
watcher watcher
.watch(&file_path, RecursiveMode::NonRecursive) .watch(&file_path, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
let mut pos = 0; let mut pos = 0;
'outer: loop { 'outer: for res in rx {
match rx.recv() { if res.is_err() {
Ok(DebouncedEvent::Write(_)) => { break 'outer;
}
let res = res.unwrap();
match res.kind {
EventKind::Modify(_) => {
let mut file = fs::File::open(&file_path).unwrap(); let mut file = fs::File::open(&file_path).unwrap();
file.seek(SeekFrom::Start(pos)).unwrap(); file.seek(SeekFrom::Start(pos)).unwrap();
@ -119,13 +130,8 @@ pub fn progress_monitor(
} }
} }
} }
Ok(DebouncedEvent::NoticeRemove(_)) => { EventKind::Remove(_) => break 'outer,
break 'outer; _ => {}
}
Ok(_) => {}
Err(_) => {
break 'outer;
}
} }
} }
}); });

View file

@ -8,7 +8,7 @@ use super::{progress_monitor, types::TranscodeConfig};
pub fn transcode( pub fn transcode(
file: File, file: File,
dest: String, dest: String,
config: TranscodeConfig, config: &TranscodeConfig,
progress_sender: Option<Sender<String>>, progress_sender: Option<Sender<String>>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut command_args: Vec<String> = Vec::new(); let mut command_args: Vec<String> = Vec::new();
@ -19,28 +19,28 @@ pub fn transcode(
file.join_path_to().to_string_lossy().to_string(), file.join_path_to().to_string_lossy().to_string(),
]); ]);
if let Some(encoder) = config.encoder { if let Some(encoder) = &config.encoder {
command_args.extend(vec!["-c:a".to_string(), encoder]); command_args.extend(vec!["-c:a".to_string(), encoder.to_string()]);
} }
if let Some(container) = config.container { if let Some(container) = &config.container {
command_args.extend(vec!["-f".to_string(), container]); command_args.extend(vec!["-f".to_string(), container.to_string()]);
} }
if let Some(sample_rate) = config.sample_rate { if let Some(sample_rate) = &config.sample_rate {
command_args.extend(vec!["-ar".to_string(), sample_rate]); command_args.extend(vec!["-ar".to_string(), sample_rate.to_string()]);
} }
if let Some(channels) = config.channels { if let Some(channels) = &config.channels {
command_args.extend(vec!["-ac".to_string(), channels]); command_args.extend(vec!["-ac".to_string(), channels.to_string()]);
} }
if let Some(quality) = config.quality { if let Some(quality) = &config.quality {
command_args.extend(vec!["-q:a".to_string(), quality]); command_args.extend(vec!["-q:a".to_string(), quality.to_string()]);
} }
if let Some(bitrate) = config.bitrate { if let Some(bitrate) = &config.bitrate {
command_args.extend(vec!["-b:a".to_string(), bitrate]); command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]);
} }
command_args.push(dest); command_args.push(dest);
@ -48,9 +48,9 @@ pub fn transcode(
let mut progress_thread: Option<JoinHandle<()>> = None; let mut progress_thread: Option<JoinHandle<()>> = None;
let mut progress_file: Option<String> = None; let mut progress_file: Option<String> = None;
if progress_sender.is_some() { if let Some(sender) = &progress_sender {
let sender = progress_sender.as_ref().unwrap();
let result = progress_monitor(file.join_path_to(), sender); let result = progress_monitor(file.join_path_to(), sender);
if let Ok(result) = result { if let Ok(result) = result {
progress_thread = Some(result.1); progress_thread = Some(result.1);
progress_file = Some(result.0.clone()); progress_file = Some(result.0.clone());