24 Commits
Author SHA1 Message Date
Craig Drummond 0153b85b65 Track limit should span normal & cue and over folders 2022-03-16 15:34:23 +00:00
Craig Drummond 311c5e4242 Only output music path if more than 1 configured 2022-03-16 10:40:30 +00:00
Craig Drummond 4f7850bc40 Merge branch 'master' into multi-folder 2022-03-16 10:37:28 +00:00
Craig Drummond e88c4446d2 Update 2022-03-16 10:30:06 +00:00
Craig Drummond 4addd27329 Revert back to x86 only mac binaries 2022-03-16 09:54:15 +00:00
Craig Drummond 0db7d882c2 Build fat binary for macOS. 2022-03-16 08:55:29 +00:00
Craig Drummond 7a6317b28c Use git version of tag reader library. 2022-03-15 20:17:54 +00:00
Craig Drummond 7998278c9b Sort paths 2022-03-15 16:37:40 +00:00
CDrummond 08e43bf609 When performing a dry-run analysis (--dry-run) print paths of all tracks to be analysed and to be removed. 2022-03-15 12:08:15 +00:00
CDrummond 302b0f9483 Output list of (up to 100) tracks that failed to analyse. 2022-03-15 12:07:24 +00:00
CDrummond a6284c3761 Add support for multiple music folders 2022-03-14 11:24:16 +00:00
Craig Drummond cf729aaef3 Simplify 2022-03-13 11:47:01 +00:00
Craig Drummond 2ff95c27c5 OK, ffmpeg needs a file extension 2022-03-13 11:39:52 +00:00
Craig Drummond 4c40a0b41b Dont transcode CUE tracks to MP3, just copy in original format - *much*
faster
2022-03-13 11:32:35 +00:00
Craig Drummond 649ce31907 Add some info on CUE file handling 2022-03-13 11:13:01 +00:00
Craig Drummond 6d53ac16a9 Make bliss-analyser in linux and mac zips executable 2022-03-13 11:06:46 +00:00
Craig Drummond a859f7ac0f If album missing, then set to audio filename (without extension) 2022-03-13 10:41:36 +00:00
Craig Drummond f3a86414dc If track artist is empty then set this to album_artist (if it is not
also empty)
2022-03-13 10:28:46 +00:00
Craig Drummond e8614340f0 Another attempt to fix terminal echo 2022-03-13 08:26:15 +00:00
Craig Drummond b361948396 Copy ffmpeg.exe into windows zip, as will be needed for cue files 2022-03-13 07:55:39 +00:00
Craig Drummond 25c01f4434 Number cue tracks from 1, should match LMS track number 2022-03-13 06:54:09 +00:00
Craig Drummond 5fef00c24a No tag error for cue 2022-03-12 23:44:36 +00:00
Craig Drummond ef772ad30f Fix ffmpeg beaking terminal echo 2022-03-12 23:43:05 +00:00
Craig Drummond caa77bd847 Add support for analysing cue tracks 2022-03-12 23:07:56 +00:00
10 changed files with 810 additions and 95 deletions

No files matched your search

