33 Commits
Author SHA1 Message Date
CDrummond bf3bdf26b9 Fix invalid task warning with 'clearfail'. 2026-08-28 18:50:37 +01:00
CDrummond cd2900b273 Update 2026-07-29 17:15:03 +01:00
CraigD e32cdf4806 Merge pull request #35 from chrober/fix/case-sensitive-path-reconciliation
Fix stale case-only paths on case-insensitive filesystems
2026-07-29 17:13:33 +01:00
Christoph Obermair 9c9fdad504 Fix case-only path reconciliation 2026-07-29 14:35:29 +02:00
CDrummond fe65d9fcf6 macOS fixes. 2026-01-04 11:57:53 +00:00
CDrummond b13efd783b macos-15 2026-01-04 09:33:43 +00:00
CDrummond 572f91eeec Update (c) year. 2026-01-02 09:53:06 +00:00
CDrummond 374be6a5c2 Mention threads commandline argument. 2025-11-10 21:02:53 +00:00
CDrummond cf902e6d0c Fix Linux builds. 2025-09-27 17:03:21 +01:00
CDrummond a3748f495c More fixes... 2025-09-27 16:59:25 +01:00
CDrummond 5c54879126 Build fixes? 2025-09-27 16:42:47 +01:00
CDrummond b57828cc22 Use bullseye, not bookwork - for glibc 2025-09-27 16:24:26 +01:00
CDrummond f438239983 Update. 2025-09-25 21:17:50 +01:00
CDrummond c1ba064f48 Fix messages. 2025-09-25 21:14:06 +01:00
CDrummond 0c4335d02d Re-enable all builds but don't use docker for Linux x86/ffmpeg - as
can't get tnis to build!
2025-09-25 20:31:42 +01:00
CDrummond fe10956341 Use musl-cross toolchain?? 2025-09-25 19:28:49 +01:00
CDrummond e860048ff0 Set clang path 2025-09-25 19:21:52 +01:00
CDrummond 1428751b9e ...and others 2025-09-25 19:17:20 +01:00
CDrummond e546793864 clang 2025-09-25 19:12:46 +01:00
CDrummond e0ffdedced /bin/ash? 2025-09-25 19:08:24 +01:00
CDrummond 81f29db352 Use alpine? 2025-09-25 18:02:44 +01:00
CDrummond 35ec7695e5 Renable... 2025-09-25 17:41:55 +01:00
CDrummond 6f1abef1ad Can't get musl to work... 2025-09-25 17:35:25 +01:00
CDrummond 5168723d55 clang required? 2025-09-25 17:24:08 +01:00
CDrummond ee3b27d959 Hmmm... 2025-09-25 17:18:06 +01:00
CDrummond 0e5f4fc1d1 Hacky way to find info 2025-09-25 17:14:36 +01:00
CDrummond beb7155ae6 Symlink musl-ar??? 2025-09-25 16:58:46 +01:00
CDrummond 84766d3e4f Create dest dir! 2025-09-25 16:52:51 +01:00
CDrummond 5ec6a30104 Next try? 2025-09-25 16:44:23 +01:00
CDrummond fa61d5a6c3 Install musl! 2025-09-25 16:26:23 +01:00
CDrummond d06263efb0 Fix builds? 2025-09-25 16:16:49 +01:00
CDrummond d7da5dcd95 Use docker for Linxu builds and link to musl 2025-09-24 22:18:46 +01:00
CDrummond 849fbe92fa Use serde_json to encode message strings to be JSON safe. 2025-09-24 21:06:04 +01:00
25 changed files with 581 additions and 274 deletions

No files matched your search

