Add CUE support for commandline ffmpeg usage.

This commit is contained in:
CDrummond 2025-03-02 20:57:59 +00:00
parent c6f9a7faf5
commit 6b9cb960a9
6 changed files with 573 additions and 116 deletions

289
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@ -16,13 +16,14 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
version = "0.7.6"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"getrandom 0.2.4",
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
@ -35,10 +36,16 @@ dependencies = [
]
[[package]]
name = "anyhow"
version = "1.0.53"
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "argparse"
@ -75,6 +82,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base-x"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base64"
version = "0.13.0"
@ -121,11 +134,13 @@ dependencies = [
"configparser",
"dirs",
"env_logger",
"hhmmss",
"if_chain",
"indicatif",
"lofty",
"log",
"num_cpus",
"rcue",
"regex",
"rusqlite",
"substring",
@ -208,7 +223,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"time 0.1.44",
"winapi",
]
@ -237,6 +252,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "const_fn"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@ -307,6 +328,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "discard"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "either"
version = "1.14.0"
@ -418,17 +445,24 @@ name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.7.0"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@ -452,6 +486,16 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hhmmss"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a3a7d0916cb01ef108a66108640419767991ea31d11a1c851bed37686a6062"
dependencies = [
"chrono",
"time 0.2.27",
]
[[package]]
name = "humantime"
version = "2.1.0"
@ -482,7 +526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@ -506,6 +550,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
@ -544,9 +594,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libsqlite3-sys"
version = "0.22.2"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
dependencies = [
"cc",
"pkg-config",
@ -583,7 +633,7 @@ checksum = "764b60e1ddd07e5665a6a17636a95cd7d8f3b86c73503a69c32979d05f72f3cf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.28",
"syn 2.0.32",
]
[[package]]
@ -762,6 +812,12 @@ dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.66"
@ -908,16 +964,15 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.25.4"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c4b1eaf239b47034fb450ee9cdedd7d0226571689d8823030c4b6c2cb407152"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
dependencies = [
"bitflags 1.3.2",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
@ -933,6 +988,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "6.1.0"
@ -979,6 +1043,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -995,6 +1065,67 @@ dependencies = [
"untrusted",
]
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]
[[package]]
name = "serde_json"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "smallvec"
version = "1.8.0"
@ -1007,6 +1138,64 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "standback"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
dependencies = [
"version_check",
]
[[package]]
name = "stdweb"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
dependencies = [
"discard",
"rustc_version",
"stdweb-derive",
"stdweb-internal-macros",
"stdweb-internal-runtime",
"wasm-bindgen",
]
[[package]]
name = "stdweb-derive"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn 1.0.99",
]
[[package]]
name = "stdweb-internal-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
dependencies = [
"base-x",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"serde_json",
"sha1",
"syn 1.0.99",
]
[[package]]
name = "stdweb-internal-runtime"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "strength_reduce"
version = "0.2.4"
@ -1054,9 +1243,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.28"
version = "2.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
dependencies = [
"proc-macro2",
"quote",
@ -1099,7 +1288,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.28",
"syn 2.0.32",
]
[[package]]
@ -1113,6 +1302,44 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
"const_fn",
"libc",
"standback",
"stdweb",
"time-macros",
"version_check",
"winapi",
]
[[package]]
name = "time-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
dependencies = [
"proc-macro-hack",
"time-macros-impl",
]
[[package]]
name = "time-macros-impl"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"standback",
"syn 1.0.99",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
@ -1422,3 +1649,23 @@ name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]

View File

@ -12,7 +12,7 @@ readme = "README.md"
[dependencies]
argparse = "0.2.2"
anyhow = "1.0.40"
rusqlite = { version = "0.25.0", features = ["bundled"] }
rusqlite = { version = "0.28.0", features = ["bundled"] }
log = "0.4.14"
env_logger = "0.8.4"
indicatif = "0.16.2"
@ -26,6 +26,8 @@ configparser = "3.0.0"
if_chain = "1.0.2"
num_cpus = "1.13.0"
which = "7.0.2"
rcue = "0.1.3"
hhmmss = "0.1.0"
[dependencies.bliss-audio]
default-features = false

View File

