Compare commits

..

No commits in common. "d5f3cdde3a76f437f6910f39e82a0db9c2e73cfe" and "da77dc89acdf8cc0e4896288cf59bbacc7ae8020" have entirely different histories.

38 changed files with 685 additions and 1817 deletions

1011
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,54 +3,40 @@ 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.0", features = ["derive"] } serde = { version = "1.0.143", features = ["derive"] }
serde_yaml = "0.9" serde_yaml = "0.9.9"
serde_json = "1.0" serde_json = "1.0"
serde_with = "3" serde_with = "1.3.1"
# argument parsing # argument parsing
clap = { version = "4", features = ["derive"] } clap = { version = "3.2.17", features = ["derive"] }
# ascii_reduce constants & transcode presets # ascii_reduce constants & transcode presets
lazy_static = "1" lazy_static = "1.4.0"
# for scan_for_music # for scan_for_music
walkdir = "2" walkdir = "2.3.2"
# format detection
infer = "0.15"
bytes = "1"
# tag reading # tag reading
id3 = "1" id3 = "1.3.0"
metaflac = "0.2" metaflac = "0.2.5"
taglib = { path = "./modules/taglib", optional = true }
# for genhtml command # for genhtml command
html-escape = "0.2" html-escape = "0.2.11"
urlencoding = "2"
# error handling # error handling
thiserror = "1" thiserror = "1.0"
string-error = "0.1" string-error = "0.1.0"
# 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 = "6" notify = "4.0.17"
[features] # scoped threads
default = ["taglib"] crossbeam = "0.8"
taglib = ["dep:taglib"]

3
README.md Normal file
View file

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

View file

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

