78 Commits
Author SHA1 Message Date
CDrummond f438239983 Update. 2025-09-25 21:17:50 +01:00
CDrummond c1ba064f48 Fix messages. 2025-09-25 21:14:06 +01:00
CDrummond 0c4335d02d Re-enable all builds but don't use docker for Linux x86/ffmpeg - as
can't get tnis to build!
2025-09-25 20:31:42 +01:00
CDrummond fe10956341 Use musl-cross toolchain?? 2025-09-25 19:28:49 +01:00
CDrummond e860048ff0 Set clang path 2025-09-25 19:21:52 +01:00
CDrummond 1428751b9e ...and others 2025-09-25 19:17:20 +01:00
CDrummond e546793864 clang 2025-09-25 19:12:46 +01:00
CDrummond e0ffdedced /bin/ash? 2025-09-25 19:08:24 +01:00
CDrummond 81f29db352 Use alpine? 2025-09-25 18:02:44 +01:00
CDrummond 35ec7695e5 Renable... 2025-09-25 17:41:55 +01:00
CDrummond 6f1abef1ad Can't get musl to work... 2025-09-25 17:35:25 +01:00
CDrummond 5168723d55 clang required? 2025-09-25 17:24:08 +01:00
CDrummond ee3b27d959 Hmmm... 2025-09-25 17:18:06 +01:00
CDrummond 0e5f4fc1d1 Hacky way to find info 2025-09-25 17:14:36 +01:00
CDrummond beb7155ae6 Symlink musl-ar??? 2025-09-25 16:58:46 +01:00
CDrummond 84766d3e4f Create dest dir! 2025-09-25 16:52:51 +01:00
CDrummond 5ec6a30104 Next try? 2025-09-25 16:44:23 +01:00
CDrummond fa61d5a6c3 Install musl! 2025-09-25 16:26:23 +01:00
CDrummond d06263efb0 Fix builds? 2025-09-25 16:16:49 +01:00
CDrummond d7da5dcd95 Use docker for Linxu builds and link to musl 2025-09-24 22:18:46 +01:00
CDrummond 849fbe92fa Use serde_json to encode message strings to be JSON safe. 2025-09-24 21:06:04 +01:00
CDrummond d42f28d1f2 Update for reduced set of builds. 2025-09-23 17:07:46 +01:00
CDrummond 0cf766cfeb Tidy up 2025-09-23 16:47:52 +01:00
CDrummond 3f997b979b Drop support for using ffmpeg via commandline. 2025-09-22 21:12:48 +01:00
CDrummond 39229efd99 Refactor max threads 2025-09-22 20:33:57 +01:00
CDrummond 50c564cde0 Add option to remove failures 2025-09-22 12:26:04 +01:00
CDrummond 0202c68579 Seprate out read and write tags options 2025-09-21 17:06:29 +01:00
CDrummond f27804a2bb Only read tags if also set to write? 2025-09-21 16:27:53 +01:00
CDrummond 9a7f316419 Change notif text 2025-09-21 16:22:21 +01:00
CDrummond bc68f73de9 Store file failures in DB so don't re-analyse unless timestamp of file changes. 2025-09-21 12:02:26 +01:00
CDrummond 5a11b8b3e5 Oops! 2025-09-21 09:44:38 +01:00
CDrummond b1508d27c5 Strip windows builds? 2025-09-21 09:33:56 +01:00
CDrummond 172bcac11a Update 2025-09-20 12:44:46 +01:00
CDrummond f6d99afd9e Just set URL 2025-09-20 12:26:30 +01:00
CDrummond 9d4db09660 Windows ffmpeg version 2025-09-20 12:10:57 +01:00
CDrummond f6d15c41c5 Re-enable more builds 2025-09-20 09:24:06 +01:00
CDrummond 30d5b15f08 Fix staticlibav 2025-09-20 08:45:03 +01:00
CDrummond 23d491db55 Multi-threaded read of existing analysis tags. 2025-09-19 18:19:51 +01:00
CDrummond 50331e518a Send FINISHED notif when complete. 2025-09-19 12:03:02 +01:00
CDrummond d046add1ea Better notifications 2025-09-18 12:22:07 +01:00
CDrummond 8a26eea76a Send notifications every 2(?) seconds 2025-09-17 21:16:45 +01:00
CDrummond cb4e7e97f9 Fix updates 2025-09-17 17:53:06 +01:00
CDrummond bb4c268f82 Add sending of notifications 2025-09-17 13:16:42 +01:00
CDrummond e7a9156289 Use latest ubuntu 2025-09-16 18:16:37 +01:00
CDrummond 8a54a38673 Use Windows 2022 2025-09-16 16:08:19 +01:00
CDrummond 6af1865691 Reason strings - not sure if correct!!! 2025-09-16 12:38:43 +01:00
CDrummond 75955c15cb Updates to support invoking from LMS plugin. 2025-09-16 12:23:07 +01:00
CDrummond 4f74a6ea58 Update deps 2025-09-16 12:22:35 +01:00
CDrummond 9709909378 Fix ffmpeg build 2025-09-16 12:22:17 +01:00
CDrummond b8a2f248af Update to newer bliss-rs, and default to Symphonia 2025-09-15 19:37:29 +01:00
CDrummond 7bcc176ac2 Set log level for tag reader library. 2025-09-12 19:37:31 +01:00
CraigD e488903d7f Merge pull request #32 from chincheta0815/docker_updates
change docker commands to get more images updates
2025-06-15 12:25:22 +00:00
CDrummond 2d2ddc7b17 Don't crash if file has no tags.
Closes #33
2025-06-15 13:21:49 +01:00
chincheta0815@nowhere.local 9e107c5fc2 change docker commands to get more images updates 2025-06-08 08:20:50 +02:00
CDrummond 4714f8f4ef Formatting 2025-05-26 17:57:25 +01:00
CDrummond fd38256dc0 Handle both BLISS_ANALYSIS and bliss_analysis
Issue #31
2025-05-26 17:38:00 +01:00
CDrummond b4eb7562cf Gracefully handle Ctrl-C. 2025-05-22 12:07:24 +01:00
CDrummond c7d436c13b Spelling 2025-05-22 12:07:04 +01:00
CDrummond e04b22ebcc Strip debug info from release binaries. 2025-05-20 12:10:00 +01:00
CDrummond 6055fa2af7 Consistent status messages 2025-05-20 12:09:40 +01:00
CDrummond 0a1b9d8790 Formatting 2025-05-20 12:09:25 +01:00
CDrummond e395ff515b Consistency 2025-05-20 12:09:11 +01:00
CDrummond 4794a9b747 Remove --read-tags and --write-tags. Always read tags, so --tags just
controls writing.
Issue #29
2025-05-20 12:08:20 +01:00
CDrummond b623c1939b No need to mention multi threaded export, as export itself is new to
this release.
2025-05-19 12:08:00 +01:00
CDrummond de232d2a33 Use max of 16 significant digits (traling zeros stripped) when writing analysis results to file tags.
Issue #26
2025-05-19 12:07:28 +01:00
CDrummond 4830ef92b3 Make constant name more meaningful 2025-05-19 12:06:36 +01:00
CDrummond 520190c52e When writing new BLISS_ANALYSIS tag remove any old comment tags.
Issue #27
2025-05-19 12:06:01 +01:00
CDrummond d4f32d8b4d Next version is 0.4.0, not 0.5.0!!! 2025-05-15 13:53:21 +01:00
CDrummond 0250117b15 Add --read-tags and --write-tags commandline options.
Closes #25
2025-05-15 12:04:49 +01:00
CDrummond d2b3db43f7 Move functions to where they belong. 2025-05-15 12:04:04 +01:00
CDrummond 2759f7f47c Multi-thread DB export.
Closes #24
2025-05-15 12:03:47 +01:00
CDrummond a79712cb24 Check for multiple genre tags, and if found add as semi-colon separated list to DB. 2025-05-14 12:23:45 +01:00
CDrummond 5722b04c85 - Store analysis in BLISS_ANALYSIS tag, not in a COMMENT tag.
- Fix read of tags during analysis phase.
Closes #23
2025-05-13 12:25:50 +01:00
CDrummond 50a55ee61a ...and again... 2025-05-01 07:16:40 +01:00
CDrummond b5c9356ef2 Fix formatting. 2025-05-01 07:15:39 +01:00
CDrummond 9d9117e8f7 Allow "export" !!! 2025-03-24 17:07:26 +00:00
CDrummond 417ac5f652 Add option to preserve file modification time when writing tags.
Issue #21
2025-03-24 07:17:51 +00:00
CDrummond 342440f04b Add action to export analysis results from DB to files.
Issue #20
2025-03-24 06:47:35 +00:00
29 changed files with 1390 additions and 1558 deletions

No files matched your search

