75 Commits
Author SHA1 Message Date
CDrummond 04baedbfc2 Don't try to download ffmpeg5 or macOS builds - currently broken 2023-08-11 17:16:41 +01:00
CDrummond 609b6faff9 So, ffmpeg5 ppa is now paywalled so cannot use. 2023-08-10 21:25:21 +01:00
CDrummond fe65d35066 Revert ffmpeg apt steps, and disable macOS builds (failing!) 2023-08-10 21:07:23 +01:00
CDrummond 75aeee128f Use 22.04 for ffmpeg5? 2023-08-10 20:50:46 +01:00
CDrummond 65e40acce4 Update bliss and lofty libraries 2023-08-10 20:08:17 +01:00
CDrummond a9f819f39b Update version of bliss library. 2023-02-28 15:48:16 +00:00
CDrummond 4704624b2f Add option to limit number of concurrent threads. 2023-02-28 15:41:01 +00:00
CDrummond c3083d9a1a Update (C) year 2023-01-02 09:28:26 +00:00
CDrummond 90d4dfec23 Nope, revert back to bullseye as build fails on stretch 2022-12-20 21:50:30 +00:00
CDrummond e89aee1f4e Build on stretch? 2022-12-20 21:40:58 +00:00
CDrummond 703e39508c Update lofty to 0.9.0 2022-12-20 09:25:45 +00:00
CDrummond eb16d8379d Update bliss-rs 2022-10-22 11:49:39 +01:00
CDrummond a220b4d0c5 Update bliss-rs and lofty-rs 2022-10-04 17:06:52 +01:00
CDrummond 6d61616dda Update lofty version 2022-08-21 08:55:23 +01:00
CDrummond d85424d27c Update lofty to 0.7.3 2022-08-04 22:26:32 +01:00
CDrummond 144c8a7ffe Update version of tag reader library. 2022-07-03 20:17:36 +01:00
CDrummond ab0adc96ca Show analysis results on a new line after progress. Just show
"Finished!" as last progress message.
Issue #5
2022-06-19 17:53:24 +01:00
CDrummond 7cf62c3ad0 Fix chmod call 2022-06-18 10:18:46 +01:00
CDrummond d9841b3527 Copy files within docker 2022-06-18 10:00:10 +01:00
CDrummond 3b6872b2a0 Place ARM binaries in bin sub-folder 2022-06-18 09:05:00 +01:00
CDrummond 45b20902d6 Merge branch 'master' into linux-arm 2022-06-18 08:44:47 +01:00
CDrummond 9feb80bbd1 Download arm builds 2022-06-18 08:44:17 +01:00
CDrummond b9b8fdabfc Add script to launch arm for correct arch 2022-06-18 08:40:17 +01:00
CDrummond a818628706 Add other deps 2022-06-18 08:05:44 +01:00
CDrummond 370c533a49 Revery to using docker, and add changes from chincheta0815 - #3 2022-06-18 07:55:54 +01:00
CDrummond 9aae7825d0 No sudo on bullseye either... 2022-06-16 20:18:25 +01:00
CDrummond ebddf8febe Use sudo for mount 2022-06-16 19:35:43 +01:00
CDrummond 143fb2bf62 bullseye? 2022-06-16 19:34:42 +01:00
CDrummond 323978cbeb tmpfs? 2022-06-16 19:26:22 +01:00
CDrummond a749c03195 source $HOME/.cargo/env 2022-06-16 19:16:42 +01:00
CDrummond bfd2a44e2e Download rust? #3 2022-06-16 19:05:46 +01:00
CDrummond 943227c16b Update run-on-arch-action version. #3 2022-06-16 18:32:39 +01:00
CDrummond b7259cd0ae Install cargo? #3 2022-06-16 18:01:19 +01:00
CDrummond f023faa336 Alternate method 2022-06-13 13:21:25 +01:00
CDrummond 074be07d18 Rust 2021? 2022-06-13 13:08:18 +01:00
CDrummond a12971df17 Split docker runs? 2022-06-13 12:53:32 +01:00
CDrummond 8ba10a0f8e nightly? 2022-06-13 12:51:09 +01:00
CDrummond cf0e6cf5fb edition2021 requires nightly 2022-06-13 12:47:57 +01:00
CDrummond de2c2dfaa3 Add "cargo-features = ["edition2021"]" at top 2022-06-13 12:39:44 +01:00
CDrummond 1a45791e58 cargo-features = ["edition2021"] ???? 2022-06-13 12:24:23 +01:00
CDrummond b5f35dcf36 Build for Linux ARM 2022-06-13 12:02:25 +01:00
CDrummond cd12e805c3 More compact 2022-05-28 17:50:19 +01:00
CDrummond fa87a7e135 Fix checking if CUE already analysed.
Issue #2
2022-05-28 17:24:09 +01:00
CDrummond df17032377 Fix Job names 2022-05-26 12:08:52 +01:00
CDrummond e47c2b477e Single run command 2022-05-26 12:07:18 +01:00
CDrummond 133e3e2204 Produce ffmpeg 4 and 5 builds for Linux 2022-05-26 12:04:15 +01:00
CraigD 3dec74d328 Mention password protected LMS access 2022-05-21 10:11:17 +01:00
CDrummond cb323f3efe Increment version 2022-05-21 10:04:10 +01:00
CDrummond 69303d62c6 Update lofty 2022-05-21 09:30:09 +01:00
CDrummond f05e11da2e Add Fedora deps instructions 2022-05-21 09:24:00 +01:00
Craig Drummond 37947564fc Remove mention of temporary files, as this is no longer relevant. 2022-04-24 08:51:07 +01:00
Craig Drummond 8a5d9bc276 Don't need ffmpeg.exe for Windows builds, as bliss-rs does cue splitting 2022-04-24 08:35:27 +01:00
Craig Drummond 467e341cfb Update version of bliss-rs, this now handles CUE processing internally. 2022-04-18 20:20:32 +01:00
Craig Drummond 2788759829 More 'cargo fmt' reverts 2022-04-15 09:03:47 +01:00
Craig Drummond 0653e365b8 Undo some 'cargo fmt' changes. 2022-04-13 21:32:44 +01:00
Craig Drummond 985902325e Minor formatting mods 2022-04-13 21:07:09 +01:00
Craig Drummond f3903383c9 Update version of bliss-rs 2022-04-12 21:17:29 +01:00
Craig Drummond f13601f1c2 loft-rs 0.6.1 2022-04-10 20:27:29 +01:00
CDrummond dfb87881c9 Add colon 2022-04-06 13:23:41 +01:00
CDrummond b1f89050e2 Mention that must run from terminal window 2022-04-06 13:22:47 +01:00
CDrummond 94ef2e2c1f Show error if can't open/create database 2022-04-06 13:22:30 +01:00
CDrummond 7fb2e66c58 Update version of tag reader library, should now support ID3v2 in FLAC. 2022-04-06 13:10:05 +01:00
CDrummond 9f8ea6e490 Fix indentation 2022-03-23 20:16:27 +00:00
CDrummond 0aa10918ae Tidy up code, thanks to Serial-ATA 2022-03-23 20:15:54 +00:00
CraigD caa8f124a7 Merge pull request #1 from Serial-ATA/master
Cleanup
2022-03-23 07:34:44 +00:00
Serial dabbfdb206 More typos
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 19:08:50 -04:00
Serial 182d980395 Typos
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 19:07:59 -04:00
Serial c51de47696 Missing derive
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 18:05:39 -04:00
Serial f6edb983d3 Cleanup upload module
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:49:27 -04:00
Serial 625248fbc6 Cleanup tags module
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:44:40 -04:00
Serial 37d9f627ad Cleanup db module
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:41:02 -04:00
Serial b61aee0a6a Cleanup cue module
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:32:31 -04:00
Serial 51ce6cadc7 Cleanup analyse module
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:25:03 -04:00
Serial b097f06b5f Add Metadata::is_empty
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:05:33 -04:00
Serial e0a6759af5 fmt
Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
2022-03-22 17:02:08 -04:00
16 changed files with 761 additions and 1267 deletions

