mirror of
https://github.com/CDrummond/bliss-analyser.git
synced 2025-04-07 20:50:04 +03:00
Add option to write analysis results to files, and use for future scans.
Issue #4
This commit is contained in:
parent
bff4ba18b4
commit
6cc26c399e
@ -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
|
||||
-----
|
||||
|
@ -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<String>, cue_tracks:&mut Vec<cue::CueTrack>, file_count:&mut usize, max_num_files: usize) {
|
||||
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>, 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<String>, cue_tracks:&mut Vec<cue::CueTrack>, file_count:&mut usize, max_num_files: usize) {
|
||||
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>, 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<max_num_files {
|
||||
get_file_list(db, mpath, &pb, track_paths, cue_tracks, file_count, max_num_files);
|
||||
get_file_list(db, mpath, &pb, track_paths, cue_tracks, file_count, max_num_files, use_tags, tagged_file_count, dry_run);
|
||||
}
|
||||
} else if pb.is_file() && (max_num_files<=0 || *file_count<max_num_files) {
|
||||
if_chain! {
|
||||
@ -106,8 +106,22 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
|
||||
} else {
|
||||
if let Ok(id) = db.get_rowid(&sname) {
|
||||
if id<=0 {
|
||||
track_paths.push(String::from(pb.to_string_lossy()));
|
||||
*file_count+=1;
|
||||
let mut tags_used = false;
|
||||
if use_tags {
|
||||
let meta = tags::read(&sname, true);
|
||||
if !meta.is_empty() && !meta.analysis.is_none() {
|
||||
if !dry_run {
|
||||
db.add_track(&sname, &meta, &meta.analysis.unwrap());
|
||||
}
|
||||
*tagged_file_count+=1;
|
||||
tags_used = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !tags_used {
|
||||
track_paths.push(String::from(pb.to_string_lossy()));
|
||||
*file_count+=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,7 +130,7 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_errors(failed: &mut Vec<String>, tag_error: &mut Vec<String>) {
|
||||
fn show_errors(failed: &mut Vec<String>, tag_error: &mut Vec<String>) {
|
||||
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<String>, tag_error: &mut Vec<String>) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "libav")]
|
||||
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize) -> Result<()> {
|
||||
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, 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<String>,
|
||||
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<String>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libav"))]
|
||||
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize) -> Result<()> {
|
||||
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, 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<String>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libav"))]
|
||||
pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
|
||||
fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
|
||||
let num_cpus = num_cpus::get();
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
@ -331,7 +351,7 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libav"))]
|
||||
pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
|
||||
fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> 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<cue::C
|
||||
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 }
|
||||
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<cue::C
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, 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<PathBuf>, 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<PathBuf>, dry_run: bool, keep_o
|
||||
let mut track_paths: Vec<String> = Vec::new();
|
||||
let mut cue_tracks:Vec<cue::CueTrack> = 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<PathBuf>, 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); }
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ pub struct Metadata {
|
||||
pub album: String,
|
||||
pub genre: String,
|
||||
pub duration: u32,
|
||||
pub analysis: Option<Analysis>,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -41,6 +41,7 @@ fn main() {
|
||||
let mut max_num_files: usize = 0;
|
||||
let mut music_paths: Vec<PathBuf> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
src/tags.rs
76
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::<u16>() {
|
||||
Ok(ver) => {
|
||||
if ver!=ANALYSIS_TAG_VER {
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (index - 2) < NUM_ANALYSIS_VALS {
|
||||
match part.parse::<f32>() {
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user