mirror of
https://github.com/CDrummond/bliss-analyser.git
synced 2026-10-22 22:32:08 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4714f8f4ef | ||
|
|
fd38256dc0 | ||
|
|
b4eb7562cf | ||
|
|
c7d436c13b | ||
|
|
e04b22ebcc | ||
|
|
6055fa2af7 | ||
|
|
0a1b9d8790 | ||
|
|
e395ff515b | ||
|
|
4794a9b747 | ||
|
|
b623c1939b | ||
|
|
de232d2a33 | ||
|
|
4830ef92b3 | ||
|
|
520190c52e | ||
|
|
d4f32d8b4d | ||
|
|
0250117b15 | ||
|
|
d2b3db43f7 | ||
|
|
2759f7f47c | ||
|
|
a79712cb24 | ||
|
|
5722b04c85 | ||
|
|
50a55ee61a | ||
|
|
b5c9356ef2 | ||
|
|
9d9117e8f7 | ||
|
|
417ac5f652 | ||
|
|
342440f04b | ||
|
|
85a2e985bf | ||
|
|
c999297430 | ||
|
|
db9b12be3f | ||
|
|
2468a83d4b | ||
|
|
22149c2349 | ||
|
|
0dda8a17e7 | ||
|
|
a2ff1aade1 | ||
|
|
471063b53d | ||
|
|
94011d12ea | ||
|
|
2210c44614 | ||
|
|
00be6015f7 | ||
|
|
e4522cc683 | ||
|
|
5c7b33a5d3 | ||
|
|
6fd78df3ef | ||
|
|
f23202e956 | ||
|
|
7ea6c3befd | ||
|
|
302609ed1b | ||
|
|
5e5bd9841a | ||
|
|
32610f22b7 | ||
|
|
751dca7091 | ||
|
|
ac87ebb66c | ||
|
|
98b9cd61e0 | ||
|
|
786b7d2c2d | ||
|
|
1e00e9593a | ||
|
|
0d877fe79e | ||
|
|
6cc26c399e | ||
|
|
bff4ba18b4 | ||
|
|
6fc9ac7f2e | ||
|
|
3a387be3bf | ||
|
|
2cb7dc0fa0 | ||
|
|
b91c2edafd | ||
|
|
f559abb395 | ||
|
|
cca929a13e | ||
|
|
6dcd61d9bb | ||
|
|
f8ea9947ac | ||
|
|
b275568000 | ||
|
|
0249bf8edb | ||
|
|
6d5b7c80ef | ||
|
|
b078736224 | ||
|
|
a610dab7c0 | ||
|
|
05fb19e2f9 | ||
|
|
375abe91d0 | ||
|
|
f9984e8ca0 | ||
|
|
73d4ae72cd | ||
|
|
92c2a90d25 | ||
|
|
dbe36c35db | ||
|
|
decbbd18e5 | ||
|
|
36f44d6550 | ||
|
|
01be5d107a | ||
|
|
388e571571 | ||
|
|
5f247ef10f | ||
|
|
eb7214644a | ||
|
|
25362b9635 | ||
|
|
4a98282ce2 | ||
|
|
331d770d46 | ||
|
|
0d1c33b131 | ||
|
|
c9d72b3f71 | ||
|
|
bfd73ea0bc | ||
|
|
b643272d1f | ||
|
|
9321be0ad5 | ||
|
|
55f944fbdd | ||
|
|
d7d7c87582 | ||
|
|
4e6d522829 | ||
|
|
25f399f932 | ||
|
|
a5db8f48ce | ||
|
|
136651ada7 | ||
|
|
e5ef67f8c6 | ||
|
|
6b9cb960a9 | ||
|
|
c6f9a7faf5 | ||
|
|
2ac4b9d17f | ||
|
|
7071ea14e7 | ||
|
|
e23aba88e3 | ||
|
|
5fa745975c | ||
|
|
22133436b9 | ||
|
|
68ed1fc3b0 | ||
|
|
3b465543d7 | ||
|
|
56ed0b9045 | ||
|
|
62c956bc78 | ||
|
|
a880fff993 | ||
|
|
777d8e3c6c | ||
|
|
319f72d2a4 | ||
|
|
297ed3304e |
No files matched your search
+382
-55
@@ -7,19 +7,19 @@ 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 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 -y
|
||||
|
||||
- name: Build x86 ffmpeg version
|
||||
run: |
|
||||
cargo build --release
|
||||
cargo build --release --features=ffmpeg
|
||||
strip target/release/bliss-analyser
|
||||
mkdir releases
|
||||
cp target/release/bliss-analyser releases/bliss-analyser
|
||||
@@ -28,62 +28,256 @@ 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-linux-x86-ffmpeg
|
||||
path: releases/
|
||||
|
||||
# Linux ffmpeg5 builds used a ppa which is now paywalled. Therefore until there is a github runner-image
|
||||
# Of 24.04 (which should have ffmpeg5?) then these builds are disabled. :(
|
||||
#
|
||||
# Linux_ffmpeg5:
|
||||
# runs-on: ubuntu-24.04
|
||||
#
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v1
|
||||
#
|
||||
# - 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
|
||||
# run: |
|
||||
# cargo build --release
|
||||
# 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@v2
|
||||
# with:
|
||||
# name: bliss-analyser-linux-x86-ffmpeg5
|
||||
# path: releases/
|
||||
|
||||
|
||||
Linux_ARM:
|
||||
runs-on: ubuntu-20.04
|
||||
Linux_static_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=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@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bliss-analyser-linux-arm
|
||||
name: bliss-analyser-linux-arm-ffmpeg
|
||||
path: releases/
|
||||
|
||||
# macOs builds are failing: error: could not find system library 'libavutil' required by the 'ffmpeg-sys-next' crate
|
||||
# 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
|
||||
@@ -96,7 +290,58 @@ jobs:
|
||||
#
|
||||
# - name: Install deps
|
||||
# run: |
|
||||
# brew install ffmpeg@5
|
||||
# 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: |
|
||||
@@ -109,13 +354,64 @@ jobs:
|
||||
# cp configs/macos.ini releases/config.ini
|
||||
#
|
||||
# - name: Upload artifacts
|
||||
# uses: actions/upload-artifact@v2
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: bliss-analyser-mac
|
||||
# name: bliss-analyser-mac-libav
|
||||
# path: releases/
|
||||
|
||||
|
||||
Windows:
|
||||
macOS_symphonia:
|
||||
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
|
||||
|
||||
- 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,symphonia
|
||||
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,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@v4
|
||||
with:
|
||||
name: bliss-analyser-mac-symphonia
|
||||
path: releases/
|
||||
|
||||
|
||||
Windows_libav:
|
||||
runs-on: windows-2019
|
||||
|
||||
steps:
|
||||
@@ -142,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/
|
||||
@@ -152,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
+873
-195
File diff suppressed because it is too large.
Load diff
+26
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bliss-analyser"
|
||||
version = "0.2.2"
|
||||
version = "0.4.0"
|
||||
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-only"
|
||||
@@ -9,20 +9,41 @@ repository = "https://github.com/CDrummond/bliss-analyser"
|
||||
keywords = ["audio", "song", "similarity"]
|
||||
readme = "README.md"
|
||||
|
||||
[profile.release]
|
||||
strip=true
|
||||
|
||||
[dependencies]
|
||||
bliss-audio = { version = "0.6.9", 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.15.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 }
|
||||
ctrlc = "3.4"
|
||||
|
||||
[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"
|
||||
@@ -1,3 +1,32 @@
|
||||
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
|
||||
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.
|
||||
@@ -18,7 +47,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
|
||||
|
||||
@@ -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
|
||||
|
||||
+85
-10
@@ -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 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 files when anlysed.
|
||||
* `preserve` specifies whether file modification time should be preserved when
|
||||
writing tags. Set to `true` or `false`.
|
||||
|
||||
|
||||
Command-line parameters
|
||||
@@ -134,21 +170,26 @@ 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` 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 specied in the INI config file (detailed above) will override
|
||||
any specified on the commandline.
|
||||
Equivalent items specified in the INI config file (detailed above) will override
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -177,11 +218,24 @@ is shown.
|
||||
As a rough guide, a 2015-era i7 8-core laptop with SSD analyses around 14000
|
||||
tracks/hour.
|
||||
|
||||
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 `--tags` 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
|
||||
---------
|
||||
|
||||
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 +356,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:
|
||||
@@ -319,6 +373,27 @@ is accomplished as follows:
|
||||
|
||||
|
||||
|
||||
Exporting Analysis
|
||||
==================
|
||||
|
||||
If you have analysis results stored within the SQLite 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.
|
||||
|
||||
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Executable
+29
@@ -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
|
||||
Executable
+29
@@ -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
|
||||
Executable
+29
@@ -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
|
||||
Executable
+18
@@ -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
|
||||
+12
-7
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# LMS-BlissMixer
|
||||
#
|
||||
# Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
|
||||
# Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
|
||||
# MIT license.
|
||||
#
|
||||
|
||||
@@ -11,9 +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"
|
||||
#UNIX_ARTIFICATS = ["bliss-analyser-linux-x86-ffmpeg4", "bliss-analyser-linux-x86-ffmpeg5", "bliss-analyser-linux-arm", "bliss-analyser-mac"]
|
||||
UNIX_ARTIFACTS = ["bliss-analyser-linux-x86-ffmpeg4", "bliss-analyser-linux-arm"]
|
||||
GITHUB_ARTIFACTS = UNIX_ARTIFACTS + ["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)
|
||||
@@ -66,7 +70,6 @@ def download_artifacts(version):
|
||||
f.write(chunk)
|
||||
if not os.path.exists(dest):
|
||||
info("Failed to download %s" % item)
|
||||
break
|
||||
|
||||
|
||||
def make_executable(version):
|
||||
@@ -79,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)
|
||||
|
||||
|
||||
+387
-158
@@ -1,54 +1,89 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022-2023 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_with_cores};
|
||||
#[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::fs::DirEntry;
|
||||
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>) {
|
||||
static mut TERMINATE_ANALYSIS_FLAG: bool = false;
|
||||
|
||||
fn terminate_analysis() -> bool {
|
||||
unsafe {
|
||||
return TERMINATE_ANALYSIS_FLAG
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_ctrl_c() {
|
||||
unsafe {
|
||||
TERMINATE_ANALYSIS_FLAG = true;
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, 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, 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, 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();
|
||||
@@ -66,15 +101,41 @@ 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;
|
||||
let meta = tags::read(&String::from(pb.to_string_lossy()), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,96 +144,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>, max_threads: usize) -> 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 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) => {
|
||||
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);
|
||||
@@ -197,12 +169,281 @@ 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, write_tags: bool, preserve_mod_times: 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 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)); }
|
||||
};
|
||||
|
||||
if inc_progress {
|
||||
progress.inc(1);
|
||||
}
|
||||
if terminate_analysis() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_tracks: usize, max_threads: usize) {
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize, write_tags: bool, preserve_mod_times: 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 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)); }
|
||||
};
|
||||
|
||||
progress.inc(1);
|
||||
if terminate_analysis() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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(_) => { },
|
||||
};
|
||||
}
|
||||
|
||||
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 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);
|
||||
if terminate_analysis() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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, write_tags: bool, preserve_mod_times: bool) {
|
||||
let mut db = db::Db::new(&String::from(db_path));
|
||||
let mut track_count_left = max_num_tracks;
|
||||
|
||||
ctrlc::set_handler(move || {
|
||||
handle_ctrl_c();
|
||||
}).expect("Error setting Ctrl-C handler");
|
||||
|
||||
db.init();
|
||||
|
||||
@@ -210,75 +451,63 @@ 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, &mut tagged_file_count, dry_run);
|
||||
track_paths.sort();
|
||||
log::info!("Num new files: {}", track_paths.len());
|
||||
log::info!("New untagged files: {}", track_paths.len());
|
||||
if !cue_tracks.is_empty() {
|
||||
log::info!("New cue tracks: {}", cue_tracks.len());
|
||||
}
|
||||
log::info!("New tagged files: {}", tagged_file_count);
|
||||
|
||||
if dry_run {
|
||||
if !track_paths.is_empty() {
|
||||
log::info!("The following need to be analysed:");
|
||||
for track in track_paths {
|
||||
log::info!(" {}", track);
|
||||
}
|
||||
}
|
||||
} 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, max_threads) {
|
||||
Ok(_) => { }
|
||||
Err(e) => { log::error!("Analysis returned error: {}", e); }
|
||||
if !terminate_analysis() {
|
||||
if dry_run {
|
||||
if !track_paths.is_empty() || !cue_tracks.is_empty() {
|
||||
log::info!("The following need to be analysed:");
|
||||
for track in track_paths {
|
||||
log::info!(" {}", track);
|
||||
}
|
||||
for track in cue_tracks {
|
||||
log::info!(" {}", track.track_path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("No new files to analyse");
|
||||
}
|
||||
if !track_paths.is_empty() {
|
||||
match analyse_new_files(&db, &mpath, track_paths, max_threads, write_tags, preserve_mod_times) {
|
||||
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() && !terminate_analysis() {
|
||||
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
|
||||
Ok(_) => { changes_made = true; },
|
||||
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
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 changes_made && ignore_path.exists() && ignore_path.is_file() {
|
||||
log::info!("Updating 'ignore' flags");
|
||||
db::update_ignore(&db_path, &ignore_path);
|
||||
}
|
||||
|
||||
db.close();
|
||||
}
|
||||
+110
@@ -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
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022-2023 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};
|
||||
use rusqlite::{params, Connection};
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
use num_cpus;
|
||||
|
||||
pub const CUE_MARKER: &str = ".CUE_TRACK.";
|
||||
|
||||
@@ -27,6 +35,12 @@ pub struct FileMetadata {
|
||||
pub duration: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AnalysisResults {
|
||||
pub file: String,
|
||||
pub analysis: Analysis,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub struct Metadata {
|
||||
pub title: String,
|
||||
@@ -35,6 +49,7 @@ pub struct Metadata {
|
||||
pub album: String,
|
||||
pub genre: String,
|
||||
pub duration: u32,
|
||||
pub analysis: Option<Analysis>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
@@ -47,6 +62,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,
|
||||
}
|
||||
@@ -275,6 +304,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 +312,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 {
|
||||
@@ -327,4 +364,144 @@ impl Db {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 FROM Tracks 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)?]),
|
||||
})
|
||||
})
|
||||
.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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total = tracks.len();
|
||||
if total <= 0 {
|
||||
log::info!("Nothing to export");
|
||||
return;
|
||||
}
|
||||
log::info!("Starting export");
|
||||
let cpu_threads: NonZeroUsize = match max_threads {
|
||||
0 => NonZeroUsize::new(num_cpus::get()).unwrap(),
|
||||
_ => NonZeroUsize::new(max_threads).unwrap(),
|
||||
}.into();
|
||||
let num_threads = cpu_threads.into();
|
||||
let chunk_size = total/cpu_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..num_threads {
|
||||
let tid:usize = thread;
|
||||
let start = tid * chunk_size;
|
||||
let end = if tid+1 == num_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 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
@@ -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()))
|
||||
}
|
||||
}
|
||||
+49
-10
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022-2023 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,9 +37,12 @@ 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 write_tags = false;
|
||||
let mut preserve_mod_times = false;
|
||||
|
||||
match dirs::home_dir() {
|
||||
Some(path) => {
|
||||
@@ -50,6 +58,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
|
||||
@@ -64,9 +73,12 @@ 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 task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, stopmixer.");
|
||||
arg_parse.refer(&mut write_tags).add_option(&["-T", "--tags"], StoreTrue, "When analysing files, also store results within files themselves");
|
||||
arg_parse.refer(&mut preserve_mod_times).add_option(&["-p", "--preserve"], StoreTrue, "Preserve modification time when writing results to files");
|
||||
arg_parse.refer(&mut task).add_argument("task", Store, "Task to perform; analyse, tags, ignore, upload, export, stopmixer.");
|
||||
arg_parse.parse_args_or_exit();
|
||||
}
|
||||
|
||||
@@ -74,24 +86,36 @@ 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())
|
||||
});
|
||||
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, 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") {
|
||||
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);
|
||||
if path.exists() && path.is_file() {
|
||||
@@ -113,10 +137,22 @@ 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) => { write_tags = val.eq("true"); }
|
||||
None => { }
|
||||
}
|
||||
match config.get(TOP_LEVEL_INI_TAG, "preserve") {
|
||||
Some(val) => { preserve_mod_times = val.eq("true"); }
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load config file. {}", e);
|
||||
@@ -131,7 +167,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);
|
||||
@@ -146,7 +182,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);
|
||||
@@ -164,7 +200,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() {
|
||||
@@ -175,9 +211,12 @@ 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, max_threads, preserve_mod_times);
|
||||
} else {
|
||||
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads);
|
||||
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, write_tags, preserve_mod_times);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+191
-7
@@ -1,20 +1,148 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022-2023 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, TaggedFileExt};
|
||||
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::fs::File;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use substring::Substring;
|
||||
use std::time::SystemTime;
|
||||
use bliss_audio::{Analysis, AnalysisIndex};
|
||||
|
||||
const MAX_GENRE_VAL: usize = 192;
|
||||
const NUM_ANALYSIS_VALS: usize = 20;
|
||||
const ANALYSIS_TAG: &str = "BLISS_ANALYSIS";
|
||||
const ANALYSIS_TAG_FORMAT_VER: u16 = 1;
|
||||
|
||||
pub fn read(track: &String) -> db::Metadata {
|
||||
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]));
|
||||
|
||||
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,
|
||||
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()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 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) {
|
||||
have_old = true;
|
||||
} else {
|
||||
keep.push(ItemValue::Text(entry.to_string()));
|
||||
}
|
||||
}
|
||||
if have_old {
|
||||
tag.remove_key(&ItemKey::Comment);
|
||||
for k in keep {
|
||||
tag.push(TagItem::new(ItemKey::Comment, k));
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn read(track: &String, read_analysis: bool) -> db::Metadata {
|
||||
let mut meta = db::Metadata {
|
||||
duration: 180,
|
||||
..db::Metadata::default()
|
||||
@@ -30,10 +158,22 @@ pub fn read(track: &String) -> db::Metadata {
|
||||
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(&lofty::FileType::Mpeg) {
|
||||
if file.file_type().eq(&FileType::Mpeg) {
|
||||
match tag.genre() {
|
||||
Some(genre) => {
|
||||
let test = genre.parse::<u8>();
|
||||
@@ -55,8 +195,7 @@ pub fn read(track: &String) -> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +210,51 @@ pub fn read(track: &String) -> db::Metadata {
|
||||
}
|
||||
|
||||
meta.duration = file.properties().duration().as_secs() as u32;
|
||||
|
||||
if read_analysis {
|
||||
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;
|
||||
}
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meta
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Analyse music with Bliss
|
||||
*
|
||||
* Copyright (c) 2022-2023 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)); }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user