diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 90b64ce..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Rust - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build-test-lint-linux: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - submodules: recursive - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly-2023-02-16 - override: false - - 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 - - name: Check format - run: cargo fmt -- --check - - name: Lint - run: cargo clippy --examples --features=serde,library -- -D warnings - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose - - name: Run library tests - run: cargo test --verbose --features=library - - name: Run example tests - run: cargo test --verbose --examples - - name: Build benches - run: cargo +nightly-2023-02-16 bench --verbose --features=bench --no-run - - name: Build examples - run: cargo build --examples --verbose --features=serde,library - - build-test-lint-windows: - name: Windows - build, test and lint - runs-on: windows-latest - strategy: - matrix: - include: - - ffmpeg_version: latest - ffmpeg_download_url: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full-shared.7z - fail-fast: false - env: - FFMPEG_DOWNLOAD_URL: ${{ matrix.ffmpeg_download_url }} - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: | - $VCINSTALLDIR = $(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath) - Add-Content $env:GITHUB_ENV "LIBCLANG_PATH=${VCINSTALLDIR}\VC\Tools\LLVM\x64\bin`n" - Invoke-WebRequest "${env:FFMPEG_DOWNLOAD_URL}" -OutFile ffmpeg-release-full-shared.7z - 7z x ffmpeg-release-full-shared.7z - mkdir ffmpeg - mv ffmpeg-*/* ffmpeg/ - Add-Content $env:GITHUB_ENV "FFMPEG_DIR=${pwd}\ffmpeg`n" - Add-Content $env:GITHUB_PATH "${pwd}\ffmpeg\bin`n" - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - components: rustfmt, clippy - - name: Lint - run: cargo clippy --examples --features=serde -- -D warnings - - name: Check format - run: cargo fmt -- --check - - name: Build - run: cargo build --examples - - name: Test - run: cargo test --examples --features=serde diff --git a/.gitignore b/.gitignore index eb5a316..de21e51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ target +node_modules +index.node +index-*.node +bliss-rs-bliss-rs-*.tgz diff --git a/Cargo.lock b/Cargo.lock index 52f4773..0bd5095 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,61 +2,28 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler32" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" -dependencies = [ - "memchr 0.1.11", -] - [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ - "memchr 2.6.4", + "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -73,7 +40,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -88,20 +55,18 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cexpr", "clang-sys", - "lazy_static 1.4.0", + "lazy_static", "lazycell", - "log", "peeking_take_while", "proc-macro2", "quote", - "regex 1.10.2", + "regex", "rustc-hash", "shlex", "syn 1.0.109", - "which", ] [[package]] @@ -110,44 +75,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "bliss-audio" -version = "0.6.11" -dependencies = [ - "adler32", - "anyhow", - "bliss-audio-aubio-rs", - "clap", - "dirs", - "ffmpeg-next", - "ffmpeg-sys-next", - "glob", - "indicatif", - "log", - "mime_guess", - "ndarray", - "ndarray-npy", - "ndarray-stats", - "noisy_float", - "pretty_assertions", - "rcue", - "rusqlite", - "rustfft", - "serde", - "serde_ini", - "serde_json", - "strum", - "strum_macros", - "tempdir", - "thiserror", -] - [[package]] name = "bliss-audio-aubio-rs" version = "0.2.1" @@ -163,9 +90,35 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd4d47e7b82164c30a806717cf5562f87e5b136b79b3d942c9ad789134116f2f" dependencies = [ - "bindgen", "cc", - "fftw-sys", +] + +[[package]] +name = "bliss-rs" +version = "0.6.11" +dependencies = [ + "adler32", + "anyhow", + "bliss-audio-aubio-rs", + "clap", + "crossbeam", + "ffmpeg-next", + "glob", + "log", + "mime_guess", + "ndarray", + "ndarray-npy", + "ndarray-stats", + "neon", + "noisy_float", + "pretty_assertions", + "rustfft", + "serde", + "serde_ini", + "serde_json", + "strum", + "strum_macros", + "thiserror", ] [[package]] @@ -183,27 +136,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "cc" version = "1.0.83" @@ -229,25 +161,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" -dependencies = [ - "num", - "time", -] - [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.1", ] [[package]] @@ -258,26 +180,13 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags 1.3.2", + "bitflags", "strsim", "textwrap", "unicode-width", "vec_map", ] -[[package]] -name = "console" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" -dependencies = [ - "encode_unicode", - "lazy_static 1.4.0", - "libc", - "unicode-width", - "windows-sys 0.45.0", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -288,12 +197,25 @@ dependencies = [ ] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "crossbeam" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ - "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", ] [[package]] @@ -318,6 +240,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.18" @@ -353,68 +284,19 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "ffmpeg-next" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e72c72e8dcf638fb0fb03f033a954691662b5dabeaa3f85a6607d101569fccd" dependencies = [ - "bitflags 1.3.2", + "bitflags", "ffmpeg-sys-next", "libc", ] @@ -433,63 +315,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "fftw-src" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08962470ab0e91e74ec7d338c8731476c28ed4e503a3080b0f001692e395a7c" -dependencies = [ - "anyhow", - "cc", - "fs_extra", - "ftp", - "zip", -] - -[[package]] -name = "fftw-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e3951d695cc2f17610cd041e87ebc15078d1af5eb8c6be77921381fc98b3fd" -dependencies = [ - "fftw-src", - "libc", - "num-complex 0.3.1", -] - -[[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "ftp" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542951aad0071952c27409e3bd7cb62d1a3ad419c4e7314106bf994e0083ad5d" -dependencies = [ - "chrono", - "lazy_static 0.1.16", - "regex 0.1.80", -] - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "generic-array" version = "0.14.7" @@ -508,7 +333,7 @@ checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -523,25 +348,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.3", -] - [[package]] name = "heck" version = "0.4.1" @@ -563,15 +369,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -579,29 +376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indicatif" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", + "hashbrown", ] [[package]] @@ -621,29 +396,13 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "lazy_static" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" - [[package]] name = "lazy_static" version = "1.4.0" @@ -669,36 +428,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "libredox" -version = "0.0.1" +name = "libloading" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ - "bitflags 2.4.1", - "libc", - "redox_syscall", + "cfg-if", + "windows-sys", ] -[[package]] -name = "libsqlite3-sys" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" - [[package]] name = "log" version = "0.4.20" @@ -715,15 +457,6 @@ dependencies = [ "rawpointer", ] -[[package]] -name = "memchr" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.6.4" @@ -752,15 +485,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - [[package]] name = "ndarray" version = "0.15.6" @@ -768,7 +492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" dependencies = [ "matrixmultiply", - "num-complex 0.4.4", + "num-complex", "num-integer", "num-traits", "rawpointer", @@ -799,7 +523,32 @@ dependencies = [ "noisy_float", "num-integer", "num-traits", - "rand 0.8.5", + "rand", +] + +[[package]] +name = "neon" +version = "1.0.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8687031acf51f8b065aaf906b5a694f8d6b547c5c9430b6d636ab42422bfd0cc" +dependencies = [ + "libloading 0.7.4", + "neon-macros", + "once_cell", + "semver", + "send_wrapper", + "smallvec", +] + +[[package]] +name = "neon-macros" +version = "1.0.0-alpha.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "facd664405d5140677a63d7b7c5762425dd9d4179e07d2e67da29089c50b1ddd" +dependencies = [ + "quote", + "syn 1.0.109", + "syn-mid", ] [[package]] @@ -817,21 +566,10 @@ version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "memchr 2.6.4", + "memchr", "minimal-lexical", ] -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-integer", - "num-iter", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.4" @@ -843,15 +581,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" -dependencies = [ - "num-traits", -] - [[package]] name = "num-complex" version = "0.4.4" @@ -871,17 +600,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.17" @@ -901,24 +619,12 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "peeking_take_while" version = "0.1.2" @@ -931,7 +637,7 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ - "memchr 2.6.4", + "memchr", "thiserror", "ucd-trie", ] @@ -976,12 +682,6 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" -[[package]] -name = "portable-atomic" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1023,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1" dependencies = [ "num-bigint", - "num-complex 0.4.4", + "num-complex", "num-traits", "pest", "pest_derive", @@ -1038,19 +738,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.9", -] - [[package]] name = "rand" version = "0.8.5" @@ -1059,7 +746,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1069,24 +756,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -1122,118 +794,41 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rcue" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca1481d62f18158646de2ec552dd63f8bdc5be6448389b192ba95c939df997e" - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" -version = "0.1.80" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ - "aho-corasick 0.5.3", - "memchr 0.1.11", - "regex-syntax 0.3.9", - "thread_local", - "utf8-ranges", -] - -[[package]] -name = "regex" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick 1.1.2", - "memchr 2.6.4", + "aho-corasick", + "memchr", "regex-automata", - "regex-syntax 0.8.2", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ - "aho-corasick 1.1.2", - "memchr 2.6.4", - "regex-syntax 0.8.2", + "aho-corasick", + "memchr", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" - [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "result" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560" -[[package]] -name = "rusqlite" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" -dependencies = [ - "bitflags 1.3.2", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", -] - [[package]] name = "rustc-hash" version = "1.1.0" @@ -1246,7 +841,7 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" dependencies = [ - "num-complex 0.4.4", + "num-complex", "num-integer", "num-traits", "primal-check", @@ -1255,19 +850,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "rustix" -version = "0.38.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" -dependencies = [ - "bitflags 2.4.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustversion" version = "1.0.14" @@ -1280,6 +862,18 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.193" @@ -1335,9 +929,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" @@ -1399,13 +993,14 @@ dependencies = [ ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "syn-mid" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" dependencies = [ - "rand 0.4.6", - "remove_dir_all", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -1437,36 +1032,6 @@ dependencies = [ "syn 2.0.42", ] -[[package]] -name = "thread-id" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" -dependencies = [ - "kernel32-sys", - "libc", -] - -[[package]] -name = "thread_local" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" -dependencies = [ - "thread-id", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi 0.3.9", -] - [[package]] name = "transpose" version = "0.2.2" @@ -1510,12 +1075,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "utf8-ranges" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" - [[package]] name = "vcpkg" version = "0.2.15" @@ -1540,36 +1099,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -1580,12 +1115,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1598,46 +1127,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] @@ -1646,192 +1142,59 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "zerocopy" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.42", -] - -[[package]] -name = "zip" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" -dependencies = [ - "byteorder", - "bzip2", - "crc32fast", - "flate2", - "thiserror", - "time", -] diff --git a/Cargo.toml b/Cargo.toml index 401e17a..4199585 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bliss-audio" +name = "bliss-rs" version = "0.6.11" build = "build.rs" authors = ["Polochon-street "] @@ -10,38 +10,21 @@ homepage = "https://lelele.io/bliss.html" repository = "https://github.com/Polochon-street/bliss-rs" keywords = ["audio", "analysis", "MIR", "playlist", "similarity"] readme = "README.md" -exclude = ["data/"] +exclude = ["data/", "index.node"] + +[lib] +crate-type = ["rlib", "cdylib"] [package.metadata.docs.rs] features = ["bliss-audio-aubio-rs/rustdoc", "library"] no-default-features = true -[features] -default = ["bliss-audio-aubio-rs/static"] -# Build ffmpeg instead of using the host's. -build-ffmpeg = ["ffmpeg-next/build"] -ffmpeg-static = ["ffmpeg-next/static"] -# Build for raspberry pis -rpi = ["ffmpeg-next/rpi"] -# Use if you get "No prebuilt bindings. Try use `bindgen` feature" -update-aubio-bindings = ["bliss-audio-aubio-rs/bindgen"] -# Use if you want to build python bindings with maturin. -python-bindings = ["bliss-audio-aubio-rs/fftw3"] -# Enable the benchmarks with `cargo +nightly bench --features=bench` -bench = [] -library = [ - "serde", "dep:rusqlite", "dep:dirs", "dep:tempdir", - "dep:anyhow", "dep:serde_ini", "dep:serde_json", - "dep:indicatif", -] -serde = ["dep:serde"] - [dependencies] # Until https://github.com/aubio/aubio/issues/336 is somehow solved # Hopefully we'll be able to use the official aubio-rs at some point. -bliss-audio-aubio-rs = "0.2.1" -ffmpeg-next = "6.1.1" -ffmpeg-sys-next = { version = "6.1.0", default-features = false } +bliss-audio-aubio-rs = { version = "0.2.1", features = ["static"] } +crossbeam = "0.8.2" +ffmpeg-next = { version = "6.1.1", features = ["static"] } log = "0.4.17" # `rayon` is used only by `par_mapv_inplace` in chroma.rs. # TODO: is the speed gain that substantial? @@ -53,17 +36,16 @@ rustfft = "6.1.0" thiserror = "1.0.40" strum = "0.24.1" strum_macros = "0.24.3" -rcue = "0.1.3" # Deps for the library feature serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "1.0.59", optional = true } serde_ini = { version = "0.2.0", optional = true } -tempdir = { version = "0.3.7", optional = true } -rusqlite = { version = "0.28.0", optional = true } -dirs = { version = "5.0.0", optional = true } -anyhow = { version = "1.0.58", optional = true } -indicatif = { version = "0.17.0", optional = true } + +[dependencies.neon] +version = "1.0.0-alpha.4" +default-features = false +features = ["napi-6", "channel-api", "promise-api", "try-catch-api"] [dev-dependencies] ndarray-npy = { version = "0.8.1", default-features = false } @@ -73,16 +55,3 @@ anyhow = "1.0.45" clap = "2.33.3" pretty_assertions = "1.3.0" serde_json = "1.0.59" - -[[example]] -name = "library" -required-features = ["library"] - -[[example]] -name = "library_extra_info" -required-features = ["library"] - - -[[example]] -name = "playlist" -required-features = ["serde"] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ca7aa6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-slim + +RUN apt-get update +RUN apt-get install -yqq gnupg dirmngr apt-transport-https software-properties-common + +RUN gpg -K && gpg --no-default-keyring \ + --keyring /usr/share/keyrings/deb-multimedia.gpg \ + --keyserver keyserver.ubuntu.com \ + --recv-keys 5C808C2B65558117 +RUN echo "deb [signed-by=/usr/share/keyrings/deb-multimedia.gpg] https://www.deb-multimedia.org $(lsb_release -sc) main non-free" \ + | tee /etc/apt/sources.list.d/deb-multimedia.list + +RUN apt-get update +RUN apt-get install -yqq wget build-essential yasm libavutil-dev libavcodec-dev libavformat-dev libavfilter-dev libavfilter-dev libavdevice-dev libswresample-dev libfftw3-dev libclang-dev ffmpeg + +WORKDIR /opt/rust +RUN wget https://sh.rustup.rs -O rustup-init.sh +RUN chmod +x rustup-init.sh + +RUN ./rustup-init.sh -y -t x86_64-unknown-linux-gnu x86_64-unknown-linux-musl aarch64-unknown-linux-gnu aarch64-unknown-linux-musl diff --git a/README.md b/README.md index 776c4e3..bfd5210 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,26 @@ [![build](https://github.com/Polochon-street/bliss-rs/workflows/Rust/badge.svg)](https://github.com/Polochon-street/bliss-rs/actions) [![doc](https://docs.rs/bliss-audio/badge.svg)](https://docs.rs/bliss-audio/) -# bliss music analyzer - Rust version +# Fork notice +This repo is a fork of [bliss-rs](https://github.com/Polochon-street/bliss-rs) with bindings for Node.js (using N-API and Neon). + +## Example usage: +The package is published to the Gitea registry: https://gitea.antonlyap.pp.ua/antonlyap/-/packages/npm/@bliss-rs%2Fbliss-rs/1.0.0 +```ts +import { analyze, analyzeSync } from '@bliss-rs/bliss-rs'; + +await analyze("/path/to/track.mp3") // returns Uint8Array +``` + +## Return value +The output of `bliss-rs` consists of single-precision floats, currently 20 of them. This fork contains code to convert it into an array of 80 bytes in little endian order. An additional version (also comes from `bliss-rs`, currently equal to `1`) is prepended at the start (16-bit unsigned little-endian integer). Therefore, the total output size is 82 bytes. + +### Usage +The output (without the version) is meant to be converted back into floats and used to calculate the [Euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions) between two songs. Other distance algorithms are being worked on by the Bliss team. + +--- + +# (Original README) bliss music analyzer - Rust version bliss-rs is the Rust improvement of [bliss](https://github.com/Polochon-street/bliss), a library used to make playlists by analyzing songs, and computing distance between them. diff --git a/examples/analyze.rs b/examples/analyze.rs index 2ffab60..49384f1 100644 --- a/examples/analyze.rs +++ b/examples/analyze.rs @@ -1,4 +1,4 @@ -use bliss_audio::Song; +use bliss_rs::bliss_lib::Song; use std::env; /** diff --git a/examples/distance.rs b/examples/distance.rs index 0fa2ac8..2db4d2d 100644 --- a/examples/distance.rs +++ b/examples/distance.rs @@ -1,4 +1,4 @@ -use bliss_audio::Song; +use bliss_rs::bliss_lib::Song; use std::env; /** @@ -13,14 +13,21 @@ fn main() -> Result<(), String> { let first_path = paths.next().ok_or("Help: ./distance ")?; let second_path = paths.next().ok_or("Help: ./distance ")?; - let song1 = Song::from_path(first_path).map_err(|x| x.to_string())?; - let song2 = Song::from_path(second_path).map_err(|x| x.to_string())?; + let song1 = Song::from_path(&first_path).map_err(|x| x.to_string())?; + let song2 = Song::from_path(&second_path).map_err(|x| x.to_string())?; + + let mut distance_squared: f64 = 0.0; + let analysis1 = song1.analysis.as_bytes(); + let analysis2 = song2.analysis.as_bytes(); + for (i, feature1) in analysis1.iter().enumerate() { + distance_squared += (feature1 - analysis2[i]).pow(2) as f64; + } println!( "d({:?}, {:?}) = {}", - song1.path, - song2.path, - song1.distance(&song2) + &first_path, + &second_path, + distance_squared.sqrt(), ); Ok(()) } diff --git a/examples/library.rs b/examples/library.rs deleted file mode 100644 index f6e61d7..0000000 --- a/examples/library.rs +++ /dev/null @@ -1,204 +0,0 @@ -/// Basic example of how one would combine bliss with an "audio player", -/// through [Library]. -/// -/// For simplicity's sake, this example recursively gets songs from a folder -/// to emulate an audio player library, without handling CUE files. -use anyhow::Result; -use bliss_audio::library::{AppConfigTrait, BaseConfig, Library}; -use clap::{App, Arg, SubCommand}; -use glob::glob; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; - -#[derive(Serialize, Deserialize, Clone, Debug)] -// A config structure, that will be serialized as a -// JSON file upon Library creation. -pub struct Config { - #[serde(flatten)] - // The base configuration, containing both the config file - // path, as well as the database path. - pub base_config: BaseConfig, - // An extra field, to store the music library path. Any number - // of arbitrary fields (even Serializable structures) can - // of course be added. - pub music_library_path: PathBuf, -} - -impl Config { - pub fn new( - music_library_path: PathBuf, - config_path: Option, - database_path: Option, - number_cores: Option, - ) -> Result { - let base_config = BaseConfig::new(config_path, database_path, number_cores)?; - Ok(Self { - base_config, - music_library_path, - }) - } -} - -// The AppConfigTrait must know how to access the base config. -impl AppConfigTrait for Config { - fn base_config(&self) -> &BaseConfig { - &self.base_config - } - - fn base_config_mut(&mut self) -> &mut BaseConfig { - &mut self.base_config - } -} - -// A trait allowing to implement methods for the Library, -// useful if you don't need to store extra information in fields. -// Otherwise, doing -// ``` -// struct CustomLibrary { -// library: Library, -// extra_field: ..., -// } -// ``` -// and implementing functions for that struct would be the way to go. -// That's what the [reference](https://github.com/Polochon-street/blissify-rs) -// implementation does. -trait CustomLibrary { - fn song_paths(&self) -> Result>; -} - -impl CustomLibrary for Library { - /// Get all songs in the player library - fn song_paths(&self) -> Result> { - let music_path = &self.config.music_library_path; - let pattern = Path::new(&music_path).join("**").join("*"); - - Ok(glob(&pattern.to_string_lossy())? - .map(|e| fs::canonicalize(e.unwrap()).unwrap()) - .filter(|e| match mime_guess::from_path(e).first() { - Some(m) => m.type_() == "audio", - None => false, - }) - .map(|x| x.to_string_lossy().to_string()) - .collect::>()) - } -} - -// A simple example of what a CLI-app would look. -// -// Note that `Library::new` is used only on init, and subsequent -// commands use `Library::from_path`. -fn main() -> Result<()> { - let matches = App::new("library-example") - .version(env!("CARGO_PKG_VERSION")) - .author("Polochon_street") - .about("Example binary implementing bliss for an audio player.") - .subcommand( - SubCommand::with_name("init") - .about( - "Initialize a Library, both storing the config and analyzing folders - containing songs.", - ) - .arg( - Arg::with_name("FOLDER") - .help("A folder containing the music library to analyze.") - .required(true), - ) - .arg( - Arg::with_name("database-path") - .short("d") - .long("database-path") - .help( - "Optional path where to store the database file containing - the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.", - ) - .takes_value(true), - ) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to store the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("update") - .about( - "Update a Library's songs, trying to analyze failed songs, - as well as songs not in the library.", - ) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to load the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("playlist") - .about( - "Make a playlist, starting with the song at SONG_PATH, returning - the songs' paths.", - ) - .arg(Arg::with_name("SONG_PATH").takes_value(true)) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to load the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ) - .arg( - Arg::with_name("playlist-length") - .short("l") - .long("playlist-length") - .help("Optional playlist length. Defaults to 20.") - .takes_value(true), - ), - ) - .get_matches(); - if let Some(sub_m) = matches.subcommand_matches("init") { - let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap()); - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let database_path = sub_m.value_of("database-path").map(PathBuf::from); - - let config = Config::new(folder, config_path, database_path, None)?; - let mut library = Library::new(config)?; - - library.analyze_paths(library.song_paths()?, true)?; - } else if let Some(sub_m) = matches.subcommand_matches("update") { - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let mut library: Library = Library::from_config_path(config_path)?; - library.update_library(library.song_paths()?, true, true)?; - } else if let Some(sub_m) = matches.subcommand_matches("playlist") { - let song_path = sub_m.value_of("SONG_PATH").unwrap(); - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let playlist_length = sub_m - .value_of("playlist-length") - .unwrap_or("20") - .parse::()?; - let library: Library = Library::from_config_path(config_path)?; - let songs = library.playlist_from::<()>(song_path, playlist_length)?; - let song_paths = songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(); - for song in song_paths { - println!("{song:?}"); - } - } - - Ok(()) -} diff --git a/examples/library_extra_info.rs b/examples/library_extra_info.rs deleted file mode 100644 index ec84cfd..0000000 --- a/examples/library_extra_info.rs +++ /dev/null @@ -1,227 +0,0 @@ -/// Basic example of how one would combine bliss with an "audio player", -/// through [Library], showing how to put extra info in the database for -/// each song. -/// -/// For simplicity's sake, this example recursively gets songs from a folder -/// to emulate an audio player library, without handling CUE files. -use anyhow::Result; -use bliss_audio::library::{AppConfigTrait, BaseConfig, Library}; -use clap::{App, Arg, SubCommand}; -use glob::glob; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; - -#[derive(Serialize, Deserialize, Clone, Debug)] -/// A config structure, that will be serialized as a -/// JSON file upon Library creation. -pub struct Config { - #[serde(flatten)] - /// The base configuration, containing both the config file - /// path, as well as the database path. - pub base_config: BaseConfig, - /// An extra field, to store the music library path. Any number - /// of arbitrary fields (even Serializable structures) can - /// of course be added. - pub music_library_path: PathBuf, -} - -impl Config { - pub fn new( - music_library_path: PathBuf, - config_path: Option, - database_path: Option, - number_cores: Option, - ) -> Result { - let base_config = BaseConfig::new(config_path, database_path, number_cores)?; - Ok(Self { - base_config, - music_library_path, - }) - } -} - -// The AppConfigTrait must know how to access the base config. -impl AppConfigTrait for Config { - fn base_config(&self) -> &BaseConfig { - &self.base_config - } - - fn base_config_mut(&mut self) -> &mut BaseConfig { - &mut self.base_config - } -} - -// A trait allowing to implement methods for the Library, -// useful if you don't need to store extra information in fields. -// Otherwise, doing -// ``` -// struct CustomLibrary { -// library: Library, -// extra_field: ..., -// } -// ``` -// and implementing functions for that struct would be the way to go. -// That's what the [reference](https://github.com/Polochon-street/blissify-rs) -// implementation does. -trait CustomLibrary { - fn song_paths_info(&self) -> Result>; -} - -impl CustomLibrary for Library { - /// Get all songs in the player library, along with the extra info - /// one would want to store along with each song. - fn song_paths_info(&self) -> Result> { - let music_path = &self.config.music_library_path; - let pattern = Path::new(&music_path).join("**").join("*"); - - Ok(glob(&pattern.to_string_lossy())? - .map(|e| fs::canonicalize(e.unwrap()).unwrap()) - .filter_map(|e| { - mime_guess::from_path(&e).first().map(|m| { - ( - e.to_string_lossy().to_string(), - ExtraInfo { - extension: e.extension().map(|e| e.to_string_lossy().to_string()), - file_name: e.file_name().map(|e| e.to_string_lossy().to_string()), - mime_type: format!("{}/{}", m.type_(), m.subtype()), - }, - ) - }) - }) - .collect::>()) - } -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)] -// An (somewhat simple) example of what extra metadata one would put, along -// with song analysis data. -struct ExtraInfo { - extension: Option, - file_name: Option, - mime_type: String, -} - -// A simple example of what a CLI-app would look. -// -// Note that `Library::new` is used only on init, and subsequent -// commands use `Library::from_path`. -fn main() -> Result<()> { - let matches = App::new("library-example") - .version(env!("CARGO_PKG_VERSION")) - .author("Polochon_street") - .about("Example binary implementing bliss for an audio player.") - .subcommand( - SubCommand::with_name("init") - .about( - "Initialize a Library, both storing the config and analyzing folders - containing songs.", - ) - .arg( - Arg::with_name("FOLDER") - .help("A folder containing the music library to analyze.") - .required(true), - ) - .arg( - Arg::with_name("database-path") - .short("d") - .long("database-path") - .help( - "Optional path where to store the database file containing - the songs' analysis. Defaults to XDG_DATA_HOME/bliss-rs/bliss.db.", - ) - .takes_value(true), - ) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to store the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("update") - .about( - "Update a Library's songs, trying to analyze failed songs, - as well as songs not in the library.", - ) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to load the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("playlist") - .about( - "Make a playlist, starting with the song at SONG_PATH, returning - the songs' paths.", - ) - .arg(Arg::with_name("SONG_PATH").takes_value(true)) - .arg( - Arg::with_name("config-path") - .short("c") - .long("config-path") - .help( - "Optional path where to load the config file containing - the library setup. Defaults to XDG_DATA_HOME/bliss-rs/config.json.", - ) - .takes_value(true), - ) - .arg( - Arg::with_name("playlist-length") - .short("l") - .long("playlist-length") - .help("Optional playlist length. Defaults to 20.") - .takes_value(true), - ), - ) - .get_matches(); - if let Some(sub_m) = matches.subcommand_matches("init") { - let folder = PathBuf::from(sub_m.value_of("FOLDER").unwrap()); - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let database_path = sub_m.value_of("database-path").map(PathBuf::from); - - let config = Config::new(folder, config_path, database_path, None)?; - let mut library = Library::new(config)?; - - library.analyze_paths_extra_info(library.song_paths_info()?, true)?; - } else if let Some(sub_m) = matches.subcommand_matches("update") { - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let mut library: Library = Library::from_config_path(config_path)?; - library.update_library_extra_info(library.song_paths_info()?, true, true)?; - } else if let Some(sub_m) = matches.subcommand_matches("playlist") { - let song_path = sub_m.value_of("SONG_PATH").unwrap(); - let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let playlist_length = sub_m - .value_of("playlist-length") - .unwrap_or("20") - .parse::()?; - let library: Library = Library::from_config_path(config_path)?; - let songs = library.playlist_from::(song_path, playlist_length)?; - let playlist = songs - .into_iter() - .map(|s| { - ( - s.bliss_song.path.to_string_lossy().to_string(), - s.extra_info.mime_type, - ) - }) - .collect::>(); - for (path, mime_type) in playlist { - println!("{path} <{mime_type}>"); - } - } - - Ok(()) -} diff --git a/examples/playlist.rs b/examples/playlist.rs deleted file mode 100644 index 86e118f..0000000 --- a/examples/playlist.rs +++ /dev/null @@ -1,95 +0,0 @@ -use anyhow::Result; -use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance}; -use bliss_audio::{analyze_paths, Song}; -use clap::{App, Arg}; -use glob::glob; -use std::env; -use std::fs; -use std::io::BufReader; -use std::path::{Path, PathBuf}; - -/* Analyzes a folder recursively, and make a playlist out of the file - * provided by the user. */ -// How to use: ./playlist [-o file.m3u] [-a analysis.json] -fn main() -> Result<()> { - let matches = App::new("playlist") - .version(env!("CARGO_PKG_VERSION")) - .author("Polochon_street") - .about("Analyze a folder and make a playlist from a target song") - .arg(Arg::with_name("output-playlist").short("o").long("output-playlist") - .value_name("PLAYLIST.M3U") - .help("Outputs the playlist to a file.") - .takes_value(true)) - .arg(Arg::with_name("analysis-file").short("a").long("analysis-file") - .value_name("ANALYSIS.JSON") - .help("Use the songs that have been analyzed in , and appends newly analyzed songs to it. Defaults to /tmp/analysis.json.") - .takes_value(true)) - .arg(Arg::with_name("FOLDER").help("Folders containing some songs.").required(true)) - .arg(Arg::with_name("FIRST-SONG").help("Song to start from (can be outside of FOLDER).").required(true)) - .get_matches(); - - let folder = matches.value_of("FOLDER").unwrap(); - let file = fs::canonicalize(matches.value_of("FIRST-SONG").unwrap())?; - let pattern = Path::new(folder).join("**").join("*"); - - let mut songs: Vec = Vec::new(); - let analysis_path = matches - .value_of("analysis-file") - .unwrap_or("/tmp/analysis.json"); - let analysis_file = fs::File::open(analysis_path); - if let Ok(f) = analysis_file { - let reader = BufReader::new(f); - songs = serde_json::from_reader(reader)?; - } - - let analyzed_paths = songs - .iter() - .map(|s| s.path.to_owned()) - .collect::>(); - - let paths = glob(&pattern.to_string_lossy())? - .map(|e| fs::canonicalize(e.unwrap()).unwrap()) - .filter(|e| match mime_guess::from_path(e).first() { - Some(m) => m.type_() == "audio", - None => false, - }) - .map(|x| x.to_string_lossy().to_string()) - .collect::>(); - - let song_iterator = analyze_paths( - paths - .iter() - .filter(|p| !analyzed_paths.contains(&PathBuf::from(p))) - .map(|p| p.to_owned()) - .collect::>(), - ); - let first_song = Song::from_path(file)?; - let mut analyzed_songs = vec![first_song.to_owned()]; - for (path, result) in song_iterator { - match result { - Ok(song) => analyzed_songs.push(song), - Err(e) => println!("error analyzing {}: {}", path.display(), e), - }; - } - analyzed_songs.extend_from_slice(&songs); - let serialized = serde_json::to_string(&analyzed_songs).unwrap(); - let mut songs_to_chose_from: Vec<_> = analyzed_songs - .into_iter() - .filter(|x| x == &first_song || paths.contains(&x.path.to_string_lossy().to_string())) - .collect(); - closest_to_first_song(&first_song, &mut songs_to_chose_from, euclidean_distance); - dedup_playlist(&mut songs_to_chose_from, None); - - fs::write(analysis_path, serialized)?; - let playlist = songs_to_chose_from - .iter() - .map(|s| s.path.to_string_lossy().to_string()) - .collect::>() - .join("\n"); - if let Some(m) = matches.value_of("output-playlist") { - fs::write(m, playlist)?; - } else { - println!("{playlist}"); - } - Ok(()) -} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..fa01684 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,2 @@ +export function analyzeSync(path: string): Uint8Array; +export function analyze(path: string): Promise; diff --git a/index.js b/index.js new file mode 100644 index 0000000..05797ea --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +try { + module.exports = require('./index.node'); +} catch { + const isLinux = process.platform === 'linux'; + + if (isLinux && process.arch === 'x64') { + module.exports = require('./index-x86_64-unknown-linux-gnu.node'); + } else if (isLinux && process.arch === 'arm64') { + module.exports = require('./index-aarch64-unknown-linux-gnu.node'); + } else { + throw new Error('Bliss: unsupported architecture'); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9763328 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "bliss-rs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bliss-rs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cargo-cp-artifact": "^0.1.8" + }, + "devDependencies": { + "@types/node": "^20.10.5" + } + }, + "node_modules/@types/node": { + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.8.tgz", + "integrity": "sha512-3j4DaoTrsCD1MRkTF2Soacii0Nx7UHCce0EwUf4fHnggwiE4fbmF2AbnfzayR36DF8KGadfh7M/Yfy625kgPlA==", + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..128d7ff --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@bliss-rs/bliss-rs", + "version": "0.0.4", + "description": "A fork of the bliss-rs library with Node.js bindings", + "main": "index.js", + "types": "index.d.ts", + "directories": { + "example": "examples" + }, + "files": ["index.js", "index.d.ts", "index-*.node"], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics" + }, + "author": "antonlyap", + "license": "GPL", + "dependencies": { + "cargo-cp-artifact": "^0.1.8" + }, + "devDependencies": { + "@types/node": "^20.10.5" + } +} diff --git a/src/bliss_lib.rs b/src/bliss_lib.rs new file mode 100644 index 0000000..2aea534 --- /dev/null +++ b/src/bliss_lib.rs @@ -0,0 +1,82 @@ +//! # bliss audio library +//! +//! bliss is a library for making "smart" audio playlists. +//! +//! The core of the library is the [Song] object, which relates to a +//! specific analyzed song and contains its path, title, analysis, and +//! other metadata fields (album, genre...). +//! Analyzing a song is as simple as running `Song::from_path("/path/to/song")`. +//! +//! The [analysis](Song::analysis) field of each song is an array of f32, which +//! makes the comparison between songs easy, by just using e.g. euclidean +//! distance (see [distance](Song::distance) for instance). +//! +//! Once several songs have been analyzed, making a playlist from one Song +//! is as easy as computing distances between that song and the rest, and ordering +//! the songs by distance, ascending. +//! +//! # Examples +//! +//! ### Analyze & compute the distance between two songs +//! ```no_run +//! use bliss_audio::{BlissResult, Song}; +//! +//! fn main() -> BlissResult<()> { +//! let song1 = Song::from_path("/path/to/song1")?; +//! let song2 = Song::from_path("/path/to/song2")?; +//! +//! println!("Distance between song1 and song2 is {}", song1.distance(&song2)); +//! Ok(()) +//! } +//! ``` +#![cfg_attr(feature = "bench", feature(test))] +#![warn(missing_docs)] + +use thiserror::Error; + +pub use crate::song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; + +/// Target channels for ffmpeg +pub const CHANNELS: u16 = 1; + +/// Target sample rate for ffmpeg +pub const SAMPLE_RATE: u32 = 22050; +/// Stores the current version of bliss-rs' features. +/// It is bumped every time one or more feature is added, updated or removed, +/// so plug-ins can rescan libraries when there is a major change. +pub const FEATURES_VERSION: u16 = 1; + +#[derive(Error, Clone, Debug, PartialEq, Eq)] +/// Umbrella type for bliss error types +pub enum BlissError { + #[error("error happened while decoding file – {0}")] + /// An error happened while decoding an (audio) file. + DecodingError(String), + #[error("error happened while analyzing file – {0}")] + /// An error happened during the analysis of the song's samples by bliss. + AnalysisError(String), + #[error("error happened with the music library provider - {0}")] + /// An error happened with the music library provider. + /// Useful to report errors when you implement bliss for an audio player. + ProviderError(String), +} + +/// bliss error type +pub type BlissResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_send_song() { + fn assert_send() {} + assert_send::(); + } + + #[test] + fn test_sync_song() { + fn assert_sync() {} + assert_sync::(); + } +} diff --git a/src/chroma.rs b/src/chroma.rs index a8a3727..bfd3ed9 100644 --- a/src/chroma.rs +++ b/src/chroma.rs @@ -7,7 +7,7 @@ extern crate noisy_float; use crate::utils::stft; use crate::utils::{hz_to_octs_inplace, Normalize}; -use crate::{BlissError, BlissResult}; +use crate::bliss_lib::{BlissError, BlissResult}; use ndarray::{arr1, arr2, concatenate, s, Array, Array1, Array2, Axis, Zip}; use ndarray_stats::interpolate::Midpoint; use ndarray_stats::QuantileExt; @@ -365,7 +365,7 @@ fn chroma_stft( mod test { use super::*; use crate::utils::stft; - use crate::{Song, SAMPLE_RATE}; + use crate::bliss_lib::{Song, SAMPLE_RATE}; use ndarray::{arr1, arr2, Array2}; use ndarray_npy::ReadNpyExt; use std::fs::File; @@ -437,7 +437,7 @@ mod test { fn test_chroma_desc() { let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); - chroma_desc.do_(&song.sample_array).unwrap(); + chroma_desc.do_(&song).unwrap(); let expected_values = vec![ -0.35661936, -0.63578653, @@ -457,9 +457,7 @@ mod test { #[test] fn test_chroma_stft_decode() { - let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")) - .unwrap() - .sample_array; + let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut stft = stft(&signal, 8192, 2205); let file = File::open("data/chroma.npy").unwrap(); @@ -490,9 +488,7 @@ mod test { #[test] fn test_estimate_tuning_decode() { - let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")) - .unwrap() - .sample_array; + let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let stft = stft(&signal, 8192, 2205); let tuning = estimate_tuning(22050, &stft, 8192, 0.01, 12).unwrap(); diff --git a/src/cue.rs b/src/cue.rs deleted file mode 100644 index 390b140..0000000 --- a/src/cue.rs +++ /dev/null @@ -1,339 +0,0 @@ -//! CUE-handling module. -//! -//! Using [BlissCue::songs_from_path] is most likely what you want. - -use crate::{Analysis, BlissError, BlissResult, Song, FEATURES_VERSION, SAMPLE_RATE}; -use rcue::cue::{Cue, Track}; -use rcue::parser::parse_from_file; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Default, Debug, PartialEq, Eq, Clone)] -/// A struct populated when the corresponding [Song] has been extracted from an -/// audio file split with the help of a CUE sheet. -pub struct CueInfo { - /// The path of the original CUE sheet, e.g. `/path/to/album_name.cue`. - pub cue_path: PathBuf, - /// The path of the audio file the song was extracted from, e.g. - /// `/path/to/album_name.wav`. Used because one CUE sheet can refer to - /// several audio files. - pub audio_file_path: PathBuf, -} - -/// A struct to handle CUEs with bliss. -/// Use either [analyze_paths](crate::analyze_paths) with CUE files or -/// [songs_from_path](BlissCue::songs_from_path) to return a list of [Song]s -/// from CUE files. -pub struct BlissCue { - cue: Cue, - cue_path: PathBuf, -} - -#[allow(missing_docs)] -#[derive(Default, Debug, PartialEq, Clone)] -struct BlissCueFile { - sample_array: Vec, - album: Option, - artist: Option, - genre: Option, - tracks: Vec, - cue_path: PathBuf, - audio_file_path: PathBuf, -} - -impl BlissCue { - /// Analyze songs from a CUE file, extracting individual [Song] objects - /// for each individual song. - /// - /// Each returned [Song] has a populated [cue_info](Song::cue_info) object, that can be - /// be used to retrieve which CUE sheet was used to extract it, as well - /// as the corresponding audio file. - pub fn songs_from_path>(path: P) -> BlissResult>> { - let cue = BlissCue::from_path(&path)?; - let cue_files = cue.files(); - let mut songs = Vec::new(); - for cue_file in cue_files.into_iter() { - match cue_file { - Ok(f) => { - if !f.sample_array.is_empty() { - songs.extend_from_slice(&f.get_songs()); - } else { - songs.push(Err(BlissError::DecodingError( - "empty audio file associated to CUE sheet".into(), - ))); - } - } - Err(e) => songs.push(Err(e)), - } - } - Ok(songs) - } - - // Extract a BlissCue from a given path. - fn from_path>(path: P) -> BlissResult { - let cue = parse_from_file(&path.as_ref().to_string_lossy(), false).map_err(|e| { - BlissError::DecodingError(format!( - "when opening CUE file '{:?}': {:?}", - path.as_ref(), - e - )) - })?; - Ok(BlissCue { - cue, - cue_path: path.as_ref().to_owned(), - }) - } - - // List all BlissCueFile from a BlissCue. - fn files(&self) -> Vec> { - let mut cue_files = Vec::new(); - for cue_file in self.cue.files.iter() { - let audio_file_path = match &self.cue_path.parent() { - Some(parent) => parent.join(Path::new(&cue_file.file)), - None => PathBuf::from(cue_file.file.to_owned()), - }; - let genre = self - .cue - .comments - .iter() - .find(|(c, _)| c == "GENRE") - .map(|(_, v)| v.to_owned()); - let raw_song = Song::decode(Path::new(&audio_file_path)); - if let Ok(song) = raw_song { - let bliss_cue_file = BlissCueFile { - sample_array: song.sample_array, - genre, - artist: self.cue.performer.to_owned(), - album: self.cue.title.to_owned(), - tracks: cue_file.tracks.to_owned(), - audio_file_path, - cue_path: self.cue_path.to_owned(), - }; - cue_files.push(Ok(bliss_cue_file)) - } else { - cue_files.push(Err(raw_song.unwrap_err())); - } - } - cue_files - } -} - -impl BlissCueFile { - fn create_song( - &self, - analysis: BlissResult, - current_track: &Track, - duration: Duration, - index: usize, - ) -> BlissResult { - if let Ok(a) = analysis { - let song = Song { - path: PathBuf::from(format!( - "{}/CUE_TRACK{:03}", - self.cue_path.to_string_lossy(), - index, - )), - album: self.album.to_owned(), - artist: current_track.performer.to_owned(), - album_artist: self.artist.to_owned(), - analysis: a, - duration, - genre: self.genre.to_owned(), - title: current_track.title.to_owned(), - track_number: Some(current_track.no.to_owned()), - features_version: FEATURES_VERSION, - cue_info: Some(CueInfo { - cue_path: self.cue_path.to_owned(), - audio_file_path: self.audio_file_path.to_owned(), - }), - }; - Ok(song) - } else { - Err(analysis.unwrap_err()) - } - } - - // Get all songs from a BlissCueFile, using Song::analyze, each song being - // located using the sample_array and the timestamp delimiter. - fn get_songs(&self) -> Vec> { - let mut songs = Vec::new(); - for (index, tuple) in (self.tracks[..]).windows(2).enumerate() { - let (current_track, next_track) = (tuple[0].to_owned(), tuple[1].to_owned()); - if let Some((_, start_current)) = current_track.indices.first() { - if let Some((_, end_current)) = next_track.indices.first() { - let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; - let end_current = (end_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; - let duration = Duration::from_secs_f32( - (end_current - start_current) as f32 / SAMPLE_RATE as f32, - ); - let analysis = Song::analyze(&self.sample_array[start_current..end_current]); - - let song = self.create_song(analysis, ¤t_track, duration, index + 1); - songs.push(song); - } - } - } - // Take care of the last track, since the windows iterator doesn't. - if let Some(last_track) = self.tracks.last() { - if let Some((_, start_current)) = last_track.indices.first() { - let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; - let duration = Duration::from_secs_f32( - (self.sample_array.len() - start_current) as f32 / SAMPLE_RATE as f32, - ); - let analysis = Song::analyze(&self.sample_array[start_current..]); - let song = self.create_song(analysis, last_track, duration, self.tracks.len()); - songs.push(song); - } - } - songs - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_empty_cue() { - let songs = BlissCue::songs_from_path("data/empty.cue").unwrap(); - let error = songs[0].to_owned().unwrap_err(); - assert_eq!( - error, - BlissError::DecodingError("empty audio file associated to CUE sheet".to_string()) - ); - } - - #[test] - fn test_cue_analysis() { - let songs = BlissCue::songs_from_path("data/testcue.cue").unwrap(); - let expected = vec![ - Ok(Song { - path: Path::new("data/testcue.cue/CUE_TRACK001").to_path_buf(), - analysis: Analysis { - internal_analysis: [ - 0.38463724, - -0.85219246, - -0.761946, - -0.8904667, - -0.63892543, - -0.73945934, - -0.8004017, - -0.8237293, - 0.33865356, - 0.32481194, - -0.35692245, - -0.6355889, - -0.29584837, - 0.06431806, - 0.21875131, - -0.58104205, - -0.9466792, - -0.94811195, - -0.9820919, - -0.9596871, - ], - }, - album: Some(String::from("Album for CUE test")), - artist: Some(String::from("David TMX")), - title: Some(String::from("Renaissance")), - genre: Some(String::from("Random")), - track_number: Some(String::from("01")), - features_version: FEATURES_VERSION, - album_artist: Some(String::from("Polochon_street")), - duration: Duration::from_secs_f32(11.066666603), - cue_info: Some(CueInfo { - cue_path: PathBuf::from("data/testcue.cue"), - audio_file_path: PathBuf::from("data/testcue.flac"), - }), - ..Default::default() - }), - Ok(Song { - path: Path::new("data/testcue.cue/CUE_TRACK002").to_path_buf(), - analysis: Analysis { - internal_analysis: [ - 0.18622077, - -0.5989029, - -0.5554645, - -0.6343865, - -0.24163479, - -0.25766593, - -0.40616858, - -0.23334873, - 0.76875293, - 0.7785741, - -0.5075115, - -0.5272629, - -0.56706166, - -0.568486, - -0.5639081, - -0.5706943, - -0.96501005, - -0.96501285, - -0.9649896, - -0.96498996, - ], - }, - features_version: FEATURES_VERSION, - album: Some(String::from("Album for CUE test")), - artist: Some(String::from("Polochon_street")), - title: Some(String::from("Piano")), - genre: Some(String::from("Random")), - track_number: Some(String::from("02")), - album_artist: Some(String::from("Polochon_street")), - duration: Duration::from_secs_f64(5.853333473), - cue_info: Some(CueInfo { - cue_path: PathBuf::from("data/testcue.cue"), - audio_file_path: PathBuf::from("data/testcue.flac"), - }), - ..Default::default() - }), - Ok(Song { - path: Path::new("data/testcue.cue/CUE_TRACK003").to_path_buf(), - analysis: Analysis { - internal_analysis: [ - 0.0024261475, - 0.9874661, - 0.97330654, - -0.9724426, - 0.99678576, - -0.9961549, - -0.9840142, - -0.9269961, - 0.7498772, - 0.22429907, - -0.8355152, - -0.9977258, - -0.9977849, - -0.997785, - -0.99778515, - -0.997785, - -0.99999976, - -0.99999976, - -0.99999976, - -0.99999976, - ], - }, - album: Some(String::from("Album for CUE test")), - artist: Some(String::from("Polochon_street")), - title: Some(String::from("Tone")), - genre: Some(String::from("Random")), - track_number: Some(String::from("03")), - features_version: FEATURES_VERSION, - album_artist: Some(String::from("Polochon_street")), - duration: Duration::from_secs_f32(5.586666584), - cue_info: Some(CueInfo { - cue_path: PathBuf::from("data/testcue.cue"), - audio_file_path: PathBuf::from("data/testcue.flac"), - }), - ..Default::default() - }), - Err(BlissError::DecodingError(String::from( - "while opening format for file 'data/not-existing.wav': \ - ffmpeg::Error(2: No such file or directory).", - ))), - ]; - assert_eq!(expected, songs); - } -} diff --git a/src/lib.rs b/src/lib.rs index abfc207..a647be5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,332 +1,67 @@ -//! # bliss audio library -//! -//! bliss is a library for making "smart" audio playlists. -//! -//! The core of the library is the [Song] object, which relates to a -//! specific analyzed song and contains its path, title, analysis, and -//! other metadata fields (album, genre...). -//! Analyzing a song is as simple as running `Song::from_path("/path/to/song")`. -//! -//! The [analysis](Song::analysis) field of each song is an array of f32, which -//! makes the comparison between songs easy, by just using e.g. euclidean -//! distance (see [distance](Song::distance) for instance). -//! -//! Once several songs have been analyzed, making a playlist from one Song -//! is as easy as computing distances between that song and the rest, and ordering -//! the songs by distance, ascending. -//! -//! If you want to implement a bliss plugin for an already existing audio -//! player, the [Library] struct is a collection of goodies that should prove -//! useful (it contains utilities to store analyzed songs in a self-contained -//! database file, to make playlists directly from the database, etc). -//! [blissify](https://github.com/Polochon-street/blissify-rs/) for both -//! an example of how the [Library] struct works, and a real-life demo of bliss -//! implemented for [MPD](https://www.musicpd.org/). -//! -//! # Examples -//! -//! ### Analyze & compute the distance between two songs -//! ```no_run -//! use bliss_audio::{BlissResult, Song}; -//! -//! fn main() -> BlissResult<()> { -//! let song1 = Song::from_path("/path/to/song1")?; -//! let song2 = Song::from_path("/path/to/song2")?; -//! -//! println!("Distance between song1 and song2 is {}", song1.distance(&song2)); -//! Ok(()) -//! } -//! ``` -//! -//! ### Make a playlist from a song, discarding failed songs -//! ```no_run -//! use bliss_audio::{ -//! analyze_paths, -//! playlist::{closest_to_first_song, euclidean_distance}, -//! BlissResult, Song, -//! }; -//! -//! fn main() -> BlissResult<()> { -//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"]; -//! let mut songs: Vec = analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect(); -//! -//! // Assuming there is a first song -//! let first_song = songs.first().unwrap().to_owned(); -//! -//! closest_to_first_song(&first_song, &mut songs, euclidean_distance); -//! -//! println!("Playlist is:"); -//! for song in songs { -//! println!("{}", song.path.display()); -//! } -//! Ok(()) -//! } -//! ``` -#![cfg_attr(feature = "bench", feature(test))] -#![warn(missing_docs)] +pub mod bliss_lib; mod chroma; -pub mod cue; -#[cfg(feature = "library")] -pub mod library; -mod misc; -pub mod playlist; mod song; +mod misc; mod temporal; mod timbral; mod utils; -#[cfg(feature = "serde")] -#[macro_use] -extern crate serde; -use crate::cue::BlissCue; -use log::info; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; -use std::sync::mpsc; -use std::thread; -use thiserror::Error; +use neon::{prelude::*, types::buffer::TypedArray}; +use song::Song; +use bliss_lib::BlissResult; -pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; - -const CHANNELS: u16 = 1; -const SAMPLE_RATE: u32 = 22050; -/// Stores the current version of bliss-rs' features. -/// It is bumped every time one or more feature is added, updated or removed, -/// so plug-ins can rescan libraries when there is a major change. -pub const FEATURES_VERSION: u16 = 1; - -#[derive(Error, Clone, Debug, PartialEq, Eq)] -/// Umbrella type for bliss error types -pub enum BlissError { - #[error("error happened while decoding file – {0}")] - /// An error happened while decoding an (audio) file. - DecodingError(String), - #[error("error happened while analyzing file – {0}")] - /// An error happened during the analysis of the song's samples by bliss. - AnalysisError(String), - #[error("error happened with the music library provider - {0}")] - /// An error happened with the music library provider. - /// Useful to report errors when you implement bliss for an audio player. - ProviderError(String), +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + cx.export_function("analyzeSync", analyze)?; + cx.export_function("analyze", analyze_async)?; + Ok(()) } -/// bliss error type -pub type BlissResult = Result; - -/// Analyze songs in `paths`, and return the analyzed [Song] objects through an -/// [mpsc::IntoIter]. -/// -/// Returns an iterator, whose items are a tuple made of -/// the song path (to display to the user in case the analysis failed), -/// and a Result. -/// -/// # Note -/// -/// This function also works with CUE files - it finds the audio files -/// mentionned in the CUE sheet, and then runs the analysis on each song -/// defined by it, returning a proper [Song] object for each one of them. -/// -/// Make sure that you don't submit both the audio file along with the CUE -/// sheet if your library uses them, otherwise the audio file will be -/// analyzed as one, single, long song. For instance, with a CUE sheet named -/// `cue-file.cue` with the corresponding audio files `album-1.wav` and -/// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue` -/// to `analyze_paths`, and it will return [Song]s from both files, with -/// more information about which file it is extracted from in the -/// [cue info field](Song::cue_info). -/// -/// # Example: -/// ```no_run -/// use bliss_audio::{analyze_paths, BlissResult}; -/// -/// fn main() -> BlissResult<()> { -/// let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; -/// for (path, result) in analyze_paths(&paths) { -/// match result { -/// Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title), -/// Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e), -/// } -/// } -/// Ok(()) -/// } -/// ``` -pub fn analyze_paths, F: IntoIterator>( - paths: F, -) -> mpsc::IntoIter<(PathBuf, BlissResult)> { - let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); - analyze_paths_with_cores(paths, cores) -} - -/// Analyze songs in `paths`, and return the analyzed [Song] objects through an -/// [mpsc::IntoIter]. `number_cores` sets the number of cores the analysis -/// will use, capped by your system's capacity. Most of the time, you want to -/// use the simpler `analyze_paths` functions, which autodetects the number -/// of cores in your system. -/// -/// Return an iterator, whose items are a tuple made of -/// the song path (to display to the user in case the analysis failed), -/// and a Result. -/// -/// # Note -/// -/// This function also works with CUE files - it finds the audio files -/// mentionned in the CUE sheet, and then runs the analysis on each song -/// defined by it, returning a proper [Song] object for each one of them. -/// -/// Make sure that you don't submit both the audio file along with the CUE -/// sheet if your library uses them, otherwise the audio file will be -/// analyzed as one, single, long song. For instance, with a CUE sheet named -/// `cue-file.cue` with the corresponding audio files `album-1.wav` and -/// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue` -/// to `analyze_paths`, and it will return [Song]s from both files, with -/// more information about which file it is extracted from in the -/// [cue info field](Song::cue_info). -/// -/// # Example: -/// ```no_run -/// use bliss_audio::{analyze_paths, BlissResult}; -/// -/// fn main() -> BlissResult<()> { -/// let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; -/// for (path, result) in analyze_paths(&paths) { -/// match result { -/// Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title), -/// Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e), -/// } -/// } -/// Ok(()) -/// } -/// ``` -pub fn analyze_paths_with_cores, F: IntoIterator>( - paths: F, - number_cores: NonZeroUsize, -) -> mpsc::IntoIter<(PathBuf, BlissResult)> { - let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); - if cores > number_cores { - cores = number_cores; - } - let paths: Vec = paths.into_iter().map(|p| p.into()).collect(); - #[allow(clippy::type_complexity)] - let (tx, rx): ( - mpsc::Sender<(PathBuf, BlissResult)>, - mpsc::Receiver<(PathBuf, BlissResult)>, - ) = mpsc::channel(); - if paths.is_empty() { - return rx.into_iter(); - } - let mut handles = Vec::new(); - let mut chunk_length = paths.len() / cores; - if chunk_length == 0 { - chunk_length = paths.len(); - } - for chunk in paths.chunks(chunk_length) { - let tx_thread = tx.clone(); - let owned_chunk = chunk.to_owned(); - let child = thread::spawn(move || { - for path in owned_chunk { - info!("Analyzing file '{:?}'", path); - if let Some(extension) = Path::new(&path).extension() { - let extension = extension.to_string_lossy().to_lowercase(); - if extension == "cue" { - match BlissCue::songs_from_path(&path) { - Ok(songs) => { - for song in songs { - tx_thread.send((path.to_owned(), song)).unwrap(); - } - } - Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(), - }; - continue; - } - } - let song = Song::from_path(&path); - tx_thread.send((path.to_owned(), song)).unwrap(); - } - }); - handles.push(child); - } - - rx.into_iter() -} - -#[cfg(test)] -mod tests { - use super::*; - #[cfg(test)] - use pretty_assertions::assert_eq; - - #[test] - fn test_send_song() { - fn assert_send() {} - assert_send::(); - } - - #[test] - fn test_sync_song() { - fn assert_sync() {} - assert_sync::(); - } - - #[test] - fn test_analyze_paths() { - let paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/testcue.cue", - "./data/white_noise.mp3", - "definitely-not-existing.foo", - "not-existing.foo", - ]; - let mut results = analyze_paths(&paths) - .map(|x| match &x.1 { - Ok(s) => (true, s.path.to_owned(), None), - Err(e) => (false, x.0.to_owned(), Some(e.to_string())), +#[allow(deprecated)] +fn analyze_async(mut cx: FunctionContext) -> JsResult { + let path = cx.argument::(0)?.value(&mut cx); + let promise = cx.task(move || { + analyze_raw(&path) + }).promise(|mut cx, result| { + result + .map(|(version_bytes, analysis_bytes)| { + let mut buffer_handle = JsUint8Array::new( + &mut cx, + analysis_bytes.len() + version_bytes.len(), + ).unwrap(); + let buffer = buffer_handle.as_mut_slice(&mut cx); + + buffer[0..version_bytes.len()].copy_from_slice(&version_bytes); + buffer[version_bytes.len()..].copy_from_slice(&analysis_bytes); + buffer_handle }) - .collect::>(); - results.sort(); - let expected_results = vec![ - ( - false, - PathBuf::from("./data/testcue.cue"), - Some(String::from( - "error happened while decoding file – while \ - opening format for file './data/not-existing.wav': \ - ffmpeg::Error(2: No such file or directory).", - )), - ), - ( - false, - PathBuf::from("definitely-not-existing.foo"), - Some(String::from( - "error happened while decoding file – while \ - opening format for file 'definitely-not-existing\ - .foo': ffmpeg::Error(2: No such file or directory).", - )), - ), - ( - false, - PathBuf::from("not-existing.foo"), - Some(String::from( - "error happened while decoding file – \ - while opening format for file 'not-existing.foo': \ - ffmpeg::Error(2: No such file or directory).", - )), - ), - (true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK001"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK002"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK003"), None), - (true, PathBuf::from("./data/white_noise.mp3"), None), - ]; - - assert_eq!(results, expected_results); - - let mut results = analyze_paths_with_cores(&paths, NonZeroUsize::new(1).unwrap()) - .map(|x| match &x.1 { - Ok(s) => (true, s.path.to_owned(), None), - Err(e) => (false, x.0.to_owned(), Some(e.to_string())), - }) - .collect::>(); - results.sort(); - assert_eq!(results, expected_results); - } + .or_else(|e| cx.throw_error(e.to_string())) + }); + Ok(promise) +} + +/// Returns a Uint8Array, with the first 2 bytes being the version (16-bit unsigned little endian) +/// and the rest (currently 80 bytes) being the analysis data in little endian +fn analyze(mut cx: FunctionContext) -> JsResult { + let path = cx.argument::(0)?.value(&mut cx); + let (version_bytes, analysis_bytes) = analyze_raw(&path) + .or_else(|e| cx.throw_error(e.to_string()))?; + + let mut buffer_handle = JsUint8Array::new( + &mut cx, + analysis_bytes.len() + version_bytes.len(), + )?; + let buffer = buffer_handle.as_mut_slice(&mut cx); + + buffer[0..version_bytes.len()].copy_from_slice(&version_bytes); + buffer[version_bytes.len()..].copy_from_slice(&analysis_bytes); + + Ok(buffer_handle) +} + +fn analyze_raw(path: &str) -> BlissResult<([u8; 2], [u8; 80])> { + let song = Song::from_path(path)?; + let version_bytes = song.features_version.to_le_bytes(); + let analysis_bytes = song.analysis.as_bytes(); + Ok((version_bytes, analysis_bytes)) } diff --git a/src/library.rs b/src/library.rs deleted file mode 100644 index 3fa8efa..0000000 --- a/src/library.rs +++ /dev/null @@ -1,3220 +0,0 @@ -//! Module containing utilities to properly manage a library of [Song]s, -//! for people wanting to e.g. implement a bliss plugin for an existing -//! audio player. A good resource to look at for inspiration is -//! [blissify](https://github.com/Polochon-street/blissify-rs)'s source code. -//! -//! Useful to have direct and easy access to functions that analyze -//! and store analysis of songs in a SQLite database, as well as retrieve it, -//! and make playlists directly from analyzed songs. All functions are as -//! thoroughly tested as possible, so you don't have to do it yourself, -//! including for instance bliss features version handling, etc. -//! -//! It works in three parts: -//! * The first part is the configuration part, which allows you to -//! specify extra information that your plugin might need that will -//! be automatically stored / retrieved when you instanciate a -//! [Library] (the core of your plugin). -//! -//! To do so implies specifying a configuration struct, that will implement -//! [AppConfigTrait], i.e. implement `Serialize`, `Deserialize`, and a -//! function to retrieve the [BaseConfig] (which is just a structure -//! holding the path to the configuration file and the path to the database). -//! -//! The most straightforward way to do so is to have something like this ( -//! in this example, we assume that `path_to_extra_information` is something -//! you would want stored in your configuration file, path to a second music -//! folder for instance: -//! ``` -//! use anyhow::Result; -//! use serde::{Deserialize, Serialize}; -//! use std::path::PathBuf; -//! use std::num::NonZeroUsize; -//! use bliss_audio::BlissError; -//! use bliss_audio::library::{AppConfigTrait, BaseConfig}; -//! -//! #[derive(Serialize, Deserialize, Clone, Debug)] -//! pub struct Config { -//! #[serde(flatten)] -//! pub base_config: BaseConfig, -//! pub music_library_path: PathBuf, -//! } -//! -//! impl AppConfigTrait for Config { -//! fn base_config(&self) -> &BaseConfig { -//! &self.base_config -//! } -//! -//! fn base_config_mut(&mut self) -> &mut BaseConfig { -//! &mut self.base_config -//! } -//! } -//! impl Config { -//! pub fn new( -//! music_library_path: PathBuf, -//! config_path: Option, -//! database_path: Option, -//! number_cores: Option, -//! ) -> Result { -//! // Note that by passing `(None, None)` here, the paths will -//! // be inferred automatically using user data dirs. -//! let base_config = BaseConfig::new(config_path, database_path, number_cores)?; -//! Ok(Self { -//! base_config, -//! music_library_path, -//! }) -//! } -//! } -//! ``` -//! * The second part is the actual [Library] structure, that makes the -//! bulk of the plug-in. To initialize a library once with a given config, -//! you can do (here with a base configuration): -//! ```no_run -//! use anyhow::{Error, Result}; -//! use bliss_audio::library::{BaseConfig, Library}; -//! use std::path::PathBuf; -//! -//! let config_path = Some(PathBuf::from("path/to/config/config.json")); -//! let database_path = Some(PathBuf::from("path/to/config/bliss.db")); -//! let config = BaseConfig::new(config_path, database_path, None)?; -//! let library: Library = Library::new(config)?; -//! # Ok::<(), Error>(()) -//! ``` -//! Once this is done, you can simply load the library by doing -//! `Library::from_config_path(config_path);` -//! * The third part is using the [Library] itself: it provides you with -//! utilies such as [Library::analyze_paths], which analyzes all songs -//! in given paths and stores it in the databases, as well as -//! [Library::playlist_from], which allows you to generate a playlist -//! from any given analyzed song. -//! -//! The [Library] structure also comes with a [LibrarySong] song struct, -//! which represents a song stored in the database. -//! -//! It is made of a `bliss_song` field, containing the analyzed bliss -//! song (with the normal metatada such as the artist, etc), and an -//! `extra_info` field, which can be any user-defined serialized struct. -//! For most use cases, it would just be the unit type `()` (which is no -//! extra info), that would be used like -//! `library.playlist_from<()>(song, path, playlist_length)`, -//! but functions such as [Library::analyze_paths_extra_info] and -//! [Library::analyze_paths_convert_extra_info] let you customize what -//! information you store for each song. -//! -//! The files in -//! [examples/library.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library.rs) -//! and -//! [examples/libray_extra_info.rs](https://github.com/Polochon-street/bliss-rs/blob/master/examples/library_extra_info.rs) -//! should provide the user with enough information to start with. For a more -//! "real-life" example, the -//! [blissify](https://github.com/Polochon-street/blissify-rs)'s code is using -//! [Library] to implement bliss for a MPD player. -use crate::analyze_paths_with_cores; -use crate::cue::CueInfo; -use crate::playlist::closest_album_to_group_by_key; -use crate::playlist::closest_to_first_song_by_key; -use crate::playlist::dedup_playlist_by_key; -use crate::playlist::dedup_playlist_custom_distance_by_key; -use crate::playlist::euclidean_distance; -use crate::playlist::DistanceMetric; -use anyhow::{bail, Context, Result}; -#[cfg(not(test))] -use dirs::data_local_dir; -use indicatif::{ProgressBar, ProgressStyle}; -use log::warn; -use noisy_float::prelude::*; -use rusqlite::params; -use rusqlite::params_from_iter; -use rusqlite::Connection; -use rusqlite::OptionalExtension; -use rusqlite::Params; -use rusqlite::Row; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::collections::{HashMap, HashSet}; -use std::env; -use std::fs; -use std::fs::create_dir_all; -use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread; - -use crate::Song; -use crate::FEATURES_VERSION; -use crate::{Analysis, BlissError, NUMBER_FEATURES}; -use rusqlite::Error as RusqliteError; -use std::convert::TryInto; -use std::time::Duration; - -/// Configuration trait, used for instance to customize -/// the format in which the configuration file should be written. -pub trait AppConfigTrait: Serialize + Sized + DeserializeOwned { - // Implementers have to provide these. - /// This trait should return the [BaseConfig] from the parent, - /// user-created `Config`. - fn base_config(&self) -> &BaseConfig; - - // Implementers have to provide these. - /// This trait should return the [BaseConfig] from the parent, - /// user-created `Config`. - fn base_config_mut(&mut self) -> &mut BaseConfig; - - // Default implementation to output the config as a JSON file. - /// Convert the current config to a [String], to be written to - /// a file. - /// - /// The default writes a JSON file, but any format can be used, - /// using for example the various Serde libraries (`serde_yaml`, etc) - - /// just overwrite this method. - fn serialize_config(&self) -> Result { - Ok(serde_json::to_string_pretty(&self)?) - } - - /// Set the number of desired cores for analysis, and write it to the - /// configuration file. - fn set_number_cores(&mut self, number_cores: NonZeroUsize) -> Result<()> { - self.base_config_mut().number_cores = number_cores; - self.write() - } - - /// Get the number of desired cores for analysis, and write it to the - /// configuration file. - fn get_number_cores(&self) -> NonZeroUsize { - self.base_config().number_cores - } - - /// Default implementation to load a config from a JSON file. - /// Reads from a string. - /// - /// If you change the serialization format to use something else - /// than JSON, you need to also overwrite that function with the - /// format you chose. - fn deserialize_config(data: &str) -> Result { - Ok(serde_json::from_str(data)?) - } - - /// Load a config from the specified path, using `deserialize_config`. - /// - /// This method can be overriden in the very unlikely case - /// the user wants to do something Serde cannot. - fn from_path(path: &str) -> Result { - let data = fs::read_to_string(path)?; - Self::deserialize_config(&data) - } - - // This default impl is what requires the `Serialize` supertrait - /// Write the configuration to a file using - /// [AppConfigTrait::serialize_config]. - /// - /// This method can be overriden - /// to not use [AppConfigTrait::serialize_config], in the very unlikely - /// case the user wants to do something Serde cannot. - fn write(&self) -> Result<()> { - let serialized = self.serialize_config()?; - fs::write(&self.base_config().config_path, serialized)?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -/// The minimum configuration an application needs to work with -/// a [Library]. -pub struct BaseConfig { - /// The path to where the configuration file should be stored, - /// e.g. `/home/foo/.local/share/bliss-rs/config.json` - config_path: PathBuf, - /// The path to where the database file should be stored, - /// e.g. `/home/foo/.local/share/bliss-rs/bliss.db` - database_path: PathBuf, - /// The latest features version a song has been analyzed - /// with. - features_version: u16, - /// The number of CPU cores an analysis will be performed with. - /// Defaults to the number of CPUs in the user's computer. - number_cores: NonZeroUsize, -} - -impl BaseConfig { - pub(crate) fn get_default_data_folder() -> Result { - let path = match env::var("XDG_DATA_HOME") { - Ok(path) => Path::new(&path).join("bliss-rs"), - Err(_) => { - data_local_dir() - .with_context(|| "No suitable path found to store bliss' song database. Consider specifying such a path.")? - .join("bliss-rs") - }, - }; - Ok(path) - } - - /// Create a new, basic config. Upon calls of `Config.write()`, it will be - /// written to `config_path`. - // - /// Any path omitted will instead default to a "clever" path using - /// data directory inference. The number of cores is the number of cores - /// that should be used for any analysis. If not provided, it will default - /// to the computer's number of cores. - pub fn new( - config_path: Option, - database_path: Option, - number_cores: Option, - ) -> Result { - let config_path = { - // User provided a path; let the future file creation determine - // whether it points to something valid or not - if let Some(path) = config_path { - path - } else { - Self::get_default_data_folder()?.join(Path::new("config.json")) - } - }; - - let database_path = { - if let Some(path) = database_path { - path - } else { - Self::get_default_data_folder()?.join(Path::new("songs.db")) - } - }; - - let number_cores = number_cores.unwrap_or_else(|| { - thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()) - }); - - Ok(Self { - config_path, - database_path, - features_version: FEATURES_VERSION, - number_cores, - }) - } -} - -impl AppConfigTrait for BaseConfig { - fn base_config(&self) -> &BaseConfig { - self - } - - fn base_config_mut(&mut self) -> &mut BaseConfig { - self - } -} - -/// A struct used to hold a collection of [Song]s, with convenience -/// methods to add, remove and update songs. -/// -/// Provide it either the `BaseConfig`, or a `Config` extending -/// `BaseConfig`. -/// TODO code example -pub struct Library { - /// The configuration struct, containing both information - /// from `BaseConfig` as well as user-defined values. - pub config: Config, - /// SQL connection to the database. - pub sqlite_conn: Arc>, -} - -/// Struct holding both a Bliss song, as well as any extra info -/// that a user would want to store in the database related to that -/// song. -/// -/// The only constraint is that `extra_info` must be serializable, so, -/// something like -/// ```no_compile -/// #[derive(Serialize)] -/// struct ExtraInfo { -/// ignore: bool, -/// unique_id: i64, -/// } -/// let extra_info = ExtraInfo { ignore: true, unique_id = 123 }; -/// let song = LibrarySong { bliss_song: song, extra_info }; -/// ``` -/// is totally possible. -#[derive(Debug, PartialEq, Clone)] -pub struct LibrarySong { - /// Actual bliss song, containing the song's metadata, as well - /// as the bliss analysis. - pub bliss_song: Song, - /// User-controlled information regarding that specific song. - pub extra_info: T, -} - -// TODO add logging statement -// TODO concrete examples -// TODO example LibrarySong without any extra_info -// TODO maybe return number of elements updated / deleted / whatev in analysis -// functions? -// TODO add full rescan -// TODO a song_from_path with custom filters -// TODO "smart" playlist -// TODO should it really use anyhow errors? -// TODO make sure that the path to string is consistent -// TODO make a function that returns a list of all analyzed songs in the db -impl Library { - /// Create a new [Library] object from the given Config struct that - /// implements the [AppConfigTrait]. - /// writing the configuration to the file given in - /// `config.config_path`. - /// - /// This function should only be called once, when a user wishes to - /// create a completely new "library". - /// Otherwise, load an existing library file using - /// [Library::from_config_path]. - pub fn new(config: Config) -> Result { - if !config - .base_config() - .config_path - .parent() - .ok_or_else(|| { - BlissError::ProviderError(format!( - "specified path {} is not a valid file path.", - config.base_config().config_path.display() - )) - })? - .is_dir() - { - create_dir_all(config.base_config().config_path.parent().unwrap())?; - } - let sqlite_conn = Connection::open(&config.base_config().database_path)?; - sqlite_conn.execute( - " - create table if not exists song ( - id integer primary key, - path text not null unique, - duration float, - album_artist text, - artist text, - title text, - album text, - track_number text, - genre text, - cue_path text, - audio_file_path text, - stamp timestamp default current_timestamp, - version integer, - analyzed boolean default false, - extra_info json, - error text - ); - ", - [], - )?; - sqlite_conn.execute("pragma foreign_keys = on;", [])?; - sqlite_conn.execute( - " - create table if not exists feature ( - id integer primary key, - song_id integer not null, - feature real not null, - feature_index integer not null, - unique(song_id, feature_index), - foreign key(song_id) references song(id) on delete cascade - ) - ", - [], - )?; - config.write()?; - Ok(Library { - config, - sqlite_conn: Arc::new(Mutex::new(sqlite_conn)), - }) - } - - /// Load a library from a configuration path. - /// - /// If no configuration path is provided, the path is - /// set using default data folder path. - pub fn from_config_path(config_path: Option) -> Result { - let config_path: Result = - config_path.map_or_else(|| Ok(BaseConfig::new(None, None, None)?.config_path), Ok); - let config_path = config_path?; - let data = fs::read_to_string(config_path)?; - let config = Config::deserialize_config(&data)?; - let sqlite_conn = Connection::open(&config.base_config().database_path)?; - let mut library = Library { - config, - sqlite_conn: Arc::new(Mutex::new(sqlite_conn)), - }; - if !library.version_sanity_check()? { - warn!( - "Songs have been analyzed with different versions of bliss; \ - older versions will be ignored from playlists. Update your \ - bliss library to correct the issue." - ); - } - Ok(library) - } - - /// Check whether the library contains songs analyzed with different, - /// incompatible versions of bliss. - /// - /// Returns true if the database is clean (only one version of the - /// features), and false otherwise. - pub fn version_sanity_check(&mut self) -> Result { - let connection = self - .sqlite_conn - .lock() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - let count: u32 = connection - .query_row("select count(distinct version) from song", [], |row| { - row.get(0) - }) - .optional()? - .unwrap_or(0); - Ok(count <= 1) - } - - /// Create a new [Library] object from a minimal configuration setup, - /// writing it to `config_path`. - pub fn new_from_base( - config_path: Option, - database_path: Option, - number_cores: Option, - ) -> Result - where - BaseConfig: Into, - { - let base = BaseConfig::new(config_path, database_path, number_cores)?; - let config = base.into(); - Self::new(config) - } - - /// Build a playlist of `playlist_length` items from an already analyzed - /// song in the library at `song_path`. - /// - /// It uses a simple euclidean distance between songs, and deduplicates songs - /// that are too close. - pub fn playlist_from( - &self, - song_path: &str, - playlist_length: usize, - ) -> Result>> { - let first_song: LibrarySong = self.song_from_path(song_path)?; - let mut songs = self.songs_from_library()?; - closest_to_first_song_by_key( - &first_song, - &mut songs, - euclidean_distance, - |s: &LibrarySong| s.bliss_song.to_owned(), - ); - songs.sort_by_cached_key(|song| n32(first_song.bliss_song.distance(&song.bliss_song))); - dedup_playlist_by_key(&mut songs, None, |s: &LibrarySong| { - s.bliss_song.to_owned() - }); - songs.truncate(playlist_length); - Ok(songs) - } - - /// Build a playlist of `playlist_length` items from an already analyzed - /// song in the library at `song_path`, using distance metric `distance`, - /// the sorting function `sort_by` and deduplicating if `dedup` is set to - /// `true`. - /// - /// You can use ready to use distance metrics such as - /// [euclidean_distance], and ready to use sorting functions like - /// [closest_to_first_song_by_key]. - /// - /// In most cases, you just want to use [Library::playlist_from]. - /// Use `playlist_from_custom` if you want to experiment with different - /// distance metrics / sorting functions. - /// - /// Example: - /// `library.playlist_from_song_custom(song_path, 20, euclidean_distance, - /// closest_to_first_song_by_key, true)`. - /// TODO path here too - pub fn playlist_from_custom( - &self, - song_path: &str, - playlist_length: usize, - distance: G, - mut sort_by: F, - dedup: bool, - ) -> Result>> - where - F: FnMut(&LibrarySong, &mut Vec>, G, fn(&LibrarySong) -> Song), - G: DistanceMetric + Copy, - { - let first_song: LibrarySong = self.song_from_path(song_path).map_err(|_| { - BlissError::ProviderError(format!("song '{song_path}' has not been analyzed")) - })?; - let mut songs = self.songs_from_library()?; - sort_by(&first_song, &mut songs, distance, |s: &LibrarySong| { - s.bliss_song.to_owned() - }); - if dedup { - dedup_playlist_custom_distance_by_key( - &mut songs, - None, - distance, - |s: &LibrarySong| s.bliss_song.to_owned(), - ); - } - songs.truncate(playlist_length); - Ok(songs) - } - - /// Make a playlist of `number_albums` albums closest to the album - /// with title `album_title`. - /// The playlist starts with the album with `album_title`, and contains - /// `number_albums` on top of that one. - /// - /// Returns the songs of each album ordered by bliss' `track_number`. - pub fn album_playlist_from( - &self, - album_title: String, - number_albums: usize, - ) -> Result>> { - let album = self.songs_from_album(&album_title)?; - // Every song should be from the same album. Hopefully... - let songs = self.songs_from_library()?; - let playlist = closest_album_to_group_by_key(album, songs, |s| s.bliss_song.to_owned())?; - - let mut album_count = 0; - let mut index = 0; - let mut current_album = Some(album_title); - for song in playlist.iter() { - if song.bliss_song.album != current_album { - album_count += 1; - if album_count > number_albums { - break; - } - current_album = song.bliss_song.album.to_owned(); - } - index += 1; - } - let playlist = &playlist[..index]; - Ok(playlist.to_vec()) - } - - /// Analyze and store all songs in `paths` that haven't been already analyzed. - /// - /// Use this function if you don't have any extra data to bundle with each song. - /// - /// Setting `delete_everything_else` to true will delete the paths that are - /// not mentionned in `paths_extra_info` from the database. If you do not - /// use it, because you only pass the new paths that need to be analyzed to - /// this function, make sure to delete yourself from the database the songs - /// that have been deleted from storage. - /// - /// If your library - /// contains CUE files, pass the CUE file path only, and not individual - /// CUE track names: passing `vec![file.cue]` will add - /// individual tracks with the `cue_info` field set in the database. - pub fn update_library>( - &mut self, - paths: Vec

