mirror of
https://github.com/CDrummond/bliss-analyser.git
synced 2026-10-22 22:32:08 +03:00
8 files changed
+626
-499
No files matched your search
Generated
+7
@@ -129,6 +129,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hhmmss",
|
"hhmmss",
|
||||||
|
"if_chain",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"lofty",
|
"lofty",
|
||||||
"log",
|
"log",
|
||||||
@@ -635,6 +636,12 @@ dependencies = [
|
|||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "if_chain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|||||||
@@ -29,3 +29,4 @@ hhmmss = "0.1.0"
|
|||||||
num_cpus = "1.13.0"
|
num_cpus = "1.13.0"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
subprocess = "0.2.8"
|
subprocess = "0.2.8"
|
||||||
|
if_chain = "1.0.2"
|
||||||
+214
-186
@@ -1,3 +1,6 @@
|
|||||||
|
use crate::cue;
|
||||||
|
use crate::db;
|
||||||
|
use crate::tags;
|
||||||
/**
|
/**
|
||||||
* Analyse music with Bliss
|
* Analyse music with Bliss
|
||||||
*
|
*
|
||||||
@@ -5,116 +8,116 @@
|
|||||||
* GPLv3 license.
|
* GPLv3 license.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
use anyhow::Result;
|
||||||
use anyhow::{Result};
|
|
||||||
use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song};
|
use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song};
|
||||||
use hhmmss::Hhmmss;
|
use hhmmss::Hhmmss;
|
||||||
|
use if_chain::if_chain;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use num_cpus;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::{DirEntry, File};
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
use subprocess::{Exec, NullFile};
|
use subprocess::{Exec, NullFile};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use num_cpus;
|
|
||||||
use crate::cue;
|
|
||||||
use crate::db;
|
|
||||||
use crate::tags;
|
|
||||||
|
|
||||||
const DONT_ANALYSE:&str = ".notmusic";
|
const DONT_ANALYSE: &str = ".notmusic";
|
||||||
const MAX_ERRORS_TO_SHOW:usize = 100;
|
const MAX_ERRORS_TO_SHOW: usize = 100;
|
||||||
const MAX_TAG_ERRORS_TO_SHOW:usize = 50;
|
const MAX_TAG_ERRORS_TO_SHOW: usize = 50;
|
||||||
|
const VALID_EXTENSIONS: [&str; 5] = ["m4a", "mp3", "ogg", "flac", "opus"];
|
||||||
|
|
||||||
fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
|
fn get_file_list(
|
||||||
if path.is_dir() {
|
db: &mut db::Db,
|
||||||
match path.read_dir() {
|
mpath: &Path,
|
||||||
Ok(items) => {
|
path: &Path,
|
||||||
for item in items {
|
track_paths: &mut Vec<String>,
|
||||||
match item {
|
cue_tracks: &mut Vec<cue::CueTrack>,
|
||||||
Ok(entry) => {
|
) {
|
||||||
let pb = entry.path().to_path_buf();
|
if !path.is_dir() {
|
||||||
if entry.path().is_dir() {
|
return;
|
||||||
let mut check = pb.clone();
|
}
|
||||||
check.push(PathBuf::from(DONT_ANALYSE));
|
|
||||||
if check.exists() {
|
if let Ok(items) = path.read_dir() {
|
||||||
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
|
for item in items {
|
||||||
} else {
|
if let Ok(entry) = item {
|
||||||
get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks);
|
check_dir_entry(db, mpath, entry, track_paths, cue_tracks);
|
||||||
}
|
}
|
||||||
} else if entry.path().is_file() {
|
|
||||||
let e = pb.extension();
|
|
||||||
if e.is_some() {
|
|
||||||
let ext = e.unwrap().to_string_lossy();
|
|
||||||
if ext=="m4a" || ext=="mp3" || ext=="ogg" || ext=="flac" || ext=="opus" {
|
|
||||||
match pb.strip_prefix(mpath) {
|
|
||||||
Ok(stripped) => {
|
|
||||||
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);
|
|
||||||
for track in this_cue_tracks {
|
|
||||||
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 {
|
|
||||||
cue_tracks.push(track.clone());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let spb = stripped.to_path_buf();
|
|
||||||
let sname = String::from(spb.to_string_lossy());
|
|
||||||
match db.get_rowid(&sname) {
|
|
||||||
Ok(id) => {
|
|
||||||
if id<=0 {
|
|
||||||
track_paths.push(String::from(pb.to_string_lossy()));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -> Result<()> {
|
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, cue_tracks);
|
||||||
|
}
|
||||||
|
} else if pb.is_file() {
|
||||||
|
if_chain! {
|
||||||
|
if let Some(ext) = pb.extension();
|
||||||
|
let ext = ext.to_string_lossy();
|
||||||
|
if VALID_EXTENSIONS.contains(&&*ext);
|
||||||
|
if let Ok(stripped) = pb.strip_prefix(mpath);
|
||||||
|
then {
|
||||||
|
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);
|
||||||
|
for track in this_cue_tracks {
|
||||||
|
if let Ok(tstripped) = track.track_path.strip_prefix(mpath) {
|
||||||
|
let sname = String::from(tstripped.to_string_lossy());
|
||||||
|
|
||||||
|
if let Ok(id) = db.get_rowid(&sname) {
|
||||||
|
if id<=0 {
|
||||||
|
cue_tracks.push(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let sname = String::from(stripped.to_string_lossy());
|
||||||
|
if let Ok(id) = db.get_rowid(&sname) {
|
||||||
|
if id<=0 {
|
||||||
|
track_paths.push(String::from(pb.to_string_lossy()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>) -> Result<()> {
|
||||||
let total = track_paths.len();
|
let total = track_paths.len();
|
||||||
let pb = ProgressBar::new(total.try_into().unwrap());
|
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
|
||||||
let style = ProgressStyle::default_bar()
|
ProgressStyle::default_bar()
|
||||||
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
||||||
.progress_chars("=> ");
|
.progress_chars("=> "),
|
||||||
pb.set_style(style);
|
);
|
||||||
|
|
||||||
let results = analyze_paths_streaming(track_paths)?;
|
let results = analyze_paths_streaming(track_paths)?;
|
||||||
let mut analysed = 0;
|
let mut analysed = 0;
|
||||||
let mut failed:Vec<String> = Vec::new();
|
let mut failed: Vec<String> = Vec::new();
|
||||||
let mut tag_error:Vec<String> = Vec::new();
|
let mut tag_error: Vec<String> = Vec::new();
|
||||||
|
|
||||||
log::info!("Analysing new tracks");
|
log::info!("Analysing new tracks");
|
||||||
for (path, result) in results {
|
for (path, result) in results {
|
||||||
@@ -122,34 +125,40 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
|
|||||||
let stripped = pbuff.strip_prefix(mpath).unwrap();
|
let stripped = pbuff.strip_prefix(mpath).unwrap();
|
||||||
let spbuff = stripped.to_path_buf();
|
let spbuff = stripped.to_path_buf();
|
||||||
let sname = String::from(spbuff.to_string_lossy());
|
let sname = String::from(spbuff.to_string_lossy());
|
||||||
pb.set_message(format!("{}", sname));
|
progress.set_message(format!("{}", sname));
|
||||||
match result {
|
match result {
|
||||||
Ok(track) => {
|
Ok(track) => {
|
||||||
let cpath = String::from(path);
|
let cpath = String::from(path);
|
||||||
let meta = tags::read(&cpath);
|
let meta = tags::read(&cpath);
|
||||||
if meta.title.is_empty() && meta.artist.is_empty() && meta.album.is_empty() && meta.genre.is_empty() {
|
if meta.is_empty() {
|
||||||
tag_error.push(sname.clone());
|
tag_error.push(sname.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
db.add_track(&sname, &meta, &track.analysis);
|
db.add_track(&sname, &meta, &track.analysis);
|
||||||
analysed += 1;
|
analysed += 1;
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
failed.push(format!("{} - {}", sname, e));
|
failed.push(format!("{} - {}", sname, e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
pb.inc(1);
|
|
||||||
|
progress.inc(1);
|
||||||
}
|
}
|
||||||
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
|
|
||||||
|
progress.finish_with_message(format!(
|
||||||
|
"{} Analysed. {} Failure(s).",
|
||||||
|
analysed,
|
||||||
|
failed.len()
|
||||||
|
));
|
||||||
if !failed.is_empty() {
|
if !failed.is_empty() {
|
||||||
let total = failed.len();
|
let total = failed.len();
|
||||||
failed.truncate(MAX_ERRORS_TO_SHOW);
|
failed.truncate(MAX_ERRORS_TO_SHOW);
|
||||||
|
|
||||||
log::error!("Failed to analyse the folling track(s):");
|
log::error!("Failed to analyse the following track(s):");
|
||||||
for err in failed {
|
for err in failed {
|
||||||
log::error!(" {}", err);
|
log::error!(" {}", err);
|
||||||
}
|
}
|
||||||
if total>MAX_ERRORS_TO_SHOW {
|
if total > MAX_ERRORS_TO_SHOW {
|
||||||
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
|
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,18 +166,20 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
|
|||||||
let total = tag_error.len();
|
let total = tag_error.len();
|
||||||
tag_error.truncate(MAX_TAG_ERRORS_TO_SHOW);
|
tag_error.truncate(MAX_TAG_ERRORS_TO_SHOW);
|
||||||
|
|
||||||
log::error!("Failed to read tags of the folling track(s):");
|
log::error!("Failed to read tags of the following track(s):");
|
||||||
for err in tag_error {
|
for err in tag_error {
|
||||||
log::error!(" {}", err);
|
log::error!(" {}", err);
|
||||||
}
|
}
|
||||||
if total>MAX_TAG_ERRORS_TO_SHOW {
|
if total > MAX_TAG_ERRORS_TO_SHOW {
|
||||||
log::error!(" + {} other(s)", total - MAX_TAG_ERRORS_TO_SHOW);
|
log::error!(" + {} other(s)", total - MAX_TAG_ERRORS_TO_SHOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
|
pub fn analyze_cue_streaming(
|
||||||
|
tracks: Vec<cue::CueTrack>,
|
||||||
|
) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
|
||||||
let num_cpus = num_cpus::get();
|
let num_cpus = num_cpus::get();
|
||||||
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
|
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
|
||||||
|
|
||||||
@@ -193,63 +204,63 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
|
|||||||
let tx_thread = tx.clone();
|
let tx_thread = tx.clone();
|
||||||
let owned_chunk = chunk.to_owned();
|
let owned_chunk = chunk.to_owned();
|
||||||
let child = thread::spawn(move || {
|
let child = thread::spawn(move || {
|
||||||
|
let dir = TempDir::new("bliss");
|
||||||
|
if let Err(e) = dir {
|
||||||
|
log::error!("Failed to create temp folder. {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
match &TempDir::new("bliss") {
|
let dir = dir.unwrap();
|
||||||
Ok(dir) => {
|
for mut cue_track in owned_chunk {
|
||||||
for cue_track in owned_chunk {
|
let audio_path = String::from(cue_track.audio_path.to_string_lossy());
|
||||||
let audio_path = String::from(cue_track.audio_path.to_string_lossy());
|
let ext = cue_track.audio_path.extension();
|
||||||
let ext = cue_track.audio_path.extension();
|
let track_path = String::from(cue_track.track_path.to_string_lossy());
|
||||||
let track_path = String::from(cue_track.track_path.to_string_lossy());
|
let mut tmp_file = PathBuf::from(dir.path());
|
||||||
let mut tmp_file = PathBuf::from(dir.path());
|
if ext.is_some() {
|
||||||
if ext.is_some() {
|
tmp_file.push(format!("{}.{}", idx, ext.unwrap().to_string_lossy()));
|
||||||
tmp_file.push(format!("{}.{}", idx, ext.unwrap().to_string_lossy()));
|
} else {
|
||||||
} else {
|
tmp_file.push(format!("{}.flac", idx));
|
||||||
tmp_file.push(format!("{}.flac", idx));
|
}
|
||||||
}
|
idx += 1;
|
||||||
idx += 1;
|
|
||||||
|
|
||||||
log::debug!("Extracting '{}'", track_path);
|
log::debug!("Extracting '{}'", track_path);
|
||||||
match Exec::cmd("ffmpeg").arg("-i").arg(&audio_path)
|
let cmd = Exec::cmd("ffmpeg")
|
||||||
.arg("-ss").arg(&cue_track.start.hhmmss())
|
.arg("-i")
|
||||||
.arg("-t").arg(&cue_track.duration.hhmmss())
|
.arg(&audio_path)
|
||||||
.arg("-c").arg("copy")
|
.arg("-ss")
|
||||||
.arg(String::from(tmp_file.to_string_lossy()))
|
.arg(&cue_track.start.hhmmss())
|
||||||
.stderr(NullFile)
|
.arg("-t")
|
||||||
.join() {
|
.arg(&cue_track.duration.hhmmss())
|
||||||
Ok(_) => { },
|
.arg("-c")
|
||||||
Err(e) => { log::error!("Failed to call ffmpeg. {}", e); }
|
.arg("copy")
|
||||||
}
|
.arg(String::from(tmp_file.to_string_lossy()))
|
||||||
|
.stderr(NullFile)
|
||||||
|
.join();
|
||||||
|
|
||||||
if ! cfg!(windows) {
|
if let Err(e) = cmd {
|
||||||
// ffmpeg seeks to break echo on terminal? 'stty echo' restores...
|
log::error!("Failed to call ffmpeg. {}", e);
|
||||||
match Exec::cmd("stty").arg("echo").join() {
|
}
|
||||||
Ok(_) => { },
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tmp_file.exists() {
|
if !cfg!(windows) {
|
||||||
log::debug!("Analyzing '{}'", track_path);
|
// ffmpeg seeks to break echo on terminal? 'stty echo' restores...
|
||||||
let song = Song::new(&tmp_file);
|
let _ = Exec::cmd("stty").arg("echo").join();
|
||||||
if cue_track.duration>=last_track_duration {
|
}
|
||||||
// Last track, so read duration from temp file
|
|
||||||
let mut cloned = cue_track.clone();
|
if tmp_file.exists() {
|
||||||
let meta = tags::read(&String::from(tmp_file.to_string_lossy()));
|
log::debug!("Analyzing '{}'", track_path);
|
||||||
cloned.duration = Duration::new(meta.duration as u64, 0);
|
let song = Song::new(&tmp_file);
|
||||||
tx_thread.send((cloned, song)).unwrap();
|
if cue_track.duration >= last_track_duration {
|
||||||
} else {
|
// Last track, so read duration from temp file
|
||||||
tx_thread.send((cue_track, song)).unwrap();
|
let meta = tags::read(&String::from(tmp_file.to_string_lossy()));
|
||||||
}
|
cue_track.duration = Duration::new(meta.duration as u64, 0);
|
||||||
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); }
|
tx_thread.send((cue_track, song)).unwrap();
|
||||||
|
let _ = fs::remove_file(tmp_file);
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to create temp file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
handles.push(child);
|
handles.push(child);
|
||||||
@@ -258,61 +269,75 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
|
|||||||
Ok(rx)
|
Ok(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
|
pub fn analyse_new_cue_tracks(
|
||||||
|
db: &db::Db,
|
||||||
|
mpath: &PathBuf,
|
||||||
|
cue_tracks: Vec<cue::CueTrack>,
|
||||||
|
) -> Result<()> {
|
||||||
let total = cue_tracks.len();
|
let total = cue_tracks.len();
|
||||||
let pb = ProgressBar::new(total.try_into().unwrap());
|
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
|
||||||
let style = ProgressStyle::default_bar()
|
ProgressStyle::default_bar()
|
||||||
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
||||||
.progress_chars("=> ");
|
.progress_chars("=> "),
|
||||||
pb.set_style(style);
|
);
|
||||||
|
|
||||||
let results = analyze_cue_streaming(cue_tracks)?;
|
let results = analyze_cue_streaming(cue_tracks)?;
|
||||||
let mut analysed = 0;
|
let mut analysed = 0;
|
||||||
let mut failed:Vec<String> = Vec::new();
|
let mut failed: Vec<String> = Vec::new();
|
||||||
|
|
||||||
log::info!("Analysing new cue tracks");
|
log::info!("Analysing new cue tracks");
|
||||||
for (track, result) in results {
|
for (track, result) in results {
|
||||||
let stripped = track.track_path.strip_prefix(mpath).unwrap();
|
let stripped = track.track_path.strip_prefix(mpath).unwrap();
|
||||||
let spbuff = stripped.to_path_buf();
|
let spbuff = stripped.to_path_buf();
|
||||||
let sname = String::from(spbuff.to_string_lossy());
|
let sname = String::from(spbuff.to_string_lossy());
|
||||||
pb.set_message(format!("{}", sname));
|
progress.set_message(format!("{}", sname));
|
||||||
match result {
|
match result {
|
||||||
Ok(song) => {
|
Ok(song) => {
|
||||||
let meta = db::Metadata {
|
let meta = db::Metadata {
|
||||||
title:track.title,
|
title: track.title,
|
||||||
artist:track.artist,
|
artist: track.artist,
|
||||||
album_artist:track.album_artist,
|
album_artist: track.album_artist,
|
||||||
album:track.album,
|
album: track.album,
|
||||||
genre:track.genre,
|
genre: track.genre,
|
||||||
duration:track.duration.as_secs() as u32
|
duration: track.duration.as_secs() as u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
db.add_track(&sname, &meta, &song.analysis);
|
db.add_track(&sname, &meta, &song.analysis);
|
||||||
analysed += 1;
|
analysed += 1;
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
failed.push(format!("{} - {}", sname, e));
|
failed.push(format!("{} - {}", sname, e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
pb.inc(1);
|
progress.inc(1);
|
||||||
}
|
}
|
||||||
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
|
progress.finish_with_message(format!(
|
||||||
|
"{} Analysed. {} Failure(s).",
|
||||||
|
analysed,
|
||||||
|
failed.len()
|
||||||
|
));
|
||||||
if !failed.is_empty() {
|
if !failed.is_empty() {
|
||||||
let total = failed.len();
|
let total = failed.len();
|
||||||
failed.truncate(MAX_ERRORS_TO_SHOW);
|
failed.truncate(MAX_ERRORS_TO_SHOW);
|
||||||
|
|
||||||
log::error!("Failed to analyse the folling track(s):");
|
log::error!("Failed to analyse the following track(s):");
|
||||||
for err in failed {
|
for err in failed {
|
||||||
log::error!(" {}", err);
|
log::error!(" {}", err);
|
||||||
}
|
}
|
||||||
if total>MAX_ERRORS_TO_SHOW {
|
if total > MAX_ERRORS_TO_SHOW {
|
||||||
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
|
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_old:bool, max_num_tracks:usize) {
|
pub fn analyse_files(
|
||||||
|
db_path: &str,
|
||||||
|
mpaths: &Vec<PathBuf>,
|
||||||
|
dry_run: bool,
|
||||||
|
keep_old: bool,
|
||||||
|
max_num_tracks: usize,
|
||||||
|
) {
|
||||||
let mut db = db::Db::new(&String::from(db_path));
|
let mut db = db::Db::new(&String::from(db_path));
|
||||||
let mut track_count_left = max_num_tracks;
|
let mut track_count_left = max_num_tracks;
|
||||||
|
|
||||||
@@ -325,10 +350,10 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
|
|||||||
for path in mpaths {
|
for path in mpaths {
|
||||||
let mpath = path.clone();
|
let mpath = path.clone();
|
||||||
let cur = path.clone();
|
let cur = path.clone();
|
||||||
let mut track_paths:Vec<String> = Vec::new();
|
let mut track_paths: Vec<String> = Vec::new();
|
||||||
let mut cue_tracks:Vec<cue::CueTrack> = Vec::new();
|
let mut cue_tracks: Vec<cue::CueTrack> = Vec::new();
|
||||||
|
|
||||||
if mpaths.len()>1 {
|
if mpaths.len() > 1 {
|
||||||
log::info!("Looking for new tracks in {}", mpath.to_string_lossy());
|
log::info!("Looking for new tracks in {}", mpath.to_string_lossy());
|
||||||
} else {
|
} else {
|
||||||
log::info!("Looking for new tracks");
|
log::info!("Looking for new tracks");
|
||||||
@@ -339,6 +364,7 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
|
|||||||
if !cue_tracks.is_empty() {
|
if !cue_tracks.is_empty() {
|
||||||
log::info!("Num new cue tracks: {}", cue_tracks.len());
|
log::info!("Num new cue tracks: {}", cue_tracks.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
if dry_run {
|
if dry_run {
|
||||||
if !track_paths.is_empty() || !cue_tracks.is_empty() {
|
if !track_paths.is_empty() || !cue_tracks.is_empty() {
|
||||||
log::info!("The following need to be analysed:");
|
log::info!("The following need to be analysed:");
|
||||||
@@ -350,18 +376,18 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if max_num_tracks>0 {
|
if max_num_tracks > 0 {
|
||||||
if track_paths.len()>track_count_left {
|
if track_paths.len() > track_count_left {
|
||||||
log::info!("Only analysing {} tracks", track_count_left);
|
log::info!("Only analysing {} tracks", track_count_left);
|
||||||
track_paths.truncate(track_count_left);
|
track_paths.truncate(track_count_left);
|
||||||
}
|
}
|
||||||
track_count_left -= track_paths.len();
|
track_count_left -= track_paths.len();
|
||||||
}
|
}
|
||||||
if max_num_tracks>0 {
|
if max_num_tracks > 0 {
|
||||||
if track_count_left == 0 {
|
if track_count_left == 0 {
|
||||||
cue_tracks.clear();
|
cue_tracks.clear();
|
||||||
} else {
|
} else {
|
||||||
if cue_tracks.len()>track_count_left {
|
if cue_tracks.len() > track_count_left {
|
||||||
log::info!("Only analysing {} cue tracks", track_count_left);
|
log::info!("Only analysing {} cue tracks", track_count_left);
|
||||||
cue_tracks.truncate(track_count_left);
|
cue_tracks.truncate(track_count_left);
|
||||||
}
|
}
|
||||||
@@ -371,21 +397,22 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
|
|||||||
|
|
||||||
if !track_paths.is_empty() {
|
if !track_paths.is_empty() {
|
||||||
match analyse_new_files(&db, &mpath, track_paths) {
|
match analyse_new_files(&db, &mpath, track_paths) {
|
||||||
Ok(_) => { },
|
Ok(_) => {}
|
||||||
Err(e) => { log::error!("Analysis returned error: {}", e); }
|
Err(e) => {
|
||||||
|
log::error!("Analysis returned error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log::info!("No new tracks to analyse");
|
log::info!("No new tracks to analyse");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cue_tracks.is_empty() {
|
if !cue_tracks.is_empty() {
|
||||||
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
|
if let Err(e) = analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
|
||||||
Ok(_) => { },
|
log::error!("Cue analysis returned error: {}", e);
|
||||||
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if max_num_tracks>0 && track_count_left<=0 {
|
if max_num_tracks > 0 && track_count_left <= 0 {
|
||||||
log::info!("Track limit reached");
|
log::info!("Track limit reached");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -409,11 +436,12 @@ pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
|
|||||||
db.init();
|
db.init();
|
||||||
|
|
||||||
db.clear_ignore();
|
db.clear_ignore();
|
||||||
for (_index, line) in reader.lines().enumerate() {
|
let mut lines = reader.lines();
|
||||||
let line = line.unwrap();
|
while let Some(Ok(line)) = lines.next() {
|
||||||
if !line.is_empty() && !line.starts_with("#") {
|
if !line.is_empty() && !line.starts_with("#") {
|
||||||
db.set_ignore(&line);
|
db.set_ignore(&line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
+61
-54
@@ -5,35 +5,34 @@
|
|||||||
* GPLv3 license.
|
* GPLv3 license.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|
||||||
extern crate rcue;
|
extern crate rcue;
|
||||||
|
|
||||||
use rcue::parser::parse_from_file;
|
use rcue::parser::parse_from_file;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub const MARKER:&str = ".CUE_TRACK.";
|
pub const MARKER: &str = ".CUE_TRACK.";
|
||||||
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
|
pub const LAST_TRACK_DURATION: u64 = 60 * 60 * 24;
|
||||||
const GENRE:&str = "GENRE";
|
const GENRE: &str = "GENRE";
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CueTrack {
|
pub struct CueTrack {
|
||||||
pub audio_path:PathBuf,
|
pub audio_path: PathBuf,
|
||||||
pub track_path:PathBuf,
|
pub track_path: PathBuf,
|
||||||
pub title:String,
|
pub title: String,
|
||||||
pub artist:String,
|
pub artist: String,
|
||||||
pub album:String,
|
pub album: String,
|
||||||
pub album_artist:String,
|
pub album_artist: String,
|
||||||
pub genre:String,
|
pub genre: String,
|
||||||
pub start:Duration,
|
pub start: Duration,
|
||||||
pub duration:Duration
|
pub duration: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
|
pub fn parse(audio_path: &PathBuf, cue_path: &PathBuf) -> Vec<CueTrack> {
|
||||||
let mut resp:Vec<CueTrack> = Vec::new();
|
let mut resp: Vec<CueTrack> = Vec::new();
|
||||||
|
|
||||||
match parse_from_file(&cue_path.to_string_lossy(), false) {
|
match parse_from_file(&cue_path.to_string_lossy(), false) {
|
||||||
Ok(cue) => {
|
Ok(mut cue) => {
|
||||||
let album = cue.title.unwrap_or(String::new());
|
let album = cue.title.unwrap_or(String::new());
|
||||||
let album_artist = cue.performer.unwrap_or(String::new());
|
let album_artist = cue.performer.unwrap_or(String::new());
|
||||||
let mut genre = String::new();
|
let mut genre = String::new();
|
||||||
@@ -42,55 +41,63 @@ pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
|
|||||||
genre = comment.1;
|
genre = comment.1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if 1 == cue.files.len() {
|
|
||||||
for file in cue.files {
|
if let Some(file) = cue.files.pop() {
|
||||||
for track in file.tracks {
|
for track in file.tracks {
|
||||||
match track.indices.get(0) {
|
match track.indices.get(0) {
|
||||||
Some((_, start)) => {
|
Some((_, start)) => {
|
||||||
let mut track_path = audio_path.clone();
|
let mut track_path = audio_path.clone();
|
||||||
let ext = audio_path.extension().unwrap().to_string_lossy();
|
let ext = audio_path.extension().unwrap().to_string_lossy();
|
||||||
track_path.set_extension(format!("{}{}{}", ext, MARKER, resp.len()+1));
|
track_path.set_extension(format!(
|
||||||
let mut ctrack = CueTrack {
|
"{}{}{}",
|
||||||
audio_path: audio_path.clone(),
|
ext,
|
||||||
track_path: track_path,
|
MARKER,
|
||||||
title: track.title.unwrap_or(String::new()),
|
resp.len() + 1
|
||||||
artist: track.performer.unwrap_or(String::new()),
|
));
|
||||||
album_artist: album_artist.clone(),
|
|
||||||
album: album.clone(),
|
let mut ctrack = CueTrack {
|
||||||
genre: genre.clone(),
|
audio_path: audio_path.clone(),
|
||||||
start: start.clone(),
|
track_path,
|
||||||
duration: Duration::new(LAST_TRACK_DURATION, 0),
|
title: track.title.unwrap_or_default(),
|
||||||
};
|
artist: track.performer.unwrap_or_default(),
|
||||||
if ctrack.artist.is_empty() && !ctrack.album_artist.is_empty() {
|
album_artist: album_artist.clone(),
|
||||||
ctrack.artist = ctrack.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() {
|
||||||
|
if let Some(name) = audio_path.file_name() {
|
||||||
|
ctrack.album = String::from(name.to_string_lossy());
|
||||||
}
|
}
|
||||||
if ctrack.album.is_empty() {
|
}
|
||||||
let mut path = audio_path.clone();
|
|
||||||
path.set_extension("");
|
resp.push(ctrack);
|
||||||
match path.file_name() {
|
|
||||||
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); }
|
|
||||||
None => { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp.push(ctrack);
|
|
||||||
},
|
|
||||||
None => { }
|
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => { log::error!("Failed to parse '{}'. {}", cue_path.to_string_lossy(), e);}
|
Err(e) => {
|
||||||
|
log::error!("Failed to parse '{}'. {}", cue_path.to_string_lossy(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..(resp.len()-1) {
|
for i in 0..(resp.len() - 1) {
|
||||||
let mut next_start = Duration::new(0, 0);
|
let mut next_start = Duration::ZERO;
|
||||||
if let Some(next) = resp.get(i+1) {
|
if let Some(next) = resp.get(i + 1) {
|
||||||
next_start = next.start;
|
next_start = next.start;
|
||||||
}
|
}
|
||||||
if let Some(elem) = resp.get_mut(i) {
|
if let Some(elem) = resp.get_mut(i) {
|
||||||
(*elem).duration = next_start - elem.start;
|
(*elem).duration = next_start - elem.start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp
|
resp
|
||||||
}
|
}
|
||||||
@@ -5,38 +5,48 @@
|
|||||||
* GPLv3 license.
|
* GPLv3 license.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
use crate::cue;
|
||||||
|
use crate::tags;
|
||||||
use bliss_audio::{Analysis, AnalysisIndex};
|
use bliss_audio::{Analysis, AnalysisIndex};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{params, Connection};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process;
|
use std::process;
|
||||||
use crate::cue;
|
|
||||||
use crate::tags;
|
|
||||||
|
|
||||||
pub struct FileMetadata {
|
pub struct FileMetadata {
|
||||||
pub rowid:usize,
|
pub rowid: usize,
|
||||||
pub file:String,
|
pub file: String,
|
||||||
pub title:Option<String>,
|
pub title: Option<String>,
|
||||||
pub artist:Option<String>,
|
pub artist: Option<String>,
|
||||||
pub album_artist:Option<String>,
|
pub album_artist: Option<String>,
|
||||||
pub album:Option<String>,
|
pub album: Option<String>,
|
||||||
pub genre:Option<String>,
|
pub genre: Option<String>,
|
||||||
pub duration:u32
|
pub duration: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub title:String,
|
pub title: String,
|
||||||
pub artist:String,
|
pub artist: String,
|
||||||
pub album_artist:String,
|
pub album_artist: String,
|
||||||
pub album:String,
|
pub album: String,
|
||||||
pub genre:String,
|
pub genre: String,
|
||||||
pub duration:u32
|
pub duration: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.title.is_empty()
|
||||||
|
&& self.artist.is_empty()
|
||||||
|
&& self.album_artist.is_empty()
|
||||||
|
&& self.album.is_empty()
|
||||||
|
&& self.genre.is_empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
pub conn: Connection
|
pub conn: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
@@ -47,7 +57,7 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(&self) {
|
pub fn init(&self) {
|
||||||
match self.conn.execute(
|
let cmd = self.conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS Tracks (
|
"CREATE TABLE IF NOT EXISTS Tracks (
|
||||||
File text primary key,
|
File text primary key,
|
||||||
Title text,
|
Title text,
|
||||||
@@ -77,39 +87,42 @@ impl Db {
|
|||||||
Chroma8 real,
|
Chroma8 real,
|
||||||
Chroma9 real,
|
Chroma9 real,
|
||||||
Chroma10 real
|
Chroma10 real
|
||||||
);",[]) {
|
);",
|
||||||
Ok(_) => { },
|
[],
|
||||||
Err(_) => {
|
);
|
||||||
log::error!("Failed to create DB table");
|
|
||||||
process::exit(-1);
|
if cmd.is_err() {
|
||||||
}
|
log::error!("Failed to create DB table");
|
||||||
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
match self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)", []) {
|
|
||||||
Ok(_) => { },
|
let cmd = self.conn.execute(
|
||||||
Err(_) => {
|
"CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)",
|
||||||
log::error!("Failed to create DB index");
|
[],
|
||||||
process::exit(-1);
|
);
|
||||||
}
|
|
||||||
|
if cmd.is_err() {
|
||||||
|
log::error!("Failed to create DB index");
|
||||||
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(self) {
|
pub fn close(self) {
|
||||||
match self.conn.close() {
|
let _ = self.conn.close();
|
||||||
Ok(_) => { },
|
|
||||||
Err(_) => { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rowid(&self, path: &String) -> Result<usize, rusqlite::Error> {
|
pub fn get_rowid(&self, path: &str) -> Result<usize, rusqlite::Error> {
|
||||||
let mut db_path = path.clone();
|
let mut db_path = path.to_string();
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
db_path = db_path.replace("\\", "/");
|
db_path = db_path.replace("\\", "/");
|
||||||
}
|
}
|
||||||
let mut stmt = self.conn.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
|
let mut stmt = self
|
||||||
let track_iter = stmt.query_map(&[(":path", &db_path)], |row| {
|
.conn
|
||||||
Ok(row.get(0)?)
|
.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
|
||||||
}).unwrap();
|
let track_iter = stmt
|
||||||
let mut rowid:usize = 0;
|
.query_map(&[(":path", &db_path)], |row| Ok(row.get(0)?))
|
||||||
|
.unwrap();
|
||||||
|
let mut rowid: usize = 0;
|
||||||
for tr in track_iter {
|
for tr in track_iter {
|
||||||
rowid = tr.unwrap();
|
rowid = tr.unwrap();
|
||||||
break;
|
break;
|
||||||
@@ -117,14 +130,14 @@ impl Db {
|
|||||||
Ok(rowid)
|
Ok(rowid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_track(&self, path: &String, meta: &Metadata, analysis:&Analysis) {
|
pub fn add_track(&self, path: &String, meta: &Metadata, analysis: &Analysis) {
|
||||||
let mut db_path = path.clone();
|
let mut db_path = path.clone();
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
db_path = db_path.replace("\\", "/");
|
db_path = db_path.replace("\\", "/");
|
||||||
}
|
}
|
||||||
match self.get_rowid(&path) {
|
match self.get_rowid(&path) {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
if id<=0 {
|
if id <= 0 {
|
||||||
match self.conn.execute("INSERT INTO Tracks (File, Title, Artist, AlbumArtist, Album, Genre, Duration, Ignore, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
match self.conn.execute("INSERT INTO Tracks (File, Title, Artist, AlbumArtist, Album, Genre, Duration, Ignore, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||||
params![db_path, meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration, 0,
|
params![db_path, meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration, 0,
|
||||||
analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff],
|
analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff],
|
||||||
@@ -145,26 +158,24 @@ impl Db {
|
|||||||
Err(e) => { log::error!("Failed to update '{}' in database. {}", path, e); }
|
Err(e) => { log::error!("Failed to update '{}' in database. {}", path, e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(_) => { }
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run:bool) {
|
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run: bool) {
|
||||||
log::info!("Looking for non-existant tracks");
|
log::info!("Looking for non-existent tracks");
|
||||||
let mut stmt = self.conn.prepare("SELECT File FROM Tracks;").unwrap();
|
let mut stmt = self.conn.prepare("SELECT File FROM Tracks;").unwrap();
|
||||||
let track_iter = stmt.query_map([], |row| {
|
let track_iter = stmt.query_map([], |row| Ok((row.get(0)?,))).unwrap();
|
||||||
Ok((row.get(0)?,))
|
let mut to_remove: Vec<String> = Vec::new();
|
||||||
}).unwrap();
|
|
||||||
let mut to_remove:Vec<String> = Vec::new();
|
|
||||||
for tr in track_iter {
|
for tr in track_iter {
|
||||||
let mut db_path:String = tr.unwrap().0;
|
let mut db_path: String = tr.unwrap().0;
|
||||||
let orig_path = db_path.clone();
|
let orig_path = db_path.clone();
|
||||||
match orig_path.find(cue::MARKER) {
|
match orig_path.find(cue::MARKER) {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
db_path.truncate(s);
|
db_path.truncate(s);
|
||||||
},
|
}
|
||||||
None => { }
|
None => {}
|
||||||
}
|
}
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
db_path = db_path.replace("/", "\\");
|
db_path = db_path.replace("/", "\\");
|
||||||
@@ -187,8 +198,8 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let num_to_remove = to_remove.len();
|
let num_to_remove = to_remove.len();
|
||||||
log::info!("Num non-existant tracks: {}", num_to_remove);
|
log::info!("Num non-existent tracks: {}", num_to_remove);
|
||||||
if num_to_remove>0 {
|
if num_to_remove > 0 {
|
||||||
if dry_run {
|
if dry_run {
|
||||||
log::info!("The following need to be removed from database:");
|
log::info!("The following need to be removed from database:");
|
||||||
for t in to_remove {
|
for t in to_remove {
|
||||||
@@ -198,9 +209,12 @@ impl Db {
|
|||||||
let count_before = self.get_track_count();
|
let count_before = self.get_track_count();
|
||||||
for t in to_remove {
|
for t in to_remove {
|
||||||
//log::debug!("Remove '{}'", t);
|
//log::debug!("Remove '{}'", t);
|
||||||
match self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]) {
|
let cmd = self
|
||||||
Ok(_) => { },
|
.conn
|
||||||
Err(e) => { log::error!("Failed to remove '{}' - {}", t, e) }
|
.execute("DELETE FROM Tracks WHERE File = ?;", params![t]);
|
||||||
|
|
||||||
|
if let Err(e) = cmd {
|
||||||
|
log::error!("Failed to remove '{}' - {}", t, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let count_now = self.get_track_count();
|
let count_now = self.get_track_count();
|
||||||
@@ -213,10 +227,8 @@ impl Db {
|
|||||||
|
|
||||||
pub fn get_track_count(&self) -> usize {
|
pub fn get_track_count(&self) -> usize {
|
||||||
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM Tracks;").unwrap();
|
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM Tracks;").unwrap();
|
||||||
let track_iter = stmt.query_map([], |row| {
|
let track_iter = stmt.query_map([], |row| Ok(row.get(0)?)).unwrap();
|
||||||
Ok(row.get(0)?)
|
let mut count: usize = 0;
|
||||||
}).unwrap();
|
|
||||||
let mut count:usize = 0;
|
|
||||||
for tr in track_iter {
|
for tr in track_iter {
|
||||||
count = tr.unwrap();
|
count = tr.unwrap();
|
||||||
break;
|
break;
|
||||||
@@ -226,48 +238,53 @@ impl Db {
|
|||||||
|
|
||||||
pub fn update_tags(&self, mpaths: &Vec<PathBuf>) {
|
pub fn update_tags(&self, mpaths: &Vec<PathBuf>) {
|
||||||
let total = self.get_track_count();
|
let total = self.get_track_count();
|
||||||
if total>0 {
|
if total > 0 {
|
||||||
let pb = ProgressBar::new(total.try_into().unwrap());
|
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
|
||||||
let style = ProgressStyle::default_bar()
|
ProgressStyle::default_bar()
|
||||||
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
|
.template(
|
||||||
.progress_chars("=> ");
|
"[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}",
|
||||||
pb.set_style(style);
|
)
|
||||||
|
.progress_chars("=> "),
|
||||||
|
);
|
||||||
|
|
||||||
let mut stmt = self.conn.prepare("SELECT rowid, File, Title, Artist, AlbumArtist, Album, Genre, Duration FROM Tracks ORDER BY File ASC;").unwrap();
|
let mut stmt = self.conn.prepare("SELECT rowid, File, Title, Artist, AlbumArtist, Album, Genre, Duration FROM Tracks ORDER BY File ASC;").unwrap();
|
||||||
let track_iter = stmt.query_map([], |row| {
|
let track_iter = stmt
|
||||||
Ok(FileMetadata {
|
.query_map([], |row| {
|
||||||
rowid: row.get(0)?,
|
Ok(FileMetadata {
|
||||||
file: row.get(1)?,
|
rowid: row.get(0)?,
|
||||||
title: row.get(2)?,
|
file: row.get(1)?,
|
||||||
artist: row.get(3)?,
|
title: row.get(2)?,
|
||||||
album_artist: row.get(4)?,
|
artist: row.get(3)?,
|
||||||
album: row.get(5)?,
|
album_artist: row.get(4)?,
|
||||||
genre: row.get(6)?,
|
album: row.get(5)?,
|
||||||
duration: row.get(7)?,
|
genre: row.get(6)?,
|
||||||
|
duration: row.get(7)?,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}).unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut updated = 0;
|
let mut updated = 0;
|
||||||
for tr in track_iter {
|
for tr in track_iter {
|
||||||
let dbtags = tr.unwrap();
|
let dbtags = tr.unwrap();
|
||||||
if !dbtags.file.contains(cue::MARKER) {
|
if !dbtags.file.contains(cue::MARKER) {
|
||||||
let dtags = Metadata{
|
let dtags = Metadata {
|
||||||
title:dbtags.title.unwrap_or(String::new()),
|
title: dbtags.title.unwrap_or_default(),
|
||||||
artist:dbtags.artist.unwrap_or(String::new()),
|
artist: dbtags.artist.unwrap_or_default(),
|
||||||
album_artist:dbtags.album_artist.unwrap_or(String::new()),
|
album_artist: dbtags.album_artist.unwrap_or_default(),
|
||||||
album:dbtags.album.unwrap_or(String::new()),
|
album: dbtags.album.unwrap_or_default(),
|
||||||
genre:dbtags.genre.unwrap_or(String::new()),
|
genre: dbtags.genre.unwrap_or_default(),
|
||||||
duration:dbtags.duration
|
duration: dbtags.duration,
|
||||||
};
|
};
|
||||||
pb.set_message(format!("{}", dbtags.file));
|
progress.set_message(format!("{}", dbtags.file));
|
||||||
|
|
||||||
for mpath in mpaths {
|
for mpath in mpaths {
|
||||||
let track_path = mpath.join(&dbtags.file);
|
let track_path = mpath.join(&dbtags.file);
|
||||||
if track_path.exists() {
|
if track_path.exists() {
|
||||||
let path = String::from(track_path.to_string_lossy());
|
let path = String::from(track_path.to_string_lossy());
|
||||||
let ftags = tags::read(&path);
|
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() {
|
if ftags.is_empty() {
|
||||||
log::error!("Failed to read tags of '{}'", dbtags.file);
|
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 {
|
} else if ftags != dtags {
|
||||||
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;",
|
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]) {
|
params![ftags.title, ftags.artist, ftags.album_artist, ftags.album, ftags.genre, ftags.duration, dbtags.rowid]) {
|
||||||
Ok(_) => { updated += 1; },
|
Ok(_) => { updated += 1; },
|
||||||
@@ -278,32 +295,40 @@ impl Db {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pb.inc(1);
|
progress.inc(1);
|
||||||
}
|
}
|
||||||
pb.finish_with_message(format!("{} Updated.", updated))
|
progress.finish_with_message(format!("{} Updated.", updated))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_ignore(&self) {
|
pub fn clear_ignore(&self) {
|
||||||
match self.conn.execute("UPDATE Tracks SET Ignore=0;", []) {
|
let cmd = self.conn.execute("UPDATE Tracks SET Ignore=0;", []);
|
||||||
Ok(_) => { },
|
|
||||||
Err(e) => { log::error!("Failed clear Ignore column. {}", e); }
|
if let Err(e) = cmd {
|
||||||
|
log::error!("Failed clear Ignore column. {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ignore(&self, line:&str) {
|
pub fn set_ignore(&self, line: &str) {
|
||||||
log::info!("Ignore: {}", line);
|
log::info!("Ignore: {}", line);
|
||||||
if line.starts_with("SQL:") {
|
if line.starts_with("SQL:") {
|
||||||
let sql = &line[4..];
|
let sql = &line[4..];
|
||||||
match self.conn.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []) {
|
let cmd = self
|
||||||
Ok(_) => { },
|
.conn
|
||||||
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
|
.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []);
|
||||||
|
|
||||||
|
if let Err(e) = cmd {
|
||||||
|
log::error!("Failed set Ignore column for '{}'. {}", line, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line), []) {
|
let cmd = self.conn.execute(
|
||||||
Ok(_) => { },
|
&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line),
|
||||||
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = cmd {
|
||||||
|
log::error!("Failed set Ignore column for '{}'. {}", line, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+102
-36
@@ -19,9 +19,8 @@ mod db;
|
|||||||
mod tags;
|
mod tags;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
|
|
||||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
const TOP_LEVEL_INI_TAG:&str = "Bliss";
|
const TOP_LEVEL_INI_TAG: &str = "Bliss";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut config_file = "config.ini".to_string();
|
let mut config_file = "config.ini".to_string();
|
||||||
@@ -29,24 +28,32 @@ fn main() {
|
|||||||
let mut logging = "info".to_string();
|
let mut logging = "info".to_string();
|
||||||
let mut music_path = ".".to_string();
|
let mut music_path = ".".to_string();
|
||||||
let mut ignore_file = "ignore.txt".to_string();
|
let mut ignore_file = "ignore.txt".to_string();
|
||||||
let mut keep_old:bool = false;
|
let mut keep_old: bool = false;
|
||||||
let mut dry_run:bool = false;
|
let mut dry_run: bool = false;
|
||||||
let mut task = "".to_string();
|
let mut task = "".to_string();
|
||||||
let mut lms_host = "127.0.0.1".to_string();
|
let mut lms_host = "127.0.0.1".to_string();
|
||||||
let mut max_num_tracks:usize = 0;
|
let mut max_num_tracks: usize = 0;
|
||||||
let mut music_paths:Vec<PathBuf> = Vec::new();
|
let mut music_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
match dirs::home_dir() {
|
match dirs::home_dir() {
|
||||||
Some(path) => { music_path = String::from(path.join("Music").to_string_lossy()); }
|
Some(path) => {
|
||||||
None => { }
|
music_path = String::from(path.join("Music").to_string_lossy());
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let config_file_help = format!("config file (default: {})", &config_file);
|
let config_file_help = format!("config file (default: {})", &config_file);
|
||||||
let music_path_help = format!("Music folder (default: {})", &music_path);
|
let music_path_help = format!("Music folder (default: {})", &music_path);
|
||||||
let db_path_help = format!("Database location (default: {})", &db_path);
|
let db_path_help = format!("Database location (default: {})", &db_path);
|
||||||
let logging_help = format!("Log level; trace, debug, info, warn, error. (default: {})", logging);
|
let logging_help = format!(
|
||||||
let ignore_file_help = format!("File containg items to mark as ignored. (default: {})", ignore_file);
|
"Log level; trace, debug, info, warn, error. (default: {})",
|
||||||
|
logging
|
||||||
|
);
|
||||||
|
let ignore_file_help = format!(
|
||||||
|
"File contains items to mark as ignored. (default: {})",
|
||||||
|
ignore_file
|
||||||
|
);
|
||||||
let lms_host_help = format!("LMS hostname or IP address (default: {})", &lms_host);
|
let lms_host_help = format!("LMS hostname or IP address (default: {})", &lms_host);
|
||||||
let description = format!("Bliss Analyser v{}", VERSION);
|
let description = format!("Bliss Analyser v{}", VERSION);
|
||||||
|
|
||||||
@@ -54,33 +61,80 @@ fn main() {
|
|||||||
// borrow per scope, hence this section is enclosed in { }
|
// borrow per scope, hence this section is enclosed in { }
|
||||||
let mut arg_parse = ArgumentParser::new();
|
let mut arg_parse = ArgumentParser::new();
|
||||||
arg_parse.set_description(&description);
|
arg_parse.set_description(&description);
|
||||||
arg_parse.refer(&mut config_file).add_option(&["-c", "--config"], Store, &config_file_help);
|
arg_parse
|
||||||
arg_parse.refer(&mut music_path).add_option(&["-m", "--music"], Store, &music_path_help);
|
.refer(&mut config_file)
|
||||||
arg_parse.refer(&mut db_path).add_option(&["-d", "--db"], Store, &db_path_help);
|
.add_option(&["-c", "--config"], Store, &config_file_help);
|
||||||
arg_parse.refer(&mut logging).add_option(&["-l", "--logging"], Store, &logging_help);
|
arg_parse
|
||||||
arg_parse.refer(&mut keep_old).add_option(&["-k", "--keep-old"], StoreTrue, "Don't remove tracks from DB if they don't exist (used with analyse task)");
|
.refer(&mut music_path)
|
||||||
arg_parse.refer(&mut dry_run).add_option(&["-r", "--dry-run"], StoreTrue, "Dry run, only show what needs to be done (used with analyse task)");
|
.add_option(&["-m", "--music"], Store, &music_path_help);
|
||||||
arg_parse.refer(&mut ignore_file).add_option(&["-i", "--ignore"], Store, &ignore_file_help);
|
arg_parse
|
||||||
arg_parse.refer(&mut lms_host).add_option(&["-L", "--lms"], Store, &lms_host_help);
|
.refer(&mut db_path)
|
||||||
arg_parse.refer(&mut max_num_tracks).add_option(&["-n", "--numtracks"], Store, "Maximum number of tracks to analyse");
|
.add_option(&["-d", "--db"], Store, &db_path_help);
|
||||||
arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, stopmixer.");
|
arg_parse
|
||||||
|
.refer(&mut logging)
|
||||||
|
.add_option(&["-l", "--logging"], Store, &logging_help);
|
||||||
|
arg_parse.refer(&mut keep_old).add_option(
|
||||||
|
&["-k", "--keep-old"],
|
||||||
|
StoreTrue,
|
||||||
|
"Don't remove tracks from DB if they don't exist (used with analyse task)",
|
||||||
|
);
|
||||||
|
arg_parse.refer(&mut dry_run).add_option(
|
||||||
|
&["-r", "--dry-run"],
|
||||||
|
StoreTrue,
|
||||||
|
"Dry run, only show what needs to be done (used with analyse task)",
|
||||||
|
);
|
||||||
|
arg_parse
|
||||||
|
.refer(&mut ignore_file)
|
||||||
|
.add_option(&["-i", "--ignore"], Store, &ignore_file_help);
|
||||||
|
arg_parse
|
||||||
|
.refer(&mut lms_host)
|
||||||
|
.add_option(&["-L", "--lms"], Store, &lms_host_help);
|
||||||
|
arg_parse.refer(&mut max_num_tracks).add_option(
|
||||||
|
&["-n", "--numtracks"],
|
||||||
|
Store,
|
||||||
|
"Maximum number of tracks to analyse",
|
||||||
|
);
|
||||||
|
arg_parse.refer(&mut task).add_argument(
|
||||||
|
"task",
|
||||||
|
Store,
|
||||||
|
"Task to perform; analyse, tags, ignore, upload, stopmixer.",
|
||||||
|
);
|
||||||
arg_parse.parse_args_or_exit();
|
arg_parse.parse_args_or_exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(logging.eq_ignore_ascii_case("trace") || logging.eq_ignore_ascii_case("debug") || logging.eq_ignore_ascii_case("info") || logging.eq_ignore_ascii_case("warn") || logging.eq_ignore_ascii_case("error")) {
|
if !(logging.eq_ignore_ascii_case("trace")
|
||||||
|
|| logging.eq_ignore_ascii_case("debug")
|
||||||
|
|| logging.eq_ignore_ascii_case("info")
|
||||||
|
|| logging.eq_ignore_ascii_case("warn")
|
||||||
|
|| logging.eq_ignore_ascii_case("error"))
|
||||||
|
{
|
||||||
logging = String::from("info");
|
logging = String::from("info");
|
||||||
}
|
}
|
||||||
let mut builder = env_logger::Builder::from_env(env_logger::Env::default().filter_or("XXXXXXXX", logging));
|
let mut builder =
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().filter_or("XXXXXXXX", logging));
|
||||||
builder.filter(Some("bliss_audio"), LevelFilter::Error);
|
builder.filter(Some("bliss_audio"), LevelFilter::Error);
|
||||||
builder.format(|buf, record| writeln!(buf, "[{} {:.1}] {}", Local::now().format("%Y-%m-%d %H:%M:%S"), record.level(), record.args()));
|
builder.format(|buf, record| {
|
||||||
|
writeln!(
|
||||||
|
buf,
|
||||||
|
"[{} {:.1}] {}",
|
||||||
|
Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||||
|
record.level(),
|
||||||
|
record.args()
|
||||||
|
)
|
||||||
|
});
|
||||||
builder.init();
|
builder.init();
|
||||||
|
|
||||||
if task.is_empty() {
|
if task.is_empty() {
|
||||||
log::error!("No task specified, please choose from; analyse, tags, ignore, upload");
|
log::error!("No task specified, please choose from; analyse, tags, ignore, upload");
|
||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !task.eq_ignore_ascii_case("analyse") && !task.eq_ignore_ascii_case("tags") && !task.eq_ignore_ascii_case("ignore") && !task.eq_ignore_ascii_case("upload") && !task.eq_ignore_ascii_case("stopmixer") {
|
if !task.eq_ignore_ascii_case("analyse")
|
||||||
|
&& !task.eq_ignore_ascii_case("tags")
|
||||||
|
&& !task.eq_ignore_ascii_case("ignore")
|
||||||
|
&& !task.eq_ignore_ascii_case("upload")
|
||||||
|
&& !task.eq_ignore_ascii_case("stopmixer")
|
||||||
|
{
|
||||||
log::error!("Invalid task ({}) supplied", task);
|
log::error!("Invalid task ({}) supplied", task);
|
||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
@@ -91,26 +145,35 @@ fn main() {
|
|||||||
let mut config = Ini::new();
|
let mut config = Ini::new();
|
||||||
match config.load(&config_file) {
|
match config.load(&config_file) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let path_keys: [&str; 5] = ["music", "music_1", "music_2", "music_3", "music_4"];
|
let path_keys: [&str; 5] =
|
||||||
|
["music", "music_1", "music_2", "music_3", "music_4"];
|
||||||
for key in &path_keys {
|
for key in &path_keys {
|
||||||
match config.get(TOP_LEVEL_INI_TAG, key) {
|
match config.get(TOP_LEVEL_INI_TAG, key) {
|
||||||
Some(val) => { music_paths.push(PathBuf::from(&val)); },
|
Some(val) => {
|
||||||
None => { }
|
music_paths.push(PathBuf::from(&val));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match config.get(TOP_LEVEL_INI_TAG, "db") {
|
match config.get(TOP_LEVEL_INI_TAG, "db") {
|
||||||
Some(val) => { db_path = val; },
|
Some(val) => {
|
||||||
None => { }
|
db_path = val;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
match config.get(TOP_LEVEL_INI_TAG, "lms") {
|
match config.get(TOP_LEVEL_INI_TAG, "lms") {
|
||||||
Some(val) => { lms_host = val; },
|
Some(val) => {
|
||||||
None => { }
|
lms_host = val;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
match config.get(TOP_LEVEL_INI_TAG, "ignore") {
|
match config.get(TOP_LEVEL_INI_TAG, "ignore") {
|
||||||
Some(val) => { ignore_file = val; },
|
Some(val) => {
|
||||||
None => { }
|
ignore_file = val;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load config file. {}", e);
|
log::error!("Failed to load config file. {}", e);
|
||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
@@ -151,7 +214,10 @@ fn main() {
|
|||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
if !mpath.is_dir() {
|
if !mpath.is_dir() {
|
||||||
log::error!("Music path ({}) is not a directory", mpath.to_string_lossy());
|
log::error!(
|
||||||
|
"Music path ({}) is not a directory",
|
||||||
|
mpath.to_string_lossy()
|
||||||
|
);
|
||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-64
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::db;
|
||||||
/**
|
/**
|
||||||
* Analyse music with Bliss
|
* Analyse music with Bliss
|
||||||
*
|
*
|
||||||
@@ -5,83 +6,75 @@
|
|||||||
* GPLv3 license.
|
* GPLv3 license.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
use lofty::{Accessor, ItemKey};
|
||||||
use lofty::{Accessor, ItemKey, Probe};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use substring::Substring;
|
use substring::Substring;
|
||||||
use crate::db;
|
|
||||||
|
|
||||||
const MAX_GENRE_VAL:usize = 192;
|
const MAX_GENRE_VAL: usize = 192;
|
||||||
|
|
||||||
pub fn read(track:&String) -> db::Metadata {
|
pub fn read(track: &String) -> db::Metadata {
|
||||||
let mut meta = db::Metadata{
|
let mut meta = db::Metadata {
|
||||||
title:String::new(),
|
duration: 180,
|
||||||
artist:String::new(),
|
..db::Metadata::default()
|
||||||
album:String::new(),
|
|
||||||
album_artist:String::new(),
|
|
||||||
genre:String::new(),
|
|
||||||
duration:180
|
|
||||||
};
|
};
|
||||||
let path = Path::new(track);
|
|
||||||
match Probe::open(path) {
|
|
||||||
Ok(probe) => {
|
|
||||||
match probe.read(true) {
|
|
||||||
Ok(file) => {
|
|
||||||
let tag = match file.primary_tag() {
|
|
||||||
Some(primary_tag) => primary_tag,
|
|
||||||
None => file.first_tag().expect("Error: No tags found!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
meta.title=tag.title().unwrap_or("").to_string();
|
if let Ok(file) = lofty::read_from_path(Path::new(track), true) {
|
||||||
meta.artist=tag.artist().unwrap_or("").to_string();
|
let tag = match file.primary_tag() {
|
||||||
meta.album=tag.album().unwrap_or("").to_string();
|
Some(primary_tag) => primary_tag,
|
||||||
meta.album_artist=tag.get_string(&ItemKey::AlbumArtist).unwrap_or("").to_string();
|
None => file.first_tag().expect("Error: No tags found!"),
|
||||||
meta.genre=tag.genre().unwrap_or("").to_string();
|
};
|
||||||
// Check whether MP3 as numeric genre, and if so covert to text
|
|
||||||
if file.file_type().eq(&lofty::FileType::MP3) {
|
meta.title = tag.title().unwrap_or_default().to_string();
|
||||||
match tag.genre() {
|
meta.artist = tag.artist().unwrap_or_default().to_string();
|
||||||
Some(genre) => {
|
meta.album = tag.album().unwrap_or_default().to_string();
|
||||||
let test = &genre.parse::<u8>();
|
meta.album_artist = tag
|
||||||
match test {
|
.get_string(&ItemKey::AlbumArtist)
|
||||||
Ok(val) => {
|
.unwrap_or_default()
|
||||||
let idx:usize = *val as usize;
|
.to_string();
|
||||||
if idx<MAX_GENRE_VAL {
|
meta.genre = tag.genre().unwrap_or_default().to_string();
|
||||||
meta.genre=lofty::id3::v1::GENRES[idx].to_string();
|
|
||||||
}
|
// Check whether MP3 as numeric genre, and if so covert to text
|
||||||
},
|
if file.file_type().eq(&lofty::FileType::MP3) {
|
||||||
Err(_) => {
|
match tag.genre() {
|
||||||
// Check for "(number)text"
|
Some(genre) => {
|
||||||
let re = Regex::new(r"^\([0-9]+\)").unwrap();
|
let test = genre.parse::<u8>();
|
||||||
if re.is_match(&genre) {
|
match test {
|
||||||
match genre.find(")") {
|
Ok(val) => {
|
||||||
Some(end) => {
|
let idx: usize = val as usize;
|
||||||
let test = &genre.to_string().substring(1, end).parse::<u8>();
|
if idx < MAX_GENRE_VAL {
|
||||||
match test {
|
meta.genre = lofty::id3::v1::GENRES[idx].to_string();
|
||||||
Ok(val) => {
|
}
|
||||||
let idx:usize = *val as usize;
|
}
|
||||||
if idx<MAX_GENRE_VAL {
|
Err(_) => {
|
||||||
meta.genre=lofty::id3::v1::GENRES[idx].to_string();
|
// Check for "(number)text"
|
||||||
}
|
let re = Regex::new(r"^\([0-9]+\)").unwrap();
|
||||||
},
|
if re.is_match(&genre) {
|
||||||
Err(_) => { }
|
match genre.find(")") {
|
||||||
}
|
Some(end) => {
|
||||||
},
|
let test =
|
||||||
None => { }
|
genre.to_string().substring(1, end).parse::<u8>();
|
||||||
|
|
||||||
|
if let Ok(val) = test {
|
||||||
|
let idx: usize = val as usize;
|
||||||
|
if idx < MAX_GENRE_VAL {
|
||||||
|
meta.genre =
|
||||||
|
lofty::id3::v1::GENRES[idx].to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meta.duration=file.properties().duration().as_secs() as u32;
|
}
|
||||||
},
|
None => {}
|
||||||
Err(_) => { }
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(_) => { }
|
|
||||||
|
meta.duration = file.properties().duration().as_secs() as u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
meta
|
meta
|
||||||
}
|
}
|
||||||
+54
-54
@@ -5,100 +5,100 @@
|
|||||||
* GPLv3 license.
|
* GPLv3 license.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::process;
|
use std::process;
|
||||||
use substring::Substring;
|
use substring::Substring;
|
||||||
use ureq;
|
use ureq;
|
||||||
|
|
||||||
|
fn fail(msg: &str) {
|
||||||
fn fail(msg:&str) {
|
|
||||||
log::error!("{}", msg);
|
log::error!("{}", msg);
|
||||||
process::exit(-1);
|
process::exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_mixer(lms:&String) {
|
pub fn stop_mixer(lms: &String) {
|
||||||
let stop_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"stop\"]]}";
|
let stop_req =
|
||||||
|
"{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"stop\"]]}";
|
||||||
|
|
||||||
log::info!("Asking plugin to stop mixer");
|
log::info!("Asking plugin to stop mixer");
|
||||||
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req) {
|
let req = ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req);
|
||||||
Ok(_) => { },
|
if let Err(e) = req {
|
||||||
Err(e) => { log::error!("Failed to ask plugin to stop mixer. {}", e); }
|
log::error!("Failed to ask plugin to stop mixer. {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_db(db_path:&String, lms:&String) {
|
pub fn upload_db(db_path: &String, lms: &String) {
|
||||||
// First tell LMS to restart the mixer in upload mode
|
// First tell LMS to restart the mixer in upload mode
|
||||||
let start_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"start-upload\"]]}";
|
let start_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"start-upload\"]]}";
|
||||||
let mut port:u16 = 0;
|
let mut port: u16 = 0;
|
||||||
|
|
||||||
log::info!("Requesting LMS plugin to allow uploads");
|
log::info!("Requesting LMS plugin to allow uploads");
|
||||||
|
|
||||||
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&start_req) {
|
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&start_req) {
|
||||||
Ok(resp) => {
|
Ok(resp) => match resp.into_string() {
|
||||||
match resp.into_string() {
|
Ok(text) => match text.find("\"port\":") {
|
||||||
Ok(text) => {
|
Some(s) => {
|
||||||
match text.find("\"port\":") {
|
let txt = text.to_string().substring(s + 7, text.len()).to_string();
|
||||||
Some(s) => {
|
match txt.find("}") {
|
||||||
let txt = text.to_string().substring(s+7, text.len()).to_string();
|
Some(e) => {
|
||||||
match txt.find("}") {
|
let p = txt.substring(0, e);
|
||||||
Some(e) => {
|
let test = p.parse::<u16>();
|
||||||
let p = txt.substring(0, e);
|
match test {
|
||||||
let test = p.parse::<u16>();
|
Ok(val) => {
|
||||||
match test {
|
port = val;
|
||||||
Ok(val) => {
|
}
|
||||||
port = val;
|
Err(_) => {
|
||||||
},
|
fail("Could not parse resp (cast)");
|
||||||
Err(_) => { fail("Could not parse resp (cast)"); }
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
None => { fail("Could not parse resp (closing)"); }
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => { fail("Could not parse resp (no port)"); }
|
None => {
|
||||||
|
fail("Could not parse resp (closing)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(_) => { fail("No text?")}
|
None => {
|
||||||
}
|
fail("Could not parse resp (no port)");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => fail("No text?"),
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail(&format!("Failed to ask LMS plugin to allow upload. {}", e));
|
fail(&format!("Failed to ask LMS plugin to allow upload. {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if port<=0 {
|
if port == 0 {
|
||||||
fail("Invalid port");
|
fail("Invalid port");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we have port number, do the actual upload...
|
// Now we have port number, do the actual upload...
|
||||||
log::info!("Uploading {}", db_path);
|
log::info!("Uploading {}", db_path);
|
||||||
match File::open(db_path) {
|
match File::open(db_path) {
|
||||||
Ok(file) => {
|
Ok(file) => match file.metadata() {
|
||||||
match file.metadata() {
|
Ok(meta) => {
|
||||||
Ok(meta) => {
|
let buffered_reader = BufReader::new(file);
|
||||||
let buffered_reader = BufReader::new(file);
|
log::info!("Length: {}", meta.len());
|
||||||
log::info!("Length: {}", meta.len());
|
match ureq::put(&format!("http://{}:{}/upload", lms, port))
|
||||||
match ureq::put(&format!("http://{}:{}/upload", lms, port))
|
.set("Content-Length", &meta.len().to_string())
|
||||||
.set("Content-Length", &meta.len().to_string())
|
.set("Content-Type", "application/octet-stream")
|
||||||
.set("Content-Type", "application/octet-stream")
|
.send(buffered_reader)
|
||||||
.send(buffered_reader) {
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Database uploaded");
|
log::info!("Database uploaded");
|
||||||
stop_mixer(lms);
|
stop_mixer(lms);
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail(&format!("Failed to upload database. {}", e));
|
fail(&format!("Failed to upload database. {}", e));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
fail(&format!("Failed to open database. {}", e));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
fail(&format!("Failed to open database. {}", e));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail(&format!("Failed to open database. {}", e));
|
fail(&format!("Failed to open database. {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user