92 Commits
Author SHA1 Message Date
CraigD 85a2e985bf Merge pull request #19 from Polochon-street/add-rpi-feature
Add rpi feature
2025-03-20 17:46:51 +00:00
Polochon_street c999297430 Add rpi feature 2025-03-20 18:13:03 +01:00
CDrummond db9b12be3f Update 2025-03-17 18:16:39 +00:00
CDrummond 2468a83d4b Reduce code duplication 2025-03-17 17:55:24 +00:00
CDrummond 22149c2349 Document build steps for Pi 2025-03-16 17:04:07 +00:00
CDrummond 0dda8a17e7 bliss-rs 0.10.0 2025-03-16 08:17:48 +00:00
CDrummond a2ff1aade1 Specify git rev until release made 2025-03-15 10:59:40 +00:00
CDrummond 471063b53d Copy docs and example config for mac builds! 2025-03-15 10:22:34 +00:00
CDrummond 94011d12ea _Slightly_ slower 2025-03-15 09:33:32 +00:00
CDrummond 2210c44614 Quiten symphonia log messages 2025-03-15 09:27:35 +00:00
CDrummond 00be6015f7 Symphonia is not actually that slow... 2025-03-15 09:24:52 +00:00
CDrummond e4522cc683 Add symphonia builds 2025-03-14 12:17:52 +00:00
CDrummond 5c7b33a5d3 Use correct name 2025-03-09 15:22:34 +00:00
CDrummond 6fd78df3ef Fix build 2025-03-09 13:26:39 +00:00
CDrummond f23202e956 Document tags option 2025-03-09 13:26:21 +00:00
CDrummond 7ea6c3befd Add excplcit 'ffmpeg' feature flag. 2025-03-09 08:02:37 +00:00
CDrummond 302609ed1b Update lofty version 2025-03-05 19:10:47 +00:00
CDrummond 5e5bd9841a ...and use ffprobe when updating tags. 2025-03-05 18:43:05 +00:00
CDrummond 32610f22b7 Use ffprobe to read meta-data if not compiled against libav
Closes #18
2025-03-05 18:39:54 +00:00
CDrummond 751dca7091 Enable DSF support for libav builds 2025-03-05 17:58:19 +00:00
CDrummond ac87ebb66c When saving anlysis results to a comment tag, remove any previous
results.
2025-03-05 17:40:25 +00:00
CDrummond 98b9cd61e0 Only stop when found correct BLISS_ANALYSIS comment 2025-03-05 17:37:51 +00:00
CDrummond 786b7d2c2d If log level set to 'trace' then set this level for the Bliss library too.
Issue #17
2025-03-05 17:09:52 +00:00
CDrummond 1e00e9593a Spelling 2025-03-05 17:01:48 +00:00
CDrummond 0d877fe79e Save analysis in a new COMMENT tag.
Closes #4
2025-03-04 21:57:06 +00:00
CDrummond 6cc26c399e Add option to write analysis results to files, and use for future scans.
Issue #4
2025-03-04 20:04:37 +00:00
CDrummond bff4ba18b4 Update (C) year 2025-03-04 19:17:24 +00:00
CDrummond 6fc9ac7f2e Fix output format 2025-03-04 19:15:12 +00:00
CDrummond 3a387be3bf If new files analysed and 'ignore' file exists then update DB's 'ignore' flags.
Closes #9
2025-03-04 19:06:59 +00:00
CDrummond 2cb7dc0fa0 Add ability to specify LMS JSONRPC port.
Closes #16
2025-03-04 19:01:20 +00:00
CDrummond b91c2edafd Spelling 2025-03-04 18:06:33 +00:00
CDrummond f559abb395 Mention statically linked variant. 2025-03-04 17:59:15 +00:00
CDrummond cca929a13e Need to use libav with staticlibav 2025-03-04 17:40:02 +00:00
CDrummond 6dcd61d9bb Remove bliss-analyser-mac-libav 2025-03-04 13:51:08 +00:00
CDrummond f8ea9947ac Nope, cant build ffmpeg for macOS :( 2025-03-04 13:46:13 +00:00
CDrummond b275568000 Static libav failed on macOS, try dynamic? 2025-03-04 13:25:45 +00:00
CDrummond 0249bf8edb nasm for macOS 2025-03-04 13:10:11 +00:00
CDrummond 6d5b7c80ef Fix syntax 2025-03-04 12:48:44 +00:00
CDrummond b078736224 staticlibav 2025-03-04 12:47:21 +00:00
CDrummond a610dab7c0 ...and for other binary! 2025-03-03 22:09:42 +00:00
CDrummond 05fb19e2f9 update-aubio-bindings 3rd try... 2025-03-03 22:06:00 +00:00
CDrummond 375abe91d0 bliss-audio-aubio-sys/bindgen? 2025-03-03 22:01:39 +00:00
CDrummond f9984e8ca0 bindgen? 2025-03-03 21:57:16 +00:00
CDrummond 73d4ae72cd Fix job IDs 2025-03-03 21:48:00 +00:00
CDrummond 92c2a90d25 Fix indentation 2025-03-03 21:45:09 +00:00
CDrummond dbe36c35db Fat binaries for macOS 2025-03-02 22:56:15 +00:00
CDrummond decbbd18e5 Update 2025-03-02 22:55:55 +00:00
CDrummond 36f44d6550 Mention libav/ffmpeg variants 2025-03-02 22:54:57 +00:00
CDrummond 01be5d107a Put libav/ffmpeg last 2025-03-02 22:44:48 +00:00
CDrummond 388e571571 Rename jobs, and artifacts, to match OS 2025-03-02 22:35:17 +00:00
CDrummond 5f247ef10f Fix build, as it was copied from ARM scripts. 2025-03-02 22:14:51 +00:00
CDrummond eb7214644a Oops! Should not have been added! 2025-03-02 21:50:00 +00:00
CDrummond 25362b9635 Rename jobs 2025-03-03 21:38:21 +00:00
CDrummond 4a98282ce2 Fix names, again! 2025-03-03 21:37:01 +00:00
CDrummond 331d770d46 Fix target 2025-03-03 21:28:43 +00:00
CDrummond 0d1c33b131 Fix command 2025-03-03 21:23:01 +00:00
CDrummond c9d72b3f71 Fix artifact name 2025-03-03 21:22:50 +00:00
CDrummond bfd73ea0bc actions/upload-artifact@v4 2025-03-03 21:15:34 +00:00
CDrummond b643272d1f Update versions 2025-03-03 21:13:51 +00:00
CDrummond 9321be0ad5 Build libav and ffmpeg variants 2025-03-03 18:29:06 +00:00
CDrummond 55f944fbdd Remove macOS deps 2025-03-03 18:09:30 +00:00
CDrummond d7d7c87582 Re-enable mac builds, but use ffmpeg from commandline 2025-03-03 17:34:42 +00:00
CDrummond 4e6d522829 Update github workflow scripts 2025-03-03 17:33:12 +00:00
CDrummond 25f399f932 Document libav feature 2025-03-03 17:26:38 +00:00
CDrummond a5db8f48ce Reduce duplication 2025-03-03 17:14:31 +00:00
CDrummond 136651ada7 Add cargo features flag to enable libav usage. 2025-03-03 17:06:09 +00:00
CDrummond e5ef67f8c6 Sort directory items, and do limiting when collecting files to analyse. 2025-03-02 22:10:55 +00:00
CDrummond 6b9cb960a9 Add CUE support for commandline ffmpeg usage. 2025-03-02 20:57:59 +00:00
CDrummond c6f9a7faf5 Only invoke "stty sane" if not Windows. 2025-03-02 20:56:47 +00:00
CDrummond 2ac4b9d17f Avoid pushing and reallocating all the time. 2025-03-02 17:29:17 +00:00
CDrummond 7071ea14e7 Reset terminal 2025-03-02 09:40:57 +00:00
CDrummond e23aba88e3 Have ffmpeg output to a pipe, and read from that - saves creating temp
files.
2025-03-02 08:57:08 +00:00
CDrummond 5fa745975c Check ffmpeg is in PATH 2025-03-01 16:06:07 +00:00
CDrummond 22133436b9 Use 'ffmpeg' commandline to decode files, and not ffmpeg libraries. 2025-03-01 15:45:47 +00:00
CDrummond 68ed1fc3b0 Update version of bliss library. 2024-11-02 08:58:50 +00:00
CDrummond 3b465543d7 Update 2024-04-28 13:32:20 +01:00
CraigD 56ed0b9045 Merge pull request #13 from terual/wavpack
Add support for (DSD) WavPack
2024-04-28 12:30:47 +00:00
Bart Lauret 62c956bc78 Add support for (DSD) WavPack
- Added wv as valid extension
- lofty had incorrect duration scanning for DSD Wavpack in version 0.15.0, so version bumped to 0.16.0
2024-04-28 11:55:34 +02:00
CraigD a880fff993 Merge pull request #12 from chincheta0815/docker_ubuntu
make ubuntu a docker build for ffmpeg5
2024-02-22 17:37:47 +00:00
chincheta0815 777d8e3c6c make ubuntu a docker build for ffmpeg5 2024-02-22 07:00:03 +01:00
CraigD 319f72d2a4 Merge pull request #11 from chincheta0815/bookworm_and_bullseye
add builds for bullseye which requires newer ffmpeg
2024-02-16 17:34:06 +00:00
chincheta0815 297ed3304e add builds for bullseye which requires newer ffmpeg 2024-02-16 15:13:06 +01:00
CDrummond 04baedbfc2 Don't try to download ffmpeg5 or macOS builds - currently broken 2023-08-11 17:16:41 +01:00
CDrummond 609b6faff9 So, ffmpeg5 ppa is now paywalled so cannot use. 2023-08-10 21:25:21 +01:00
CDrummond fe65d35066 Revert ffmpeg apt steps, and disable macOS builds (failing!) 2023-08-10 21:07:23 +01:00
CDrummond 75aeee128f Use 22.04 for ffmpeg5? 2023-08-10 20:50:46 +01:00
CDrummond 65e40acce4 Update bliss and lofty libraries 2023-08-10 20:08:17 +01:00
CDrummond a9f819f39b Update version of bliss library. 2023-02-28 15:48:16 +00:00
CDrummond 4704624b2f Add option to limit number of concurrent threads. 2023-02-28 15:41:01 +00:00
CDrummond c3083d9a1a Update (C) year 2023-01-02 09:28:26 +00:00
CDrummond 90d4dfec23 Nope, revert back to bullseye as build fails on stretch 2022-12-20 21:50:30 +00:00
CDrummond e89aee1f4e Build on stretch? 2022-12-20 21:40:58 +00:00
26 changed files with 2502 additions and 597 deletions

No files matched your search

+367 -36
View File
@@ -7,19 +7,73 @@ env:
CARGO_TERM_COLOR: always
jobs:
Linux_ffmpeg4:
runs-on: ubuntu-20.04
Linux_ffmpeg_x86:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build x86 ffmpeg version
run: |
cargo build --release --features=ffmpeg
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-ffmpeg
path: releases/
Linux_static_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=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
- name: Build 22.04 libav version
run: |
cargo build --release
cargo build --release --features=libav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
@@ -28,24 +82,25 @@ jobs:
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-x86-ffmpeg4
name: bliss-analyser-ubuntu-22.04-x86-libav
path: releases/
Linux_ffmpeg5:
runs-on: ubuntu-20.04
Ubuntu_2404_libav_x86:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Packages
run: sudo add-apt-repository ppa:savoury1/ffmpeg4 -y && sudo add-apt-repository ppa:savoury1/ffmpeg5 -y && 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
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
cargo build --release --features=libav
strip target/release/bliss-analyser
mkdir releases
cp target/release/bliss-analyser releases/bliss-analyser
@@ -54,32 +109,259 @@ jobs:
cp configs/linux.ini releases/config.ini
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-x86-ffmpeg5
name: bliss-analyser-ubuntu-24.04-x86-libav
path: releases/
Linux_ARM:
runs-on: ubuntu-20.04
Linux_symphonia_x86:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Build ARM
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build x86 static-libav version
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile
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@v2
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-arm
name: bliss-analyser-linux-arm-ffmpeg
path: releases/
macOS:
runs-on: macos-11.0
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
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-linux-arm-symphonia
path: releases/
macOS_ffmpeg:
runs-on: macos-13
steps:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Checkout
uses: actions/checkout@v2
- name: Install Rust support for ARM64 & prepare environment
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
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-mac-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
steps:
- name: Install Rust
@@ -92,26 +374,44 @@ jobs:
- name: Install deps
run: |
brew install ffmpeg@5
brew install pkg-config
- name: Install Rust support for ARM64 & prepare environment
run: |
rustup target add aarch64-apple-darwin
mkdir releases
- name: Build
run: |
mkdir releases
cargo build --release
cargo build --release --features update-aubio-bindings,symphonia
strip target/release/bliss-analyser
cp target/release/bliss-analyser releases/bliss-analyser
cp target/release/bliss-analyser releases/bliss-analyser-x86_64
cargo build --target=aarch64-apple-darwin --release --features update-aubio-bindings,symphonia
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@v2
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-mac
name: bliss-analyser-mac-symphonia
path: releases/
Windows:
Windows_libav:
runs-on: windows-2019
steps:
@@ -138,7 +438,7 @@ jobs:
- name: Build
run: |
cargo build --release
cargo build --release --features=libav
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
cp deps/bin/*.dll releases/
@@ -148,7 +448,38 @@ jobs:
cp c:\Windows\system32\vcruntime140.dll releases
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: bliss-analyser-windows
name: bliss-analyser-windows-libav
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
+888 -402
View File
File diff suppressed because it is too large. Load diff
+23 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "bliss-analyser"
version = "0.2.2"
version = "0.3.0"
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
@@ -10,18 +10,36 @@ keywords = ["audio", "song", "similarity"]
readme = "README.md"
[dependencies]
bliss-audio = { version = "0.6.5", features = [] }
argparse = "0.2.2"
anyhow = "1.0.40"
rusqlite = { version = "0.25.0", features = ["bundled"] }
rusqlite = { version = "0.28.0", features = ["bundled"] }
log = "0.4.14"
env_logger = "0.8.4"
indicatif = "0.16.2"
lofty = "0.9.0"
lofty = "0.22.2"
dirs = "1"
chrono = "0.4.19"
chrono = "0.4.40"
regex = "1"
substring = "1.4.5"
ureq = "2.4.0"
configparser = "3.0.0"
if_chain = "1.0.2"
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 }
[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"
#git = "https://github.com/Polochon-street/bliss-rs.git"
#rev = "006927ac16752ff2e00bfe0d6b7756f67fa822c0"
+20
View File
@@ -1,3 +1,23 @@
0.3.0
-----
1. Add support for (DSD) WavPack - thanks to Bart Lauret
2. Update version of tag reader library.
3. Update version of bliss library.
4. Allow builds to either use dynamic ffmpeg libraries, static ffmpeg
libraries, symphonia, or ffmpeg on commandline.
5. Add ability to specify LMS JSONRPC port.
6. If new files analysed and 'ignore' file exists then update DB's 'ignore'
flags.
7. Add option to write analysis results to files, and use for future scans.
8. If log level set to 'trace' then set this level for the bliss library too.
9. Enable support for '.dsf' files.
0.2.3
-----
1. Add option to limit number of concurrent threads.
2. Update version of tag reader library.
3. Update version of bliss library.
0.2.2
-----
1. Update version of tag reader library.
+99 -2
View File
@@ -7,7 +7,31 @@ bliss analysis. This is then intended to be used by [Bliss Mixer](https://github
# Building
clang, pkg-config, and ffmpeg are required to build, as well as
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
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.
## Build for 'libavcodec' library usage
`clang`, `pkg-config`, and `ffmpeg` are required to build, as well as
[Rust](https://www.rust-lang.org/tools/install)
To install dependencies on a Debian system:
@@ -21,7 +45,80 @@ To install dependencies on a Fedora system:
dnf install ffmpeg-devel clang pkg-config
```
Build with `cargo build --release`
Build with `cargo build --release --features=libav`
If building on a Raspberry Pi, then `rpi` also needs to be passed to `--features`, e.g.
`cargo build --release --features=libav,rpi`
The resultant application will be less portable, due to dependencies on `libavcodec` libraries (and
their dependencies).
## Build for 'libavcodec' library usage, statically linked
`clang`, `pkg-config`, and `ffmpeg` 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 libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev pkg-config
```
To install dependencies on a Fedora system:
```
dnf install ffmpeg-devel clang pkg-config
```
Build with `cargo build --release --features=libav,libavstatic`
If building on a Raspberry Pi, then `rpi` also needs to be passed to `--features`, e.g.
`cargo build --release --features=libav,libavstatic,rpi`
## Build for 'symphonia'
`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=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
+46 -8
View File
@@ -10,14 +10,43 @@ 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.
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 appropiate values
3. Edit the supplied `config.ini` in the current folder to set appropriate values
for `music` and `lms` - e.g.:
```
[Bliss]
@@ -45,8 +74,10 @@ Installation
============
For Windows no extra installation steps are required, as all dependencies are
bundled within its ZIP file. However, both the Linux and macOS versions require
that `ffmpeg` be installed.
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
@@ -100,6 +131,7 @@ music=/home/user/Music
db=bliss.db
lms=127.0.0.1
ignore=ignore.txt
tags=true
```
The following items are supported:
@@ -111,9 +143,13 @@ 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.
* `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`
Command-line parameters
@@ -134,9 +170,11 @@ analysis will be performed, instead the logging will inform you how many new
tracks are to be analysed and how many old tracks are left in the database.
* `-i` / `--ignore` Name and location of the file containing items to ignore.
* `-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.
Equivalent items specied in the INI config file (detailed above) will override
Equivalent items specified in the INI config file (detailed above) will override
any specified on the commandline.
`bliss-analyser` requires one extra parameter, which is used to determine the
@@ -181,7 +219,7 @@ tracks/hour.
CUE files
---------
If the anlyser encounters an audio file with a matching CUE file (e.g.
If the analyser encounters an audio file with a matching CUE file (e.g.
`album.flac` and `album.cue` in same folder) then it will attempt to analyse the
individual tracks contained within.
@@ -302,7 +340,7 @@ The SQL LIKE lines do sub-string matching. So '%Dance%' will match any genre
string that contains 'Dance' - e.g. 'Classical Dance'. The 4 lines with 'Rock'
show how you can explicitly look for an exact match. The 1st line means 'Rock'
is the only genre, 2nd means 'Rock' is the first genre, 3rd means 'Rock' is the
last genre, and 4th means 'Rock' is amongs other genres.
last genre, and 4th means 'Rock' is amongst other genres.
Assuming `config.ini` is in the current folder and contains valid entries, this
is accomplished as follows:
+34
View File
@@ -0,0 +1,34 @@
# 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
@@ -0,0 +1,38 @@
# 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"]
+30
View File
@@ -0,0 +1,30 @@
# Cross compilation environment for bliss-analyser
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 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"]
@@ -1,4 +1,4 @@
# Cross compilation environment for bliss-mixer
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
@@ -32,5 +32,5 @@ ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build.sh"]
CMD ["/src/docker/docker-build-arm-libav.sh"]
+36
View File
@@ -0,0 +1,36 @@
# 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"]
+30
View File
@@ -0,0 +1,30 @@
# 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 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-symphonia.sh"]
+25
View File
@@ -0,0 +1,25 @@
# 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"]
+29
View File
@@ -0,0 +1,29 @@
#!/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=ffmpeg --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
@@ -14,7 +14,7 @@ function build {
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 --target $1
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
+29
View File
@@ -0,0 +1,29 @@
#!/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
+29
View File
@@ -0,0 +1,29 @@
#!/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=symphonia --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
+18
View File
@@ -0,0 +1,18 @@
#!/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
+13 -6
View File
@@ -3,7 +3,7 @@
#
# LMS-BlissMixer
#
# Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
# Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
# MIT license.
#
@@ -11,7 +11,13 @@ import datetime, os, requests, shutil, subprocess, sys, tempfile, time
GITHUB_TOKEN_FILE = "%s/.config/github-token" % os.path.expanduser('~')
GITHUB_REPO = "CDrummond/bliss-analyser"
GITHUB_ARTIFACTS = ["bliss-analyser-linux-x86-ffmpeg4", "bliss-analyser-linux-x86-ffmpeg5", "bliss-analyser-linux-arm", "bliss-analyser-mac", "bliss-analyser-windows"]
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"]
UNIX_ARTIFACTS = LINUX_ARM_ARTIFACTS + LINUX_X86_ARTIFACTS + MAC_ARTIFACTS
GITHUB_ARTIFACTS = UNIX_ARTIFACTS + WINDOWS_ARTIFACTS
def info(s):
print("INFO: %s" %s)
@@ -64,12 +70,11 @@ def download_artifacts(version):
f.write(chunk)
if not os.path.exists(dest):
info("Failed to download %s" % item)
break
def make_executable(version):
cwd = os.getcwd()
for a in ["bliss-analyser-linux-x86-ffmpeg4", "bliss-analyser-linux-x86-ffmpeg5", "bliss-analyser-linux-arm", "bliss-analyser-mac"]:
for a in UNIX_ARTIFACTS:
archive = "%s-%s.zip" % (a, version)
info("Making analyser executable in %s" % archive)
with tempfile.TemporaryDirectory() as td:
@@ -77,8 +82,10 @@ def make_executable(version):
os.remove(archive)
os.chdir(td)
subprocess.call(["chmod", "a+x", "%s/bliss-analyser" % td], shell=False)
if a == "bliss-analyser-linux-arm":
subprocess.call(["chmod", "a+x", "%s/bin/bliss-analyser-armhf" % td, "%s/bin/bliss-analyser-aarch64" % td], shell=False)
bindir = os.path.join(td, "bin")
if os.path.isdir(bindir):
for e in os.listdir(bindir):
subprocess.call(["chmod", "a+x", os.path.join(bindir, e)], shell=False)
shutil.make_archive("%s/%s-%s" % (cwd, a, version), "zip")
os.chdir(cwd)
+339 -117
View File
@@ -1,52 +1,76 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use crate::cue;
use crate::db;
#[cfg(feature = "ffmpeg")]
use crate::ffmpeg;
use crate::tags;
use anyhow::Result;
use bliss_audio::{analyze_paths};
#[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 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};
const DONT_ANALYSE: &str = ".notmusic";
const MAX_ERRORS_TO_SHOW: usize = 100;
const MAX_TAG_ERRORS_TO_SHOW: usize = 50;
const VALID_EXTENSIONS: [&str; 5] = ["m4a", "mp3", "ogg", "flac", "opus"];
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>) {
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) {
if !path.is_dir() {
return;
}
if let Ok(items) = path.read_dir() {
for item in items {
if let Ok(entry) = item {
check_dir_entry(db, mpath, entry, track_paths);
}
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);
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>) {
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) {
let pb = entry.path();
if pb.is_dir() {
let check = pb.join(DONT_ANALYSE);
if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
} else {
get_file_list(db, mpath, &pb, track_paths);
} 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);
}
} else if pb.is_file() {
} else if pb.is_file() && (max_num_files<=0 || *file_count<max_num_files) {
if_chain! {
if let Some(ext) = pb.extension();
let ext = ext.to_string_lossy();
@@ -64,15 +88,43 @@ 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 {
track_paths.push(String::from(pb.to_string_lossy()));
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 {
track_paths.push(String::from(pb.to_string_lossy()));
*file_count+=1;
}
}
}
}
@@ -81,92 +133,7 @@ fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths:
}
}
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>) -> 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 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();
log::info!("Analysing new files");
for (path, result) in analyze_paths(track_paths) {
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));
let mut inc_progress = true; // Only want to increment progress once for cue tracks
match result {
Ok(track) => {
let cpath = String::from(path.to_string_lossy());
match track.cue_info {
Some(cue) => {
match track.track_number {
Some(track_num) => {
let t_num = track_num.parse::<i32>().unwrap();
if reported_cue.contains(&cpath) {
inc_progress = false;
} else {
analysed += 1;
reported_cue.insert(cpath);
}
let meta = db::Metadata {
title: track.title.unwrap_or_default().to_string(),
artist: track.artist.unwrap_or_default().to_string(),
album: track.album.unwrap_or_default().to_string(),
album_artist: track.album_artist.unwrap_or_default().to_string(),
genre: track.genre.unwrap_or_default().to_string(),
duration: track.duration.as_secs() as u32
};
// Remove prefix from audio_file_path
let pbuff = PathBuf::from(&cue.audio_file_path);
let stripped = pbuff.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
let db_path = format!("{}{}{}", sname, db::CUE_MARKER, t_num);
db.add_track(&db_path, &meta, &track.analysis);
}
None => { failed.push(format!("{} - No track number?", sname)); }
}
}
None => {
// Use lofty to read tags here, and not bliss's, so that if update
// tags is ever used they are from the same source.
let mut meta = tags::read(&cpath);
if meta.is_empty() {
// Lofty failed? Try from bliss...
meta.title = track.title.unwrap_or_default().to_string();
meta.artist = track.artist.unwrap_or_default().to_string();
meta.album = track.album.unwrap_or_default().to_string();
meta.album_artist = track.album_artist.unwrap_or_default().to_string();
meta.genre = track.genre.unwrap_or_default().to_string();
meta.duration = track.duration.as_secs() as u32;
}
if meta.is_empty() {
tag_error.push(sname.clone());
}
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
}
}
}
Err(e) => { failed.push(format!("{} - {}", sname, e)); }
};
if inc_progress {
progress.inc(1);
}
}
progress.finish_with_message("Finished!");
log::info!("{} Analysed. {} Failure(s).", analysed, failed.len());
fn show_errors(failed: &mut Vec<String>, tag_error: &mut Vec<String>) {
if !failed.is_empty() {
let total = failed.len();
failed.truncate(MAX_ERRORS_TO_SHOW);
@@ -191,12 +158,255 @@ pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>)
log::error!(" + {} other(s)", total - MAX_TAG_ERRORS_TO_SHOW);
}
}
}
#[cfg(not(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(),
};
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();
log::info!("Analysing new files");
for (path, result) in SongDecoder::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));
let mut inc_progress = true; // Only want to increment progress once for cue tracks
match result {
Ok(track) => {
let cpath = String::from(path.to_string_lossy());
match track.cue_info {
Some(cue) => {
match track.track_number {
Some(track_num) => {
if reported_cue.contains(&cpath) {
inc_progress = false;
} else {
analysed += 1;
reported_cue.insert(cpath);
}
let meta = db::Metadata {
title: track.title.unwrap_or_default().to_string(),
artist: track.artist.unwrap_or_default().to_string(),
album: track.album.unwrap_or_default().to_string(),
album_artist: track.album_artist.unwrap_or_default().to_string(),
genre: track.genre.unwrap_or_default().to_string(),
duration: track.duration.as_secs() as u32,
analysis: None
};
// Remove prefix from audio_file_path
let pbuff = PathBuf::from(&cue.audio_file_path);
let stripped = pbuff.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
let db_path = format!("{}{}{}", sname, db::CUE_MARKER, track_num);
db.add_track(&db_path, &meta, &track.analysis);
}
None => { failed.push(format!("{} - No track number?", sname)); }
}
}
None => {
// Use lofty to read tags here, and not bliss's, so that if update
// tags is ever used they are from the same source.
let mut meta = tags::read(&cpath, false);
if meta.is_empty() {
// Lofty failed? Try from bliss...
meta.title = track.title.unwrap_or_default().to_string();
meta.artist = track.artist.unwrap_or_default().to_string();
meta.album = track.album.unwrap_or_default().to_string();
meta.album_artist = track.album_artist.unwrap_or_default().to_string();
meta.genre = track.genre.unwrap_or_default().to_string();
meta.duration = track.duration.as_secs() as u32;
}
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)); }
};
if inc_progress {
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_tracks: usize) {
#[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(),
};
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));
let mut track_count_left = max_num_tracks;
db.init();
@@ -204,53 +414,65 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
db.remove_old(mpaths, dry_run);
}
let mut changes_made = false;
for path in mpaths {
let mpath = path.clone();
let cur = path.clone();
let mut track_paths: Vec<String> = Vec::new();
let mut cue_tracks:Vec<cue::CueTrack> = Vec::new();
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);
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);
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);
}
if dry_run {
if !track_paths.is_empty() {
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 max_num_tracks > 0 {
if track_paths.len() > track_count_left {
log::info!("Only analysing {} files", track_count_left);
track_paths.truncate(track_count_left);
}
track_count_left -= track_paths.len();
}
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths) {
Ok(_) => { }
match analyse_new_files(&db, &mpath, track_paths, max_threads, use_tags) {
Ok(_) => { changes_made = true; }
Err(e) => { log::error!("Analysis returned error: {}", e); }
}
} else {
log::info!("No new files to analyse");
}
if max_num_tracks > 0 && track_count_left <= 0 {
log::info!("File limit reached");
break;
#[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); }
}
}
}
}
db.close();
if changes_made && ignore_path.exists() && ignore_path.is_file() {
log::info!("Updating 'ignore' flags");
update_ignore(&db_path, &ignore_path);
}
}
pub fn read_tags(db_path: &str, mpaths: &Vec<PathBuf>) {
+110
View File
@@ -0,0 +1,110 @@
/**
* 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
}
+13 -2
View File
@@ -1,11 +1,13 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
#[cfg(feature = "ffmpeg")]
use crate::ffmpeg;
use crate::tags;
use bliss_audio::{Analysis, AnalysisIndex};
use indicatif::{ProgressBar, ProgressStyle};
@@ -35,6 +37,7 @@ pub struct Metadata {
pub album: String,
pub genre: String,
pub duration: u32,
pub analysis: Option<Analysis>,
}
impl Metadata {
@@ -275,6 +278,7 @@ impl Db {
album: dbtags.album.unwrap_or_default(),
genre: dbtags.genre.unwrap_or_default(),
duration: dbtags.duration,
analysis: None,
};
progress.set_message(format!("{}", dbtags.file));
@@ -282,7 +286,14 @@ impl Db {
let track_path = mpath.join(&dbtags.file);
if track_path.exists() {
let path = String::from(track_path.to_string_lossy());
let ftags = tags::read(&path);
#[allow(unused_mut)] // ftags is mutable if using ffmpeg on commandline
let mut 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 {
+141
View File
@@ -0,0 +1,141 @@
/**
* 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()))
}
}
+38 -5
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -14,8 +14,13 @@ use log::LevelFilter;
use std::io::Write;
use std::path::PathBuf;
use std::process;
#[cfg(feature = "ffmpeg")]
use which::which;
mod analyse;
mod cue;
mod db;
#[cfg(feature = "ffmpeg")]
mod ffmpeg;
mod tags;
mod upload;
@@ -32,8 +37,11 @@ fn main() {
let mut dry_run: bool = false;
let mut task = "".to_string();
let mut lms_host = "127.0.0.1".to_string();
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;
match dirs::home_dir() {
Some(path) => {
@@ -49,6 +57,7 @@ fn main() {
let logging_help = format!("Log level; trace, debug, info, warn, error. (default: {})", logging);
let ignore_file_help = format!("File contains items to mark as ignored. (default: {})", ignore_file);
let lms_host_help = format!("LMS hostname or IP address (default: {})", &lms_host);
let lms_json_port_help = format!("LMS JSONRPC port (default: {})", &lms_json_port);
let description = format!("Bliss Analyser v{}", VERSION);
// arg_parse.refer 'borrows' db_path, etc, and can only have one
@@ -63,7 +72,10 @@ fn main() {
arg_parse.refer(&mut dry_run).add_option(&["-r", "--dry-run"], StoreTrue, "Dry run, only show what needs to be done (used with analyse task)");
arg_parse.refer(&mut ignore_file).add_option(&["-i", "--ignore"], Store, &ignore_file_help);
arg_parse.refer(&mut lms_host).add_option(&["-L", "--lms"], Store, &lms_host_help);
arg_parse.refer(&mut 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.parse_args_or_exit();
}
@@ -72,8 +84,10 @@ fn main() {
|| logging.eq_ignore_ascii_case("warn") || logging.eq_ignore_ascii_case("error")) {
logging = String::from("info");
}
let other_level = if logging.eq_ignore_ascii_case("trace") { LevelFilter::Trace } else { LevelFilter::Error };
let mut builder = env_logger::Builder::from_env(env_logger::Env::default().filter_or("XXXXXXXX", logging));
builder.filter(Some("bliss_audio"), LevelFilter::Error);
builder.filter(Some("bliss_audio"), other_level);
builder.filter(Some("symphonia"), other_level);
builder.format(|buf, record| {
writeln!(buf, "[{} {:.1}] {}", Local::now().format("%Y-%m-%d %H:%M:%S"), record.level(), record.args())
});
@@ -90,6 +104,16 @@ fn main() {
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);
if path.exists() && path.is_file() {
@@ -111,10 +135,18 @@ fn main() {
Some(val) => { lms_host = val; }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "json") {
Some(val) => { lms_json_port = val.parse::<u16>().unwrap(); }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "ignore") {
Some(val) => { ignore_file = val; }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "tags") {
Some(val) => { use_tags = val.eq("true"); }
None => { }
}
}
Err(e) => {
log::error!("Failed to load config file. {}", e);
@@ -129,7 +161,7 @@ fn main() {
}
if task.eq_ignore_ascii_case("stopmixer") {
upload::stop_mixer(&lms_host);
upload::stop_mixer(&lms_host, lms_json_port);
} else {
if db_path.len() < 3 {
log::error!("Invalid DB path ({}) supplied", db_path);
@@ -144,7 +176,7 @@ fn main() {
if task.eq_ignore_ascii_case("upload") {
if path.exists() {
upload::upload_db(&db_path, &lms_host);
upload::upload_db(&db_path, &lms_host, lms_json_port);
} else {
log::error!("DB ({}) does not exist", db_path);
process::exit(-1);
@@ -175,7 +207,8 @@ fn main() {
}
analyse::update_ignore(&db_path, &ignore_path);
} else {
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files);
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);
}
}
}
+97 -4
View File
@@ -1,20 +1,68 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use crate::db;
use lofty::{Accessor, AudioFile, ItemKey};
use lofty::config::WriteOptions;
use lofty::file::FileType;
use lofty::prelude::{Accessor, AudioFile, ItemKey, TagExt, TaggedFileExt};
use lofty::tag::{ItemValue, Tag, TagItem};
use regex::Regex;
use std::path::Path;
use substring::Substring;
use bliss_audio::{Analysis, AnalysisIndex};
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;
pub fn read(track: &String) -> db::Metadata {
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]);
if let Ok(mut file) = lofty::read_from_path(Path::new(track)) {
let tag = match file.primary_tag_mut() {
Some(primary_tag) => primary_tag,
None => {
if let Some(first_tag) = file.first_tag_mut() {
first_tag
} else {
let tag_type = file.primary_tag_type();
file.insert_tag(Tag::new(tag_type));
file.primary_tag_mut().unwrap()
}
},
};
// Remove any existing analysis result tag
let entries = tag.get_strings(&ANALYSIS_TAG);
let mut keep: Vec<ItemValue> = Vec::new();
for entry in entries {
if !entry.starts_with(ANALYSIS_TAG_START) {
keep.push(ItemValue::Text(entry.to_string()));
}
}
tag.remove_key(&ANALYSIS_TAG);
for k in keep {
tag.push(TagItem::new(ANALYSIS_TAG, k));
}
// Store analysis results
tag.push(TagItem::new(ANALYSIS_TAG, ItemValue::Text(value)));
let _ = tag.save_to_path(Path::new(track), WriteOptions::default());
}
}
pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
let mut meta = db::Metadata {
duration: 180,
..db::Metadata::default()
@@ -33,7 +81,7 @@ pub fn read(track: &String) -> db::Metadata {
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(&lofty::FileType::MPEG) {
if file.file_type().eq(&FileType::Mpeg) {
match tag.genre() {
Some(genre) => {
let test = genre.parse::<u8>();
@@ -71,6 +119,51 @@ pub fn read(track: &String) -> 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 {
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;
}
index += 1;
}
if index == (NUM_ANALYSIS_VALS+2) {
meta.analysis = Some(Analysis::new(vals));
break;
}
}
}
}
}
meta
+7 -7
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -17,24 +17,24 @@ fn fail(msg: &str) {
process::exit(-1);
}
pub fn stop_mixer(lms: &String) {
pub fn stop_mixer(lms_host: &String, json_port: u16) {
let stop_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"stop\"]]}";
log::info!("Asking plugin to stop mixer");
let req = ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req);
let req = ureq::post(&format!("http://{}:{}/jsonrpc.js", lms_host, json_port)).send_string(&stop_req);
if let Err(e) = req {
log::error!("Failed to ask plugin to stop mixer. {}", e);
}
}
pub fn upload_db(db_path: &String, lms: &String) {
pub fn upload_db(db_path: &String, lms_host: &String, json_port: u16) {
// First tell LMS to restart the mixer in upload mode
let start_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"start-upload\"]]}";
let mut port: u16 = 0;
log::info!("Requesting LMS plugin to allow uploads");
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&start_req) {
match ureq::post(&format!("http://{}:{}/jsonrpc.js", lms_host, json_port)).send_string(&start_req) {
Ok(resp) => match resp.into_string() {
Ok(text) => match text.find("\"port\":") {
Some(s) => {
@@ -69,13 +69,13 @@ pub fn upload_db(db_path: &String, lms: &String) {
Ok(meta) => {
let buffered_reader = BufReader::new(file);
log::info!("Length: {}", meta.len());
match ureq::put(&format!("http://{}:{}/upload", lms, port))
match ureq::put(&format!("http://{}:{}/upload", lms_host, port))
.set("Content-Length", &meta.len().to_string())
.set("Content-Type", "application/octet-stream")
.send(buffered_reader) {
Ok(_) => {
log::info!("Database uploaded");
stop_mixer(lms);
stop_mixer(lms_host, json_port);
}
Err(e) => { fail(&format!("Failed to upload database. {}", e)); }
}