, - delete_everything_else: bool, - show_progress_bar: bool, - ) -> Result<()> { - let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::>(); - self.update_library_convert_extra_info( - paths_extra_info, - delete_everything_else, - show_progress_bar, - |x, _, _| x, - ) - } - - /// Analyze and store all songs in `paths_extra_info` that haven't already - /// been analyzed, along with some extra metadata serializable, and known - /// before song analysis. - /// - /// Setting `delete_everything_else` to true will delete the paths that are - /// not mentionned in `paths_extra_info` from the database. If you do not - /// use it, because you only pass the new paths that need to be analyzed to - /// this function, make sure to delete yourself from the database the songs - /// that have been deleted from storage. - pub fn update_library_extra_info>( - &mut self, - paths_extra_info: Vec<(P, T)>, - delete_everything_else: bool, - show_progress_bar: bool, - ) -> Result<()> { - self.update_library_convert_extra_info( - paths_extra_info, - delete_everything_else, - show_progress_bar, - |extra_info, _, _| extra_info, - ) - } - - /// Analyze and store all songs in `paths_extra_info` that haven't - /// been already analyzed, as well as handling extra, user-specified metadata, - /// that can't directly be serializable, - /// or that need input from the analyzed Song to be processed. If you - /// just want to analyze and store songs along with some directly - /// serializable values, consider using [Library::update_library_extra_info], - /// or [Library::update_library] if you just want the analyzed songs - /// stored as is. - /// - /// `paths_extra_info` is a tuple made out of song paths, along - /// with any extra info you want to store for each song. - /// If your library - /// contains CUE files, pass the CUE file path only, and not individual - /// CUE track names: passing `vec![file.cue]` will add - /// individual tracks with the `cue_info` field set in the database. - /// - /// Setting `delete_everything_else` to true will delete the paths that are - /// not mentionned in `paths_extra_info` from the database. If you do not - /// use it, because you only pass the new paths that need to be analyzed to - /// this function, make sure to delete yourself from the database the songs - /// that have been deleted from storage. - /// - /// `convert_extra_info` is a function that you should specify how - /// to convert that extra info to something serializable. - pub fn update_library_convert_extra_info< - T: Serialize + DeserializeOwned, - U, - P: Into, - >( - &mut self, - paths_extra_info: Vec<(P, U)>, - delete_everything_else: bool, - show_progress_bar: bool, - convert_extra_info: fn(U, &Song, &Self) -> T, - ) -> Result<()> { - let existing_paths = { - let connection = self - .sqlite_conn - .lock() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - let mut path_statement = connection.prepare( - " - select - path - from song where analyzed = true and version = ? order by id - ", - )?; - #[allow(clippy::let_and_return)] - let return_value = path_statement - .query_map([FEATURES_VERSION], |row| { - Ok(row.get_unwrap::(0)) - })? - .map(|x| PathBuf::from(x.unwrap())) - .collect::>(); - return_value - }; - - let paths_extra_info: Vec<_> = paths_extra_info - .into_iter() - .map(|(x, y)| (x.into(), y)) - .collect(); - let paths: HashSet<_> = paths_extra_info.iter().map(|(p, _)| p.to_owned()).collect(); - - if delete_everything_else { - let paths_to_delete = existing_paths.difference(&paths); - - self.delete_paths(paths_to_delete)?; - } - - // Can't use hashsets because we need the extra info here too, - // and U might not be hashable. - let paths_to_analyze = paths_extra_info - .into_iter() - .filter(|(path, _)| !existing_paths.contains(path)) - .collect::>(); - - self.analyze_paths_convert_extra_info( - paths_to_analyze, - show_progress_bar, - convert_extra_info, - ) - } - - /// Analyze and store all songs in `paths`. - /// - /// Updates the value of `features_version` in the config, using bliss' - /// latest version. - /// - /// Use this function if you don't have any extra data to bundle with each song. - /// - /// If your library - /// contains CUE files, pass the CUE file path only, and not individual - /// CUE track names: passing `vec![file.cue]` will add - /// individual tracks with the `cue_info` field set in the database. - pub fn analyze_paths>( - &mut self, - paths: Vec

