diff --git a/ChangeLog b/ChangeLog index 4979a20..f45c533 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,7 @@ 4. Add ability to specify LMS JSONRPC port. 5. If new files analysed and 'ignore' file exists then update DB's 'ignore' flags. +6. Add option to write analysis results to files, and use for future scans. 0.2.3 ----- diff --git a/src/analyse.rs b/src/analyse.rs index f94fb5a..882282e 100644 --- a/src/analyse.rs +++ b/src/analyse.rs @@ -42,7 +42,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, cue_tracks:&mut Vec, file_count:&mut usize, max_num_files: usize) { +fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec, cue_tracks:&mut Vec, file_count:&mut usize, max_num_files: usize, use_tags: bool, tagged_file_count:&mut usize, dry_run: bool) { if !path.is_dir() { return; } @@ -51,21 +51,21 @@ fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut V items.sort_by_key(|dir| dir.path()); for item in items { - check_dir_entry(db, mpath, item, track_paths, cue_tracks, file_count, max_num_files); + check_dir_entry(db, mpath, item, track_paths, cue_tracks, file_count, max_num_files, use_tags, tagged_file_count, dry_run); if max_num_files>0 && *file_count>=max_num_files { break; } } } -fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec, cue_tracks:&mut Vec, file_count:&mut usize, max_num_files: usize) { +fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec, cue_tracks:&mut Vec, file_count:&mut usize, max_num_files: usize, use_tags: bool, tagged_file_count:&mut usize, dry_run: bool) { 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 if max_num_files<=0 || *file_count, tag_error: &mut Vec) { +fn show_errors(failed: &mut Vec, tag_error: &mut Vec) { if !failed.is_empty() { let total = failed.len(); failed.truncate(MAX_ERRORS_TO_SHOW); @@ -144,7 +158,7 @@ pub fn show_errors(failed: &mut Vec, tag_error: &mut Vec) { } #[cfg(feature = "libav")] -pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, max_threads: usize) -> Result<()> { +fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, max_threads: usize, use_tags: bool) -> Result<()> { let total = track_paths.len(); let progress = ProgressBar::new(total.try_into().unwrap()).with_style( ProgressStyle::default_bar() @@ -205,7 +219,7 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, 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); + let mut meta = tags::read(&cpath, false); if meta.is_empty() { // Lofty failed? Try from bliss... meta.title = track.title.unwrap_or_default().to_string(); @@ -218,10 +232,13 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, if meta.is_empty() { tag_error.push(sname.clone()); } + if use_tags { + tags::write_analysis(&cpath, &track.analysis); + } db.add_track(&sname, &meta, &track.analysis); - analysed += 1; } } + analysed += 1; } Err(e) => { failed.push(format!("{} - {}", sname, e)); } }; @@ -238,7 +255,7 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, } #[cfg(not(feature = "libav"))] -pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, max_threads: usize) -> Result<()> { +fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, max_threads: usize, use_tags: bool) -> Result<()> { let total = track_paths.len(); let progress = ProgressBar::new(total.try_into().unwrap()).with_style( ProgressStyle::default_bar() @@ -263,10 +280,13 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, match result { Ok(track) => { let cpath = String::from(path.to_string_lossy()); - let meta = tags::read(&cpath); + let meta = tags::read(&cpath, false); if meta.is_empty() { tag_error.push(sname.clone()); } + if use_tags { + tags::write_analysis(&cpath, &track.analysis); + } db.add_track(&sname, &meta, &track.analysis); analysed += 1; } @@ -291,7 +311,7 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec, } #[cfg(not(feature = "libav"))] -pub fn analyze_cue_streaming(tracks: Vec,) -> BlissResult)>> { +fn analyze_cue_streaming(tracks: Vec,) -> BlissResult)>> { let num_cpus = num_cpus::get(); #[allow(clippy::type_complexity)] @@ -331,7 +351,7 @@ pub fn analyze_cue_streaming(tracks: Vec,) -> BlissResult) -> Result<()> { +fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec) -> Result<()> { let total = cue_tracks.len(); let progress = ProgressBar::new(total.try_into().unwrap()).with_style( ProgressStyle::default_bar() @@ -359,7 +379,8 @@ pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec=last_track_duration { song.duration.as_secs() as u32 } else { track.duration.as_secs() as u32 } + duration:if track.duration>=last_track_duration { song.duration.as_secs() as u32 } else { track.duration.as_secs() as u32 }, + analysis: None }; db.add_track(&sname, &meta, &song.analysis); @@ -377,7 +398,7 @@ pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec, dry_run: bool, keep_old: bool, max_num_files: usize, max_threads: usize, ignore_path: &PathBuf) { +pub fn analyse_files(db_path: &str, mpaths: &Vec, dry_run: bool, keep_old: bool, max_num_files: usize, max_threads: usize, ignore_path: &PathBuf, use_tags: bool) { let mut db = db::Db::new(&String::from(db_path)); db.init(); @@ -393,18 +414,22 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec, dry_run: bool, keep_o let mut track_paths: Vec = Vec::new(); let mut cue_tracks:Vec = Vec::new(); let mut file_count:usize = 0; + let mut tagged_file_count:usize = 0; 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, &mut cue_tracks, &mut file_count, max_num_files); + get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks, &mut file_count, max_num_files, use_tags, &mut tagged_file_count, dry_run); 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 use_tags { + log::info!("Num tagged files: {}", tagged_file_count); + } if dry_run { if !track_paths.is_empty() || !cue_tracks.is_empty() { @@ -418,7 +443,7 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec, dry_run: bool, keep_o } } else { if !track_paths.is_empty() { - match analyse_new_files(&db, &mpath, track_paths, max_threads) { + match analyse_new_files(&db, &mpath, track_paths, max_threads, use_tags) { Ok(_) => { changes_made = true; } Err(e) => { log::error!("Analysis returned error: {}", e); } } diff --git a/src/db.rs b/src/db.rs index f893cd1..869bae1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -35,6 +35,7 @@ pub struct Metadata { pub album: String, pub genre: String, pub duration: u32, + pub analysis: Option, } impl Metadata { @@ -275,6 +276,7 @@ impl Db { album: dbtags.album.unwrap_or_default(), genre: dbtags.genre.unwrap_or_default(), duration: dbtags.duration, + analysis: None, }; progress.set_message(format!("{}", dbtags.file)); @@ -282,7 +284,7 @@ impl Db { let track_path = mpath.join(&dbtags.file); if track_path.exists() { let path = String::from(track_path.to_string_lossy()); - let ftags = tags::read(&path); + let ftags = tags::read(&path, false); if ftags.is_empty() { log::error!("Failed to read tags of '{}'", dbtags.file); } else if ftags != dtags { diff --git a/src/main.rs b/src/main.rs index 272e096..93cb7f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,7 @@ fn main() { let mut max_num_files: usize = 0; let mut music_paths: Vec = Vec::new(); let mut max_threads: usize = 0; + let mut use_tags = false; match dirs::home_dir() { Some(path) => { @@ -74,6 +75,7 @@ fn main() { arg_parse.refer(&mut lms_json_port).add_option(&["-J", "--json"], Store, &lms_json_port_help); arg_parse.refer(&mut max_num_files).add_option(&["-n", "--numfiles"], Store, "Maximum number of files to analyse"); arg_parse.refer(&mut max_threads).add_option(&["-t", "--threads"], Store, "Maximum number of threads to use for analysis"); + arg_parse.refer(&mut use_tags).add_option(&["-T", "--tags"], StoreTrue, "Read/write analysis results from/to source fles"); arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, stopmixer."); arg_parse.parse_args_or_exit(); } @@ -200,7 +202,7 @@ fn main() { analyse::update_ignore(&db_path, &ignore_path); } else { let ignore_path = PathBuf::from(&ignore_file); - analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads, &ignore_path); + analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads, &ignore_path, use_tags); } } } diff --git a/src/tags.rs b/src/tags.rs index 300210e..31b6ed4 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -7,14 +7,44 @@ **/ use crate::db; -use lofty::{Accessor, AudioFile, ItemKey, TaggedFileExt}; +use lofty::{Accessor, AudioFile, ItemKey, Tag, TagExt, TaggedFileExt}; use regex::Regex; use std::path::Path; use substring::Substring; +use bliss_audio::{Analysis, AnalysisIndex}; const MAX_GENRE_VAL: usize = 192; +const NUM_ANALYSIS_VALS: usize = 20; +const ANALYSIS_TAG:ItemKey = ItemKey::Script; +const ANALYSIS_TAG_START: &str = "BLISS_ANALYSIS"; +const ANALYSIS_TAG_VER: u16 = 1; -pub fn read(track: &String) -> db::Metadata { +pub fn write_analysis(track: &String, analysis: &Analysis) { + let value = format!("{},{},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24}", ANALYSIS_TAG_START, ANALYSIS_TAG_VER, + 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], + analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5], + analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10]); + + if let Ok(mut file) = lofty::read_from_path(Path::new(track)) { + let tag = match file.primary_tag_mut() { + Some(primary_tag) => primary_tag, + None => { + if let Some(first_tag) = file.first_tag_mut() { + first_tag + } else { + let tag_type = file.primary_tag_type(); + file.insert_tag(Tag::new(tag_type)); + file.primary_tag_mut().unwrap() + } + }, + }; + tag.insert_text(ANALYSIS_TAG, value); + let _ = tag.save_to_path(Path::new(track)); + } +} + +pub fn read(track: &String, read_analysis: bool) -> db::Metadata { let mut meta = db::Metadata { duration: 180, ..db::Metadata::default() @@ -71,6 +101,48 @@ pub fn read(track: &String) -> db::Metadata { } meta.duration = file.properties().duration().as_secs() as u32; + + if read_analysis { + let analysis_string = tag.get_string(&ANALYSIS_TAG).unwrap_or(""); + if analysis_string.len()>(ANALYSIS_TAG_START.len()+(NUM_ANALYSIS_VALS*8)) { + let parts = analysis_string.split(","); + let mut index = 0; + let mut vals = [0.; NUM_ANALYSIS_VALS]; + for part in parts { + if 0==index { + if part!=ANALYSIS_TAG_START { + break; + } + } else if 1==index { + match part.parse::() { + Ok(ver) => { + if ver!=ANALYSIS_TAG_VER { + break; + } + }, + Err(_) => { + break; + } + } + } else if (index - 2) < NUM_ANALYSIS_VALS { + match part.parse::() { + Ok(val) => { + vals[index - 2] = val; + }, + Err(_) => { + break; + } + } + } else { + break; + } + index += 1; + } + if index == (NUM_ANALYSIS_VALS+2) { + meta.analysis = Some(Analysis::new(vals)); + } + } + } } meta