diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 71ce8a9..cd3b15a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,14 +30,16 @@ jobs: 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-2022-02-16 bench --verbose --features=bench --no-run - name: Build examples - run: cargo build --examples --verbose --features=serde + run: cargo build --examples --verbose --features=serde,library - name: Lint - run: cargo clippy --examples --features=serde -- -D warnings + run: cargo clippy --examples --features=serde,library -- -D warnings build-test-lint-windows: name: Windows - build, test and lint diff --git a/Cargo.lock b/Cargo.lock index 426f18e..c8b2bf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.5.3" @@ -19,11 +30,11 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ - "memchr 2.4.1", + "memchr 2.5.0", ] [[package]] @@ -37,9 +48,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "atty" @@ -72,7 +83,7 @@ dependencies = [ "peeking_take_while", "proc-macro2", "quote", - "regex 1.5.5", + "regex 1.6.0", "rustc-hash", "shlex", ] @@ -91,9 +102,11 @@ dependencies = [ "bliss-audio-aubio-rs", "clap", "crossbeam", + "dirs", "env_logger", "ffmpeg-next", "glob", + "indicatif", "lazy_static 1.4.0", "log", "mime_guess", @@ -106,11 +119,14 @@ dependencies = [ "rayon", "rcue", "ripemd160", + "rusqlite", "rustfft", "serde", + "serde_ini", "serde_json", "strum", "strum_macros", + "tempdir", "thiserror", ] @@ -133,42 +149,24 @@ dependencies = [ "fftw-sys", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] -name = "block-padding" -version = "0.1.5" +name = "block-buffer" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ - "byte-tools", + "generic-array", ] -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.4.3" @@ -232,9 +230,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -256,6 +254,29 @@ dependencies = [ "vec_map", ] +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "unicode-width", + "winapi 0.3.9", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -267,9 +288,9 @@ dependencies = [ [[package]] name = "crossbeam" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if", "crossbeam-channel", @@ -281,9 +302,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -291,9 +312,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -302,23 +323,23 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "lazy_static 1.4.0", "memoffset", + "once_cell", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if", "crossbeam-utils", @@ -326,19 +347,29 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", - "lazy_static 1.4.0", + "once_cell", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", ] [[package]] name = "ctor" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb" dependencies = [ "quote", "syn", @@ -346,18 +377,9 @@ dependencies = [ [[package]] name = "diff" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" @@ -365,14 +387,50 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", ] [[package]] name = "either" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "env_logger" @@ -383,15 +441,21 @@ dependencies = [ "atty", "humantime", "log", - "regex 1.5.5", + "regex 1.6.0", "termcolor", ] [[package]] -name = "fake-simd" -version = "0.1.2" +name = "fallible-iterator" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +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" @@ -444,13 +508,11 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] @@ -472,19 +534,16 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.12.4" +name = "fuchsia-cprng" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -492,13 +551,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -512,6 +571,24 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] [[package]] name = "heck" @@ -539,28 +616,39 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indicatif" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b" +dependencies = [ + "console", + "number_prefix", + "unicode-width", ] [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jobserver" @@ -601,9 +689,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.123" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libloading" @@ -616,19 +704,23 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.16" +name = "libsqlite3-sys" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" dependencies = [ - "cfg-if", + "pkg-config", + "vcpkg", ] [[package]] -name = "maplit" -version = "1.0.2" +name = "log" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] [[package]] name = "matrixmultiply" @@ -650,9 +742,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" @@ -687,21 +779,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "ndarray" -version = "0.15.4" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec23e6762830658d2b3d385a75aa212af2f67a4586d4442907144f3bb6a1ca8" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" dependencies = [ "matrixmultiply", - "num-complex 0.4.0", + "num-complex 0.4.2", "num-integer", "num-traits", "rawpointer", @@ -716,7 +808,7 @@ checksum = "f85776816e34becd8bd9540818d7dc77bf28307f3b3dcc51cc82403c6931680c" dependencies = [ "byteorder", "ndarray", - "num-complex 0.4.0", + "num-complex 0.4.2", "num-traits", "py_literal", "zip", @@ -724,9 +816,9 @@ dependencies = [ [[package]] name = "ndarray-stats" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22877ad014bafa2f7dcfa5d556b0c7a52b0546cc98061a1ebef6d1834957b069" +checksum = "af5a8477ac96877b5bd1fd67e0c28736c12943aba24eda92b127e036b0c8f400" dependencies = [ "indexmap", "itertools", @@ -734,7 +826,7 @@ dependencies = [ "noisy_float", "num-integer", "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -752,7 +844,7 @@ version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "memchr 2.4.1", + "memchr 2.5.0", "minimal-lexical", ] @@ -789,18 +881,18 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" dependencies = [ "num-traits", ] [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -808,9 +900,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg", "num-integer", @@ -819,9 +911,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -837,10 +929,16 @@ dependencies = [ ] [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "opaque-debug" @@ -865,18 +963,19 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pest" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" dependencies = [ + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" dependencies = [ "pest", "pest_generator", @@ -884,9 +983,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" dependencies = [ "pest", "pest_meta", @@ -897,13 +996,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" dependencies = [ - "maplit", + "once_cell", "pest", - "sha-1", + "sha1", ] [[package]] @@ -920,32 +1019,32 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pretty_assertions" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89f989ac94207d048d92db058e4f6ec7342b0971fc58d1271ca148b799b3563" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" dependencies = [ - "ansi_term", "ctor", "diff", "output_vt100", + "yansi", ] [[package]] name = "primal-check" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01419cee72c1a1ca944554e23d83e483e1bccf378753344e881de28b5487511d" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" dependencies = [ "num-integer", ] [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -955,7 +1054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1" dependencies = [ "num-bigint", - "num-complex 0.4.0", + "num-complex 0.4.2", "num-traits", "pest", "pest_derive", @@ -963,13 +1062,26 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 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" @@ -978,7 +1090,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -988,14 +1100,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] name = "rand_core" -version = "0.6.3" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -1008,9 +1135,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ "autocfg", "crossbeam-deque", @@ -1020,9 +1147,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -1036,6 +1163,35 @@ 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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "0.1.80" @@ -1051,13 +1207,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.5" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ - "aho-corasick 0.7.18", - "memchr 2.4.1", - "regex-syntax 0.6.25", + "aho-corasick 0.7.19", + "memchr 2.5.0", + "regex-syntax 0.6.27", ] [[package]] @@ -1068,9 +1224,24 @@ checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[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 = "ripemd160" @@ -1080,7 +1251,22 @@ checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr 2.5.0", + "smallvec", ] [[package]] @@ -1105,9 +1291,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "scopeguard" @@ -1117,18 +1303,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -1136,10 +1322,21 @@ dependencies = [ ] [[package]] -name = "serde_json" -version = "1.0.79" +name = "serde_ini" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "eb236687e2bb073a7521c021949be944641e671b8505a94069ca37b656c81139" +dependencies = [ + "result", + "serde", + "void", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -1147,15 +1344,14 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.8.2" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", + "cfg-if", + "cpufeatures", + "digest 0.10.5", ] [[package]] @@ -1164,6 +1360,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + [[package]] name = "strength_reduce" version = "0.2.3" @@ -1196,13 +1398,23 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.91" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", ] [[package]] @@ -1214,6 +1426,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1225,18 +1447,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -1264,11 +1486,12 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi 0.3.9", ] @@ -1290,9 +1513,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "unicase" @@ -1304,22 +1527,22 @@ dependencies = [ ] [[package]] -name = "unicode-segmentation" -version = "1.9.0" +name = "unicode-ident" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "utf8-ranges" @@ -1346,10 +1569,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +name = "void" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +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 = "winapi" @@ -1394,6 +1629,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zip" version = "0.5.13" diff --git a/Cargo.toml b/Cargo.toml index 501c514..19194e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,12 @@ ffmpeg-static = ["ffmpeg-next/static"] 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] ripemd160 = "0.9.0" @@ -44,13 +50,35 @@ thiserror = "1.0.24" bliss-audio-aubio-rs = "0.2.0" strum = "0.21" strum_macros = "0.21" -serde = { version = "1.0", optional = true, features = ["derive"] } rcue = "0.1.1" +# 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.27.0", optional = true } +dirs = { version = "4.0.0", optional = true } +anyhow = { version = "1.0.58", optional = true } +indicatif = { version = "0.17.0", optional = true } + [dev-dependencies] mime_guess = "2.0.3" glob = "0.3.0" anyhow = "1.0.45" -serde_json = "1.0.59" clap = "2.33.3" pretty_assertions = "1.2.1" +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/examples/library.rs b/examples/library.rs new file mode 100644 index 0000000..e6913d8 --- /dev/null +++ b/examples/library.rs @@ -0,0 +1,174 @@ +/// 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. +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::path::{Path, PathBuf}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + #[serde(flatten)] + pub base_config: BaseConfig, + pub music_library_path: PathBuf, +} + +impl Config { + pub fn new( + music_library_path: PathBuf, + config_path: Option, + database_path: Option, + ) -> Result { + let base_config = BaseConfig::new(config_path, database_path)?; + Ok(Self { + base_config, + music_library_path, + }) + } +} + +impl AppConfigTrait for Config { + fn base_config(&self) -> &BaseConfig { + &self.base_config + } +} + +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::>()) + } +} + +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)?; + 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)?; + } 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 new file mode 100644 index 0000000..59bca74 --- /dev/null +++ b/examples/library_extra_info.rs @@ -0,0 +1,194 @@ +/// 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. +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::path::{Path, PathBuf}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + #[serde(flatten)] + pub base_config: BaseConfig, + pub music_library_path: PathBuf, +} + +impl Config { + pub fn new( + music_library_path: PathBuf, + config_path: Option, + database_path: Option, + ) -> Result { + let base_config = BaseConfig::new(config_path, database_path)?; + Ok(Self { + base_config, + music_library_path, + }) + } +} + +impl AppConfigTrait for Config { + fn base_config(&self) -> &BaseConfig { + &self.base_config + } +} + +trait CustomLibrary { + fn song_paths_info(&self) -> Result>; +} + +impl CustomLibrary for Library { + /// Get all songs in the player library + 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)] +struct ExtraInfo { + extension: Option, + file_name: Option, + mime_type: String, + // TODO add mime-type so it's more real +} + +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)?; + 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)?; + } 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 index 1851c4d..5e10840 100644 --- a/examples/playlist.rs +++ b/examples/playlist.rs @@ -1,26 +1,16 @@ -#[cfg(feature = "serde")] use anyhow::Result; -#[cfg(feature = "serde")] use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance}; -#[cfg(feature = "serde")] use bliss_audio::{analyze_paths, Song}; -#[cfg(feature = "serde")] use clap::{App, Arg}; -#[cfg(feature = "serde")] use glob::glob; -#[cfg(feature = "serde")] use std::env; -#[cfg(feature = "serde")] use std::fs; -#[cfg(feature = "serde")] use std::io::BufReader; -#[cfg(feature = "serde")] 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] -#[cfg(feature = "serde")] fn main() -> Result<()> { let matches = App::new("playlist") .version(env!("CARGO_PKG_VERSION")) @@ -103,8 +93,3 @@ fn main() -> Result<()> { } Ok(()) } - -#[cfg(not(feature = "serde"))] -fn main() { - println!("You need the serde feature enabled to run this file."); -} diff --git a/src/lib.rs b/src/lib.rs index dcbf505..934762f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,8 @@ #![warn(rustdoc::missing_doc_code_examples)] mod chroma; pub mod cue; +#[cfg(feature = "library")] +pub mod library; mod misc; pub mod playlist; mod song; diff --git a/src/library.rs b/src/library.rs new file mode 100644 index 0000000..7acc3f7 --- /dev/null +++ b/src/library.rs @@ -0,0 +1,1843 @@ +//! Module containing utilities to manage a SQLite library of [Song]s. +use crate::analyze_paths; +use crate::playlist::euclidean_distance; +use anyhow::{bail, Context, Result}; +#[cfg(not(test))] +use dirs::data_local_dir; +use indicatif::{ProgressBar, ProgressStyle}; +use noisy_float::prelude::*; +use rusqlite::params; +use rusqlite::Connection; +use rusqlite::Row; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::Mutex; + +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; + + // 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(&self)?) + } + + /// 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(()) + } +} + +/// Actual configuration trait that will be used. +pub trait ConfigTrait: AppConfigTrait { + /// Do some specific configuration things. + fn do_config_things(&self) { + let config = self.base_config(); + config.do_config_things() + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +/// The minimum configuration an application needs to work with +/// a [Library]. +pub struct BaseConfig { + config_path: PathBuf, + database_path: PathBuf, +} + +impl BaseConfig { + pub(crate) fn get_default_data_folder() -> Result { + match env::var("XDG_DATA_HOME") { + Ok(path) => Ok(Path::new(&path).join("bliss-rs")), + Err(_) => { + Ok( + data_local_dir() + .with_context(|| "No suitable path found to store bliss' song database. Consider specifying such a path.")? + .join("bliss-rs") + ) + }, + } + } + + /// 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. + pub fn new(config_path: Option, database_path: 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")) + } + }; + Ok(Self { + config_path, + database_path, + }) + } + + fn do_config_things(&self) {} +} + +impl ConfigTrait for App {} +impl AppConfigTrait for BaseConfig { + fn base_config(&self) -> &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 simple playlist +// TODO add logging statement +// TODO replace String by pathbufs / the ref thing +// TODO concrete examples +// TODO example LibrarySong without any extra_info +// TODO maybe return number of elements updated / deleted / whatev in analysis +// functions? +// TODO manage bliss feature version +impl Library { + /// Create a new [Library] object from the given [Config] struct, + /// 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]. + pub fn new(config: Config) -> Result { + 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, + 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)?.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)?; + Ok(Library { + config, + sqlite_conn: Arc::new(Mutex::new(sqlite_conn)), + }) + } + + /// 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, + ) -> Result + where + BaseConfig: Into, + { + let base = BaseConfig::new(config_path, database_path)?; + 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()?; + songs.sort_by_cached_key(|song| n32(first_song.bliss_song.distance(&song.bliss_song))); + songs.truncate(playlist_length); + songs.dedup_by(|s1, s2| { + n32(s1 + .bliss_song + .custom_distance(&s2.bliss_song, &euclidean_distance)) + < 0.05 + || (s1.bliss_song.title.is_some() + && s2.bliss_song.title.is_some() + && s1.bliss_song.artist.is_some() + && s2.bliss_song.artist.is_some() + && s1.bliss_song.title == s2.bliss_song.title + && s1.bliss_song.artist == s2.bliss_song.artist) + }); + Ok(songs) + } + + /// 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. + pub fn update_library(&mut self, paths: Vec, 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, 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. + pub fn update_library_extra_info( + &mut self, + paths_extra_info: Vec<(String, T)>, + show_progress_bar: bool, + ) -> Result<()> { + self.update_library_convert_extra_info( + paths_extra_info, + 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 [update_library_extra_info]. + /// + /// `paths_extra_info` is a tuple made out of song paths, along + /// with any extra info you want to store for each song. + /// + /// `convert_extra_info` is a function that you should specify + /// to convert that extra info to something serializable. + pub fn update_library_convert_extra_info( + &mut self, + paths_extra_info: Vec<(String, U)>, + 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| x.unwrap()) + .collect::>(); + return_value + }; + + 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`. + /// + /// Use this function if you don't have any extra data to bundle with each song. + 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. + pub fn analyze_paths_extra_info( + &mut self, + paths_extra_info: Vec<(String, 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 + /// [analyze_paths_extra_info]. + /// + /// `paths_extra_info` is a tuple made out of song paths, along + /// with any extra info you want to store for each song. + /// + /// `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( + &mut self, + paths_extra_info: Vec<(String, 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().collect(); + let results = analyze_paths(paths_extra_info.keys()); + 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)); + } + match result { + Ok(song) => { + let extra = paths_extra_info.remove(&path).unwrap(); + let e = convert_extra_info(extra, &song, self); + let library_song = LibrarySong:: { + bliss_song: song, + extra_info: e, + }; + self.store_song(&library_song)?; + success_count += 1; + } + Err(e) => { + log::error!( + "Analysis of song '{}' failed: {} The error has been stored.", + path, + e + ); + + self.store_failed_song(path, e)?; + failure_count += 1; + } + }; + pb.inc(1); + } + pb.finish_with_message(format!( + "Analyzed {} song(s) successfully. {} Failure(s).", + success_count, failure_count + )); + + log::info!( + "Analyzed {} song(s) successfully. {} Failure(s).", + success_count, + failure_count, + ); + + Ok(()) + } + + /// 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 allow to specify the version? + // TODO maybe the error should make the song id / song path + // accessible easily? + pub fn songs_from_library( + &self, + ) -> Result>> { + let connection = self + .sqlite_conn + .lock() + .map_err(|e| BlissError::ProviderError(e.to_string()))?; + let mut songs_statement = connection.prepare( + " + select + path, artist, title, album, album_artist, + track_number, genre, duration, version, extra_info, id + from song where analyzed = true and version = ? order by id + ", + )?; + let mut features_statement = connection.prepare( + " + 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 song_rows = songs_statement.query_map([FEATURES_VERSION], |row| { + Ok((row.get(10)?, Self::_song_from_row_closure(row)?)) + })?; + let feature_rows = features_statement + .query_map([FEATURES_VERSION], |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) + } + + fn _song_from_row_closure( + row: &Row, + ) -> Result, RusqliteError> { + let path: String = row.get(0)?; + 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: None, + }; + + let serialized: String = row.get(9).unwrap(); + let extra_info = serde_json::from_str(&serialized).unwrap(); + Ok(LibrarySong { + bliss_song: song, + extra_info, + }) + } + + /// Get a LibrarySong from a given file path. + 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 + 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() + .into_iter() + .map(|x| x.unwrap()) + .collect::>() + .try_into() + .map_err(|_| { + BlissError::ProviderError(format!( + "song has more or less than {} features", + NUMBER_FEATURES + )) + })?, + }; + song.bliss_song.analysis = analysis_vector; + Ok(song) + } + + /// Store a [Song] in the database, overidding any existing + /// song with the same path by that one. + 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; + tx.execute( + " + insert into song ( + path, artist, title, album, album_artist, + duration, track_number, genre, analyzed, version, extra_info + ) + values ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11 + ) + 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 + ", + 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()))?, + ], + ) + .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: String, e: BlissError) -> Result<()> { + self.sqlite_conn + .lock() + .unwrap() + .execute( + " + insert or replace into song (path, error) values (?1, ?2) + ", + [song_path, 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_song(&mut self, song_path: String) -> Result<()> { + let count = self + .sqlite_conn + .lock() + .unwrap() + .execute( + " + delete from song where path = ?1; + ", + [song_path.to_owned()], + ) + .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, + ))); + } + Ok(()) + } +} + +#[cfg(test)] +fn data_local_dir() -> Option { + Some(PathBuf::from("/local/directory")) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{Analysis, NUMBER_FEATURES}; + 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 + } + } + + // 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, + ), + ) { + 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)).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("01".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 Album5001".into()), + album_artist: Some("An Album Artist5001".into()), + track_number: Some("04".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 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 + ) values ( + 1001, '/path/to/song1001', 'Artist1001', 'Title1001', 'An Album1001', + 'An Album Artist1001', '01', 'Electronica1001', 310, true, + 1, '{\"ignore\": true, \"metadata_bliss_does_not_have\": + \"/path/to/charlie1001\"}' + ), + ( + 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\"}' + ), + ( + 3001, '/path/to/song3001', null, null, null, + null, null, null, null, false, + 1, '{}' + ), + ( + 4001, '/path/to/song4001', 'Artist4001', 'Title4001', 'An Album4001', + 'An Album Artist4001', '03', 'Electronica4001', 510, true, + 0, '{\"ignore\": false, \"metadata_bliss_does_not_have\": + \"/path/to/charlie4001\"}' + ), + ( + 5001, '/path/to/song5001', 'Artist5001', 'Title5001', 'An Album5001', + 'An Album Artist5001', '04', 'Electronica5001', 610, true, + 1, '{\"ignore\": false, \"metadata_bliss_does_not_have\": + \"/path/to/charlie5001\"}' + ); + ", + [], + ) + .unwrap(); + for index in 0..NUMBER_FEATURES { + connection + .execute( + " + insert into feature(song_id, feature, feature_index) + values (1001, ?1, ?2), (2001, ?3, ?2), (3001, ?4, ?2), (5001, ?5, ?2); + ", + params![ + index as f32 / 10., + index, + index as f32 + 10., + index as f32 / 10. + 1., + index as f32 / 2. + ], + ) + .unwrap(); + } + } + (library, config_dir, (first_song, second_song, third_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 + from song where path=? + ", + params![song_path], + |row| { + let path: String = row.get(0)?; + 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: None, + }; + + 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 probably does not exist 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 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/song5001", + "/path/to/song1001" + ], + songs + .into_iter() + .map(|s| s.bliss_song.path.to_string_lossy().to_string()) + .collect::>(), + ) + } + + #[test] + fn test_library_delete_song_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_song("not-existing".into()).is_err()); + } + + #[test] + fn test_library_delete_song() { + 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_song(String::from("/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_analyze_paths() { + let (mut library, _temp_dir, _) = setup_test_library(); + + let paths = vec![ + "./data/s16_mono_22_5kHz.flac".into(), + "./data/s16_stereo_22_5kHz.flac".into(), + "non-existing".into(), + ]; + 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); + } + + #[test] + fn test_analyze_paths_convert_extra_info() { + let (mut library, _temp_dir, _) = setup_test_library(); + + let paths = vec![ + ("./data/s16_mono_22_5kHz.flac".into(), true), + ("./data/s16_stereo_22_5kHz.flac".into(), false), + ("non-existing".into(), 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); + } + + #[test] + fn test_analyze_paths_extra_info() { + let (mut library, _temp_dir, _) = setup_test_library(); + + let paths = vec![ + ( + "./data/s16_mono_22_5kHz.flac".into(), + ExtraInfo { + ignore: true, + metadata_bliss_does_not_have: String::from("hey"), + }, + ), + ( + "./data/s16_stereo_22_5kHz.flac".into(), + ExtraInfo { + ignore: false, + metadata_bliss_does_not_have: String::from("hello"), + }, + ), + ( + "non-existing".into(), + 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(); + + for input in vec![ + ("./data/s16_mono_22_5kHz.flac".into(), true), + ("./data/s16_mono_22_5khz.flac".into(), false), + ] + .into_iter() + { + let paths = vec![input.to_owned()]; + library + .update_library_convert_extra_info::( + paths.to_owned(), + 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); + } + } + + fn _get_song_analyzed(connection: MutexGuard, path: String) -> bool { + let mut stmt = connection + .prepare( + " + select + analyzed from song + where song.path = ? + ", + ) + .unwrap(); + stmt.query_row([path], |row| row.get(0)).unwrap() + } + + #[test] + fn test_update_library() { + let (mut library, _temp_dir, _) = setup_test_library(); + + { + 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())); + } + + let paths = vec![ + "./data/s16_mono_22_5kHz.flac".into(), + "./data/s16_stereo_22_5kHz.flac".into(), + "/path/to/song4001".into(), + "non-existing".into(), + ]; + library.update_library(paths.to_owned(), false).unwrap(); + library.update_library(paths.to_owned(), 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())); + } + } + + #[test] + fn test_update_extra_info() { + let (mut library, _temp_dir, _) = setup_test_library(); + + { + 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())); + } + + let paths = vec![ + ("./data/s16_mono_22_5kHz.flac".into(), true), + ("./data/s16_stereo_22_5kHz.flac".into(), false), + ("/path/to/song4001".into(), false), + ("non-existing".into(), false), + ]; + library + .update_library_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![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())); + } + } + + #[test] + fn test_update_convert_extra_info() { + let (mut library, _temp_dir, _) = setup_test_library(); + + { + 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())); + } + + let paths = vec![ + ("./data/s16_mono_22_5kHz.flac".into(), true), + ("./data/s16_stereo_22_5kHz.flac".into(), false), + ("/path/to/song4001".into(), false), + ("non-existing".into(), false), + ]; + library + .update_library_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); + { + 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())); + } + } + + #[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".into(), + 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(), 3); + assert_eq!( + expected_library_songs, + ( + library_songs[0].to_owned(), + library_songs[1].to_owned(), + library_songs[2].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(); + assert_eq!( + config_content, + format!( + "{{\"config_path\":\"{}\",\"database_path\":\"{}\"}}", + library.config.base_config().config_path.display(), + library.config.base_config().database_path.display(), + ) + ); + } + + #[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)).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)).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(), + ); + } +} diff --git a/src/song.rs b/src/song.rs index 53807ae..d534672 100644 --- a/src/song.rs +++ b/src/song.rs @@ -62,6 +62,7 @@ pub struct Song { /// 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,