, - show_progress_bar: bool, - ) -> Result<()> { - let paths_extra_info = paths.into_iter().map(|path| (path, ())).collect::>(); - self.analyze_paths_convert_extra_info(paths_extra_info, show_progress_bar, |x, _, _| x) - } - - /// Analyze and store all songs in `paths_extra_info`, along with some - /// extra metadata serializable, and known before song analysis. - /// - /// Updates the value of `features_version` in the config, using bliss' - /// latest version. - /// If your library - /// contains CUE files, pass the CUE file path only, and not individual - /// CUE track names: passing `vec![file.cue]` will add - /// individual tracks with the `cue_info` field set in the database. - pub fn analyze_paths_extra_info< - T: Serialize + DeserializeOwned + std::fmt::Debug, - P: Into, - >( - &mut self, - paths_extra_info: Vec<(P, T)>, - show_progress_bar: bool, - ) -> Result<()> { - self.analyze_paths_convert_extra_info( - paths_extra_info, - show_progress_bar, - |extra_info, _, _| extra_info, - ) - } - - /// Analyze and store all songs in `paths_extra_info`, along with some - /// extra, user-specified metadata, that can't directly be serializable, - /// or that need input from the analyzed Song to be processed. - /// If you just want to analyze and store songs, along with some - /// directly serializable metadata values, consider using - /// [Library::analyze_paths_extra_info], or [Library::analyze_paths] for - /// the simpler use cases. - /// - /// Updates the value of `features_version` in the config, using bliss' - /// latest version. - /// - /// `paths_extra_info` is a tuple made out of song paths, along - /// with any extra info you want to store for each song. If your library - /// contains CUE files, pass the CUE file path only, and not individual - /// CUE track names: passing `vec![file.cue]` will add - /// individual tracks with the `cue_info` field set in the database. - /// - /// `convert_extra_info` is a function that you should specify - /// to convert that extra info to something serializable. - pub fn analyze_paths_convert_extra_info< - T: Serialize + DeserializeOwned, - U, - P: Into, - >( - &mut self, - paths_extra_info: Vec<(P, U)>, - show_progress_bar: bool, - convert_extra_info: fn(U, &Song, &Self) -> T, - ) -> Result<()> { - let number_songs = paths_extra_info.len(); - if number_songs == 0 { - log::info!("No (new) songs found."); - return Ok(()); - } - log::info!( - "Analyzing {} songs, this might take some time…", - number_songs - ); - let pb = if show_progress_bar { - ProgressBar::new(number_songs.try_into().unwrap()) - } else { - ProgressBar::hidden() - }; - let style = ProgressStyle::default_bar() - .template("[{elapsed_precise}] {bar:40} {pos:>7}/{len:7} {wide_msg}")? - .progress_chars("##-"); - pb.set_style(style); - - let mut paths_extra_info: HashMap = paths_extra_info - .into_iter() - .map(|(x, y)| (x.into(), y)) - .collect(); - let mut cue_extra_info: HashMap = HashMap::new(); - - let results = analyze_paths_with_cores( - paths_extra_info.keys(), - self.config.base_config().number_cores, - ); - let mut success_count = 0; - let mut failure_count = 0; - for (path, result) in results { - if show_progress_bar { - pb.set_message(format!("Analyzing {}", path.display())); - } - match result { - Ok(song) => { - let is_cue = song.cue_info.is_some(); - // If it's a song that's part of a CUE, its path will be - // something like `testcue.flac/CUE_TRACK001`, so we need - // to get the path of the main CUE file. - let path = { - if let Some(cue_info) = song.cue_info.to_owned() { - cue_info.cue_path - } else { - path - } - }; - // Some magic to avoid having to depend on T: Clone, because - // all CUE tracks on a CUE file have the same extra_info. - // This serializes the data, store the serialized version - // in a hashmap, and then deserializes that when needed. - let extra = { - if is_cue && paths_extra_info.contains_key(&path) { - let extra = paths_extra_info.remove(&path).unwrap(); - let e = convert_extra_info(extra, &song, self); - cue_extra_info.insert( - path, - serde_json::to_string(&e) - .map_err(|e| BlissError::ProviderError(e.to_string()))?, - ); - e - } else if is_cue { - let serialized_extra_info = - cue_extra_info.get(&path).unwrap().to_owned(); - serde_json::from_str(&serialized_extra_info).unwrap() - } else { - let extra = paths_extra_info.remove(&path).unwrap(); - convert_extra_info(extra, &song, self) - } - }; - let library_song = LibrarySong:: { - bliss_song: song, - extra_info: extra, - }; - self.store_song(&library_song)?; - success_count += 1; - } - Err(e) => { - log::error!( - "Analysis of song '{}' failed: {} The error has been stored.", - path.display(), - e - ); - - self.store_failed_song(path, e)?; - failure_count += 1; - } - }; - pb.inc(1); - } - pb.finish_with_message(format!( - "Analyzed {success_count} song(s) successfully. {failure_count} Failure(s).", - )); - - log::info!( - "Analyzed {} song(s) successfully. {} Failure(s).", - success_count, - failure_count, - ); - - self.config.base_config_mut().features_version = FEATURES_VERSION; - self.config.write()?; - - Ok(()) - } - - // Get songs from a songs / features statement. - // BEWARE that the two songs and features query MUST be the same - fn _songs_from_statement( - &self, - songs_statement: &str, - features_statement: &str, - params: P, - ) -> Result>> { - let connection = self - .sqlite_conn - .lock() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - let mut songs_statement = connection.prepare(songs_statement)?; - let mut features_statement = connection.prepare(features_statement)?; - let song_rows = songs_statement.query_map(params.to_owned(), |row| { - Ok((row.get(12)?, Self::_song_from_row_closure(row)?)) - })?; - let feature_rows = - features_statement.query_map(params, |row| Ok((row.get(1)?, row.get(0)?)))?; - - let mut feature_iterator = feature_rows.into_iter().peekable(); - let mut songs = Vec::new(); - // Poor man's way to double check that each feature correspond to the - // right song, and group them. - for row in song_rows { - let song_id: u32 = row.as_ref().unwrap().0; - let mut chunk: Vec = Vec::with_capacity(NUMBER_FEATURES); - - while let Some(first_value) = feature_iterator.peek() { - let (song_feature_id, feature): (u32, f32) = *first_value.as_ref().unwrap(); - if song_feature_id == song_id { - chunk.push(feature); - feature_iterator.next(); - } else { - break; - }; - } - let mut song = row.unwrap().1; - song.bliss_song.analysis = Analysis { - internal_analysis: chunk.try_into().map_err(|_| { - BlissError::ProviderError(format!( - "Song with ID {} and path {} has a different feature \ - number than expected. Please rescan or update \ - the song library.", - song_id, - song.bliss_song.path.display(), - )) - })?, - }; - songs.push(song); - } - Ok(songs) - } - - /// Retrieve all songs which have been analyzed with - /// current bliss version. - /// - /// Returns an error if one or several songs have a different number of - /// features than they should, indicating the offending song id. - /// - // TODO maybe the error should make the song id / song path - // accessible easily? - pub fn songs_from_library( - &self, - ) -> Result>> { - let songs_statement = " - select - path, artist, title, album, album_artist, - track_number, genre, duration, version, extra_info, cue_path, - audio_file_path, id - from song where analyzed = true and version = ? order by id - "; - let features_statement = " - select - feature, song.id from feature join song on song.id = feature.song_id - where song.analyzed = true and song.version = ? order by song_id, feature_index - "; - let params = params![self.config.base_config().features_version]; - self._songs_from_statement(songs_statement, features_statement, params) - } - - /// Get a LibrarySong from a given album title. - /// - /// This will return all songs with corresponding bliss "album" tag, - /// and will order them by track number. - pub fn songs_from_album( - &self, - album_title: &str, - ) -> Result>> { - let params = params![album_title, self.config.base_config().features_version]; - let songs_statement = " - select - path, artist, title, album, album_artist, - track_number, genre, duration, version, extra_info, cue_path, - audio_file_path, id - from song where album = ? and analyzed = true and version = ? - order - by cast(track_number as integer); - "; - - // Get the song's analysis, and attach it to the existing song. - let features_statement = " - select - feature, song.id from feature join song on song.id = feature.song_id - where album=? and analyzed = true and version = ? - order by cast(track_number as integer); - "; - let songs = self._songs_from_statement(songs_statement, features_statement, params)?; - if songs.is_empty() { - bail!(BlissError::ProviderError(String::from( - "target album was not found in the database.", - ))); - }; - Ok(songs) - } - - /// Get a LibrarySong from a given file path. - /// TODO pathbuf here too - pub fn song_from_path( - &self, - song_path: &str, - ) -> Result> { - let connection = self - .sqlite_conn - .lock() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - // Get the song's metadata. The analysis is populated yet. - let mut song = connection.query_row( - " - select - path, artist, title, album, album_artist, - track_number, genre, duration, version, extra_info, - cue_path, audio_file_path - from song where path=? and analyzed = true - ", - params![song_path], - Self::_song_from_row_closure, - )?; - - // Get the song's analysis, and attach it to the existing song. - let mut stmt = connection.prepare( - " - select - feature from feature join song on song.id = feature.song_id - where song.path = ? order by feature_index - ", - )?; - let analysis_vector = Analysis { - internal_analysis: stmt - .query_map(params![song_path], |row| row.get(0)) - .unwrap() - .map(|x| x.unwrap()) - .collect::>() - .try_into() - .map_err(|_| { - BlissError::ProviderError(format!( - "song has more or less than {NUMBER_FEATURES} features", - )) - })?, - }; - song.bliss_song.analysis = analysis_vector; - Ok(song) - } - - fn _song_from_row_closure( - row: &Row, - ) -> Result, RusqliteError> { - let path: String = row.get(0)?; - - let cue_path: Option = row.get(10)?; - let audio_file_path: Option = row.get(11)?; - let mut cue_info = None; - if let Some(cue_path) = cue_path { - cue_info = Some(CueInfo { - cue_path: PathBuf::from(cue_path), - audio_file_path: PathBuf::from(audio_file_path.unwrap()), - }) - }; - - let song = Song { - path: PathBuf::from(path), - artist: row - .get_ref(1) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - title: row - .get_ref(2) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - album: row - .get_ref(3) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - album_artist: row - .get_ref(4) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - track_number: row - .get_ref(5) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - genre: row - .get_ref(6) - .unwrap() - .as_bytes_or_null() - .unwrap() - .map(|v| String::from_utf8_lossy(v).to_string()), - analysis: Analysis { - internal_analysis: [0.; NUMBER_FEATURES], - }, - duration: Duration::from_secs_f64(row.get(7).unwrap()), - features_version: row.get(8).unwrap(), - cue_info, - }; - - let serialized: Option = row.get(9).unwrap(); - let serialized = serialized.unwrap_or_else(|| "null".into()); - let extra_info = serde_json::from_str(&serialized).unwrap(); - Ok(LibrarySong { - bliss_song: song, - extra_info, - }) - } - - /// Store a [Song] in the database, overidding any existing - /// song with the same path by that one. - // TODO to_str() returns an option; return early and avoid panicking - pub fn store_song( - &mut self, - library_song: &LibrarySong, - ) -> Result<(), BlissError> { - let mut sqlite_conn = self.sqlite_conn.lock().unwrap(); - let tx = sqlite_conn - .transaction() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - let song = &library_song.bliss_song; - let (cue_path, audio_file_path) = match &song.cue_info { - Some(c) => ( - Some(c.cue_path.to_string_lossy()), - Some(c.audio_file_path.to_string_lossy()), - ), - None => (None, None), - }; - tx.execute( - " - insert into song ( - path, artist, title, album, album_artist, - duration, track_number, genre, analyzed, version, extra_info, - cue_path, audio_file_path - ) - values ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13 - ) - on conflict(path) - do update set - artist=excluded.artist, - title=excluded.title, - album=excluded.album, - track_number=excluded.track_number, - album_artist=excluded.album_artist, - duration=excluded.duration, - genre=excluded.genre, - analyzed=excluded.analyzed, - version=excluded.version, - extra_info=excluded.extra_info, - cue_path=excluded.cue_path, - audio_file_path=excluded.audio_file_path - ", - params![ - song.path.to_str(), - song.artist, - song.title, - song.album, - song.album_artist, - song.duration.as_secs_f64(), - song.track_number, - song.genre, - true, - song.features_version, - serde_json::to_string(&library_song.extra_info) - .map_err(|e| BlissError::ProviderError(e.to_string()))?, - cue_path, - audio_file_path, - ], - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - - // Override existing features. - tx.execute( - "delete from feature where song_id in (select id from song where path = ?1);", - params![song.path.to_str()], - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - - for (index, feature) in song.analysis.as_vec().iter().enumerate() { - tx.execute( - " - insert into feature (song_id, feature, feature_index) - values ((select id from song where path = ?1), ?2, ?3) - on conflict(song_id, feature_index) do update set feature=excluded.feature; - ", - params![song.path.to_str(), feature, index], - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - } - tx.commit() - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - Ok(()) - } - - /// Store an errored [Song](Song) in the SQLite database. - /// - /// If there already is an existing song with that path, replace it by - /// the latest failed result. - pub fn store_failed_song>( - &mut self, - song_path: P, - e: BlissError, - ) -> Result<()> { - self.sqlite_conn - .lock() - .unwrap() - .execute( - " - insert or replace into song (path, error) values (?1, ?2) - ", - [ - song_path.into().to_string_lossy().to_string(), - e.to_string(), - ], - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - Ok(()) - } - - /// Delete a song with path `song_path` from the database. - /// - /// Errors out if the song is not in the database. - pub fn delete_path>(&mut self, song_path: P) -> Result<()> { - let song_path = song_path.into(); - let count = self - .sqlite_conn - .lock() - .unwrap() - .execute( - " - delete from song where path = ?1; - ", - [song_path.to_str()], - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - if count == 0 { - bail!(BlissError::ProviderError(format!( - "tried to delete song {}, not existing in the database.", - song_path.display(), - ))); - } - Ok(()) - } - - /// Delete a set of songs with paths `song_paths` from the database. - /// - /// Will return Ok(count) even if less songs than expected were deleted from the database. - pub fn delete_paths, I: IntoIterator>( - &mut self, - paths: I, - ) -> Result { - let song_paths: Vec = paths - .into_iter() - .map(|x| x.into().to_string_lossy().to_string()) - .collect(); - if song_paths.is_empty() { - return Ok(0); - }; - let count = self - .sqlite_conn - .lock() - .unwrap() - .execute( - &format!( - "delete from song where path in ({})", - repeat_vars(song_paths.len()), - ), - params_from_iter(song_paths), - ) - .map_err(|e| BlissError::ProviderError(e.to_string()))?; - Ok(count) - } -} - -// Copied from -// https://docs.rs/rusqlite/latest/rusqlite/struct.ParamsFromIter.html#realistic-use-case -fn repeat_vars(count: usize) -> String { - assert_ne!(count, 0); - let mut s = "?,".repeat(count); - // Remove trailing comma - s.pop(); - s -} - -#[cfg(test)] -fn data_local_dir() -> Option { - Some(PathBuf::from("/local/directory")) -} - -#[cfg(test)] -// TODO refactor (especially the helper functions) -// TODO the tests should really open a songs.db -// TODO test with invalid UTF-8 -mod test { - use super::*; - use crate::{Analysis, NUMBER_FEATURES}; - use ndarray::Array1; - use pretty_assertions::assert_eq; - use serde::{de::DeserializeOwned, Deserialize}; - use std::{convert::TryInto, fmt::Debug, sync::MutexGuard, time::Duration}; - use tempdir::TempDir; - - #[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)] - struct ExtraInfo { - ignore: bool, - metadata_bliss_does_not_have: String, - } - - #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)] - struct CustomConfig { - #[serde(flatten)] - base_config: BaseConfig, - second_path_to_music_library: String, - ignore_wav_files: bool, - } - - impl AppConfigTrait for CustomConfig { - fn base_config(&self) -> &BaseConfig { - &self.base_config - } - - fn base_config_mut(&mut self) -> &mut BaseConfig { - &mut self.base_config - } - } - - fn nzus(i: usize) -> NonZeroUsize { - NonZeroUsize::new(i).unwrap() - } - - // Returning the TempDir here, so it doesn't go out of scope, removing - // the directory. - // - // Setup a test library made of 3 analyzed songs, with every field being different, - // as well as an unanalyzed song and a song analyzed with a previous version. - fn setup_test_library() -> ( - Library, - TempDir, - ( - LibrarySong, - LibrarySong, - LibrarySong, - LibrarySong, - LibrarySong, - LibrarySong, - LibrarySong, - ), - ) { - let config_dir = TempDir::new("coucou").unwrap(); - let config_file = config_dir.path().join("config.json"); - let database_file = config_dir.path().join("bliss.db"); - let library = - Library::::new_from_base(Some(config_file), Some(database_file), None) - .unwrap(); - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 / 10.) - .collect::>() - .try_into() - .unwrap(); - let song = Song { - path: "/path/to/song1001".into(), - artist: Some("Artist1001".into()), - title: Some("Title1001".into()), - album: Some("An Album1001".into()), - album_artist: Some("An Album Artist1001".into()), - track_number: Some("03".into()), - genre: Some("Electronica1001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(310), - features_version: 1, - cue_info: None, - }; - let first_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("/path/to/charlie1001"), - }, - }; - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 + 10.) - .collect::>() - .try_into() - .unwrap(); - - let song = Song { - path: "/path/to/song2001".into(), - artist: Some("Artist2001".into()), - title: Some("Title2001".into()), - album: Some("An Album2001".into()), - album_artist: Some("An Album Artist2001".into()), - track_number: Some("02".into()), - genre: Some("Electronica2001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(410), - features_version: 1, - cue_info: None, - }; - let second_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie2001"), - }, - }; - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 / 2.) - .collect::>() - .try_into() - .unwrap(); - let song = Song { - path: "/path/to/song5001".into(), - artist: Some("Artist5001".into()), - title: Some("Title5001".into()), - album: Some("An Album1001".into()), - album_artist: Some("An Album Artist5001".into()), - track_number: Some("01".into()), - genre: Some("Electronica5001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(610), - features_version: 1, - cue_info: None, - }; - let third_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie5001"), - }, - }; - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 * 0.9) - .collect::>() - .try_into() - .unwrap(); - let song = Song { - path: "/path/to/song6001".into(), - artist: Some("Artist6001".into()), - title: Some("Title6001".into()), - album: Some("An Album2001".into()), - album_artist: Some("An Album Artist6001".into()), - track_number: Some("01".into()), - genre: Some("Electronica6001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(710), - features_version: 1, - cue_info: None, - }; - let fourth_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie6001"), - }, - }; - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 * 50.) - .collect::>() - .try_into() - .unwrap(); - let song = Song { - path: "/path/to/song7001".into(), - artist: Some("Artist7001".into()), - title: Some("Title7001".into()), - album: Some("An Album7001".into()), - album_artist: Some("An Album Artist7001".into()), - track_number: Some("01".into()), - genre: Some("Electronica7001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(810), - features_version: 1, - cue_info: None, - }; - let fifth_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie7001"), - }, - }; - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 * 100.) - .collect::>() - .try_into() - .unwrap(); - - let song = Song { - path: "/path/to/cuetrack.cue/CUE_TRACK001".into(), - artist: Some("CUE Artist".into()), - title: Some("CUE Title 01".into()), - album: Some("CUE Album".into()), - album_artist: Some("CUE Album Artist".into()), - track_number: Some("01".into()), - genre: None, - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(810), - features_version: 1, - cue_info: Some(CueInfo { - cue_path: PathBuf::from("/path/to/cuetrack.cue"), - audio_file_path: PathBuf::from("/path/to/cuetrack.flac"), - }), - }; - let sixth_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie7001"), - }, - }; - - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 * 101.) - .collect::>() - .try_into() - .unwrap(); - - let song = Song { - path: "/path/to/cuetrack.cue/CUE_TRACK002".into(), - artist: Some("CUE Artist".into()), - title: Some("CUE Title 02".into()), - album: Some("CUE Album".into()), - album_artist: Some("CUE Album Artist".into()), - track_number: Some("02".into()), - genre: None, - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(910), - features_version: 1, - cue_info: Some(CueInfo { - cue_path: PathBuf::from("/path/to/cuetrack.cue"), - audio_file_path: PathBuf::from("/path/to/cuetrack.flac"), - }), - }; - let seventh_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie7001"), - }, - }; - - { - let connection = library.sqlite_conn.lock().unwrap(); - connection - .execute( - " - insert into song ( - id, path, artist, title, album, album_artist, track_number, - genre, duration, analyzed, version, extra_info, - cue_path, audio_file_path - ) values ( - 1001, '/path/to/song1001', 'Artist1001', 'Title1001', 'An Album1001', - 'An Album Artist1001', '03', 'Electronica1001', 310, true, - 1, '{\"ignore\": true, \"metadata_bliss_does_not_have\": - \"/path/to/charlie1001\"}', null, null - ), - ( - 2001, '/path/to/song2001', 'Artist2001', 'Title2001', 'An Album2001', - 'An Album Artist2001', '02', 'Electronica2001', 410, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie2001\"}', null, null - ), - ( - 3001, '/path/to/song3001', null, null, null, - null, null, null, null, false, 1, '{}', null, null - ), - ( - 4001, '/path/to/song4001', 'Artist4001', 'Title4001', 'An Album4001', - 'An Album Artist4001', '01', 'Electronica4001', 510, true, - 0, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie4001\"}', null, null - ), - ( - 5001, '/path/to/song5001', 'Artist5001', 'Title5001', 'An Album1001', - 'An Album Artist5001', '01', 'Electronica5001', 610, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie5001\"}', null, null - ), - ( - 6001, '/path/to/song6001', 'Artist6001', 'Title6001', 'An Album2001', - 'An Album Artist6001', '01', 'Electronica6001', 710, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie6001\"}', null, null - ), - ( - 7001, '/path/to/song7001', 'Artist7001', 'Title7001', 'An Album7001', - 'An Album Artist7001', '01', 'Electronica7001', 810, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie7001\"}', null, null - ), - ( - 7002, '/path/to/cuetrack.cue/CUE_TRACK001', 'CUE Artist', - 'CUE Title 01', 'CUE Album', - 'CUE Album Artist', '01', null, 810, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie7001\"}', '/path/to/cuetrack.cue', - '/path/to/cuetrack.flac' - ), - ( - 7003, '/path/to/cuetrack.cue/CUE_TRACK002', 'CUE Artist', - 'CUE Title 02', 'CUE Album', - 'CUE Album Artist', '02', null, 910, true, - 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie7001\"}', '/path/to/cuetrack.cue', - '/path/to/cuetrack.flac' - ), - ( - 8001, '/path/to/song8001', 'Artist8001', 'Title8001', 'An Album1001', - 'An Album Artist8001', '03', 'Electronica8001', 910, true, - 0, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie8001\"}', null, null - ), - ( - 9001, './data/s16_stereo_22_5kHz.flac', 'Artist9001', 'Title9001', - 'An Album9001', 'An Album Artist8001', '03', 'Electronica8001', - 1010, true, 0, '{\"ignore\": false, \"metadata_bliss_does_not_have\": - \"/path/to/charlie7001\"}', null, null - ); - ", - [], - ) - .unwrap(); - for index in 0..NUMBER_FEATURES { - connection - .execute( - " - insert into feature(song_id, feature, feature_index) - values - (1001, ?2, ?1), - (2001, ?3, ?1), - (3001, ?4, ?1), - (5001, ?5, ?1), - (6001, ?6, ?1), - (7001, ?7, ?1), - (7002, ?8, ?1), - (7003, ?9, ?1); - ", - params![ - index, - index as f32 / 10., - index as f32 + 10., - index as f32 / 10. + 1., - index as f32 / 2., - index as f32 * 0.9, - index as f32 * 50., - index as f32 * 100., - index as f32 * 101., - ], - ) - .unwrap(); - } - // Imaginary version 0 of bliss with less features. - for index in 0..NUMBER_FEATURES - 5 { - connection - .execute( - " - insert into feature(song_id, feature, feature_index) - values - (8001, ?2, ?1), - (9001, ?3, ?1); - ", - params![index, index as f32 / 20., index + 1], - ) - .unwrap(); - } - } - ( - library, - config_dir, - ( - first_song, - second_song, - third_song, - fourth_song, - fifth_song, - sixth_song, - seventh_song, - ), - ) - } - - fn _library_song_from_database( - connection: MutexGuard, - song_path: &str, - ) -> LibrarySong { - let mut song = connection - .query_row( - " - select - path, artist, title, album, album_artist, - track_number, genre, duration, version, extra_info, - cue_path, audio_file_path - from song where path=? - ", - params![song_path], - |row| { - let path: String = row.get(0)?; - let cue_path: Option = row.get(10)?; - let audio_file_path: Option = row.get(11)?; - let mut cue_info = None; - if let Some(cue_path) = cue_path { - cue_info = Some(CueInfo { - cue_path: PathBuf::from(cue_path), - audio_file_path: PathBuf::from(audio_file_path.unwrap()), - }) - }; - let song = Song { - path: PathBuf::from(path), - artist: row.get(1).unwrap(), - title: row.get(2).unwrap(), - album: row.get(3).unwrap(), - album_artist: row.get(4).unwrap(), - track_number: row.get(5).unwrap(), - genre: row.get(6).unwrap(), - analysis: Analysis { - internal_analysis: [0.; NUMBER_FEATURES], - }, - duration: Duration::from_secs_f64(row.get(7).unwrap()), - features_version: row.get(8).unwrap(), - cue_info, - }; - - let serialized: String = row.get(9).unwrap(); - let extra_info = serde_json::from_str(&serialized).unwrap(); - Ok(LibrarySong { - bliss_song: song, - extra_info, - }) - }, - ) - .expect("Song does not exist in the database"); - let mut stmt = connection - .prepare( - " - select - feature from feature join song on song.id = feature.song_id - where song.path = ? order by feature_index - ", - ) - .unwrap(); - let analysis_vector = Analysis { - internal_analysis: stmt - .query_map(params![song_path], |row| row.get(0)) - .unwrap() - .into_iter() - .map(|x| x.unwrap()) - .collect::>() - .try_into() - .unwrap(), - }; - song.bliss_song.analysis = analysis_vector; - song - } - - fn _basic_song_from_database(connection: MutexGuard, song_path: &str) -> Song { - let mut expected_song = connection - .query_row( - " - select - path, artist, title, album, album_artist, - track_number, genre, duration, version - from song where path=? and analyzed = true - ", - params![song_path], - |row| { - let path: String = row.get(0)?; - Ok(Song { - path: PathBuf::from(path), - artist: row.get(1).unwrap(), - title: row.get(2).unwrap(), - album: row.get(3).unwrap(), - album_artist: row.get(4).unwrap(), - track_number: row.get(5).unwrap(), - genre: row.get(6).unwrap(), - analysis: Analysis { - internal_analysis: [0.; NUMBER_FEATURES], - }, - duration: Duration::from_secs_f64(row.get(7).unwrap()), - features_version: row.get(8).unwrap(), - cue_info: None, - }) - }, - ) - .expect("Song is probably not in the db"); - let mut stmt = connection - .prepare( - " - select - feature from feature join song on song.id = feature.song_id - where song.path = ? order by feature_index - ", - ) - .unwrap(); - let expected_analysis_vector = Analysis { - internal_analysis: stmt - .query_map(params![song_path], |row| row.get(0)) - .unwrap() - .into_iter() - .map(|x| x.unwrap()) - .collect::>() - .try_into() - .unwrap(), - }; - expected_song.analysis = expected_analysis_vector; - expected_song - } - - fn _generate_basic_song(path: Option) -> Song { - let path = path.unwrap_or_else(|| "/path/to/song".into()); - // Add some "randomness" to the features - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 + 0.1) - .collect::>() - .try_into() - .unwrap(); - Song { - path: path.into(), - artist: Some("An Artist".into()), - title: Some("Title".into()), - album: Some("An Album".into()), - album_artist: Some("An Album Artist".into()), - track_number: Some("03".into()), - genre: Some("Electronica".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(80), - features_version: 1, - cue_info: None, - } - } - - fn _generate_library_song(path: Option) -> LibrarySong { - let song = _generate_basic_song(path); - let extra_info = ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: "FoobarIze".into(), - }; - LibrarySong { - bliss_song: song, - extra_info, - } - } - - #[test] - fn test_library_playlist_song_not_existing() { - let (library, _temp_dir, _) = setup_test_library(); - assert!(library - .playlist_from::("not-existing", 2) - .is_err()); - } - - #[test] - fn test_library_playlist_crop() { - let (library, _temp_dir, _) = setup_test_library(); - let songs: Vec> = - library.playlist_from("/path/to/song2001", 2).unwrap(); - assert_eq!(2, songs.len()); - } - - #[test] - fn test_library_simple_playlist() { - let (library, _temp_dir, _) = setup_test_library(); - let songs: Vec> = - library.playlist_from("/path/to/song2001", 20).unwrap(); - assert_eq!( - vec![ - "/path/to/song2001", - "/path/to/song6001", - "/path/to/song5001", - "/path/to/song1001", - "/path/to/song7001", - "/path/to/cuetrack.cue/CUE_TRACK001", - "/path/to/cuetrack.cue/CUE_TRACK002", - ], - songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_custom_playlist_distance() { - let (library, _temp_dir, _) = setup_test_library(); - let distance = - |a: &Array1, b: &Array1| (a.get(1).unwrap() - b.get(1).unwrap()).abs(); - let songs: Vec> = library - .playlist_from_custom( - "/path/to/song2001", - 20, - distance, - closest_to_first_song_by_key, - true, - ) - .unwrap(); - assert_eq!( - vec![ - "/path/to/song2001", - "/path/to/song6001", - "/path/to/song5001", - "/path/to/song1001", - "/path/to/song7001", - "/path/to/cuetrack.cue/CUE_TRACK001", - "/path/to/cuetrack.cue/CUE_TRACK002", - ], - songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - fn custom_sort( - _: &LibrarySong, - songs: &mut Vec>, - _distance: impl DistanceMetric, - key_fn: F, - ) where - F: Fn(&LibrarySong) -> Song, - { - songs.sort_by_key(|song| key_fn(song).path); - } - - #[test] - fn test_library_custom_playlist_sort() { - let (library, _temp_dir, _) = setup_test_library(); - let songs: Vec> = library - .playlist_from_custom( - "/path/to/song2001", - 20, - euclidean_distance, - custom_sort, - true, - ) - .unwrap(); - assert_eq!( - vec![ - "/path/to/cuetrack.cue/CUE_TRACK001", - "/path/to/cuetrack.cue/CUE_TRACK002", - "/path/to/song1001", - "/path/to/song2001", - "/path/to/song5001", - "/path/to/song6001", - "/path/to/song7001", - ], - songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_custom_playlist_dedup() { - let (library, _temp_dir, _) = setup_test_library(); - let distance = |a: &Array1, b: &Array1| { - ((a.get(1).unwrap() - b.get(1).unwrap()).abs() / 30.).floor() - }; - let songs: Vec> = library - .playlist_from_custom( - "/path/to/song2001", - 20, - distance, - closest_to_first_song_by_key, - true, - ) - .unwrap(); - assert_eq!( - vec![ - "/path/to/song1001", - "/path/to/song7001", - "/path/to/cuetrack.cue/CUE_TRACK001" - ], - songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ); - - let distance = - |a: &Array1, b: &Array1| ((a.get(1).unwrap() - b.get(1).unwrap()).abs()); - let songs: Vec> = library - .playlist_from_custom( - "/path/to/song2001", - 20, - distance, - closest_to_first_song_by_key, - false, - ) - .unwrap(); - assert_eq!( - vec![ - "/path/to/song2001", - "/path/to/song6001", - "/path/to/song5001", - "/path/to/song1001", - "/path/to/song7001", - "/path/to/cuetrack.cue/CUE_TRACK001", - "/path/to/cuetrack.cue/CUE_TRACK002", - ], - songs - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_album_playlist() { - let (library, _temp_dir, _) = setup_test_library(); - let album: Vec> = library - .album_playlist_from("An Album1001".to_string(), 20) - .unwrap(); - assert_eq!( - vec![ - // First album. - "/path/to/song5001".to_string(), - "/path/to/song1001".to_string(), - // Second album, well ordered. - "/path/to/song6001".to_string(), - "/path/to/song2001".to_string(), - // Third album. - "/path/to/song7001".to_string(), - // Fourth album. - "/path/to/cuetrack.cue/CUE_TRACK001".to_string(), - "/path/to/cuetrack.cue/CUE_TRACK002".to_string(), - ], - album - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_album_playlist_crop() { - let (library, _temp_dir, _) = setup_test_library(); - let album: Vec> = library - .album_playlist_from("An Album1001".to_string(), 1) - .unwrap(); - assert_eq!( - vec![ - // First album. - "/path/to/song5001".to_string(), - "/path/to/song1001".to_string(), - // Second album, well ordered. - "/path/to/song6001".to_string(), - "/path/to/song2001".to_string(), - ], - album - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_songs_from_album() { - let (library, _temp_dir, _) = setup_test_library(); - let album: Vec> = library.songs_from_album("An Album1001").unwrap(); - assert_eq!( - vec![ - "/path/to/song5001".to_string(), - "/path/to/song1001".to_string() - ], - album - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_songs_from_album_proper_features_version() { - let (library, _temp_dir, _) = setup_test_library(); - let album: Vec> = library.songs_from_album("An Album1001").unwrap(); - assert_eq!( - vec![ - "/path/to/song5001".to_string(), - "/path/to/song1001".to_string() - ], - album - .into_iter() - .map(|s| s.bliss_song.path.to_string_lossy().to_string()) - .collect::>(), - ) - } - - #[test] - fn test_library_songs_from_album_not_existing() { - let (library, _temp_dir, _) = setup_test_library(); - assert!(library - .songs_from_album::("not-existing") - .is_err()); - } - - #[test] - fn test_library_delete_path_non_existing() { - let (mut library, _temp_dir, _) = setup_test_library(); - { - let connection = library.sqlite_conn.lock().unwrap(); - let count: u32 = connection - .query_row( - "select count(*) from feature join song on song.id = feature.song_id where song.path = ?", - ["not-existing"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(count, 0); - let count: u32 = connection - .query_row( - "select count(*) from song where path = ?", - ["not-existing"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(count, 0); - } - assert!(library.delete_path("not-existing").is_err()); - } - - #[test] - fn test_library_delete_path() { - let (mut library, _temp_dir, _) = setup_test_library(); - { - let connection = library.sqlite_conn.lock().unwrap(); - let count: u32 = connection - .query_row( - "select count(*) from feature join song on song.id = feature.song_id where song.path = ?", - ["/path/to/song1001"], - |row| row.get(0), - ) - .unwrap(); - assert!(count >= 1); - let count: u32 = connection - .query_row( - "select count(*) from song where path = ?", - ["/path/to/song1001"], - |row| row.get(0), - ) - .unwrap(); - assert!(count >= 1); - } - - library.delete_path("/path/to/song1001").unwrap(); - - { - let connection = library.sqlite_conn.lock().unwrap(); - let count: u32 = connection - .query_row( - "select count(*) from feature join song on song.id = feature.song_id where song.path = ?", - ["/path/to/song1001"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(0, count); - let count: u32 = connection - .query_row( - "select count(*) from song where path = ?", - ["/path/to/song1001"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(0, count); - } - } - - #[test] - fn test_library_delete_paths() { - let (mut library, _temp_dir, _) = setup_test_library(); - { - let connection = library.sqlite_conn.lock().unwrap(); - let count: u32 = connection - .query_row( - "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)", - ["/path/to/song1001", "/path/to/song2001"], - |row| row.get(0), - ) - .unwrap(); - assert!(count >= 1); - let count: u32 = connection - .query_row( - "select count(*) from song where path in (?1, ?2)", - ["/path/to/song1001", "/path/to/song2001"], - |row| row.get(0), - ) - .unwrap(); - assert!(count >= 1); - } - - library - .delete_paths(vec!["/path/to/song1001", "/path/to/song2001"]) - .unwrap(); - - { - let connection = library.sqlite_conn.lock().unwrap(); - let count: u32 = connection - .query_row( - "select count(*) from feature join song on song.id = feature.song_id where song.path in (?1, ?2)", - ["/path/to/song1001", "/path/to/song2001"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(0, count); - let count: u32 = connection - .query_row( - "select count(*) from song where path in (?1, ?2)", - ["/path/to/song1001", "/path/to/song2001"], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(0, count); - // Make sure we did not delete everything else - let count: u32 = connection - .query_row("select count(*) from feature", [], |row| row.get(0)) - .unwrap(); - assert!(count >= 1); - let count: u32 = connection - .query_row("select count(*) from song", [], |row| row.get(0)) - .unwrap(); - assert!(count >= 1); - } - } - - #[test] - fn test_library_delete_paths_empty() { - let (mut library, _temp_dir, _) = setup_test_library(); - assert_eq!(library.delete_paths::([]).unwrap(), 0); - } - - #[test] - fn test_library_delete_paths_non_existing() { - let (mut library, _temp_dir, _) = setup_test_library(); - assert_eq!(library.delete_paths(["not-existing"]).unwrap(), 0); - } - - #[test] - fn test_analyze_paths_cue() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - { - let sqlite_conn = - Connection::open(&library.config.base_config().database_path).unwrap(); - sqlite_conn.execute("delete from song", []).unwrap(); - } - - let paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/testcue.cue", - "non-existing", - ]; - library.analyze_paths(paths.to_owned(), false).unwrap(); - let expected_analyzed_paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/testcue.cue/CUE_TRACK001", - "./data/testcue.cue/CUE_TRACK002", - "./data/testcue.cue/CUE_TRACK003", - ]; - { - let connection = library.sqlite_conn.lock().unwrap(); - let mut stmt = connection - .prepare( - " - select - path from song where analyzed = true and path not like '%song%' - order by path - ", - ) - .unwrap(); - let paths = stmt - .query_map(params![], |row| row.get(0)) - .unwrap() - .map(|x| x.unwrap()) - .collect::>(); - - assert_eq!(paths, expected_analyzed_paths); - } - { - let connection = library.sqlite_conn.lock().unwrap(); - let song: LibrarySong<()> = - _library_song_from_database(connection, "./data/testcue.cue/CUE_TRACK001"); - assert!(song.bliss_song.cue_info.is_some()); - } - } - - #[test] - fn test_analyze_paths() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - let paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/s16_stereo_22_5kHz.flac", - "non-existing", - ]; - library.analyze_paths(paths.to_owned(), false).unwrap(); - let songs = paths[..2] - .iter() - .map(|path| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip(vec![(), ()].into_iter()) - .map(|(path, expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - fn test_analyze_paths_convert_extra_info() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - let paths = vec![ - ("./data/s16_mono_22_5kHz.flac", true), - ("./data/s16_stereo_22_5kHz.flac", false), - ("non-existing", false), - ]; - library - .analyze_paths_convert_extra_info(paths.to_owned(), true, |b, _, _| ExtraInfo { - ignore: b, - metadata_bliss_does_not_have: String::from("coucou"), - }) - .unwrap(); - library - .analyze_paths_convert_extra_info(paths.to_owned(), false, |b, _, _| ExtraInfo { - ignore: b, - metadata_bliss_does_not_have: String::from("coucou"), - }) - .unwrap(); - let songs = paths[..2] - .iter() - .map(|(path, _)| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip( - vec![ - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ] - .into_iter(), - ) - .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - fn test_analyze_paths_extra_info() { - let (mut library, _temp_dir, _) = setup_test_library(); - - let paths = vec![ - ( - "./data/s16_mono_22_5kHz.flac", - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("hey"), - }, - ), - ( - "./data/s16_stereo_22_5kHz.flac", - ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("hello"), - }, - ), - ( - "non-existing", - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ), - ]; - library - .analyze_paths_extra_info(paths.to_owned(), false) - .unwrap(); - let songs = paths[..2] - .iter() - .map(|(path, _)| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip( - vec![ - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("hey"), - }, - ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("hello"), - }, - ] - .into_iter(), - ) - .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - } - - #[test] - // Check that a song already in the database is not - // analyzed again on updates. - fn test_update_skip_analyzed() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - for input in vec![ - ("./data/s16_mono_22_5kHz.flac", true), - ("./data/s16_mono_22_5kHz.flac", false), - ] - .into_iter() - { - let paths = vec![input.to_owned()]; - library - .update_library_convert_extra_info(paths.to_owned(), true, false, |b, _, _| { - ExtraInfo { - ignore: b, - metadata_bliss_does_not_have: String::from("coucou"), - } - }) - .unwrap(); - let song = { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database::(connection, "./data/s16_mono_22_5kHz.flac") - }; - let expected_song = { - LibrarySong { - bliss_song: Song::from_path("./data/s16_mono_22_5kHz.flac").unwrap(), - extra_info: ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("coucou"), - }, - } - }; - assert_eq!(song, expected_song); - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - } - - fn _get_song_analyzed( - connection: MutexGuard, - path: String, - ) -> Result { - let mut stmt = connection.prepare( - " - select - analyzed from song - where song.path = ? - ", - )?; - stmt.query_row([path], |row| (row.get(0))) - } - - #[test] - fn test_update_library_override_old_features() { - let (mut library, _temp_dir, _) = setup_test_library(); - let path: String = "./data/s16_stereo_22_5kHz.flac".into(); - - { - let connection = library.sqlite_conn.lock().unwrap(); - let mut stmt = connection - .prepare( - " - select - feature from feature join song on song.id = feature.song_id - where song.path = ? order by feature_index - ", - ) - .unwrap(); - let analysis_vector = stmt - .query_map(params![path], |row| row.get(0)) - .unwrap() - .into_iter() - .map(|x| x.unwrap()) - .collect::>(); - assert_eq!( - analysis_vector, - vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15.] - ) - } - - library - .update_library(vec![path.to_owned()], true, false) - .unwrap(); - - let connection = library.sqlite_conn.lock().unwrap(); - let mut stmt = connection - .prepare( - " - select - feature from feature join song on song.id = feature.song_id - where song.path = ? order by feature_index - ", - ) - .unwrap(); - let analysis_vector = Analysis { - internal_analysis: stmt - .query_map(params![path], |row| row.get(0)) - .unwrap() - .into_iter() - .map(|x| x.unwrap()) - .collect::>() - .try_into() - .unwrap(), - }; - let expected_analysis_vector = Song::from_path(path).unwrap().analysis; - assert_eq!(analysis_vector, expected_analysis_vector); - } - - #[test] - fn test_update_library() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - - let paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/s16_stereo_22_5kHz.flac", - "/path/to/song4001", - "non-existing", - ]; - library - .update_library(paths.to_owned(), true, false) - .unwrap(); - library - .update_library(paths.to_owned(), true, true) - .unwrap(); - - let songs = paths[..2] - .iter() - .map(|path| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip(vec![(), ()].into_iter()) - .map(|(path, expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - - assert_eq!(songs, expected_songs); - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - fn test_update_extra_info() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - - let paths = vec![ - ("./data/s16_mono_22_5kHz.flac", true), - ("./data/s16_stereo_22_5kHz.flac", false), - ("/path/to/song4001", false), - ("non-existing", false), - ]; - library - .update_library_extra_info(paths.to_owned(), true, false) - .unwrap(); - let songs = paths[..2] - .iter() - .map(|(path, _)| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip(vec![true, false].into_iter()) - .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - fn test_update_convert_extra_info() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that all the starting songs are there - assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap()); - } - - let paths = vec![ - ("./data/s16_mono_22_5kHz.flac", true), - ("./data/s16_stereo_22_5kHz.flac", false), - ("/path/to/song4001", false), - ("non-existing", false), - ]; - library - .update_library_convert_extra_info(paths.to_owned(), true, false, |b, _, _| ExtraInfo { - ignore: b, - metadata_bliss_does_not_have: String::from("coucou"), - }) - .unwrap(); - let songs = paths[..2] - .iter() - .map(|(path, _)| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip( - vec![ - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ] - .into_iter(), - ) - .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we deleted older songs - assert_eq!( - rusqlite::Error::QueryReturnedNoRows, - _get_song_analyzed(connection, "/path/to/song2001".into()).unwrap_err(), - ); - } - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - // TODO maybe we can merge / DRY this and the function ⬆ - fn test_update_convert_extra_info_do_not_delete() { - let (mut library, _temp_dir, _) = setup_test_library(); - library.config.base_config_mut().features_version = 0; - - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that all the starting songs are there - assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap()); - } - - let paths = vec![ - ("./data/s16_mono_22_5kHz.flac", true), - ("./data/s16_stereo_22_5kHz.flac", false), - ("/path/to/song4001", false), - ("non-existing", false), - ]; - library - .update_library_convert_extra_info(paths.to_owned(), false, false, |b, _, _| { - ExtraInfo { - ignore: b, - metadata_bliss_does_not_have: String::from("coucou"), - } - }) - .unwrap(); - let songs = paths[..2] - .iter() - .map(|(path, _)| { - let connection = library.sqlite_conn.lock().unwrap(); - _library_song_from_database(connection, path) - }) - .collect::>>(); - let expected_songs = paths[..2] - .iter() - .zip( - vec![ - ExtraInfo { - ignore: true, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("coucou"), - }, - ] - .into_iter(), - ) - .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), - extra_info: expected_extra_info, - }) - .collect::>>(); - assert_eq!(songs, expected_songs); - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we tried to "update" song4001 with the new features. - assert!(!_get_song_analyzed(connection, "/path/to/song4001".into()).unwrap()); - } - { - let connection = library.sqlite_conn.lock().unwrap(); - // Make sure that we did not delete older songs - assert!(_get_song_analyzed(connection, "/path/to/song2001".into()).unwrap()); - } - assert_eq!( - library.config.base_config_mut().features_version, - FEATURES_VERSION - ); - } - - #[test] - fn test_song_from_path() { - let (library, _temp_dir, _) = setup_test_library(); - let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) - .map(|x| x as f32 + 10.) - .collect::>() - .try_into() - .unwrap(); - - let song = Song { - path: "/path/to/song2001".into(), - artist: Some("Artist2001".into()), - title: Some("Title2001".into()), - album: Some("An Album2001".into()), - album_artist: Some("An Album Artist2001".into()), - track_number: Some("02".into()), - genre: Some("Electronica2001".into()), - analysis: Analysis { - internal_analysis: analysis_vector, - }, - duration: Duration::from_secs(410), - features_version: 1, - cue_info: None, - }; - let expected_song = LibrarySong { - bliss_song: song, - extra_info: ExtraInfo { - ignore: false, - metadata_bliss_does_not_have: String::from("/path/to/charlie2001"), - }, - }; - - let song = library - .song_from_path::("/path/to/song2001") - .unwrap(); - assert_eq!(song, expected_song) - } - - #[test] - fn test_store_failed_song() { - let (mut library, _temp_dir, _) = setup_test_library(); - library - .store_failed_song( - "/some/failed/path", - BlissError::ProviderError("error with the analysis".into()), - ) - .unwrap(); - let connection = library.sqlite_conn.lock().unwrap(); - let (error, analyzed): (String, bool) = connection - .query_row( - " - select - error, analyzed - from song where path=? - ", - params!["/some/failed/path"], - |row| Ok((row.get_unwrap(0), row.get_unwrap(1))), - ) - .unwrap(); - assert_eq!( - error, - String::from( - "error happened with the music library provider - error with the analysis" - ) - ); - assert_eq!(analyzed, false); - let count_features: u32 = connection - .query_row( - " - select - count(*) from feature join song - on song.id = feature.song_id where path=? - ", - params!["/some/failed/path"], - |row| Ok(row.get_unwrap(0)), - ) - .unwrap(); - assert_eq!(count_features, 0); - } - - #[test] - fn test_songs_from_library() { - let (library, _temp_dir, expected_library_songs) = setup_test_library(); - - let library_songs = library.songs_from_library::().unwrap(); - assert_eq!(library_songs.len(), 7); - assert_eq!( - expected_library_songs, - ( - library_songs[0].to_owned(), - library_songs[1].to_owned(), - library_songs[2].to_owned(), - library_songs[3].to_owned(), - library_songs[4].to_owned(), - library_songs[5].to_owned(), - library_songs[6].to_owned(), - ) - ); - } - - #[test] - fn test_songs_from_library_screwed_db() { - let (library, _temp_dir, _) = setup_test_library(); - { - let connection = library.sqlite_conn.lock().unwrap(); - connection - .execute( - "insert into feature (song_id, feature, feature_index) - values (2001, 1.5, 21) - ", - [], - ) - .unwrap(); - } - - let error = library.songs_from_library::().unwrap_err(); - assert_eq!( - error.to_string(), - String::from( - "error happened with the music library provider - \ - Song with ID 2001 and path /path/to/song2001 has a \ - different feature number than expected. Please rescan or \ - update the song library.", - ), - ); - } - - #[test] - fn test_song_from_path_not_analyzed() { - let (library, _temp_dir, _) = setup_test_library(); - let error = library.song_from_path::("/path/to/song4001"); - assert!(error.is_err()); - } - - #[test] - fn test_song_from_path_not_found() { - let (library, _temp_dir, _) = setup_test_library(); - let error = library.song_from_path::("/path/to/song4001"); - assert!(error.is_err()); - } - - #[test] - fn test_get_default_data_folder_no_default_path() { - env::set_var("XDG_DATA_HOME", "/home/foo/.local/share/"); - assert_eq!( - PathBuf::from("/home/foo/.local/share/bliss-rs"), - BaseConfig::get_default_data_folder().unwrap() - ); - env::remove_var("XDG_DATA_HOME"); - - assert_eq!( - PathBuf::from("/local/directory/bliss-rs"), - BaseConfig::get_default_data_folder().unwrap() - ); - } - - #[test] - fn test_library_new_default_write() { - let (library, _temp_dir, _) = setup_test_library(); - let config_content = fs::read_to_string(&library.config.base_config().config_path) - .unwrap() - .replace(' ', "") - .replace('\n', ""); - assert_eq!( - config_content, - format!( - "{{\"config_path\":\"{}\",\"database_path\":\"{}\",\"features_version\":{},\"number_cores\":{}}}", - library.config.base_config().config_path.display(), - library.config.base_config().database_path.display(), - FEATURES_VERSION, - thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()), - ) - ); - } - - #[test] - fn test_library_new_create_database() { - let (library, _temp_dir, _) = setup_test_library(); - let sqlite_conn = Connection::open(&library.config.base_config().database_path).unwrap(); - sqlite_conn - .execute( - " - insert into song ( - id, path, artist, title, album, album_artist, - track_number, genre, stamp, version, duration, analyzed, - extra_info - ) - values ( - 1, '/random/path', 'Some Artist', 'A Title', 'Some Album', - 'Some Album Artist', '01', 'Electronica', '2022-01-01', - 1, 250, true, '{\"key\": \"value\"}' - ); - ", - [], - ) - .unwrap(); - sqlite_conn - .execute( - " - insert into feature(id, song_id, feature, feature_index) - values (2000, 1, 1.1, 1) - on conflict(song_id, feature_index) do update set feature=excluded.feature; - ", - [], - ) - .unwrap(); - } - - #[test] - fn test_library_store_song() { - let (mut library, _temp_dir, _) = setup_test_library(); - let song = _generate_basic_song(None); - let library_song = LibrarySong { - bliss_song: song.to_owned(), - extra_info: (), - }; - library.store_song(&library_song).unwrap(); - let connection = library.sqlite_conn.lock().unwrap(); - let expected_song = _basic_song_from_database(connection, &song.path.to_string_lossy()); - assert_eq!(expected_song, song); - } - - #[test] - fn test_library_extra_info() { - let (mut library, _temp_dir, _) = setup_test_library(); - let song = _generate_library_song(None); - library.store_song(&song).unwrap(); - let connection = library.sqlite_conn.lock().unwrap(); - let returned_song = - _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy()); - assert_eq!(returned_song, song); - } - - #[test] - fn test_from_config_path_non_existing() { - assert!( - Library::::from_config_path(Some(PathBuf::from("non-existing"))).is_err() - ); - } - - #[test] - fn test_from_config_path() { - let config_dir = TempDir::new("coucou").unwrap(); - let config_file = config_dir.path().join("config.json"); - let database_file = config_dir.path().join("bliss.db"); - - // In reality, someone would just do that with `(None, None)` to get the default - // paths. - let base_config = BaseConfig::new( - Some(config_file.to_owned()), - Some(database_file), - Some(nzus(1)), - ) - .unwrap(); - - let config = CustomConfig { - base_config, - second_path_to_music_library: "/path/to/somewhere".into(), - ignore_wav_files: true, - }; - // Test that it is possible to store a song in a library instance, - // make that instance go out of scope, load the library again, and - // get the stored song. - let song = _generate_library_song(None); - { - let mut library = Library::new(config.to_owned()).unwrap(); - library.store_song(&song).unwrap(); - } - - let library: Library = Library::from_config_path(Some(config_file)).unwrap(); - let connection = library.sqlite_conn.lock().unwrap(); - let returned_song = - _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy()); - - assert_eq!(library.config, config); - assert_eq!(song, returned_song); - } - - #[test] - fn test_config_serialize_deserialize() { - let config_dir = TempDir::new("coucou").unwrap(); - let config_file = config_dir.path().join("config.json"); - let database_file = config_dir.path().join("bliss.db"); - - // In reality, someone would just do that with `(None, None)` to get the default - // paths. - let base_config = BaseConfig::new( - Some(config_file.to_owned()), - Some(database_file), - Some(nzus(1)), - ) - .unwrap(); - - let config = CustomConfig { - base_config, - second_path_to_music_library: "/path/to/somewhere".into(), - ignore_wav_files: true, - }; - config.write().unwrap(); - - assert_eq!( - config, - CustomConfig::from_path(&config_file.to_string_lossy()).unwrap(), - ); - } - - #[test] - fn test_library_sanity_check_fail() { - let (mut library, _temp_dir, _) = setup_test_library(); - assert!(!library.version_sanity_check().unwrap()); - } - - #[test] - fn test_library_sanity_check_ok() { - let (mut library, _temp_dir, _) = setup_test_library(); - { - let sqlite_conn = - Connection::open(&library.config.base_config().database_path).unwrap(); - sqlite_conn - .execute("delete from song where version != 1", []) - .unwrap(); - } - assert!(library.version_sanity_check().unwrap()); - } - - #[test] - fn test_config_number_cpus() { - let config_dir = TempDir::new("coucou").unwrap(); - let config_file = config_dir.path().join("config.json"); - let database_file = config_dir.path().join("bliss.db"); - - let base_config = BaseConfig::new( - Some(config_file.to_owned()), - Some(database_file.to_owned()), - None, - ) - .unwrap(); - let config = CustomConfig { - base_config, - second_path_to_music_library: "/path/to/somewhere".into(), - ignore_wav_files: true, - }; - - assert_eq!( - config.get_number_cores().get(), - usize::from(thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap())), - ); - - let base_config = - BaseConfig::new(Some(config_file), Some(database_file), Some(nzus(1))).unwrap(); - let mut config = CustomConfig { - base_config, - second_path_to_music_library: "/path/to/somewhere".into(), - ignore_wav_files: true, - }; - - assert_eq!(config.get_number_cores().get(), 1); - config.set_number_cores(nzus(2)).unwrap(); - assert_eq!(config.get_number_cores().get(), 2); - } - - #[test] - fn test_library_create_all_dirs() { - let config_dir = TempDir::new("coucou") - .unwrap() - .path() - .join("path") - .join("to"); - assert!(!config_dir.is_dir()); - let config_file = config_dir.join("config.json"); - let database_file = config_dir.join("bliss.db"); - Library::::new_from_base(Some(config_file), Some(database_file), Some(nzus(1))) - .unwrap(); - assert!(config_dir.is_dir()); - } -} diff --git a/src/misc.rs b/src/misc.rs index 1693590..0e73071 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -63,14 +63,14 @@ impl Normalize for LoudnessDesc { #[cfg(test)] mod tests { use super::*; - use crate::Song; + use crate::bliss_lib::Song; use std::path::Path; #[test] fn test_loudness() { let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut loudness_desc = LoudnessDesc::default(); - for chunk in song.sample_array.chunks_exact(LoudnessDesc::WINDOW_SIZE) { + for chunk in song.chunks_exact(LoudnessDesc::WINDOW_SIZE) { loudness_desc.do_(&chunk); } let expected_values = vec![0.271263, 0.2577181]; diff --git a/src/playlist.rs b/src/playlist.rs deleted file mode 100644 index 93477c1..0000000 --- a/src/playlist.rs +++ /dev/null @@ -1,984 +0,0 @@ -//! Module containing various functions to build playlists, as well as various -//! distance metrics. -//! -//! All of the distance functions are intended to be used with the -//! [custom_distance](Song::custom_distance) method, or with -//! -//! They will yield different styles of playlists, so don't hesitate to -//! experiment with them if the default (euclidean distance for now) doesn't -//! suit you. -// TODO on the `by_key` functions: maybe Fn(&T) -> &Song is enough? Compared -// to -> Song -use crate::{BlissError, BlissResult, Song, NUMBER_FEATURES}; -use ndarray::{Array, Array1, Array2, Axis}; -use ndarray_stats::QuantileExt; -use noisy_float::prelude::*; -use std::collections::HashMap; - -/// Convenience trait for user-defined distance metrics. -pub trait DistanceMetric: Fn(&Array1, &Array1) -> f32 {} -impl DistanceMetric for F where F: Fn(&Array1, &Array1) -> f32 {} - -/// Return the [euclidean -/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions) -/// between two vectors. -pub fn euclidean_distance(a: &Array1, b: &Array1) -> f32 { - // Could be any square symmetric positive semi-definite matrix; - // just no metric learning has been done yet. - // See https://lelele.io/thesis.pdf chapter 4. - let m = Array::eye(NUMBER_FEATURES); - - (a - b).dot(&m).dot(&(a - b)).sqrt() -} - -/// Return the [cosine -/// distance](https://en.wikipedia.org/wiki/Cosine_similarity#Angular_distance_and_similarity) -/// between two vectors. -pub fn cosine_distance(a: &Array1, b: &Array1) -> f32 { - let similarity = a.dot(b) / (a.dot(a).sqrt() * b.dot(b).sqrt()); - 1. - similarity -} - -/// Sort `songs` in place by putting songs close to `first_song` first -/// using the `distance` metric. -pub fn closest_to_first_song( - first_song: &Song, - #[allow(clippy::ptr_arg)] songs: &mut Vec, - distance: impl DistanceMetric, -) { - songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance))); -} - -/// Sort `songs` in place by putting songs close to `first_song` first -/// using the `distance` metric. -/// -/// Sort songs with a key extraction function, useful for when you have a -/// structure like `CustomSong { bliss_song: Song, something_else: bool }` -pub fn closest_to_first_song_by_key( - first_song: &T, - #[allow(clippy::ptr_arg)] songs: &mut Vec, - distance: impl DistanceMetric, - key_fn: F, -) where - F: Fn(&T) -> Song, -{ - let first_song = key_fn(first_song); - songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&key_fn(song), &distance))); -} - -/// Sort `songs` in place using the `distance` metric and ordering by -/// the smallest distance between each song. -/// -/// If the generated playlist is `[song1, song2, song3, song4]`, it means -/// song2 is closest to song1, song3 is closest to song2, and song4 is closest -/// to song3. -/// -/// Note that this has a tendency to go from one style to the other very fast, -/// and it can be slow on big libraries. -pub fn song_to_song(first_song: &Song, songs: &mut Vec, distance: impl DistanceMetric) { - let mut new_songs = Vec::with_capacity(songs.len()); - let mut song = first_song.to_owned(); - - while !songs.is_empty() { - let distances: Array1 = - Array::from_shape_fn(songs.len(), |i| song.custom_distance(&songs[i], &distance)); - let idx = distances.argmin().unwrap(); - song = songs[idx].to_owned(); - new_songs.push(song.to_owned()); - songs.retain(|s| s != &song); - } - *songs = new_songs; -} - -/// Sort `songs` in place using the `distance` metric and ordering by -/// the smallest distance between each song. -/// -/// If the generated playlist is `[song1, song2, song3, song4]`, it means -/// song2 is closest to song1, song3 is closest to song2, and song4 is closest -/// to song3. -/// -/// Note that this has a tendency to go from one style to the other very fast, -/// and it can be slow on big libraries. -/// -/// Sort songs with a key extraction function, useful for when you have a -/// structure like `CustomSong { bliss_song: Song, something_else: bool }` -// TODO: maybe Clone is not needed? -pub fn song_to_song_by_key( - first_song: &T, - songs: &mut Vec, - distance: impl DistanceMetric, - key_fn: F, -) where - F: Fn(&T) -> Song, -{ - let mut new_songs: Vec = Vec::with_capacity(songs.len()); - let mut bliss_song = key_fn(&first_song.to_owned()); - - while !songs.is_empty() { - let distances: Array1 = Array::from_shape_fn(songs.len(), |i| { - bliss_song.custom_distance(&key_fn(&songs[i]), &distance) - }); - let idx = distances.argmin().unwrap(); - let song = songs[idx].to_owned(); - bliss_song = key_fn(&songs[idx]).to_owned(); - new_songs.push(song.to_owned()); - songs.retain(|s| s != &song); - } - *songs = new_songs; -} - -/// Remove duplicate songs from a playlist, in place. -/// -/// Two songs are considered duplicates if they either have the same, -/// non-empty title and artist name, or if they are close enough in terms -/// of distance. -/// -/// # Arguments -/// -/// * `songs`: The playlist to remove duplicates from. -/// * `distance_threshold`: The distance threshold under which two songs are -/// considered identical. If `None`, a default value of 0.05 will be used. -pub fn dedup_playlist(songs: &mut Vec, distance_threshold: Option) { - dedup_playlist_custom_distance(songs, distance_threshold, euclidean_distance); -} - -/// Remove duplicate songs from a playlist, in place. -/// -/// Two songs are considered duplicates if they either have the same, -/// non-empty title and artist name, or if they are close enough in terms -/// of distance. -/// -/// Dedup songs with a key extraction function, useful for when you have a -/// structure like `CustomSong { bliss_song: Song, something_else: bool }` you -/// want to deduplicate. -/// -/// # Arguments -/// -/// * `songs`: The playlist to remove duplicates from. -/// * `distance_threshold`: The distance threshold under which two songs are -/// considered identical. If `None`, a default value of 0.05 will be used. -/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`. -pub fn dedup_playlist_by_key(songs: &mut Vec, distance_threshold: Option, key_fn: F) -where - F: Fn(&T) -> Song, -{ - dedup_playlist_custom_distance_by_key(songs, distance_threshold, euclidean_distance, key_fn); -} - -/// Remove duplicate songs from a playlist, in place, using a custom distance -/// metric. -/// -/// Two songs are considered duplicates if they either have the same, -/// non-empty title and artist name, or if they are close enough in terms -/// of distance. -/// -/// # Arguments -/// -/// * `songs`: The playlist to remove duplicates from. -/// * `distance_threshold`: The distance threshold under which two songs are -/// considered identical. If `None`, a default value of 0.05 will be used. -/// * `distance`: A custom distance metric. -pub fn dedup_playlist_custom_distance( - songs: &mut Vec, - distance_threshold: Option, - distance: impl DistanceMetric, -) { - songs.dedup_by(|s1, s2| { - n32(s1.custom_distance(s2, &distance)) < distance_threshold.unwrap_or(0.05) - || (s1.title.is_some() - && s2.title.is_some() - && s1.artist.is_some() - && s2.artist.is_some() - && s1.title == s2.title - && s1.artist == s2.artist) - }); -} - -/// Remove duplicate songs from a playlist, in place, using a custom distance -/// metric. -/// -/// Two songs are considered duplicates if they either have the same, -/// non-empty title and artist name, or if they are close enough in terms -/// of distance. -/// -/// Dedup songs with a key extraction function, useful for when you have a -/// structure like `CustomSong { bliss_song: Song, something_else: bool }` -/// you want to deduplicate. -/// -/// # Arguments -/// -/// * `songs`: The playlist to remove duplicates from. -/// * `distance_threshold`: The distance threshold under which two songs are -/// considered identical. If `None`, a default value of 0.05 will be used. -/// * `distance`: A custom distance metric. -/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`. -pub fn dedup_playlist_custom_distance_by_key( - songs: &mut Vec, - distance_threshold: Option, - distance: impl DistanceMetric, - key_fn: F, -) where - F: Fn(&T) -> Song, -{ - songs.dedup_by(|s1, s2| { - let s1 = key_fn(s1); - let s2 = key_fn(s2); - n32(s1.custom_distance(&s2, &distance)) < distance_threshold.unwrap_or(0.05) - || (s1.title.is_some() - && s2.title.is_some() - && s1.artist.is_some() - && s2.artist.is_some() - && s1.title == s2.title - && s1.artist == s2.artist) - }); -} - -/// Return a list of albums in a `pool` of songs that are similar to -/// songs in `group`, discarding songs that don't belong to an album. -/// It basically makes an "album" playlist from the `pool` of songs. -/// -/// `group` should be ordered by track number. -/// -/// Songs from `group` would usually just be songs from an album, but not -/// necessarily - they are discarded from `pool` no matter what. -/// -/// # Arguments -/// -/// * `group` - A small group of songs, e.g. an album. -/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song -/// library. -/// -/// # Returns -/// -/// A vector of songs, including `group` at the beginning, that you -/// most likely want to plug in your audio player by using something like -/// `ret.map(|song| song.path.to_owned()).collect::>()`. -pub fn closest_album_to_group(group: Vec, pool: Vec) -> BlissResult> { - let mut albums_analysis: HashMap<&str, Array2> = HashMap::new(); - let mut albums = Vec::new(); - - // Remove songs from the group from the pool. - let pool = pool - .into_iter() - .filter(|s| !group.contains(s)) - .collect::>(); - for song in &pool { - if let Some(album) = &song.album { - if let Some(analysis) = albums_analysis.get_mut(album as &str) { - analysis - .push_row(song.analysis.as_arr1().view()) - .map_err(|e| { - BlissError::ProviderError(format!("while computing distances: {e}")) - })?; - } else { - let mut array = Array::zeros((1, song.analysis.as_arr1().len())); - array.assign(&song.analysis.as_arr1()); - albums_analysis.insert(album, array); - } - } - } - let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES)); - for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) { - column.assign(&song.analysis.as_arr1()); - } - let first_analysis = group_analysis - .mean_axis(Axis(0)) - .ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?; - for (album, analysis) in albums_analysis.iter() { - let mean_analysis = analysis - .mean_axis(Axis(0)) - .ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?; - let album = album.to_owned(); - albums.push((album, mean_analysis.to_owned())); - } - - albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis))); - let mut playlist = group; - for (album, _) in albums { - let mut al = pool - .iter() - .filter(|s| s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string()) - .map(|s| s.to_owned()) - .collect::>(); - al.sort_by(|s1, s2| { - let track_number1 = s1 - .track_number - .to_owned() - .unwrap_or_else(|| String::from("")); - let track_number2 = s2 - .track_number - .to_owned() - .unwrap_or_else(|| String::from("")); - if let Ok(x) = track_number1.parse::() { - if let Ok(y) = track_number2.parse::() { - return x.cmp(&y); - } - } - s1.track_number.cmp(&s2.track_number) - }); - playlist.extend_from_slice(&al); - } - Ok(playlist) -} - -/// Return a list of albums in a `pool` of songs that are similar to -/// songs in `group`, discarding songs that don't belong to an album. -/// It basically makes an "album" playlist from the `pool` of songs. -/// -/// `group` should be ordered by track number. -/// -/// Songs from `group` would usually just be songs from an album, but not -/// necessarily - they are discarded from `pool` no matter what. -/// -/// Order songs with a key extraction function, useful for when you have a -/// structure like `CustomSong { bliss_song: Song, something_else: bool }` -/// you want to order. -/// -/// # Arguments -/// -/// * `group` - A small group of songs, e.g. an album. -/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song -/// library. -/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`. -/// -/// # Returns -/// -/// A vector of T, including `group` at the beginning, that you -/// most likely want to plug in your audio player by using something like -/// `ret.map(|song| song.path.to_owned()).collect::>()`. -// TODO: maybe Clone is not needed? -pub fn closest_album_to_group_by_key( - group: Vec, - pool: Vec, - key_fn: F, -) -> BlissResult> -where - F: Fn(&T) -> Song, -{ - let mut albums_analysis: HashMap> = HashMap::new(); - let mut albums = Vec::new(); - - // Remove songs from the group from the pool. - let pool = pool - .into_iter() - .filter(|s| !group.contains(s)) - .collect::>(); - for song in &pool { - let song = key_fn(song); - if let Some(album) = song.album { - if let Some(analysis) = albums_analysis.get_mut(&album as &str) { - analysis - .push_row(song.analysis.as_arr1().view()) - .map_err(|e| { - BlissError::ProviderError(format!("while computing distances: {e}")) - })?; - } else { - let mut array = Array::zeros((1, song.analysis.as_arr1().len())); - array.assign(&song.analysis.as_arr1()); - albums_analysis.insert(album.to_owned(), array); - } - } - } - let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES)); - for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) { - let song = key_fn(song); - column.assign(&song.analysis.as_arr1()); - } - let first_analysis = group_analysis - .mean_axis(Axis(0)) - .ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?; - for (album, analysis) in albums_analysis.iter() { - let mean_analysis = analysis - .mean_axis(Axis(0)) - .ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?; - let album = album.to_owned(); - albums.push((album, mean_analysis.to_owned())); - } - - albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis))); - let mut playlist = group; - for (album, _) in albums { - let mut al = pool - .iter() - .filter(|s| { - let s = key_fn(s); - s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string() - }) - .map(|s| s.to_owned()) - .collect::>(); - al.sort_by(|s1, s2| { - let s1 = key_fn(s1); - let s2 = key_fn(s2); - let track_number1 = s1 - .track_number - .to_owned() - .unwrap_or_else(|| String::from("")); - let track_number2 = s2 - .track_number - .to_owned() - .unwrap_or_else(|| String::from("")); - if let Ok(x) = track_number1.parse::() { - if let Ok(y) = track_number2.parse::() { - return x.cmp(&y); - } - } - s1.track_number.cmp(&s2.track_number) - }); - playlist.extend_from_slice(&al); - } - Ok(playlist) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::Analysis; - use ndarray::arr1; - use std::path::Path; - - #[derive(Debug, Clone, PartialEq)] - struct CustomSong { - something: bool, - bliss_song: Song, - } - - #[test] - fn test_dedup_playlist_custom_distance() { - let first_song = Song { - path: Path::new("path-to-first").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - let first_song_dupe = Song { - path: Path::new("path-to-dupe").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - - let second_song = Song { - path: Path::new("path-to-second").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1., - ]), - title: Some(String::from("dupe-title")), - artist: Some(String::from("dupe-artist")), - ..Default::default() - }; - let third_song = Song { - path: Path::new("path-to-third").to_path_buf(), - title: Some(String::from("dupe-title")), - artist: Some(String::from("dupe-artist")), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1., - ]), - ..Default::default() - }; - let fourth_song = Song { - path: Path::new("path-to-fourth").to_path_buf(), - artist: Some(String::from("no-dupe-artist")), - title: Some(String::from("dupe-title")), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1., - ]), - ..Default::default() - }; - let fifth_song = Song { - path: Path::new("path-to-fourth").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0.001, 1., 1., 1., - ]), - ..Default::default() - }; - - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_custom_distance(&mut playlist, None, euclidean_distance); - assert_eq!( - playlist, - vec![ - first_song.to_owned(), - second_song.to_owned(), - fourth_song.to_owned(), - ], - ); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_custom_distance(&mut playlist, Some(20.), cosine_distance); - assert_eq!(playlist, vec![first_song.to_owned()]); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist(&mut playlist, Some(20.)); - assert_eq!(playlist, vec![first_song.to_owned()]); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist(&mut playlist, None); - assert_eq!( - playlist, - vec![ - first_song.to_owned(), - second_song.to_owned(), - fourth_song.to_owned(), - ] - ); - - let first_song = CustomSong { - bliss_song: first_song, - something: true, - }; - let second_song = CustomSong { - bliss_song: second_song, - something: true, - }; - let first_song_dupe = CustomSong { - bliss_song: first_song_dupe, - something: true, - }; - let third_song = CustomSong { - bliss_song: third_song, - something: true, - }; - let fourth_song = CustomSong { - bliss_song: fourth_song, - something: true, - }; - - let fifth_song = CustomSong { - bliss_song: fifth_song, - something: true, - }; - - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_custom_distance_by_key(&mut playlist, None, euclidean_distance, |s| { - s.bliss_song.to_owned() - }); - assert_eq!( - playlist, - vec![ - first_song.to_owned(), - second_song.to_owned(), - fourth_song.to_owned(), - ], - ); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_custom_distance_by_key(&mut playlist, Some(20.), cosine_distance, |s| { - s.bliss_song.to_owned() - }); - assert_eq!(playlist, vec![first_song.to_owned()]); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_by_key(&mut playlist, Some(20.), |s| s.bliss_song.to_owned()); - assert_eq!(playlist, vec![first_song.to_owned()]); - let mut playlist = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - dedup_playlist_by_key(&mut playlist, None, |s| s.bliss_song.to_owned()); - assert_eq!( - playlist, - vec![ - first_song.to_owned(), - second_song.to_owned(), - fourth_song.to_owned(), - ] - ); - } - - #[test] - fn test_song_to_song() { - let first_song = Song { - path: Path::new("path-to-first").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - let first_song_dupe = Song { - path: Path::new("path-to-dupe").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - - let second_song = Song { - path: Path::new("path-to-second").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1., - ]), - ..Default::default() - }; - let third_song = Song { - path: Path::new("path-to-third").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1., - ]), - ..Default::default() - }; - let fourth_song = Song { - path: Path::new("path-to-fourth").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1., - ]), - ..Default::default() - }; - let mut songs = vec![ - first_song.to_owned(), - third_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - fourth_song.to_owned(), - ]; - song_to_song(&first_song, &mut songs, euclidean_distance); - assert_eq!( - songs, - vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - ], - ); - - let first_song = CustomSong { - bliss_song: first_song, - something: true, - }; - let second_song = CustomSong { - bliss_song: second_song, - something: true, - }; - let first_song_dupe = CustomSong { - bliss_song: first_song_dupe, - something: true, - }; - let third_song = CustomSong { - bliss_song: third_song, - something: true, - }; - let fourth_song = CustomSong { - bliss_song: fourth_song, - something: true, - }; - - let mut songs: Vec = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - second_song.to_owned(), - ]; - - song_to_song_by_key(&first_song, &mut songs, euclidean_distance, |s| { - s.bliss_song.to_owned() - }); - - assert_eq!( - songs, - vec![ - first_song, - first_song_dupe, - second_song, - third_song, - fourth_song, - ], - ); - } - - #[test] - fn test_sort_closest_to_first_song() { - let first_song = Song { - path: Path::new("path-to-first").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - let first_song_dupe = Song { - path: Path::new("path-to-dupe").to_path_buf(), - analysis: Analysis::new([ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., - ]), - ..Default::default() - }; - - let second_song = Song { - path: Path::new("path-to-second").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1., - ]), - ..Default::default() - }; - let third_song = Song { - path: Path::new("path-to-third").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1., - ]), - ..Default::default() - }; - let fourth_song = Song { - path: Path::new("path-to-fourth").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1., - ]), - ..Default::default() - }; - let fifth_song = Song { - path: Path::new("path-to-fifth").to_path_buf(), - analysis: Analysis::new([ - 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1., - ]), - ..Default::default() - }; - - let mut songs = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - closest_to_first_song(&first_song, &mut songs, euclidean_distance); - - let first_song = CustomSong { - bliss_song: first_song, - something: true, - }; - let second_song = CustomSong { - bliss_song: second_song, - something: true, - }; - let first_song_dupe = CustomSong { - bliss_song: first_song_dupe, - something: true, - }; - let third_song = CustomSong { - bliss_song: third_song, - something: true, - }; - let fourth_song = CustomSong { - bliss_song: fourth_song, - something: true, - }; - - let fifth_song = CustomSong { - bliss_song: fifth_song, - something: true, - }; - - let mut songs: Vec = vec![ - first_song.to_owned(), - first_song_dupe.to_owned(), - second_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - fifth_song.to_owned(), - ]; - - closest_to_first_song_by_key(&first_song, &mut songs, euclidean_distance, |s| { - s.bliss_song.to_owned() - }); - - assert_eq!( - songs, - vec![ - first_song, - first_song_dupe, - second_song, - fourth_song, - fifth_song, - third_song - ], - ); - } - - #[test] - fn test_euclidean_distance() { - let a = arr1(&[ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., - ]); - let b = arr1(&[ - 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., - ]); - assert_eq!(euclidean_distance(&a, &b), 4.242640687119285); - - let a = arr1(&[0.5; 20]); - let b = arr1(&[0.5; 20]); - assert_eq!(euclidean_distance(&a, &b), 0.); - assert_eq!(euclidean_distance(&a, &b), 0.); - } - - #[test] - fn test_cosine_distance() { - let a = arr1(&[ - 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., - ]); - let b = arr1(&[ - 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., - ]); - assert_eq!(cosine_distance(&a, &b), 0.7705842661294382); - - let a = arr1(&[0.5; 20]); - let b = arr1(&[0.5; 20]); - assert_eq!(cosine_distance(&a, &b), 0.); - assert_eq!(cosine_distance(&a, &b), 0.); - } - - #[test] - fn test_closest_to_group() { - let first_song = Song { - path: Path::new("path-to-first").to_path_buf(), - analysis: Analysis::new([0.; 20]), - album: Some(String::from("Album")), - artist: Some(String::from("Artist")), - track_number: Some(String::from("01")), - ..Default::default() - }; - - let second_song = Song { - path: Path::new("path-to-second").to_path_buf(), - analysis: Analysis::new([0.1; 20]), - album: Some(String::from("Another Album")), - artist: Some(String::from("Artist")), - track_number: Some(String::from("10")), - ..Default::default() - }; - - let third_song = Song { - path: Path::new("path-to-third").to_path_buf(), - analysis: Analysis::new([10.; 20]), - album: Some(String::from("Album")), - artist: Some(String::from("Another Artist")), - track_number: Some(String::from("02")), - ..Default::default() - }; - - let fourth_song = Song { - path: Path::new("path-to-fourth").to_path_buf(), - analysis: Analysis::new([20.; 20]), - album: Some(String::from("Another Album")), - artist: Some(String::from("Another Artist")), - track_number: Some(String::from("01")), - ..Default::default() - }; - let fifth_song = Song { - path: Path::new("path-to-fifth").to_path_buf(), - analysis: Analysis::new([40.; 20]), - artist: Some(String::from("Third Artist")), - album: None, - ..Default::default() - }; - - let pool = vec![ - first_song.to_owned(), - fourth_song.to_owned(), - third_song.to_owned(), - second_song.to_owned(), - fifth_song.to_owned(), - ]; - let group = vec![first_song.to_owned(), third_song.to_owned()]; - assert_eq!( - vec![ - first_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - second_song.to_owned() - ], - closest_album_to_group(group, pool.to_owned()).unwrap(), - ); - - let first_song = CustomSong { - bliss_song: first_song, - something: true, - }; - let second_song = CustomSong { - bliss_song: second_song, - something: true, - }; - let third_song = CustomSong { - bliss_song: third_song, - something: true, - }; - let fourth_song = CustomSong { - bliss_song: fourth_song, - something: true, - }; - - let fifth_song = CustomSong { - bliss_song: fifth_song, - something: true, - }; - - let pool = vec![ - first_song.to_owned(), - fourth_song.to_owned(), - third_song.to_owned(), - second_song.to_owned(), - fifth_song.to_owned(), - ]; - let group = vec![first_song.to_owned(), third_song.to_owned()]; - assert_eq!( - vec![ - first_song.to_owned(), - third_song.to_owned(), - fourth_song.to_owned(), - second_song.to_owned() - ], - closest_album_to_group_by_key(group, pool.to_owned(), |s| s.bliss_song.to_owned()) - .unwrap(), - ); - } -} diff --git a/src/song.rs b/src/song.rs index 673c7bc..7aa97fd 100644 --- a/src/song.rs +++ b/src/song.rs @@ -11,15 +11,11 @@ extern crate ffmpeg_next as ffmpeg; extern crate ndarray; use crate::chroma::ChromaDesc; -use crate::cue::CueInfo; use crate::misc::LoudnessDesc; -#[cfg(doc)] -use crate::playlist; -use crate::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance, DistanceMetric}; use crate::temporal::BPMDesc; use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc}; -use crate::{BlissError, BlissResult, SAMPLE_RATE}; -use crate::{CHANNELS, FEATURES_VERSION}; +use crate::bliss_lib::{BlissError, BlissResult, SAMPLE_RATE}; +use crate::bliss_lib::{CHANNELS, FEATURES_VERSION}; use ::log::warn; use core::ops::Index; use ffmpeg_next::codec::threading::{Config, Type as ThreadingType}; @@ -35,11 +31,9 @@ use ndarray::{arr1, Array1}; use std::convert::TryInto; use std::fmt; use std::path::Path; -use std::path::PathBuf; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread; -use std::time::Duration; use strum::{EnumCount, IntoEnumIterator}; use strum_macros::{EnumCount, EnumIter}; @@ -48,35 +42,12 @@ use strum_macros::{EnumCount, EnumIter}; /// Simple object used to represent a Song, with its path, analysis, and /// other metadata (artist, genre...) pub struct Song { - /// Song's provided file path - pub path: PathBuf, - /// Song's artist, read from the metadata - pub artist: Option, - /// Song's title, read from the metadata - pub title: Option, - /// Song's album name, read from the metadata - pub album: Option, - /// Song's album's artist name, read from the metadata - pub album_artist: Option, - /// Song's tracked number, read from the metadata - /// TODO normalize this into an integer - pub track_number: Option, - /// Song's genre, read from the metadata (`""` if empty) - pub genre: Option, /// bliss analysis results pub analysis: Analysis, - /// The song's duration - pub duration: Duration, /// Version of the features the song was analyzed with. /// A simple integer that is bumped every time a breaking change /// is introduced in the features. pub features_version: u16, - /// Populated only if the song was extracted from a larger audio file, - /// through the use of a CUE sheet. - /// By default, such a song's path would be - /// `path/to/cue_file.wav/CUE_TRACK00`. Using this field, - /// you can change `song.path` to fit your needs. - pub cue_info: Option, } #[derive(Debug, EnumIter, EnumCount)] @@ -189,95 +160,36 @@ impl Analysis { self.internal_analysis.to_vec() } - /// Compute distance between two analysis using a user-provided distance - /// metric. You most likely want to use `song.custom_distance` directly - /// rather than this function. - /// - /// For this function to be integrated properly with the rest - /// of bliss' parts, it should be a valid distance metric, i.e.: - /// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y - /// 2. For X, Y real vectors, d(X, Y) >= 0 - /// 3. For X, Y real vectors, d(X, Y) = d(Y, X) - /// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y) - /// - /// Note that almost all distance metrics you will find obey these - /// properties, so don't sweat it too much. - pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 { - distance(&self.as_arr1(), &other.as_arr1()) + /// Returns a little endian byte array representing the analysis' features. + pub fn as_bytes(&self) -> [u8; 80] { + let mut result = [0; 80]; + for (i, float) in self.internal_analysis.iter().enumerate() { + let [a, b, c, d] = float.to_le_bytes(); + result[4*i] = a; + result[4*i + 1] = b; + result[4*i + 2] = c; + result[4*i + 3] = d; + } + result + } + + /// Creates an Analysis object from a little endian byte array + pub fn from_bytes(bytes: &[u8]) -> Option { + let floats = bytes + .chunks(4) + .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect::>(); + if floats.len() != NUMBER_FEATURES { + return None; + } + match floats.try_into() { + Ok(arr) => Some(Analysis { internal_analysis: arr }), + Err(_) => None, + } } } impl Song { - #[allow(dead_code)] - /// Compute the distance between the current song and any given - /// Song. - /// - /// The smaller the number, the closer the songs; usually more useful - /// if compared between several songs - /// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is - /// closer to song2 than it is to song3. - /// - /// Currently uses the euclidean distance, but this can change in an - /// upcoming release if another metric performs better. - pub fn distance(&self, other: &Self) -> f32 { - self.analysis - .custom_distance(&other.analysis, euclidean_distance) - } - - /// Compute distance between two songs using a user-provided distance - /// metric. - /// - /// For this function to be integrated properly with the rest - /// of bliss' parts, it should be a valid distance metric, i.e.: - /// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y - /// 2. For X, Y real vectors, d(X, Y) >= 0 - /// 3. For X, Y real vectors, d(X, Y) = d(Y, X) - /// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y) - /// - /// Note that almost all distance metrics you will find obey these - /// properties, so don't sweat it too much. - pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 { - self.analysis.custom_distance(&other.analysis, distance) - } - - /// Orders songs in `pool` by proximity to `self`, using the distance - /// metric `distance` to compute the order. - /// Basically return a playlist from songs in `pool`, starting - /// from `self`, using `distance` (some distance metrics can - /// be found in the [playlist] module). - /// - /// Note that contrary to [Song::closest_from_pool], `self` is NOT added - /// to the beginning of the returned vector. - /// - /// No deduplication is ran either; if you're looking for something easy - /// that works "out of the box", use [Song::closest_from_pool]. - pub fn closest_from_pool_custom( - &self, - pool: Vec, - distance: impl DistanceMetric, - ) -> Vec { - let mut pool = pool; - closest_to_first_song(self, &mut pool, distance); - pool - } - - /// Order songs in `pool` by proximity to `self`. - /// Convenience method to return a playlist from songs in `pool`, - /// starting from `self`. - /// - /// The distance is already chosen, deduplication is ran, and the first song - /// is added to the top of the playlist, to make everything easier. - /// - /// If you want more control over which distance metric is chosen, - /// run deduplication manually, etc, use [Song::closest_from_pool_custom]. - pub fn closest_from_pool(&self, pool: Vec) -> Vec { - let mut playlist = vec![self.to_owned()]; - playlist.extend_from_slice(&pool); - closest_to_first_song(self, &mut playlist, euclidean_distance); - dedup_playlist(&mut playlist, None); - playlist - } - /// Returns a decoded [Song] given a file path, or an error if the song /// could not be analyzed for some reason. /// @@ -295,20 +207,11 @@ impl Song { /// decoding ([DecodingError](BlissError::DecodingError)) or an analysis /// ([AnalysisError](BlissError::AnalysisError)) error. pub fn from_path>(path: P) -> BlissResult { - let raw_song = Song::decode(path.as_ref())?; + let samples = Song::decode(path.as_ref())?; Ok(Song { - path: raw_song.path, - artist: raw_song.artist, - album_artist: raw_song.album_artist, - title: raw_song.title, - album: raw_song.album, - track_number: raw_song.track_number, - genre: raw_song.genre, - duration: raw_song.duration, - analysis: Song::analyze(&raw_song.sample_array)?, + analysis: Song::analyze(&samples)?, features_version: FEATURES_VERSION, - cue_info: None, }) } @@ -430,7 +333,7 @@ impl Song { }) } - pub(crate) fn decode(path: &Path) -> BlissResult { + pub(crate) fn decode(path: &Path) -> BlissResult> { ffmpeg::init().map_err(|e| { BlissError::DecodingError(format!( "ffmpeg init error while decoding file '{}': {:?}.", @@ -439,10 +342,7 @@ impl Song { )) })?; log::set_level(Level::Quiet); - let mut song = InternalSong { - path: path.into(), - ..Default::default() - }; + let mut ictx = ffmpeg::format::input(&path).map_err(|e| { BlissError::DecodingError(format!( "while opening format for file '{}': {:?}.", @@ -468,8 +368,7 @@ impl Song { context.set_threading(Config { kind: ThreadingType::Frame, count: 0, - #[cfg(not(feature = "ffmpeg_6_0"))] - safe: true, + // safe: true, }); let decoder = context.decoder().audio().map_err(|e| { BlissError::DecodingError(format!( @@ -493,42 +392,7 @@ impl Song { (decoder, input.index(), expected_sample_number) }; let sample_array: Vec = Vec::with_capacity(expected_sample_number as usize); - if let Some(title) = ictx.metadata().get("title") { - song.title = match title { - "" => None, - t => Some(t.to_string()), - }; - }; - if let Some(artist) = ictx.metadata().get("artist") { - song.artist = match artist { - "" => None, - a => Some(a.to_string()), - }; - }; - if let Some(album) = ictx.metadata().get("album") { - song.album = match album { - "" => None, - a => Some(a.to_string()), - }; - }; - if let Some(genre) = ictx.metadata().get("genre") { - song.genre = match genre { - "" => None, - g => Some(g.to_string()), - }; - }; - if let Some(track_number) = ictx.metadata().get("track") { - song.track_number = match track_number { - "" => None, - t => Some(t.to_string()), - }; - }; - if let Some(album_artist) = ictx.metadata().get("album_artist") { - song.album_artist = match album_artist { - "" => None, - t => Some(t.to_string()), - }; - }; + let (empty_in_channel_layout, in_channel_layout) = { if decoder.channel_layout() == ChannelLayout::empty() { (true, ChannelLayout::default(decoder.channels().into())) @@ -569,8 +433,7 @@ impl Song { path.display() ); drop(tx); - song.sample_array = child.join().unwrap()?; - return Ok(song); + return Ok(child.join().unwrap()?); } Err(e) => warn!("error while decoding file '{}': {}", path.display(), e), }; @@ -608,8 +471,7 @@ impl Song { path.display() ); drop(tx); - song.sample_array = child.join().unwrap()?; - return Ok(song); + return Ok(child.join().unwrap()?); } Err(e) => warn!("error while decoding {}: {}", path.display(), e), }; @@ -631,26 +493,10 @@ impl Song { } drop(tx); - song.sample_array = child.join().unwrap()?; - let duration_seconds = song.sample_array.len() as f32 / SAMPLE_RATE as f32; - song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64); - Ok(song) + Ok(child.join().unwrap()?) } } -#[derive(Default, Debug)] -pub(crate) struct InternalSong { - pub path: PathBuf, - pub artist: Option, - pub album_artist: Option, - pub title: Option, - pub album: Option, - pub track_number: Option, - pub genre: Option, - pub duration: Duration, - pub sample_array: Vec, -} - fn resample_frame( rx: Receiver