33 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
Craig Drummond 05532ec6cd Fix removing old tracks when run under Windows. 2022-03-07 20:27:31 +00:00
Craig Drummond ee468e723e Only try to remove files if there are some to remove 2022-03-07 20:15:21 +00:00
Craig Drummond 7996d9a525 If fail to remove old tracks from DB, then output more info. 2022-03-07 19:49:05 +00:00
Craig Drummond a35140c10e Use newer version of tag reader library. 2022-03-07 19:48:18 +00:00
Craig Drummond 2cb17e4dd4 Detail how to exclude specific genre, even if its not the only genre of
a track.
2022-03-06 11:55:35 +00:00
Craig Drummond e60a3c7b09 Update ignore syntax to allow adding SQL WHERE clauses. 2022-03-06 11:31:02 +00:00
Craig Drummond 6f8f3d52a9 Actually items in INI override commandline params 2022-03-06 10:58:18 +00:00
Craig Drummond 2771df83db Use ubuntu-20.04 to have consistent builds incase ubuntu-latest changes,
which might also change ffmpeg version.
2022-03-05 23:35:42 +00:00
Craig Drummond c5d3b37f67 Package vcruntime140.dll with Windows ZIP. 2022-03-05 23:22:28 +00:00
10 changed files with 865 additions and 111 deletions

No files matched your search

+6 -2
View File
@@ -8,7 +8,7 @@ env:
jobs:
Linux:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
@@ -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,9 +98,11 @@ 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
cp c:\Windows\system32\vcruntime140.dll releases
- name: Upload artifacts
uses: actions/upload-artifact@v2
Generated
+300 -10
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.1"
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"
@@ -686,9 +731,8 @@ dependencies = [
[[package]]
name = "lofty"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b98a3a1ca2f3054b802e5012e5800674d37dc4af7f99f3037fb57af6a2aeaa"
version = "0.5.3"
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]]
@@ -889,9 +933,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "ogg_pager"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4507172566b8e1b4d4e3da7f33453055a223fdcf96bc7c5e5d67168f1c4b241b"
checksum = "2a846c4c0578b95d4568e6f48210dd6045e7a9572af2b020062d06b6dea049f4"
dependencies = [
"byteorder",
]
@@ -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.1"
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.0"
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"
+19
View File
@@ -1,3 +1,22 @@
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.
2. Update user docs.
3. Update ignore syntax to allow adding SQL WHERE clauses.
4. Use newer version of tag reader library.
5. If fail to remove old tracks from DB, then output more info.
6. Fix removing old tracks when run under Windows.
0.0.1
-----
1. Initial release.
+40 -12
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
@@ -110,9 +111,6 @@ when uploading the database file to LMS. This defaults to `127.0.0.1`
* `ignore` specifies the name and location of a file containing items to ignore
in mixes. See the `Ignore` section later on for more details.
All configuration items can be supplied on the command-line, and if so those
specified override those from the config file.
Command-line parameters
@@ -135,8 +133,8 @@ tracks are to be analysed and how many old tracks are left in the database.
* `-L` / `--lms` Hostname, or IP address, of your LMS server.
* `-n` / `--numtracks` Specify maximum number of tracks to analyse.
If any of these are used, then they will override their equivalent from the INI
config file.
Equivalent items specied in the INI config file (detailed above) will override
any specified on the commandline.
`bliss-analyser` requires one extra parameter, which is used to determine the
required task. This takes the following values:
@@ -177,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
---------------
@@ -247,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
@@ -261,19 +273,35 @@ accomplished be setting the `Ignore` column to `1` for such tracks. To make this
easier, `bliss-analyser` can read a text file containing items to ignore and
will update the database as appropriate.
This `ignore` file is a plain text file where each line contains the unique
path to be ignored. i.e. it could contain the complete path (relative to your
music folder) of a track, an album name (to exclude a whole album), or an artist
name (to exclude all tracks by the artist). e.g.
This `ignore` file is a plain text file where each line contains either:
1. The unique path to be ignored. i.e. it could contain the complete path
(relative to your music folder) of a track, an album name (to exclude a whole
album), or an artist name (to exclude all tracks by the artist).
2. An SQL selector. If so, line must start "SQL:" followed by code that will be
run after WHERE
```
ABBA/Gold - Greatest Hits/01 Dancing Queen.mp3
AC-DC/Power Up/
The Police/
SQL:Genre='Blues'
SQL:Genre LIKE '%Dance%'
SQL:Genre='Rock'
SQL:Genre LIKE 'Rock;%'
SQL:Genre LIKE '%;Rock'
SQL:Genre LIKE '%;Rock;%'
```
This would exclude 'Dancing Queen' by ABBA, all of AC/DC's 'Power Up', and all
tracks by 'The Police'
This would exclude 'Dancing Queen' by ABBA, all of AC/DC's 'Power Up', all
tracks by 'The Police', all tracks with 'Genre' set to 'Blues', and all tracks
that have 'Dance' or 'Rock' as part of their 'Genre'.
The SQL LIKE lines do sub-string matching. So '%Dance%' will match any genre
string that contains 'Dance' - e.g. 'Classical Dance'. The 4 lines with 'Rock'
show how you can explicitly look for an exact match. The 1st line means 'Rock'
is the only genre, 2nd means 'Rock' is the first genre, 3rd means 'Rock' is the
last genre, and 4th means 'Rock' is amongs other genres.
Assuming `config.ini` is in the current folder and contains valid entries, this
is accomplished as follows:
+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
}
+86 -36
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| {
@@ -158,21 +159,53 @@ impl Db {
let mut to_remove:Vec<String> = Vec::new();
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() {
to_remove.push(db_path);
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);
}
}
log::info!("Num non-existant tracks: {}", to_remove.len());
if !dry_run {
for t in to_remove {
match self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]) {
Ok(_) => { },
Err(_) => { }
let num_to_remove = to_remove.len();
log::info!("Num non-existant tracks: {}", num_to_remove);
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);
}
}
}
@@ -191,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());
@@ -216,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);
@@ -249,11 +291,19 @@ impl Db {
}
}
pub fn set_ignore(&self, like:&str) {
log::info!("Ignore: {}", like);
match self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", like), []) {
Ok(_) => { },
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", like, e); }
pub fn set_ignore(&self, line:&str) {
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); }
}
} 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); }
}
}
}
}
+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);
}
}
}