diff --git a/Cargo.lock b/Cargo.lock index 0fd4161..e62c6a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -109,6 +115,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -226,11 +241,29 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "bumpalo" @@ -238,6 +271,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -326,6 +365,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.1" @@ -361,6 +415,66 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -370,6 +484,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -390,11 +516,20 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "equivalent" @@ -412,6 +547,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -430,6 +587,17 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -458,6 +626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -466,6 +635,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + [[package]] name = "futures-sink" version = "0.3.32" @@ -485,11 +682,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -561,6 +771,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -570,12 +782,54 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -904,6 +1158,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -917,6 +1174,34 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -935,6 +1220,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -968,6 +1262,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1022,6 +1326,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sqlx", "thiserror", "tokio", "tonic", @@ -1042,12 +1347,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1055,6 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1069,6 +1411,44 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1111,6 +1491,39 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "potential_utf" version = "0.1.5" @@ -1357,6 +1770,24 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -1448,6 +1879,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1514,6 +1965,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -1599,6 +2056,28 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1614,6 +2093,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.12" @@ -1625,6 +2114,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1646,12 +2138,238 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2032,18 +2750,45 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2104,6 +2849,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2143,6 +2894,12 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.120" @@ -2261,6 +3018,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2320,13 +3087,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2338,34 +3114,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2378,24 +3187,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 4a6070c..3bc4636 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ roxmltree = "0.20" base64 = "0.22" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4", features = ["derive"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] } tonic = "0.12" prost = "0.13" diff --git a/containers/docker-compose.yml b/containers/docker-compose.yml index e5977c2..c6308fd 100644 --- a/containers/docker-compose.yml +++ b/containers/docker-compose.yml @@ -9,6 +9,7 @@ services: POSTGRES_DB: music_aggregator volumes: - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: - "5433:5432" healthcheck: diff --git a/containers/init.sql b/containers/init.sql new file mode 100644 index 0000000..83bd92b --- /dev/null +++ b/containers/init.sql @@ -0,0 +1,225 @@ +-- Music Aggregator Database Schema +-- Based on docs/erd.puml + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ══════════════════════════════════════════════════════════════ +-- CONFIGURATION +-- ══════════════════════════════════════════════════════════════ + +CREATE TABLE quality_profiles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL UNIQUE, + cutoff INT NOT NULL DEFAULT 0, + items JSONB NOT NULL DEFAULT '[]', + upgrade_allowed BOOLEAN NOT NULL DEFAULT true +); + +CREATE TABLE metadata_profiles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL UNIQUE, + primary_album_types JSONB NOT NULL DEFAULT '["Album", "EP"]', + secondary_album_types JSONB NOT NULL DEFAULT '[]', + release_statuses JSONB NOT NULL DEFAULT '["Official"]' +); + +CREATE TABLE root_folders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + path TEXT NOT NULL UNIQUE, + default_quality_profile_id UUID REFERENCES quality_profiles(id), + default_metadata_profile_id UUID REFERENCES metadata_profiles(id) +); + +CREATE TABLE indexers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + implementation TEXT NOT NULL, + settings JSONB NOT NULL DEFAULT '{}', + enable_rss BOOLEAN NOT NULL DEFAULT true, + enable_search BOOLEAN NOT NULL DEFAULT true, + priority INT NOT NULL DEFAULT 25 +); + +CREATE TABLE download_clients ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + implementation TEXT NOT NULL, + settings JSONB NOT NULL DEFAULT '{}', + protocol TEXT NOT NULL DEFAULT 'torrent', + priority INT NOT NULL DEFAULT 1, + enabled BOOLEAN NOT NULL DEFAULT true +); + +-- ══════════════════════════════════════════════════════════════ +-- CORE MUSIC ENTITIES +-- ══════════════════════════════════════════════════════════════ + +CREATE TABLE artist_metadata ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + foreign_artist_id TEXT UNIQUE, + name TEXT NOT NULL, + sort_name TEXT, + disambiguation TEXT, + artist_type TEXT, + status TEXT, + overview TEXT, + images JSONB NOT NULL DEFAULT '[]', + links JSONB NOT NULL DEFAULT '[]', + genres JSONB NOT NULL DEFAULT '[]', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE artists ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + metadata_id UUID NOT NULL REFERENCES artist_metadata(id) ON DELETE CASCADE, + quality_profile_id UUID REFERENCES quality_profiles(id), + metadata_profile_id UUID REFERENCES metadata_profiles(id), + root_folder_id UUID REFERENCES root_folders(id), + path TEXT, + monitored BOOLEAN NOT NULL DEFAULT true, + monitor_new_items TEXT NOT NULL DEFAULT 'all', + last_info_sync TIMESTAMPTZ, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE albums ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + artist_metadata_id UUID NOT NULL REFERENCES artist_metadata(id) ON DELETE CASCADE, + foreign_album_id TEXT UNIQUE, + title TEXT NOT NULL, + clean_title TEXT, + disambiguation TEXT, + overview TEXT, + album_type TEXT, + release_date DATE, + images JSONB NOT NULL DEFAULT '[]', + genres JSONB NOT NULL DEFAULT '[]', + monitored BOOLEAN NOT NULL DEFAULT true, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE album_releases ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE, + foreign_release_id TEXT UNIQUE, + title TEXT NOT NULL, + status TEXT, + duration_ms INT, + release_date DATE, + country TEXT[], + label TEXT[], + format TEXT, + track_count INT, + monitored BOOLEAN NOT NULL DEFAULT true +); + +CREATE TABLE track_files ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + album_id UUID NOT NULL REFERENCES albums(id) ON DELETE CASCADE, + path TEXT NOT NULL, + relative_path TEXT NOT NULL, + size BIGINT NOT NULL DEFAULT 0, + file_hash TEXT, + audio_hash TEXT, + quality JSONB NOT NULL DEFAULT '{}', + media_info JSONB NOT NULL DEFAULT '{}', + scene_name TEXT, + release_group TEXT, + date_added TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE tracks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + album_release_id UUID NOT NULL REFERENCES album_releases(id) ON DELETE CASCADE, + artist_metadata_id UUID NOT NULL REFERENCES artist_metadata(id) ON DELETE CASCADE, + track_file_id UUID REFERENCES track_files(id) ON DELETE SET NULL, + foreign_track_id TEXT UNIQUE, + title TEXT NOT NULL, + track_number INT NOT NULL DEFAULT 1, + disc_number INT NOT NULL DEFAULT 1, + duration_ms INT, + explicit BOOLEAN NOT NULL DEFAULT false +); + +-- ══════════════════════════════════════════════════════════════ +-- DOWNLOAD TRACKING +-- ══════════════════════════════════════════════════════════════ + +CREATE TABLE wanted_albums ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + album_id UUID NOT NULL UNIQUE REFERENCES albums(id) ON DELETE CASCADE, + priority INT NOT NULL DEFAULT 0, + search_count INT NOT NULL DEFAULT 0, + last_searched_at TIMESTAMPTZ, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE download_queue ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + artist_id UUID REFERENCES artists(id) ON DELETE SET NULL, + album_id UUID REFERENCES albums(id) ON DELETE SET NULL, + download_id TEXT, + title TEXT NOT NULL, + size BIGINT NOT NULL DEFAULT 0, + size_left BIGINT NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'queued', + progress REAL NOT NULL DEFAULT 0.0, + error_message TEXT, + protocol TEXT NOT NULL DEFAULT 'torrent', + indexer TEXT, + download_client TEXT, + torrent_hash TEXT, + output_path TEXT, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ +); + +CREATE TABLE blocklist ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + artist_id UUID NOT NULL REFERENCES artists(id) ON DELETE CASCADE, + album_id UUID REFERENCES albums(id) ON DELETE CASCADE, + source_title TEXT NOT NULL, + quality JSONB NOT NULL DEFAULT '{}', + size BIGINT NOT NULL DEFAULT 0, + protocol TEXT, + indexer TEXT, + message TEXT, + torrent_hash TEXT, + date TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ══════════════════════════════════════════════════════════════ +-- INDEXES +-- ══════════════════════════════════════════════════════════════ + +CREATE INDEX idx_artist_metadata_name ON artist_metadata(name); +CREATE INDEX idx_artist_metadata_foreign_id ON artist_metadata(foreign_artist_id); +CREATE INDEX idx_albums_artist ON albums(artist_metadata_id); +CREATE INDEX idx_albums_foreign_id ON albums(foreign_album_id); +CREATE INDEX idx_albums_release_date ON albums(release_date); +CREATE INDEX idx_album_releases_album ON album_releases(album_id); +CREATE INDEX idx_tracks_release ON tracks(album_release_id); +CREATE INDEX idx_tracks_artist ON tracks(artist_metadata_id); +CREATE INDEX idx_track_files_album ON track_files(album_id); +CREATE INDEX idx_track_files_hash ON track_files(file_hash); +CREATE INDEX idx_track_files_audio_hash ON track_files(audio_hash); +CREATE INDEX idx_wanted_albums_priority ON wanted_albums(priority DESC); +CREATE INDEX idx_download_queue_status ON download_queue(status); +CREATE INDEX idx_download_queue_album ON download_queue(album_id); +CREATE INDEX idx_blocklist_artist ON blocklist(artist_id); +CREATE INDEX idx_blocklist_torrent ON blocklist(torrent_hash); + +-- ══════════════════════════════════════════════════════════════ +-- DEFAULT DATA +-- ══════════════════════════════════════════════════════════════ + +INSERT INTO quality_profiles (name, cutoff, items, upgrade_allowed) VALUES +('Any', 0, '[]', true), +('Lossless', 1, '[{"quality": "FLAC", "allowed": true}, {"quality": "ALAC", "allowed": true}]', true), +('Standard', 2, '[{"quality": "MP3-320", "allowed": true}, {"quality": "MP3-VBR-V0", "allowed": true}]', true); + +INSERT INTO metadata_profiles (name, primary_album_types, secondary_album_types, release_statuses) VALUES +('Standard', '["Album", "EP"]', '[]', '["Official"]'), +('All', '["Album", "EP", "Single", "Broadcast", "Other"]', '["Compilation", "Soundtrack", "Spokenword", "Interview", "Audiobook", "Live", "Remix", "DJ-mix", "Mixtape/Street", "Demo"]', '["Official", "Promotional", "Bootleg"]'); diff --git a/src/api/library_controller.rs b/src/api/library_controller.rs new file mode 100644 index 0000000..8570aed --- /dev/null +++ b/src/api/library_controller.rs @@ -0,0 +1,124 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + routing::get, + Json, Router, +}; +use serde::Serialize; +use uuid::Uuid; + +use crate::services::{AlbumRow, AlbumWithArtistRow, ArtistMetadataRow}; +use crate::AppState; + +pub fn routes() -> Router { + Router::new() + .route("/artists", get(list_artists)) + .route("/artists/{id}/albums", get(list_artist_albums)) + .route("/albums", get(list_albums)) + .route("/stats", get(library_stats)) +} + +#[derive(Serialize)] +struct ArtistsResponse { + artists: Vec, + total: usize, +} + +async fn list_artists( + State(state): State, +) -> Result, (StatusCode, String)> { + let state = state.read().await; + + let db = state.db_service.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + "database not connected".to_string(), + ))?; + + let artists = db + .list_artists() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let total = artists.len(); + Ok(Json(ArtistsResponse { artists, total })) +} + +#[derive(Serialize)] +struct ArtistAlbumsResponse { + albums: Vec, + total: usize, +} + +async fn list_artist_albums( + State(state): State, + Path(id): Path, +) -> Result, (StatusCode, String)> { + let state = state.read().await; + + let db = state.db_service.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + "database not connected".to_string(), + ))?; + + let albums = db + .list_albums_by_artist(id) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let total = albums.len(); + Ok(Json(ArtistAlbumsResponse { albums, total })) +} + +#[derive(Serialize)] +struct AlbumsResponse { + albums: Vec, + total: usize, +} + +async fn list_albums( + State(state): State, +) -> Result, (StatusCode, String)> { + let state = state.read().await; + + let db = state.db_service.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + "database not connected".to_string(), + ))?; + + let albums = db + .list_all_albums() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let total = albums.len(); + Ok(Json(AlbumsResponse { albums, total })) +} + +#[derive(Serialize)] +struct LibraryStats { + artists: i64, + albums: i64, +} + +async fn library_stats( + State(state): State, +) -> Result, (StatusCode, String)> { + let state = state.read().await; + + let db = state.db_service.as_ref().ok_or(( + StatusCode::SERVICE_UNAVAILABLE, + "database not connected".to_string(), + ))?; + + let artists = db + .count_artists() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let albums = db + .count_albums() + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(LibraryStats { artists, albums })) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 855b09e..a11d063 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ mod indexer_controller; +mod library_controller; mod metadata_controller; mod sync_controller; mod torrent_controller; @@ -29,6 +30,7 @@ pub fn routes(state: AppState) -> Router { .nest("/torrents", torrent_controller::routes()) .nest("/metadata", metadata_controller::routes()) .nest("/sync", sync_controller::routes()) + .nest("/library", library_controller::routes()) .with_state(state) } diff --git a/src/api/sync_controller.rs b/src/api/sync_controller.rs index 50744c5..6b7c904 100644 --- a/src/api/sync_controller.rs +++ b/src/api/sync_controller.rs @@ -1,29 +1,46 @@ use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; use serde::Deserialize; -use crate::services::{ArtistSyncResult, DownloadService}; +use crate::services::{DownloadService, SyncOptions, SyncResult}; use crate::AppState; pub fn routes() -> Router { - Router::new().route("/artist", post(sync_artist)) + Router::new().route("/", post(sync)) } #[derive(Debug, Deserialize)] -pub struct SyncArtistRequest { - pub name: String, +pub struct SyncRequest { + pub artist: String, + pub album: Option, + #[serde(default = "default_true")] + pub download: bool, + #[serde(default = "default_true")] + pub store: bool, } -async fn sync_artist( +fn default_true() -> bool { + true +} + +async fn sync( State(state): State, - Json(req): Json, -) -> Result, (StatusCode, String)> { + Json(req): Json, +) -> Result, (StatusCode, String)> { let state = state.read().await; - let result = DownloadService::sync_artist( - &req.name, + let options = SyncOptions { + artist: req.artist, + album: req.album, + download: req.download, + store: req.store, + }; + + let result = DownloadService::sync( + options, &state.metadata_service, &state.indexer_service, &state.torrent_service, + state.db_service.as_ref(), ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e))?; diff --git a/src/lib.rs b/src/lib.rs index 09c1e56..c07ed4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ pub struct AppServices { pub indexer_service: services::IndexerService, pub torrent_service: services::TorrentService, pub metadata_service: services::MetadataService, + pub db_service: Option, config_path: String, } @@ -22,6 +23,7 @@ impl AppServices { indexer_service: services::IndexerService, torrent_service: services::TorrentService, metadata_service: services::MetadataService, + db_service: Option, config_path: String, ) -> Self { Self { @@ -29,6 +31,7 @@ impl AppServices { indexer_service, torrent_service, metadata_service, + db_service, config_path, } } diff --git a/src/main.rs b/src/main.rs index 0fb02bd..a59882e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use axum::Router; use clap::Parser; use music_agregator::{ api, config, - services::{IndexerService, MetadataService, TorrentService}, + services::{DbService, IndexerService, MetadataService, TorrentService}, AppServices, AppState, }; use tower_http::cors::{Any, CorsLayer}; @@ -91,10 +91,25 @@ async fn main() { } } + let db_service = match DbService::new(&config.database.url).await { + Ok(svc) => { + tracing::info!("connected to database"); + Some(svc) + } + Err(e) => { + tracing::warn!( + "failed to connect to database: {} (continuing without db)", + e + ); + None + } + }; + let state: AppState = Arc::new(RwLock::new(AppServices::new( indexer_service, torrent_service, metadata_service, + db_service, args.config.clone(), ))); diff --git a/src/services/db_service.rs b/src/services/db_service.rs new file mode 100644 index 0000000..f9858f5 --- /dev/null +++ b/src/services/db_service.rs @@ -0,0 +1,211 @@ +use sqlx::{postgres::PgPoolOptions, FromRow, PgPool}; +use uuid::Uuid; + +use crate::metadata::proto::{Album, Artist}; + +#[derive(Clone)] +pub struct DbService { + pool: PgPool, +} + +impl DbService { + pub async fn new(database_url: &str) -> Result { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(database_url) + .await?; + + Ok(Self { pool }) + } + + pub async fn upsert_artist_metadata(&self, artist: &Artist) -> Result { + let id = Uuid::parse_str(&artist.id).unwrap_or_else(|_| Uuid::new_v4()); + let genres: serde_json::Value = serde_json::json!(artist + .genres + .iter() + .map(|g| serde_json::json!({"id": g.id, "name": g.name})) + .collect::>()); + let links: serde_json::Value = serde_json::json!(artist + .external_ids + .iter() + .map( + |e| serde_json::json!({"source": e.source, "source_id": e.source_id, "url": e.url}) + ) + .collect::>()); + + let row: (Uuid,) = sqlx::query_as( + r#" + INSERT INTO artist_metadata ( + id, foreign_artist_id, name, sort_name, disambiguation, + artist_type, status, overview, genres, links, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) + ON CONFLICT (foreign_artist_id) DO UPDATE SET + name = EXCLUDED.name, + sort_name = EXCLUDED.sort_name, + artist_type = EXCLUDED.artist_type, + overview = EXCLUDED.overview, + genres = EXCLUDED.genres, + links = EXCLUDED.links, + updated_at = NOW() + RETURNING id + "#, + ) + .bind(id) + .bind(&artist.id) + .bind(&artist.name) + .bind(&artist.sort_name) + .bind(&artist.description) + .bind(&artist.artist_type) + .bind("active") + .bind(&artist.description) + .bind(&genres) + .bind(&links) + .fetch_one(&self.pool) + .await?; + + Ok(row.0) + } + + pub async fn upsert_album( + &self, + album: &Album, + artist_metadata_id: Uuid, + ) -> Result { + let id = Uuid::parse_str(&album.id).unwrap_or_else(|_| Uuid::new_v4()); + let genres: serde_json::Value = serde_json::json!(album + .genres + .iter() + .map(|g| serde_json::json!({"id": g.id, "name": g.name})) + .collect::>()); + let images: serde_json::Value = serde_json::json!([]); + let release_date = chrono::NaiveDate::parse_from_str(&album.release_date, "%Y-%m-%d").ok(); + let clean_title = album + .title + .to_lowercase() + .replace(|c: char| !c.is_alphanumeric(), ""); + + let row: (Uuid,) = sqlx::query_as( + r#" + INSERT INTO albums ( + id, artist_metadata_id, foreign_album_id, title, clean_title, + overview, album_type, release_date, images, genres + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (foreign_album_id) DO UPDATE SET + title = EXCLUDED.title, + album_type = EXCLUDED.album_type, + release_date = EXCLUDED.release_date, + genres = EXCLUDED.genres + RETURNING id + "#, + ) + .bind(id) + .bind(artist_metadata_id) + .bind(&album.id) + .bind(&album.title) + .bind(&clean_title) + .bind("") + .bind(&album.album_type) + .bind(release_date) + .bind(&images) + .bind(&genres) + .fetch_one(&self.pool) + .await?; + + Ok(row.0) + } + + pub async fn list_artists(&self) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at + FROM artist_metadata + ORDER BY name + "#, + ) + .fetch_all(&self.pool) + .await + } + + pub async fn list_albums_by_artist( + &self, + artist_metadata_id: Uuid, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT id, artist_metadata_id, foreign_album_id, title, album_type, release_date, monitored, added_at + FROM albums + WHERE artist_metadata_id = $1 + ORDER BY release_date DESC NULLS LAST + "#, + ) + .bind(artist_metadata_id) + .fetch_all(&self.pool) + .await + } + + pub async fn list_all_albums(&self) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT + a.id, a.foreign_album_id, a.title, a.album_type, a.release_date, a.monitored, a.added_at, + am.id as artist_id, am.name as artist_name + FROM albums a + JOIN artist_metadata am ON a.artist_metadata_id = am.id + ORDER BY a.added_at DESC + "#, + ) + .fetch_all(&self.pool) + .await + } + + pub async fn count_artists(&self) -> Result { + let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM artist_metadata") + .fetch_one(&self.pool) + .await?; + Ok(row.0) + } + + pub async fn count_albums(&self) -> Result { + let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM albums") + .fetch_one(&self.pool) + .await?; + Ok(row.0) + } +} + +#[derive(Debug, serde::Serialize, FromRow)] +pub struct ArtistMetadataRow { + pub id: Uuid, + pub foreign_artist_id: Option, + pub name: String, + pub sort_name: Option, + pub artist_type: Option, + pub genres: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +#[derive(Debug, serde::Serialize, FromRow)] +pub struct AlbumRow { + pub id: Uuid, + pub artist_metadata_id: Uuid, + pub foreign_album_id: Option, + pub title: String, + pub album_type: Option, + pub release_date: Option, + pub monitored: bool, + pub added_at: chrono::DateTime, +} + +#[derive(Debug, serde::Serialize, FromRow)] +pub struct AlbumWithArtistRow { + pub id: Uuid, + pub foreign_album_id: Option, + pub title: String, + pub album_type: Option, + pub release_date: Option, + pub monitored: bool, + pub added_at: chrono::DateTime, + pub artist_id: Uuid, + pub artist_name: String, +} diff --git a/src/services/download_service.rs b/src/services/download_service.rs index c40e932..9985d17 100644 --- a/src/services/download_service.rs +++ b/src/services/download_service.rs @@ -1,17 +1,42 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; use crate::indexer::SearchResult; -use super::{IndexerService, MetadataService, TorrentService}; +use super::{DbService, IndexerService, MetadataService, TorrentService}; + +#[derive(Debug, Deserialize)] +pub struct SyncOptions { + pub artist: String, + pub album: Option, + pub download: bool, + pub store: bool, +} #[derive(Debug, Serialize)] -pub struct AlbumDownloadResult { +pub struct SyncResult { + pub artist_id: String, + pub artist_name: String, + pub total_albums: usize, + pub albums_stored: usize, + pub albums_downloaded: usize, + pub albums_no_results: usize, + pub albums_failed: usize, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub results: Vec, +} + +#[derive(Debug, Serialize)] +pub struct AlbumSyncResult { pub album_id: String, pub album_title: String, - pub artist_name: String, - pub status: DownloadStatus, + pub stored: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub download_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub torrent_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub indexer: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } @@ -24,89 +49,150 @@ pub enum DownloadStatus { Skipped, } -#[derive(Debug, Serialize)] -pub struct ArtistSyncResult { - pub artist_id: String, - pub artist_name: String, - pub total_albums: usize, - pub albums_added: usize, - pub albums_failed: usize, - pub albums_no_results: usize, - pub results: Vec, +struct DownloadResult { + status: DownloadStatus, + torrent_hash: Option, + indexer: Option, + error: Option, } pub struct DownloadService; impl DownloadService { - pub async fn sync_artist( - artist_name: &str, + pub async fn sync( + options: SyncOptions, metadata: &MetadataService, indexers: &IndexerService, torrent: &TorrentService, - ) -> Result { + db: Option<&DbService>, + ) -> Result { let search_result = metadata - .search_artists(artist_name, Some(1), None) + .search_artists(&options.artist, Some(1), None) .await .map_err(|e| format!("metadata search failed: {}", e))?; let artist = search_result .artists .first() - .ok_or_else(|| format!("artist '{}' not found", artist_name))?; + .ok_or_else(|| format!("artist '{}' not found", options.artist))?; + + let artist_metadata_id = if options.store { + if let Some(db) = db { + match db.upsert_artist_metadata(artist).await { + Ok(id) => { + tracing::info!("stored artist metadata: {} ({})", artist.name, id); + Some(id) + } + Err(e) => { + tracing::warn!("failed to store artist metadata: {}", e); + None + } + } + } else { + None + } + } else { + None + }; let albums_response = metadata - .get_artist_albums(&artist.id, Some(100), None) + .get_artist_albums(&artist.id, Some(500), None) .await .map_err(|e| format!("failed to get albums: {}", e))?; - let mut results = Vec::new(); - let mut albums_added = 0; - let mut albums_failed = 0; - let mut albums_no_results = 0; + let albums_to_process: Vec<_> = if let Some(ref album_filter) = options.album { + let filter_lower = album_filter.to_lowercase(); + albums_response + .albums + .iter() + .filter(|a| a.title.to_lowercase().contains(&filter_lower)) + .collect() + } else { + albums_response.albums.iter().collect() + }; - for album in &albums_response.albums { - let result = Self::download_album( - &artist.name, - &album.id, - &album.title, - album + let mut results = Vec::new(); + let mut albums_stored = 0; + let mut albums_downloaded = 0; + let mut albums_no_results = 0; + let mut albums_failed = 0; + + for album in albums_to_process.iter() { + let stored = if options.store { + if let (Some(db), Some(artist_id)) = (db, artist_metadata_id) { + match db.upsert_album(album, artist_id).await { + Ok(_) => { + albums_stored += 1; + true + } + Err(e) => { + tracing::warn!("failed to store album {}: {}", album.title, e); + false + } + } + } else { + false + } + } else { + false + }; + + let (download_status, torrent_hash, indexer, error) = if options.download { + let year = album .release_date .split('-') .next() - .and_then(|y| y.parse().ok()), - indexers, - torrent, - ) - .await; + .and_then(|y| y.parse().ok()); - match result.status { - DownloadStatus::Added => albums_added += 1, - DownloadStatus::NoResults => albums_no_results += 1, - DownloadStatus::Failed | DownloadStatus::Skipped => albums_failed += 1, - } + let dl_result = + Self::download_album(&artist.name, &album.title, year, indexers, torrent).await; - results.push(result); + match dl_result.status { + DownloadStatus::Added => albums_downloaded += 1, + DownloadStatus::NoResults => albums_no_results += 1, + DownloadStatus::Failed | DownloadStatus::Skipped => albums_failed += 1, + } + + ( + Some(dl_result.status), + dl_result.torrent_hash, + dl_result.indexer, + dl_result.error, + ) + } else { + (None, None, None, None) + }; + + results.push(AlbumSyncResult { + album_id: album.id.clone(), + album_title: album.title.clone(), + stored, + download_status, + torrent_hash, + indexer, + error, + }); } - Ok(ArtistSyncResult { + Ok(SyncResult { artist_id: artist.id.clone(), artist_name: artist.name.clone(), - total_albums: albums_response.albums.len(), - albums_added, - albums_failed, + total_albums: albums_to_process.len(), + albums_stored, + albums_downloaded, albums_no_results, + albums_failed, results, }) } async fn download_album( artist_name: &str, - album_id: &str, album_title: &str, year: Option, indexers: &IndexerService, torrent: &TorrentService, - ) -> AlbumDownloadResult { + ) -> DownloadResult { let criteria = crate::indexer::MusicSearchCriteria { artist: artist_name.to_string(), album: Some(album_title.to_string()), @@ -118,10 +204,7 @@ impl DownloadService { let search_results = match indexers.search(&criteria, None).await { Ok(r) => r, Err(e) => { - return AlbumDownloadResult { - album_id: album_id.to_string(), - album_title: album_title.to_string(), - artist_name: artist_name.to_string(), + return DownloadResult { status: DownloadStatus::Failed, torrent_hash: None, indexer: None, @@ -131,10 +214,7 @@ impl DownloadService { }; if search_results.is_empty() { - return AlbumDownloadResult { - album_id: album_id.to_string(), - album_title: album_title.to_string(), - artist_name: artist_name.to_string(), + return DownloadResult { status: DownloadStatus::NoResults, torrent_hash: None, indexer: None, @@ -145,19 +225,13 @@ impl DownloadService { let best = Self::select_best_result(&search_results); match torrent.add_torrent_url(&best.download_url, None).await { - Ok(()) => AlbumDownloadResult { - album_id: album_id.to_string(), - album_title: album_title.to_string(), - artist_name: artist_name.to_string(), + Ok(()) => DownloadResult { status: DownloadStatus::Added, torrent_hash: best.infohash.clone(), indexer: Some(best.indexer.clone()), error: None, }, - Err(e) => AlbumDownloadResult { - album_id: album_id.to_string(), - album_title: album_title.to_string(), - artist_name: artist_name.to_string(), + Err(e) => DownloadResult { status: DownloadStatus::Failed, torrent_hash: None, indexer: Some(best.indexer.clone()), diff --git a/src/services/mod.rs b/src/services/mod.rs index f6ded07..e373f81 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,11 +1,11 @@ +mod db_service; mod download_service; mod indexer_service; mod metadata_service; mod torrent_service; -pub use download_service::{ - AlbumDownloadResult, ArtistSyncResult, DownloadService, DownloadStatus, -}; +pub use db_service::{AlbumRow, AlbumWithArtistRow, ArtistMetadataRow, DbService}; +pub use download_service::{DownloadService, DownloadStatus, SyncOptions, SyncResult}; pub use indexer_service::{IndexerInfo, IndexerService}; pub use metadata_service::MetadataService; pub use torrent_service::TorrentService;