Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
Serial 2022-03-22 17:02:08 -04:00
parent 0153b85b65
commit e0a6759af5
6 changed files with 495 additions and 313 deletions

View File

@ -1,3 +1,6 @@
use crate::cue;
use crate::db;
use crate::tags;
/** /**
* Analyse music with Bliss * Analyse music with Bliss
* *
@ -5,32 +8,34 @@
* 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 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::File;
use std::io::{BufRead, BufReader}; use std::io::{BufRead, BufReader};
use std::path::PathBuf; use std::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;
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(
db: &mut db::Db,
mpath: &PathBuf,
path: &PathBuf,
track_paths: &mut Vec<String>,
cue_tracks: &mut Vec<cue::CueTrack>,
) {
if path.is_dir() { if path.is_dir() {
match path.read_dir() { match path.read_dir() {
Ok(items) => { Ok(items) => {
@ -42,36 +47,56 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut
let mut check = pb.clone(); let mut check = pb.clone();
check.push(PathBuf::from(DONT_ANALYSE)); check.push(PathBuf::from(DONT_ANALYSE));
if check.exists() { if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE); log::info!(
"Skipping '{}', found '{}'",
pb.to_string_lossy(),
DONT_ANALYSE
);
} else { } else {
get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks); get_file_list(
db,
mpath,
&entry.path(),
track_paths,
cue_tracks,
);
} }
} else if entry.path().is_file() { } else if entry.path().is_file() {
let e = pb.extension(); let e = pb.extension();
if e.is_some() { if e.is_some() {
let ext = e.unwrap().to_string_lossy(); let ext = e.unwrap().to_string_lossy();
if ext=="m4a" || ext=="mp3" || ext=="ogg" || ext=="flac" || ext=="opus" { if ext == "m4a"
|| ext == "mp3"
|| ext == "ogg"
|| ext == "flac"
|| ext == "opus"
{
match pb.strip_prefix(mpath) { match pb.strip_prefix(mpath) {
Ok(stripped) => { Ok(stripped) => {
let mut cue_file = pb.clone(); let mut cue_file = pb.clone();
cue_file.set_extension("cue"); cue_file.set_extension("cue");
if cue_file.exists() { if cue_file.exists() {
// Found a CUE file, try to parse and then check if tracks exists in DB // Found a CUE file, try to parse and then check if tracks exists in DB
let this_cue_tracks = cue::parse(&pb, &cue_file); let this_cue_tracks =
cue::parse(&pb, &cue_file);
for track in this_cue_tracks { for track in this_cue_tracks {
match track.track_path.strip_prefix(mpath) { match track.track_path.strip_prefix(mpath) {
Ok(tstripped) => { Ok(tstripped) => {
let spb = tstripped.to_path_buf(); let spb = tstripped.to_path_buf();
let sname = String::from(spb.to_string_lossy()); let sname = String::from(
spb.to_string_lossy(),
);
match db.get_rowid(&sname) { match db.get_rowid(&sname) {
Ok(id) => { Ok(id) => {
if id <= 0 { if id <= 0 {
cue_tracks.push(track.clone()); cue_tracks.push(
track.clone(),
);
}
} }
},
Err(_) => {} Err(_) => {}
} }
}, }
Err(_) => {} Err(_) => {}
} }
} }
@ -81,23 +106,25 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut
match db.get_rowid(&sname) { match db.get_rowid(&sname) {
Ok(id) => { Ok(id) => {
if id <= 0 { if id <= 0 {
track_paths.push(String::from(pb.to_string_lossy())); track_paths.push(String::from(
pb.to_string_lossy(),
));
}
} }
},
Err(_) => {} Err(_) => {}
} }
} }
}, }
Err(_) => {} Err(_) => {}
} }
} }
} }
} }
}, }
Err(_) => {} Err(_) => {}
} }
} }
}, }
Err(_) => {} Err(_) => {}
} }
} }
@ -127,20 +154,28 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
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.title.is_empty()
&& meta.artist.is_empty()
&& meta.album.is_empty()
&& meta.genre.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); pb.inc(1);
} }
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len())); pb.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);
@ -168,7 +203,9 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
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);
@ -209,21 +246,29 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
idx += 1; idx += 1;
log::debug!("Extracting '{}'", track_path); log::debug!("Extracting '{}'", track_path);
match Exec::cmd("ffmpeg").arg("-i").arg(&audio_path) match 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(&cue_track.start.hhmmss())
.arg("-t")
.arg(&cue_track.duration.hhmmss())
.arg("-c")
.arg("copy")
.arg(String::from(tmp_file.to_string_lossy())) .arg(String::from(tmp_file.to_string_lossy()))
.stderr(NullFile) .stderr(NullFile)
.join() { .join()
Ok(_) => { }, {
Err(e) => { log::error!("Failed to call ffmpeg. {}", e); } Ok(_) => {}
Err(e) => {
log::error!("Failed to call ffmpeg. {}", e);
}
} }
if !cfg!(windows) { if !cfg!(windows) {
// ffmpeg seeks to break echo on terminal? 'stty echo' restores... // ffmpeg seeks to break echo on terminal? 'stty echo' restores...
match Exec::cmd("stty").arg("echo").join() { match Exec::cmd("stty").arg("echo").join() {
Ok(_) => { }, Ok(_) => {}
Err(_) => {} Err(_) => {}
} }
} }
@ -241,15 +286,17 @@ pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receive
tx_thread.send((cue_track, song)).unwrap(); tx_thread.send((cue_track, song)).unwrap();
} }
match fs::remove_file(tmp_file) { match fs::remove_file(tmp_file) {
Ok(_) => { }, Ok(_) => {}
Err(_) => {} Err(_) => {}
} }
} else { } else {
log::error!("Failed to create temp file"); log::error!("Failed to create temp file");
} }
} }
}, }
Err(e) => { log::error!("Failed to create temp folder. {}", e); } Err(e) => {
log::error!("Failed to create temp folder. {}", e);
}
} }
}); });
handles.push(child); handles.push(child);
@ -258,7 +305,11 @@ 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 pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar() let style = ProgressStyle::default_bar()
@ -284,19 +335,23 @@ pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::C
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); pb.inc(1);
} }
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len())); pb.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);
@ -312,7 +367,13 @@ pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::C
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;
@ -371,8 +432,10 @@ 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");
@ -380,8 +443,10 @@ 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() {
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) { match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
Ok(_) => { }, Ok(_) => {}
Err(e) => { log::error!("Cue analysis returned error: {}", e); } Err(e) => {
log::error!("Cue analysis returned error: {}", e);
}
} }
} }

View File

@ -5,7 +5,6 @@
* GPLv3 license. * GPLv3 license.
* *
**/ **/
extern crate rcue; extern crate rcue;
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
@ -26,7 +25,7 @@ pub struct CueTrack {
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> {
@ -49,7 +48,12 @@ pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
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!(
"{}{}{}",
ext,
MARKER,
resp.len() + 1
));
let mut ctrack = CueTrack { let mut ctrack = CueTrack {
audio_path: audio_path.clone(), audio_path: audio_path.clone(),
track_path: track_path, track_path: track_path,
@ -68,19 +72,23 @@ pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
let mut path = audio_path.clone(); let mut path = audio_path.clone();
path.set_extension(""); path.set_extension("");
match path.file_name() { match path.file_name() {
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); } Some(n) => {
ctrack.album = String::from(n.to_string_lossy());
}
None => {} None => {}
} }
} }
resp.push(ctrack); 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) {

116
src/db.rs
View File

@ -1,3 +1,5 @@
use crate::cue;
use crate::tags;
/** /**
* Analyse music with Bliss * Analyse music with Bliss
* *
@ -5,15 +7,12 @@
* GPLv3 license. * GPLv3 license.
* *
**/ **/
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,
@ -23,7 +22,7 @@ pub struct FileMetadata {
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,
} }
pub struct Metadata { pub struct Metadata {
@ -32,11 +31,11 @@ pub struct Metadata {
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,
} }
pub struct Db { pub struct Db {
pub conn: Connection pub conn: Connection,
} }
impl Db { impl Db {
@ -77,15 +76,20 @@ impl Db {
Chroma8 real, Chroma8 real,
Chroma9 real, Chroma9 real,
Chroma10 real Chroma10 real
);",[]) { );",
Ok(_) => { }, [],
) {
Ok(_) => {}
Err(_) => { Err(_) => {
log::error!("Failed to create DB table"); log::error!("Failed to create DB table");
process::exit(-1); process::exit(-1);
} }
} }
match self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)", []) { match self.conn.execute(
Ok(_) => { }, "CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)",
[],
) {
Ok(_) => {}
Err(_) => { Err(_) => {
log::error!("Failed to create DB index"); log::error!("Failed to create DB index");
process::exit(-1); process::exit(-1);
@ -95,7 +99,7 @@ impl Db {
pub fn close(self) { pub fn close(self) {
match self.conn.close() { match self.conn.close() {
Ok(_) => { }, Ok(_) => {}
Err(_) => {} Err(_) => {}
} }
} }
@ -105,10 +109,12 @@ impl Db {
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
.query_map(&[(":path", &db_path)], |row| Ok(row.get(0)?))
.unwrap();
let mut rowid: usize = 0; let mut rowid: usize = 0;
for tr in track_iter { for tr in track_iter {
rowid = tr.unwrap(); rowid = tr.unwrap();
@ -145,7 +151,7 @@ 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(_) => {}
} }
} }
@ -153,9 +159,7 @@ impl Db {
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-existant 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)?,))
}).unwrap();
let mut to_remove: Vec<String> = Vec::new(); 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;
@ -163,7 +167,7 @@ impl Db {
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) {
@ -198,9 +202,14 @@ 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]) { match self
Ok(_) => { }, .conn
Err(e) => { log::error!("Failed to remove '{}' - {}", t, e) } .execute("DELETE FROM Tracks WHERE File = ?;", params![t])
{
Ok(_) => {}
Err(e) => {
log::error!("Failed to remove '{}' - {}", t, e)
}
} }
} }
let count_now = self.get_track_count(); let count_now = self.get_track_count();
@ -213,9 +222,7 @@ 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)?)
}).unwrap();
let mut count: usize = 0; let mut count: usize = 0;
for tr in track_iter { for tr in track_iter {
count = tr.unwrap(); count = tr.unwrap();
@ -229,11 +236,14 @@ impl Db {
if total > 0 { if total > 0 {
let pb = ProgressBar::new(total.try_into().unwrap()); let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar() let style = 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); pb.set_style(style);
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
.query_map([], |row| {
Ok(FileMetadata { Ok(FileMetadata {
rowid: row.get(0)?, rowid: row.get(0)?,
file: row.get(1)?, file: row.get(1)?,
@ -244,7 +254,8 @@ impl Db {
genre: row.get(6)?, genre: row.get(6)?,
duration: row.get(7)?, duration: row.get(7)?,
}) })
}).unwrap(); })
.unwrap();
let mut updated = 0; let mut updated = 0;
for tr in track_iter { for tr in track_iter {
@ -256,7 +267,7 @@ impl Db {
album_artist: dbtags.album_artist.unwrap_or(String::new()), album_artist: dbtags.album_artist.unwrap_or(String::new()),
album: dbtags.album.unwrap_or(String::new()), album: dbtags.album.unwrap_or(String::new()),
genre: dbtags.genre.unwrap_or(String::new()), genre: dbtags.genre.unwrap_or(String::new()),
duration:dbtags.duration duration: dbtags.duration,
}; };
pb.set_message(format!("{}", dbtags.file)); pb.set_message(format!("{}", dbtags.file));
@ -265,9 +276,20 @@ impl Db {
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.title.is_empty()
&& ftags.artist.is_empty()
&& ftags.album_artist.is_empty()
&& ftags.album.is_empty()
&& ftags.genre.is_empty()
{
log::error!("Failed to read tags of '{}'", dbtags.file); 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.duration != dtags.duration
|| ftags.title != dtags.title
|| ftags.artist != dtags.artist
|| ftags.album_artist != dtags.album_artist
|| ftags.album != dtags.album
|| ftags.genre != dtags.genre
{
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;", 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; },
@ -286,8 +308,10 @@ impl Db {
pub fn clear_ignore(&self) { pub fn clear_ignore(&self) {
match self.conn.execute("UPDATE Tracks SET Ignore=0;", []) { match self.conn.execute("UPDATE Tracks SET Ignore=0;", []) {
Ok(_) => { }, Ok(_) => {}
Err(e) => { log::error!("Failed clear Ignore column. {}", e); } Err(e) => {
log::error!("Failed clear Ignore column. {}", e);
}
} }
} }
@ -295,14 +319,24 @@ impl Db {
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), []) { match self
Ok(_) => { }, .conn
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); } .execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), [])
{
Ok(_) => {}
Err(e) => {
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), []) { match 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); } [],
) {
Ok(_) => {}
Err(e) => {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
} }
} }
} }