No files matched your search

+72 -25
View File
@@ -7,7 +7,7 @@ env:
CARGO_TERM_COLOR: always
jobs:
Linux:
Linux_ffmpeg4:
runs-on: ubuntu-20.04
steps:
@@ -15,8 +15,8 @@ jobs:
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 libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev ffmpeg
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
@@ -30,42 +30,90 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: bliss-analyser-linux
name: bliss-analyser-linux-x86-ffmpeg4
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/
macOS:
runs-on: macos-11.0
Linux_ARM:
runs-on: ubuntu-20.04
steps:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Checkout
uses: actions/checkout@v2
- name: Install deps
- name: Build ARM
run: |
brew install ffmpeg@5
- name: Build
run: |
mkdir releases
cargo build --release
strip target/release/bliss-analyser
cp target/release/bliss-analyser releases/bliss-analyser
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/macos.ini releases/config.ini
docker build -t bliss-analyser-cross - < docker/Dockerfile
docker run --rm -v $PWD/target:/build -v $PWD:/src bliss-analyser-cross
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: bliss-analyser-mac
name: bliss-analyser-linux-arm
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
#
# steps:
# - name: Install Rust
# uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
#
# - name: Checkout
# uses: actions/checkout@v2
#
# - name: Install deps
# run: |
# brew install ffmpeg@5
#
# - name: Build
# run: |
# mkdir releases
# cargo build --release
# strip target/release/bliss-analyser
# cp target/release/bliss-analyser releases/bliss-analyser
# cp UserGuide.md releases/README.md
# cp LICENSE releases/
# cp configs/macos.ini releases/config.ini
#
# - name: Upload artifacts
# uses: actions/upload-artifact@v2
# with:
# name: bliss-analyser-mac
# path: releases/
Windows:
runs-on: windows-2019
@@ -98,7 +146,6 @@ jobs:
mkdir releases
cp target/release/bliss-analyser.exe releases/bliss-analyser.exe
cp deps/bin/*.dll releases/
cp deps/bin/ffmpeg.exe releases/
cp UserGuide.md releases/README.md
cp LICENSE releases/
cp configs/windows.ini releases/config.ini
Generated
+141 -569
View File
File diff suppressed because it is too large. Load diff
+5 -8
View File
@@ -1,8 +1,8 @@
[package]
name = "bliss-analyser"
version = "0.1.0"
version = "0.2.2"
authors = ["Craig Drummond <craig.p.drummond@gmail.com>"]
edition = "2018"
edition = "2021"
license = "GPL-3.0-only"
description = "Analyse audio files with bliss-rs"
repository = "https://github.com/CDrummond/bliss-analyser"
@@ -10,22 +10,19 @@ keywords = ["audio", "song", "similarity"]
readme = "README.md"
[dependencies]
bliss-audio = { git = "https://github.com/Polochon-street/bliss-rs", rev = "5f366b0" }
bliss-audio = { version = "0.6.9", features = [] }
argparse = "0.2.2"
anyhow = "1.0.40"
rusqlite = { version = "0.25.0", features = ["bundled"] }
log = "0.4.14"
env_logger = "0.8.4"
indicatif = "0.16.2"
lofty = { git = "https://github.com/Serial-ATA/lofty-rs", rev = "45182b6" }
lofty = "0.15.0"
dirs = "1"
chrono = "0.4.19"
regex = "1"
substring = "1.4.5"
ureq = "2.4.0"
configparser = "3.0.0"
rcue = { git = "https://github.com/gyng/rcue" }
hhmmss = "0.1.0"
if_chain = "1.0.2"
num_cpus = "1.13.0"
tempdir = "0.3.7"
subprocess = "0.2.8"
+23
View File
@@ -1,3 +1,26 @@
0.2.3
-----
1. Add option to limit number of concurrent threads.
2. Update version of tag reader library.
3. Update version of bliss library.
0.2.2
-----
1. Update version of tag reader library.
2. Update version of bliss library.
0.2.1
-----
1. Update version of tag reader library.
2. Fix checking if CUE already analysed.
0.2.0
-----
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.
4. Update version of bliss-rs, this now handles CUE processing internally.
0.1.0
-----
1. Add support for analysing CUE files.
+5
View File
@@ -16,6 +16,11 @@ 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`
+5 -4
View File
@@ -6,6 +6,8 @@ upload its database of music analysis to LMS. The `Bliss Mixer` LMS plugin can
then use this information to provide music mixes for LMS's `Don't Stop the Music`
feature.
**NOTE:** You must run this application from a terminal window (e.g. cmd.com or
PowerShell for Windows), as there is no graphical user interface.
Quick guide
@@ -107,7 +109,8 @@ music folders may be specified via `music_1`, `music_2`, `music_3`, and `music_4
* `db` specifies the name and location of the database file used to store the
analysis results. This will default to `bliss.db` in the current folder.
* `lms` specifies the hostname, or IP address, of your LMS server. This is used
when uploading the database file to LMS. This defaults to `127.0.0.1`
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`
* `ignore` specifies the name and location of a file containing items to ignore
in mixes. See the `Ignore` section later on for more details.
@@ -180,9 +183,7 @@ CUE files
If the anlyser encounters an audio file with a matching CUE file (e.g.
`album.flac` and `album.cue` in same folder) then it will attempt to analyse the
individual tracks contained within. To do this the analyser uses `ffmpeg` to
create temporary files of each track, which are then analysed. (These temporary
files are removed afterwards).
individual tracks contained within.
Exclude folders
+36
View File
@@ -0,0 +1,36 @@
# Cross compilation environment for bliss-mixer
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.sh"]
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
## #!/usr/bin/env bash
set -eux
uname -a
DESTDIR=/src/releases
mkdir -p $DESTDIR/bin
rm -rf $DESTDIR/bin/*
function build {
echo Building for $1 to $3...
if [[ ! -f /build/$1/release/bliss-analyser ]]; then
export RUST_BACKTRACE=full
export PKG_CONFIG=${1//unknown-/}-pkg-config
BINDGEN_EXTRA_CLANG_ARGS="--sysroot /usr/${1//unknown-/}" cargo build --release --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
+7 -3
View File
@@ -3,7 +3,7 @@
#
# LMS-BlissMixer
#
# Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
# Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
# MIT license.
#
@@ -11,7 +11,9 @@ import datetime, os, requests, shutil, subprocess, sys, tempfile, time
GITHUB_TOKEN_FILE = "%s/.config/github-token" % os.path.expanduser('~')
GITHUB_REPO = "CDrummond/bliss-analyser"
GITHUB_ARTIFACTS = ["bliss-analyser-linux", "bliss-analyser-mac", "bliss-analyser-windows"]
#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"]
def info(s):
print("INFO: %s" %s)
@@ -69,7 +71,7 @@ def download_artifacts(version):
def make_executable(version):
cwd = os.getcwd()
for a in ["bliss-analyser-linux", "bliss-analyser-mac"]:
for a in UNIX_ARTIFACTS:
archive = "%s-%s.zip" % (a, version)
info("Making analyser executable in %s" % archive)
with tempfile.TemporaryDirectory() as td:
@@ -77,6 +79,8 @@ 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)
shutil.make_archive("%s/%s-%s" % (cwd, a, version), "zip")
os.chdir(cwd)
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
ARCH=`arch`
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
if [ "$ARCH" = "aarch64" ] ; then
$SCRIPT_DIR/bin/bliss-analyser-aarch64 $*
else
$SCRIPT_DIR/bin/bliss-analyser-armhf $*
fi
+172 -307
View File
@@ -1,155 +1,187 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use anyhow::{Result};
use bliss_audio::{library::analyze_paths_streaming, BlissResult, Song};
use hhmmss::Hhmmss;
use indicatif::{ProgressBar, ProgressStyle};
use std::convert::TryInto;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::time::Duration;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use subprocess::{Exec, NullFile};
use tempdir::TempDir;
use num_cpus;
use crate::cue;
use crate::db;
use crate::tags;
use anyhow::Result;
use bliss_audio::{analyze_paths_with_cores};
use if_chain::if_chain;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashSet;
use std::convert::TryInto;
use std::fs::{DirEntry, File};
use std::io::{BufRead, BufReader};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use num_cpus;
const DONT_ANALYSE:&str = ".notmusic";
const MAX_ERRORS_TO_SHOW:usize = 100;
const MAX_TAG_ERRORS_TO_SHOW:usize = 50;
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"];
fn get_file_list(db:&mut db::Db, mpath:&PathBuf, path:&PathBuf, track_paths:&mut Vec<String>, cue_tracks:&mut Vec<cue::CueTrack>) {
if path.is_dir() {
match path.read_dir() {
Ok(items) => {
for item in items {
match item {
Ok(entry) => {
let pb = entry.path().to_path_buf();
if entry.path().is_dir() {
let mut check = pb.clone();
check.push(PathBuf::from(DONT_ANALYSE));
if check.exists() {
log::info!("Skipping '{}', found '{}'", pb.to_string_lossy(), DONT_ANALYSE);
} else {
get_file_list(db, mpath, &entry.path(), track_paths, cue_tracks);
}
} else if entry.path().is_file() {
let e = pb.extension();
if e.is_some() {
let ext = e.unwrap().to_string_lossy();
if ext=="m4a" || ext=="mp3" || ext=="ogg" || ext=="flac" || ext=="opus" {
match pb.strip_prefix(mpath) {
Ok(stripped) => {
let mut cue_file = pb.clone();
cue_file.set_extension("cue");
if cue_file.exists() {
// Found a CUE file, try to parse and then check if tracks exists in DB
let this_cue_tracks = cue::parse(&pb, &cue_file);
for track in this_cue_tracks {
match track.track_path.strip_prefix(mpath) {
Ok(tstripped) => {
let spb = tstripped.to_path_buf();
let sname = String::from(spb.to_string_lossy());
match db.get_rowid(&sname) {
Ok(id) => {
if id<=0 {
cue_tracks.push(track.clone());
}
},
Err(_) => { }
}
},
Err(_) => { }
}
}
} else {
let spb = stripped.to_path_buf();
let sname = String::from(spb.to_string_lossy());
match db.get_rowid(&sname) {
Ok(id) => {
if id<=0 {
track_paths.push(String::from(pb.to_string_lossy()));
}
},
Err(_) => { }
}
}
},
Err(_) => { }
}
}
}
}
},
Err(_) => { }
}
}
},
Err(_) => { }
fn get_file_list(db: &mut db::Db, mpath: &Path, path: &Path, track_paths: &mut Vec<String>) {
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);
}
}
}
}
pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -> Result<()> {
fn check_dir_entry(db: &mut db::Db, mpath: &Path, entry: DirEntry, track_paths: &mut Vec<String>) {
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 pb.is_file() {
if_chain! {
if let Some(ext) = pb.extension();
let ext = ext.to_string_lossy();
if VALID_EXTENSIONS.contains(&&*ext);
if let Ok(stripped) = pb.strip_prefix(mpath);
then {
let sname = String::from(stripped.to_string_lossy());
let mut cue_file = pb.clone();
cue_file.set_extension("cue");
if cue_file.exists() {
// For cue files, check if first track is in DB
let mut cue_track_path = pb.clone();
let ext = pb.extension().unwrap().to_string_lossy();
cue_track_path.set_extension(format!("{}{}1", ext, db::CUE_MARKER));
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) {
if id<=0 {
track_paths.push(String::from(cue_file.to_string_lossy()));
}
}
}
} else {
if let Ok(id) = db.get_rowid(&sname) {
if id<=0 {
track_paths.push(String::from(pb.to_string_lossy()));
}
}
}
}
}
}
}
pub fn analyse_new_files(db: &db::Db, mpath: &PathBuf, track_paths: Vec<String>, max_threads: usize) -> Result<()> {
let total = track_paths.len();
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
let 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 results = analyze_paths_streaming(track_paths)?;
let mut analysed = 0;
let mut failed:Vec<String> = Vec::new();
let mut tag_error:Vec<String> = Vec::new();
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 tracks");
for (path, result) in results {
let pbuff = PathBuf::from(&path);
let stripped = pbuff.strip_prefix(mpath).unwrap();
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());
pb.set_message(format!("{}", sname));
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);
let meta = tags::read(&cpath);
if meta.title.is_empty() && meta.artist.is_empty() && meta.album.is_empty() && meta.genre.is_empty() {
tag_error.push(sname.clone());
}
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
};
db.add_track(&sname, &meta, &track.analysis);
analysed += 1;
},
Err(e) => {
failed.push(format!("{} - {}", sname, e));
// 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)); }
};
pb.inc(1);
if inc_progress {
progress.inc(1);
}
}
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
progress.finish_with_message("Finished!");
log::info!("{} Analysed. {} Failure(s).", analysed, failed.len());
if !failed.is_empty() {
let total = failed.len();
failed.truncate(MAX_ERRORS_TO_SHOW);
log::error!("Failed to analyse the folling track(s):");
log::error!("Failed to analyse the following file(s):");
for err in failed {
log::error!(" {}", err);
}
if total>MAX_ERRORS_TO_SHOW {
if total > MAX_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
}
}
@@ -157,162 +189,18 @@ pub fn analyse_new_files(db:&db::Db, mpath: &PathBuf, track_paths:Vec<String>) -
let total = tag_error.len();
tag_error.truncate(MAX_TAG_ERRORS_TO_SHOW);
log::error!("Failed to read tags of the folling track(s):");
log::error!("Failed to read tags of the following file(s):");
for err in tag_error {
log::error!(" {}", err);
}
if total>MAX_TAG_ERRORS_TO_SHOW {
if total > MAX_TAG_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_TAG_ERRORS_TO_SHOW);
}
}
Ok(())
}
pub fn analyze_cue_streaming(tracks: Vec<cue::CueTrack>,) -> BlissResult<Receiver<(cue::CueTrack, BlissResult<Song>)>> {
let num_cpus = num_cpus::get();
let last_track_duration = Duration::new(cue::LAST_TRACK_DURATION, 0);
#[allow(clippy::type_complexity)]
let (tx, rx): (
Sender<(cue::CueTrack, BlissResult<Song>)>,
Receiver<(cue::CueTrack, BlissResult<Song>)>,
) = mpsc::channel();
if tracks.is_empty() {
return Ok(rx);
}
let mut handles = Vec::new();
let mut chunk_length = tracks.len() / num_cpus;
if chunk_length == 0 {
chunk_length = tracks.len();
} else if chunk_length == 1 && tracks.len() > num_cpus {
chunk_length = 2;
}
for chunk in tracks.chunks(chunk_length) {
let tx_thread = tx.clone();
let owned_chunk = chunk.to_owned();
let child = thread::spawn(move || {
let mut idx = 0;
match &TempDir::new("bliss") {
Ok(dir) => {
for cue_track in owned_chunk {
let audio_path = String::from(cue_track.audio_path.to_string_lossy());
let ext = cue_track.audio_path.extension();
let track_path = String::from(cue_track.track_path.to_string_lossy());
let mut tmp_file = PathBuf::from(dir.path());
if ext.is_some() {
tmp_file.push(format!("{}.{}", idx, ext.unwrap().to_string_lossy()));
} else {
tmp_file.push(format!("{}.flac", idx));
}
idx += 1;
log::debug!("Extracting '{}'", track_path);
match Exec::cmd("ffmpeg").arg("-i").arg(&audio_path)
.arg("-ss").arg(&cue_track.start.hhmmss())
.arg("-t").arg(&cue_track.duration.hhmmss())
.arg("-c").arg("copy")
.arg(String::from(tmp_file.to_string_lossy()))
.stderr(NullFile)
.join() {
Ok(_) => { },
Err(e) => { log::error!("Failed to call ffmpeg. {}", e); }
}
if ! cfg!(windows) {
// ffmpeg seeks to break echo on terminal? 'stty echo' restores...
match Exec::cmd("stty").arg("echo").join() {
Ok(_) => { },
Err(_) => { }
}
}
if tmp_file.exists() {
log::debug!("Analyzing '{}'", track_path);
let song = Song::new(&tmp_file);
if cue_track.duration>=last_track_duration {
// Last track, so read duration from temp file
let mut cloned = cue_track.clone();
let meta = tags::read(&String::from(tmp_file.to_string_lossy()));
cloned.duration = Duration::new(meta.duration as u64, 0);
tx_thread.send((cloned, song)).unwrap();
} else {
tx_thread.send((cue_track, song)).unwrap();
}
match fs::remove_file(tmp_file) {
Ok(_) => { },
Err(_) => { }
}
} else {
log::error!("Failed to create temp file");
}
}
},
Err(e) => { log::error!("Failed to create temp folder. {}", e); }
}
});
handles.push(child);
}
Ok(rx)
}
pub fn analyse_new_cue_tracks(db:&db::Db, mpath: &PathBuf, cue_tracks:Vec<cue::CueTrack>) -> Result<()> {
let total = cue_tracks.len();
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
let results = analyze_cue_streaming(cue_tracks)?;
let mut analysed = 0;
let mut failed:Vec<String> = Vec::new();
log::info!("Analysing new cue tracks");
for (track, result) in results {
let stripped = track.track_path.strip_prefix(mpath).unwrap();
let spbuff = stripped.to_path_buf();
let sname = String::from(spbuff.to_string_lossy());
pb.set_message(format!("{}", sname));
match result {
Ok(song) => {
let meta = db::Metadata {
title:track.title,
artist:track.artist,
album_artist:track.album_artist,
album:track.album,
genre:track.genre,
duration:track.duration.as_secs() as u32
};
db.add_track(&sname, &meta, &song.analysis);
analysed += 1;
},
Err(e) => {
failed.push(format!("{} - {}", sname, e));
}
};
pb.inc(1);
}
pb.finish_with_message(format!("{} Analysed. {} Failure(s).", analysed, failed.len()));
if !failed.is_empty() {
let total = failed.len();
failed.truncate(MAX_ERRORS_TO_SHOW);
log::error!("Failed to analyse the folling track(s):");
for err in failed {
log::error!(" {}", err);
}
if total>MAX_ERRORS_TO_SHOW {
log::error!(" + {} other(s)", total - MAX_ERRORS_TO_SHOW);
}
}
Ok(())
}
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_old:bool, max_num_tracks:usize) {
pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run: bool, keep_old: bool, max_num_tracks: usize, max_threads: usize) {
let mut db = db::Db::new(&String::from(db_path));
let mut track_count_left = max_num_tracks;
@@ -325,68 +213,44 @@ pub fn analyse_files(db_path: &str, mpaths: &Vec<PathBuf>, dry_run:bool, keep_ol
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 track_paths: Vec<String> = Vec::new();
if mpaths.len()>1 {
log::info!("Looking for new tracks in {}", mpath.to_string_lossy());
if mpaths.len() > 1 {
log::info!("Looking for new files in {}", mpath.to_string_lossy());
} else {
log::info!("Looking for new tracks");
log::info!("Looking for new files");
}
get_file_list(&mut db, &mpath, &cur, &mut track_paths, &mut cue_tracks);
get_file_list(&mut db, &mpath, &cur, &mut track_paths);
track_paths.sort();
log::info!("Num new tracks: {}", track_paths.len());
if !cue_tracks.is_empty() {
log::info!("Num new cue tracks: {}", cue_tracks.len());
}
log::info!("Num new files: {}", track_paths.len());
if dry_run {
if !track_paths.is_empty() || !cue_tracks.is_empty() {
if !track_paths.is_empty() {
log::info!("The following need to be analysed:");
for track in track_paths {
log::info!(" {}", track);
}
for track in cue_tracks {
log::info!(" {}", track.track_path.to_string_lossy());
}
}
} else {
if max_num_tracks>0 {
if track_paths.len()>track_count_left {
log::info!("Only analysing {} tracks", track_count_left);
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 max_num_tracks>0 {
if track_count_left == 0 {
cue_tracks.clear();
} else {
if cue_tracks.len()>track_count_left {
log::info!("Only analysing {} cue tracks", track_count_left);
cue_tracks.truncate(track_count_left);
}
track_count_left -= track_paths.len();
}
}
if !track_paths.is_empty() {
match analyse_new_files(&db, &mpath, track_paths) {
Ok(_) => { },
match analyse_new_files(&db, &mpath, track_paths, max_threads) {
Ok(_) => { }
Err(e) => { log::error!("Analysis returned error: {}", e); }
}
} else {
log::info!("No new tracks to analyse");
log::info!("No new files to analyse");
}
if !cue_tracks.is_empty() {
match analyse_new_cue_tracks(&db, &mpath, cue_tracks) {
Ok(_) => { },
Err(e) => { log::error!("Cue analysis returned error: {}", e); }
}
}
if max_num_tracks>0 && track_count_left<=0 {
log::info!("Track limit reached");
if max_num_tracks > 0 && track_count_left <= 0 {
log::info!("File limit reached");
break;
}
}
@@ -409,11 +273,12 @@ pub fn update_ignore(db_path: &str, ignore_path: &PathBuf) {
db.init();
db.clear_ignore();
for (_index, line) in reader.lines().enumerate() {
let line = line.unwrap();
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();
}
-96
View File
@@ -1,96 +0,0 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
extern crate rcue;
use rcue::parser::parse_from_file;
use std::path::PathBuf;
use std::time::Duration;
pub const MARKER:&str = ".CUE_TRACK.";
pub const LAST_TRACK_DURATION:u64 = 60*60*24;
const GENRE:&str = "GENRE";
#[derive(Clone)]
pub struct CueTrack {
pub audio_path:PathBuf,
pub track_path:PathBuf,
pub title:String,
pub artist:String,
pub album:String,
pub album_artist:String,
pub genre:String,
pub start:Duration,
pub duration:Duration
}
pub fn parse(audio_path:&PathBuf, cue_path:&PathBuf) -> Vec<CueTrack> {
let mut resp:Vec<CueTrack> = Vec::new();
match parse_from_file(&cue_path.to_string_lossy(), false) {
Ok(cue) => {
let album = cue.title.unwrap_or(String::new());
let album_artist = cue.performer.unwrap_or(String::new());
let mut genre = String::new();
for comment in cue.comments {
if comment.0.eq(GENRE) {
genre = comment.1;
}
}
if 1 == cue.files.len() {
for file in cue.files {
for track in file.tracks {
match track.indices.get(0) {
Some((_, start)) => {
let mut track_path = audio_path.clone();
let ext = audio_path.extension().unwrap().to_string_lossy();
track_path.set_extension(format!("{}{}{}", ext, MARKER, resp.len()+1));
let mut ctrack = CueTrack {
audio_path: audio_path.clone(),
track_path: track_path,
title: track.title.unwrap_or(String::new()),
artist: track.performer.unwrap_or(String::new()),
album_artist: album_artist.clone(),
album: album.clone(),
genre: genre.clone(),
start: start.clone(),
duration: Duration::new(LAST_TRACK_DURATION, 0),
};
if ctrack.artist.is_empty() && !ctrack.album_artist.is_empty() {
ctrack.artist = ctrack.album_artist.clone();
}
if ctrack.album.is_empty() {
let mut path = audio_path.clone();
path.set_extension("");
match path.file_name() {
Some(n) => { ctrack.album = String::from(n.to_string_lossy()); }
None => { }
}
}
resp.push(ctrack);
},
None => { }
}
}
}
}
},
Err(e) => { log::error!("Failed to parse '{}'. {}", cue_path.to_string_lossy(), e);}
}
for i in 0..(resp.len()-1) {
let mut next_start = Duration::new(0, 0);
if let Some(next) = resp.get(i+1) {
next_start = next.start;
}
if let Some(elem) = resp.get_mut(i) {
(*elem).duration = next_start - elem.start;
}
}
resp
}
+131 -110
View File
@@ -1,53 +1,73 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use crate::tags;
use bliss_audio::{Analysis, AnalysisIndex};
use indicatif::{ProgressBar, ProgressStyle};
use rusqlite::{Connection, params};
use rusqlite::{params, Connection};
use std::convert::TryInto;
use std::path::PathBuf;
use std::process;
use crate::cue;
use crate::tags;
pub const CUE_MARKER: &str = ".CUE_TRACK.";
pub struct FileMetadata {
pub rowid:usize,
pub file:String,
pub title:Option<String>,
pub artist:Option<String>,
pub album_artist:Option<String>,
pub album:Option<String>,
pub genre:Option<String>,
pub duration:u32
pub rowid: usize,
pub file: String,
pub title: Option<String>,
pub artist: Option<String>,
pub album_artist: Option<String>,
pub album: Option<String>,
pub genre: Option<String>,
pub duration: u32,
}
#[derive(Default, PartialEq)]
pub struct Metadata {
pub title:String,
pub artist:String,
pub album_artist:String,
pub album:String,
pub genre:String,
pub duration:u32
pub title: String,
pub artist: String,
pub album_artist: String,
pub album: String,
pub genre: String,
pub duration: u32,
}
impl Metadata {
pub fn is_empty(&self) -> bool {
self.title.is_empty()
&& self.artist.is_empty()
&& self.album_artist.is_empty()
&& self.album.is_empty()
&& self.genre.is_empty()
}
}
pub struct Db {
pub conn: Connection
pub conn: Connection,
}
impl Db {
pub fn new(path: &String) -> Self {
Self {
conn: Connection::open(path).unwrap(),
match Connection::open(path) {
Ok(conn) => {
Self {
conn: conn,
}
}
Err(e) => {
log::error!("Failed top open/create database. {}", e);
process::exit(-1);
}
}
}
pub fn init(&self) {
match self.conn.execute(
let cmd = self.conn.execute(
"CREATE TABLE IF NOT EXISTS Tracks (
File text primary key,
Title text,
@@ -77,39 +97,35 @@ impl Db {
Chroma8 real,
Chroma9 real,
Chroma10 real
);",[]) {
Ok(_) => { },
Err(_) => {
log::error!("Failed to create DB table");
process::exit(-1);
}
);",
[],
);
if cmd.is_err() {
log::error!("Failed to create DB table");
process::exit(-1);
}
match self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)", []) {
Ok(_) => { },
Err(_) => {
log::error!("Failed to create DB index");
process::exit(-1);
}
let cmd = self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS Tracks_idx ON Tracks(File)", []);
if cmd.is_err() {
log::error!("Failed to create DB index");
process::exit(-1);
}
}
pub fn close(self) {
match self.conn.close() {
Ok(_) => { },
Err(_) => { }
}
let _ = self.conn.close();
}
pub fn get_rowid(&self, path: &String) -> Result<usize, rusqlite::Error> {
let mut db_path = path.clone();
pub fn get_rowid(&self, path: &str) -> Result<usize, rusqlite::Error> {
let mut db_path = path.to_string();
if cfg!(windows) {
db_path = db_path.replace("\\", "/");
}
let mut stmt = self.conn.prepare("SELECT rowid FROM Tracks WHERE File=:path;")?;
let track_iter = stmt.query_map(&[(":path", &db_path)], |row| {
Ok(row.get(0)?)
}).unwrap();
let mut rowid:usize = 0;
let track_iter = stmt.query_map(&[(":path", &db_path)], |row| Ok(row.get(0)?)).unwrap();
let mut rowid: usize = 0;
for tr in track_iter {
rowid = tr.unwrap();
break;
@@ -117,21 +133,21 @@ impl Db {
Ok(rowid)
}
pub fn add_track(&self, path: &String, meta: &Metadata, analysis:&Analysis) {
pub fn add_track(&self, path: &String, meta: &Metadata, analysis: &Analysis) {
let mut db_path = path.clone();
if cfg!(windows) {
db_path = db_path.replace("\\", "/");
}
match self.get_rowid(&path) {
Ok(id) => {
if id<=0 {
if id <= 0 {
match self.conn.execute("INSERT INTO Tracks (File, Title, Artist, AlbumArtist, Album, Genre, Duration, Ignore, Tempo, Zcr, MeanSpectralCentroid, StdDevSpectralCentroid, MeanSpectralRolloff, StdDevSpectralRolloff, MeanSpectralFlatness, StdDevSpectralFlatness, MeanLoudness, StdDevLoudness, Chroma1, Chroma2, Chroma3, Chroma4, Chroma5, Chroma6, Chroma7, Chroma8, Chroma9, Chroma10) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
params![db_path, meta.title, meta.artist, meta.album_artist, meta.album, meta.genre, meta.duration, 0,
analysis[AnalysisIndex::Tempo], analysis[AnalysisIndex::Zcr], analysis[AnalysisIndex::MeanSpectralCentroid], analysis[AnalysisIndex::StdDeviationSpectralCentroid], analysis[AnalysisIndex::MeanSpectralRolloff],
analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness],
analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5],
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10]]) {
Ok(_) => { },
Ok(_) => { }
Err(e) => { log::error!("Failed to insert '{}' into database. {}", path, e); }
}
} else {
@@ -141,30 +157,28 @@ impl Db {
analysis[AnalysisIndex::StdDeviationSpectralRolloff], analysis[AnalysisIndex::MeanSpectralFlatness], analysis[AnalysisIndex::StdDeviationSpectralFlatness], analysis[AnalysisIndex::MeanLoudness], analysis[AnalysisIndex::StdDeviationLoudness],
analysis[AnalysisIndex::Chroma1], analysis[AnalysisIndex::Chroma2], analysis[AnalysisIndex::Chroma3], analysis[AnalysisIndex::Chroma4], analysis[AnalysisIndex::Chroma5],
analysis[AnalysisIndex::Chroma6], analysis[AnalysisIndex::Chroma7], analysis[AnalysisIndex::Chroma8], analysis[AnalysisIndex::Chroma9], analysis[AnalysisIndex::Chroma10], id]) {
Ok(_) => { },
Ok(_) => { }
Err(e) => { log::error!("Failed to update '{}' in database. {}", path, e); }
}
}
},
}
Err(_) => { }
}
}
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run:bool) {
log::info!("Looking for non-existant tracks");
pub fn remove_old(&self, mpaths: &Vec<PathBuf>, dry_run: bool) {
log::info!("Looking for non-existent tracks");
let mut stmt = self.conn.prepare("SELECT File FROM Tracks;").unwrap();
let track_iter = stmt.query_map([], |row| {
Ok((row.get(0)?,))
}).unwrap();
let mut to_remove:Vec<String> = Vec::new();
let track_iter = stmt.query_map([], |row| Ok((row.get(0)?,))).unwrap();
let mut to_remove: Vec<String> = Vec::new();
for tr in track_iter {
let mut db_path:String = tr.unwrap().0;
let mut db_path: String = tr.unwrap().0;
let orig_path = db_path.clone();
match orig_path.find(cue::MARKER) {
match orig_path.find(CUE_MARKER) {
Some(s) => {
db_path.truncate(s);
},
None => { }
}
None => {}
}
if cfg!(windows) {
db_path = db_path.replace("/", "\\");
@@ -187,8 +201,8 @@ impl Db {
}
let num_to_remove = to_remove.len();
log::info!("Num non-existant tracks: {}", num_to_remove);
if num_to_remove>0 {
log::info!("Num non-existent tracks: {}", num_to_remove);
if num_to_remove > 0 {
if dry_run {
log::info!("The following need to be removed from database:");
for t in to_remove {
@@ -198,9 +212,10 @@ impl Db {
let count_before = self.get_track_count();
for t in to_remove {
//log::debug!("Remove '{}'", t);
match self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]) {
Ok(_) => { },
Err(e) => { log::error!("Failed to remove '{}' - {}", t, e) }
let cmd = self.conn.execute("DELETE FROM Tracks WHERE File = ?;", params![t]);
if let Err(e) = cmd {
log::error!("Failed to remove '{}' - {}", t, e)
}
}
let count_now = self.get_track_count();
@@ -213,10 +228,8 @@ impl Db {
pub fn get_track_count(&self) -> usize {
let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM Tracks;").unwrap();
let track_iter = stmt.query_map([], |row| {
Ok(row.get(0)?)
}).unwrap();
let mut count:usize = 0;
let track_iter = stmt.query_map([], |row| Ok(row.get(0)?)).unwrap();
let mut count: usize = 0;
for tr in track_iter {
count = tr.unwrap();
break;
@@ -226,51 +239,56 @@ impl Db {
pub fn update_tags(&self, mpaths: &Vec<PathBuf>) {
let total = self.get_track_count();
if total>0 {
let pb = ProgressBar::new(total.try_into().unwrap());
let style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:25}] {percent:>3}% {pos:>6}/{len:6} {wide_msg}")
.progress_chars("=> ");
pb.set_style(style);
if total > 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("=> "),
);
let mut stmt = self.conn.prepare("SELECT rowid, File, Title, Artist, AlbumArtist, Album, Genre, Duration FROM Tracks ORDER BY File ASC;").unwrap();
let track_iter = stmt.query_map([], |row| {
Ok(FileMetadata {
rowid: row.get(0)?,
file: row.get(1)?,
title: row.get(2)?,
artist: row.get(3)?,
album_artist: row.get(4)?,
album: row.get(5)?,
genre: row.get(6)?,
duration: row.get(7)?,
let track_iter = stmt
.query_map([], |row| {
Ok(FileMetadata {
rowid: row.get(0)?,
file: row.get(1)?,
title: row.get(2)?,
artist: row.get(3)?,
album_artist: row.get(4)?,
album: row.get(5)?,
genre: row.get(6)?,
duration: row.get(7)?,
})
})
}).unwrap();
.unwrap();
let mut updated = 0;
for tr in track_iter {
let dbtags = tr.unwrap();
if !dbtags.file.contains(cue::MARKER) {
let dtags = Metadata{
title:dbtags.title.unwrap_or(String::new()),
artist:dbtags.artist.unwrap_or(String::new()),
album_artist:dbtags.album_artist.unwrap_or(String::new()),
album:dbtags.album.unwrap_or(String::new()),
genre:dbtags.genre.unwrap_or(String::new()),
duration:dbtags.duration
if !dbtags.file.contains(CUE_MARKER) {
let dtags = Metadata {
title: dbtags.title.unwrap_or_default(),
artist: dbtags.artist.unwrap_or_default(),
album_artist: dbtags.album_artist.unwrap_or_default(),
album: dbtags.album.unwrap_or_default(),
genre: dbtags.genre.unwrap_or_default(),
duration: dbtags.duration,
};
pb.set_message(format!("{}", dbtags.file));
progress.set_message(format!("{}", dbtags.file));
for mpath in mpaths {
let track_path = mpath.join(&dbtags.file);
if track_path.exists() {
let path = String::from(track_path.to_string_lossy());
let ftags = tags::read(&path);
if ftags.title.is_empty() && ftags.artist.is_empty() && ftags.album_artist.is_empty() && ftags.album.is_empty() && ftags.genre.is_empty() {
if ftags.is_empty() {
log::error!("Failed to read tags of '{}'", dbtags.file);
} else if ftags.duration!=dtags.duration || ftags.title!=dtags.title || ftags.artist!=dtags.artist || ftags.album_artist!=dtags.album_artist || ftags.album!=dtags.album || ftags.genre!=dtags.genre {
} else if ftags != dtags {
match self.conn.execute("UPDATE Tracks SET Title=?, Artist=?, AlbumArtist=?, Album=?, Genre=?, Duration=? WHERE rowid=?;",
params![ftags.title, ftags.artist, ftags.album_artist, ftags.album, ftags.genre, ftags.duration, dbtags.rowid]) {
Ok(_) => { updated += 1; },
Ok(_) => { updated += 1; }
Err(e) => { log::error!("Failed to update tags of '{}'. {}", dbtags.file, e); }
}
}
@@ -278,32 +296,35 @@ impl Db {
}
}
}
pb.inc(1);
progress.inc(1);
}
pb.finish_with_message(format!("{} Updated.", updated))
progress.finish_with_message(format!("{} Updated.", updated))
}
}
pub fn clear_ignore(&self) {
match self.conn.execute("UPDATE Tracks SET Ignore=0;", []) {
Ok(_) => { },
Err(e) => { log::error!("Failed clear Ignore column. {}", e); }
let cmd = self.conn.execute("UPDATE Tracks SET Ignore=0;", []);
if let Err(e) = cmd {
log::error!("Failed clear Ignore column. {}", e);
}
}
pub fn set_ignore(&self, line:&str) {
pub fn set_ignore(&self, line: &str) {
log::info!("Ignore: {}", line);
if line.starts_with("SQL:") {
let sql = &line[4..];
match self.conn.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []) {
Ok(_) => { },
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
let cmd = self.conn.execute(&format!("UPDATE Tracks Set Ignore=1 WHERE {}", sql), []);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
} else {
match self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line), []) {
Ok(_) => { },
Err(e) => { log::error!("Failed set Ignore column for '{}'. {}", line, e); }
let cmd = self.conn.execute(&format!("UPDATE Tracks SET Ignore=1 WHERE File LIKE \"{}%\"", line), []);
if let Err(e) = cmd {
log::error!("Failed set Ignore column for '{}'. {}", line, e);
}
}
}
}
}
+30 -23
View File
@@ -1,10 +1,11 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use argparse::{ArgumentParser, Store, StoreTrue};
use chrono::Local;
use configparser::ini::Ini;
@@ -14,14 +15,12 @@ use std::io::Write;
use std::path::PathBuf;
use std::process;
mod analyse;
mod cue;
mod db;
mod tags;
mod upload;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const TOP_LEVEL_INI_TAG:&str = "Bliss";
const TOP_LEVEL_INI_TAG: &str = "Bliss";
fn main() {
let mut config_file = "config.ini".to_string();
@@ -29,16 +28,19 @@ fn main() {
let mut logging = "info".to_string();
let mut music_path = ".".to_string();
let mut ignore_file = "ignore.txt".to_string();
let mut keep_old:bool = false;
let mut dry_run:bool = false;
let mut keep_old: bool = false;
let mut dry_run: bool = false;
let mut task = "".to_string();
let mut lms_host = "127.0.0.1".to_string();
let mut max_num_tracks:usize = 0;
let mut music_paths:Vec<PathBuf> = Vec::new();
let mut max_num_files: usize = 0;
let mut music_paths: Vec<PathBuf> = Vec::new();
let mut max_threads: usize = 0;
match dirs::home_dir() {
Some(path) => { music_path = String::from(path.join("Music").to_string_lossy()); }
None => { }
Some(path) => {
music_path = String::from(path.join("Music").to_string_lossy());
}
None => {}
}
{
@@ -46,7 +48,7 @@ fn main() {
let music_path_help = format!("Music folder (default: {})", &music_path);
let db_path_help = format!("Database location (default: {})", &db_path);
let logging_help = format!("Log level; trace, debug, info, warn, error. (default: {})", logging);
let ignore_file_help = format!("File containg items to mark as ignored. (default: {})", ignore_file);
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 description = format!("Bliss Analyser v{}", VERSION);
@@ -58,29 +60,34 @@ fn main() {
arg_parse.refer(&mut music_path).add_option(&["-m", "--music"], Store, &music_path_help);
arg_parse.refer(&mut db_path).add_option(&["-d", "--db"], Store, &db_path_help);
arg_parse.refer(&mut logging).add_option(&["-l", "--logging"], Store, &logging_help);
arg_parse.refer(&mut keep_old).add_option(&["-k", "--keep-old"], StoreTrue, "Don't remove tracks from DB if they don't exist (used with analyse task)");
arg_parse.refer(&mut keep_old).add_option(&["-k", "--keep-old"], StoreTrue, "Don't remove files from DB if they don't exist (used with analyse task)");
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 max_num_tracks).add_option(&["-n", "--numtracks"], Store, "Maximum number of tracks to analyse");
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.parse_args_or_exit();
}
if !(logging.eq_ignore_ascii_case("trace") || logging.eq_ignore_ascii_case("debug") || logging.eq_ignore_ascii_case("info") || logging.eq_ignore_ascii_case("warn") || logging.eq_ignore_ascii_case("error")) {
if !(logging.eq_ignore_ascii_case("trace") || logging.eq_ignore_ascii_case("debug") || logging.eq_ignore_ascii_case("info")
|| logging.eq_ignore_ascii_case("warn") || logging.eq_ignore_ascii_case("error")) {
logging = String::from("info");
}
let mut builder = env_logger::Builder::from_env(env_logger::Env::default().filter_or("XXXXXXXX", logging));
builder.filter(Some("bliss_audio"), LevelFilter::Error);
builder.format(|buf, record| writeln!(buf, "[{} {:.1}] {}", Local::now().format("%Y-%m-%d %H:%M:%S"), record.level(), record.args()));
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() {
if task.is_empty() {
log::error!("No task specified, please choose from; analyse, tags, ignore, upload");
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") {
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") {
log::error!("Invalid task ({}) supplied", task);
process::exit(-1);
}
@@ -94,23 +101,23 @@ fn main() {
let path_keys: [&str; 5] = ["music", "music_1", "music_2", "music_3", "music_4"];
for key in &path_keys {
match config.get(TOP_LEVEL_INI_TAG, key) {
Some(val) => { music_paths.push(PathBuf::from(&val)); },
Some(val) => { music_paths.push(PathBuf::from(&val)); }
None => { }
}
}
match config.get(TOP_LEVEL_INI_TAG, "db") {
Some(val) => { db_path = val; },
Some(val) => { db_path = val; }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "lms") {
Some(val) => { lms_host = val; },
Some(val) => { lms_host = val; }
None => { }
}
match config.get(TOP_LEVEL_INI_TAG, "ignore") {
Some(val) => { ignore_file = val; },
Some(val) => { ignore_file = val; }
None => { }
}
},
}
Err(e) => {
log::error!("Failed to load config file. {}", e);
process::exit(-1);
@@ -170,7 +177,7 @@ fn main() {
}
analyse::update_ignore(&db_path, &ignore_path);
} else {
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_tracks);
analyse::analyse_files(&db_path, &music_paths, dry_run, keep_old, max_num_files, max_threads);
}
}
}
+54 -64
View File
@@ -1,87 +1,77 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
use lofty::{Accessor, ItemKey, Probe};
use crate::db;
use lofty::{Accessor, AudioFile, ItemKey, TaggedFileExt};
use regex::Regex;
use std::path::Path;
use substring::Substring;
use crate::db;
const MAX_GENRE_VAL:usize = 192;
const MAX_GENRE_VAL: usize = 192;
pub fn read(track:&String) -> db::Metadata {
let mut meta = db::Metadata{
title:String::new(),
artist:String::new(),
album:String::new(),
album_artist:String::new(),
genre:String::new(),
duration:180
pub fn read(track: &String) -> db::Metadata {
let mut meta = db::Metadata {
duration: 180,
..db::Metadata::default()
};
let path = Path::new(track);
match Probe::open(path) {
Ok(probe) => {
match probe.read(true) {
Ok(file) => {
let tag = match file.primary_tag() {
Some(primary_tag) => primary_tag,
None => file.first_tag().expect("Error: No tags found!"),
};
meta.title=tag.title().unwrap_or("").to_string();
meta.artist=tag.artist().unwrap_or("").to_string();
meta.album=tag.album().unwrap_or("").to_string();
meta.album_artist=tag.get_string(&ItemKey::AlbumArtist).unwrap_or("").to_string();
meta.genre=tag.genre().unwrap_or("").to_string();
// Check whether MP3 as numeric genre, and if so covert to text
if file.file_type().eq(&lofty::FileType::MP3) {
match tag.genre() {
Some(genre) => {
let test = &genre.parse::<u8>();
match test {
Ok(val) => {
let idx:usize = *val as usize;
if idx<MAX_GENRE_VAL {
meta.genre=lofty::id3::v1::GENRES[idx].to_string();
}
},
Err(_) => {
// Check for "(number)text"
let re = Regex::new(r"^\([0-9]+\)").unwrap();
if re.is_match(&genre) {
match genre.find(")") {
Some(end) => {
let test = &genre.to_string().substring(1, end).parse::<u8>();
match test {
Ok(val) => {
let idx:usize = *val as usize;
if idx<MAX_GENRE_VAL {
meta.genre=lofty::id3::v1::GENRES[idx].to_string();
}
},
Err(_) => { }
}
},
None => { }
if let Ok(file) = lofty::read_from_path(Path::new(track)) {
let tag = match file.primary_tag() {
Some(primary_tag) => primary_tag,
None => file.first_tag().expect("Error: No tags found!"),
};
meta.title = tag.title().unwrap_or_default().to_string();
meta.artist = tag.artist().unwrap_or_default().to_string();
meta.album = tag.album().unwrap_or_default().to_string();
meta.album_artist = tag.get_string(&ItemKey::AlbumArtist).unwrap_or_default().to_string();
meta.genre = tag.genre().unwrap_or_default().to_string();
// Check whether MP3 has numeric genre, and if so covert to text
if file.file_type().eq(&lofty::FileType::Mpeg) {
match tag.genre() {
Some(genre) => {
let test = genre.parse::<u8>();
match test {
Ok(val) => {
let idx: usize = val as usize;
if idx < MAX_GENRE_VAL {
meta.genre = lofty::id3::v1::GENRES[idx].to_string();
}
}
Err(_) => {
// Check for "(number)text"
let re = Regex::new(r"^\([0-9]+\)").unwrap();
if re.is_match(&genre) {
match genre.find(")") {
Some(end) => {
let test = genre.to_string().substring(1, end).parse::<u8>();
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();
}
}
}
None => { }
}
},
None => {}
}
}
}
meta.duration=file.properties().duration().as_secs() as u32;
},
Err(_) => { }
}
None => { }
}
},
Err(_) => { }
}
meta.duration = file.properties().duration().as_secs() as u32;
}
meta
}
}
+41 -58
View File
@@ -1,7 +1,7 @@
/**
* Analyse music with Bliss
*
* Copyright (c) 2022 Craig Drummond <craig.p.drummond@gmail.com>
* Copyright (c) 2022-2023 Craig Drummond <craig.p.drummond@gmail.com>
* GPLv3 license.
*
**/
@@ -12,93 +12,76 @@ use std::process;
use substring::Substring;
use ureq;
fn fail(msg:&str) {
fn fail(msg: &str) {
log::error!("{}", msg);
process::exit(-1);
}
pub fn stop_mixer(lms:&String) {
pub fn stop_mixer(lms: &String) {
let stop_req = "{\"id\":1, \"method\":\"slim.request\",\"params\":[\"\",[\"blissmixer\",\"stop\"]]}";
log::info!("Asking plugin to stop mixer");
match ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).send_string(&stop_req) {
Ok(_) => { },
Err(e) => { log::error!("Failed to ask plugin to stop mixer. {}", e); }
let req = ureq::post(&format!("http://{}:9000/jsonrpc.js", lms)).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: &String) {
// 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;
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) {
Ok(resp) => {
match resp.into_string() {
Ok(text) => {
match text.find("\"port\":") {
Some(s) => {
let txt = text.to_string().substring(s+7, text.len()).to_string();
match txt.find("}") {
Some(e) => {
let p = txt.substring(0, e);
let test = p.parse::<u16>();
match test {
Ok(val) => {
port = val;
},
Err(_) => { fail("Could not parse resp (cast)"); }
}
},
None => { fail("Could not parse resp (closing)"); }
Ok(resp) => match resp.into_string() {
Ok(text) => match text.find("\"port\":") {
Some(s) => {
let txt = text.to_string().substring(s + 7, text.len()).to_string();
match txt.find("}") {
Some(e) => {
let p = txt.substring(0, e);
let test = p.parse::<u16>();
match test {
Ok(val) => { port = val; }
Err(_) => { fail("Could not parse resp (cast)"); }
}
},
None => { fail("Could not parse resp (no port)"); }
}
None => { fail("Could not parse resp (closing)"); }
}
},
Err(_) => { fail("No text?")}
}
None => { fail("Could not parse resp (no port)"); }
}
},
Err(e) => {
fail(&format!("Failed to ask LMS plugin to allow upload. {}", e));
Err(_) => fail("No text?"),
}
Err(e) => { fail(&format!("Failed to ask LMS plugin to allow upload. {}", e)); }
}
if port<=0 {
if port == 0 {
fail("Invalid port");
}
// Now we have port number, do the actual upload...
log::info!("Uploading {}", db_path);
match File::open(db_path) {
Ok(file) => {
match file.metadata() {
Ok(meta) => {
let buffered_reader = BufReader::new(file);
log::info!("Length: {}", meta.len());
match ureq::put(&format!("http://{}:{}/upload", lms, 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);
},
Err(e) => {
fail(&format!("Failed to upload database. {}", e));
}
Ok(file) => match file.metadata() {
Ok(meta) => {
let buffered_reader = BufReader::new(file);
log::info!("Length: {}", meta.len());
match ureq::put(&format!("http://{}:{}/upload", lms, 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);
}
},
Err(e) => {
fail(&format!("Failed to open database. {}", e));
Err(e) => { fail(&format!("Failed to upload database. {}", e)); }
}
}
},
Err(e) => {
fail(&format!("Failed to open database. {}", e));
Err(e) => { fail(&format!("Failed to open database. {}", e)); }
}
Err(e) => { fail(&format!("Failed to open database. {}", e)); }
}
}
}