diff --git a/Cargo.lock b/Cargo.lock index b01bf2c..d8c0065 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + [[package]] name = "base64" version = "0.13.0" @@ -113,7 +119,7 @@ dependencies = [ [[package]] name = "bliss-analyser" -version = "0.0.2" +version = "0.1.0" dependencies = [ "anyhow", "argparse", @@ -122,12 +128,17 @@ dependencies = [ "configparser", "dirs", "env_logger", + "hhmmss", "indicatif", "lofty", "log", + "num_cpus", + "rcue", "regex", "rusqlite", + "subprocess", "substring", + "tempdir", "ureq", ] @@ -254,7 +265,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time", + "time 0.1.44", "winapi", ] @@ -294,6 +305,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -406,6 +423,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.6.1" @@ -496,6 +519,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "generic-array" version = "0.12.4" @@ -579,6 +608,16 @@ dependencies = [ "libc", ] +[[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" @@ -627,6 +666,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jobserver" version = "0.1.24" @@ -800,7 +845,7 @@ dependencies = [ "noisy_float", "num-integer", "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -996,6 +1041,12 @@ dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.36" @@ -1027,6 +1078,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -1035,7 +1099,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -1045,9 +1109,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.3" @@ -1088,6 +1167,20 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rcue" +version = "0.1.0" +source = "git+https://github.com/gyng/rcue#9ecd1ccbb764acfb7d54c3395525a4c85b43daea" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -1122,6 +1215,15 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1181,6 +1283,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[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 = "5.1.1" @@ -1207,6 +1318,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1223,6 +1340,49 @@ 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.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -1235,6 +1395,21 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "shlex" version = "1.1.0" @@ -1253,6 +1428,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", +] + +[[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", +] + +[[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.3" @@ -1277,6 +1510,16 @@ dependencies = [ "syn", ] +[[package]] +name = "subprocess" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055cf3ebc2981ad8f0a5a17ef6652f652d87831f79fddcba2ac57bcb9a0aa407" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "substring" version = "1.4.5" @@ -1297,6 +1540,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -1347,6 +1600,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", +] + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/Cargo.toml b/Cargo.toml index 8e7997c..8b19890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bliss-analyser" -version = "0.0.2" +version = "0.1.0" authors = ["Craig Drummond "] edition = "2018" license = "GPL-3.0-only" @@ -24,3 +24,8 @@ regex = "1" substring = "1.4.5" ureq = "2.4.0" configparser = "3.0.0" +rcue = { git = "https://github.com/gyng/rcue" } +hhmmss = "0.1.0" +num_cpus = "1.13.0" +tempdir = "0.3.7" +subprocess = "0.2.8" diff --git a/ChangeLog b/ChangeLog index 78a7e8b..5aebf50 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +0.1.0 +----- +1. Add support for analysing CUE files. + 0.0.2 ----- 1. Package vcruntime140.dll with Windows ZIP. diff --git a/src/analyse.rs b/src/analyse.rs index c17a4e6..34dee8c 100644 --- a/src/analyse.rs +++ b/src/analyse.rs @@ -7,19 +7,29 @@ **/ use anyhow::{Result}; -use bliss_audio::{library::analyze_paths_streaming}; +use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song}; +use hhmmss::Hhmmss; use indicatif::{ProgressBar, ProgressStyle}; use std::convert::TryInto; +use std::fs; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use std::time::Duration; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; +use std::thread; +use subprocess::{Popen, PopenConfig}; +use tempdir::TempDir; +use num_cpus; +use crate::cue; use crate::db; use crate::tags; const DONT_ANALYSE:&str = ".notmusic"; const MAX_TAG_ERRORS_TO_SHOW:usize = 25; -fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut Vec) { +fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut Vec, cue_tracks:&mut Vec) { if path.is_dir() { match path.read_dir() { Ok(items) => { @@ -33,7 +43,7 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut if check.exists() { log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE); } else { - get_file_list(db, mpath, &entry.path(), track_paths); + get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks); } } else if entry.path().is_file() { let e = pb.extension(); @@ -42,10 +52,35 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut if ext=="m4a" || ext=="mp3" || ext=="ogg" || ext=="flac" || ext=="opus" { match pb.strip_prefix(mpath) { Ok(stripped) => { - let mut cue = pb.clone(); - cue.set_extension("cue"); - if cue.exists() { - log::warn!("Found CUE album '{}' - not currently handled!", pb.to_string_lossy()); + let mut cue_file = pb.clone(); + cue_file.set_extension("cue"); + if cue_file.exists() { + // Found a CUE file, try to parse and then check if tracks exists in DB + let this_cue_tracks = cue::parse(&pb, &cue_file); + let mut analyse = false; + for track in this_cue_tracks.iter() { + match track.track_path.strip_prefix(mpath) { + Ok(tstripped) => { + let spb = tstripped.to_path_buf(); + let sname = String::from(spb.to_string_lossy()); + match db.get_rowid(&sname) { + Ok(id) => { + if id<=0 { + analyse = true; + } + }, + Err(_) => { } + } + }, + Err(_) => { } + } + } + + if analyse { + for track in this_cue_tracks { + cue_tracks.push(track); + } + } } else { let spb = stripped.to_path_buf(); let sname = String::from(spb.to_string_lossy()); @@ -127,19 +162,172 @@ 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(); + let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0); + + #[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 || { + let mut idx = 0; + match &TempDir::new("bliss") { + Ok(dir) => { + for cue_track in owned_chunk { + let audio_path = String::from(cue_track.audio_path.to_string_lossy()); + let track_path = String::from(cue_track.track_path.to_string_lossy()); + let mut tmp_file = PathBuf::from(dir.path()); + tmp_file.push(format!("{}.mp3", idx)); + idx += 1; + + log::debug!("Extracting '{}'", track_path); + if cue_track.duration { + match proc.wait() { + Ok(_) => { }, + Err(_) => { } + } + }, + Err(e) => { log::error!("Wait failed for ffmpeg. {}", e); } + } + } else { + match Popen::create(&["ffmpeg", "-hide_banner", "-loglevel", "panic", "-i", &audio_path, "-b:a", "128k", + "-ss", &cue_track.start.hhmmss(), &tmp_file.to_string_lossy()], PopenConfig::default()) { + Ok(mut proc) => { + match proc.wait() { + Ok(_) => { }, + Err(e) => { log::error!("Wait failed for ffmpeg. {}", e); } + } + }, + Err(e) => { log::error!("Failed to call ffmpeg. {}", e); } + } + } + + if tmp_file.exists() { + log::debug!("Analyzing '{}'", track_path); + let song = Song::new(&tmp_file); + if cue_track.duration>=last_track_duration { + // Last track, so read duration from temp file + let mut cloned = cue_track.clone(); + let meta = tags::read(&String::from(tmp_file.to_string_lossy())); + cloned.duration = Duration::new(meta.duration as u64, 0); + tx_thread.send((cloned, song)).unwrap(); + } else { + tx_thread.send((cue_track, song)).unwrap(); + } + match fs::remove_file(tmp_file) { + Ok(_) => { }, + Err(_) => { } + } + } else { + log::error!("Failed to create temp file"); + } + } + }, + Err(e) => { log::error!("Failed to create temp folder. {}", e); } + } + }); + 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 = 0; + let mut tag_error:Vec = Vec::new(); + + 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:track.duration.as_secs() as u32 + }; + + db.add_track(&sname, &meta, &song.analysis); + analysed += 1; + }, + Err(_) => { + failed += 1; + } + }; + pb.inc(1); + } + pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed)); + if !tag_error.is_empty() { + let total = tag_error.len(); + tag_error.truncate(MAX_TAG_ERRORS_TO_SHOW); + + log::error!("Failed to read tags of the folling track(s):"); + for err in tag_error { + log::error!(" {}", err); + } + if total>MAX_TAG_ERRORS_TO_SHOW { + log::error!(" + {} other(s)", total - MAX_TAG_ERRORS_TO_SHOW); + } + } + Ok(()) +} + pub fn analyse_files(db_path: &str, mpath: &PathBuf, dry_run:bool, keep_old:bool, max_num_tracks:usize) { let mut track_paths:Vec = Vec::new(); + let mut cue_tracks:Vec = Vec::new(); let mut db = db::Db::new(&String::from(db_path)); let cur = PathBuf::from(mpath); db.init(); log::info!("Looking for new tracks"); - get_file_list(&mut db, mpath, &cur, &mut track_paths); + get_file_list(&mut db, mpath, &cur, &mut track_paths, &mut cue_tracks); log::info!("Num new tracks: {}", track_paths.len()); + if !cue_tracks.is_empty() { + log::info!("Num new cue tracks: {}", cue_tracks.len()); + } if !dry_run && max_num_tracks>0 && track_paths.len()>max_num_tracks { log::info!("Only analysing {} tracks", max_num_tracks); track_paths.truncate(max_num_tracks); } + if !dry_run && max_num_tracks>0 && cue_tracks.len()>max_num_tracks { + log::info!("Only analysing {} cue tracks", max_num_tracks); + cue_tracks.truncate(max_num_tracks); + } if !keep_old { db.remove_old(mpath, dry_run); } @@ -147,11 +335,17 @@ pub fn analyse_files(db_path: &str, mpath: &PathBuf, dry_run:bool, keep_old:bool if track_paths.len()>0 { match analyse_new_files(&db, mpath, track_paths) { Ok(_) => { }, - Err(_) => { } + Err(e) => { log::error!("Analysis returned error: {}", e); } } } else { log::info!("No new tracks 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); } + } + } } db.close(); diff --git a/src/cue.rs b/src/cue.rs new file mode 100644 index 0000000..b1e0528 --- /dev/null +++ b/src/cue.rs @@ -0,0 +1,87 @@ +/** + * Analyse music with Bliss + * + * Copyright (c) 2022 Craig Drummond + * GPLv3 license. + * + **/ + +extern crate rcue; + +use rcue::parser::parse_from_file; +use std::path::PathBuf; +use std::time::Duration; + +pub const MARKER:&str = ".CUE_TRACK."; +pub const LAST_TRACK_DURATION:u64 = 60*60*24*7; +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 analyse:bool +} + +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!("{}{}{}.mp3", ext, MARKER, resp.len())); + let 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), + analyse: false + }; + 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/db.rs b/src/db.rs index f85184c..101d5e7 100644 --- a/src/db.rs +++ b/src/db.rs @@ -12,6 +12,7 @@ use rusqlite::{Connection, params}; use std::convert::TryInto; use std::path::{Path, PathBuf}; use std::process; +use crate::cue; use crate::tags; pub struct FileMetadata { @@ -134,7 +135,7 @@ impl Db { Err(e) => { log::error!("Failed to insert '{}' into database. {}", path, e); } } } else { - match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=?, Tempo=?, Zcr=?, MeanSpectralCentroid=?, StdDevSpectralCentroid=?, MeanSpectralRolloff=?, StdDevSpectralRolloff=?, MeanSpectralFlatness=?, StdDevSpectralFlatness=?, MeanLoudness=?, StdDevLoudness=?, Chroma1=?, Chroma2=?, Chroma3=?, Chroma4=?, Chroma5=?, Chroma6=?, Chroma7=?, Chroma8=?, Chroma9=?, Chroma10=? WHERE rowid=?);", + match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=?, Tempo=?, Zcr=?, MeanSpectralCentroid=?, StdDevSpectralCentroid=?, MeanSpectralRolloff=?, StdDevSpectralRolloff=?, MeanSpectralFlatness=?, StdDevSpectralFlatness=?, MeanLoudness=?, StdDevLoudness=?, Chroma1=?, Chroma2=?, Chroma3=?, Chroma4=?, Chroma5=?, Chroma6=?, Chroma7=?, Chroma8=?, Chroma9=?, Chroma10=? WHERE rowid=?;", params![meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration, analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff], analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness], @@ -159,10 +160,17 @@ impl Db { for tr in track_iter { let mut db_path:String = tr.unwrap().0; let orig_path = db_path.clone(); + match orig_path.find(cue::MARKER) { + Some(s) => { + db_path.truncate(s); + }, + None => { } + } if cfg!(windows) { db_path = db_path.replace("/", "\\"); } let path = mpath.join(PathBuf::from(db_path.clone())); + //log::debug!("Check if '{}' exists.", path.to_string_lossy()); if !path.exists() { to_remove.push(orig_path); @@ -173,7 +181,7 @@ impl Db { if !dry_run && num_to_remove>0 { let count_before = self.get_track_count(); for t in to_remove { - log::debug!("Remove '{}'", t); + //log::debug!("Remove '{}'", t); match self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]) { Ok(_) => { }, Err(e) => { log::error!("Failed to remove '{}' - {}", t, e) } @@ -224,24 +232,26 @@ impl Db { let mut updated = 0; for tr in track_iter { let dbtags = tr.unwrap(); - let dtags = Metadata{ - title:dbtags.title.unwrap_or(String::new()), - artist:dbtags.artist.unwrap_or(String::new()), - album_artist:dbtags.album_artist.unwrap_or(String::new()), - album:dbtags.album.unwrap_or(String::new()), - genre:dbtags.genre.unwrap_or(String::new()), - duration:dbtags.duration - }; - pb.set_message(format!("{}", dbtags.file)); - let path = String::from(mpath.join(&dbtags.file).to_string_lossy()); - let ftags = tags::read(&path); - if ftags.title.is_empty() && ftags.artist.is_empty() && ftags.album_artist.is_empty() && ftags.album.is_empty() && ftags.genre.is_empty() { - log::error!("Failed to read tags of '{}'", dbtags.file); - } else if ftags.duration!=dtags.duration || ftags.title!=dtags.title || ftags.artist!=dtags.artist || ftags.album_artist!=dtags.album_artist || ftags.album!=dtags.album || ftags.genre!=dtags.genre { - match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;", - params![ftags.title, ftags.artist, ftags.album_artist, ftags.album, ftags.genre, ftags.duration, dbtags.rowid]) { - Ok(_) => { updated += 1; }, - Err(e) => { log::error!("Failed to update tags of '{}'. {}", dbtags.file, e); } + if !dbtags.file.contains(cue::MARKER) { + let dtags = Metadata{ + title:dbtags.title.unwrap_or(String::new()), + artist:dbtags.artist.unwrap_or(String::new()), + album_artist:dbtags.album_artist.unwrap_or(String::new()), + album:dbtags.album.unwrap_or(String::new()), + genre:dbtags.genre.unwrap_or(String::new()), + duration:dbtags.duration + }; + pb.set_message(format!("{}", dbtags.file)); + let path = String::from(mpath.join(&dbtags.file).to_string_lossy()); + let ftags = tags::read(&path); + if ftags.title.is_empty() && ftags.artist.is_empty() && ftags.album_artist.is_empty() && ftags.album.is_empty() && ftags.genre.is_empty() { + log::error!("Failed to read tags of '{}'", dbtags.file); + } else if ftags.duration!=dtags.duration || ftags.title!=dtags.title || ftags.artist!=dtags.artist || ftags.album_artist!=dtags.album_artist || ftags.album!=dtags.album || ftags.genre!=dtags.genre { + match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;", + params![ftags.title, ftags.artist, ftags.album_artist, ftags.album, ftags.genre, ftags.duration, dbtags.rowid]) { + Ok(_) => { updated += 1; }, + Err(e) => { log::error!("Failed to update tags of '{}'. {}", dbtags.file, e); } + } } } pb.inc(1); diff --git a/src/main.rs b/src/main.rs index 70df355..497d4b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use std::io::Write; use std::path::PathBuf; use std::process; mod analyse; +mod cue; mod db; mod tags; mod upload;