View file

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1668681692,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,16 +18,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1695644571, "lastModified": 1670980281,
"narHash": "sha256-asS9dCCdlt1lPq0DLwkVBbVoEKuEuz+Zi3DG7pR/RxA=", "narHash": "sha256-g0t/SmQca/JBEd+3Ry1qFgDfDK8ME9AM6EP4YUl8/lo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6500b4580c2a1f3d0f980d32d285739d8e156d92", "rev": "5cb48ea3c19ce2e5746a44d6b91847396bd28c1f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "release-22.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@ -39,31 +39,13 @@
"utils": "utils" "utils": "utils"
} }
}, },
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": { "utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1694529238, "lastModified": 1667395993,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"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/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/release-22.05";
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,47 +17,31 @@
... ...
}: }:
{ {
overlays.musicutil = final: prev: let overlay = final: prev: let
system = final.system; system = final.system;
pkgs = final.pkgs;
lib = pkgs.lib;
stdenv = pkgs.stdenv;
in { in {
musicutil = pkgs.rustPlatform.buildRustPackage rec { musicutil = final.rustPlatform.buildRustPackage rec {
pname = "musicutil"; pname = "musicutil";
version = "latest"; version = "latest";
src = ./.; src = ./.;
cargoLock = {lockFile = ./Cargo.lock;}; cargoLock = {lockFile = ./Cargo.lock;};
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' '${pkgs.ffmpeg}/bin/ffmpeg' substituteInPlace src/meta.rs --replace 'ffmpeg' '${final.ffmpeg}/bin/ffmpeg'
substituteInPlace src/meta.rs --replace 'ffprobe' '${pkgs.ffmpeg}/bin/ffprobe' substituteInPlace src/meta.rs --replace 'ffprobe' '${final.ffmpeg}/bin/ffprobe'
''; '';
doCheck = false; doCheck = false;
nativeBuildInputs = with pkgs; [pkg-config rustc cargo]; nativeBuildInputs = with final.pkgs; [pkg-config rustc cargo];
buildInputs = with pkgs; [ffmpeg zlib taglib]; buildInputs = with final; [ffmpeg];
}; };
}; };
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.overlays.default]; overlays = [self.overlay];
}; };
in { in {
defaultPackage = self.packages."${system}".musicutil; defaultPackage = self.packages."${system}".musicutil;
@ -76,7 +60,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; [zlib taglib pkg-config rustc cargo clippy rust-analyzer rustfmt]; buildInputs = with pkgs; [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;

View file

@ -1,14 +0,0 @@
[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"

View file

@ -1,36 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,87 +0,0 @@
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

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

View file

@ -1,33 +0,0 @@
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

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

View file

@ -1,22 +0,0 @@
#![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

@ -1,12 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,98 +0,0 @@
#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

@ -1,30 +0,0 @@
#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: Commands, pub command: Option<Commands>,
} }
#[derive(Debug, Clone, Subcommand)] #[derive(Debug, 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, Clone, Args)] #[derive(Debug, 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, Clone, Args)] #[derive(Debug, Args)]
pub struct GenHTMLCommandArgs { pub struct GenHTMLCommandArgs {
pub source: String, pub source: String,
pub dest: String, pub dest: String,
@ -38,11 +38,9 @@ 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, Clone, Args)] #[derive(Debug, Args)]
pub struct TranscodeCommandArgs { pub struct TranscodeCommandArgs {
pub source: String, pub source: String,
pub dest: String, pub dest: String,

View file

@ -5,9 +5,10 @@ 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,
@ -23,7 +24,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() {
@ -44,7 +45,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 mut handler = get_format_handler(file)?; let handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(true)?; file.info = handler.get_audio_file_info(true)?;
} }
@ -56,7 +57,6 @@ 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())
.expect("invalid destination") .unwrap()
.join(directory), .join(directory),
)?; )?;
} }
@ -108,15 +108,14 @@ 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()).expect("invalid destination"); let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap();
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!(
@ -150,7 +149,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() {
@ -160,15 +159,14 @@ fn transcode_file(
} }
}; };
let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).expect("invalid destination"); let to_path_dest = PathBuf::from_str(copy_args.dest.as_str()).unwrap();
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() {
@ -181,14 +179,11 @@ 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);
} }
@ -204,38 +199,42 @@ 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(),
) )
.expect("transcode config error"); .unwrap();
let threads = copy_args.threads.unwrap_or(1); if copy_args.threads.is_some() && copy_args.threads.unwrap() > 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 copy_args = Arc::new(copy_args); let transcode_config_arc = Arc::new(transcode_config);
let transcode_config = Arc::new(transcode_config);
scope(|s| { scope(|s| {
for _ in 0..threads { for _ in 0..copy_args.threads.unwrap() {
s.spawn(|| loop { s.spawn(|_| loop {
let mut jobs = jobs.lock().unwrap(); let mut jobs = jobs.lock().unwrap();
let job = jobs.pop(); let job = jobs.pop();
if let Some(job) = job { if job.is_none() {
let result = transcode_file(&job, &copy_args, &transcode_config, true);
if let Err(err) = result {
panic!("Error Transcoding: {}", err)
}
} else {
break; break;
} }
}); let job = job.unwrap().clone();
drop(jobs);
let result = transcode_file(
&job,
&copy_args_arc,
transcode_config_arc.as_ref().clone(),
true,
);
if result.is_err() {
panic!("Error Transcoding: {}", result.unwrap_err())
} }
}); });
}
})
.expect("threads haunted");
} else { } else {
for file in files.iter() { for file in files.iter() {
transcode_file(file, copy_args, &transcode_config, false)?; transcode_file(file, copy_args, transcode_config.clone(), false)?;
} }
} }

View file

