Merge pull request #1 from Serial-ATA/master

Cleanup
This commit is contained in:
CraigD authored and GitHub committed 2022-03-23 07:34:44 +00:00
commit caa8f124a7
8 files changed
+415 -288

No files matched your search

Generated
+7
View File
@@ -129,6 +129,7 @@ dependencies = [
"dirs",
"env_logger",
"hhmmss",
"if_chain",
"indicatif",
"lofty",
"log",
@@ -635,6 +636,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "if_chain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "indexmap"
version = "1.8.0"
+1
View File
@@ -29,3 +29,4 @@ hhmmss = "0.1.0"
num_cpus = "1.13.0"
tempdir = "0.3.7"
subprocess = "0.2.8"
if_chain = "1.0.2"
+144 -116
View File
@@ -1,3 +1,6 @@
use crate::cue;
use crate::db;
use crate::tags;
/**
* Analyse music with Bliss
*
@@ -5,111 +8,111 @@
* GPLv3 license.
*
**/
use anyhow::{Result};
use anyhow::Result;
use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song};
use hhmmss::Hhmmss;
use if_chain::if_chain;
use indicatif::{ProgressBar, ProgressStyle};
use num_cpus;
use std::convert::TryInto;
use std::fs;
use std::fs::File;
use std::fs::{DirEntry, File};
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::time::Duration;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::Duration;
use subprocess::{Exec, NullFile};
use tempdir::TempDir;
use num_cpus;
use crate::cue;
use crate::db;
use crate::tags;
const DONT_ANALYSE: &str = ".notmusic";
const MAX_ERRORS_TO_SHOW: usize = 100;
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>) {
if path.is_dir() {
match path.read_dir() {
Ok(items) => {
for item in items {
match item {
Ok(entry) => {
let pb = entry.path().to_path_buf();
if entry.path().is_dir() {
let mut check = pb.clone();
check.push(PathBuf::from(DONT_ANALYSE));
if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
} else {
get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks);
fn get_file_list(
db: &mut db::Db,
mpath: &Path,
path: &Path,
track_paths: &mut Vec<String>,
cue_tracks: &mut Vec<cue::CueTrack>,
) {
if !path.is_dir() {
return;
}
} 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) => {
if let Ok(items) = path.read_dir() {
for item in items {
if let Ok(entry) = item {
check_dir_entry(db, mpath, entry, track_paths, cue_tracks);
}
}
}
}
fn check_dir_entry(
db: &mut db::Db,
mpath: &Path,
entry: DirEntry,
track_paths: &mut Vec<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 {
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 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.clone());
cue_tracks.push(track);
}
},
Err(_) => { }
}
},
Err(_) => { }
}
}
} else {
let spb = stripped.to_path_buf();
let sname = String::from(spb.to_string_lossy());
match db.get_rowid(&sname) {
Ok(id) => {
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()));
}
},
Err(_) => { }
}
}
},
Err(_) => { }
}
}
}
}
},
Err(_) => { }
}
}
},
Err(_) => { }
}
}
}
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>) -> Result<()> {
let total = track_paths.len();
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
.progress_chars("=> "),
);
let results = analyze_paths_streaming(track_paths)?;
let mut analysed = 0;
@@ -122,30 +125,36 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
let stripped = pbuff.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
pb.set_message(format!("{}", sname));
progress.set_message(format!("{}", sname));
match result {
Ok(track) => {
let cpath = String::from(path);
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());
}
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
},
}
Err(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() {
let total = failed.len();
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 {
log::error!(" {}", err);
}
@@ -157,7 +166,7 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
let total = tag_error.len();
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 {
log::error!(" {}", err);
}
@@ -168,7 +177,9 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
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 last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
@@ -193,10 +204,15 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
let tx_thread = tx.clone();
let owned_chunk = chunk.to_owned();
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;
match &TempDir::new("bliss") {
Ok(dir) => {
for cue_track in owned_chunk {
let dir = dir.unwrap();
for mut cue_track in owned_chunk {
let audio_path = String::from(cue_track.audio_path.to_string_lossy());
let ext = cue_track.audio_path.extension();
let track_path = String::from(cue_track.track_path.to_string_lossy());
@@ -209,23 +225,26 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
idx += 1;
log::debug!("Extracting '{}'", track_path);
match Exec::cmd("ffmpeg").arg("-i").arg(&audio_path)
.arg("-ss").arg(&cue_track.start.hhmmss())
.arg("-t").arg(&cue_track.duration.hhmmss())
.arg("-c").arg("copy")
let cmd = Exec::cmd("ffmpeg")
.arg("-i")
.arg(&audio_path)
.arg("-ss")
.arg(&cue_track.start.hhmmss())
.arg("-t")
.arg(&cue_track.duration.hhmmss())
.arg("-c")
.arg("copy")
.arg(String::from(tmp_file.to_string_lossy()))
.stderr(NullFile)
.join() {
Ok(_) => { },
Err(e) => { log::error!("Failed to call ffmpeg. {}", e); }
.join();
if let Err(e) = cmd {
log::error!("Failed to call ffmpeg. {}", e);
}
if !cfg!(windows) {
// ffmpeg seeks to break echo on terminal? 'stty echo' restores...
match Exec::cmd("stty").arg("echo").join() {
Ok(_) => { },
Err(_) => { }
}
let _ = Exec::cmd("stty").arg("echo").join();
}
if tmp_file.exists() {
@@ -233,24 +252,16 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
let song = Song::new(&tmp_file);
if cue_track.duration >= last_track_duration {
// Last track, so read duration from temp file
let mut cloned = cue_track.clone();
let meta = tags::read(&String::from(tmp_file.to_string_lossy()));
cloned.duration = Duration::new(meta.duration as u64, 0);
tx_thread.send((cloned, song)).unwrap();
} else {
cue_track.duration = Duration::new(meta.duration as u64, 0);
}
tx_thread.send((cue_track, song)).unwrap();
}
match fs::remove_file(tmp_file) {
Ok(_) => { },
Err(_) => { }
}
let _ = fs::remove_file(tmp_file);
} else {
log::error!("Failed to create temp file");
}
}
},
Err(e) => { log::error!("Failed to create temp folder. {}", e); }
}
});
handles.push(child);
}
@@ -258,13 +269,17 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
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 pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
.progress_chars("=> "),
);
let results = analyze_cue_streaming(cue_tracks)?;
let mut analysed = 0;
@@ -275,7 +290,7 @@ pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::C
let stripped = track.track_path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
pb.set_message(format!("{}", sname));
progress.set_message(format!("{}", sname));
match result {
Ok(song) => {
let meta = db::Metadata {
@@ -284,24 +299,28 @@ 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:track.duration.as_secs() as u32
duration: track.duration.as_secs() as u32,
};
db.add_track(&sname, &meta, &song.analysis);
analysed += 1;
},
}
Err(e) => {
failed.push(format!("{} - {}", sname, e));
}
};
pb.inc(1);
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() {
let total = failed.len();
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 {
log::error!(" {}", err);
}
@@ -312,7 +331,13 @@ 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_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 track_count_left = max_num_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() {
log::info!("Num new cue tracks: {}", cue_tracks.len());
}
if dry_run {
if !track_paths.is_empty() || !cue_tracks.is_empty() {
log::info!("The following need to be analysed:");
@@ -371,17 +397,18 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths) {
Ok(_) => { },
Err(e) => { log::error!("Analysis returned error: {}", e); }
Ok(_) => {}
Err(e) => {
log::error!("Analysis returned error: {}", e);
}
}
} else {
log::info!("No new tracks to analyse");
}
if !cue_tracks.is_empty() {
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
Ok(_) => { },
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
if let Err(e) = analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
log::error!("Cue analysis returned error: {}", e);
}
}
@@ -409,11 +436,12 @@ pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
db.init();
db.clear_ignore();
for (_index, line) in reader.lines().enumerate() {
let line = line.unwrap();
let mut lines = reader.lines();
while let Some(Ok(line)) = lines.next() {
if !line.is_empty() && !line.starts_with("#") {
db.set_ignore(&line);
}
}
db.close();
}
+25 -18
View File
@@ -5,7 +5,6 @@
* GPLv3 license.
*
**/
extern crate rcue;
use rcue::parser::parse_from_file;
@@ -26,14 +25,14 @@ pub struct CueTrack {
pub album_artist: String,
pub genre: String,
pub start: Duration,
pub duration:Duration
pub duration: Duration,
}
pub fn parse(audio_path: &PathBuf, cue_path: &PathBuf) -> Vec<CueTrack> {
let mut resp: Vec<CueTrack> = Vec::new();
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_artist = cue.performer.unwrap_or(String::new());
let mut genre = String::new();
@@ -42,49 +41,56 @@ pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
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 {
match track.indices.get(0) {
Some((_, start)) => {
let mut track_path = audio_path.clone();
let ext = audio_path.extension().unwrap().to_string_lossy();
track_path.set_extension(format!("{}{}{}", ext, MARKER, resp.len()+1));
track_path.set_extension(format!(
"{}{}{}",
ext,
MARKER,
resp.len() + 1
));
let mut ctrack = CueTrack {
audio_path: audio_path.clone(),
track_path: track_path,
title: track.title.unwrap_or(String::new()),
artist: track.performer.unwrap_or(String::new()),
track_path,
title: track.title.unwrap_or_default(),
artist: track.performer.unwrap_or_default(),
album_artist: album_artist.clone(),
album: album.clone(),
genre: genre.clone(),
start: start.clone(),
duration: Duration::new(LAST_TRACK_DURATION, 0),
};
if ctrack.artist.is_empty() && !ctrack.album_artist.is_empty() {
ctrack.artist = ctrack.album_artist.clone();
}
if ctrack.album.is_empty() {
let mut path = audio_path.clone();
path.set_extension("");
match path.file_name() {
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); }
None => { }
if let Some(name) = audio_path.file_name() {
ctrack.album = String::from(name.to_string_lossy());
}
}
resp.push(ctrack);
},
}
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) {
let mut next_start = Duration::new(0, 0);
let mut next_start = Duration::ZERO;
if let Some(next) = resp.get(i + 1) {
next_start = next.start;
}
@@ -92,5 +98,6 @@ pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
(*elem).duration = next_start - elem.start;
}
}
resp
}
+91 -66
View File
@@ -5,15 +5,14 @@
* GPLv3 license.
*
**/
use crate::cue;
use crate::tags;
use bliss_audio::{Analysis, AnalysisIndex};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::{Connection, params};
use rusqlite::{params, Connection};
use std::convert::TryInto;
use std::path::PathBuf;
use std::process;
use crate::cue;
use crate::tags;
pub struct FileMetadata {
pub rowid: usize,
@@ -23,20 +22,31 @@ pub struct FileMetadata {
pub album_artist: Option<String>,
pub album: Option<String>,
pub genre: Option<String>,
pub duration:u32
pub duration: u32,
}
#[derive(Default, PartialEq)]
pub struct Metadata {
pub title: String,
pub artist: String,
pub album_artist: String,
pub album: 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 conn: Connection
pub conn: Connection,
}
impl Db {
@@ -47,7 +57,7 @@ impl Db {
}
pub fn init(&self) {
match self.conn.execute(
let cmd = self.conn.execute(
"CREATE TABLE IF NOT EXISTS Tracks (
File text primary key,
Title text,
@@ -77,38 +87,41 @@ impl Db {
Chroma8 real,
Chroma9 real,
Chroma10 real
);",[]) {
Ok(_) => { },
Err(_) => {
);",
[],
);
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(_) => { },
Err(_) => {
let cmd = self.conn.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)",
[],
);
if cmd.is_err() {
log::error!("Failed to create DB index");
process::exit(-1);
}
}
}
pub fn close(self) {
match self.conn.close() {
Ok(_) => { },
Err(_) => { }
}
let _ = self.conn.close();
}
pub fn get_rowid(&self, path: &String) -> Result<usize, rusqlite::Error> {
let mut db_path = path.clone();
pub fn get_rowid(&self, path: &str) -> Result<usize, rusqlite::Error> {
let mut db_path = path.to_string();
if cfg!(windows) {
db_path = db_path.replace("\\", "/");
}
let mut stmt = self.conn.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
let track_iter = stmt.query_map(&[(":path", &db_path)], |row| {
Ok(row.get(0)?)
}).unwrap();
let mut stmt = self
.conn
.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
let track_iter = stmt
.query_map(&[(":path", &db_path)], |row| Ok(row.get(0)?))
.unwrap();
let mut rowid: usize = 0;
for tr in track_iter {
rowid = tr.unwrap();
@@ -145,17 +158,15 @@ impl Db {
Err(e) => { log::error!("Failed to update '{}' in database. {}", path, e); }
}
}
},
}
Err(_) => {}
}
}
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 track_iter = stmt.query_map([], |row| {
Ok((row.get(0)?,))
}).unwrap();
let track_iter = stmt.query_map([], |row| Ok((row.get(0)?,))).unwrap();
let mut to_remove: Vec<String> = Vec::new();
for tr in track_iter {
let mut db_path: String = tr.unwrap().0;
@@ -163,7 +174,7 @@ impl Db {
match orig_path.find(cue::MARKER) {
Some(s) => {
db_path.truncate(s);
},
}
None => {}
}
if cfg!(windows) {
@@ -187,7 +198,7 @@ impl Db {
}
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 dry_run {
log::info!("The following need to be removed from database:");
@@ -198,9 +209,12 @@ impl Db {
let count_before = self.get_track_count();
for t in to_remove {
//log::debug!("Remove '{}'", t);
match self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]) {
Ok(_) => { },
Err(e) => { log::error!("Failed to remove '{}' - {}", t, e) }
let cmd = self
.conn
.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();
@@ -213,9 +227,7 @@ impl Db {
pub fn get_track_count(&self) -> usize {
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM Tracks;").unwrap();
let track_iter = stmt.query_map([], |row| {
Ok(row.get(0)?)
}).unwrap();
let track_iter = stmt.query_map([], |row| Ok(row.get(0)?)).unwrap();
let mut count: usize = 0;
for tr in track_iter {
count = tr.unwrap();
@@ -227,13 +239,17 @@ impl Db {
pub fn update_tags(&self, mpaths: &Vec<PathBuf>) {
let total = self.get_track_count();
if total > 0 {
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
let progress = ProgressBar::new(total.try_into().unwrap()).with_style(
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}",
)
.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 track_iter = stmt.query_map([], |row| {
let track_iter = stmt
.query_map([], |row| {
Ok(FileMetadata {
rowid: row.get(0)?,
file: row.get(1)?,
@@ -244,30 +260,31 @@ impl Db {
genre: row.get(6)?,
duration: row.get(7)?,
})
}).unwrap();
})
.unwrap();
let mut updated = 0;
for tr in track_iter {
let dbtags = tr.unwrap();
if !dbtags.file.contains(cue::MARKER) {
let dtags = Metadata {
title:dbtags.title.unwrap_or(String::new()),
artist:dbtags.artist.unwrap_or(String::new()),
album_artist:dbtags.album_artist.unwrap_or(String::new()),
album:dbtags.album.unwrap_or(String::new()),
genre:dbtags.genre.unwrap_or(String::new()),
duration:dbtags.duration
title: dbtags.title.unwrap_or_default(),
artist: dbtags.artist.unwrap_or_default(),
album_artist: dbtags.album_artist.unwrap_or_default(),
album: dbtags.album.unwrap_or_default(),
genre: dbtags.genre.unwrap_or_default(),
duration: dbtags.duration,
};
pb.set_message(format!("{}", dbtags.file));
progress.set_message(format!("{}", dbtags.file));
for mpath in mpaths {
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);
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);
} 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=?;",
params![ftags.title, ftags.artist, ftags.album_artist, ftags.album, ftags.genre, ftags.duration, dbtags.rowid]) {
Ok(_) => { updated += 1; },
@@ -278,16 +295,17 @@ 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) {
match self.conn.execute("UPDATE Tracks SET Ignore=0;", []) {
Ok(_) => { },
Err(e) => { log::error!("Failed clear Ignore column. {}", e); }
let cmd = self.conn.execute("UPDATE Tracks SET Ignore=0;", []);
if let Err(e) = cmd {
log::error!("Failed clear Ignore column. {}", e);
}
}
@@ -295,14 +313,21 @@ impl Db {
log::info!("Ignore: {}", line);
if line.starts_with("SQL:") {
let sql = &line[4..];
match self.conn.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []) {
Ok(_) => { },
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
let cmd = self
.conn
.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
} else {
match self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line), []) {
Ok(_) => { },
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
let cmd = self.conn.execute(
&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line),
[],
);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
}
}
+91 -25
View File
@@ -19,7 +19,6 @@ mod db;
mod tags;
mod upload;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const TOP_LEVEL_INI_TAG: &str = "Bliss";
@@ -37,7 +36,9 @@ fn main() {
let mut music_paths: Vec<PathBuf> = Vec::new();
match dirs::home_dir() {
Some(path) => { music_path = String::from(path.join("Music").to_string_lossy()); }
Some(path) => {
music_path = String::from(path.join("Music").to_string_lossy());
}
None => {}
}
@@ -45,8 +46,14 @@ fn main() {
let config_file_help = format!("config file (default: {})", &config_file);
let music_path_help = format!("Music folder (default: {})", &music_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 ignore_file_help = format!("File containg items to mark as ignored. (default: {})", ignore_file);
let logging_help = format!(
"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 description = format!("Bliss Analyser v{}", VERSION);
@@ -54,25 +61,67 @@ fn main() {
// borrow per scope, hence this section is enclosed in { }
let mut arg_parse = ArgumentParser::new();
arg_parse.set_description(&description);
arg_parse.refer(&mut config_file).add_option(&["-c", "--config"], Store, &config_file_help);
arg_parse.refer(&mut music_path).add_option(&["-m", "--music"], Store, &music_path_help);
arg_parse.refer(&mut db_path).add_option(&["-d", "--db"], Store, &db_path_help);
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
.refer(&mut config_file)
.add_option(&["-c", "--config"], Store, &config_file_help);
arg_parse
.refer(&mut music_path)
.add_option(&["-m", "--music"], Store, &music_path_help);
arg_parse
.refer(&mut db_path)
.add_option(&["-d", "--db"], Store, &db_path_help);
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();
}
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");
}
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.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();
if task.is_empty() {
@@ -80,7 +129,12 @@ fn main() {
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);
process::exit(-1);
}
@@ -91,26 +145,35 @@ fn main() {
let mut config = Ini::new();
match config.load(&config_file) {
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 {
match config.get(TOP_LEVEL_INI_TAG, key) {
Some(val) => { music_paths.push(PathBuf::from(&val)); },
Some(val) => {
music_paths.push(PathBuf::from(&val));
}
None => {}
}
}
match config.get(TOP_LEVEL_INI_TAG, "db") {
Some(val) => { db_path = val; },
Some(val) => {
db_path = val;
}
None => {}
}
match config.get(TOP_LEVEL_INI_TAG, "lms") {
Some(val) => { lms_host = val; },
Some(val) => {
lms_host = val;
}
None => {}
}
match config.get(TOP_LEVEL_INI_TAG, "ignore") {
Some(val) => { ignore_file = val; },
Some(val) => {
ignore_file = val;
}
None => {}
}
},
}
Err(e) => {
log::error!("Failed to load config file. {}", e);
process::exit(-1);
@@ -151,7 +214,10 @@ fn main() {
process::exit(-1);
}
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);
}
}
+29 -36
View File
@@ -1,3 +1,4 @@
use crate::db;
/**
* Analyse music with Bliss
*
@@ -5,83 +6,75 @@
* GPLv3 license.
*
**/
use lofty::{Accessor, ItemKey, Probe};
use lofty::{Accessor, ItemKey};
use regex::Regex;
use std::path::Path;
use substring::Substring;
use crate::db;
const MAX_GENRE_VAL: usize = 192;
pub fn read(track: &String) -> db::Metadata {
let mut meta = db::Metadata {
title:String::new(),
artist:String::new(),
album:String::new(),
album_artist:String::new(),
genre:String::new(),
duration:180
duration: 180,
..db::Metadata::default()
};
let path = Path::new(track);
match Probe::open(path) {
Ok(probe) => {
match probe.read(true) {
Ok(file) => {
if let Ok(file) = lofty::read_from_path(Path::new(track), true) {
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();
meta.artist=tag.artist().unwrap_or("").to_string();
meta.album=tag.album().unwrap_or("").to_string();
meta.album_artist=tag.get_string(&ItemKey::AlbumArtist).unwrap_or("").to_string();
meta.genre=tag.genre().unwrap_or("").to_string();
meta.title = tag.title().unwrap_or_default().to_string();
meta.artist = tag.artist().unwrap_or_default().to_string();
meta.album = tag.album().unwrap_or_default().to_string();
meta.album_artist = tag
.get_string(&ItemKey::AlbumArtist)
.unwrap_or_default()
.to_string();
meta.genre = tag.genre().unwrap_or_default().to_string();
// Check whether MP3 as numeric genre, and if so covert to text
if file.file_type().eq(&lofty::FileType::MP3) {
match tag.genre() {
Some(genre) => {
let test = &genre.parse::<u8>();
let test = genre.parse::<u8>();
match test {
Ok(val) => {
let idx:usize = *val as usize;
let idx: usize = val as usize;
if idx < MAX_GENRE_VAL {
meta.genre = lofty::id3::v1::GENRES[idx].to_string();
}
},
}
Err(_) => {
// Check for "(number)text"
let re = Regex::new(r"^\([0-9]+\)").unwrap();
if re.is_match(&genre) {
match genre.find(")") {
Some(end) => {
let test = &genre.to_string().substring(1, end).parse::<u8>();
match test {
Ok(val) => {
let idx:usize = *val as usize;
let test =
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();
meta.genre =
lofty::id3::v1::GENRES[idx].to_string();
}
}
},
Err(_) => { }
}
},
None => {}
}
}
}
}
},
}
None => {}
}
}
meta.duration = file.properties().duration().as_secs() as u32;
},
Err(_) => { }
}
},
Err(_) => { }
}
meta
}
+27 -27
View File
@@ -5,26 +5,25 @@
* GPLv3 license.
*
**/
use std::fs::File;
use std::io::BufReader;
use std::process;
use substring::Substring;
use ureq;
fn fail(msg: &str) {
log::error!("{}", msg);
process::exit(-1);
}
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");
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req) {
Ok(_) => { },
Err(e) => { log::error!("Failed to ask plugin to stop mixer. {}", e); }
let req = ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req);
if let Err(e) = req {
log::error!("Failed to ask plugin to stop mixer. {}", e);
}
}
@@ -36,10 +35,8 @@ pub fn upload_db(db_path:&String, lms:&String) {
log::info!("Requesting LMS plugin to allow uploads");
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&start_req) {
Ok(resp) => {
match resp.into_string() {
Ok(text) => {
match text.find("\"port\":") {
Ok(resp) => match resp.into_string() {
Ok(text) => match text.find("\"port\":") {
Some(s) => {
let txt = text.to_string().substring(s + 7, text.len()).to_string();
match txt.find("}") {
@@ -49,53 +46,56 @@ pub fn upload_db(db_path:&String, lms:&String) {
match test {
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)"); }
}
},
None => { fail("Could not parse resp (no port)"); }
}
},
Err(_) => { fail("No text?")}
}
Err(_) => fail("No text?"),
},
Err(e) => {
fail(&format!("Failed to ask LMS plugin to allow upload. {}", e));
}
}
if port<=0 {
if port == 0 {
fail("Invalid port");
}
// Now we have port number, do the actual upload...
log::info!("Uploading {}", db_path);
match File::open(db_path) {
Ok(file) => {
match file.metadata() {
Ok(file) => match file.metadata() {
Ok(meta) => {
let buffered_reader = BufReader::new(file);
log::info!("Length: {}", meta.len());
match ureq::put(&format!("http://{}:{}/upload", lms, port))
.set("Content-Length", &meta.len().to_string())
.set("Content-Type", "application/octet-stream")
.send(buffered_reader) {
.send(buffered_reader)
{
Ok(_) => {
log::info!("Database uploaded");
stop_mixer(lms);
},
}
Err(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));