mirror of
https://github.com/CDrummond/bliss-analyser.git
synced 2026-10-22 22:32:08 +03:00
8 files changed
+415
-288
No files matched your search
Generated
+7
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user