@ -4,32 +4,23 @@ 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, link_base: &Option<String>) -> String { fn table_for_files(files: Vec<File>, includes_path: bool) -> 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>
@ -38,7 +29,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
</thead> </thead>
<tbody> <tbody>
", ",
link_head, path_head path_head
) )
.as_str(), .as_str(),
); );
@ -52,14 +43,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
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 {
@ -67,49 +51,17 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
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, url_data, path_data, data_title, data_artist, data_format td_class, path_data, data_title, data_artist, data_extension
) )
.as_str(), .as_str(),
); );
@ -127,7 +79,7 @@ fn table_for_files(files: Vec<File>, includes_path: bool, link_base: &Option<Str
} }
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");
@ -137,7 +89,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 mut handler = get_format_handler(file)?; let handler = get_format_handler(file)?;
file.info = handler.get_audio_file_info(false)?; file.info = handler.get_audio_file_info(false)?;
} }
@ -180,17 +132,16 @@ pub fn genhtml_command(
.as_str(), .as_str(),
); );
html_content.push_str(&table_for_files(files, true, &genhtml_args.link_base)); html_content.push_str(&table_for_files(files, true));
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,6 +1,7 @@
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;
@ -10,7 +11,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(process_args: &ProcessCommandArgs, file: &mut File) { fn rename_file(_args: &CLIArgs, 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;
@ -107,7 +108,7 @@ pub fn add_replaygain_tags(file: &File, force: bool) -> Result<(), Box<dyn std::
return Ok(()); return Ok(());
} }
if file.info.contains_replaygain && !force { if file.info.replaygain.is_some() && !force {
println!( println!(
"Skipping replaygain for {:?}, contains already", "Skipping replaygain for {:?}, contains already",
file.join_path_from_source() file.join_path_from_source()
@ -136,7 +137,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");
@ -146,47 +147,48 @@ 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 mut handler = get_format_handler(file)?; let 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(process_args, file); rename_file(args, 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");
let threads = process_args.replaygain_threads.unwrap_or(0); if process_args.replaygain_threads.is_some() && process_args.replaygain_threads.unwrap() > 1
{
let files_copy = files.to_vec();
if threads <= 1 { let jobs: Arc<Mutex<Vec<File>>> = Arc::new(Mutex::new(files_copy));
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,9 +11,13 @@ 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();
@ -21,16 +25,19 @@ 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");
println!("Transcoding"); if transcode_config.is_err() {
panic!("Please provide Transcode Preset/Config");
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.dest)); let transcode_config = transcode_config.unwrap();
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 {
@ -62,7 +69,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,26 +17,29 @@ 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();
let command = cli.command.to_owned(); match cli.command.as_ref() {
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");
}
}
match command { Ok(())
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::format_detection::FileFormat; use crate::utils::formats::AudioContainer;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tags { pub struct Tags {
@ -24,6 +24,20 @@ 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,
@ -31,27 +45,31 @@ pub struct ReplayGainRawData {
} }
impl ReplayGainRawData { impl ReplayGainRawData {
pub fn to_normal(&self, is_ogg_opus: bool) -> ReplayGainData { pub fn to_normal(&self) -> 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(Default, Debug, Clone)] #[derive(Debug, Clone)]
pub struct AudioFileInfo { pub struct AudioFileInfo {
pub tags: Tags, pub tags: Tags,
pub contains_replaygain: bool, pub replaygain: Option<ReplayGainData>,
pub supports_replaygain: bool, pub supports_replaygain: bool,
pub format: Option<FileFormat>, pub container: AudioContainer,
}
impl Default for AudioFileInfo {
fn default() -> Self {
AudioFileInfo {
tags: Tags::default(),
replaygain: None,
supports_replaygain: false,
container: AudioContainer::Unknown,
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -72,24 +90,22 @@ 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()
.expect("filename invalid") .unwrap()
.to_string_lossy() .to_str()
.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_string_lossy().to_string() extension.to_str().unwrap().to_string()
} else { } else {
"".to_string() "".to_string()
}; };
let path_from_src = full_file_path let path_from_src = full_file_path.strip_prefix(&source_dir).unwrap();
.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();
@ -97,7 +113,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_without_extension, filename,
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> = let data: HashMap<String, String> = serde_json::from_str(MAPPINGS_DATA).unwrap();
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).expect("invalid char in string"); let b = char::from_u32(n);
if b.is_some() {
replacement_map.insert(b, repl.to_string()); replacement_map.insert(b.unwrap(), repl.to_string());
}
} }
Err(e) => { Err(e) => {
panic!( panic!(
@ -41,9 +41,14 @@ pub fn reduce_to_ascii(input: String) -> String {
continue; continue;
} }
if let Some(replacement) = MAPPINGS.get(&c) { match MAPPINGS.get(&c) {
Some(replacement) => {
output.push_str(replacement); output.push_str(replacement);
} }
None => {
output.push_str("");
}
}
} }
output output

View file

@ -1,129 +0,0 @@
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,11 +1,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::{ use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
};
use super::{AudioContainerFormat, AudioFormatError, BoxedError}; use super::{AudioContainer, AudioContainerFormat, AudioFormatError, BoxedError};
pub struct FLACAudioFormat { pub struct FLACAudioFormat {
flac_tags: metaflac::Tag, flac_tags: metaflac::Tag,
@ -44,15 +41,18 @@ impl AudioContainerFormat for FLACAudioFormat {
}) })
} }
fn contains_replaygain_tags(&self) -> bool { fn get_replaygain_data(&self) -> Option<ReplayGainData> {
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 false; return None;
} }
true Some(ReplayGainData {
track_gain: track_gain.unwrap(),
track_peak: track_peak.unwrap(),
})
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
@ -94,16 +94,13 @@ impl AudioContainerFormat for FLACAudioFormat {
Ok(()) Ok(())
} }
fn get_audio_file_info( fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
&mut self, return Ok(AudioFileInfo {
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError> {
Ok(AudioFileInfo {
tags: self.get_tags(allow_missing_tags)?, tags: self.get_tags(allow_missing_tags)?,
contains_replaygain: self.contains_replaygain_tags(), replaygain: self.get_replaygain_data(),
supports_replaygain: self.supports_replaygain(), supports_replaygain: self.supports_replaygain(),
format: Some(FileFormat::FLAC), container: AudioContainer::FLAC,
}) });
} }
} }

View file

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

@ -1,166 +0,0 @@
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,14 +2,12 @@ use std::path::PathBuf;
use id3::TagLike; use id3::TagLike;
use crate::{ use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
types::{AudioFileInfo, ReplayGainRawData, Tags},
utils::format_detection::FileFormat,
};
use super::{AudioContainerFormat, AudioFormatError, BoxedError}; use super::{AudioContainer, 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>,
} }
@ -34,10 +32,14 @@ impl AudioContainerFormat for ID3AudioFormat {
}) })
} }
fn contains_replaygain_tags(&self) -> bool { fn get_replaygain_data(&self) -> Option<ReplayGainData> {
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" {
@ -45,9 +47,11 @@ 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()
} }
_ => {} _ => {}
} }
@ -55,7 +59,11 @@ impl AudioContainerFormat for ID3AudioFormat {
} }
} }
contains_replaygain_tags if !contains_replaygain_tags {
None
} else {
Some(replaygain_data)
}
} }
fn supports_replaygain(&self) -> bool { fn supports_replaygain(&self) -> bool {
@ -111,23 +119,30 @@ impl AudioContainerFormat for ID3AudioFormat {
Ok(()) Ok(())
} }
fn get_audio_file_info( fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
&mut self, return Ok(AudioFileInfo {
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(),
format: Some(FileFormat::MP3), container: self.container_type,
contains_replaygain: self.contains_replaygain_tags(), });
})
} }
} }
pub fn new_id3_format_handler(path: &PathBuf) -> Result<ID3AudioFormat, BoxedError> { pub fn new_id3_format_handler(
let id3_tags = id3::Tag::read_from_path(path)?; path: &PathBuf,
container_type: AudioContainer,
) -> Result<ID3AudioFormat, BoxedError> {
let id3_tags = match container_type {
// Only works on ID3 containing WAV files, but doesn't seem very widespread
AudioContainer::WAV => id3::Tag::read_from_wav_path(path)?,
AudioContainer::AIFF => id3::Tag::read_from_aiff_path(path)?,
_ => id3::Tag::read_from_path(path)?,
};
Ok(ID3AudioFormat { Ok(ID3AudioFormat {
container_type,
id3_tags, id3_tags,
path: Box::new(path.clone()), path: Box::new(path.clone()),
}) })

View file

@ -2,29 +2,31 @@ 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, ReplayGainRawData, Tags}; use crate::types::{AudioFileInfo, File, ReplayGainData, 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;
#[cfg(feature = "taglib")]
use self::generic_taglib::new_taglib_format_handler;
use super::format_detection::{detect_format, FileFormat};
type BoxedError = Box<dyn Error>; type BoxedError = Box<dyn Error>;
#[derive(Copy, Clone, Debug)]
pub enum AudioContainer {
MP3,
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 contains_replaygain_tags(&self) -> bool; fn get_replaygain_data(&self) -> Option<ReplayGainData>;
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>;
@ -33,10 +35,7 @@ pub trait AudioContainerFormat {
fn save_changes(&mut self) -> Result<(), BoxedError>; fn save_changes(&mut self) -> Result<(), BoxedError>;
fn get_audio_file_info( fn get_audio_file_info(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError>;
&mut self,
allow_missing_tags: bool,
) -> Result<AudioFileInfo, BoxedError>;
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -48,92 +47,30 @@ 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> {
let format = detect_format(&file.join_path_to())?; // TODO: detect format from magic bytes rather than extension
let path = file.join_path_to(); match file.extension.as_str() {
"mp3" => {
#[cfg(feature = "taglib")] return Ok(Box::new(new_id3_format_handler(
{ &file.join_path_to(),
match format { AudioContainer::MP3,
FileFormat::OggFLAC )?))
| FileFormat::OggSpeex }
| FileFormat::OggVorbis // "aiff" => return Ok(Box::new(new_generic_ffmpeg_format_handler(&file.join_path_to(), AudioContainer::AIFF)?)),
| FileFormat::OggOpus // "wav" => return Ok(Box::new(new_generic_ffmpeg_format_handler(&file.join_path_to(), AudioContainer::WAV)?)),
| FileFormat::Wav "flac" => return Ok(Box::new(new_flac_format_handler(&file.join_path_to())?)),
| FileFormat::WavPack _ => {
| FileFormat::AIFF => { panic!("unsupported filetype");
return Ok(Box::new(new_taglib_format_handler(&path, Some(format))?));
} }
_ => {}
} }
} }
match format { pub fn is_supported_file_extension(file_path: &Path) -> bool {
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");
}
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; use super::is_supported_file_extension;
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(&entry_path) { if is_supported_file_extension(&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::{EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use serde::Deserialize; use serde::Deserialize;
use string_error::static_err; use string_error::static_err;
@ -81,27 +81,16 @@ pub fn progress_monitor(
let _ = &tempdir; let _ = &tempdir;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new( let mut watcher = watcher(tx, Duration::from_millis(100)).unwrap();
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: for res in rx { 'outer: loop {
if res.is_err() { match rx.recv() {
break 'outer; Ok(DebouncedEvent::Write(_)) => {
}
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();
@ -130,8 +119,13 @@ pub fn progress_monitor(
} }
} }
} }
EventKind::Remove(_) => break 'outer, Ok(DebouncedEvent::NoticeRemove(_)) => {
_ => {} 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.to_string()]); command_args.extend(vec!["-c:a".to_string(), encoder]);
} }
if let Some(container) = &config.container { if let Some(container) = config.container {
command_args.extend(vec!["-f".to_string(), container.to_string()]); command_args.extend(vec!["-f".to_string(), container]);
} }
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.to_string()]); command_args.extend(vec!["-ar".to_string(), sample_rate]);
} }
if let Some(channels) = &config.channels { if let Some(channels) = config.channels {
command_args.extend(vec!["-ac".to_string(), channels.to_string()]); command_args.extend(vec!["-ac".to_string(), channels]);
} }
if let Some(quality) = &config.quality { if let Some(quality) = config.quality {
command_args.extend(vec!["-q:a".to_string(), quality.to_string()]); command_args.extend(vec!["-q:a".to_string(), quality]);
} }
if let Some(bitrate) = &config.bitrate { if let Some(bitrate) = config.bitrate {
command_args.extend(vec!["-b:a".to_string(), bitrate.to_string()]); command_args.extend(vec!["-b:a".to_string(), bitrate]);
} }
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 let Some(sender) = &progress_sender { if progress_sender.is_some() {
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());