View File

@ -19,7 +19,6 @@ 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";
@ -37,7 +36,9 @@ fn main() {
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) => {
music_path = String::from(path.join("Music").to_string_lossy());
}
None => {} None => {}
} }
@ -45,8 +46,14 @@ fn main() {
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 containg 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,25 +61,67 @@ 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() {
@ -80,7 +129,12 @@ fn main() {
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) => {
music_paths.push(PathBuf::from(&val));
}
None => {} None => {}
} }
} }
match config.get(TOP_LEVEL_INI_TAG, "db") { match config.get(TOP_LEVEL_INI_TAG, "db") {
Some(val) => { db_path = val; }, Some(val) => {
db_path = val;
}
None => {} None => {}
} }
match config.get(TOP_LEVEL_INI_TAG, "lms") { match config.get(TOP_LEVEL_INI_TAG, "lms") {
Some(val) => { lms_host = val; }, Some(val) => {
lms_host = val;
}
None => {} None => {}
} }
match config.get(TOP_LEVEL_INI_TAG, "ignore") { match config.get(TOP_LEVEL_INI_TAG, "ignore") {
Some(val) => { ignore_file = val; }, Some(val) => {
ignore_file = val;
}
None => {} 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);
} }
} }

View File

@ -1,3 +1,4 @@
use crate::db;
/** /**
* Analyse music with Bliss * Analyse music with Bliss
* *
@ -5,12 +6,10 @@
* GPLv3 license. * GPLv3 license.
* *
**/ **/
use lofty::{Accessor, ItemKey, Probe}; 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;
@ -21,7 +20,7 @@ pub fn read(track:&String) -> db::Metadata {
album: String::new(), album: String::new(),
album_artist: String::new(), album_artist: String::new(),
genre: String::new(), genre: String::new(),
duration:180 duration: 180,
}; };
let path = Path::new(track); let path = Path::new(track);
match Probe::open(path) { match Probe::open(path) {
@ -36,7 +35,10 @@ pub fn read(track:&String) -> db::Metadata {
meta.title = tag.title().unwrap_or("").to_string(); meta.title = tag.title().unwrap_or("").to_string();
meta.artist = tag.artist().unwrap_or("").to_string(); meta.artist = tag.artist().unwrap_or("").to_string();
meta.album = tag.album().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.album_artist = tag
.get_string(&ItemKey::AlbumArtist)
.unwrap_or("")
.to_string();
meta.genre = tag.genre().unwrap_or("").to_string(); meta.genre = tag.genre().unwrap_or("").to_string();
// Check whether MP3 as numeric genre, and if so covert to text // Check whether MP3 as numeric genre, and if so covert to text
if file.file_type().eq(&lofty::FileType::MP3) { if file.file_type().eq(&lofty::FileType::MP3) {
@ -49,38 +51,43 @@ pub fn read(track:&String) -> db::Metadata {
if idx < MAX_GENRE_VAL { if idx < MAX_GENRE_VAL {
meta.genre = lofty::id3::v1::GENRES[idx].to_string(); meta.genre = lofty::id3::v1::GENRES[idx].to_string();
} }
}, }
Err(_) => { Err(_) => {
// Check for "(number)text" // Check for "(number)text"
let re = Regex::new(r"^\([0-9]+\)").unwrap(); let re = Regex::new(r"^\([0-9]+\)").unwrap();
if re.is_match(&genre) { if re.is_match(&genre) {
match genre.find(")") { match genre.find(")") {
Some(end) => { Some(end) => {
let test = &genre.to_string().substring(1, end).parse::<u8>(); let test = &genre
.to_string()
.substring(1, end)
.parse::<u8>();
match test { match test {
Ok(val) => { Ok(val) => {
let idx: usize = *val as usize; let idx: usize = *val as usize;
if idx < MAX_GENRE_VAL { if idx < MAX_GENRE_VAL {
meta.genre=lofty::id3::v1::GENRES[idx].to_string(); meta.genre = lofty::id3::v1::GENRES
[idx]
.to_string();
}
} }
},
Err(_) => {} Err(_) => {}
} }
}, }
None => {} None => {}
} }
} }
} }
} }
}, }
None => {} None => {}
} }
} }
meta.duration = file.properties().duration().as_secs() as u32; meta.duration = file.properties().duration().as_secs() as u32;
}, }
Err(_) => {} Err(_) => {}
} }
}, }
Err(_) => {} Err(_) => {}
} }
meta meta

View File

@ -5,26 +5,27 @@
* 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) { match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req) {
Ok(_) => { }, Ok(_) => {}
Err(e) => { log::error!("Failed to ask plugin to stop mixer. {}", e); } Err(e) => {
log::error!("Failed to ask plugin to stop mixer. {}", e);
}
} }
} }
@ -36,10 +37,8 @@ pub fn upload_db(db_path:&String, lms:&String) {
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) => {
match text.find("\"port\":") {
Some(s) => { Some(s) => {
let txt = text.to_string().substring(s + 7, text.len()).to_string(); let txt = text.to_string().substring(s + 7, text.len()).to_string();
match txt.find("}") { match txt.find("}") {
@ -49,18 +48,22 @@ pub fn upload_db(db_path:&String, lms:&String) {
match test { match test {
Ok(val) => { Ok(val) => {
port = 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));
@ -74,28 +77,27 @@ pub fn upload_db(db_path:&String, lms:&String) {
// 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) => { Err(e) => {
fail(&format!("Failed to open database. {}", 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));