diff --git a/Cargo.lock b/Cargo.lock index 46cd372..60b9590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml index 67a82a0..dbf573e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/analyse.rs b/src/analyse.rs index eb864d7..d550796 100644 --- a/src/analyse.rs +++ b/src/analyse.rs @@ -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) { +fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec, cue_tracks:&mut Vec) { 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) { +fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec, cue_tracks:&mut Vec) { 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, let mut analysed = 0; let mut failed: Vec = Vec::new(); let mut tag_error: Vec = Vec::new(); - let mut reported_cue:HashSet = HashSet::new(); log::info!("Analysing new files"); for (path, result) in ::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, 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, Ok(()) } +pub fn analyze_cue_streaming(tracks: Vec,) -> BlissResult)>> { + let num_cpus = num_cpus::get(); + + #[allow(clippy::type_complexity)] + let (tx, rx): ( + Sender<(cue::CueTrack, BlissResult)>, + Receiver<(cue::CueTrack, BlissResult)>, + ) = 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 = ::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) -> 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 = 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, 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, dry_run: bool, keep_o let mpath = path.clone(); let cur = path.clone(); let mut track_paths: Vec = Vec::new(); + let mut cue_tracks:Vec = 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, 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, 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; diff --git a/src/cue.rs b/src/cue.rs new file mode 100644 index 0000000..a011825 --- /dev/null +++ b/src/cue.rs @@ -0,0 +1,96 @@ +/** + * Analyse music with Bliss + * + * Copyright (c) 2022 Craig Drummond + * 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 { + let mut resp:Vec = 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 +} \ No newline at end of file diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index a6cd379..4f06404 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -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 = "