diff --git a/Cargo.lock b/Cargo.lock index 9fe6fc8..7a6d72a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,12 +17,79 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "assert_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e2651f366b7ee3f97729fded1441539b49d5f39eeb05b842689e11e84501b2" +dependencies = [ + "const_panic", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -34,6 +101,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" @@ -53,6 +129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", + "untrusted 0.7.1", "zeroize", ] @@ -112,17 +189,110 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.17", + "instant", + "rand 0.8.7", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] [[package]] name = "bumpalo" @@ -130,6 +300,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.0" @@ -167,8 +343,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures", - "rand_core", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -177,7 +353,12 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -189,6 +370,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -199,6 +386,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[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_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -215,6 +437,15 @@ 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 = "cpufeatures" version = "0.3.0" @@ -224,6 +455,21 @@ 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 = "crc32fast" version = "1.5.0" @@ -233,6 +479,155 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -244,17 +639,32 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] [[package]] name = "equivalent" @@ -272,6 +682,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[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" @@ -300,6 +731,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -312,6 +754,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -327,6 +775,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -334,6 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -342,6 +812,45 @@ 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-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -354,18 +863,48 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[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" @@ -379,6 +918,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -388,11 +941,34 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", + "rand_core 0.10.1", "wasm-bindgen", ] +[[package]] +name = "governor" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" +dependencies = [ + "cfg-if", + "dashmap", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.5", + "smallvec", + "spinning_top", + "web-time", +] + [[package]] name = "h2" version = "0.4.15" @@ -405,20 +981,43 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -427,12 +1026,45 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + [[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "http" version = "1.4.2" @@ -478,6 +1110,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -513,6 +1154,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -551,6 +1193,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -633,6 +1299,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -654,6 +1326,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -662,6 +1345,26 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "intervaltree" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270bc34e57047cab801a8c871c124d9dc7132f6473c6401f645524f4e6edd111" +dependencies = [ + "smallvec", ] [[package]] @@ -697,7 +1400,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -755,12 +1458,264 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leaky-bucket" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a396bb213c2d09ed6c5495fd082c991b6ab39c9daf4fff59e6727f85c73e4c5" +dependencies = [ + "parking_lot", + "pin-project-lite", + "tokio", +] + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "librqbit" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dadca8f521242010a4c846ef5f224c217009c92e272709cdc08ba9cdabe62983" +dependencies = [ + "anyhow", + "arc-swap", + "async-compression", + "async-stream", + "async-trait", + "backoff", + "base64", + "bincode 2.0.1", + "bitvec", + "byteorder", + "bytes", + "dashmap", + "futures", + "governor", + "hex", + "http", + "intervaltree", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "librqbit-dht", + "librqbit-peer-protocol", + "librqbit-sha1-wrapper", + "librqbit-tracker-comms", + "librqbit-upnp", + "memmap2", + "mime_guess", + "parking_lot", + "rand 0.9.5", + "regex", + "reqwest 0.12.28", + "rlimit", + "serde", + "serde_json", + "serde_urlencoded", + "serde_with", + "size_format", + "tokio", + "tokio-socks", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "uuid", + "walkdir", +] + +[[package]] +name = "librqbit-bencode" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606dff526ba81e3eca33e2bb28b53afa2bc0b2c41d252333fa44e6c11abb37da" +dependencies = [ + "anyhow", + "bytes", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-sha1-wrapper", + "serde", +] + +[[package]] +name = "librqbit-buffers" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c78b907d6171a7191c162b2b60db46d254ebde6a95282b77372af556c1463" +dependencies = [ + "bytes", + "librqbit-clone-to-owned", + "serde", +] + +[[package]] +name = "librqbit-clone-to-owned" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1e66d773ba9c475ff89286dc1d6f9d167cbb898603797467dd0ea6844c445" +dependencies = [ + "bytes", +] + +[[package]] +name = "librqbit-core" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a02cc6fce6743ad38661ccd6fafc6cf1ae5e0106a9922836b0524dbe752378" +dependencies = [ + "anyhow", + "assert_cfg", + "bytes", + "data-encoding", + "directories", + "hex", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "parking_lot", + "rand 0.9.5", + "serde", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "librqbit-dht" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cc129194337771a86b0399956c4d9bf1cd97c5f24d14a50be38e170f76a54b" +dependencies = [ + "anyhow", + "backoff", + "byteorder", + "bytes", + "chrono", + "dashmap", + "futures", + "hex", + "indexmap 2.14.0", + "leaky-bucket", + "librqbit-bencode", + "librqbit-clone-to-owned", + "librqbit-core", + "parking_lot", + "rand 0.9.5", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "librqbit-peer-protocol" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a73129497b500505f33d1dc0426319b6a6a208f13fdfaae56224ab8c2346a773" +dependencies = [ + "anyhow", + "bincode 1.3.3", + "bitvec", + "byteorder", + "bytes", + "itertools", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "serde", +] + +[[package]] +name = "librqbit-sha1-wrapper" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79373a02db73159e4de7ca5d27b6eeae2d540df66c6801db2b01c5513d087524" +dependencies = [ + "assert_cfg", + "aws-lc-rs", +] + +[[package]] +name = "librqbit-tracker-comms" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08204944c5be677a5de8e1230e0249fce5c14abef23048e26452c6fb03f1b260" +dependencies = [ + "anyhow", + "async-stream", + "byteorder", + "futures", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-core", + "parking_lot", + "rand 0.9.5", + "reqwest 0.12.28", + "serde", + "tokio", + "tokio-util", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "librqbit-upnp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545aad6124c97201055983137e12a19f34acad565120c3cd30596cbd72e8fa86" +dependencies = [ + "anyhow", + "bstr", + "futures", + "httparse", + "network-interface", + "quick-xml 0.37.5", + "reqwest 0.12.28", + "serde", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -773,6 +1728,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[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.33" @@ -791,18 +1755,47 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "memchr" version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -834,19 +1827,115 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" name = "music-agregator" version = "0.1.0" dependencies = [ + "anyhow", "bytes", + "librqbit", "pbjson", "pbjson-types", "prost", - "quick-xml", - "reqwest", + "quick-xml 0.41.0", + "regex", + "reqwest 0.13.4", "serde", + "serde_json", + "sha1 0.10.7", + "sqlx", + "tempfile", "tokio", "toml", "tonic", "tonic-health", "tonic-prost", "tonic-reflection", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "network-interface" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6" +dependencies = [ + "cc", + "libc", + "thiserror 2.0.18", + "winapi", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", ] [[package]] @@ -870,6 +1959,41 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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", + "smallvec", + "windows-link", +] + [[package]] name = "pbjson" version = "0.8.0" @@ -921,7 +2045,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -956,6 +2080,12 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -965,6 +2095,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1035,6 +2180,31 @@ dependencies = [ "prost", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -1059,7 +2229,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1075,14 +2245,14 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1111,12 +2281,45 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -1125,7 +2328,45 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -1140,7 +2381,56 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[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_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1172,6 +2462,47 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -1219,10 +2550,19 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1259,6 +2599,7 @@ checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1323,7 +2664,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1356,6 +2697,36 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.7.0" @@ -1415,6 +2786,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -1436,6 +2820,91 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1464,6 +2933,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "size_format" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed5f6ab2122c6dec69dca18c72fa4590a27e581ad20d44960fe74c032a0b23b" +dependencies = [ + "generic-array 0.12.4", + "num", +] + [[package]] name = "slab" version = "0.4.12" @@ -1475,6 +2954,9 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1486,12 +2968,223 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array 0.14.7", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.2", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1529,6 +3222,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.27.0" @@ -1536,18 +3235,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1561,6 +3281,45 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -1622,6 +3381,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1653,7 +3424,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime", @@ -1762,7 +3533,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -1809,6 +3580,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1832,6 +3604,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1840,18 +3638,69 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + +[[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 = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -1862,14 +3711,56 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "walkdir" version = "2.5.0" @@ -1895,6 +3786,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -1950,6 +3850,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -1979,6 +3892,37 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -1988,12 +3932,71 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2082,12 +4085,27 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.8.3" @@ -2111,6 +4129,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -2170,3 +4208,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 09f02e2..fd1ac21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,5 +27,15 @@ tonic-health = "0.14.6" toml = "1.1.2" reqwest = { version = "0.13.4", default-features = false, features = ["query", "rustls"] } quick-xml = { version = "0.41.0", features = ["serialize"] } +regex = "1" +sha1 = "0.10" +tracing = "0.1" +tracing-subscriber = "0.3" +librqbit = { version = "8", default-features = false, features = ["rust-tls"] } +tempfile = "3" +anyhow = "1" +sqlx = { version = "0.9", default-features = false, features = ["postgres", "runtime-tokio", "tls-rustls", "json", "uuid"] } +serde_json = "1" +uuid = { version = "1", features = ["v4"] } [workspace] \ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..23a8b04 --- /dev/null +++ b/config.toml @@ -0,0 +1,11 @@ +[service] +address = "[::1]:50051" +[indexer] +address = "http://localhost:9117" +api = "pj47116b3clxpf4h1b2fu9vrj07pmfa6" +[torrent] +address = "unix:///run/user/1000/devenv-ae542bd/torad.sock" +[metadata] +address = "http://localhost:50053" +[database] +url = "postgres://fujin@localhost:5432/music_agregator" diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..78bd298 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,64 @@ +-- Cached parsed-torrent metadata. Mirrors the Rust `ParsedTorrent` struct +-- field-for-field. A UUID `id` is the PK so rows can be inserted during the +-- fast path (title-only, no info_hash yet) and updated when the background +-- resolver finishes. +-- +-- A row exists in one of three states: +-- 'partial' - title-derived fields only, awaiting background resolution +-- 'resolving'- a background resolver task is currently fetching torrent bytes +-- 'resolved' - full torrent-derived data filled in +CREATE TABLE parsed_torrents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + info_hash TEXT NOT NULL DEFAULT '', + + raw_title TEXT NOT NULL DEFAULT '', + + artist TEXT NOT NULL DEFAULT '', + album TEXT NOT NULL DEFAULT '', + year INTEGER NOT NULL DEFAULT 0, + + release_type TEXT NOT NULL DEFAULT 'unknown', + genres JSONB NOT NULL DEFAULT '[]'::jsonb, + label TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'unknown', + rip_type TEXT NOT NULL DEFAULT '', + + format TEXT NOT NULL DEFAULT 'unknown', + bitrate TEXT NOT NULL DEFAULT '', + bit_depth INTEGER NOT NULL DEFAULT 0, + sample_rate INTEGER NOT NULL DEFAULT 0, + + track_names JSONB NOT NULL DEFAULT '[]'::jsonb, + track_count INTEGER NOT NULL DEFAULT 0, + release_count INTEGER NOT NULL DEFAULT 0, + audio_file_count INTEGER NOT NULL DEFAULT 0, + total_audio_size BIGINT NOT NULL DEFAULT 0, + + has_cover_art BOOLEAN NOT NULL DEFAULT false, + has_cue_sheet BOOLEAN NOT NULL DEFAULT false, + has_rip_log BOOLEAN NOT NULL DEFAULT false, + + parsed_successfully BOOLEAN NOT NULL DEFAULT false, + parse_errors JSONB NOT NULL DEFAULT '[]'::jsonb, + + -- cache-control columns (not part of ParsedTorrent) + state TEXT NOT NULL DEFAULT 'partial', + indexer TEXT NOT NULL DEFAULT '', + magnet TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- enum-backed fields (release_type, source, format) are stored as their +-- `as_str()` repr in plain TEXT rather than native ENUMs, so adding a variant +-- to the Rust enum needs no migration. + +-- lets the background resolver find rows still awaiting torrent resolution +CREATE INDEX parsed_torrents_state_idx + ON parsed_torrents (state) + WHERE state IN ('partial', 'resolving'); + +-- dedup resolved torrents by info_hash (only meaningful once non-empty) +CREATE UNIQUE INDEX parsed_torrents_info_hash_uidx + ON parsed_torrents (info_hash) + WHERE info_hash != ''; diff --git a/dev/Music Agregator/Jackett/Search.yml b/dev/Music Agregator/Jackett/Search.yml index 6dcad32..d05b682 100644 --- a/dev/Music Agregator/Jackett/Search.yml +++ b/dev/Music Agregator/Jackett/Search.yml @@ -5,10 +5,10 @@ info: http: method: GET - url: http://localhost:9117/api/v2.0/indexers/all/results/torznab/api?apikey=bjivq1zp3bfq7eaorhsswkfhvpc5issh&t=search&q=ДДТ + url: http://localhost:9117/api/v2.0/indexers/all/results/torznab/api?apikey=pj47116b3clxpf4h1b2fu9vrj07pmfa6&t=search&q=ДДТ params: - name: apikey - value: bjivq1zp3bfq7eaorhsswkfhvpc5issh + value: pj47116b3clxpf4h1b2fu9vrj07pmfa6 type: query - name: t value: search diff --git a/dev/Music Agregator/Montitor Album.yml b/dev/Music Agregator/Montitor Album.yml new file mode 100644 index 0000000..4c55a74 --- /dev/null +++ b/dev/Music Agregator/Montitor Album.yml @@ -0,0 +1,16 @@ +info: + name: Montitor Album + type: grpc + seq: 5 + +grpc: + url: localhost:50051 + method: /music.v1.MusicService/MonitorAlbum + methodType: unary + message: |- + { + "album": "Творчество в пустоте", + "artist": "ДДТ", + "quality": 0 + } + auth: inherit diff --git a/dev/Music Agregator/Torrent/Search.yml b/dev/Music Agregator/Torrent/Search.yml index c8032e8..abe29f3 100644 --- a/dev/Music Agregator/Torrent/Search.yml +++ b/dev/Music Agregator/Torrent/Search.yml @@ -9,6 +9,6 @@ grpc: methodType: unary message: |- { - "name": "ДДТ" + "name": "ДДТ Творчество в пустоте" } auth: inherit diff --git a/devenv.nix b/devenv.nix index 0be808a..e09166a 100644 --- a/devenv.nix +++ b/devenv.nix @@ -44,11 +44,11 @@ services.postgres = { enable = true; - # initialDatabases = [ - # { - # name = "toradb"; - # schema = ./db/schema.sql; - # } - # ]; + initialDatabases = [ + { + name = "music-agregator"; + schema = ./db/schema.sql; + } + ]; }; } diff --git a/proto/music/v1/music.proto b/proto/music/v1/music.proto new file mode 100644 index 0000000..449c247 --- /dev/null +++ b/proto/music/v1/music.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package music.v1; + +service MusicService { + rpc MonitorAlbum(MonitorAlbumRequest) returns (MonitorAlbumResponse); +} + +enum Quality { + MP3 = 0; + FLAC = 1; +} + +message MonitorAlbumRequest { + string album = 1; + string artist = 2; + Quality quality = 3; +} + +message MonitorAlbumResponse { + +} diff --git a/src/config.rs b/src/config.rs index b94231e..bcb8e9f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ pub struct Config { pub indexer: IndexerConfig, pub torrent: TorrentConfig, pub metadata: MetadataConfig, + pub database: DatabaseConfig, } #[derive(Debug, Deserialize)] @@ -29,3 +30,8 @@ pub struct TorrentConfig { pub struct MetadataConfig { pub address: String, } + +#[derive(Debug, Deserialize)] +pub struct DatabaseConfig { + pub url: String, +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..67de73e --- /dev/null +++ b/src/db.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use sqlx::Row; +use sqlx::postgres::{PgPool, PgPoolOptions}; +use uuid::Uuid; + +use crate::torrent_parser::{AudioFormat, ParsedTorrent, ReleaseSource, ReleaseType}; + +const SCHEMA_SQL: &str = include_str!("../db/schema.sql"); + +pub async fn connect(database_url: &str) -> Result { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(database_url) + .await?; + Ok(pool) +} + +pub async fn apply_schema(pool: &PgPool) -> Result<()> { + sqlx::raw_sql(SCHEMA_SQL).execute(pool).await?; + Ok(()) +} + +pub async fn insert_partial(pool: &PgPool, pt: &ParsedTorrent, source_url: &str) -> Result { + let row = sqlx::query( + "INSERT INTO parsed_torrents ( + info_hash, raw_title, artist, album, year, + release_type, genres, label, source, rip_type, + format, bitrate, bit_depth, sample_rate, + track_names, track_count, release_count, audio_file_count, total_audio_size, + has_cover_art, has_cue_sheet, has_rip_log, + parsed_successfully, parse_errors, + state, magnet + ) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, $10, + $11, $12, $13, $14, + $15, $16, $17, $18, $19, + $20, $21, $22, + $23, $24, + 'partial', $25 + ) + RETURNING id", + ) + .bind(&pt.info_hash) + .bind(&pt.raw_title) + .bind(&pt.artist) + .bind(&pt.album) + .bind(pt.year as i32) + .bind(pt.release_type.as_str()) + .bind(sqlx::types::Json(&pt.genres)) + .bind(&pt.label) + .bind(pt.source.as_str()) + .bind(&pt.rip_type) + .bind(pt.format.as_str()) + .bind(&pt.bitrate) + .bind(pt.bit_depth as i32) + .bind(pt.sample_rate as i32) + .bind(sqlx::types::Json(&pt.track_names)) + .bind(pt.track_count as i32) + .bind(pt.release_count as i32) + .bind(pt.audio_file_count as i32) + .bind(pt.total_audio_size as i64) + .bind(pt.has_cover_art) + .bind(pt.has_cue_sheet) + .bind(pt.has_rip_log) + .bind(pt.parsed_successfully) + .bind(sqlx::types::Json(&pt.parse_errors)) + .bind(source_url) + .fetch_one(pool) + .await?; + + Ok(row.get("id")) +} + +pub async fn update_resolved(pool: &PgPool, id: Uuid, pt: &ParsedTorrent) -> Result<()> { + sqlx::query( + "UPDATE parsed_torrents SET + info_hash = $2, + raw_title = $3, + artist = $4, + album = $5, + year = $6, + release_type = $7, + genres = $8, + label = $9, + source = $10, + rip_type = $11, + format = $12, + bitrate = $13, + bit_depth = $14, + sample_rate = $15, + track_names = $16, + track_count = $17, + release_count = $18, + audio_file_count = $19, + total_audio_size = $20, + has_cover_art = $21, + has_cue_sheet = $22, + has_rip_log = $23, + parsed_successfully = $24, + parse_errors = $25, + state = 'resolved', + updated_at = now() + WHERE id = $1", + ) + .bind(id) + .bind(&pt.info_hash) + .bind(&pt.raw_title) + .bind(&pt.artist) + .bind(&pt.album) + .bind(pt.year as i32) + .bind(pt.release_type.as_str()) + .bind(sqlx::types::Json(&pt.genres)) + .bind(&pt.label) + .bind(pt.source.as_str()) + .bind(&pt.rip_type) + .bind(pt.format.as_str()) + .bind(&pt.bitrate) + .bind(pt.bit_depth as i32) + .bind(pt.sample_rate as i32) + .bind(sqlx::types::Json(&pt.track_names)) + .bind(pt.track_count as i32) + .bind(pt.release_count as i32) + .bind(pt.audio_file_count as i32) + .bind(pt.total_audio_size as i64) + .bind(pt.has_cover_art) + .bind(pt.has_cue_sheet) + .bind(pt.has_rip_log) + .bind(pt.parsed_successfully) + .bind(sqlx::types::Json(&pt.parse_errors)) + .execute(pool) + .await?; + Ok(()) +} + +pub async fn get(pool: &PgPool, info_hash: &str) -> Result> { + let row = sqlx::query( + "SELECT info_hash, raw_title, artist, album, year, + release_type, genres, label, source, rip_type, + format, bitrate, bit_depth, sample_rate, + track_names, track_count, release_count, audio_file_count, total_audio_size, + has_cover_art, has_cue_sheet, has_rip_log, + parsed_successfully, parse_errors + FROM parsed_torrents + WHERE info_hash = $1 AND state = 'resolved'", + ) + .bind(info_hash) + .fetch_optional(pool) + .await?; + + let row = match row { + Some(r) => r, + None => return Ok(None), + }; + + let genres: sqlx::types::Json> = row.try_get("genres")?; + let track_names: sqlx::types::Json> = row.try_get("track_names")?; + let parse_errors: sqlx::types::Json> = row.try_get("parse_errors")?; + + Ok(Some(ParsedTorrent { + info_hash: row.try_get("info_hash")?, + raw_title: row.try_get("raw_title")?, + artist: row.try_get("artist")?, + album: row.try_get("album")?, + year: row.try_get::("year")? as u32, + release_type: ReleaseType::from_str(row.try_get("release_type")?), + genres: genres.0, + label: row.try_get("label")?, + source: ReleaseSource::from_str(row.try_get("source")?), + rip_type: row.try_get("rip_type")?, + format: AudioFormat::from_str(row.try_get("format")?), + bitrate: row.try_get("bitrate")?, + bit_depth: row.try_get::("bit_depth")? as u32, + sample_rate: row.try_get::("sample_rate")? as u32, + track_names: track_names.0, + track_count: row.try_get::("track_count")? as u32, + release_count: row.try_get::("release_count")? as u32, + audio_file_count: row.try_get::("audio_file_count")? as u32, + total_audio_size: row.try_get::("total_audio_size")? as u64, + has_cover_art: row.try_get("has_cover_art")?, + has_cue_sheet: row.try_get("has_cue_sheet")?, + has_rip_log: row.try_get("has_rip_log")?, + parsed_successfully: row.try_get("parsed_successfully")?, + parse_errors: parse_errors.0, + })) +} diff --git a/src/generated/music/v1/music.v1.rs b/src/generated/music/v1/music.v1.rs new file mode 100644 index 0000000..ba9e87a --- /dev/null +++ b/src/generated/music/v1/music.v1.rs @@ -0,0 +1,111 @@ +// @generated +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct MonitorAlbumRequest { + #[prost(string, tag = "1")] + pub album: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub artist: ::prost::alloc::string::String, + #[prost(enumeration = "Quality", tag = "3")] + pub quality: i32, +} +impl ::prost::Name for MonitorAlbumRequest { + const NAME: &'static str = "MonitorAlbumRequest"; + const PACKAGE: &'static str = "music.v1"; + fn full_name() -> ::prost::alloc::string::String { + "music.v1.MonitorAlbumRequest".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/music.v1.MonitorAlbumRequest".into() + } +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct MonitorAlbumResponse {} +impl ::prost::Name for MonitorAlbumResponse { + const NAME: &'static str = "MonitorAlbumResponse"; + const PACKAGE: &'static str = "music.v1"; + fn full_name() -> ::prost::alloc::string::String { + "music.v1.MonitorAlbumResponse".into() + } + fn type_url() -> ::prost::alloc::string::String { + "/music.v1.MonitorAlbumResponse".into() + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Quality { + Mp3 = 0, + Flac = 1, +} +impl Quality { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Mp3 => "MP3", + Self::Flac => "FLAC", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MP3" => Some(Self::Mp3), + "FLAC" => Some(Self::Flac), + _ => None, + } + } +} +/// Encoded file descriptor set for the `music.v1` package +pub const FILE_DESCRIPTOR_SET: &[u8] = &[ + 0x0a, 0xdc, 0x05, 0x0a, 0x14, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x75, + 0x73, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d, 0x75, 0x73, 0x69, 0x63, + 0x2e, 0x76, 0x31, 0x22, 0x70, 0x0a, 0x13, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x41, 0x6c, + 0x62, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, + 0x62, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, + 0x12, 0x16, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x07, 0x71, 0x75, 0x61, 0x6c, + 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6d, 0x75, 0x73, 0x69, + 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x07, 0x71, 0x75, + 0x61, 0x6c, 0x69, 0x74, 0x79, 0x22, 0x16, 0x0a, 0x14, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x41, 0x6c, 0x62, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x1c, 0x0a, + 0x07, 0x51, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x50, 0x33, 0x10, + 0x00, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x4c, 0x41, 0x43, 0x10, 0x01, 0x32, 0x5d, 0x0a, 0x0c, 0x4d, + 0x75, 0x73, 0x69, 0x63, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x4d, + 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x41, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x1d, 0x2e, 0x6d, 0x75, + 0x73, 0x69, 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x41, 0x6c, + 0x62, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6d, 0x75, 0x73, + 0x69, 0x63, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x41, 0x6c, 0x62, + 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4a, 0xaa, 0x03, 0x0a, 0x06, 0x12, + 0x04, 0x00, 0x00, 0x15, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12, 0x0a, + 0x08, 0x0a, 0x01, 0x02, 0x12, 0x03, 0x02, 0x00, 0x11, 0x0a, 0x0a, 0x0a, 0x02, 0x06, 0x00, 0x12, + 0x04, 0x04, 0x00, 0x06, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x00, 0x01, 0x12, 0x03, 0x04, 0x08, + 0x14, 0x0a, 0x0b, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x00, 0x12, 0x03, 0x05, 0x02, 0x47, 0x0a, 0x0c, + 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x05, 0x06, 0x12, 0x0a, 0x0c, 0x0a, 0x05, + 0x06, 0x00, 0x02, 0x00, 0x02, 0x12, 0x03, 0x05, 0x13, 0x26, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, + 0x02, 0x00, 0x03, 0x12, 0x03, 0x05, 0x31, 0x45, 0x0a, 0x0a, 0x0a, 0x02, 0x05, 0x00, 0x12, 0x04, + 0x08, 0x00, 0x0b, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x05, 0x00, 0x01, 0x12, 0x03, 0x08, 0x05, 0x0c, + 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x09, 0x02, 0x05, 0x0a, 0x0b, + 0x0a, 0x04, 0x05, 0x00, 0x02, 0x00, 0x12, 0x03, 0x09, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x05, + 0x00, 0x02, 0x00, 0x02, 0x12, 0x03, 0x09, 0x08, 0x09, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, + 0x01, 0x01, 0x12, 0x03, 0x0a, 0x02, 0x06, 0x0a, 0x0b, 0x0a, 0x04, 0x05, 0x00, 0x02, 0x01, 0x12, + 0x03, 0x0a, 0x02, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x05, 0x00, 0x02, 0x01, 0x02, 0x12, 0x03, 0x0a, + 0x09, 0x0a, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x00, 0x12, 0x04, 0x0d, 0x00, 0x11, 0x01, 0x0a, 0x0a, + 0x0a, 0x03, 0x04, 0x00, 0x01, 0x12, 0x03, 0x0d, 0x08, 0x1b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, + 0x02, 0x00, 0x05, 0x12, 0x03, 0x0e, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x00, + 0x12, 0x03, 0x0e, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, + 0x0e, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x0e, 0x11, + 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x01, 0x05, 0x12, 0x03, 0x0f, 0x02, 0x08, 0x0a, + 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x01, 0x12, 0x03, 0x0f, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, + 0x04, 0x00, 0x02, 0x01, 0x01, 0x12, 0x03, 0x0f, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, + 0x02, 0x01, 0x03, 0x12, 0x03, 0x0f, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x02, + 0x06, 0x12, 0x03, 0x10, 0x02, 0x09, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x02, 0x12, 0x03, + 0x10, 0x02, 0x16, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x02, 0x01, 0x12, 0x03, 0x10, 0x0a, + 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x02, 0x03, 0x12, 0x03, 0x10, 0x14, 0x15, 0x0a, + 0x0a, 0x0a, 0x02, 0x04, 0x01, 0x12, 0x04, 0x13, 0x00, 0x15, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, + 0x01, 0x01, 0x12, 0x03, 0x13, 0x08, 0x1c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +]; +include!("music.v1.serde.rs"); +include!("music.v1.tonic.rs"); +// @@protoc_insertion_point(module) diff --git a/src/generated/music/v1/music.v1.serde.rs b/src/generated/music/v1/music.v1.serde.rs new file mode 100644 index 0000000..39179e3 --- /dev/null +++ b/src/generated/music/v1/music.v1.serde.rs @@ -0,0 +1,270 @@ +// @generated +impl serde::Serialize for MonitorAlbumRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.album.is_empty() { + len += 1; + } + if !self.artist.is_empty() { + len += 1; + } + if self.quality != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("music.v1.MonitorAlbumRequest", len)?; + if !self.album.is_empty() { + struct_ser.serialize_field("album", &self.album)?; + } + if !self.artist.is_empty() { + struct_ser.serialize_field("artist", &self.artist)?; + } + if self.quality != 0 { + let v = Quality::try_from(self.quality).map_err(|_| { + serde::ser::Error::custom(format!("Invalid variant {}", self.quality)) + })?; + struct_ser.serialize_field("quality", &v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MonitorAlbumRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &["album", "artist", "quality"]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Album, + Artist, + Quality, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "album" => Ok(GeneratedField::Album), + "artist" => Ok(GeneratedField::Artist), + "quality" => Ok(GeneratedField::Quality), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MonitorAlbumRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct music.v1.MonitorAlbumRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut album__ = None; + let mut artist__ = None; + let mut quality__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Album => { + if album__.is_some() { + return Err(serde::de::Error::duplicate_field("album")); + } + album__ = Some(map_.next_value()?); + } + GeneratedField::Artist => { + if artist__.is_some() { + return Err(serde::de::Error::duplicate_field("artist")); + } + artist__ = Some(map_.next_value()?); + } + GeneratedField::Quality => { + if quality__.is_some() { + return Err(serde::de::Error::duplicate_field("quality")); + } + quality__ = Some(map_.next_value::()? as i32); + } + } + } + Ok(MonitorAlbumRequest { + album: album__.unwrap_or_default(), + artist: artist__.unwrap_or_default(), + quality: quality__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("music.v1.MonitorAlbumRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for MonitorAlbumResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let len = 0; + let struct_ser = serializer.serialize_struct("music.v1.MonitorAlbumResponse", len)?; + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for MonitorAlbumResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField {} + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + Err(serde::de::Error::unknown_field(value, FIELDS)) + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = MonitorAlbumResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct music.v1.MonitorAlbumResponse") + } + + fn visit_map( + self, + mut map_: V, + ) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + while map_.next_key::()?.is_some() { + let _ = map_.next_value::()?; + } + Ok(MonitorAlbumResponse {}) + } + } + deserializer.deserialize_struct("music.v1.MonitorAlbumResponse", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for Quality { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Mp3 => "MP3", + Self::Flac => "FLAC", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for Quality { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &["MP3", "FLAC"]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Quality; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + fn visit_i64(self, v: i64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self) + }) + } + + fn visit_u64(self, v: u64) -> std::result::Result + where + E: serde::de::Error, + { + i32::try_from(v) + .ok() + .and_then(|x| x.try_into().ok()) + .ok_or_else(|| { + serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self) + }) + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "MP3" => Ok(Quality::Mp3), + "FLAC" => Ok(Quality::Flac), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} diff --git a/src/generated/music/v1/music.v1.tonic.rs b/src/generated/music/v1/music.v1.tonic.rs new file mode 100644 index 0000000..1772594 --- /dev/null +++ b/src/generated/music/v1/music.v1.tonic.rs @@ -0,0 +1,274 @@ +// @generated +/// Generated client implementations. +pub mod music_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value + )] + use tonic::codegen::http::Uri; + use tonic::codegen::*; + #[derive(Debug, Clone)] + pub struct MusicServiceClient { + inner: tonic::client::Grpc, + } + impl MusicServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl MusicServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> MusicServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + >>::Error: + Into + std::marker::Send + std::marker::Sync, + { + MusicServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn monitor_album( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> + { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/music.v1.MusicService/MonitorAlbum"); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("music.v1.MusicService", "MonitorAlbum")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod music_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with MusicServiceServer. + #[async_trait] + pub trait MusicService: std::marker::Send + std::marker::Sync + 'static { + async fn monitor_album( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct MusicServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl MusicServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for MusicServiceServer + where + T: MusicService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/music.v1.MusicService/MonitorAlbum" => { + #[allow(non_camel_case_types)] + struct MonitorAlbumSvc(pub Arc); + impl tonic::server::UnaryService + for MonitorAlbumSvc + { + type Response = super::MonitorAlbumResponse; + type Future = BoxFuture, tonic::Status>; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::monitor_album(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = MonitorAlbumSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => Box::pin(async move { + let mut response = http::Response::new(tonic::body::Body::default()); + let headers = response.headers_mut(); + headers.insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers.insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }), + } + } + } + impl Clone for MusicServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "music.v1.MusicService"; + impl tonic::server::NamedService for MusicServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/src/magnet_resolver.rs b/src/magnet_resolver.rs new file mode 100644 index 0000000..dfc186e --- /dev/null +++ b/src/magnet_resolver.rs @@ -0,0 +1,74 @@ +//! Magnet-link resolver backed by librqbit. +//! +//! Turns a `magnet:` URI into the same bencoded torrent bytes a `.torrent` +//! download would yield, by resolving metadata over the BitTorrent DHT and +//! the BEP-9 `ut_metadata` peer extension. The bytes are then handed to +//! [`crate::torrent_parser::TorrentParser`] so magnet and torrent sources are +//! parsed identically. + +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, bail}; +use librqbit::{ + AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, Session, SessionOptions, +}; +use tempfile::TempDir; + +const RESOLVE_TIMEOUT: Duration = Duration::from_secs(90); + +pub struct MagnetResolver { + session: Arc, + _scratch_dir: TempDir, +} + +impl std::fmt::Debug for MagnetResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MagnetResolver").finish_non_exhaustive() + } +} + +impl MagnetResolver { + pub async fn new() -> anyhow::Result { + let scratch_dir = tempfile::TempDir::new()?; + let opts = SessionOptions { + disable_dht_persistence: true, + persistence: None, + enable_upnp_port_forwarding: false, + ..Default::default() + }; + let session = Session::new_with_opts(scratch_dir.path().to_path_buf(), opts).await?; + Ok(Self { + session, + _scratch_dir: scratch_dir, + }) + } + + pub async fn resolve(&self, magnet: &str) -> anyhow::Result> { + let added = tokio::time::timeout( + RESOLVE_TIMEOUT, + self.session.add_torrent( + AddTorrent::from_url(magnet), + Some(AddTorrentOptions { + list_only: true, + ..Default::default() + }), + ), + ) + .await + .context("magnet resolution timed out (>90s, likely no peers)")? + .context("error adding magnet to session")?; + + match added { + AddTorrentResponse::ListOnly(ListOnlyResponse { torrent_bytes, .. }) => { + Ok(torrent_bytes.to_vec()) + } + AddTorrentResponse::AlreadyManaged(_, handle) => { + handle.with_metadata(|m| m.torrent_bytes.to_vec()) + } + AddTorrentResponse::Added(_, _) => { + bail!("torrent unexpectedly added under list_only") + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 3b447b4..18f9d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ mod config; +mod db; mod greeter; mod health; mod indexer; +mod magnet_resolver; mod metadata; +mod music; mod torrent; mod torrent_manager; +mod torrent_parser; mod generated { pub mod hello { @@ -23,6 +27,10 @@ mod generated { include!("generated/metadata/v1/metadata.v1.rs"); } + pub mod music { + include!("generated/music/v1/music.v1.rs"); + } + pub mod health { include!("generated/health/health.rs"); } @@ -33,19 +41,22 @@ use std::{fs, sync::Arc}; use generated::{ health::health_server::HealthServer as AggregatorHealthServer, hello::greeter_server::GreeterServer, metadata::metadata_service_server::MetadataServiceServer, - torrent::torrents_server::TorrentsServer, + music::music_service_server::MusicServiceServer, torrent::torrents_server::TorrentsServer, torrent_manager::torrent_manager_server::TorrentManagerServer, }; use greeter::GreeterService; use health::HealthService; use indexer::Jackett; use metadata::MetadataService; +use music::MusicService; use tonic_health::pb::health_server::HealthServer; use torrent::TorrentsService; use torrent_manager::TorrentMananagerService; #[tokio::main] async fn main() -> Result<(), Box> { + tracing_subscriber::fmt().init(); + let config_str = fs::read_to_string("config.toml").expect("Failed to read file"); let config: config::Config = toml::from_str(&config_str).expect("Failed to parse TOML"); let addr = config.service.address.parse()?; @@ -56,6 +67,7 @@ async fn main() -> Result<(), Box> { .register_encoded_file_descriptor_set(generated::health::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(generated::torrent_manager::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(generated::metadata::FILE_DESCRIPTOR_SET) + .register_encoded_file_descriptor_set(generated::music::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET) .build_v1()?; @@ -65,6 +77,21 @@ async fn main() -> Result<(), Box> { let indexer = Arc::new(Jackett::new(&config.indexer)); let torrent_manager = TorrentMananagerService::new(torrents.clone(), indexer); + let db_pool = match db::connect(&config.database.url).await { + Ok(pool) => { + if let Err(e) = db::apply_schema(&pool).await { + tracing::warn!(error = %e, "failed to apply DB schema"); + } + Some(pool) + } + Err(e) => { + tracing::warn!(error = %e, "failed to connect to database; caching disabled"); + None + } + }; + + let music = MusicService::new(metadata.clone(), torrent_manager.clone(), db_pool); + tonic::transport::Server::builder() .add_service(reflection) .add_service(HealthServer::new(health.clone())) @@ -73,6 +100,7 @@ async fn main() -> Result<(), Box> { .add_service(TorrentsServer::new(torrents)) .add_service(TorrentManagerServer::new(torrent_manager)) .add_service(MetadataServiceServer::new(metadata)) + .add_service(MusicServiceServer::new(music)) .serve(addr) .await?; diff --git a/src/music.rs b/src/music.rs new file mode 100644 index 0000000..cebd273 --- /dev/null +++ b/src/music.rs @@ -0,0 +1,95 @@ +use tonic::{Request, Response, Status}; + +use crate::{ + generated::{ + metadata::{ + Album, SearchAlbumsRequest, metadata_service_server::MetadataService as MetadataSvc, + }, + music::{ + MonitorAlbumRequest, MonitorAlbumResponse, music_service_server::MusicService as Music, + }, + torrent_manager::{SearchRequest, torrent_manager_server::TorrentManager}, + }, + magnet_resolver::MagnetResolver, + metadata::MetadataService, + torrent_manager::TorrentMananagerService, + torrent_parser::{ParsedTorrent, TorrentParser}, +}; + +pub struct MusicService { + metadata_service: MetadataService, + torrent_manager: TorrentMananagerService, + db: Option, +} + +impl MusicService { + pub fn new( + metadata_service: MetadataService, + torrent_manager: TorrentMananagerService, + db: Option, + ) -> Self { + Self { + metadata_service, + torrent_manager, + db, + } + } +} + +#[tonic::async_trait] +impl Music for MusicService { + async fn monitor_album( + &self, + request: Request, + ) -> Result, Status> { + let req: &MonitorAlbumRequest = request.get_ref(); + + let search_request = SearchAlbumsRequest { + query: req.album.clone(), + artist: req.artist.clone(), + ..Default::default() + }; + + let search_response = self + .metadata_service + .search_albums(Request::new(search_request)) + .await?; + + dbg!(&search_response); + + let album: &Album = &search_response.get_ref().albums[0]; + let name = format!( + "{} {}", + album.artists[0].artist.clone().unwrap().name, + album.title + ); + + let torrent_search_request = SearchRequest { name: name }; + + dbg!(&torrent_search_request); + let torrent_search_response = self + .torrent_manager + .search(Request::new(torrent_search_request)) + .await?; + dbg!(&torrent_search_response); + + let resolver = match MagnetResolver::new().await { + Ok(r) => Some(std::sync::Arc::new(r)), + Err(e) => { + tracing::warn!( + error = %e, + "failed to init MagnetResolver; magnet links will be info-hash only" + ); + None + } + }; + let parser = + std::sync::Arc::new(TorrentParser::new_with_resolver(resolver, self.db.clone())); + let parsed_torrents: Vec = parser + .parse_search_response(torrent_search_response.get_ref()) + .await; + dbg!(&parsed_torrents); + + Ok(Response::new(MonitorAlbumResponse::default())) + } +} diff --git a/src/torrent_manager.rs b/src/torrent_manager.rs index 860140d..c0d257d 100644 --- a/src/torrent_manager.rs +++ b/src/torrent_manager.rs @@ -11,6 +11,7 @@ use crate::{ torrent::TorrentsService, }; +#[derive(Clone)] pub struct TorrentMananagerService { torrents_service: TorrentsService, indexer: Arc, diff --git a/src/torrent_parser.rs b/src/torrent_parser.rs new file mode 100644 index 0000000..4f6df17 --- /dev/null +++ b/src/torrent_parser.rs @@ -0,0 +1,1197 @@ +//! Torrent release parser. +//! +//! Ports the behaviour of the Go `tracker.GenericParser`: given a torrent +//! title (or full torrent metainfo bytes) it extracts structured release +//! metadata — artist, album, year, audio format, bitrate, bit-depth / +//! sample-rate, source, rip type and (when torrent data is available) the +//! per-file track listing. +//! +//! Primary entry point for search results is +//! [`TorrentParser::parse_search_response`]; for raw torrent bytes use +//! [`TorrentParser::parse_torrent_data`]. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use regex::Regex; +use tokio::sync::Semaphore; +use tracing::{trace, warn}; + +use crate::generated::metadata::Album; +use crate::generated::torrent_manager::{SearchItem, SearchResponse}; +use crate::magnet_resolver::MagnetResolver; + +const MAX_CONCURRENT_RESOLVES: usize = 8; + +/// Audio container/codec format detected from file extensions or title tokens. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum AudioFormat { + #[default] + Unknown, + FLAC, + MP3, + AAC, + APE, + WavPack, + ALAC, + OGG, + WAV, +} + +impl AudioFormat { + /// Lossless (uncompressed or losslessly compressed) formats. + pub fn is_lossless(self) -> bool { + matches!( + self, + Self::FLAC | Self::APE | Self::WavPack | Self::ALAC | Self::WAV + ) + } + + pub fn as_str(self) -> &'static str { + match self { + Self::Unknown => "unknown", + Self::FLAC => "FLAC", + Self::MP3 => "MP3", + Self::AAC => "AAC", + Self::APE => "APE", + Self::WavPack => "WavPack", + Self::ALAC => "ALAC", + Self::OGG => "OGG", + Self::WAV => "WAV", + } + } + + pub fn from_str(s: &str) -> Self { + match s { + "FLAC" => Self::FLAC, + "MP3" => Self::MP3, + "AAC" => Self::AAC, + "APE" => Self::APE, + "WavPack" => Self::WavPack, + "ALAC" => Self::ALAC, + "OGG" => Self::OGG, + "WAV" => Self::WAV, + _ => Self::Unknown, + } + } +} + +/// Release classification (album, EP, single, ...). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ReleaseType { + #[default] + Unknown, + Album, + EP, + Single, + Compilation, + Soundtrack, + Live, +} + +impl ReleaseType { + pub fn as_str(self) -> &'static str { + match self { + Self::Unknown => "unknown", + Self::Album => "album", + Self::EP => "ep", + Self::Single => "single", + Self::Compilation => "compilation", + Self::Soundtrack => "soundtrack", + Self::Live => "live", + } + } + + pub fn from_str(s: &str) -> Self { + match s { + "album" => Self::Album, + "ep" => Self::EP, + "single" => Self::Single, + "compilation" => Self::Compilation, + "soundtrack" => Self::Soundtrack, + "live" => Self::Live, + _ => Self::Unknown, + } + } +} + +/// Physical/digital source medium of the rip. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ReleaseSource { + #[default] + Unknown, + CD, + WEB, + Vinyl, + Cassette, + DVD, + BluRay, + SACD, + DAT, +} + +impl ReleaseSource { + pub fn as_str(self) -> &'static str { + match self { + Self::Unknown => "unknown", + Self::CD => "CD", + Self::WEB => "WEB", + Self::Vinyl => "Vinyl", + Self::Cassette => "Cassette", + Self::DVD => "DVD", + Self::BluRay => "Blu-Ray", + Self::SACD => "SACD", + Self::DAT => "DAT", + } + } + + pub fn from_str(s: &str) -> Self { + match s { + "CD" => Self::CD, + "WEB" => Self::WEB, + "Vinyl" => Self::Vinyl, + "Cassette" => Self::Cassette, + "DVD" => Self::DVD, + "Blu-Ray" => Self::BluRay, + "SACD" => Self::SACD, + "DAT" => Self::DAT, + _ => Self::Unknown, + } + } +} + +/// Maps a file extension (with leading dot, lowercase) to an audio format. +fn format_for_extension(ext: &str) -> Option { + match ext { + ".flac" => Some(AudioFormat::FLAC), + ".mp3" => Some(AudioFormat::MP3), + ".aac" => Some(AudioFormat::AAC), + ".m4a" => Some(AudioFormat::AAC), + ".ape" => Some(AudioFormat::APE), + ".wv" => Some(AudioFormat::WavPack), + ".alac" => Some(AudioFormat::ALAC), + ".ogg" => Some(AudioFormat::OGG), + ".wav" => Some(AudioFormat::WAV), + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Parsed output +// --------------------------------------------------------------------------- + +/// Structured release information extracted from a torrent title and/or its +/// metainfo bytes. +#[derive(Debug, Clone, Default)] +pub struct ParsedTorrent { + pub raw_title: String, + /// Lowercase-hex SHA-1 info-hash; populated only from torrent metainfo. + pub info_hash: String, + + pub artist: String, + pub album: String, + pub year: u32, + + pub release_type: ReleaseType, + pub genres: Vec, + pub label: String, + pub source: ReleaseSource, + pub rip_type: String, + + pub format: AudioFormat, + /// e.g. "320 kbps" (lossy only). + pub bitrate: String, + /// e.g. 16 or 24 (lossless only). + pub bit_depth: u32, + /// In Hz (e.g. 44100), not kHz. + pub sample_rate: u32, + + pub track_names: Vec, + pub track_count: u32, + pub release_count: u32, + pub audio_file_count: u32, + pub total_audio_size: u64, + + pub has_cover_art: bool, + pub has_cue_sheet: bool, + pub has_rip_log: bool, + + pub parsed_successfully: bool, + pub parse_errors: Vec, +} + +impl ParsedTorrent { + fn record_error(&mut self, msg: impl Into) { + self.parse_errors.push(msg.into()); + } + + fn set_quality_if_missing(&mut self, bit_depth: u32, sample_rate: u32) { + if self.bit_depth == 0 { + self.bit_depth = bit_depth; + } + if self.sample_rate == 0 { + self.sample_rate = sample_rate; + } + } +} + +/// File-list facts derived from a torrent's `info` dictionary. +struct TorrentFileList { + format: AudioFormat, + audio_file_count: u32, + total_audio_size: u64, + track_names: Vec, + has_cover_art: bool, + has_cue_sheet: bool, + has_rip_log: bool, +} + +/// A parsed bencode value. Byte offsets are retained so the raw span of any +/// value (notably the `info` dict) can be sliced out for SHA-1 hashing. +#[derive(Debug, Clone)] +enum BencodeValue { + Int(i64), + Bytes(Vec), + List(Vec), + Dict(BTreeMap, BencodeNode>), +} + +#[derive(Debug, Clone)] +struct BencodeNode { + value: BencodeValue, + start: usize, + end: usize, +} + +impl BencodeNode { + fn as_int(&self) -> Option { + match self.value { + BencodeValue::Int(i) => Some(i), + _ => None, + } + } + + fn as_bytes(&self) -> Option<&[u8]> { + match &self.value { + BencodeValue::Bytes(b) => Some(b), + _ => None, + } + } + + fn as_str(&self) -> Option<&str> { + self.as_bytes().and_then(|b| std::str::from_utf8(b).ok()) + } + + fn as_list(&self) -> Option<&[BencodeNode]> { + match &self.value { + BencodeValue::List(v) => Some(v), + _ => None, + } + } + + fn as_dict(&self) -> Option<&BTreeMap, BencodeNode>> { + match &self.value { + BencodeValue::Dict(d) => Some(d), + _ => None, + } + } + + fn dict_get(&self, key: &str) -> Option<&BencodeNode> { + self.as_dict().and_then(|d| d.get(key.as_bytes())) + } +} + +#[derive(Debug)] +struct BencodeError { + msg: String, +} + +impl std::fmt::Display for BencodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.msg) + } +} + +impl std::error::Error for BencodeError {} + +mod bencode { + use std::collections::BTreeMap; + + use super::{BencodeError, BencodeNode, BencodeValue}; + + pub(super) fn parse(data: &[u8]) -> Result { + let mut pos = 0usize; + parse_value(data, &mut pos) + } + + fn parse_value(data: &[u8], pos: &mut usize) -> Result { + let start = *pos; + let byte = *peek(data, pos)?; + let value = match byte { + b'i' => { + *pos += 1; + BencodeValue::Int(parse_int_until(data, pos, b'e')?) + } + b'l' => { + *pos += 1; + let mut items = Vec::new(); + loop { + if *peek(data, pos)? == b'e' { + *pos += 1; + break; + } + items.push(parse_value(data, pos)?); + } + BencodeValue::List(items) + } + b'd' => { + *pos += 1; + let mut dict = BTreeMap::new(); + loop { + if *peek(data, pos)? == b'e' { + *pos += 1; + break; + } + let key_node = parse_value(data, pos)?; + let BencodeValue::Bytes(key) = key_node.value else { + return Err(BencodeError { + msg: "bencode dict key is not a string".to_string(), + }); + }; + let val = parse_value(data, pos)?; + dict.insert(key, val); + } + BencodeValue::Dict(dict) + } + b'0'..=b'9' => BencodeValue::Bytes(parse_string(data, pos)?), + other => { + return Err(BencodeError { + msg: format!("unexpected bencode byte {other:#x}"), + }); + } + }; + Ok(BencodeNode { + value, + start, + end: *pos, + }) + } + + fn peek<'a>(data: &'a [u8], pos: &usize) -> Result<&'a u8, BencodeError> { + data.get(*pos).ok_or_else(|| BencodeError { + msg: "unexpected end of bencoded data".to_string(), + }) + } + + fn parse_string(data: &[u8], pos: &mut usize) -> Result, BencodeError> { + let len = parse_int_until(data, pos, b':')? as usize; + let bytes = data + .get(*pos..*pos + len) + .ok_or_else(|| BencodeError { + msg: "bencoded string length exceeds buffer".to_string(), + })? + .to_vec(); + *pos += len; + Ok(bytes) + } + + fn parse_int_until(data: &[u8], pos: &mut usize, terminator: u8) -> Result { + let start = *pos; + while let Some(&b) = data.get(*pos) { + if b == terminator { + let text = std::str::from_utf8(&data[start..*pos]).map_err(|e| BencodeError { + msg: format!("invalid bencode integer: {e}"), + })?; + let n: i64 = text.parse().map_err(|e| BencodeError { + msg: format!("invalid bencode integer '{text}': {e}"), + })?; + *pos += 1; + return Ok(n); + } + *pos += 1; + } + Err(BencodeError { + msg: "bencoded integer missing terminator".to_string(), + }) + } +} + +/// Stateless-after-construction torrent release parser. Compile the regexes +/// once and reuse across many titles / metainfo blobs. +pub struct TorrentParser { + magnet_resolver: Option>, + db: Option, + client: reqwest::Client, + semaphore: Arc, + bitrate: Regex, + /// Hi-res patterns tried in order; groups are (bit_depth, sample_rate_khz). + hires: Vec, + source: Regex, + rip_type: Regex, + track_number_prefix: Regex, + /// Strips `[...]` tags, `(YYYY...)` groups and stray format/quality tokens + /// when isolating the album title. + tag_strip: Regex, + year: Regex, +} + +impl std::fmt::Debug for TorrentParser { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TorrentParser").finish_non_exhaustive() + } +} + +impl Default for TorrentParser { + fn default() -> Self { + Self::new() + } +} + +impl TorrentParser { + pub fn new() -> Self { + Self::new_with_resolver(None, None) + } + + pub fn new_with_resolver( + magnet_resolver: Option>, + db: Option, + ) -> Self { + Self { + magnet_resolver, + db, + client: reqwest::Client::new(), + semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_RESOLVES)), + bitrate: Regex::new(r"(?i)(\d{2,3})\s*kbps").unwrap(), + hires: vec![ + Regex::new(r"(?i)(\d{1,2})\s*[Bb]it\s*[-/]?\s*(\d{2,3}(?:\.\d)?)\s*[kK][Hh][Zz]") + .unwrap(), + Regex::new( + r"(?i)\[?\s*(?:FLAC|Flac)\s+(\d{1,2})\s*[-/]\s*(\d{2,3}(?:\.\d)?)\s*\]?", + ) + .unwrap(), + Regex::new(r"(?i)\[?\s*(\d{1,2})\s*[Bb]it\s*\]?").unwrap(), + // Bare bracketed quality like `[24/96]` or `[16/44.1]` + // (1-2 bit-depth digits, separator, 2-3 digit kHz value). + Regex::new(r"\[(\d{1,2})[/\-](\d{2,3}(?:\.\d)?)\]").unwrap(), + ], + source: Regex::new( + r"(?i)\[(CD|WEB|Vinyl|LP|Cassette|MC|DVD|Blu-?Ray|SACD|DAT)\]", + ) + .unwrap(), + rip_type: Regex::new(r"(?i)(vinyl\s*rip|SACD[- ]?R|HDCD|DSD\d*|tape\s*rip)").unwrap(), + track_number_prefix: Regex::new(r"^\d{1,3}[\s.\-]+").unwrap(), + tag_strip: Regex::new( + r"\[[^\]]*\]|\([^)]*\)|(?i)\b(FLAC|MP3|AAC|M4A|APE|WavPack|ALAC|OGG|WAV|DSD|lossless|tracks|image\+\.cue|V0|V2|320|192|256|128|CBR|VBR)\b", + ) + .unwrap(), + year: Regex::new(r"\b(19[5-9]\d|20[0-4]\d)\b").unwrap(), + } + } + + /// Fast path: parse titles and return title-only results immediately, + /// spawning detached background tasks that resolve full torrent metadata + /// per item. Call on `Arc` so tasks share state. + pub async fn parse_search_response( + self: &Arc, + response: &SearchResponse, + ) -> Vec { + let results: Vec = + response.item.iter().map(|i| self.parse_item(i)).collect(); + + for (idx, item) in response.item.iter().enumerate() { + let row_id = if let Some(pool) = &self.db { + match crate::db::insert_partial(pool, &results[idx], &item.url).await { + Ok(id) => Some(id), + Err(e) => { + tracing::warn!(error = %e, "failed to insert partial torrent"); + None + } + } + } else { + None + }; + + let parser = Arc::clone(self); + let item = item.clone(); + tokio::spawn(async move { + let resolved = parser.resolve_detailed(item).await; + tracing::info!( + info_hash = %resolved.info_hash, + artist = %resolved.artist, + album = %resolved.album, + track_count = resolved.track_count, + audio_file_count = resolved.audio_file_count, + error_count = resolved.parse_errors.len(), + "background torrent resolution complete" + ); + if let (Some(id), Some(pool)) = (row_id, parser.db.as_ref()) { + if let Err(e) = crate::db::update_resolved(pool, id, &resolved).await { + tracing::warn!(error = %e, "failed to update resolved torrent"); + } + } + }); + } + + results + } + + async fn resolve_detailed(&self, item: SearchItem) -> ParsedTorrent { + let mut r = self.parse_title(&item.name); + let _permit = match self.semaphore.clone().acquire_owned().await { + Ok(p) => p, + Err(_) => { + r.record_error("resolution semaphore closed"); + return r; + } + }; + match fetch_item(&self.client, &self.magnet_resolver, &item.url).await { + FetchOutcome::Bytes { bytes } => self.overlay_torrent_files(&mut r, &bytes), + FetchOutcome::MagnetSalvaged { info_hash } => { + r.info_hash = info_hash; + r.record_error("magnet resolved to info-hash only; file list unavailable"); + } + FetchOutcome::Failed { error } => r.record_error(error), + } + self.finalize(&mut r); + r + } + + /// Title-only fast path: no network, no torrent-byte resolution. Fields + /// that require the file list (track names, info hash, sizes) stay empty. + pub fn parse_search_titles(&self, response: &SearchResponse) -> Vec { + response.item.iter().map(|i| self.parse_item(i)).collect() + } + + pub fn parse_item(&self, item: &SearchItem) -> ParsedTorrent { + self.parse_title(&item.name) + } + + pub fn parse_title(&self, title: &str) -> ParsedTorrent { + let mut r = ParsedTorrent { + raw_title: title.to_string(), + ..Default::default() + }; + self.fill_from_title(&mut r, title); + self.finalize(&mut r); + r + } + + /// Parse raw torrent metainfo bytes (bencoded), optionally enriched by + /// metadata `album`. Mirrors the Go `GenericParser::ParseTorrent`. + /// + /// `album` (when provided) takes precedence for identity fields; the + /// torrent file list is the sole source for track names, sizes, info-hash + /// and cover/cue/log flags. + pub fn parse_torrent_data(&self, torrent_data: &[u8], album: Option<&Album>) -> ParsedTorrent { + let mut r = ParsedTorrent::default(); + if let Some(album) = album { + self.fill_from_metadata(&mut r, album); + } + self.fill_from_torrent_data(&mut r, torrent_data); + self.finalize(&mut r); + r + } + + fn finalize(&self, r: &mut ParsedTorrent) { + r.parsed_successfully = !r.artist.is_empty() && !r.album.is_empty(); + if !r.parsed_successfully + && !r + .parse_errors + .iter() + .any(|e| e == "missing artist or album") + { + r.parse_errors.push("missing artist or album".to_string()); + } + } +} + +enum FetchOutcome { + Bytes { bytes: Vec }, + MagnetSalvaged { info_hash: String }, + Failed { error: String }, +} + +impl FetchOutcome { + fn failed(msg: impl Into) -> Self { + Self::Failed { error: msg.into() } + } +} + +async fn fetch_item( + client: &reqwest::Client, + resolver: &Option>, + url: &str, +) -> FetchOutcome { + if url.starts_with("magnet:") { + return resolve_magnet(resolver, url).await; + } + let resp = match client.get(url).send().await { + Ok(r) => r, + Err(e) => return FetchOutcome::failed(format!("http fetch failed: {e}")), + }; + if resp.status().is_redirection() { + let loc = resp + .headers() + .get(reqwest::header::LOCATION) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + if loc.starts_with("magnet:") { + return resolve_magnet(resolver, loc).await; + } + return FetchOutcome::failed(format!("unsupported redirect target: {loc}")); + } + if let Err(e) = resp.error_for_status_ref() { + return FetchOutcome::failed(format!("http status error: {e}")); + } + match resp.bytes().await { + Ok(b) if !b.is_empty() => FetchOutcome::Bytes { bytes: b.to_vec() }, + Ok(_) => FetchOutcome::failed("empty response body"), + Err(e) => FetchOutcome::failed(format!("error reading body: {e}")), + } +} + +async fn resolve_magnet(resolver: &Option>, magnet: &str) -> FetchOutcome { + if let Some(r) = resolver { + match r.resolve(magnet).await { + Ok(bytes) => return FetchOutcome::Bytes { bytes }, + Err(e) => { + warn!(magnet = %magnet, error = %e, "magnet resolution failed; salvaging info-hash") + } + } + } + match infohash_from_magnet(magnet) { + Some(h) => FetchOutcome::MagnetSalvaged { info_hash: h }, + None => FetchOutcome::failed(salvage_failure_reason(resolver)), + } +} + +fn salvage_failure_reason(resolver: &Option>) -> &'static str { + match resolver { + Some(_) => "magnet resolution failed and no parseable info-hash", + None => "magnet link with no resolver configured and no parseable info-hash", + } +} + +// Extracts the 40-hex BTv1 info-hash from a magnet's `xt=urn:btih:` param. +// Base32-encoded hashes are intentionally unsupported. +fn infohash_from_magnet(magnet: &str) -> Option { + let key = "xt=urn:btih:"; + let rest = magnet.split(key).nth(1)?; + let hash = rest.split('&').next()?; + if hash.len() == 40 && hash.bytes().all(|b| b.is_ascii_hexdigit()) { + Some(hash.to_ascii_lowercase()) + } else { + None + } +} + +impl TorrentParser { + fn fill_from_metadata(&self, r: &mut ParsedTorrent, album: &Album) { + if !album.title.is_empty() { + r.album = album.title.clone(); + } + + if let Some(credit) = album.artists.first() { + if let Some(artist) = credit.artist.as_ref() { + r.artist = artist.name.clone(); + } + } + + if album.release_date.len() >= 4 { + if let Ok(year) = album.release_date[..4].parse::() { + r.year = year; + } + } + + r.release_type = match album.album_type.to_lowercase().as_str() { + "album" => ReleaseType::Album, + "ep" => ReleaseType::EP, + "single" => ReleaseType::Single, + "compilation" => ReleaseType::Compilation, + "soundtrack" => ReleaseType::Soundtrack, + "live" => ReleaseType::Live, + _ => r.release_type, + }; + + for g in &album.genres { + r.genres.push(g.name.clone()); + } + + if let Some(label) = album.label.as_ref() { + r.label = label.name.clone(); + } + + if album.total_tracks > 0 { + r.track_count = album.total_tracks as u32; + } + if album.total_discs > 0 { + r.release_count = album.total_discs as u32; + } + + trace!( + artist = %r.artist, + album = %r.album, + year = r.year, + release_type = r.release_type.as_str(), + "torrent parser: filled from metadata" + ); + } + + fn fill_from_torrent_data(&self, r: &mut ParsedTorrent, torrent_data: &[u8]) { + if torrent_data.is_empty() { + return; + } + + let root = match bencode::parse(torrent_data) { + Ok(root) => root, + Err(e) => { + r.record_error(format!("failed to parse torrent: {e}")); + return; + } + }; + + let Some(info) = root.dict_get("info") else { + r.record_error("torrent metainfo missing 'info' dictionary"); + return; + }; + + r.info_hash = hex_sha1(&torrent_data[info.start..info.end]); + + let name = info.dict_get("name").and_then(|n| n.as_str()).unwrap_or(""); + if !name.is_empty() { + r.raw_title = name.to_string(); + } + + if let Some(files) = self.extract_file_list(info, name) { + r.format = files.format; + r.audio_file_count = files.audio_file_count; + r.total_audio_size = files.total_audio_size; + r.track_names = files.track_names; + r.has_cover_art = files.has_cover_art; + r.has_cue_sheet = files.has_cue_sheet; + r.has_rip_log = files.has_rip_log; + } + + if r.has_rip_log && r.source == ReleaseSource::Unknown { + r.source = ReleaseSource::CD; + } + + if r.track_count == 0 { + r.track_count = r.audio_file_count; + } + + self.fill_from_title(r, name); + self.deduce_from_file_size(r); + + trace!( + info_hash = %r.info_hash, + format = r.format.as_str(), + audio_files = r.audio_file_count, + audio_size = r.total_audio_size, + cover = r.has_cover_art, + cue = r.has_cue_sheet, + log = r.has_rip_log, + bit_depth = r.bit_depth, + sample_rate = r.sample_rate, + bitrate = %r.bitrate, + "torrent parser: filled from torrent" + ); + } + + /// Returns `None` when the torrent contains no audio files. + fn extract_file_list( + &self, + info: &BencodeNode, + single_file_name: &str, + ) -> Option { + let mut format_counts: HashMap = HashMap::new(); + let mut format_sizes: HashMap = HashMap::new(); + let mut track_names: Vec = Vec::new(); + let mut has_cover_art = false; + let mut has_cue_sheet = false; + let mut has_rip_log = false; + + let entries: Vec<(String, u64)> = + if let Some(files) = info.dict_get("files").and_then(|f| f.as_list()) { + files + .iter() + .map(|file| { + let length = file + .dict_get("length") + .and_then(|l| l.as_int()) + .unwrap_or(0) as u64; + let path = file + .dict_get("path") + .and_then(|p| p.as_list()) + .map(|segments| { + segments + .iter() + .filter_map(|s| s.as_str()) + .collect::>() + .join("/") + }) + .unwrap_or_default(); + (path, length) + }) + .collect() + } else { + let length = info + .dict_get("length") + .and_then(|l| l.as_int()) + .unwrap_or(0) as u64; + vec![(single_file_name.to_string(), length)] + }; + + for (path, length) in entries { + let ext = extension_lower(&path).to_string(); + let stem = file_stem(&path); + if let Some(fmt) = format_for_extension(&ext) { + *format_counts.entry(fmt).or_default() += 1; + *format_sizes.entry(fmt).or_default() += length; + track_names.push(self.clean_track_name(stem)); + } + match ext.as_str() { + ".jpg" | ".jpeg" | ".png" => has_cover_art = true, + ".cue" => has_cue_sheet = true, + ".log" => has_rip_log = true, + _ => {} + } + } + + let (&dominant, &count) = format_counts + .iter() + .max_by_key(|(_, c)| *c) + .map(|(k, v)| (k, v))?; + + Some(TorrentFileList { + format: dominant, + audio_file_count: count, + total_audio_size: format_sizes.get(&dominant).copied().unwrap_or(0), + track_names, + has_cover_art, + has_cue_sheet, + has_rip_log, + }) + } + + /// Overlay file-list facts onto a title-parsed result without disturbing + /// identity fields already derived from the title. + fn overlay_torrent_files(&self, r: &mut ParsedTorrent, torrent_data: &[u8]) { + let root = match bencode::parse(torrent_data) { + Ok(root) => root, + Err(e) => { + r.record_error(format!("failed to parse torrent: {e}")); + return; + } + }; + let Some(info) = root.dict_get("info") else { + r.record_error("torrent metainfo missing 'info' dictionary"); + return; + }; + + r.info_hash = hex_sha1(&torrent_data[info.start..info.end]); + + let name = info.dict_get("name").and_then(|n| n.as_str()).unwrap_or(""); + if let Some(files) = self.extract_file_list(info, name) { + r.format = files.format; + r.audio_file_count = files.audio_file_count; + r.total_audio_size = files.total_audio_size; + r.track_names = files.track_names; + r.has_cover_art = files.has_cover_art; + r.has_cue_sheet = files.has_cue_sheet; + r.has_rip_log = files.has_rip_log; + } + + if r.has_rip_log && r.source == ReleaseSource::Unknown { + r.source = ReleaseSource::CD; + } + if r.track_count == 0 { + r.track_count = r.audio_file_count; + } + + self.deduce_from_file_size(r); + + trace!( + info_hash = %r.info_hash, + format = r.format.as_str(), + audio_files = r.audio_file_count, + tracks = r.track_count, + bit_depth = r.bit_depth, + sample_rate = r.sample_rate, + bitrate = %r.bitrate, + "torrent parser: overlaid torrent file list" + ); + } +} + +impl TorrentParser { + fn fill_from_title(&self, r: &mut ParsedTorrent, title: &str) { + if title.is_empty() { + return; + } + + if r.year == 0 { + if let Some(m) = self.year.captures(title) { + if let Some(y) = m.get(1).and_then(|s| s.as_str().parse::().ok()) { + r.year = y; + } + } + } + + if r.bitrate.is_empty() { + if let Some(m) = self.bitrate.captures(title) { + if let Some(v) = m.get(1) { + r.bitrate = format!("{} kbps", v.as_str()); + } + } + } + + for pattern in &self.hires { + let Some(m) = pattern.captures(title) else { + continue; + }; + if r.bit_depth == 0 { + if let Some(bd) = m.get(1).and_then(|s| s.as_str().parse::().ok()) { + r.bit_depth = bd; + } + } + if r.sample_rate == 0 { + if let Some(sr_khz) = m.get(2).and_then(|s| s.as_str().parse::().ok()) { + r.sample_rate = (sr_khz * 1000.0).round() as u32; + } + } + if r.bit_depth > 0 { + break; + } + } + + if r.source == ReleaseSource::Unknown { + if let Some(m) = self.source.captures(title) { + if let Some(s) = m.get(1).map(|s| s.as_str()) { + r.source = match s.to_ascii_uppercase().as_str() { + "CD" => ReleaseSource::CD, + "WEB" => ReleaseSource::WEB, + "VINYL" | "LP" => ReleaseSource::Vinyl, + "CASSETTE" | "MC" => ReleaseSource::Cassette, + "DVD" => ReleaseSource::DVD, + "BLU-RAY" | "BLURAY" => ReleaseSource::BluRay, + "SACD" => ReleaseSource::SACD, + "DAT" => ReleaseSource::DAT, + _ => ReleaseSource::Unknown, + }; + } + } + } + + if r.rip_type.is_empty() { + if let Some(m) = self.rip_type.captures(title) { + if let Some(v) = m.get(1) { + r.rip_type = v.as_str().to_string(); + } + } + } + + if r.format == AudioFormat::Unknown { + r.format = detect_format_from_title(title); + } + + if r.artist.is_empty() || r.album.is_empty() { + let (artist, album) = self.extract_artist_album(title); + if r.artist.is_empty() { + r.artist = artist; + } + if r.album.is_empty() { + r.album = album; + } + } + + trace!( + bitrate = %r.bitrate, + bit_depth = r.bit_depth, + sample_rate = r.sample_rate, + source = r.source.as_str(), + rip_type = %r.rip_type, + format = r.format.as_str(), + artist = %r.artist, + album = %r.album, + year = r.year, + "torrent parser: filled from title" + ); + } + + /// Split a torrent title into `(artist, album)` via the conventional + /// `Artist - Album` separator, then strip year/format/source tags from + /// the album portion. Returns empty strings when no reliable split is + /// possible. + fn extract_artist_album(&self, title: &str) -> (String, String) { + let Some(sep) = [" - ", " – ", " — "] + .iter() + .copied() + .find(|s| title.contains(s)) + else { + return (String::new(), String::new()); + }; + + let Some(idx) = title.find(sep) else { + return (String::new(), String::new()); + }; + + let artist = self.strip_tags(title[..idx].trim()); + let rest = &title[idx + sep.len()..]; + let album_raw = rest.split(" - ").next().unwrap_or(rest); + let album_raw = album_raw.split(',').next().unwrap_or(album_raw); + let album = self + .strip_tags(album_raw) + .trim() + .trim_matches(|c: char| c == '-' || c == '.') + .to_string(); + + if artist.is_empty() || album.is_empty() { + return (String::new(), String::new()); + } + (artist, album) + } + + fn strip_tags(&self, s: &str) -> String { + let stripped = self.tag_strip.replace_all(s, ""); + clean_separators(&stripped) + } + + fn clean_track_name(&self, name: &str) -> String { + let cleaned = self.track_number_prefix.replace_all(name, ""); + let cleaned = cleaned.trim(); + if cleaned.is_empty() { + name.trim().to_string() + } else { + cleaned.to_string() + } + } + + fn deduce_from_file_size(&self, r: &mut ParsedTorrent) { + if r.audio_file_count == 0 || r.total_audio_size == 0 { + return; + } + + let avg_bytes = r.total_audio_size / r.audio_file_count as u64; + let avg_mb = avg_bytes as f64 / (1024.0 * 1024.0); + + if r.format.is_lossless() { + if r.bit_depth > 0 && r.sample_rate > 0 { + return; + } + // Average FLAC size per ~4 min track: + // 16/44.1 ~ 25-35 MB 24/48 ~ 40-60 MB + // 24/96 ~ 80-120 MB 24/192 ~ 160-240 MB + match avg_mb { + m if m >= 130.0 => r.set_quality_if_missing(24, 192_000), + m if m >= 65.0 => r.set_quality_if_missing(24, 96_000), + m if m >= 38.0 => r.set_quality_if_missing(24, 48_000), + _ => r.set_quality_if_missing(16, 44_100), + } + trace!( + avg_file_mb = avg_mb, + deduced_bit_depth = r.bit_depth, + deduced_sample_rate = r.sample_rate, + "torrent parser: deduced lossless quality from file size" + ); + } else if r.format == AudioFormat::MP3 { + if !r.bitrate.is_empty() { + return; + } + // Average MP3 size per ~4 min track: + // 128 kbps ~ 3.5-4 MB 192 kbps ~ 5-6 MB + // 256 kbps ~ 7-8 MB 320 kbps ~ 9-10 MB + r.bitrate = match avg_mb { + m if m >= 8.5 => "320 kbps", + m if m >= 6.5 => "256 kbps", + m if m >= 4.5 => "192 kbps", + _ => "128 kbps", + } + .to_string(); + trace!( + avg_file_mb = avg_mb, + deduced_bitrate = %r.bitrate, + "torrent parser: deduced MP3 bitrate from file size" + ); + } + } +} + +fn extension_lower(path: &str) -> &str { + match path.rfind('.') { + Some(idx) => &path[idx..], + None => "", + } +} + +fn file_stem(path: &str) -> &str { + let base = path.rsplit('/').next().unwrap_or(path); + match base.rfind('.') { + Some(idx) => &base[..idx], + None => base, + } +} + +fn clean_separators(s: &str) -> String { + let cleaned: String = s + .chars() + .map(|c| if c == '.' || c == '_' { ' ' } else { c }) + .collect(); + collapse_whitespace(&cleaned) +} + +fn collapse_whitespace(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut prev_space = false; + for ch in s.trim().chars() { + if ch.is_whitespace() { + if !prev_space { + out.push(' '); + prev_space = true; + } + } else { + out.push(ch); + prev_space = false; + } + } + out +} + +/// Conservative title-token format detection: only matches codec names that +/// are unlikely to appear as substrings of other words. +fn detect_format_from_title(title: &str) -> AudioFormat { + let lower = title.to_ascii_lowercase(); + if lower.contains("wavpack") { + return AudioFormat::WavPack; + } + if lower.contains("flac") { + return AudioFormat::FLAC; + } + if lower.contains("alac") { + return AudioFormat::ALAC; + } + if lower.contains("ape") { + return AudioFormat::APE; + } + if lower.contains("aac") || lower.contains("m4a") { + return AudioFormat::AAC; + } + if lower.contains("ogg") { + return AudioFormat::OGG; + } + // A bare "mp3" token or an explicit kbps bitrate strongly implies MP3. + if lower.contains("mp3") || lower.contains("kbps") { + return AudioFormat::MP3; + } + AudioFormat::Unknown +} + +fn hex_sha1(bytes: &[u8]) -> String { + use sha1::{Digest, Sha1}; + let digest = Sha1::digest(bytes); + let mut out = String::with_capacity(40); + for b in digest.iter() { + out.push_str(&format!("{b:02x}")); + } + out +}