@ -6,19 +6,24 @@
*
**/
use crate::cue;
use crate::db;
use crate::ffmpeg;
use crate::tags;
use anyhow::Result;
use bliss_audio::decoder::Decoder;
use bliss_audio::{decoder::Decoder, BlissResult, Song};
use hhmmss::Hhmmss;
use if_chain::if_chain;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashSet;
use std::convert::TryInto;
use std::fs::{DirEntry, File};
use std::io::{BufRead, BufReader};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::Duration;
use num_cpus;
const DONT_ANALYSE: &str = ".notmusic";
@ -26,7 +31,7 @@ const MAX_ERRORS_TO_SHOW: usize = 100;
const MAX_TAG_ERRORS_TO_SHOW: usize = 50;
const VALID_EXTENSIONS: [&str; 6] = ["m4a", "mp3", "ogg", "flac", "opus", "wv"];
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>) {
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
if !path.is_dir() {
return;
}
@ -34,20 +39,20 @@ fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut V
if let Ok(items) = path.read_dir() {
for item in items {
if let Ok(entry) = item {
check_dir_entry(db, mpath, entry, track_paths);
check_dir_entry(db, mpath, entry, track_paths, cue_tracks);
}
}
}
}
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>) {
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
let pb = entry.path();
if pb.is_dir() {
let check = pb.join(DONT_ANALYSE);
if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
} else {
get_file_list(db, mpath, &pb, track_paths);
get_file_list(db, mpath, &pb, track_paths, cue_tracks);
}
} else if pb.is_file() {
if_chain! {
@ -68,7 +73,10 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
let cue_track_sname = String::from(cue_track_stripped.to_string_lossy());
if let Ok(id) = db.get_rowid(&cue_track_sname) {
if id<=0 {
track_paths.push(String::from(cue_file.to_string_lossy()));
let this_cue_tracks = cue::parse(&pb, &cue_file);
for track in this_cue_tracks {
cue_tracks.push(track.clone());
}
}
}
}
@ -99,7 +107,6 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
let mut analysed = 0;
let mut failed: Vec<String> = Vec::new();
let mut tag_error: Vec<String> = Vec::new();
let mut reported_cue:HashSet<String> = HashSet::new();
log::info!("Analysing new files");
for (path, result) in <ffmpeg::FFmpegCmdDecoder as Decoder>::analyze_paths_with_cores(track_paths, cpu_threads) {
@ -107,68 +114,20 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
progress.set_message(format!("{}", sname));
let mut inc_progress = true; // Only want to increment progress once for cue tracks
match result {
Ok(track) => {
let cpath = String::from(path.to_string_lossy());
match track.cue_info {
Some(cue) => {
match track.track_number {
Some(track_num) => {
if reported_cue.contains(&cpath) {
inc_progress = false;
} else {
analysed += 1;
reported_cue.insert(cpath);
}
let meta = db::Metadata {
title: track.title.unwrap_or_default().to_string(),
artist: track.artist.unwrap_or_default().to_string(),
album: track.album.unwrap_or_default().to_string(),
album_artist: track.album_artist.unwrap_or_default().to_string(),
genre: track.genre.unwrap_or_default().to_string(),
duration: track.duration.as_secs() as u32
};
// Remove prefix from audio_file_path
let pbuff = PathBuf::from(&cue.audio_file_path);
let stripped = pbuff.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
let db_path = format!("{}{}{}", sname, db::CUE_MARKER, track_num);
db.add_track(&db_path, &meta, &track.analysis);
}
None => { failed.push(format!("{} - No track number?", sname)); }
}
}
None => {
// Use lofty to read tags here, and not bliss's, so that if update
// tags is ever used they are from the same source.
let mut meta = tags::read(&cpath);
if meta.is_empty() {
// Lofty failed? Try from bliss...
meta.title = track.title.unwrap_or_default().to_string();
meta.artist = track.artist.unwrap_or_default().to_string();
meta.album = track.album.unwrap_or_default().to_string();
meta.album_artist = track.album_artist.unwrap_or_default().to_string();
meta.genre = track.genre.unwrap_or_default().to_string();
meta.duration = track.duration.as_secs() as u32;
}
if meta.is_empty() {
tag_error.push(sname.clone());
}
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
}
let meta = tags::read(&cpath);
if meta.is_empty() {
tag_error.push(sname.clone());
}
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
}
Err(e) => { failed.push(format!("{} - {}", sname, e)); }
};
if inc_progress {
progress.inc(1);
}
progress.inc(1);
}
// Reset terminal, otherwise typed output does not show? Perhaps Linux only...
@ -208,6 +167,100 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>,
Ok(())
}
pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
let num_cpus = num_cpus::get();
#[allow(clippy::type_complexity)]
let (tx, rx): (
Sender<(cue::CueTrack, BlissResult<Song>)>,
Receiver<(cue::CueTrack, BlissResult<Song>)>,
) = mpsc::channel();
if tracks.is_empty() {
return Ok(rx);
}
let mut handles = Vec::new();
let mut chunk_length = tracks.len() / num_cpus;
if chunk_length == 0 {
chunk_length = tracks.len();
} else if chunk_length == 1 && tracks.len() > num_cpus {
chunk_length = 2;
}
for chunk in tracks.chunks(chunk_length) {
let tx_thread = tx.clone();
let owned_chunk = chunk.to_owned();
let child = thread::spawn(move || {
for cue_track in owned_chunk {
let audio_path = format!("{}{}{}.00{}{}.00", cue_track.audio_path.to_string_lossy(), ffmpeg::TIME_SEP, cue_track.start.hhmmss(), ffmpeg::TIME_SEP, cue_track.duration.hhmmss());
let track_path = String::from(cue_track.track_path.to_string_lossy());
log::debug!("Analyzing '{}'", track_path);
let song = <ffmpeg::FFmpegCmdDecoder as Decoder>::song_from_path(audio_path);
tx_thread.send((cue_track, song)).unwrap();
}
});
handles.push(child);
}
Ok(rx)
}
pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
let total = cue_tracks.len();
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
let results = analyze_cue_streaming(cue_tracks)?;
let mut analysed = 0;
let mut failed:Vec<String> = Vec::new();
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
log::info!("Analysing new cue tracks");
for (track, result) in results {
let stripped = track.track_path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
pb.set_message(format!("{}", sname));
match result {
Ok(song) => {
let meta = db::Metadata {
title:track.title,
artist:track.artist,
album_artist:track.album_artist,
album:track.album,
genre:track.genre,
duration:if track.duration>=last_track_duration { song.duration.as_secs() as u32 } else { track.duration.as_secs() as u32 }
};
db.add_track(&sname, &meta, &song.analysis);
analysed += 1;
},
Err(e) => {
failed.push(format!("{} - {}", sname, e));
}
};
pb.inc(1);
}
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
if !failed.is_empty() {
let total = failed.len();
failed.truncate(MAX_ERRORS_TO_SHOW);
log::error!("Failed to analyse the folling track(s):");
for err in failed {
log::error!(" {}", err);
}
if total>MAX_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
}
}
Ok(())
}
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_tracks: usize, max_threads: usize) {
let mut db = db::Db::new(&String::from(db_path));
let mut track_count_left = max_num_tracks;
@ -222,22 +275,29 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
let mpath = path.clone();
let cur = path.clone();
let mut track_paths: Vec<String> = Vec::new();
let mut cue_tracks:Vec<cue::CueTrack> = Vec::new();
if mpaths.len() > 1 {
log::info!("Looking for new files in {}", mpath.to_string_lossy());
} else {
log::info!("Looking for new files");
}
get_file_list(&mut db, &mpath, &cur, &mut track_paths);
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks);
track_paths.sort();
log::info!("Num new files: {}", track_paths.len());
if !cue_tracks.is_empty() {
log::info!("Num new cue tracks: {}", cue_tracks.len());
}
if dry_run {
if !track_paths.is_empty() {
if !track_paths.is_empty() || !cue_tracks.is_empty() {
log::info!("The following need to be analysed:");
for track in track_paths {
log::info!(" {}", track);
}
for track in cue_tracks {
log::info!(" {}", track.track_path.to_string_lossy());
}
}
} else {
if max_num_tracks > 0 {
@ -247,6 +307,17 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
}
track_count_left -= track_paths.len();
}
if max_num_tracks>0 {
if track_count_left == 0 {
cue_tracks.clear();
} /*else {
if cue_tracks.len()>track_count_left {
log::info!("Only analysing {} cue tracks", track_count_left);
cue_tracks.truncate(track_count_left);
}
track_count_left -= track_paths.len();
}*/
}
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths, max_threads) {
@ -257,6 +328,13 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
log::info!("No new files to analyse");
}
if !cue_tracks.is_empty() {
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
Ok(_) => { },
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
}
}
if max_num_tracks > 0 && track_count_left <= 0 {
log::info!("File limit reached");
break;

96
src/cue.rs Normal file
View File

@ -0,0 +1,96 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
extern crate rcue;
use crate::db;
use rcue::parser::parse_from_file;
use std::path::PathBuf;
use std::time::Duration;
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
const GENRE:&str = "GENRE";
#[derive(Clone)]
pub struct CueTrack {
pub audio_path:PathBuf,
pub track_path:PathBuf,
pub title:String,
pub artist:String,
pub album:String,
pub album_artist:String,
pub genre:String,
pub start:Duration,
pub duration:Duration
}
pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
let mut resp:Vec<CueTrack> = Vec::new();
match parse_from_file(&cue_path.to_string_lossy(), false) {
Ok(cue) => {
let album = cue.title.unwrap_or(String::new());
let album_artist = cue.performer.unwrap_or(String::new());
let mut genre = String::new();
for comment in cue.comments {
if comment.0.eq(GENRE) {
genre = comment.1;
}
}
if 1 == cue.files.len() {
for file in cue.files {
for track in file.tracks {
match track.indices.get(0) {
Some((_, start)) => {
let mut track_path = audio_path.clone();
let ext = audio_path.extension().unwrap().to_string_lossy();
track_path.set_extension(format!("{}{}{}", ext, db::CUE_MARKER, resp.len()+1));
let mut ctrack = CueTrack {
audio_path: audio_path.clone(),
track_path: track_path,
title: track.title.unwrap_or(String::new()),
artist: track.performer.unwrap_or(String::new()),
album_artist: album_artist.clone(),
album: album.clone(),
genre: genre.clone(),
start: start.clone(),
duration: Duration::new(LAST_TRACK_DURATION, 0),
};
if ctrack.artist.is_empty() && !ctrack.album_artist.is_empty() {
ctrack.artist = ctrack.album_artist.clone();
}
if ctrack.album.is_empty() {
let mut path = audio_path.clone();
path.set_extension("");
match path.file_name() {
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); }
None => { }
}
}
resp.push(ctrack);
},
None => { }
}
}
}
}
},
Err(e) => { log::error!("Failed to parse '{}'. {}", cue_path.to_string_lossy(), e);}
}
for i in 0..(resp.len()-1) {
let mut next_start = Duration::new(0, 0);
if let Some(next) = resp.get(i+1) {
next_start = next.start;
}
if let Some(elem) = resp.get_mut(i) {
(*elem).duration = next_start - elem.start;
}
}
resp
}