+35 -36
View File
@@ -12,20 +12,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v1
- name: Packages
run: sudo apt-get update && sudo apt-get install build-essential yasm -y
- name: Build
- name: Build x86
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
docker build -t bliss-analyser-x86-symphonia - < docker/Dockerfile_x86_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-x86-symphonia
- name: Upload artifacts
uses: actions/upload-artifact@v4
@@ -33,6 +25,23 @@ jobs:
name: bliss-analyser-linux-x86-symphonia
path: releases/
# Linux_x86_ffmpeg:
# runs-on: ubuntu-latest
#
# steps:
# - name: Checkout
# uses: actions/checkout@v1
#
# - name: Build x86
# run: |
# docker build -t bliss-analyser-x86-ffmpeg - < docker/Dockerfile_x86_ffmpeg
# docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-x86-ffmpeg
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v4
# with:
# name: bliss-analyser-linux-x86-ffmpeg
# path: releases/
Linux_x86_ffmpeg:
runs-on: ubuntu-22.04
@@ -60,18 +69,17 @@ jobs:
name: bliss-analyser-linux-x86-ffmpeg
path: releases/
Linux_arm_symphonia:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v1
- name: Build
- name: Build ARM
run: |
docker build --pull --no-cache -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
docker build -t bliss-analyser-arm-symphonia - < docker/Dockerfile_arm_symphonia
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-arm-symphonia
- name: Upload artifacts
uses: actions/upload-artifact@v4
@@ -79,18 +87,17 @@ jobs:
name: bliss-analyser-linux-arm-symphonia
path: releases/
Linux_arm_ffmpeg:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v1
- name: Build ARM static-ffmpeg on Debian
- name: Build ARM
run: |
docker build -t bliss-analyser-cross - < docker/Dockerfile_Bullseye_static
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
docker build -t bliss-analyser-arm-ffmpeg - < docker/Dockerfile_arm_ffmpeg
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-arm-ffmpeg
- name: Upload artifacts
uses: actions/upload-artifact@v4
@@ -98,9 +105,8 @@ jobs:
name: bliss-analyser-linux-arm-ffmpeg
path: releases/
macOS_symphonia:
runs-on: macos-13
runs-on: macos-15
steps:
- name: Install Rust
@@ -117,17 +123,14 @@ jobs:
- name: Install Rust support for ARM64 & prepare environment
run: |
rustup target add x86_64-apple-darwin
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=x86_64-apple-darwin --release --features update-aubio-bindings,symphonia
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
@@ -135,14 +138,10 @@ jobs:
- name: Build fat binary
run: |
lipo -create \
-arch x86_64 releases/bliss-analyser-x86_64 \
-arch arm64 releases/bliss-analyser-arm64 \
-arch x86_64 target/x86_64-apple-darwin/release/bliss-analyser \
-arch arm64 target/aarch64-apple-darwin/release/bliss-analyser \
-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:
Generated
+6 -3
View File
@@ -201,7 +201,7 @@ dependencies = [
[[package]]
name = "bliss-analyser"
version = "0.5.0"
version = "0.5.3"
dependencies = [
"anyhow",
"argparse",
@@ -221,6 +221,7 @@ dependencies = [
"rcue",
"regex",
"rusqlite",
"serde_json",
"substring",
"ureq",
"which 7.0.2",
@@ -1479,13 +1480,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.109"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "bliss-analyser"
version = "0.5.0"
version = "0.5.3"
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
edition = "2021"
license = "GPL-3.0-only"
@@ -33,6 +33,7 @@ which = { version = "7.0.2", optional = true }
rcue = { version = "0.1.3", optional = true }
hhmmss = { version = "0.1.0", optional = true }
ctrlc = "3.4"
serde_json = "1.0.145"
[features]
libav = ["bliss-audio/ffmpeg"]
+14
View File
@@ -1,3 +1,17 @@
0.5.3
-----
1. Fix stale case-only paths on case-insensitive filesystems - thanks to
chrober for implementation.
2. Fix invalid task warning with 'clearfail'.
0.5.2
-----
1. Fix Linux builds.
0.5.1
-----
1. Use serde_json to build JSONRPC notification strings.
0.5.0
-----
1. Update to bliss-rs 0.11.1
+2 -2
View File
@@ -31,7 +31,7 @@ files, but has more limited codec support, and can fail on more files.
To install dependencies on a Debian system:
```
apt install -y clang libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev pkg-config
apt install -y clang libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev pkg-config yasm
```
To install dependencies on a Fedora system:
@@ -57,7 +57,7 @@ their dependencies).
To install dependencies on a Debian system:
```
apt install -y clang libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev pkg-config
apt install -y clang libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libavdevice-dev pkg-config yasm
```
To install dependencies on a Fedora system:
+10 -6
View File
@@ -88,12 +88,15 @@ Command-line parameters
* `-d` / `--db` Name and location of the database file.
* `-l` / `--logging` Logging level; `trace`, `debug`, `info`, `warn`, `error`.
Default is `info`.
* `-k` / `--keep-old` When analysing tracks, `bliss-analyser` will remove any
tracks specified in its database that are no-longer on the file-system. This
parameter is used to prevent this.
* `-k` / `--keep-old` When analysing tracks, `bliss-analyser` will remove database
rows for tracks that are no longer on the filesystem. It also reconciles stored
case-only path differences with the filesystem's exact spelling, preserving the
existing analysis, and removes stale case-only duplicates. This option prevents
all of those cleanup actions.
* `-r` / `--dry-run` If this is supplied when analysing tracks, then no actual
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.
analysis or database cleanup will be performed, instead the logging will inform
you how many new tracks are to be analysed, how many old tracks are left in the
database, and which stored paths would have their filename casing corrected.
* `-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.
@@ -102,7 +105,8 @@ tracks are to be analysed and how many old tracks are left in the database.
files a `BLISS_ANALYSIS` tag (it present) in files.
* `-W` / `--writetags` When using the `analyse` task, write analysis results to files
within a `BLISS_ANALYSIS` tag.
* `-p' / '--preserve` Attempt to preserve file modification time when writing tags.
* `-p` / `--preserve` Attempt to preserve file modification time when writing tags.
* `-t` / `--threads` Maximum number of threads to use for analysis.
Equivalent items specified in the INI config file (detailed above) will override
any specified on the command-line.
-38
View File
@@ -1,38 +0,0 @@
# Cross compilation environment for bliss-analyser
FROM debian:bookworm
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y pkg-config:arm64 \
libavutil-dev:arm64 libavcodec-dev:arm64 libavformat-dev:arm64 \
libavfilter-dev:arm64 libavdevice-dev:arm64 libswresample-dev:arm64 libfftw3-dev:arm64
RUN apt-get install -y pkg-config:armhf \
libavutil-dev:armhf libavcodec-dev:armhf libavformat-dev:armhf \
libavfilter-dev:armhf libavdevice-dev:armhf libswresample-dev:armhf libfftw3-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-libav.sh"]
-36
View File
@@ -1,36 +0,0 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
RUN dpkg --add-architecture arm64 && \
dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y crossbuild-essential-armhf crossbuild-essential-arm64
RUN apt-get install -y libavutil-dev:arm64 libavcodec-dev:arm64 libavformat-dev:arm64 \
libavfilter-dev:arm64 libavdevice-dev:arm64 libswresample-dev:arm64 libfftw3-dev:arm64
RUN apt-get install -y libavutil-dev:armhf libavcodec-dev:armhf libavformat-dev:armhf \
libavfilter-dev:armhf libavdevice-dev:armhf libswresample-dev:armhf libfftw3-dev:armhf
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add aarch64-unknown-linux-gnu && \
rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-staticlibav.sh"]
-25
View File
@@ -1,25 +0,0 @@
# Cross compilation environment for bliss-anlyser
FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y curl git pkg-config
RUN apt-get install -y build-essential yasm clang
RUN apt-get install -y libavutil-dev libavcodec-dev libavformat-dev \
libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev \
ffmpeg
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-libav.sh"]
@@ -1,5 +1,3 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
RUN dpkg --add-architecture arm64 && \
@@ -32,5 +30,5 @@ ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-arm-libav.sh"]
CMD ["/src/docker/docker-build-arm-ffmpeg.sh"]
@@ -1,5 +1,3 @@
# Cross compilation environment for bliss-anlyser
FROM debian:bullseye
RUN dpkg --add-architecture arm64 && \
+30
View File
@@ -0,0 +1,30 @@
FROM debian:bullseye
RUN dpkg --add-architecture amd64
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
# Download and extract musl-cross toolchain for x86_64
RUN curl -LO https://musl.cc/x86_64-linux-musl-cross.tgz \
&& tar -xzf x86_64-linux-musl-cross.tgz \
&& mv x86_64-linux-musl-cross /opt/musl-toolchain \
&& rm x86_64-linux-musl-cross.tgz
ENV PATH="/opt/musl-toolchain/bin:/root/.cargo/bin:${PATH}"
# Optionally, still install musl-tools (for musl-gcc wrapper, not strictly needed now)
RUN apt-get install -y musl-tools musl-dev musl
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN rustup target add x86_64-unknown-linux-musl
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-ffmpeg.sh"]
+22
View File
@@ -0,0 +1,22 @@
FROM debian:bullseye
RUN dpkg --add-architecture amd64
RUN apt-get update
RUN apt-get install -y curl git pkg-config yasm
RUN apt-get install -y build-essential clang
RUN apt-get install -y musl-tools musl-dev musl
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add x86_64-unknown-linux-musl
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
RUN mkdir /src
WORKDIR /src
CMD ["/src/docker/docker-build-x86-symphonia.sh"]
@@ -9,15 +9,15 @@ mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
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
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
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=libav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
build aarch64-unknown-linux-gnu aarch64-linux-gnu-strip bin/bliss-analyser-aarch64
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
cp scripts/bliss-analyser-arm $DESTDIR/bliss-analyser
+4 -6
View File
@@ -11,13 +11,11 @@ rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --features=symphonia --target $1
fi
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features=symphonia --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build arm-unknown-linux-gnueabihf arm-linux-gnueabihf-strip bin/bliss-analyser-armhf
+22
View File
@@ -0,0 +1,22 @@
#!/bin/ash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features=update-aubio-bindings,libav,staticlibav --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build x86_64-unknown-linux-musl strip bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
export RUST_BACKTRACE=full
cargo build --release --features=libav
strip /build/release/bliss-analyser && cp /build/release/bliss-analyser $DESTDIR/bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
cargo build --release --features update-aubio-bindings,symphonia --target $1
fi
$2 /build/$1/release/bliss-analyser && cp /build/$1/release/bliss-analyser $DESTDIR/$3
}
build x86_64-unknown-linux-musl strip bliss-analyser
cp UserGuide.md $DESTDIR/README.md
cp LICENSE $DESTDIR/
cp configs/linux.ini $DESTDIR/config.ini
+1 -1
View File
@@ -3,7 +3,7 @@
#
# LMS-BlissMixer
#
# Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
# Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
# MIT license.
#
+6 -5
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -28,6 +28,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use serde_json::json;
const DONT_ANALYSE: &str = ".notmusic";
const MAX_ERRORS_TO_SHOW: usize = 100;
@@ -57,9 +58,9 @@ fn handle_ctrl_c() {
}
fn send_notif_msg(notifs: &mut NotifInfo, text: &str) {
let json = &format!("{{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"analyser\",\"act:update\",\"msg:{}\"]]}}", text);
let js = json!({"id":"1", "method":"slim.request", "params":["", ["blissmixer", "analyser", "act:update", format!("msg:{}", text)]]});
log::info!("Sending notif to LMS: {}", text);
let _ = ureq::post(&notifs.address).send_string(&json);
let _ = ureq::post(&notifs.address).send_string(&js.to_string());
}
fn send_notif(notifs: &mut NotifInfo, text: &str) {
@@ -392,12 +393,12 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_o
db.init();
let mut changes_made = false;
if !keep_old {
send_notif(&mut notifs, "Removing old files from DB");
db.remove_old(mpaths, dry_run);
changes_made = db.remove_old(mpaths, dry_run);
}
let mut changes_made = false;
for path in mpaths {
let mpath = path.clone();
let cur = path.clone();
+391 -51
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -10,10 +10,12 @@ use crate::tags;
use bliss_audio::{Analysis, AnalysisIndex, FeaturesVersion};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::{params, Connection};
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::fs::File;
use std::ffi::OsString;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::path::{Component, Path, PathBuf};
use std::process;
use std::thread;
use std::thread::JoinHandle;
@@ -76,6 +78,159 @@ pub struct Db {
pub conn: Connection,
}
#[derive(Debug, PartialEq)]
enum PathResolution {
Exact,
Corrected(String),
Missing,
Unknown,
}
struct ExactPathResolver {
roots: Vec<PathBuf>,
directory_entries: HashMap<PathBuf, Option<DirectoryEntries>>,
}
struct DirectoryEntries {
exact: HashSet<OsString>,
case_insensitive: HashMap<String, Option<OsString>>,
}
impl ExactPathResolver {
fn new(roots: &Vec<PathBuf>) -> Self {
Self {
roots: roots.to_vec(),
directory_entries: HashMap::new(),
}
}
fn entries(&mut self, directory: &Path) -> Option<&DirectoryEntries> {
if !self.directory_entries.contains_key(directory) {
let entries = match fs::read_dir(directory) {
Ok(entries) => {
let names = entries.map(|entry| entry.map(|entry| entry.file_name())).collect::<std::io::Result<Vec<_>>>();
match names {
Ok(names) => {
let mut exact = HashSet::with_capacity(names.len());
let mut case_insensitive = HashMap::with_capacity(names.len());
for name in names {
exact.insert(name.clone());
if let Some(entry_text) = name.to_str() {
let folded = entry_text.to_lowercase();
match case_insensitive.get_mut(&folded) {
Some(existing) => *existing = None,
None => {
case_insensitive.insert(folded, Some(name));
}
}
}
}
Some(DirectoryEntries {
exact,
case_insensitive,
})
}
Err(_) => None,
}
}
Err(_) => None,
};
self.directory_entries
.insert(directory.to_path_buf(), entries);
}
self.directory_entries
.get(directory)
.and_then(|entries| entries.as_ref())
}
fn resolve_under_root(&mut self, root: &Path, relative: &Path) -> PathResolution {
let mut current = root.to_path_buf();
let mut resolved = PathBuf::new();
let mut corrected = false;
for component in relative.components() {
let requested = match component {
Component::CurDir => continue,
Component::Normal(name) => name,
_ => return PathResolution::Unknown,
};
let entries = match self.entries(&current) {
Some(entries) => entries,
None => return PathResolution::Unknown,
};
let actual = if entries.exact.contains(requested) {
requested.to_os_string()
} else {
let folded = match requested.to_str() {
Some(requested) => requested.to_lowercase(),
None => return PathResolution::Missing,
};
let actual = match entries.case_insensitive.get(&folded) {
Some(Some(actual)) => actual.clone(),
Some(None) => return PathResolution::Unknown,
None => return PathResolution::Missing,
};
corrected = true;
actual
};
current.push(&actual);
resolved.push(actual);
}
if resolved.as_os_str().is_empty() {
PathResolution::Unknown
} else if corrected {
PathResolution::Corrected(normalize_db_path(&resolved))
} else {
PathResolution::Exact
}
}
fn resolve(&mut self, db_path: &str) -> PathResolution {
let (file_path, cue_suffix) = match db_path.find(CUE_MARKER) {
Some(index) => (&db_path[..index], &db_path[index..]),
None => (db_path, ""),
};
let relative = PathBuf::from(file_path);
let mut correction: Option<String> = None;
let mut unknown = false;
for root in self.roots.clone() {
match self.resolve_under_root(&root, &relative) {
PathResolution::Exact => return PathResolution::Exact,
PathResolution::Corrected(path) => {
let candidate = format!("{}{}", path, cue_suffix);
match correction.as_ref() {
Some(existing) if existing != &candidate => {
unknown = true;
}
_ => {
correction = Some(candidate);
}
}
}
PathResolution::Unknown => unknown = true,
PathResolution::Missing => {}
}
}
if unknown {
PathResolution::Unknown
} else if let Some(path) = correction {
PathResolution::Corrected(path)
} else {
PathResolution::Missing
}
}
}
fn normalize_db_path(path: &Path) -> String {
let path = path.to_string_lossy().to_string();
if cfg!(windows) {
path.replace("\\", "/")
} else {
path
}
}
impl Db {
pub fn new(path: &String) -> Self {
match Connection::open(path) {
@@ -230,9 +385,11 @@ impl Db {
let _ = self.conn.execute("DELETE FROM Failures WHERE File = ?;", params![path]);
}
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run: bool) {
self.remove_old_from_table(mpaths, dry_run, "TracksV2");
self.remove_old_from_table(mpaths, dry_run, "Failures");
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run: bool) -> bool {
let mut resolver = ExactPathResolver::new(mpaths);
let tracks_changed = self.remove_old_from_table(dry_run, "TracksV2", &mut resolver);
let failures_changed = self.remove_old_from_table(dry_run, "Failures", &mut resolver);
tracks_changed || failures_changed
}
fn clear_failures(&self) {
@@ -254,64 +411,85 @@ impl Db {
ts
}
fn remove_old_from_table(&self, mpaths: &Vec<PathBuf>, dry_run: bool, table: &str) {
log::info!("Looking for non-existent tracks");
let mut stmt = self.conn.prepare(format!("SELECT File FROM {};", table).as_str()).unwrap();
let track_iter = stmt.query_map([], |row| Ok((row.get(0)?,))).unwrap();
fn remove_old_from_table(&self, dry_run: bool, table: &str, resolver: &mut ExactPathResolver) -> bool {
log::info!("Looking for missing or case-mismatched paths in {}", table);
let mut stmt = self.conn.prepare(format!("SELECT File FROM {} ORDER BY File;", table).as_str()).unwrap();
let file_iter = stmt.query_map([], |row| Ok((row.get(0)?,))).unwrap();
let mut files: Vec<String> = Vec::new();
for file in file_iter {
files.push(file.unwrap().0);
}
let known_paths: HashSet<String> = files.iter().cloned().collect();
let mut claimed_paths: HashSet<String> = HashSet::new();
let mut to_remove: Vec<String> = Vec::new();
for tr in track_iter {
let mut db_path: String = tr.unwrap().0;
let orig_path = db_path.clone();
match orig_path.find(CUE_MARKER) {
Some(s) => {
db_path.truncate(s);
let mut to_correct: Vec<(String, String)> = Vec::new();
let mut unverified_count = 0;
for original in files {
match resolver.resolve(&original) {
PathResolution::Exact => {}
PathResolution::Corrected(corrected) => {
if known_paths.contains(&corrected)
|| !claimed_paths.insert(corrected.clone())
{
to_remove.push(original);
} else {
to_correct.push((original, corrected));
}
}
None => {}
}
if cfg!(windows) {
db_path = db_path.replace("/", "\\");
}
let mut exists = false;
for mpath in mpaths {
let path = mpath.join(PathBuf::from(db_path.clone()));
//log::debug!("Check if '{}' exists.", path.to_string_lossy());
if path.exists() {
exists = true;
break;
PathResolution::Missing => to_remove.push(original),
PathResolution::Unknown => {
unverified_count += 1;
}
}
if !exists {
to_remove.push(orig_path);
}
}
let num_to_remove = to_remove.len();
log::info!("Num non-existent tracks: {}", num_to_remove);
if num_to_remove > 0 {
if dry_run {
let num_to_correct = to_correct.len();
log::info!("Num missing/duplicate paths: {}", num_to_remove);
log::info!("Num case-only path corrections: {}", num_to_correct);
if unverified_count > 0 {
log::warn!(
"Could not verify exact filesystem spelling for {} path(s) in {}; keeping those database rows",
unverified_count,
table
);
}
if dry_run {
if num_to_remove > 0 {
log::info!("The following need to be removed from database:");
for t in to_remove {
log::info!(" {}", t);
for path in &to_remove {
log::info!(" {}", path);
}
} else {
let count_before = self.get_track_count();
for t in to_remove {
//log::debug!("Remove '{}'", t);
let cmd = self.conn.execute(format!("DELETE FROM {} WHERE File = ?;", table).as_str(), params![t]);
}
if num_to_correct > 0 {
log::info!("The following stored path spellings need correction:");
for (from, to) in &to_correct {
log::info!(" {} -> {}", from, to);
}
}
return false;
}
if let Err(e) = cmd {
log::error!("Failed to remove '{}' - {}", t, e)
}
}
let count_now = self.get_track_count();
if (count_now + num_to_remove) != count_before {
log::error!("Failed to remove all tracks. Count before: {}, wanted to remove: {}, count now: {}", count_before, num_to_remove, count_now);
let mut changed = false;
for (from, to) in to_correct {
let cmd = self.conn.execute(format!("UPDATE {} SET File = ? WHERE File = ?;", table).as_str(), params![to, from]);
match cmd {
Ok(count) => changed |= count > 0,
Err(error) => {
log::error!("Failed to correct '{}' to '{}' - {}", from, to, error)
}
}
}
for path in to_remove {
let cmd = self.conn.execute(format!("DELETE FROM {} WHERE File = ?;", table).as_str(), params![path]);
match cmd {
Ok(count) => changed |= count > 0,
Err(error) => log::error!("Failed to remove '{}' - {}", path, error),
}
}
changed
}
pub fn get_track_count(&self) -> usize {
@@ -561,3 +739,165 @@ pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
db.close();
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
struct TestMusicRoot {
path: PathBuf,
}
impl TestMusicRoot {
fn new() -> Self {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"bliss-analyser-path-test-{}-{}",
process::id(),
unique
));
fs::create_dir_all(&path).unwrap();
Self { path }
}
fn create_file(&self, relative: &str) {
let path = self.path.join(relative);
fs::create_dir_all(path.parent().unwrap()).unwrap();
File::create(path).unwrap();
}
}
impl Drop for TestMusicRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn test_db() -> Db {
let db = Db {
conn: Connection::open_in_memory().unwrap(),
};
db.init();
db
}
fn insert_track(db: &Db, path: &str) -> i64 {
db.conn
.execute("INSERT INTO TracksV2 (File) VALUES (?);", params![path])
.unwrap();
db.conn.last_insert_rowid()
}
fn track_paths(db: &Db) -> Vec<(i64, String)> {
let mut statement = db
.conn
.prepare("SELECT rowid, File FROM TracksV2 ORDER BY File;")
.unwrap();
statement
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.map(|row| row.unwrap())
.collect()
}
#[test]
fn corrects_case_only_path_in_place() {
let music = TestMusicRoot::new();
music.create_file("Artist/Album/Track.mp3");
let db = test_db();
let original_rowid = insert_track(&db, "artist/album/track.mp3");
assert!(db.remove_old(&vec![music.path.clone()], false));
assert_eq!(
vec![(original_rowid, "Artist/Album/Track.mp3".to_string())],
track_paths(&db)
);
}
#[test]
fn removes_stale_case_variant_when_exact_row_exists() {
let music = TestMusicRoot::new();
music.create_file("Artist/Album/Track.mp3");
let db = test_db();
insert_track(&db, "artist/album/track.mp3");
let exact_rowid = insert_track(&db, "Artist/Album/Track.mp3");
assert!(db.remove_old(&vec![music.path.clone()], false));
assert_eq!(
vec![(exact_rowid, "Artist/Album/Track.mp3".to_string())],
track_paths(&db)
);
}
#[test]
fn dry_run_reports_without_mutating() {
let music = TestMusicRoot::new();
music.create_file("Artist/Album/Track.mp3");
let db = test_db();
let original_rowid = insert_track(&db, "artist/album/track.mp3");
assert!(!db.remove_old(&vec![music.path.clone()], true));
assert_eq!(
vec![(original_rowid, "artist/album/track.mp3".to_string())],
track_paths(&db)
);
}
#[test]
fn corrects_case_without_losing_cue_track_suffix() {
let music = TestMusicRoot::new();
music.create_file("Artist/Album/Image.flac");
let db = test_db();
let original_rowid = insert_track(&db, "artist/album/image.flac.CUE_TRACK.02");
assert!(db.remove_old(&vec![music.path.clone()], false));
assert_eq!(
vec![(
original_rowid,
"Artist/Album/Image.flac.CUE_TRACK.02".to_string()
)],
track_paths(&db)
);
}
#[test]
fn removes_path_that_is_really_missing() {
let music = TestMusicRoot::new();
let db = test_db();
insert_track(&db, "Artist/Album/Missing.mp3");
assert!(db.remove_old(&vec![music.path.clone()], false));
assert!(track_paths(&db).is_empty());
}
#[cfg(not(windows))]
#[test]
fn keeps_distinct_files_that_differ_only_by_case() {
let music = TestMusicRoot::new();
music.create_file("Artist/Track.mp3");
music.create_file("artist/track.mp3");
let entries: HashSet<OsString> = fs::read_dir(&music.path)
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
if entries.len() != 2 {
return;
}
let db = test_db();
let first_rowid = insert_track(&db, "Artist/Track.mp3");
let second_rowid = insert_track(&db, "artist/track.mp3");
assert!(!db.remove_old(&vec![music.path.clone()], false));
assert_eq!(
vec![
(first_rowid, "Artist/Track.mp3".to_string()),
(second_rowid, "artist/track.mp3".to_string())
],
track_paths(&db)
);
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -111,12 +111,12 @@ fn main() {
}
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("export") && !task.eq_ignore_ascii_case("stopmixer") && !task.eq_ignore_ascii_case("analyse-lms") {
&& !task.eq_ignore_ascii_case("upload") && !task.eq_ignore_ascii_case("export") && !task.eq_ignore_ascii_case("clearfail")
&& !task.eq_ignore_ascii_case("stopmixer") && !task.eq_ignore_ascii_case("analyse-lms") {
log::error!("Invalid task ({}) supplied", task);
process::exit(-1);
}
if !config_file.is_empty() {
let path = PathBuf::from(&config_file);
if path.exists() && path.is_file() {
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022-2025 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2026 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/