+89 -342
View File
@@ -7,7 +7,43 @@ env:
CARGO_TERM_COLOR: always
jobs:
Linux_ffmpeg_x86:
Linux_x86_symphonia:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Build x86
run: |
docker build -t bliss-analyser-x86-symphonia - < docker/Dockerfile_x86_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-x86-symphonia
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-x86-symphonia
path: releases/
# Linux_x86_ffmpeg:
# runs-on: ubuntu-latest
#
# steps:
# - name: Checkout
# uses: actions/checkout@v1
#
# - name: Build x86
# run: |
# docker build -t bliss-analyser-x86-ffmpeg - < docker/Dockerfile_x86_ffmpeg
# docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-x86-ffmpeg
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v4
# with:
# name: bliss-analyser-linux-x86-ffmpeg
# path: releases/
Linux_x86_ffmpeg:
runs-on: ubuntu-22.04
steps:
@@ -17,9 +53,9 @@ jobs:
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build x86 ffmpeg version
- name: Build x86 static-ffmpeg version
run: |
cargo build --release --features=ffmpeg
cargo build --release --features=libav,staticlibav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
@@ -33,194 +69,17 @@ jobs:
name: bliss-analyser-linux-x86-ffmpeg
path: releases/
Linux_static_x86:
runs-on: ubuntu-22.04
Linux_arm_symphonia:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v1
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build x86 static-libav version
- name: Build ARM
run: |
cargo build --release --features=libav,staticlibav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-x86-static
path: releases/
Ubuntu_2204_libav_x86:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev ffmpeg -y
- name: Build 22.04 libav version
run: |
cargo build --release --features=libav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-ubuntu-22.04-x86-libav
path: releases/
Ubuntu_2404_libav_x86:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev ffmpeg -y
- name: Build 24.04 libav version
run: |
cargo build --release --features=libav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-ubuntu-24.04-x86-libav
path: releases/
Linux_symphonia_x86:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build x86 static-libav version
run: |
cargo build --release --features=symphonia
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-x86-symphonia
path: releases/
Linux_ffmpeg_arm:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build ARM ffmpeg on Debian
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_ffmpeg
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-arm-ffmpeg
path: releases/
Linux_static_arm:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build ARM static-libav on Debian
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_static
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-arm-static
path: releases/
Debian_libav_arm:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build ARM libav on Bullseye
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_libav
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-debian-bullseye-arm-libav
path: releases/
- name: Build ARM libav on Bookworm
run : |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bookworm_libav
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-debian-bookworm-arm-libav
path: releases/
Linux_symphonia_arm:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build ARM symphonia on Debian
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
docker build -t bliss-analyser-arm-symphonia - < docker/Dockerfile_arm_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-arm-symphonia
- name: Upload artifacts
uses: actions/upload-artifact@v4
@@ -228,138 +87,24 @@ jobs:
name: bliss-analyser-linux-arm-symphonia
path: releases/
macOS_ffmpeg:
runs-on: macos-13
Linux_arm_ffmpeg:
runs-on: ubuntu-latest
steps:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v1
- name: Install Rust support for ARM64 & prepare environment
- name: Build ARM
run: |
rustup target add aarch64-apple-darwin
mkdir releases
- name: Build
run: |
cargo build --release --features ffmpeg,update-aubio-bindings
strip target/release/bliss-analyser
cp target/release/bliss-analyser releases/bliss-analyser-x86_64
cargo build --target=aarch64-apple-darwin --release --features ffmpeg,update-aubio-bindings
strip target/aarch64-apple-darwin/release/bliss-analyser
cp target/aarch64-apple-darwin/release/bliss-analyser releases/bliss-analyser-arm64
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/macos.ini releases/config.ini
- name: Build fat binary
run: |
lipo -create \
-arch x86_64 releases/bliss-analyser-x86_64 \
-arch arm64 releases/bliss-analyser-arm64 \
-output releases/bliss-analyser
- name: Remove unused binaries
run:
rm releases/bliss-analyser-x86_64 releases/bliss-analyser-arm64
docker build -t bliss-analyser-arm-ffmpeg - < docker/Dockerfile_arm_ffmpeg
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-arm-ffmpeg
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-mac-ffmpeg
name: bliss-analyser-linux-arm-ffmpeg
path: releases/
# macOS_staticlibav:
# runs-on: macos-13
#
# steps:
# - name: Install Rust
# uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
#
# - name: Checkout
# uses: actions/checkout@v2
#
# - name: Install deps
# run: |
# brew install pkg-config ffmpeg nasm
#
# - name: Install Rust support for ARM64 & prepare environment
# run: |
# rustup target add aarch64-apple-darwin
# mkdir releases
#
# - name: Build
# run: |
# cargo build --release --features update-aubio-bindings,libav,staticlibav
# strip target/release/bliss-analyser
# cp target/release/bliss-analyser releases/bliss-analyser-x86_64
# cargo build --target=aarch64-apple-darwin --release --features update-aubio-bindings,libav,staticlibav
# strip target/aarch64-apple-darwin/release/bliss-analyser
# cp target/aarch64-apple-darwin/release/bliss-analyser releases/bliss-analyser-arm64
# cp UserGuide.md releases/README.md
# cp LICENSE releases/
# cp configs/macos.ini releases/config.ini
#
# - name: Build fat binary
# run: |
# lipo -create \
# -arch x86_64 releases/bliss-analyser-x86_64 \
# -arch arm64 releases/bliss-analyser-arm64 \
# -output releases/bliss-analyser
#
# - name: Remove unused binaries
# run:
# rm releases/bliss-analyser-x86_64 releases/bliss-analyser-arm64
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v4
# with:
# name: bliss-analyser-mac-static
# path: releases/
# macOS_libav:
# runs-on: macos-13
#
# steps:
# - name: Install Rust
# uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
#
# - name: Checkout
# uses: actions/checkout@v2
#
# - name: Install deps
# run: |
# brew install pkg-config ffmpeg
#
# - name: Build
# run: |
# mkdir releases
# cargo build --release
# strip target/release/bliss-analyser
# cp target/release/bliss-analyser releases/bliss-analyser
# cp UserGuide.md releases/README.md
# cp LICENSE releases/
# cp configs/macos.ini releases/config.ini
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v4
# with:
# name: bliss-analyser-mac-libav
# path: releases/
macOS_symphonia:
runs-on: macos-13
@@ -411,8 +156,40 @@ jobs:
path: releases/
Windows_libav:
runs-on: windows-2019
Windows_symphonia:
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build
run: |
cargo build --release --features=symphonia
strip target/release/bliss-analyser.exe
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
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@v4
with:
name: bliss-analyser-windows-symphonia
path: releases/
Windows_ffmpeg:
runs-on: windows-2022
steps:
- name: Checkout
@@ -422,7 +199,7 @@ jobs:
run: |
$VCINSTALLDIR = $(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath)
Add-Content $env:GITHUB_ENV "LIBCLANG_PATH=${VCINSTALLDIR}\VC\Tools\LLVM\x64\bin`n"
Invoke-WebRequest "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full-shared.7z" -OutFile ffmpeg-shared.7z
Invoke-WebRequest "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1.1-full_build-shared.7z" -OutFile ffmpeg-shared.7z
7z x ffmpeg-shared.7z
mkdir deps
mv ffmpeg-*/* deps/
@@ -439,6 +216,7 @@ jobs:
- name: Build
run: |
cargo build --release --features=libav
strip target/release/bliss-analyser.exe
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
cp deps/bin/*.dll releases/
@@ -450,36 +228,5 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-windows-libav
name: bliss-analyser-windows-ffmpeg
path: releases/
Windows_symphonia:
runs-on: windows-2019
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build
run: |
cargo build --release --features=symphonia
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
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@v4
with:
name: bliss-analyser-windows-symphonia
path: releases/
Generated
+359 -180
View File
@@ -14,18 +14,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@@ -35,12 +23,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -56,6 +38,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
]
[[package]]
name = "anyhow"
version = "1.0.97"
@@ -86,17 +118,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -180,15 +201,17 @@ dependencies = [
[[package]]
name = "bliss-analyser"
version = "0.3.0"
version = "0.5.1"
dependencies = [
"anyhow",
"argparse",
"bliss-audio",
"chrono",
"configparser",
"ctrlc",
"dirs",
"env_logger",
"filetime",
"hhmmss",
"if_chain",
"indicatif",
@@ -198,6 +221,7 @@ dependencies = [
"rcue",
"regex",
"rusqlite",
"serde_json",
"substring",
"ureq",
"which 7.0.2",
@@ -205,9 +229,9 @@ dependencies = [
[[package]]
name = "bliss-audio"
version = "0.10.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000abff3621dffbae7e9b7574791d587001768224cedcb169158706b0f0ce0e2"
checksum = "9efa224c03fc286d1b66b88946855b1418ea486fb1d8f772dc38c8e799d12e27"
dependencies = [
"adler32",
"bliss-audio-aubio-rs",
@@ -229,9 +253,9 @@ dependencies = [
[[package]]
name = "bliss-audio-aubio-rs"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bfb72ae2d8bdd563b7218c6f6f423c17350052d8a9f69a8fc018220537fa1bf"
checksum = "4a1eb88c6ac3439669d67bd728acf23b770c11e4587fe90d0d25be216d988172"
dependencies = [
"bliss-audio-aubio-sys",
]
@@ -289,6 +313,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.40"
@@ -320,6 +350,12 @@ dependencies = [
"libloading",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "configparser"
version = "3.0.0"
@@ -366,16 +402,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.1"
@@ -410,6 +436,16 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "ctrlc"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix",
"windows-sys 0.59.0",
]
[[package]]
name = "data-encoding"
version = "2.8.0"
@@ -454,6 +490,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_home"
version = "0.1.0"
@@ -462,17 +508,23 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.8.4"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"atty",
"humantime",
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
"regex",
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.10"
@@ -480,7 +532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@@ -502,9 +554,9 @@ dependencies = [
[[package]]
name = "fallible-iterator"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
@@ -537,6 +589,18 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "filetime"
version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.60.2",
]
[[package]]
name = "flate2"
version = "1.0.34"
@@ -547,6 +611,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@@ -587,43 +657,27 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.11.2"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"ahash",
"allocator-api2",
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.8.4"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.14.5",
"hashbrown",
]
[[package]]
name = "heck"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
@@ -647,15 +701,9 @@ version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
dependencies = [
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@@ -698,12 +746,12 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "indexmap"
version = "1.8.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
dependencies = [
"autocfg",
"hashbrown 0.11.2",
"equivalent",
"hashbrown",
]
[[package]]
@@ -719,10 +767,16 @@ dependencies = [
]
[[package]]
name = "itertools"
version = "0.10.3"
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@@ -733,6 +787,30 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
]
[[package]]
name = "jobserver"
version = "0.1.32"
@@ -766,9 +844,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
@@ -777,7 +855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -787,10 +865,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libsqlite3-sys"
version = "0.25.2"
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.9.0",
"libc",
"redox_syscall 0.5.17",
]
[[package]]
name = "libsqlite3-sys"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
dependencies = [
"cc",
"pkg-config",
@@ -882,23 +971,25 @@ dependencies = [
[[package]]
name = "ndarray"
version = "0.15.6"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
"rayon",
]
[[package]]
name = "ndarray-stats"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af5a8477ac96877b5bd1fd67e0c28736c12943aba24eda92b127e036b0c8f400"
checksum = "17ebbe97acce52d06aebed4cd4a87c0941f4b2519b59b82b4feb5bd0ce003dfd"
dependencies = [
"indexmap",
"itertools",
@@ -909,6 +1000,18 @@ dependencies = [
"rand",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "noisy_float"
version = "0.2.0"
@@ -962,7 +1065,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.2",
"hermit-abi",
"libc",
]
@@ -987,6 +1090,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "paste"
version = "1.0.15"
@@ -1011,6 +1120,21 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
@@ -1098,27 +1222,22 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.5.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.9.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
@@ -1142,6 +1261,15 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "redox_users"
version = "0.3.5"
@@ -1149,7 +1277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
dependencies = [
"getrandom 0.1.16",
"redox_syscall",
"redox_syscall 0.1.57",
"rust-argon2",
]
@@ -1199,11 +1327,11 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.28.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.9.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -1263,7 +1391,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.59.0",
]
[[package]]
@@ -1323,18 +1451,27 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.225"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.225"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.225"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516"
dependencies = [
"proc-macro2",
"quote",
@@ -1343,13 +1480,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.109"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -1451,21 +1590,20 @@ checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "strum"
version = "0.24.1"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
[[package]]
name = "strum_macros"
version = "0.24.3"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.99",
"syn 2.0.99",
]
[[package]]
@@ -1669,15 +1807,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
@@ -1690,18 +1819,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.44"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.44"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
@@ -1827,6 +1956,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -1978,15 +2113,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@@ -1999,7 +2125,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -2014,7 +2140,16 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
@@ -2023,14 +2158,30 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
@@ -2039,70 +2190,98 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.99",
]
+10 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "bliss-analyser"
version = "0.3.0"
version = "0.5.1"
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
@@ -9,14 +9,18 @@ repository = "https://github.com/CDrummond/bliss-analyser"
keywords = ["audio", "song", "similarity"]
readme = "README.md"
[profile.release]
strip=true
[dependencies]
argparse = "0.2.2"
anyhow = "1.0.40"
rusqlite = { version = "0.28.0", features = ["bundled"] }
rusqlite = { version = "0.34.0", features = ["bundled"] }
log = "0.4.14"
env_logger = "0.8.4"
env_logger = "0.11.7"
indicatif = "0.16.2"
lofty = "0.22.2"
filetime = "0.2"
dirs = "1"
chrono = "0.4.40"
regex = "1"
@@ -28,18 +32,19 @@ num_cpus = "1.13.0"
which = { version = "7.0.2", optional = true }
rcue = { version = "0.1.3", optional = true }
hhmmss = { version = "0.1.0", optional = true }
ctrlc = "3.4"
serde_json = "1.0.145"
[features]
libav = ["bliss-audio/ffmpeg"]
update-aubio-bindings = ["bliss-audio/update-aubio-bindings"]
staticlibav = ["bliss-audio/build-ffmpeg", "bliss-audio/ffmpeg-static"]
ffmpeg = ["dep:which", "dep:rcue", "dep:hhmmss"]
symphonia = ["bliss-audio/symphonia-all", "bliss-audio/symphonia-aiff", "bliss-audio/symphonia-alac"]
rpi = ["bliss-audio/rpi"]
[dependencies.bliss-audio]
default-features = false
features = ["aubio-static"]
version = "0.10.0"
version = "0.11.1"
#git = "https://github.com/Polochon-street/bliss-rs.git"
#rev = "006927ac16752ff2e00bfe0d6b7756f67fa822c0"
+31 -1
View File
@@ -1,3 +1,33 @@
0.5.1
-----
1. Use serde_json to build JSONRPC notification strings.
0.5.0
-----
1. Update to bliss-rs 0.11.1
2. Only build static-libav and symphonia by default.
3. Don't crash if file has no tags.
4. Set log level for tag reader library.
5. Multi-threaded read of existing analysis tags.
6. Store file failures in DB so don't re-analyse unless timestamp of file
changes.
7. Drop support for using ffmpeg via commandline.
0.4.0
-----
1. Add action to export results from DB to files.
2. Add option to preserve file modification time when writing tags.
3. Store analysis in BLISS_ANALYSIS tag, not in a COMMENT tag.
4. Fix reading of tags during analysis phase.
5. Check for multiple genre tags, and if found add as semi-colon separated
list to DB.
6. When analysing files, always read tags (if present), but only write tags
if '--tags' is specified on commandline.
7. Use max of 16 significant digits (trailing zeros stripped) when writing
analysis results to file tags.
8. Strip symbols from release binaries.
9. Gracefully handle Ctrl-C.
0.3.0
-----
1. Add support for (DSD) WavPack - thanks to Bart Lauret
@@ -32,7 +62,7 @@
-----
1. Tidy up code, thanks to Serial-ATA
2. Update version of tag reader library, should now support ID3v2 in FLAC.
3. Show error message if can't open, or create, database file.
3. Show error message if can't open, or create, DB file.
4. Update version of bliss-rs, this now handles CUE processing internally.
0.1.0
+6 -34
View File
@@ -12,21 +12,15 @@ This application can be built in 4 variants:
1. Using `libavcodec`, etc, to decode files.
2. Using `libavcodec`, etc, to decode files, but statically linked to `libavcodec`, etc.
3. Using `symphonia` to decode files.
4. Using command-line `ffmpeg` to decode files.
`libavcodec` is the fastest (~15% faster than `symphonia`, ~50% faster than `ffmpeg`
commandline), but might have issues with library, versioning, etc., unless these
libraries are statically linked in. `libavcodec` statically linked may reduce supported
`libavcodec` is the fastest (~15% faster than `symphonia`), but might have issues with
library, versioning, etc., unless these libraries are statically linked in. `libavcodec`
statically linked may reduce supported file formats, but is more portable.
file formats, but is more portable.
`symphonia` also produces a more portable application, is only slightly slower to decode
files, but has more limited codec support, and does not produce identical analysis results.
Therefore, it is not advisable to mix files analysed with `ffmpeg` (any variant) and
`symphonia`.
Command-line `ffmpeg` whilst being the slowest, produces a more portable application, and
supports a wider range of codecs.
files, but has more limited codec support, and can fail on more files.
## Build for 'libavcodec' library usage
@@ -71,10 +65,10 @@ To install dependencies on a Fedora system:
dnf install ffmpeg-devel clang pkg-config
```
Build with `cargo build --release --features=libav,libavstatic`
Build with `cargo build --release --features=libav,staticlibav`
If building on a Raspberry Pi, then `rpi` also needs to be passed to `--features`, e.g.
`cargo build --release --features=libav,libavstatic,rpi`
`cargo build --release --features=libav,staticlibav,rpi`
@@ -98,28 +92,6 @@ Build with `cargo build --release --features=symphonia`
## Build for 'ffmpeg' command-line usage
`clang` and `pkg-config` are required to build, as well as
[Rust](https://www.rust-lang.org/tools/install)
To install dependencies on a Debian system:
```
apt install -y clang pkg-config
```
To install dependencies on a Fedora system:
```
dnf install clang pkg-config
```
Build with `cargo build --release --features=ffmpeg`
`ffmpeg` is then a run-time dependency, and should be installed on any system where this application
is to be run - it should also be in the users `$PATH`
# Usage
+88 -93
View File
@@ -10,43 +10,13 @@ feature.
PowerShell for Windows), as there is no graphical user interface.
Variants
--------
`bliss-analyser` can be built to support using either the `ffmpeg` libraries
(`libavcodec`, etc.), `symphonia` library, or invoking the `ffmpeg` command
itself.
If the package used ended with `-libav` then `bliss-analyser` has been built
with the `ffmpeg` libraries. This allows faster decoding of files, but will
require the exact `ffmpeg` library versions to be on your system. (These
libraries are usually provided with the Windows build).
If the package used ended with `-static` then `bliss-analyser` has been built
with the `ffmpeg` libraries - but these have been statically linked. This
allows faster decoding of files, and a more portable binary - however, this
_may_ reduce the number of supported file formats.
If the package used ended with `-symphonia` then `bliss-analyser` has been built
with the `symphonia` libraries. This allows a more portable binary, but at a
slightly slower decoding speed (than `libav`) and produces analysis results that
are not the same as those produced by `ffmpeg`/`libav`.
If the package used ended with `-ffmpeg`, then `bliss-analyser` requires you
also have the `ffmpeg` application installed and in your `$PATH`. These
builds are roughly 50% slower at analysis, but are more portable as they can
use (alomost) any `ffmpeg` version.
Quick guide
===========
1. Install the `Bliss Mixer` LMS plugin.
2. Install `ffmpeg` if using Linux or macOS (and using `-libav` or `-ffmpeg`
package (see `Varaints` (above))).
3. Edit the supplied `config.ini` in the current folder to set appropriate values
2. Edit the supplied `config.ini` in the current folder to set appropriate values
for `music` and `lms` - e.g.:
```
[Bliss]
@@ -54,67 +24,19 @@ music=/home/user/Music
lms=127.0.0.1
```
4. Analyse your tracks:
3. Analyse your tracks:
```
./bliss-analyser analyse
```
5. Upload analysis database to LMS:
4. Upload analysis database to LMS:
```
./bliss-analyser upload
```
6. Set LMS to use `Bliss` in `Don't Stop the Music`
7. Play some music!
Installation
============
For Windows no extra installation steps are required, as all dependencies are
bundled within its ZIP file. However, if using a `-libav` or `-ffmpeg` package (see
`Varaints` (above)), both the Linux and macOS versions require that `ffmpeg` be
installed - if using a `-static` or `-symphinia` package, then no additional
dependencies are used.
Linux
-----
Debian based systems (e.g. Ubuntu):
```
sudo apt install ffmpeg
```
RedHat based systems (e.g. Fedora):
```
sudo yum install ffmpeg
```
macOS
-----
First install `HomeBrew`
High Sierra, Sierra, El Capitan, or earlier:
```
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
Otherwise:
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
Then install `ffmpeg`:
```
brew install ffmpeg@5
brew link ffmpeg@5
```
5. Set LMS to use `Bliss` in `Don't Stop the Music`
6. Play some music!
Configuration
@@ -131,7 +53,8 @@ music=/home/user/Music
db=bliss.db
lms=127.0.0.1
ignore=ignore.txt
tags=true
readtags=true
writetags=true
```
The following items are supported:
@@ -143,13 +66,16 @@ 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
when uploading the database file to LMS. This defaults to `127.0.0.1` If your LMS is
password protected then use `user:pass@server` - e.g. `lms=pi:abc123@127.0.0.1`
* `json` specifies the JSONRPC port number of your LMS server. This will defaul to
9000.
* `json` specifies the JSONRPC port number of your LMS server. This will default
to `9000`.
* `ignore` specifies the name and location of a file containing items to ignore
in mixes. See the `Ignore` section later on for more details.
* `tags` specifies whether analysis results should be written to, and re-read from,
files. Set to `true` or `false`. If enabled, then results are stored in a `COMMENT`
tag that starts with `BLISS_ANALYSIS`
* `readtags` specifies whether analysis results should be read from files whe
analysing.
* `writetags` specifies whether analysis results should be written to files when
analysed.
* `preserve` specifies whether file modification time should be preserved when
writing tags. Set to `true` or `false`.
Command-line parameters
@@ -172,22 +98,30 @@ 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.
* `-J` / `--json` JSONRPC port number of your LMS server.
* `-n` / `--numtracks` Specify maximum number of tracks to analyse.
* `-T` / `--tags` Write anlysis results to file tags, and read from file tags.
* `-R` / `--readtags` When using the `analyse` task, read analysis results from a
files a `BLISS_ANALYSIS` tag (it present) in files.
* `-W` / `--writetags` When using the `analyse` task, write analysis results to files
within a `BLISS_ANALYSIS` tag.
* `-p' / '--preserve` Attempt to preserve file modification time when writing tags.
Equivalent items specified in the INI config file (detailed above) will override
any specified on the commandline.
any specified on the command-line.
`bliss-analyser` requires one extra parameter, which is used to determine the
required task. This takes the following values:
* `analyse` Performs analysis of tracks.
* `upload` Uploads the database to LMS.
* `stopmixer` Asks LMS plugin to stop it instance of `bliss-mixer`
* `stopmixer` Asks LMS plugin to stop its instance of `bliss-mixer`
* `tags` Re-reads tags from your music collection, and updates the database for
any changes.
* `ignore` Reads the `ignore` file and updates the database to flag tracks as
to be ignored for mixes.
* `export` Exports tags from database and stores within the audio files.
* `clearfail` If analysis fails for a track then the track path and its modification
time is stored in the database, so that running analysis again ignores these files.
This action clears this information, so that an analysis run will attempt to analyse
these files.
Analysing tracks
@@ -215,6 +149,23 @@ is shown.
As a rough guide, a 2015-era i7 8-core laptop with SSD analyses around 14000
tracks/hour.
If analysis fails for a track, then the track's path and timestamp are stored
within the database so that subsquent analysis calls will skip these tracks.
Tags
----
When analysing tracks, the analyser will first check for a `BLISS_ANALYSIS` tag
within the file, and if found this will be used instead of re-analysing the file.
If `--writetags` is passed, the analysis results will be stored within a
`BLISS_ANALYSIS` tag in the file itself. Note, however, that only tracks that are
not currently in the database will be analysed - therefore any such tracks would
not have the `BLISS_ANALYSIS` tag updated. To export analysis results from the
database to files themselves use the `export` task.
*NOTE* Use of the `BLISS_ANALYSIS` tag is not supported for CUE files.
CUE files
---------
@@ -357,6 +308,50 @@ is accomplished as follows:
Exporting Analysis
==================
If you have analysis results stored within the database, and not within
the files themselves, then you can use the `export` action to copy these
analysis results from the database and into the files.
(Linux / macOS)
```
./bliss-analyser export
```
(Windows)
```
.\bliss-analyser.exe export
```
*NOTE* Exporting of analysis results is not implemented for CUE tracks.
Clearing Failed Tracks
======================
Any tracks that could not be analysed are stored in the database so that
subsequent analysis runs ignore such files - unless the files themselves
have been updated.
You can remove this list of failed tracks, from database, via the
following command:
(Linux / macOS)
```
./bliss-analyser clearfail
```
(Windows)
```
.\bliss-analyser.exe clearfail
```
Credits
=======
-34
View File
@@ -1,34 +0,0 @@
# Cross compilation environment for bliss-analyser
FROM debian:bookworm
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y pkg-config:arm64
RUN apt-get install -y pkg-config:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-ffmpeg.sh"]
-38
View File
@@ -1,38 +0,0 @@
# Cross compilation environment for bliss-analyser
FROM debian:bookworm
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y pkg-config:arm64 \
libavutil-dev:arm64 libavcodec-dev:arm64 libavformat-dev:arm64 \
libavfilter-dev:arm64 libavdevice-dev:arm64 libswresample-dev:arm64 libfftw3-dev:arm64
RUN apt-get install -y pkg-config:armhf \
libavutil-dev:armhf libavcodec-dev:armhf libavformat-dev:armhf \
libavfilter-dev:armhf libavdevice-dev:armhf libswresample-dev:armhf libfftw3-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-libav.sh"]
-36
View File
@@ -1,36 +0,0 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y libavutil-dev:arm64 libavcodec-dev:arm64 libavformat-dev:arm64 \
libavfilter-dev:arm64 libavdevice-dev:arm64 libswresample-dev:arm64 libfftw3-dev:arm64
RUN apt-get install -y libavutil-dev:armhf libavcodec-dev:armhf libavformat-dev:armhf \
libavfilter-dev:armhf libavdevice-dev:armhf libswresample-dev:armhf libfftw3-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-libav.sh"]
-36
View File
@@ -1,36 +0,0 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y libavutil-dev:arm64 libavcodec-dev:arm64 libavformat-dev:arm64 \
libavfilter-dev:arm64 libavdevice-dev:arm64 libswresample-dev:arm64 libfftw3-dev:arm64
RUN apt-get install -y libavutil-dev:armhf libavcodec-dev:armhf libavformat-dev:armhf \
libavfilter-dev:armhf libavdevice-dev:armhf libswresample-dev:armhf libfftw3-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-staticlibav.sh"]
-25
View File
@@ -1,25 +0,0 @@
# Cross compilation environment for bliss-anlyser
FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y curl git pkg-config
RUN apt-get install -y build-essential yasm clang
RUN apt-get install -y libavutil-dev libavcodec-dev libavformat-dev \
libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev \
ffmpeg
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-libav.sh"]
@@ -1,6 +1,4 @@
# Cross compilation environment for bliss-analyser
FROM debian:bullseye
FROM debian:bookworm
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
@@ -1,6 +1,4 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
FROM debian:bookworm
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
+30
View File
@@ -0,0 +1,30 @@
FROM debian:bookworm
RUN dpkg --add-architecture amd64
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
# Download and extract musl-cross toolchain for x86_64
RUN curl -LO https://musl.cc/x86_64-linux-musl-cross.tgz \
&& tar -xzf x86_64-linux-musl-cross.tgz \
&& mv x86_64-linux-musl-cross /opt/musl-toolchain \
&& rm x86_64-linux-musl-cross.tgz
ENV PATH="/opt/musl-toolchain/bin:/root/.cargo/bin:${PATH}"
# Optionally, still install musl-tools (for musl-gcc wrapper, not strictly needed now)
RUN apt-get install -y musl-tools musl-dev musl
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN rustup target add x86_64-unknown-linux-musl
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-ffmpeg.sh"]
+22
View File
@@ -0,0 +1,22 @@
FROM debian:bookworm
RUN dpkg --add-architecture amd64
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y musl-tools musl-dev musl
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add x86_64-unknown-linux-musl
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-symphonia.sh"]
+4 -6
View File
@@ -11,13 +11,11 @@ rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=ffmpeg --target $1
fi
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features=libav,staticlibav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=libav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
build aarch64-unknown-linux-gnu aarch64-linux-gnu-strip bin/bliss-analyser-aarch64
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
cp scripts/bliss-analyser-arm $DESTDIR/bliss-analyser
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=libav,staticlibav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
build aarch64-unknown-linux-gnu aarch64-linux-gnu-strip bin/bliss-analyser-aarch64
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
cp scripts/bliss-analyser-arm $DESTDIR/bliss-analyser
+4 -6
View File
@@ -11,13 +11,11 @@ rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=symphonia --target $1
fi
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features=symphonia --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
+22
View File
@@ -0,0 +1,22 @@
#!/bin/ash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features=update-aubio-bindings,libav,staticlibav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build x86_64-unknown-linux-musl strip bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
export RUST_BACKTRACE=full
cargo build --release --features=libav
strip /build/release/bliss-analyser && cp /build/release/bliss-analyser $DESTDIR/bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features update-aubio-bindings,symphonia --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build x86_64-unknown-linux-musl strip bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
+4 -4
View File
@@ -11,10 +11,10 @@ import datetime, os, requests, shutil, subprocess, sys, tempfile, time
GITHUB_TOKEN_FILE = "%s/.config/github-token" % os.path.expanduser('~')
GITHUB_REPO = "CDrummond/bliss-analyser"
LINUX_ARM_ARTIFACTS = ["bliss-analyser-linux-arm-ffmpeg", "bliss-analyser-linux-arm-static", "bliss-analyser-debian-bullseye-arm-libav", "bliss-analyser-debian-bookworm-arm-libav", "bliss-analyser-linux-arm-symphonia"]
LINUX_X86_ARTIFACTS = ["bliss-analyser-linux-x86-ffmpeg", "bliss-analyser-linux-x86-static", "bliss-analyser-ubuntu-22.04-x86-libav", "bliss-analyser-ubuntu-24.04-x86-libav", "bliss-analyser-linux-x86-symphonia"]
MAC_ARTIFACTS = ["bliss-analyser-mac-ffmpeg", "bliss-analyser-mac-symphonia"]
WINDOWS_ARTIFACTS = ["bliss-analyser-windows-libav", "bliss-analyser-windows-symphonia"]
LINUX_ARM_ARTIFACTS = ["bliss-analyser-linux-arm-ffmpeg", "bliss-analyser-linux-arm-symphonia"]
LINUX_X86_ARTIFACTS = ["bliss-analyser-linux-x86-ffmpeg", "bliss-analyser-linux-x86-symphonia"]
MAC_ARTIFACTS = ["bliss-analyser-mac-symphonia"]
WINDOWS_ARTIFACTS = ["bliss-analyser-windows-ffmpeg", "bliss-analyser-windows-symphonia"]
UNIX_ARTIFACTS = LINUX_ARM_ARTIFACTS + LINUX_X86_ARTIFACTS + MAC_ARTIFACTS
GITHUB_ARTIFACTS = UNIX_ARTIFACTS + WINDOWS_ARTIFACTS
+223 -271
View File
@@ -6,69 +6,102 @@
*
**/
use crate::cue;
use crate::db;
#[cfg(feature = "ffmpeg")]
use crate::ffmpeg;
use crate::tags;
use anyhow::Result;
#[cfg(feature = "ffmpeg")]
use hhmmss::Hhmmss;
use if_chain::if_chain;
use indicatif::{ProgressBar, ProgressStyle};
#[cfg(not(feature = "ffmpeg"))]
use std::collections::HashSet;
use std::convert::TryInto;
use std::fs::{DirEntry, File};
use std::io::{BufRead, BufReader};
use std::num::NonZeroUsize;
use filetime::FileTime;
use std::fs;
use std::fs::DirEntry;
use std::num::NonZero;
use std::path::{Path, PathBuf};
#[cfg(feature = "ffmpeg")]
use std::sync::mpsc;
#[cfg(feature = "ffmpeg")]
use std::sync::mpsc::{Receiver, Sender};
#[cfg(feature = "ffmpeg")]
use std::thread;
#[cfg(feature = "ffmpeg")]
use std::time::Duration;
use num_cpus;
use bliss_audio::decoder::Decoder;
#[cfg(feature = "libav")]
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as SongDecoder;
#[cfg(feature = "symphonia")]
use bliss_audio::decoder::symphonia::SymphoniaDecoder as SongDecoder;
#[cfg(feature = "ffmpeg")]
use bliss_audio::{BlissResult, Song};
use bliss_audio::{AnalysisOptions, decoder::Decoder};
use ureq;
use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use serde_json::json;
const DONT_ANALYSE: &str = ".notmusic";
const MAX_ERRORS_TO_SHOW: usize = 100;
const MAX_TAG_ERRORS_TO_SHOW: usize = 50;
const MIN_NOTIF_TIME:u64 = 2;
const VALID_EXTENSIONS: [&str; 7] = ["m4a", "mp3", "ogg", "flac", "opus", "wv", "dsf"];
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>, file_count:&mut usize, max_num_files: usize, use_tags: bool, tagged_file_count:&mut usize, dry_run: bool) {
static mut TERMINATE_ANALYSIS_FLAG: bool = false;
struct NotifInfo {
pub enabled: bool,
pub address: String,
pub last_send: u64,
pub start_time: u64
}
fn terminate_analysis() -> bool {
unsafe {
return TERMINATE_ANALYSIS_FLAG
}
}
fn handle_ctrl_c() {
unsafe {
TERMINATE_ANALYSIS_FLAG = true;
}
}
fn send_notif_msg(notifs: &mut NotifInfo, text: &str) {
let js = json!({"id":"1", "method":"slim.request", "params":["", ["blissmixer", "analyser", "act:update", format!("msg:{}", text)]]});
log::info!("Sending notif to LMS: {}", text);
let _ = ureq::post(&notifs.address).send_string(&js.to_string());
}
fn send_notif(notifs: &mut NotifInfo, text: &str) {
if notifs.enabled {
let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("time should go forward").as_secs();
if now>=notifs.last_send+MIN_NOTIF_TIME {
let dur = now - notifs.start_time;
let msg = format!("[{:02}:{:02}:{:02}] {}", (dur/60)/60, (dur/60)%60, dur%60, text);
send_notif_msg(notifs, &msg);
notifs.last_send = now;
}
}
}
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>, file_count:&mut usize, max_num_files: usize,
dry_run: bool, notifs: &mut NotifInfo) {
if !path.is_dir() {
return;
}
send_notif(notifs, &format!("SCAN DIR {}", path.to_string_lossy()));
let mut items: Vec<_> = path.read_dir().unwrap().map(|r| r.unwrap()).collect();
items.sort_by_key(|dir| dir.path());
for item in items {
check_dir_entry(db, mpath, item, track_paths, cue_tracks, file_count, max_num_files, use_tags, tagged_file_count, dry_run);
check_dir_entry(db, mpath, item, track_paths, file_count, max_num_files, dry_run, notifs);
if max_num_files>0 && *file_count>=max_num_files {
break;
}
}
}
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>, file_count:&mut usize, max_num_files: usize, use_tags: bool, tagged_file_count:&mut usize, dry_run: bool) {
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>, file_count:&mut usize, max_num_files: usize,
dry_run: bool, notifs: &mut NotifInfo) {
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 if max_num_files<=0 || *file_count<max_num_files {
get_file_list(db, mpath, &pb, track_paths, cue_tracks, file_count, max_num_files, use_tags, tagged_file_count, dry_run);
get_file_list(db, mpath, &pb, track_paths, file_count, max_num_files, dry_run, notifs);
}
} else if pb.is_file() && (max_num_files<=0 || *file_count<max_num_files) {
if_chain! {
@@ -88,42 +121,31 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
if let Ok(cue_track_stripped) = cue_track_path.strip_prefix(mpath) {
let cue_track_sname = String::from(cue_track_stripped.to_string_lossy());
if let Ok(id) = db.get_rowid(&cue_track_sname) {
#[cfg(not(feature = "ffmpeg"))]
if id<=0 {
track_paths.push(String::from(cue_file.to_string_lossy()));
*file_count+=1;
}
#[cfg(feature = "ffmpeg")]
if id<=0 {
let this_cue_tracks = cue::parse(&pb, &cue_file);
for track in this_cue_tracks {
cue_tracks.push(track.clone());
}
*file_count+=1;
}
}
}
} else {
if let Ok(id) = db.get_rowid(&sname) {
if id<=0 {
let mut tags_used = false;
if use_tags {
let meta = tags::read(&sname, true);
if !meta.is_empty() && !meta.analysis.is_none() {
if !dry_run {
db.add_track(&sname, &meta, &meta.analysis.unwrap());
}
*tagged_file_count+=1;
tags_used = true;
}
}
if !tags_used {
// Check this file is not in Failures table
let ts = db.get_failure_timestamp(&sname);
if ts<=0 {
// ...nope, not in there so analyse
track_paths.push(String::from(pb.to_string_lossy()));
*file_count+=1;
} else {
let path = String::from(pb.to_string_lossy());
let metadata = fs::metadata(&path).unwrap();
let mtime = FileTime::from_last_modification_time(&metadata).unix_seconds();
if mtime!=ts {
// ...was in failures table, but timestamp has changed
db.remove_from_failures(&sname);
track_paths.push(path);
*file_count+=1;
}
}
}
}
@@ -160,26 +182,106 @@ fn show_errors(failed: &mut Vec<String>, tag_error: &mut Vec<String>) {
}
}
#[cfg(not(feature = "ffmpeg"))]
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool) -> Result<()> {
#[derive(Clone, Default, PartialEq)]
pub struct Meta {
pub file: String,
pub meta: db::Metadata
}
fn read_tags(tracks: Vec<String>, max_threads: usize) -> Receiver<Meta> {
#[allow(clippy::type_complexity)]
let (tx, rx): (
Sender<Meta>,
Receiver<Meta>,
) = mpsc::channel();
if tracks.is_empty() {
return rx;
}
let mut handles = Vec::new();
let mut chunk_length = tracks.len() / max_threads;
if chunk_length == 0 {
chunk_length = tracks.len();
} else if chunk_length == 1 && tracks.len() > max_threads {
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 || {
for track in owned_chunk {
let _ = tx_thread.send(Meta {
file:track.clone(),
meta: tags::read(&track, true),
});
}
});
handles.push(child);
}
rx
}
fn check_for_tags(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, notifs: &mut NotifInfo) -> Vec<String> {
let mut untagged_paths:Vec<String> = Vec::new();
let total = track_paths.len();
let results = read_tags(track_paths, max_threads);
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("=> "),
);
log::info!("Reading any existing analysis tags");
send_notif(notifs, "Reading any existing analysis tags");
for res in results {
let path = PathBuf::from(&res.file);
let stripped = path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
progress.set_message(format!("{}", sname));
if !res.meta.is_empty() && !res.meta.analysis.is_none() {
db.add_track(&sname, &res.meta.clone(), &res.meta.analysis.unwrap());
} else {
untagged_paths.push(res.file);
}
if terminate_analysis() {
break
}
progress.inc(1);
if notifs.enabled {
let pc = (progress.position() as f64 * 100.0)/total as f64;
send_notif(notifs, &format!("READ TAGS {:8.2}% {}", pc, sname));
}
}
if terminate_analysis() {
progress.abandon_with_message("Terminated!");
} else {
progress.finish_with_message("Finished!");
}
untagged_paths
}
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, write_tags: bool,
preserve_mod_times: bool, notifs: &mut NotifInfo) -> Result<()> {
let total = track_paths.len();
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 cpu_threads: NonZeroUsize = match max_threads {
0 => NonZeroUsize::new(num_cpus::get()).unwrap(),
_ => NonZeroUsize::new(max_threads).unwrap(),
};
let mut analysed = 0;
let mut failed: Vec<String> = Vec::new();
let mut tag_error: Vec<String> = Vec::new();
let mut reported_cue:HashSet<String> = HashSet::new();
let mut options:AnalysisOptions = AnalysisOptions::default();
options.number_cores = NonZero::new(max_threads).unwrap();
send_notif(notifs, "Analysing new files");
log::info!("Analysing new files");
for (path, result) in SongDecoder::analyze_paths_with_cores(track_paths, cpu_threads) {
for (path, result) in SongDecoder::analyze_paths_with_options(track_paths, options) {
let stripped = path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
@@ -236,181 +338,63 @@ fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max
if meta.is_empty() {
tag_error.push(sname.clone());
}
if use_tags {
tags::write_analysis(&cpath, &track.analysis);
if write_tags {
tags::write_analysis(&cpath, &track.analysis, preserve_mod_times);
}
db.add_track(&sname, &meta, &track.analysis);
}
}
analysed += 1;
}
Err(e) => { failed.push(format!("{} - {}", sname, e)); }
Err(e) => {
failed.push(format!("{} - {}", sname, e));
let metadata = fs::metadata(path).unwrap();
let mtime = FileTime::from_last_modification_time(&metadata);
db.add_to_failures(&sname, mtime.unix_seconds(), &format!("{}", e));
}
};
if inc_progress {
progress.inc(1);
if notifs.enabled {
let pc = (progress.position() as f64 * 100.0)/total as f64;
send_notif(notifs, &format!("ANALYSE {:8.2}% {}", pc, sname));
}
}
if terminate_analysis() {
break
}
}
progress.finish_with_message("Finished!");
log::info!("{} Analysed. {} Failure(s).", analysed, failed.len());
if terminate_analysis() {
progress.abandon_with_message("Terminated!");
} else {
progress.finish_with_message("Finished!");
}
log::info!("{} Analysed. {} Failed.", analysed, failed.len());
show_errors(&mut failed, &mut tag_error);
Ok(())
}
#[cfg(feature = "ffmpeg")]
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, use_tags: bool) -> Result<()> {
let total = track_paths.len();
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 cpu_threads: NonZeroUsize = match max_threads {
0 => NonZeroUsize::new(num_cpus::get()).unwrap(),
_ => NonZeroUsize::new(max_threads).unwrap(),
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_files: usize,
max_threads: usize, ignore_path: &PathBuf, read_tags: bool, write_tags: bool, preserve_mod_times: bool,
lms_host: &String, json_port: u16, send_notifs: bool) -> bool {
let mut db = db::Db::new(&String::from(db_path));
let mut notifs = NotifInfo {
enabled: send_notifs,
address: format!("http://{}:{}/jsonrpc.js", lms_host, json_port),
last_send: 0,
start_time: SystemTime::now().duration_since(UNIX_EPOCH).expect("time should go forward").as_secs(),
};
let mut analysed = 0;
let mut failed: Vec<String> = Vec::new();
let mut tag_error: Vec<String> = Vec::new();
log::info!("Analysing new files");
for (path, result) in <ffmpeg::FFmpegCmdDecoder as Decoder>::analyze_paths_with_cores(track_paths, cpu_threads) {
let stripped = path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
progress.set_message(format!("{}", sname));
match result {
Ok(track) => {
let cpath = String::from(path.to_string_lossy());
let mut meta = tags::read(&cpath, false);
if meta.is_empty() {
meta = ffmpeg::read_tags(&cpath);
}
if meta.is_empty() {
tag_error.push(sname.clone());
}
if use_tags {
tags::write_analysis(&cpath, &track.analysis);
}
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
}
Err(e) => { failed.push(format!("{} - {}", sname, e)); }
};
progress.inc(1);
}
// Reset terminal, otherwise typed output does not show? Perhaps Linux only...
if ! cfg!(windows) {
match std::process::Command::new("stty").arg("sane").spawn() {
Ok(_) => { },
Err(_) => { },
};
}
progress.finish_with_message("Finished!");
log::info!("{} Analysed. {} Failure(s).", analysed, failed.len());
show_errors(&mut failed, &mut tag_error);
Ok(())
}
#[cfg(feature = "ffmpeg")]
fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
let num_cpus = num_cpus::get();
#[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 || {
for cue_track in owned_chunk {
let audio_path = format!("{}{}{}.00{}{}.00", cue_track.audio_path.to_string_lossy(), ffmpeg::TIME_SEP, cue_track.start.hhmmss(), ffmpeg::TIME_SEP, cue_track.duration.hhmmss());
let track_path = String::from(cue_track.track_path.to_string_lossy());
log::debug!("Analyzing '{}'", track_path);
let song = <ffmpeg::FFmpegCmdDecoder as Decoder>::song_from_path(audio_path);
tx_thread.send((cue_track, song)).unwrap();
}
});
handles.push(child);
}
Ok(rx)
}
#[cfg(feature = "ffmpeg")]
fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
let total = cue_tracks.len();
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 results = analyze_cue_streaming(cue_tracks)?;
let mut analysed = 0;
let mut failed:Vec<String> = Vec::new();
let mut tag_error: Vec<String> = Vec::new();
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
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());
progress.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:if track.duration>=last_track_duration { song.duration.as_secs() as u32 } else { track.duration.as_secs() as u32 },
analysis: None
};
db.add_track(&sname, &meta, &song.analysis);
analysed += 1;
},
Err(e) => {
failed.push(format!("{} - {}", sname, e));
}
};
progress.inc(1);
}
progress.finish_with_message("Finished!");
log::info!("{} Analysed. {} Failure(s).", analysed, failed.len());
show_errors(&mut failed, &mut tag_error);
Ok(())
}
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_files: usize, max_threads: usize, ignore_path: &PathBuf, use_tags: bool) {
let mut db = db::Db::new(&String::from(db_path));
ctrlc::set_handler(move || {
handle_ctrl_c();
}).expect("Error setting Ctrl-C handler");
db.init();
if !keep_old {
send_notif(&mut notifs, "Removing old files from DB");
db.remove_old(mpaths, dry_run);
}
@@ -419,50 +403,37 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
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();
let mut file_count:usize = 0;
let mut tagged_file_count:usize = 0;
if mpaths.len() > 1 {
log::info!("Looking for new files in {}", mpath.to_string_lossy());
} else {
log::info!("Looking for new files");
}
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks, &mut file_count, max_num_files, use_tags, &mut tagged_file_count, dry_run);
log::info!("Looking for new files in {}", mpath.to_string_lossy());
send_notif(&mut notifs, &format!("Looking for new files in {}", mpath.to_string_lossy()));
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut file_count, max_num_files, dry_run, &mut notifs);
track_paths.sort();
log::info!("Num new files: {}", track_paths.len());
if !cue_tracks.is_empty() {
log::info!("Num new cue tracks: {}", cue_tracks.len());
}
if use_tags {
log::info!("Num tagged files: {}", tagged_file_count);
}
log::info!("New files: {}", track_paths.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 {
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths, max_threads, use_tags) {
Ok(_) => { changes_made = true; }
Err(e) => { log::error!("Analysis returned error: {}", e); }
if !terminate_analysis() {
if dry_run {
if !track_paths.is_empty() {
log::info!("The following need to be analysed (or tags read):");
for track in track_paths {
log::info!(" {}", track);
}
}
} else {
log::info!("No new files to analyse");
}
if !track_paths.is_empty() {
let untagged_paths = if read_tags { check_for_tags(&db, &mpath, track_paths, max_threads, &mut notifs) } else { track_paths };
#[cfg(feature = "ffmpeg")]
if !cue_tracks.is_empty() {
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
Ok(_) => { changes_made = true; },
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
if !untagged_paths.is_empty() {
log::info!("New untagged files: {}", untagged_paths.len());
match analyse_new_files(&db, &mpath, untagged_paths, max_threads, write_tags, preserve_mod_times, &mut notifs) {
Ok(_) => { changes_made = true; }
Err(e) => { log::error!("Analysis returned error: {}", e); }
}
} else {
log::info!("No new untagged files to analyse");
send_notif(&mut notifs, "No new files to analyse");
}
}
}
}
@@ -471,30 +442,11 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
db.close();
if changes_made && ignore_path.exists() && ignore_path.is_file() {
log::info!("Updating 'ignore' flags");
update_ignore(&db_path, &ignore_path);
send_notif(&mut notifs, "Updating ignore");
db::update_ignore(&db_path, &ignore_path);
}
}
pub fn read_tags(db_path: &str, mpaths: &Vec<PathBuf>) {
let db = db::Db::new(&String::from(db_path));
db.init();
db.update_tags(&mpaths);
db.close();
}
pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
let file = File::open(ignore_path).unwrap();
let reader = BufReader::new(file);
let db = db::Db::new(&String::from(db_path));
db.init();
db.clear_ignore();
let mut lines = reader.lines();
while let Some(Ok(line)) = lines.next() {
if !line.is_empty() && !line.starts_with("#") {
db.set_ignore(&line);
}
if send_notifs {
send_notif_msg(&mut notifs, "FINISHED");
}
db.close();
changes_made
}
-110
View File
@@ -1,110 +0,0 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
#[cfg(feature = "ffmpeg")]
extern crate rcue;
#[cfg(feature = "ffmpeg")]
use crate::db;
#[cfg(feature = "ffmpeg")]
use rcue::parser::parse_from_file;
use std::path::PathBuf;
#[cfg(feature = "ffmpeg")]
use std::time::Duration;
#[cfg(feature = "ffmpeg")]
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
#[cfg(feature = "ffmpeg")]
const GENRE:&str = "GENRE";
#[cfg(not(feature = "ffmpeg"))]
#[derive(Clone)]
pub struct CueTrack {
pub track_path:PathBuf
}
#[cfg(feature = "ffmpeg")]
#[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
}
#[cfg(feature = "ffmpeg")]
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, db::CUE_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
}
+251 -29
View File
@@ -6,15 +6,17 @@
*
**/
#[cfg(feature = "ffmpeg")]
use crate::ffmpeg;
use crate::tags;
use bliss_audio::{Analysis, AnalysisIndex};
use bliss_audio::{Analysis, AnalysisIndex, FeaturesVersion};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::{params, Connection};
use std::convert::TryInto;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process;
use std::thread;
use std::thread::JoinHandle;
pub const CUE_MARKER: &str = ".CUE_TRACK.";
@@ -29,7 +31,13 @@ pub struct FileMetadata {
pub duration: u32,
}
#[derive(Default, PartialEq)]
#[derive(Clone)]
struct AnalysisResults {
pub file: String,
pub analysis: Analysis,
}
#[derive(Clone, Default, PartialEq)]
pub struct Metadata {
pub title: String,
pub artist: String,
@@ -50,6 +58,20 @@ impl Metadata {
}
}
static mut TERMINATE_EXPORT_FLAG: bool = false;
fn terminate_export() -> bool {
unsafe {
return TERMINATE_EXPORT_FLAG
}
}
fn handle_ctrl_c() {
unsafe {
TERMINATE_EXPORT_FLAG = true;
}
}
pub struct Db {
pub conn: Connection,
}
@@ -70,8 +92,13 @@ impl Db {
}
pub fn init(&self) {
let _ = self.conn.execute("DROP TABLE IF EXISTS Tracks;", [],);
let _ = self.conn.execute("DROP INDEX IF EXISTS Tracks_idx", []);
let _ = self.conn.execute("VACUUM", [],);
let cmd = self.conn.execute(
"CREATE TABLE IF NOT EXISTS Tracks (
"CREATE TABLE IF NOT EXISTS TracksV2 (
File text primary key,
Title text,
Artist text,
@@ -99,20 +126,44 @@ impl Db {
Chroma7 real,
Chroma8 real,
Chroma9 real,
Chroma10 real
Chroma10 real,
Chroma11 real,
Chroma12 real,
Chroma13 real
);",
[],
);
if cmd.is_err() {
log::error!("Failed to create DB table");
log::error!("Failed to create Files table");
process::exit(-1);
}
let cmd = self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)", []);
let cmd = self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS TracksV2_idx ON TracksV2(File)", []);
if cmd.is_err() {
log::error!("Failed to create DB index");
log::error!("Failed to create Files index");
process::exit(-1);
}
let cmd = self.conn.execute(
"CREATE TABLE IF NOT EXISTS Failures (
File text primary key,
Timestamp integer,
Reason text
);",
[],
);
if cmd.is_err() {
log::error!("Failed to create Failures table");
process::exit(-1);
}
let cmd = self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Failures_idx ON Failures(File)", []);
if cmd.is_err() {
log::error!("Failed to create Failures index");
process::exit(-1);
}
}
@@ -126,7 +177,7 @@ impl Db {
if cfg!(windows) {
db_path = db_path.replace("\\", "/");
}
let mut stmt = self.conn.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
let mut stmt = self.conn.prepare("SELECT rowid FROM TracksV2 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 {
@@ -144,23 +195,24 @@ impl Db {
match self.get_rowid(&path) {
Ok(id) => {
if id <= 0 {
match self.conn.execute("INSERT INTO Tracks (File, Title, Artist, AlbumArtist, Album, Genre, Duration, Ignore, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
match self.conn.execute("INSERT INTO TracksV2 (File, Title, Artist, AlbumArtist, Album, Genre, Duration, Ignore, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10, Chroma11, Chroma12, Chroma13) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
params![db_path, meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration, 0,
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],
analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5],
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10]]) {
Ok(_) => { }
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10],
analysis[AnalysisIndex::Chroma11], analysis[AnalysisIndex::Chroma12], analysis[AnalysisIndex::Chroma13]]) {
Ok(_) => { self.remove_from_failures(path); }
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 TracksV2 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=?, Chroma11=?, Chroma12=?, Chroma13=? 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],
analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5],
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10], id]) {
Ok(_) => { }
Ok(_) => { self.remove_from_failures(path); }
Err(e) => { log::error!("Failed to update '{}' in database. {}", path, e); }
}
}
@@ -169,9 +221,42 @@ impl Db {
}
}
pub fn add_to_failures(&self, path: &String, timestamp:i64, reason: &String) {
self.remove_from_failures(path);
let _ = self.conn.execute("INSERT INTO Failures (File, Timestamp, Reason) VALUES (?, ?, ?);", params![path, timestamp, reason]);
}
pub fn remove_from_failures(&self, path: &String) {
let _ = self.conn.execute("DELETE FROM Failures WHERE File = ?;", params![path]);
}
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run: bool) {
self.remove_old_from_table(mpaths, dry_run, "TracksV2");
self.remove_old_from_table(mpaths, dry_run, "Failures");
}
fn clear_failures(&self) {
let _ = self.conn.execute("DELETE FROM Failures;", []);
}
pub fn get_failure_timestamp(&self, path: &String) -> i64 {
let mut ts:i64 = 0;
let mut db_path = path.to_string();
if cfg!(windows) {
db_path = db_path.replace("\\", "/");
}
let mut stmt = self.conn.prepare("SELECT Timestamp FROM Failures WHERE File=:path;").unwrap();
let track_iter = stmt.query_map(&[(":path", &db_path)], |row| Ok(row.get(0)?)).unwrap();
for tr in track_iter {
ts = tr.unwrap();
break;
}
ts
}
fn remove_old_from_table(&self, mpaths: &Vec<PathBuf>, dry_run: bool, table: &str) {
log::info!("Looking for non-existent tracks");
let mut stmt = self.conn.prepare("SELECT File FROM Tracks;").unwrap();
let mut stmt = self.conn.prepare(format!("SELECT File FROM {};", table).as_str()).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 {
@@ -215,7 +300,7 @@ impl Db {
let count_before = self.get_track_count();
for t in to_remove {
//log::debug!("Remove '{}'", t);
let cmd = self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]);
let cmd = self.conn.execute(format!("DELETE FROM {} WHERE File = ?;", table).as_str(), params![t]);
if let Err(e) = cmd {
log::error!("Failed to remove '{}' - {}", t, e)
@@ -230,7 +315,7 @@ impl Db {
}
pub fn get_track_count(&self) -> usize {
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM Tracks;").unwrap();
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM TracksV2;").unwrap();
let track_iter = stmt.query_map([], |row| Ok(row.get(0)?)).unwrap();
let mut count: usize = 0;
for tr in track_iter {
@@ -251,7 +336,7 @@ impl Db {
.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 mut stmt = self.conn.prepare("SELECT rowid, File, Title, Artist, AlbumArtist, Album, Genre, Duration FROM TracksV2 ORDER BY File ASC;").unwrap();
let track_iter = stmt
.query_map([], |row| {
Ok(FileMetadata {
@@ -286,18 +371,13 @@ impl Db {
let track_path = mpath.join(&dbtags.file);
if track_path.exists() {
let path = String::from(track_path.to_string_lossy());
#[allow(unused_mut)] // ftags is mutable if using ffmpeg on commandline
let mut ftags = tags::read(&path, false);
let ftags = tags::read(&path, false);
#[cfg(feature = "ffmpeg")]
if ftags.is_empty() {
ftags = ffmpeg::read_tags(&path);
}
if ftags.is_empty() {
log::error!("Failed to read tags of '{}'", dbtags.file);
} else if ftags != dtags {
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;",
match self.conn.execute("UPDATE TracksV2 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); }
@@ -314,7 +394,7 @@ impl Db {
}
pub fn clear_ignore(&self) {
let cmd = self.conn.execute("UPDATE Tracks SET Ignore=0;", []);
let cmd = self.conn.execute("UPDATE TracksV2 SET Ignore=0;", []);
if let Err(e) = cmd {
log::error!("Failed clear Ignore column. {}", e);
@@ -325,17 +405,159 @@ impl Db {
log::info!("Ignore: {}", line);
if line.starts_with("SQL:") {
let sql = &line[4..];
let cmd = self.conn.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []);
let cmd = self.conn.execute(&format!("UPDATE TracksV2 Set Ignore=1 WHERE {}", sql), []);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
} else {
let cmd = self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line), []);
let cmd = self.conn.execute(&format!("UPDATE TracksV2 SET Ignore=1 WHERE File LIKE \"{}%\"", line), []);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
}
}
pub fn export(&self, mpaths: &Vec<PathBuf>, max_threads: usize, preserve_mod_times: bool) {
ctrlc::set_handler(move || {
handle_ctrl_c();
}).expect("Error setting Ctrl-C handler");
log::info!("Querying database");
let mut tracks:Vec<AnalysisResults> = Vec::new();
let mut stmt = self.conn.prepare("SELECT File, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10, Chroma11, Chroma12, Chroma13 FROM TracksV2 ORDER BY File ASC;").unwrap();
let track_iter = stmt
.query_map([], |row| {
Ok(AnalysisResults {
file: row.get(0)?,
analysis: Analysis::new([row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?, row.get(8)?, row.get(9)?, row.get(10)?, row.get(11)?, row.get(12)?, row.get(13)?, row.get(14)?, row.get(15)?, row.get(16)?, row.get(17)?, row.get(18)?, row.get(19)?, row.get(20)?, row.get(21)?, row.get(22)?, row.get(23)?].to_vec(), FeaturesVersion::LATEST).expect("number of vals should be 23"),
})
})
.unwrap();
for tr in track_iter {
let dbtags = tr.unwrap();
if !dbtags.file.contains(CUE_MARKER) {
for mpath in mpaths {
let track_path = mpath.join(dbtags.file.clone());
if track_path.exists() {
tracks.push(AnalysisResults{file:String::from(track_path.to_string_lossy()), analysis:dbtags.analysis.clone()});
}
}
}
}
let total = tracks.len();
if total <= 0 {
log::info!("Nothing to export");
return;
}
log::info!("Starting export");
let chunk_size = total/max_threads;
let mut threads: Vec<JoinHandle<()>> = vec![];
let (sender, receiver) = std::sync::mpsc::channel();
let reporting_thread = std::thread::spawn(move || {
let mut processed = 0;
let mut had_tags = 0;
let mut failed_to_write = 0;
let mut exported = 0;
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("=> "),
);
for resp in receiver {
progress.inc(1);
processed+=1;
if resp==0 {
had_tags+=1;
} else if resp==1 {
failed_to_write+=1;
} else {
exported+=1;
}
if processed == total {
break;
}
if terminate_export() {
break
}
}
if terminate_export() {
progress.abandon_with_message("Terminated!");
} else {
progress.finish_with_message(format!("Finished!"));
}
log::info!("{} Exported. {} Existing. {} Failed.", exported, had_tags, failed_to_write);
});
threads.push(reporting_thread);
for thread in 0..max_threads {
let tid:usize = thread;
let start = tid * chunk_size;
let end = if tid+1 == max_threads { total } else { start + chunk_size };
let sndr = sender.clone();
let trks = Vec::from_iter(tracks[start..end].iter().cloned());
threads.push(thread::spawn(move || {
for track in trks {
let mut updated = 0;
let meta = tags::read(&track.file, true);
if meta.is_empty() || meta.analysis.is_none() || meta.analysis.unwrap()!=track.analysis {
updated = 1;
if tags::write_analysis(&track.file, &track.analysis, preserve_mod_times) {
updated = 2;
}
}
sndr.send(updated).unwrap();
if terminate_export() {
break
}
}
}));
}
for thread in threads {
let _ = thread.join();
}
}
}
pub fn read_tags(db_path: &str, mpaths: &Vec<PathBuf>) {
let db = Db::new(&String::from(db_path));
db.init();
db.update_tags(&mpaths);
db.close();
}
pub fn export(db_path: &str, mpaths: &Vec<PathBuf>, max_threads: usize, preserve_mod_times: bool) {
let db = Db::new(&String::from(db_path));
db.init();
db.export(&mpaths, max_threads, preserve_mod_times);
db.close();
}
pub fn clear_failures(db_path: &str) {
let db = Db::new(&String::from(db_path));
db.init();
db.clear_failures();
db.close();
}
pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
let file = File::open(ignore_path).unwrap();
let reader = BufReader::new(file);
let db = Db::new(&String::from(db_path));
db.init();
db.clear_ignore();
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();
}
-141
View File
@@ -1,141 +0,0 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use crate::db;
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::PreAnalyzedSong;
use bliss_audio::{BlissError, BlissResult};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::io;
use std::io::Read;
use std::io::BufRead;
use std::io::BufReader;
use std::time::Duration;
pub const TIME_SEP:&str = "<TIME>";
pub struct FFmpegCmdDecoder;
fn handle_command(mut child: Child) -> BlissResult<PreAnalyzedSong> {
let mut decoded_song = PreAnalyzedSong::default();
let stdout = child.stdout.as_mut().expect("Failed to capture stdout");
let mut reader = io::BufReader::new(stdout);
let mut buffer: Vec<u8> = Vec::new();
reader.read_to_end(&mut buffer).map_err(|e| {
BlissError::DecodingError(format!("Could not read the decoded file into a buffer: {}", e))
})?;
decoded_song.sample_array = buffer
.chunks_exact(4)
.map(|x| {
let mut a: [u8; 4] = [0; 4];
a.copy_from_slice(x);
f32::from_le_bytes(a)
})
.collect();
let duration_seconds = decoded_song.sample_array.len() as f32 / 22050 as f32;
decoded_song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64);
Ok(decoded_song)
}
fn get_val(line: String) -> String {
let parts = line.split("=");
let mut resp:Vec<String> = Vec::new();
let mut first =true;
for part in parts {
if !first {
resp.push(String::from(part));
}
first = false
}
resp.join("=")
}
pub fn read_tags(path: &String) -> db::Metadata {
let mut meta = db::Metadata {
duration: 0,
..db::Metadata::default()
};
if let Ok(child) = Command::new("ffprobe")
.arg("-hide_banner")
.arg("-v").arg("quiet")
.arg("-show_entries").arg("format")
.arg(path)
.stdout(Stdio::piped())
.spawn() {
let out = child.stdout.unwrap();
let lines = BufReader::new(out).lines().filter_map(|line| line.ok());
for line in lines {
if line.starts_with("duration=") {
let val = get_val(line);
match val.parse::<f32>() {
Ok(v) => {
meta.duration = v as u32;
},
Err(_) => { }
}
} else if line.starts_with("TAG:title=") {
meta.title = get_val(line);
} else if line.starts_with("TAG:artist=") {
meta.artist = get_val(line);
} else if line.starts_with("TAG:album=") {
meta.album = get_val(line);
} else if line.starts_with("TAG:album_artist=") {
meta.album_artist = get_val(line);
} else if line.starts_with("TAG:genre=") {
meta.genre = get_val(line);
}
}
}
meta
}
impl DecoderTrait for FFmpegCmdDecoder {
fn decode(path: &Path) -> BlissResult<PreAnalyzedSong> {
let binding = path.to_string_lossy();
// First check if this is a CUE file track - which will have start and duration
let mut parts = binding.split(TIME_SEP);
if parts.clone().count()==3 {
if let Ok(child) = Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-loglevel").arg("panic")
.arg("-i").arg(parts.next().unwrap_or(""))
.arg("-ss").arg(parts.next().unwrap_or(""))
.arg("-t").arg(parts.next().unwrap_or(""))
.arg("-ar").arg("22050")
.arg("-ac").arg("1")
.arg("-c:a")
.arg("pcm_f32le")
.arg("-f").arg("f32le")
.arg("pipe:1")
.stdout(Stdio::piped())
.spawn() {
return handle_command(child);
}
} else {
if let Ok(child) = Command::new("ffmpeg")
.arg("-hide_banner")
.arg("-loglevel").arg("panic")
.arg("-i").arg(path)
.arg("-ar").arg("22050")
.arg("-ac").arg("1")
.arg("-c:a")
.arg("pcm_f32le")
.arg("-f").arg("f32le")
.arg("pipe:1")
.stdout(Stdio::piped())
.spawn() {
return handle_command(child);
}
}
Err(BlissError::DecodingError("ffmpeg command failed".to_string()))
}
}
+64 -25
View File
@@ -14,13 +14,9 @@ use log::LevelFilter;
use std::io::Write;
use std::path::PathBuf;
use std::process;
#[cfg(feature = "ffmpeg")]
use which::which;
use num_cpus;
mod analyse;
mod cue;
mod db;
#[cfg(feature = "ffmpeg")]
mod ffmpeg;
mod tags;
mod upload;
@@ -32,6 +28,10 @@ fn main() {
let mut db_path = "bliss.db".to_string();
let mut logging = "info".to_string();
let mut music_path = ".".to_string();
let mut music_1_path = "".to_string();
let mut music_2_path = "".to_string();
let mut music_3_path = "".to_string();
let mut music_4_path = "".to_string();
let mut ignore_file = "ignore.txt".to_string();
let mut keep_old: bool = false;
let mut dry_run: bool = false;
@@ -40,8 +40,11 @@ fn main() {
let mut lms_json_port:u16 = 9000;
let mut max_num_files: usize = 0;
let mut music_paths: Vec<PathBuf> = Vec::new();
let mut max_threads: usize = 0;
let mut use_tags = false;
let mut max_threads: i16 = 0;
let mut write_tags = false;
let mut read_tags = false;
let mut preserve_mod_times = false;
let mut send_notifs = false;
match dirs::home_dir() {
Some(path) => {
@@ -53,6 +56,7 @@ fn main() {
{
let config_file_help = format!("config file (default: {})", &config_file);
let music_path_help = format!("Music folder (default: {})", &music_path);
let alt_music_path_help = format!("Other music folder");
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 contains items to mark as ignored. (default: {})", ignore_file);
@@ -66,6 +70,10 @@ fn main() {
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 music_1_path).add_option(&["--music_1"], Store, &alt_music_path_help);
arg_parse.refer(&mut music_2_path).add_option(&["--music_2"], Store, &alt_music_path_help);
arg_parse.refer(&mut music_3_path).add_option(&["--music_3"], Store, &alt_music_path_help);
arg_parse.refer(&mut music_4_path).add_option(&["--music_4"], Store, &alt_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 files from DB if they don't exist (used with analyse task)");
@@ -75,8 +83,11 @@ fn main() {
arg_parse.refer(&mut lms_json_port).add_option(&["-J", "--json"], Store, &lms_json_port_help);
arg_parse.refer(&mut max_num_files).add_option(&["-n", "--numfiles"], Store, "Maximum number of files to analyse");
arg_parse.refer(&mut max_threads).add_option(&["-t", "--threads"], Store, "Maximum number of threads to use for analysis");
arg_parse.refer(&mut use_tags).add_option(&["-T", "--tags"], StoreTrue, "Read/write analysis results from/to source files");
arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, stopmixer.");
arg_parse.refer(&mut write_tags).add_option(&["-W", "--writetags"], StoreTrue, "When analysing files, also store results within files themselves");
arg_parse.refer(&mut read_tags).add_option(&["-R", "--readtags"], StoreTrue, "When analysing files, attempt to read results from tags");
arg_parse.refer(&mut preserve_mod_times).add_option(&["-p", "--preserve"], StoreTrue, "Preserve modification time when writing results to files");
arg_parse.refer(&mut send_notifs).add_option(&["-N", "--notifs"], StoreTrue, "Periodically send notification messages to LMS");
arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, export, clearfail, stopmixer.");
arg_parse.parse_args_or_exit();
}
@@ -88,31 +99,23 @@ fn main() {
let mut builder = env_logger::Builder::from_env(env_logger::Env::default().filter_or("XXXXXXXX", logging));
builder.filter(Some("bliss_audio"), other_level);
builder.filter(Some("symphonia"), other_level);
builder.filter(Some("lofty"), other_level);
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() {
log::error!("No task specified, please choose from; analyse, tags, ignore, upload");
log::error!("No task specified, please choose from; analyse, tags, ignore, upload, export, clearfail, stopmixer");
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") {
&& !task.eq_ignore_ascii_case("upload") && !task.eq_ignore_ascii_case("export") && !task.eq_ignore_ascii_case("stopmixer") && !task.eq_ignore_ascii_case("analyse-lms") {
log::error!("Invalid task ({}) supplied", task);
process::exit(-1);
}
// Ensure ffmpeg is in PATH...
#[cfg(feature = "ffmpeg")]
match which("ffmpeg") {
Ok(_) => { }
Err(_) => {
log::error!("'ffmpeg' was not found! Please ensure this in your PATH");
process::exit(-1);
},
}
if !config_file.is_empty() {
let path = PathBuf::from(&config_file);
@@ -143,8 +146,16 @@ fn main() {
Some(val) => { ignore_file = val; }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "tags") {
Some(val) => { use_tags = val.eq("true"); }
match config.get(TOP_LEVEL_INI_TAG, "writetags") {
Some(val) => { write_tags = val.eq("true"); }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "readtags") {
Some(val) => { read_tags = val.eq("true"); }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "preserve") {
Some(val) => { preserve_mod_times = val.eq("true"); }
None => { }
}
}
@@ -158,6 +169,25 @@ fn main() {
if music_paths.is_empty() {
music_paths.push(PathBuf::from(&music_path));
if !music_1_path.is_empty() {
music_paths.push(PathBuf::from(&music_1_path));
}
if !music_2_path.is_empty() {
music_paths.push(PathBuf::from(&music_2_path));
}
if !music_3_path.is_empty() {
music_paths.push(PathBuf::from(&music_3_path));
}
if !music_4_path.is_empty() {
music_paths.push(PathBuf::from(&music_4_path));
}
}
let mut thread_limit:usize = num_cpus::get() as usize;
if max_threads<0 && (max_threads.abs() as usize) < thread_limit-1 {
thread_limit = thread_limit - (max_threads.abs() as usize)
} else if max_threads>0 && (max_threads as usize) < thread_limit {
thread_limit = max_threads as usize;
}
if task.eq_ignore_ascii_case("stopmixer") {
@@ -194,7 +224,7 @@ fn main() {
}
if task.eq_ignore_ascii_case("tags") {
analyse::read_tags(&db_path, &music_paths);
db::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() {
@@ -205,10 +235,19 @@ fn main() {
log::error!("Ignore file ({}) is not a file", ignore_file);
process::exit(-1);
}
analyse::update_ignore(&db_path, &ignore_path);
db::update_ignore(&db_path, &ignore_path);
} else if task.eq_ignore_ascii_case("export") {
db::export(&db_path, &music_paths, thread_limit, preserve_mod_times);
} else if task.eq_ignore_ascii_case("clearfail") {
db::clear_failures(&db_path);
} else {
let ignore_path = PathBuf::from(&ignore_file);
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads, &ignore_path, use_tags);
let modified = analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, thread_limit,
&ignore_path, read_tags, write_tags, preserve_mod_times, &lms_host, lms_json_port,
send_notifs);
if modified && task.eq_ignore_ascii_case("analyse-lms") && path.exists() {
upload::stop_mixer(&lms_host, lms_json_port);
}
}
}
}
+158 -60
View File
@@ -12,23 +12,31 @@ use lofty::file::FileType;
use lofty::prelude::{Accessor, AudioFile, ItemKey, TagExt, TaggedFileExt};
use lofty::tag::{ItemValue, Tag, TagItem};
use regex::Regex;
use std::fs::File;
use std::fs;
use std::path::Path;
use substring::Substring;
use bliss_audio::{Analysis, AnalysisIndex};
use std::time::SystemTime;
use bliss_audio::{Analysis, AnalysisIndex, FeaturesVersion};
const MAX_GENRE_VAL: usize = 192;
const NUM_ANALYSIS_VALS: usize = 20;
const ANALYSIS_TAG:ItemKey = ItemKey::Comment;
const ANALYSIS_TAG_START: &str = "BLISS_ANALYSIS";
const ANALYSIS_TAG_VER: u16 = 1;
const NUM_ANALYSIS_VALS: usize = 23;
const ANALYSIS_TAG: &str = "BLISS_ANALYSIS";
const ANALYSIS_TAG_FORMAT_VER: u16 = 2;
pub fn write_analysis(track: &String, analysis: &Analysis) {
let value = format!("{},{},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24},{:.24}", ANALYSIS_TAG_START, ANALYSIS_TAG_VER,
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],
analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5],
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10]);
fn fmt(val: f32) -> String {
format!("{:.16}", val).trim_end_matches("0").to_string()
}
pub fn write_analysis(track: &String, analysis: &Analysis, preserve_mod_times: bool) -> bool {
let value = format!("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", ANALYSIS_TAG_FORMAT_VER,
fmt(analysis[AnalysisIndex::Tempo]), fmt(analysis[AnalysisIndex::Zcr]), fmt(analysis[AnalysisIndex::MeanSpectralCentroid]), fmt(analysis[AnalysisIndex::StdDeviationSpectralCentroid]), fmt(analysis[AnalysisIndex::MeanSpectralRolloff]),
fmt(analysis[AnalysisIndex::StdDeviationSpectralRolloff]), fmt(analysis[AnalysisIndex::MeanSpectralFlatness]), fmt(analysis[AnalysisIndex::StdDeviationSpectralFlatness]), fmt(analysis[AnalysisIndex::MeanLoudness]), fmt(analysis[AnalysisIndex::StdDeviationLoudness]),
fmt(analysis[AnalysisIndex::Chroma1]), fmt(analysis[AnalysisIndex::Chroma2]), fmt(analysis[AnalysisIndex::Chroma3]), fmt(analysis[AnalysisIndex::Chroma4]), fmt(analysis[AnalysisIndex::Chroma5]),
fmt(analysis[AnalysisIndex::Chroma6]), fmt(analysis[AnalysisIndex::Chroma7]), fmt(analysis[AnalysisIndex::Chroma8]), fmt(analysis[AnalysisIndex::Chroma9]), fmt(analysis[AnalysisIndex::Chroma10]),
fmt(analysis[AnalysisIndex::Chroma11]), fmt(analysis[AnalysisIndex::Chroma12]), fmt(analysis[AnalysisIndex::Chroma13]));
let mut written = false;
if let Ok(mut file) = lofty::read_from_path(Path::new(track)) {
let tag = match file.primary_tag_mut() {
Some(primary_tag) => primary_tag,
@@ -43,23 +51,96 @@ pub fn write_analysis(track: &String, analysis: &Analysis) {
},
};
// Remove any existing analysis result tag
let entries = tag.get_strings(&ANALYSIS_TAG);
// Store analysis results
let tag_key = ItemKey::Unknown(ANALYSIS_TAG.to_string());
tag.remove_key(&tag_key);
let lower_tag_key = ItemKey::Unknown(ANALYSIS_TAG.to_lowercase().to_string());
tag.remove_key(&lower_tag_key);
tag.insert_unchecked(TagItem::new(tag_key, ItemValue::Text(value)));
// If we have any of the older analysis-in-comment tags, then remove these
let entries = tag.get_strings(&ItemKey::Comment);
let mut keep: Vec<ItemValue> = Vec::new();
let mut have_old = false;
for entry in entries {
if !entry.starts_with(ANALYSIS_TAG_START) {
if entry.starts_with(ANALYSIS_TAG) {
have_old = true;
} else {
keep.push(ItemValue::Text(entry.to_string()));
}
}
tag.remove_key(&ANALYSIS_TAG);
for k in keep {
tag.push(TagItem::new(ANALYSIS_TAG, k));
if have_old {
tag.remove_key(&ItemKey::Comment);
for k in keep {
tag.push(TagItem::new(ItemKey::Comment, k));
}
}
// Store analysis results
tag.push(TagItem::new(ANALYSIS_TAG, ItemValue::Text(value)));
let _ = tag.save_to_path(Path::new(track), WriteOptions::default());
let now = SystemTime::now();
let mut mod_time = now;
if preserve_mod_times {
if let Ok(fmeta) = fs::metadata(track) {
if let Ok(time) = fmeta.modified() {
mod_time = time;
}
}
}
if let Ok(_) = tag.save_to_path(Path::new(track), WriteOptions::default()) {
if preserve_mod_times {
if mod_time<now {
if let Ok(f) = File::open(track) {
let _ = f.set_modified(mod_time);
}
}
}
written = true;
}
}
written
}
fn read_analysis_string(tag_str: &str, start_tag_pos:usize, version_pos:usize) -> Option<Analysis> {
let parts = tag_str.split(",");
let mut index = 0;
let mut num_read_vals = 0;
let mut vals = [0.; NUM_ANALYSIS_VALS];
let val_start_pos = version_pos+1;
for part in parts {
if index==start_tag_pos && start_tag_pos<version_pos {
if part!=ANALYSIS_TAG {
break;
}
} else if index==version_pos {
match part.parse::<u16>() {
Ok(ver) => {
if ver!=ANALYSIS_TAG_FORMAT_VER {
break;
}
},
Err(_) => {
break;
}
}
} else if (index - val_start_pos) < NUM_ANALYSIS_VALS {
match part.parse::<f32>() {
Ok(val) => {
num_read_vals += 1;
vals[index - val_start_pos] = val;
},
Err(_) => {
break;
}
}
} else {
break;
}
index += 1;
}
if num_read_vals == NUM_ANALYSIS_VALS {
return Some(Analysis::new(vals.to_vec(), FeaturesVersion::LATEST).expect("number of vals should be 23"));
}
None
}
pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
@@ -71,14 +152,32 @@ pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
if let Ok(file) = lofty::read_from_path(Path::new(track)) {
let tag = match file.primary_tag() {
Some(primary_tag) => primary_tag,
None => file.first_tag().expect("Error: No tags found!"),
None => {
if let Some(first_tag) = file.first_tag() {
first_tag
} else {
return meta;
}
}
};
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();
// If file has multiple genre tags then read all.
let genres = tag.get_strings(&ItemKey::Genre);
let mut genre_list:Vec<String> = Vec::new();
for genre in genres {
genre_list.push(genre.to_string());
}
if genre_list.len()>1 {
meta.genre = genre_list.join(";");
} else {
meta.genre = tag.genre().unwrap_or_default().to_string();
}
// Check whether MP3 has numeric genre, and if so covert to text
if file.file_type().eq(&FileType::Mpeg) {
@@ -103,8 +202,7 @@ pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
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();
}
}
}
@@ -121,45 +219,45 @@ pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
meta.duration = file.properties().duration().as_secs() as u32;
if read_analysis {
let entries = tag.get_strings(&ANALYSIS_TAG);
for entry in entries {
if entry.len()>(ANALYSIS_TAG_START.len()+(NUM_ANALYSIS_VALS*8)) && entry.starts_with(ANALYSIS_TAG_START) {
let parts = entry.split(",");
let mut index = 0;
let mut vals = [0.; NUM_ANALYSIS_VALS];
for part in parts {
if 0==index {
if part!=ANALYSIS_TAG_START {
match tag.get_string(&ItemKey::Unknown(ANALYSIS_TAG.to_string())) {
Some(tag_str) => {
match read_analysis_string(tag_str, 100, 0) {
Some(analysis) => {
meta.analysis = Some(analysis);
}
None => { }
}
}
None => { }
}
if meta.analysis.is_none() {
// Try lowercase
match tag.get_string(&ItemKey::Unknown(ANALYSIS_TAG.to_lowercase().to_string())) {
Some(tag_str) => {
match read_analysis_string(tag_str, 100, 0) {
Some(analysis) => {
meta.analysis = Some(analysis);
}
None => { }
}
}
None => { }
}
}
if meta.analysis.is_none() {
// Try old, stored in comment
let entries = tag.get_strings(&ItemKey::Comment);
for entry in entries {
if entry.len()>(ANALYSIS_TAG.len()+(NUM_ANALYSIS_VALS*8)) && entry.starts_with(ANALYSIS_TAG) {
match read_analysis_string(entry, 0, 1) {
Some(analysis) => {
meta.analysis = Some(analysis);
break;
}
} else if 1==index {
match part.parse::<u16>() {
Ok(ver) => {
if ver!=ANALYSIS_TAG_VER {
break;
}
},
Err(_) => {
break;
}
}
} else if (index - 2) < NUM_ANALYSIS_VALS {
match part.parse::<f32>() {
Ok(val) => {
vals[index - 2] = val;
},
Err(_) => {
break;
}
}
} else {
break;
None => { }
}
index += 1;
}
if index == (NUM_ANALYSIS_VALS+2) {
meta.analysis = Some(Analysis::new(vals));
break;
}
}
}