+4 -1
View File
@@ -33,6 +33,7 @@ jobs:
name: bliss-analyser-linux
path: releases/
macOS:
runs-on: macos-11.0
@@ -47,7 +48,7 @@ jobs:
- name: Install deps
run: |
brew install ffmpeg@5
brew install ffmpeg@5
- name: Build
run: |
@@ -65,6 +66,7 @@ jobs:
name: bliss-analyser-mac
path: releases/
Windows:
runs-on: windows-2019
@@ -96,6 +98,7 @@ jobs:
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
cp deps/bin/*.dll releases/
cp deps/bin/ffmpeg.exe releases/
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/windows.ini releases/config.ini
Generated
+297 -7
View File
@@ -69,6 +69,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base-x"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.13.0"
@@ -113,7 +119,7 @@ dependencies = [
[[package]]
name = "bliss-analyser"
version = "0.0.2"
version = "0.1.0"
dependencies = [
"anyhow",
"argparse",
@@ -122,12 +128,17 @@ dependencies = [
"configparser",
"dirs",
"env_logger",
"hhmmss",
"indicatif",
"lofty",
"log",
"num_cpus",
"rcue",
"regex",
"rusqlite",
"subprocess",
"substring",
"tempdir",
"ureq",
]
@@ -254,7 +265,7 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"time 0.1.44",
"winapi",
]
@@ -294,6 +305,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "const_fn"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@@ -406,6 +423,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "discard"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "either"
version = "1.6.1"
@@ -496,6 +519,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "generic-array"
version = "0.12.4"
@@ -579,6 +608,16 @@ dependencies = [
"libc",
]
[[package]]
name = "hhmmss"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a3a7d0916cb01ef108a66108640419767991ea31d11a1c851bed37686a6062"
dependencies = [
"chrono",
"time 0.2.27",
]
[[package]]
name = "humantime"
version = "2.1.0"
@@ -627,6 +666,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "jobserver"
version = "0.1.24"
@@ -687,8 +732,7 @@ dependencies = [
[[package]]
name = "lofty"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc751ba10f732cf5c522b9bc887027c9eee277512a38a3a0c4f1806bdcd730d"
source = "git+https://github.com/Serial-ATA/lofty-rs?rev=45182b6#45182b6e3d71f9b9cc022aca9e3e192abb82ad10"
dependencies = [
"base64",
"byteorder",
@@ -800,7 +844,7 @@ dependencies = [
"noisy_float",
"num-integer",
"num-traits",
"rand",
"rand 0.8.5",
]
[[package]]
@@ -996,6 +1040,12 @@ dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.36"
@@ -1027,6 +1077,19 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand"
version = "0.8.5"
@@ -1035,7 +1098,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_core 0.6.3",
]
[[package]]
@@ -1045,9 +1108,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.3",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rand_core"
version = "0.6.3"
@@ -1088,6 +1166,20 @@ dependencies = [
"num_cpus",
]
[[package]]
name = "rcue"
version = "0.1.0"
source = "git+https://github.com/gyng/rcue#9ecd1ccbb764acfb7d54c3395525a4c85b43daea"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
@@ -1122,6 +1214,15 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
@@ -1181,6 +1282,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "5.1.1"
@@ -1207,6 +1317,12 @@ dependencies = [
"webpki",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -1223,6 +1339,49 @@ dependencies = [
"untrusted",
]
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha-1"
version = "0.8.2"
@@ -1235,6 +1394,21 @@ dependencies = [
"opaque-debug 0.2.3",
]
[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "shlex"
version = "1.1.0"
@@ -1253,6 +1427,64 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "standback"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
dependencies = [
"version_check",
]
[[package]]
name = "stdweb"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
dependencies = [
"discard",
"rustc_version",
"stdweb-derive",
"stdweb-internal-macros",
"stdweb-internal-runtime",
"wasm-bindgen",
]
[[package]]
name = "stdweb-derive"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn",
]
[[package]]
name = "stdweb-internal-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
dependencies = [
"base-x",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"serde_json",
"sha1",
"syn",
]
[[package]]
name = "stdweb-internal-runtime"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "strength_reduce"
version = "0.2.3"
@@ -1277,6 +1509,16 @@ dependencies = [
"syn",
]
[[package]]
name = "subprocess"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "055cf3ebc2981ad8f0a5a17ef6652f652d87831f79fddcba2ac57bcb9a0aa407"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "substring"
version = "1.4.5"
@@ -1297,6 +1539,16 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand 0.4.6",
"remove_dir_all",
]
[[package]]
name = "termcolor"
version = "1.1.2"
@@ -1347,6 +1599,44 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
dependencies = [
"const_fn",
"libc",
"standback",
"stdweb",
"time-macros",
"version_check",
"winapi",
]
[[package]]
name = "time-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
dependencies = [
"proc-macro-hack",
"time-macros-impl",
]
[[package]]
name = "time-macros-impl"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"standback",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
+7 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "bliss-analyser"
version = "0.0.2"
version = "0.1.0"
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
edition = "2018"
license = "GPL-3.0-only"
@@ -17,10 +17,15 @@ rusqlite = { version = "0.25.0", features = ["bundled"] }
log = "0.4.14"
env_logger = "0.8.4"
indicatif = "0.16.2"
lofty = "0.5.3"
lofty = { git = "https://github.com/Serial-ATA/lofty-rs", rev = "45182b6" }
dirs = "1"
chrono = "0.4.19"
regex = "1"
substring = "1.4.5"
ureq = "2.4.0"
configparser = "3.0.0"
rcue = { git = "https://github.com/gyng/rcue" }
hhmmss = "0.1.0"
num_cpus = "1.13.0"
tempdir = "0.3.7"
subprocess = "0.2.8"
+10
View File
@@ -1,3 +1,13 @@
0.1.0
-----
1. Add support for analysing CUE files.
2. Output list of (up to 100) tracks that failed to analyse.
3. When performing a dry-run analysis (--dry-run) print paths of all tracks to
be analysed and to be removed.
4. Use git version of tag reader library.
5. Support up to 5 music folders (music, music_1, music_2, music_3, and
music_4).
0.0.2
-----
1. Package vcruntime140.dll with Windows ZIP.
+16 -1
View File
@@ -102,7 +102,8 @@ ignore=ignore.txt
The following items are supported:
* `music` specifies the location of your music collection - e.g. `c:\Users\user\Music`
for windows. This default to `Music` within the user's home folder.
for windows. This default to `Music` within the user's home folder. Up to 4 other
music folders may be specified via `music_1`, `music_2`, `music_3`, and `music_4`
* `db` specifies the name and location of the database file used to store the
analysis results. This will default to `bliss.db` in the current folder.
* `lms` specifies the hostname, or IP address, of your LMS server. This is used
@@ -174,6 +175,16 @@ As a rough guide, a 2015-era i7 8-core laptop with SSD analyses around 14000
tracks/hour.
CUE files
---------
If the anlyser encounters an audio file with a matching CUE file (e.g.
`album.flac` and `album.cue` in same folder) then it will attempt to analyse the
individual tracks contained within. To do this the analyser uses `ffmpeg` to
create temporary files of each track, which are then analysed. (These temporary
files are removed afterwards).
Exclude folders
---------------
@@ -244,6 +255,10 @@ entries):
.\bliss-analyser.exe tags
```
*NOTE* Tag re-reading is not implemented for CUE tracks. Therefore if you update
your CUE files you will need to remove these from the database and re-analyse,
or manually edit the meta-data in `bliss.db` using an SQLite GUI.
Ignoring tracks in mixes
+16 -2
View File
@@ -7,7 +7,7 @@
# MIT license.
#
import datetime, os, requests, sys, time
import datetime, os, requests, shutil, subprocess, sys, tempfile, time
GITHUB_TOKEN_FILE = "%s/.config/github-token" % os.path.expanduser('~')
GITHUB_REPO = "CDrummond/bliss-analyser"
@@ -67,6 +67,20 @@ def download_artifacts(version):
break
def make_executable(version):
cwd = os.getcwd()
for a in ["bliss-analyser-linux", "bliss-analyser-mac"]:
archive = "%s-%s.zip" % (a, version)
info("Making analyser executable in %s" % archive)
with tempfile.TemporaryDirectory() as td:
subprocess.call(["unzip", archive, "-d", td], shell=False)
os.remove(archive)
os.chdir(td)
subprocess.call(["chmod", "a+x", "%s/bliss-analyser" % td], shell=False)
shutil.make_archive("%s/%s-%s" % (cwd, a, version), "zip")
os.chdir(cwd)
def checkVersion(version):
try:
parts=version.split('.')
@@ -85,4 +99,4 @@ if version!="test":
checkVersion(version)
download_artifacts(version)
make_executable(version)
+269 -31
View File
@@ -7,19 +7,30 @@
**/
use anyhow::{Result};
use bliss_audio::{library::analyze_paths_streaming};
use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song};
use hhmmss::Hhmmss;
use indicatif::{ProgressBar, ProgressStyle};
use std::convert::TryInto;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::time::Duration;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
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_TAG_ERRORS_TO_SHOW:usize = 25;
const MAX_ERRORS_TO_SHOW:usize = 100;
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>) {
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) => {
@@ -33,7 +44,7 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut
if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
} else {
get_file_list(db, mpath, &entry.path(), track_paths);
get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks);
}
} else if entry.path().is_file() {
let e = pb.extension();
@@ -42,10 +53,28 @@ fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut
if ext=="m4a" || ext=="mp3" || ext=="ogg" || ext=="flac" || ext=="opus" {
match pb.strip_prefix(mpath) {
Ok(stripped) => {
let mut cue = pb.clone();
cue.set_extension("cue");
if cue.exists() {
log::warn!("Found CUE album '{}' - not currently handled!", pb.to_string_lossy());
let mut cue_file = pb.clone();
cue_file.set_extension("cue");
if cue_file.exists() {
// Found a CUE file, try to parse and then check if tracks exists in DB
let this_cue_tracks = cue::parse(&pb, &cue_file);
for track in this_cue_tracks {
match track.track_path.strip_prefix(mpath) {
Ok(tstripped) => {
let spb = tstripped.to_path_buf();
let sname = String::from(spb.to_string_lossy());
match db.get_rowid(&sname) {
Ok(id) => {
if id<=0 {
cue_tracks.push(track.clone());
}
},
Err(_) => { }
}
},
Err(_) => { }
}
}
} else {
let spb = stripped.to_path_buf();
let sname = String::from(spb.to_string_lossy());
@@ -84,7 +113,7 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
let results = analyze_paths_streaming(track_paths)?;
let mut analysed = 0;
let mut failed = 0;
let mut failed:Vec<String> = Vec::new();
let mut tag_error:Vec<String> = Vec::new();
log::info!("Analysing new tracks");
@@ -105,13 +134,25 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
},
Err(_) => {
failed += 1;
Err(e) => {
failed.push(format!("{} - {}", sname, e));
}
};
pb.inc(1);
}
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed));
pb.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):");
for err in failed {
log::error!(" {}", err);
}
if total>MAX_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
}
}
if !tag_error.is_empty() {
let total = tag_error.len();
tag_error.truncate(MAX_TAG_ERRORS_TO_SHOW);
@@ -127,40 +168,237 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
Ok(())
}
pub fn analyse_files(db_path: &str, mpath: &PathBuf, dry_run:bool, keep_old:bool, max_num_tracks:usize) {
let mut track_paths:Vec<String> = Vec::new();
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);
#[allow(clippy::type_complexity)]
let (tx, rx): (
Sender<(cue::CueTrack, BlissResult<Song>)>,
Receiver<(cue::CueTrack, BlissResult<Song>)>,
) = mpsc::channel();
if tracks.is_empty() {
return Ok(rx);
}
let mut handles = Vec::new();
let mut chunk_length = tracks.len() / num_cpus;
if chunk_length == 0 {
chunk_length = tracks.len();
} else if chunk_length == 1 && tracks.len() > num_cpus {
chunk_length = 2;
}
for chunk in tracks.chunks(chunk_length) {
let tx_thread = tx.clone();
let owned_chunk = chunk.to_owned();
let child = thread::spawn(move || {
let mut idx = 0;
match &TempDir::new("bliss") {
Ok(dir) => {
for 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());
let mut tmp_file = PathBuf::from(dir.path());
if ext.is_some() {
tmp_file.push(format!("{}.{}", idx, ext.unwrap().to_string_lossy()));
} else {
tmp_file.push(format!("{}.flac", idx));
}
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")
.arg(String::from(tmp_file.to_string_lossy()))
.stderr(NullFile)
.join() {
Ok(_) => { },
Err(e) => { 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(_) => { }
}
}
if tmp_file.exists() {
log::debug!("Analyzing '{}'", track_path);
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 {
tx_thread.send((cue_track, song)).unwrap();
}
match fs::remove_file(tmp_file) {
Ok(_) => { },
Err(_) => { }
}
} else {
log::error!("Failed to create temp file");
}
}
},
Err(e) => { log::error!("Failed to create temp folder. {}", e); }
}
});
handles.push(child);
}
Ok(rx)
}
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()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
let results = analyze_cue_streaming(cue_tracks)?;
let mut analysed = 0;
let mut failed:Vec<String> = Vec::new();
log::info!("Analysing new cue tracks");
for (track, result) in results {
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));
match result {
Ok(song) => {
let meta = db::Metadata {
title:track.title,
artist:track.artist,
album_artist:track.album_artist,
album:track.album,
genre:track.genre,
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);
}
pb.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):");
for err in failed {
log::error!(" {}", err);
}
if total>MAX_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
}
}
Ok(())
}
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 cur = PathBuf::from(mpath);
let mut track_count_left = max_num_tracks;
db.init();
log::info!("Looking for new tracks");
get_file_list(&mut db, mpath, &cur, &mut track_paths);
log::info!("Num new tracks: {}", track_paths.len());
if !dry_run && max_num_tracks>0 && track_paths.len()>max_num_tracks {
log::info!("Only analysing {} tracks", max_num_tracks);
track_paths.truncate(max_num_tracks);
}
if !keep_old {
db.remove_old(mpath, dry_run);
db.remove_old(mpaths, dry_run);
}
if !dry_run {
if track_paths.len()>0 {
match analyse_new_files(&db, mpath, track_paths) {
Ok(_) => { },
Err(_) => { }
for path in mpaths {
let mpath = path.clone();
let cur = path.clone();
let mut track_paths:Vec<String> = Vec::new();
let mut cue_tracks:Vec<cue::CueTrack> = Vec::new();
if mpaths.len()>1 {
log::info!("Looking for new tracks in {}", mpath.to_string_lossy());
} else {
log::info!("Looking for new tracks");
}
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks);
track_paths.sort();
log::info!("Num new tracks: {}", track_paths.len());
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:");
for track in track_paths {
log::info!(" {}", track);
}
for track in cue_tracks {
log::info!(" {}", track.track_path.to_string_lossy());
}
}
} else {
log::info!("No new tracks to analyse");
if max_num_tracks>0 {
if track_paths.len()>track_count_left {
log::info!("Only analysing {} tracks", track_count_left);
track_paths.truncate(track_count_left);
}
track_count_left -= track_paths.len();
}
if max_num_tracks>0 {
if track_count_left == 0 {
cue_tracks.clear();
} else {
if cue_tracks.len()>track_count_left {
log::info!("Only analysing {} cue tracks", track_count_left);
cue_tracks.truncate(track_count_left);
}
track_count_left -= track_paths.len();
}
}
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths) {
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 max_num_tracks>0 && track_count_left<=0 {
log::info!("Track limit reached");
break;
}
}
}
db.close();
}
pub fn read_tags(db_path: &str, mpath: &PathBuf) {
pub fn read_tags(db_path: &str, mpaths: &Vec<PathBuf>) {
let db = db::Db::new(&String::from(db_path));
db.init();
db.update_tags(&mpath);
db.update_tags(&mpaths);
db.close();
}
+96
View File
@@ -0,0 +1,96 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
extern crate rcue;
use rcue::parser::parse_from_file;
use std::path::PathBuf;
use std::time::Duration;
pub const MARKER:&str = ".CUE_TRACK.";
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
const GENRE:&str = "GENRE";
#[derive(Clone)]
pub struct CueTrack {
pub audio_path:PathBuf,
pub track_path:PathBuf,
pub title:String,
pub artist:String,
pub album:String,
pub album_artist:String,
pub genre:String,
pub start: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) => {
let album = cue.title.unwrap_or(String::new());
let album_artist = cue.performer.unwrap_or(String::new());
let mut genre = String::new();
for comment in cue.comments {
if comment.0.eq(GENRE) {
genre = comment.1;
}
}
if 1 == cue.files.len() {
for file in cue.files {
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));
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()),
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 => { }
}
}
resp.push(ctrack);
},
None => { }
}
}
}
}
},
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);
if let Some(next) = resp.get(i+1) {
next_start = next.start;
}
if let Some(elem) = resp.get_mut(i) {
(*elem).duration = next_start - elem.start;
}
}
resp
}
+69 -35
View File
@@ -10,8 +10,9 @@ use bliss_audio::{Analysis, AnalysisIndex};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::{Connection, params};
use std::convert::TryInto;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::process;
use crate::cue;
use crate::tags;
pub struct FileMetadata {
@@ -134,7 +135,7 @@ impl Db {
Err(e) => { log::error!("Failed to insert '{}' into database. {}", path, e); }
}
} else {
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=?, Tempo=?, Zcr=?, MeanSpectralCentroid=?, StdDevSpectralCentroid=?, MeanSpectralRolloff=?, StdDevSpectralRolloff=?, MeanSpectralFlatness=?, StdDevSpectralFlatness=?, MeanLoudness=?, StdDevLoudness=?, Chroma1=?, Chroma2=?, Chroma3=?, Chroma4=?, Chroma5=?, Chroma6=?, Chroma7=?, Chroma8=?, Chroma9=?, Chroma10=? WHERE rowid=?);",
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=?, Tempo=?, Zcr=?, MeanSpectralCentroid=?, StdDevSpectralCentroid=?, MeanSpectralRolloff=?, StdDevSpectralRolloff=?, MeanSpectralFlatness=?, StdDevSpectralFlatness=?, MeanLoudness=?, StdDevLoudness=?, Chroma1=?, Chroma2=?, Chroma3=?, Chroma4=?, Chroma5=?, Chroma6=?, Chroma7=?, Chroma8=?, Chroma9=?, Chroma10=? WHERE rowid=?;",
params![meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration,
analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff],
analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness],
@@ -149,7 +150,7 @@ impl Db {
}
}
pub fn remove_old(&self, mpath:&Path, dry_run:bool) {
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run:bool) {
log::info!("Looking for non-existant tracks");
let mut stmt = self.conn.prepare("SELECT File FROM Tracks;").unwrap();
let track_iter = stmt.query_map([], |row| {
@@ -159,29 +160,53 @@ impl Db {
for tr in track_iter {
let mut db_path:String = tr.unwrap().0;
let orig_path = db_path.clone();
match orig_path.find(cue::MARKER) {
Some(s) => {
db_path.truncate(s);
},
None => { }
}
if cfg!(windows) {
db_path = db_path.replace("/", "\\");
}
let path = mpath.join(PathBuf::from(db_path.clone()));
let mut exists = false;
if !path.exists() {
for mpath in mpaths {
let path = mpath.join(PathBuf::from(db_path.clone()));
//log::debug!("Check if '{}' exists.", path.to_string_lossy());
if path.exists() {
exists = true;
break;
}
}
if !exists {
to_remove.push(orig_path);
}
}
let num_to_remove = to_remove.len();
log::info!("Num non-existant tracks: {}", num_to_remove);
if !dry_run && num_to_remove>0 {
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) }
if num_to_remove>0 {
if dry_run {
log::info!("The following need to be removed from database:");
for t in to_remove {
log::info!(" {}", t);
}
} else {
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 count_now = self.get_track_count();
if (count_now + num_to_remove) != count_before {
log::error!("Failed to remove all tracks. Count before: {}, wanted to remove: {}, count now: {}", count_before, num_to_remove, count_now);
}
}
let count_now = self.get_track_count();
if (count_now + num_to_remove) != count_before {
log::error!("Failed to remove all tracks. Count before: {}, wanted to remove: {}, count now: {}", count_before, num_to_remove, count_now);
}
}
}
@@ -199,7 +224,7 @@ impl Db {
count
}
pub fn update_tags(&self, mpath:&PathBuf) {
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());
@@ -224,24 +249,33 @@ impl Db {
let mut updated = 0;
for tr in track_iter {
let dbtags = tr.unwrap();
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
};
pb.set_message(format!("{}", dbtags.file));
let path = String::from(mpath.join(&dbtags.file).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() {
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 {
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; },
Err(e) => { log::error!("Failed to update tags of '{}'. {}", dbtags.file, e); }
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
};
pb.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() {
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 {
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; },
Err(e) => { log::error!("Failed to update tags of '{}'. {}", dbtags.file, e); }
}
}
break;
}
}
}
pb.inc(1);
+26 -16
View File
@@ -14,6 +14,7 @@ use std::io::Write;
use std::path::PathBuf;
use std::process;
mod analyse;
mod cue;
mod db;
mod tags;
mod upload;
@@ -33,6 +34,7 @@ fn main() {
let mut task = "".to_string();
let mut lms_host = "127.0.0.1".to_string();
let mut max_num_tracks:usize = 0;
let mut music_paths:Vec<PathBuf> = Vec::new();
match dirs::home_dir() {
Some(path) => { music_path = String::from(path.join("Music").to_string_lossy()); }
@@ -89,20 +91,23 @@ fn main() {
let mut config = Ini::new();
match config.load(&config_file) {
Ok(_) => {
match config.get(TOP_LEVEL_INI_TAG, "music") {
Some(val) => { music_path = val; },
None => { }
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)); },
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 => { }
}
},
@@ -114,6 +119,10 @@ fn main() {
}
}
if music_paths.is_empty() {
music_paths.push(PathBuf::from(&music_path));
}
if task.eq_ignore_ascii_case("stopmixer") {
upload::stop_mixer(&lms_host);
} else {
@@ -136,18 +145,19 @@ fn main() {
process::exit(-1);
}
} else {
let mpath = PathBuf::from(&music_path);
if !mpath.exists() {
log::error!("Music path ({}) does not exist", music_path);
process::exit(-1);
}
if !mpath.is_dir() {
log::error!("Music path ({}) is not a directory", music_path);
process::exit(-1);
for mpath in &music_paths {
if !mpath.exists() {
log::error!("Music path ({}) does not exist", mpath.to_string_lossy());
process::exit(-1);
}
if !mpath.is_dir() {
log::error!("Music path ({}) is not a directory", mpath.to_string_lossy());
process::exit(-1);
}
}
if task.eq_ignore_ascii_case("tags") {
analyse::read_tags(&db_path, &mpath);
analyse::read_tags(&db_path, &music_paths);
} else if task.eq_ignore_ascii_case("ignore") {
let ignore_path = PathBuf::from(&ignore_file);
if !ignore_path.exists() {
@@ -160,7 +170,7 @@ fn main() {
}
analyse::update_ignore(&db_path, &ignore_path);
} else {
analyse::analyse_files(&db_path, &mpath, dry_run, keep_old, max_num_tracks);
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_tracks);
}
}
}