View File

@ -10,43 +10,76 @@ use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::PreAnalyzedSong;
use bliss_audio::{BlissError, BlissResult};
use std::path::Path;
use std::process::{Command, Stdio};
use std::process::{Child, Command, Stdio};
use std::io;
use std::io::Read;
use std::time::Duration;
pub const TIME_SEP:&str = "<TIME>";
pub struct FFmpegCmdDecoder;
fn handle_command(mut child: Child) -> BlissResult<PreAnalyzedSong> {
let mut decoded_song = PreAnalyzedSong::default();
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
let mut reader = io::BufReader::new(stdout);
let mut buffer: Vec<u8> = Vec::new();
reader.read_to_end(&mut buffer).map_err(|e| {
BlissError::DecodingError(format!("Could not read the decoded file into a buffer: {}", e))
})?;
decoded_song.sample_array = buffer
.chunks_exact(4)
.map(|x| {
let mut a: [u8; 4] = [0; 4];
a.copy_from_slice(x);
f32::from_le_bytes(a)
})
.collect();
let duration_seconds = decoded_song.sample_array.len() as f32 / 22050 as f32;
decoded_song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64);
Ok(decoded_song)
}
impl DecoderTrait for FFmpegCmdDecoder {
fn decode(path: &Path) -> BlissResult<PreAnalyzedSong> {
let mut decoded_song = PreAnalyzedSong::default();
if let Ok(mut child) = Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-loglevel").arg("panic")
.arg("-i").arg(path)
.arg("-ar").arg("22050")
.arg("-ac").arg("1")
.arg("-c:a")
.arg("pcm_f32le")
.arg("-f").arg("f32le")
.arg("pipe:1")
.stdout(Stdio::piped())
.spawn() {
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
let mut reader = io::BufReader::new(stdout);
let mut buffer: Vec<u8> = Vec::new();
reader.read_to_end(&mut buffer).map_err(|e| {
BlissError::DecodingError(format!("Could not read the decoded file into a buffer: {}", e))
})?;
decoded_song.sample_array = buffer
.chunks_exact(4)
.map(|x| {
let mut a: [u8; 4] = [0; 4];
a.copy_from_slice(x);
f32::from_le_bytes(a)
})
.collect();
let binding = path.to_string_lossy();
// First check if this is a CUE file track - which will have start and duration
let mut parts = binding.split(TIME_SEP);
if parts.clone().count()==3 {
if let Ok(child) = Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-loglevel").arg("panic")
.arg("-i").arg(parts.next().unwrap_or(""))
.arg("-ss").arg(parts.next().unwrap_or(""))
.arg("-t").arg(parts.next().unwrap_or(""))
.arg("-ar").arg("22050")
.arg("-ac").arg("1")
.arg("-c:a")
.arg("pcm_f32le")
.arg("-f").arg("f32le")
.arg("pipe:1")
.stdout(Stdio::piped())
.spawn() {
return handle_command(child);
}
} else {
if let Ok(child) = Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-loglevel").arg("panic")
.arg("-i").arg(path)
.arg("-ar").arg("22050")
.arg("-ac").arg("1")
.arg("-c:a")
.arg("pcm_f32le")
.arg("-f").arg("f32le")
.arg("pipe:1")
.stdout(Stdio::piped())
.spawn() {
return handle_command(child);
}
}
Ok(decoded_song)
Err(BlissError::DecodingError("ffmpeg command failed".to_string()))
}
}

View File

@ -16,6 +16,7 @@ use std::path::PathBuf;
use std::process;
use which::which;
mod analyse;
mod cue;
mod db;
mod ffmpeg;
mod tags;