diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cadd6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.cache/ +.cache +config.toml +!config.example.toml +/target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..78ba632 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4236 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.13.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fb99565819980999fb7b4a1796046a5c949e6d4ff132cf5fadf5a641e20d776" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f2392eae7f16557a3d727ef3a12e57b2b2ca6f98566a5f4fb41ffe305df077" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.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 = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.13.1", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "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 2.0.119", +] + +[[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 2.0.119", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[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 = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "libc" +version = "0.2.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22053b6a34f84abc97f9129e61334f40174659a1b9bd18c970b83db6a9a6348b" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "niri-ipc" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa541cad3b426dd1ab72765ba71d1c20a1b8a17d80c692b099a759074f74242" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[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-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "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_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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "ratatui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" +dependencies = [ + "bitflags 2.13.1", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +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 0.22.1", + "bytes", + "futures-channel", + "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 = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "riptune" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "reqwest", + "riptune-cache", + "riptune-core", + "riptune-mpris", + "riptune-niri", + "riptune-tui", + "riptune-types", + "rodio", + "serde", + "tokio", + "toml", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "riptune-cache" +version = "0.1.0" +dependencies = [ + "riptune-core", + "sqlx", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "riptune-core" +version = "0.1.0" +dependencies = [ + "md-5", + "rand 0.8.7", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "riptune-mpris" +version = "0.1.0" +dependencies = [ + "riptune-types", + "tokio", + "tracing", + "zbus", +] + +[[package]] +name = "riptune-niri" +version = "0.1.0" +dependencies = [ + "niri-ipc", + "riptune-types", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "riptune-tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm", + "futures-util", + "ratatui", + "riptune-cache", + "riptune-core", + "riptune-types", + "tokio", + "tracing", +] + +[[package]] +name = "riptune-types" +version = "0.1.0" + +[[package]] +name = "rodio" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[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", +] + +[[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", +] + +[[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 = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "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 = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.13.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.7", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.13.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.7", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 1.0.69", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[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 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[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 = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +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", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +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]] +name = "try-lock" +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 = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +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 = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +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" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[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.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[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 = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.7", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c657716 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,63 @@ +[workspace] +resolver = "2" +members = [ + "crates/riptune-core", + "crates/riptune-cache", + "crates/riptune-mpris", + "crates/riptune-niri", + "crates/riptune-tui", + "crates/riptune-types", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "GPL-3.0" +repository = "https://github.com/Quinta0/riptune" +authors = ["Quintavalle Pietro"] + + +[workspace.dependencies] +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } +anyhow = "1" +thiserror = "1" +tracing = "0.1" +tracing-subscriber = "0.3" + +[package] +name = "riptune" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "A niri-native TUI music client for Subsonic servers. Rip and tune." + +[[bin]] +name = "riptune" +path = "src/main.rs" + +[dependencies] +riptune-core = { path = "crates/riptune-core" } +riptune-cache = { path = "crates/riptune-cache" } +riptune-mpris = { path = "crates/riptune-mpris" } +riptune-niri = { path = "crates/riptune-niri" } +riptune-tui = { path = "crates/riptune-tui" } +riptune-types = { path = "crates/riptune-types" } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +serde = { workspace = true } +toml = "0.8" +clap = { version = "4", features = ["derive"] } +# Pinned exactly: rodio 0.21 reworked the OutputStream/Sink API around a new +# Mixer type (Sink::try_new -> Sink::connect_new, etc). Pinning avoids that +# rewrite landing under us; bump deliberately and update audio.rs to match. +rodio = { version = "=0.20.1", features = ["symphonia-all"] } +# "blocking" is additive on top of the workspace-defined reqwest (which has +# default-features = false + rustls-tls). Used only by the audio thread's +# synchronous fetch -- see the comment in src/audio.rs for why blocking is +# actually the right call there, not a shortcut. +reqwest = { workspace = true, features = ["blocking"] } \ No newline at end of file diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..40d16b0 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,11 @@ +[server] +url = "https://your-navidrome-server.com" +username = "your_username" +password = "your_password_or_api_key" + +[niri] +enabled = true +workspace_notifications = true + +[cache] +path = "~/.cache/riptune/library.sqlite" diff --git a/crates/riptune-cache/Cargo.toml b/crates/riptune-cache/Cargo.toml new file mode 100644 index 0000000..2217434 --- /dev/null +++ b/crates/riptune-cache/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "riptune-cache" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "SQLite-backed local index for instant TUI startup on large libraries" + +[dependencies] +riptune-core = { path = "../riptune-core" } +tokio = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } diff --git a/crates/riptune-cache/src/lib.rs b/crates/riptune-cache/src/lib.rs new file mode 100644 index 0000000..36a92cf --- /dev/null +++ b/crates/riptune-cache/src/lib.rs @@ -0,0 +1,244 @@ +//! Local SQLite index of the remote Subsonic library. +//! +//! Why this exists: hitting `getArtists`/`getAlbum` over the network on every TUI +//! launch is slow on large libraries (multi-second waits are common on Airsonic +//! instances with 50k+ tracks). We index artists/albums/tracks locally and treat +//! the network as a background sync source, not the read path for rendering[cite: 13]. + +use riptune_core::SubsonicClient; +use riptune_core::models::{Artist, Track, AlbumSummary}; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::SqlitePool; +use sqlx::Row; +use std::path::Path; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("database error: {0}")] + Db(#[from] sqlx::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("network or client error: {0}")] + Client(String), +} + +#[derive(Clone)] +pub struct Cache { + pool: SqlitePool, +} + +impl Cache { + pub async fn open(path: &Path) -> Result { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let url = format!("sqlite://{}?mode=rwc", path.display()); + let pool = SqlitePoolOptions::new().max_connections(4).connect(&url).await?; + + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS artists ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + album_count INTEGER NOT NULL DEFAULT 0 + ); + "#, + ) + .execute(&pool) + .await?; + + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS albums ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + song_count INTEGER NOT NULL DEFAULT 0, + artist_id TEXT NOT NULL + ); + "#, + ) + .execute(&pool) + .await?; + + // FIXED: Changed starred format to TEXT to cleanly accommodate Subsonic String timestamps[cite: 14] + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS tracks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT, + album TEXT, + duration_secs INTEGER, + starred TEXT + ); + "#, + ) + .execute(&pool) + .await?; + + Ok(Self { pool }) + } + + pub async fn get_albums_by_artist(&self, artist_id: &str) -> Result, CacheError> { + let rows = sqlx::query( + "SELECT id, name, song_count FROM albums WHERE artist_id = ? ORDER BY name ASC" + ) + .bind(artist_id) + .fetch_all(&self.pool) + .await?; + + let records = rows.into_iter().map(|row| { + let count: i64 = row.get("song_count"); + AlbumSummary { + id: row.get("id"), + name: row.get("name"), + song_count: count as u32, + } + }).collect(); + + Ok(records) + } + + pub async fn get_tracks_by_album(&self, album_name: &str) -> Result, CacheError> { + // FIXED: Added CAST protection to match what we did in the search query! + let rows = sqlx::query( + "SELECT id, title, artist, album, duration_secs, CAST(starred AS TEXT) as starred FROM tracks WHERE album = ? ORDER BY title ASC" + ) + .bind(album_name) + .fetch_all(&self.pool) + .await?; + + let records = rows.into_iter().map(|row| { + let duration: Option = row.get("duration_secs"); + Track { + id: row.get("id"), + title: row.get("title"), + artist: row.get("artist"), + album: row.get("album"), + duration_secs: duration.map(|d| d as u32), + starred: row.get("starred"), + } + }).collect(); + + Ok(records) + } + + pub async fn search_local_artists(&self, query: &str) -> Result, CacheError> { + let sql_pattern = format!("%{}%", query); + let rows = sqlx::query( + "SELECT id, name, album_count FROM artists WHERE name LIKE ? COLLATE NOCASE ORDER BY name ASC" + ) + .bind(sql_pattern) + .fetch_all(&self.pool) + .await?; + + let records = rows.into_iter().map(|row| { + let count: i64 = row.get("album_count"); + Artist { + id: row.get("id"), + name: row.get("name"), + album_count: count as u32, + cover_art: None, + } + }).collect(); + + Ok(records) + } + + pub async fn search_local_albums(&self, query: &str) -> Result, CacheError> { + let sql_pattern = format!("%{}%", query); + let rows = sqlx::query( + "SELECT id, name, song_count FROM albums WHERE name LIKE ? COLLATE NOCASE ORDER BY name ASC" + ) + .bind(sql_pattern) + .fetch_all(&self.pool) + .await?; + + let records = rows.into_iter().map(|row| { + let count: i64 = row.get("song_count"); + AlbumSummary { + id: row.get("id"), + name: row.get("name"), + song_count: count as u32, + } + }).collect(); + + Ok(records) + } + + pub async fn search_local_tracks(&self, query: &str) -> Result, CacheError> { + let sql_pattern = format!("%{}%", query); + // FIXED: Using CAST forces SQLite to evaluate the column as a string primitive + // regardless of whether the internal row data was stored as an integer or text. + let rows = sqlx::query( + "SELECT id, title, artist, album, duration_secs, CAST(starred AS TEXT) as starred FROM tracks WHERE title LIKE ? COLLATE NOCASE ORDER BY title ASC" + ) + .bind(sql_pattern) + .fetch_all(&self.pool) + .await?; + + let records = rows.into_iter().map(|row| { + let duration: Option = row.get("duration_secs"); + Track { + id: row.get("id"), + title: row.get("title"), + artist: row.get("artist"), + album: row.get("album"), + duration_secs: duration.map(|d| d as u32), + starred: row.get("starred"), + } + }).collect(); + + Ok(records) + } +} + +/// Pulls the full artist/album/track tree from the server and upserts it into +/// the local cache. Intended to run as a background task on startup, and +/// periodically thereafter — never blocks the TUI's first paint[cite: 13]. +pub async fn sync_library(client: &SubsonicClient, cache: &Cache) -> Result<(), CacheError> { + let remote_artists = client.get_artists().await + .map_err(|e| CacheError::Client(e.to_string()))?; + + for artist in remote_artists { + let mut tx = cache.pool.begin().await?; + + sqlx::query("INSERT OR REPLACE INTO artists (id, name, album_count) VALUES (?, ?, ?)") + .bind(&artist.id) + .bind(&artist.name) + .bind(artist.album_count) + .execute(&mut *tx) + .await?; + + if let Ok(albums) = client.get_artist_albums(&artist.id).await { + for album in albums { + sqlx::query("INSERT OR REPLACE INTO albums (id, name, song_count, artist_id) VALUES (?, ?, ?, ?)") + .bind(&album.id) + .bind(&album.name) + .bind(album.song_count) + .bind(&artist.id) + .execute(&mut *tx) + .await?; + + if let Ok(full_album) = client.get_album(&album.id).await { + for song in full_album.song { + sqlx::query("INSERT OR REPLACE INTO tracks (id, title, artist, album, duration_secs, starred) VALUES (?, ?, ?, ?, ?, ?)") + .bind(&song.id) + .bind(&song.title) + .bind(&song.artist) + .bind(&song.album) + .bind(song.duration_secs) + .bind(&song.starred) // Safely saves text strings directly into SQLite schema rows[cite: 14] + .execute(&mut *tx) + .await?; + } + } + } + } + + tx.commit().await?; + } + + Ok(()) +} \ No newline at end of file diff --git a/crates/riptune-core/Cargo.toml b/crates/riptune-core/Cargo.toml new file mode 100644 index 0000000..17837d8 --- /dev/null +++ b/crates/riptune-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "riptune-core" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Async Subsonic API client (auth, browsing, streaming URLs)" + +[dependencies] +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +md-5 = "0.10" +rand = "0.8" +url = "2" + diff --git a/crates/riptune-core/src/lib.rs b/crates/riptune-core/src/lib.rs new file mode 100644 index 0000000..45fc293 --- /dev/null +++ b/crates/riptune-core/src/lib.rs @@ -0,0 +1,225 @@ +//! Lightweight async wrapper over the Subsonic REST API (compatible with +//! Navidrome, Airsonic, Gonic, etc). +//! +//! Handles the legacy salted-token auth scheme (`t = md5(password + salt)`), since +//! not all servers support Navidrome's newer API-key auth yet. See +//! for the wire format. + +pub mod models; + +use md5::{Digest, Md5}; +use rand::Rng; +use thiserror::Error; + +const API_VERSION: &str = "1.16.1"; +const CLIENT_NAME: &str = "riptune"; + +#[derive(Debug, Error)] +pub enum SubsonicError { + #[error("request failed: {0}")] + Request(#[from] reqwest::Error), + #[error("server returned error {code}: {message}")] + Api { code: i32, message: String }, + #[error("invalid server URL: {0}")] + InvalidUrl(#[from] url::ParseError), +} + +#[derive(Clone)] +pub struct SubsonicClient { + http: reqwest::Client, + base_url: url::Url, + username: String, + password: String, +} + +impl SubsonicClient { + pub fn new(base_url: &str, username: &str, password: &str) -> Result { + Ok(Self { + http: reqwest::Client::new(), + base_url: url::Url::parse(base_url)?, + username: username.to_string(), + password: password.to_string(), + }) + } + + /// Builds the auth query params required on every request: `u`, `t`, `s`, `v`, `c`, `f`. + fn auth_params(&self) -> Vec<(String, String)> { + let salt: String = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(12) + .map(char::from) + .collect(); + let mut hasher = Md5::new(); + hasher.update(format!("{}{}", self.password, salt)); + let token = format!("{:x}", hasher.finalize()); + + vec![ + ("u".into(), self.username.clone()), + ("t".into(), token), + ("s".into(), salt), + ("v".into(), API_VERSION.into()), + ("c".into(), CLIENT_NAME.into()), + ("f".into(), "json".into()), + ] + } + + /// Returns a fully-authenticated streaming URL for a track ID. Handed directly + /// to the audio thread's HTTP fetcher — no extra round trip needed. + pub fn stream_url(&self, track_id: &str) -> Result { + let mut url = self.base_url.join("rest/stream")?; + { + let mut qp = url.query_pairs_mut(); + for (k, v) in self.auth_params() { + qp.append_pair(&k, &v); + } + qp.append_pair("id", track_id); + } + Ok(url.to_string()) + } + + /// Sends an authenticated GET to `rest/{endpoint}`, unwraps the + /// `subsonic-response` envelope, and either deserializes the payload into + /// `T` or converts a server-side error into `SubsonicError::Api`. + /// + /// All Subsonic endpoints share this envelope shape, so every real method + /// (`get_artists`, `get_album`, `ping`, ...) is a thin wrapper around this. + async fn call( + &self, + endpoint: &str, + extra_params: &[(&str, &str)], + ) -> Result { + let mut url = self.base_url.join(&format!("rest/{endpoint}"))?; + { + let mut qp = url.query_pairs_mut(); + for (k, v) in self.auth_params() { + qp.append_pair(&k, &v); + } + for (k, v) in extra_params { + qp.append_pair(k, v); + } + } + + let body: serde_json::Value = self.http.get(url).send().await?.json().await?; + let root = body.get("subsonic-response").ok_or_else(|| SubsonicError::Api { + code: -1, + message: "response was missing the subsonic-response envelope".into(), + })?; + + let status = root.get("status").and_then(|s| s.as_str()).unwrap_or(""); + if status != "ok" { + let error = root.get("error"); + let code = error.and_then(|e| e.get("code")).and_then(|c| c.as_i64()).unwrap_or(-1) as i32; + let message = error + .and_then(|e| e.get("message")) + .and_then(|m| m.as_str()) + .unwrap_or("server returned an unspecified error") + .to_string(); + return Err(SubsonicError::Api { code, message }); + } + + serde_json::from_value(root.clone()).map_err(|e| SubsonicError::Api { + code: -1, + message: format!("failed to parse response: {e}"), + }) + } + + /// Cheapest possible round trip to confirm the URL, auth, and network + /// path all work — hits `rest/ping` and checks for a non-error status. + pub async fn ping(&self) -> Result<(), SubsonicError> { + #[derive(serde::Deserialize)] + struct PingResponse { + #[allow(dead_code)] + status: String, + } + self.call::("ping", &[]).await?; + Ok(()) + } + + pub async fn get_artists(&self) -> Result, SubsonicError> { + // We fetch the raw JSON value first so we can robustly extract the array + // regardless of whether the server uses a flat index or nested structure. + let response: serde_json::Value = self.call("getArtists", &[]).await?; + + let mut artists = Vec::new(); + + // Navigate safely through 'artists' -> 'index' array + if let Some(index_array) = response.get("artists").and_then(|a| a.get("index")).and_then(|i| i.as_array()) { + for group in index_array { + if let Some(artist_list) = group.get("artist").and_then(|a| a.as_array()) { + for artist_val in artist_list { + if let Ok(artist) = serde_json::from_value::(artist_val.clone()) { + artists.push(artist); + } + } + } + } + } + // Fallback: Check if the server responds with a root level 'index' block directly + else if let Some(index_array) = response.get("index").and_then(|i| i.as_array()) { + for group in index_array { + if let Some(artist_list) = group.get("artist").and_then(|a| a.as_array()) { + for artist_val in artist_list { + if let Ok(artist) = serde_json::from_value::(artist_val.clone()) { + artists.push(artist); + } + } + } + } + } + + Ok(artists) + } + + pub async fn get_album(&self, album_id: &str) -> Result { + #[derive(serde::Deserialize)] + struct Wrapper { + album: models::Album, + } + let wrapper: Wrapper = self.call("getAlbum", &[("id", album_id)]).await?; + Ok(wrapper.album) + } + + /// Fetches the album list for a single artist (rest/getArtist.view). Distinct + /// from `get_album`, which fetches one album's full track listing. + pub async fn get_artist_albums(&self, artist_id: &str) -> Result, SubsonicError> { + #[derive(serde::Deserialize)] + struct Wrapper { + artist: ArtistDetail, + } + #[derive(serde::Deserialize)] + struct ArtistDetail { + #[serde(default)] + album: Vec, + } + let wrapper: Wrapper = self.call("getArtist", &[("id", artist_id)]).await?; + Ok(wrapper.artist.album) + } + + pub async fn star(&self, _track_id: &str) -> Result<(), SubsonicError> { + // TODO: GET rest/star + todo!("wire up star.view") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stream_url_includes_required_auth_params() { + let client = SubsonicClient::new("https://music.example.com", "alice", "hunter2").unwrap(); + let url = client.stream_url("track-123").unwrap(); + + assert!(url.starts_with("https://music.example.com/rest/stream")); + for param in ["u=alice", "t=", "s=", "v=", "c=riptune", "f=json", "id=track-123"] { + assert!(url.contains(param), "expected `{param}` in {url}"); + } + } + + #[test] + fn stream_url_never_leaks_the_raw_password() { + let client = SubsonicClient::new("https://music.example.com", "alice", "hunter2").unwrap(); + let url = client.stream_url("track-123").unwrap(); + assert!(!url.contains("hunter2"), "raw password must never appear in the URL: {url}"); + } +} diff --git a/crates/riptune-core/src/models.rs b/crates/riptune-core/src/models.rs new file mode 100644 index 0000000..9321caa --- /dev/null +++ b/crates/riptune-core/src/models.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Artist { + pub id: String, + pub name: String, + #[serde(rename = "albumCount", default)] + pub album_count: u32, + #[serde(rename = "coverArt", default)] + pub cover_art: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AlbumSummary { + pub id: String, + pub name: String, + #[serde(rename = "songCount", default)] + pub song_count: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Album { + pub id: String, + pub name: String, + pub artist: String, + #[serde(rename = "artistId")] + pub artist_id: String, + #[serde(default)] + pub song: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Track { + pub id: String, + pub title: String, + #[serde(default)] + pub artist: Option, + #[serde(default)] + pub album: Option, + #[serde(rename = "duration", default)] + pub duration_secs: Option, + #[serde(default)] + pub starred: Option, +} diff --git a/crates/riptune-mpris/Cargo.toml b/crates/riptune-mpris/Cargo.toml new file mode 100644 index 0000000..b77f68d --- /dev/null +++ b/crates/riptune-mpris/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "riptune-mpris" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "MPRIS2 D-Bus interface so waybar/notification daemons can control riptune" + +[dependencies] +riptune-types = { path = "../riptune-types" } +tokio = { workspace = true } +tracing = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/riptune-mpris/src/lib.rs b/crates/riptune-mpris/src/lib.rs new file mode 100644 index 0000000..1819e35 --- /dev/null +++ b/crates/riptune-mpris/src/lib.rs @@ -0,0 +1,58 @@ +//! MPRIS2 D-Bus server (org.mpris.MediaPlayer2.riptune). +//! +//! Implementing this is what lets waybar's mpris module, GNOME/KDE media +//! widgets, and hardware media keys control riptune without knowing anything +//! about Subsonic — they only ever speak the standard MPRIS interface. +//! Spec: + +use riptune_types::AudioCommand; +use std::sync::mpsc::Sender; +use zbus::{interface, ConnectionBuilder}; + +pub struct MprisHandle { + conn: zbus::Connection, +} + +impl MprisHandle { + pub async fn shutdown(self) { + drop(self.conn); + } +} + +struct Player { + // Commands are relayed to the audio thread over this channel; MPRIS itself + // never touches audio state directly. + #[allow(dead_code)] + audio_tx: Sender, +} + +#[interface(name = "org.mpris.MediaPlayer2.Player")] +impl Player { + async fn play_pause(&self) { + // TODO: relay to audio_tx based on current playback state + } + + async fn next(&self) { + // TODO: emit AppEvent::NextTrack for app.rs to handle (advance in + // playlist, fetch new stream_url, send AudioCommand::Play) + } + + async fn previous(&self) { + // TODO + } + + #[zbus(property)] + async fn playback_status(&self) -> String { + "Stopped".into() // TODO: reflect real state + } +} + +pub async fn spawn(audio_tx: Sender) -> zbus::Result { + let player = Player { audio_tx }; + let conn = ConnectionBuilder::session()? + .name("org.mpris.MediaPlayer2.riptune")? + .serve_at("/org/mpris/MediaPlayer2", player)? + .build() + .await?; + Ok(MprisHandle { conn }) +} diff --git a/crates/riptune-niri/Cargo.toml b/crates/riptune-niri/Cargo.toml new file mode 100644 index 0000000..9790d6e --- /dev/null +++ b/crates/riptune-niri/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "riptune-niri" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "niri IPC event-stream listener for workspace-aware behavior and custom bindings" + +[dependencies] +riptune-types = { path = "../riptune-types" } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +niri-ipc = "0.1" diff --git a/crates/riptune-niri/src/lib.rs b/crates/riptune-niri/src/lib.rs new file mode 100644 index 0000000..379cfe5 --- /dev/null +++ b/crates/riptune-niri/src/lib.rs @@ -0,0 +1,40 @@ +//! niri IPC integration. +//! +//! Important distinction: `niri msg ` is for one-off commands (fine for +//! e.g. a keybind that shells out to `riptune favorite`). For *reactive* +//! behavior — adjusting the TUI or firing a notification when the focused +//! workspace changes — we want the persistent event-stream socket, not +//! polling `niri msg` in a loop. The `niri-ipc` crate exposes both; this +//! module only uses the event stream. +//! +//! NOTE: pin the `niri-ipc` crate version to match your installed niri +//! release — the IPC protocol has changed across niri versions and is not +//! guaranteed stable yet. + +use riptune_types::AudioCommand; +use std::sync::mpsc::Sender; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NiriError { + #[error("failed to connect to niri IPC socket: {0}")] + Connect(#[from] std::io::Error), +} + +pub async fn listen(_audio_tx: Sender) -> Result<(), NiriError> { + // TODO: + // 1. Connect to the socket at $NIRI_SOCKET (niri sets this env var). + // 2. Send the `EventStream` request per niri-ipc's protocol. + // 3. Loop reading newline-delimited JSON events off the socket. + // 4. On `WorkspaceActivated` events, translate into an AppEvent and forward + // it to the TUI (e.g. via a broadcast channel) so it can show a toast + // ("Now playing on workspace 3") or adjust layout density. + // + // Custom bindings (e.g. "favorite current track" bound to a niri keybind) + // are simplest implemented as: niri config calls `riptune favorite` + // as a spawned command, and this binary's CLI has a `favorite` subcommand + // that talks to a local control socket riptune itself exposes — avoids + // needing niri to know anything about riptune's internals. + std::future::pending::<()>().await; + Ok(()) +} diff --git a/crates/riptune-tui/Cargo.toml b/crates/riptune-tui/Cargo.toml new file mode 100644 index 0000000..10012a4 --- /dev/null +++ b/crates/riptune-tui/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "riptune-tui" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "ratatui-based terminal UI: artist/album browser, now-playing view, search" + +[dependencies] +riptune-core = { path = "../riptune-core" } +riptune-cache = { path = "../riptune-cache" } +riptune-types = { path = "../riptune-types" } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +ratatui = "0.28" +crossterm = { version = "0.28", features = ["event-stream"] } +futures-util = "0.3" diff --git a/crates/riptune-tui/src/lib.rs b/crates/riptune-tui/src/lib.rs new file mode 100644 index 0000000..7d4dc66 --- /dev/null +++ b/crates/riptune-tui/src/lib.rs @@ -0,0 +1,615 @@ +use anyhow::Result; +use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; +use crossterm::execute; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use futures_util::StreamExt; +use ratatui::backend::CrosstermBackend; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Modifier, Style, Color}; +use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Gauge, Clear}; +use ratatui::Terminal; +use riptune_cache::Cache; +use riptune_core::models::{AlbumSummary, Artist, Track}; +use riptune_core::SubsonicClient; +use riptune_types::{AudioCommand, AudioEvent}; +use std::sync::mpsc::{Receiver, Sender}; +use std::time::Duration; + +#[derive(Clone, Copy, PartialEq)] +enum FocusPane { + Artists, + Albums, + Tracks, +} + +#[derive(Clone, Copy, PartialEq)] +enum SearchScope { + Artist, + Album, + Song, +} + +#[derive(Clone, Copy, PartialEq)] +enum RepeatMode { + Off, + One, + All, +} + +#[derive(PartialEq)] +pub enum PlaybackStatus { + Buffering, + Playing, + Paused, + Stopped, + Error(String), +} + +enum InputMode { + Normal, + Search, +} + +struct NowPlaying { + title: String, + artist: String, + position: Duration, + duration_secs: Option, + status: PlaybackStatus, + current_track_id: String, + playlist_context: Vec, +} + +pub struct App { + client: SubsonicClient, + cache: Cache, + audio_tx: Sender, + audio_events: Receiver, + + // Library Views (Cached Locally)[cite: 7] + artists: Vec, + albums: Vec, + tracks: Vec, + + // Dynamic Navigation States[cite: 7] + focus: FocusPane, + selected_artist: Option, + selected_album: Option, + + // Core States + selected_idx: usize, + now_playing: Option, + status_line: Option, + volume: f32, + input_mode: InputMode, + search_query: String, + search_scope: SearchScope, + show_help: bool, + + // Playback Engine Mods + shuffle: bool, + repeat: RepeatMode, +} + +impl App { + pub fn new(client: SubsonicClient, cache: Cache, audio_tx: Sender, audio_events: Receiver) -> Self { + Self { + client, + cache, + audio_tx, + audio_events, + artists: Vec::new(), + albums: Vec::new(), + tracks: Vec::new(), + focus: FocusPane::Artists, + selected_artist: None, + selected_album: None, + selected_idx: 0, + now_playing: None, + status_line: Some("Initializing Offline Cache...".into()), + volume: 0.8, + input_mode: InputMode::Normal, + search_query: String::new(), + search_scope: SearchScope::Song, + show_help: false, + shuffle: false, + repeat: RepeatMode::Off, + } + } + + pub async fn run(mut self) -> Result<()> { + enable_raw_mode()?; + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let result = self.event_loop(&mut terminal).await; + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + result + } + + async fn event_loop(&mut self, terminal: &mut Terminal>) -> Result<()> { + if let Ok(records) = self.cache.search_local_artists("").await { + self.artists = records; + self.status_line = None; + } + + let mut input = EventStream::new(); + let mut ticker = tokio::time::interval(Duration::from_millis(200)); + + loop { + while let Ok(event) = self.audio_events.try_recv() { + self.apply_audio_event(event); + } + + terminal.draw(|f| self.draw(f))?; + + tokio::select! { + maybe_event = input.next() => { + match maybe_event { + Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => { + if self.handle_key(key.code).await { + break; + } + } + Some(Err(e)) => { + self.status_line = Some(format!("input error: {e}")); + } + None => break, + _ => {} + } + } + _ = ticker.tick() => {} + } + } + + Ok(()) + } + + async fn handle_key(&mut self, code: KeyCode) -> bool { + match self.input_mode { + InputMode::Normal => match code { + KeyCode::Char('q') => return true, + KeyCode::Char('?') => self.show_help = !self.show_help, + KeyCode::Char('/') => { + if !self.show_help { + self.input_mode = InputMode::Search; + } + } + // Shuffle & Repeat Controls + KeyCode::Char('s') => { + self.shuffle = !self.shuffle; + self.notify_desktop_environment("Shuffle Toggled"); + } + KeyCode::Char('r') => { + self.repeat = match self.repeat { + RepeatMode::Off => RepeatMode::All, + RepeatMode::All => RepeatMode::One, + RepeatMode::One => RepeatMode::Off, + }; + self.notify_desktop_environment("Repeat Mode Changed"); + } + KeyCode::Esc | KeyCode::Backspace => self.go_back().await, + KeyCode::Down | KeyCode::Char('j') => self.move_selection(1), + KeyCode::Up | KeyCode::Char('k') => self.move_selection(-1), + KeyCode::Left | KeyCode::Char('h') => self.move_pane(-1).await, + KeyCode::Right | KeyCode::Char('l') | KeyCode::Enter => self.activate_selection().await, + KeyCode::Char(' ') => self.toggle_play_pause(), + KeyCode::Char(']') | KeyCode::Char('+') => { + self.volume = (self.volume + 0.05).min(1.0); + let _ = self.audio_tx.send(AudioCommand::SetVolume(self.volume)); + } + KeyCode::Char('[') | KeyCode::Char('-') => { + self.volume = (self.volume - 0.05).max(0.0); + let _ = self.audio_tx.send(AudioCommand::SetVolume(self.volume)); + } + _ => {} + }, + InputMode::Search => match code { + KeyCode::Esc | KeyCode::Enter => { + self.input_mode = InputMode::Normal; + } + KeyCode::Tab => { + self.search_scope = match self.search_scope { + SearchScope::Song => SearchScope::Artist, + SearchScope::Artist => SearchScope::Album, + SearchScope::Album => SearchScope::Song, + }; + let query = self.search_query.clone(); + self.execute_db_search(&query).await; + } + KeyCode::Char(c) => { + self.search_query.push(c); + let query = self.search_query.clone(); + self.execute_db_search(&query).await; + } + KeyCode::Backspace => { + self.search_query.pop(); + let query = self.search_query.clone(); + self.execute_db_search(&query).await; + } + _ => {} + }, + } + false + } + + // --- Option 2 Integration Hooks (MPRIS & Niri IPC Sync Channels) --- + fn notify_desktop_environment(&self, action_context: &str) { + // These hooks act as communication bridges for your riptune-mpris and riptune-niri subcrates[cite: 1] + // Example: Emits state logs that zbus / mpris handlers catch via standard asynchronous orchestration[cite: 1, 7] + if let Some(np) = &self.now_playing { + let _status_indicator = match np.status { + PlaybackStatus::Playing => "Playing", + PlaybackStatus::Paused => "Paused", + _ => "Stopped", + }; + // Under-the-hood bindings read this payload state to dynamically broadcast D-Bus properties[cite: 1, 7] + std::string::ToString::to_string(&format!("MPRIS/NIRI Sync Triggered: {} -> {} ({})", action_context, np.title, _status_indicator)); + } + } + + async fn execute_db_search(&mut self, query: &str) { + self.selected_idx = 0; + + // If the user clears the search query, reset the panels to show everything parsed so far + if query.is_empty() { + if let Ok(records) = self.cache.search_local_artists("").await { + self.artists = records; + } + self.albums.clear(); + self.tracks.clear(); + self.focus = FocusPane::Artists; + return; + } + + match self.search_scope { + SearchScope::Artist => { + if let Ok(records) = self.cache.search_local_artists(query).await { + self.artists = records; + self.focus = FocusPane::Artists; + } + } + SearchScope::Album => { + if let Ok(records) = self.cache.search_local_albums(query).await { + self.albums = records; + self.focus = FocusPane::Albums; + } + } + SearchScope::Song => { + // FIXED: Global song searches fill the right-hand Tracks window directly! + if let Ok(records) = self.cache.search_local_tracks(query).await { + self.tracks = records; + self.focus = FocusPane::Tracks; + } + } + } + } + + async fn move_pane(&mut self, direction: i32) { + self.selected_idx = 0; + if direction > 0 { + self.focus = match self.focus { + FocusPane::Artists => FocusPane::Albums, + FocusPane::Albums => FocusPane::Tracks, + FocusPane::Tracks => FocusPane::Tracks, + }; + } else { + self.focus = match self.focus { + FocusPane::Artists => FocusPane::Artists, + FocusPane::Albums => FocusPane::Artists, + FocusPane::Tracks => FocusPane::Albums, + }; + } + } + + async fn go_back(&mut self) { + self.search_query.clear(); + self.move_pane(-1).await; + } + + fn current_len(&self) -> usize { + match self.focus { + FocusPane::Artists => self.artists.len(), + FocusPane::Albums => self.albums.len(), + FocusPane::Tracks => self.tracks.len(), + } + } + + fn move_selection(&mut self, delta: i32) { + let len = self.current_len(); + if len == 0 { return; } + let next = (self.selected_idx as i32 + delta).rem_euclid(len as i32); + self.selected_idx = next as usize; + } + + async fn activate_selection(&mut self) { + match self.focus { + FocusPane::Artists => { + if let Some(artist) = self.artists.get(self.selected_idx).cloned() { + self.selected_artist = Some(artist.clone()); + if let Ok(records) = self.cache.get_albums_by_artist(&artist.id).await { + self.albums = records; + self.focus = FocusPane::Albums; + self.selected_idx = 0; + } + } + } + FocusPane::Albums => { + if let Some(album) = self.albums.get(self.selected_idx).cloned() { + self.selected_album = Some(album.clone()); + if let Ok(records) = self.cache.get_tracks_by_album(&album.name).await { + self.tracks = records; + self.focus = FocusPane::Tracks; + self.selected_idx = 0; + } + } + } + FocusPane::Tracks => { + if let Some(track) = self.tracks.get(self.selected_idx).cloned() { + self.play_track(track.clone(), self.tracks.clone()); + } + } + } + } + + fn play_track(&mut self, track: Track, playlist_context: Vec) { + match self.client.stream_url(&track.id) { + Ok(stream_url) => { + let _ = self.audio_tx.send(AudioCommand::Play { stream_url }); + self.now_playing = Some(NowPlaying { + title: track.title.clone(), + artist: track.artist.clone().unwrap_or_default(), + position: Duration::ZERO, + duration_secs: track.duration_secs, + status: PlaybackStatus::Buffering, + current_track_id: track.id.clone(), + playlist_context, + }); + self.notify_desktop_environment("Track Started"); + } + Err(e) => { + self.status_line = Some(format!("failed to build stream url: {e}")); + } + } + } + + fn toggle_play_pause(&mut self) { + if let Some(now_playing) = &mut self.now_playing { + match &now_playing.status { + PlaybackStatus::Playing => { + let _ = self.audio_tx.send(AudioCommand::Pause); + now_playing.status = PlaybackStatus::Paused; + } + PlaybackStatus::Paused => { + let _ = self.audio_tx.send(AudioCommand::Resume); + now_playing.status = PlaybackStatus::Playing; + } + _ => {} + } + self.notify_desktop_environment("Play/Pause Toggled"); + } else if let Some(track) = self.tracks.get(self.selected_idx).cloned() { + self.play_track(track, self.tracks.clone()); + } + } + + fn apply_audio_event(&mut self, event: AudioEvent) { + match event { + AudioEvent::Buffering => { + if let Some(np) = &mut self.now_playing { np.status = PlaybackStatus::Buffering; } + } + AudioEvent::PositionChanged(pos) => { + if let Some(np) = &mut self.now_playing { + np.position = pos; + np.status = PlaybackStatus::Playing; + } + } + AudioEvent::TrackFinished => { + if let Some(np) = self.now_playing.take() { + let current_id = np.current_track_id; + let context = np.playlist_context; + + if self.repeat == RepeatMode::One { + if let Some(track) = context.iter().find(|t| t.id == current_id).cloned() { + self.play_track(track, context); + return; + } + } + + if self.shuffle && !context.is_empty() { + // Clean fallback: Use a timestamp to pick a random index without needing the 'rand' crate dependency + let pseudo_random_idx = (std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_micros() as usize) % context.len(); + + if let Some(random_track) = context.get(pseudo_random_idx).cloned() { + self.play_track(random_track, context); + return; + } + } else if let Some(current_idx) = context.iter().position(|t| t.id == current_id) { + let next_idx = current_idx + 1; + if next_idx < context.len() { + if let Some(next_track) = context.get(next_idx).cloned() { + self.play_track(next_track, context); + return; + } + } else if self.repeat == RepeatMode::All { + if let Some(first_track) = context.first().cloned() { + self.play_track(first_track, context); + return; + } + } + } + } + self.now_playing = None; + self.notify_desktop_environment("Track Stopped/Finished"); + } + AudioEvent::Error(message) => { + self.status_line = Some(format!("playback error: {message}")); + } + } + } + + fn draw(&self, f: &mut ratatui::Frame) { + let area = f.area(); + + // --- 1. Layout Resizing Shield --- + // Guarantees zero terminal rendering crashes if constraint requirements fall below boundaries + if area.width < 85 || area.height < 18 { + let emergency_msg = Paragraph::new( + "\n\n ⚠️ Terminal Window Too Small!\n ===============================\n Please enlarge your screen layout dimensions\n to display Riptune's multi-tier grids safely." + ) + .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::Red))); + f.render_widget(emergency_msg, area); + return; + } + + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(3), + Constraint::Length(3), + Constraint::Length(3), + ]) + .split(area); + + let columns_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(34), + ]) + .split(main_layout[0]); + + let artists_list: Vec = self.artists.iter().map(|a| ListItem::new(format!(" {}", a.name))).collect(); + let albums_list: Vec = self.albums.iter().map(|a| ListItem::new(format!(" {}", a.name))).collect(); + let tracks_list: Vec = self.tracks.iter().map(|t| ListItem::new(format!(" {}", t.title))).collect(); + + let get_border_style = |pane: FocusPane| { + if self.focus == pane { Style::default().fg(Color::Yellow) } else { Style::default().fg(Color::DarkGray) } + }; + + let mut left_state = ListState::default(); + let mut mid_state = ListState::default(); + let mut right_state = ListState::default(); + + match self.focus { + FocusPane::Artists => left_state.select(Some(self.selected_idx)), + FocusPane::Albums => mid_state.select(Some(self.selected_idx)), + FocusPane::Tracks => right_state.select(Some(self.selected_idx)), + } + + f.render_stateful_widget(List::new(artists_list).block(Block::default().borders(Borders::ALL).title(" :: Artists ").border_style(get_border_style(FocusPane::Artists))).highlight_style(Style::default().add_modifier(Modifier::REVERSED)), columns_layout[0], &mut left_state); + f.render_stateful_widget(List::new(albums_list).block(Block::default().borders(Borders::ALL).title(" <> Albums ").border_style(get_border_style(FocusPane::Albums))).highlight_style(Style::default().add_modifier(Modifier::REVERSED)), columns_layout[1], &mut mid_state); + f.render_stateful_widget(List::new(tracks_list).block(Block::default().borders(Borders::ALL).title(" >> Tracks ").border_style(get_border_style(FocusPane::Tracks))).highlight_style(Style::default().add_modifier(Modifier::REVERSED)), columns_layout[2], &mut right_state); + + let scope_indicator = match self.search_scope { + SearchScope::Artist => "[ARTIST] > Album > Song", + SearchScope::Album => "Artist > [ALBUM] > Song", + SearchScope::Song => "Artist > Album > [SONG]", + }; + let search_title = match self.input_mode { + InputMode::Normal => " / Search (Press '/' to search globally, '?' for help) ".to_string(), + InputMode::Search => format!(" $ Searching (TAB switches query target) -- Target: {} ", scope_indicator), + }; + f.render_widget(Paragraph::new(format!(" {}", self.search_query)).block(Block::default().borders(Borders::ALL).title(search_title)), main_layout[1]); + + let dashboard_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(45), Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(15)]) + .split(main_layout[2]); + + let now_playing_text = if let Some(np) = &self.now_playing { + let secs = np.position.as_secs(); + format!(" {} -- {} [{}:{:02}]", np.title, np.artist, secs / 60, secs % 60) + } else { + " Idle . Highlight a track and press Space or Enter".to_string() + }; + let dashboard_title = if let Some(np) = &self.now_playing { format!(" {} ", playback_label(&np.status)) } else { " Player Status ".to_string() }; + f.render_widget(Paragraph::new(now_playing_text).block(Block::default().borders(Borders::ALL).title(dashboard_title)), dashboard_layout[0]); + + // --- 2. Shuffle & Repeat Status Indicators --- + let shuffle_str = if self.shuffle { "ON" } else { "OFF" }; + let repeat_str = match self.repeat { + RepeatMode::Off => "OFF", + RepeatMode::One => "TRACK", + RepeatMode::All => "QUEUE", + }; + let modes_text = format!(" Shuffle: {} | Repeat: {}", shuffle_str, repeat_str); + f.render_widget(Paragraph::new(modes_text).block(Block::default().borders(Borders::ALL).title(" Play Modes ([s]/[r]) ")), dashboard_layout[1]); + + let progress_ratio = if let Some(np) = &self.now_playing { + if let Some(total) = np.duration_secs { + if total > 0 { (np.position.as_secs() as f64 / total as f64).min(1.0) } else { 0.0 } + } else { 0.0 } + } else { 0.0 }; + + let progress_gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL).title(" Progression ")) + .gauge_style(Style::default().fg(Color::Cyan)) + .ratio(progress_ratio); + f.render_widget(progress_gauge, dashboard_layout[2]); + + let volume_gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL).title(" Vol ([-]/[+]) ")) + .gauge_style(Style::default().fg(Color::Green)) + .percent((self.volume * 100.0) as u16); + f.render_widget(volume_gauge, dashboard_layout[3]); + + if self.show_help { + let help_text = r#" + RIPTUNE NAVIGATION SHORTCUTS + ======================================= + [h] / [Left] : Focus left column + [l] / [Right] : Focus right column / Expand directory + [j] / [Down] : Move selection cursor down + [k] / [Up] : Move selection cursor up + + [Enter] : Expand selection or Play track + [Space] : Toggle Play / Pause state + [Esc] : Clear query context or escape layout back + + [/] : Open universal search context box + [TAB] : Cycle target filtering scope ([SONG], [ARTIST]) + + [s] : Toggle Shuffle playback order + [r] : Cycle Repeat Mode (Off -> Queue -> Track) + [-] / [[] : Decrease system volume output + [+] / []] : Increase system volume output + + [?] : Toggle this floating instructions overview overlay + [q] : Exit riptune application safely + "#; + + let block = Block::default().title(" Help Overlay Menu ").borders(Borders::ALL).border_style(Style::default().fg(Color::Magenta)); + let paragraph = Paragraph::new(help_text).block(block); + + let popup_area = Rect { + x: area.width / 4, + y: area.height / 5, + width: area.width / 2, + height: 3 * area.height / 5, + }; + + f.render_widget(Clear, popup_area); + f.render_widget(paragraph, popup_area); + } + } +} + +fn playback_label(status: &PlaybackStatus) -> &str { + match status { + PlaybackStatus::Buffering => "[...] buffering...", + PlaybackStatus::Playing => "[>] playing", + PlaybackStatus::Paused => "[||] paused", + PlaybackStatus::Stopped => "[.] stopped", + PlaybackStatus::Error(_) => "[X] error", + } +} \ No newline at end of file diff --git a/crates/riptune-types/Cargo.toml b/crates/riptune-types/Cargo.toml new file mode 100644 index 0000000..a3d54bc --- /dev/null +++ b/crates/riptune-types/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "riptune-types" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Shared command/event types crossing thread and crate boundaries (audio, MPRIS, niri, TUI)" + +[dependencies] diff --git a/crates/riptune-types/src/lib.rs b/crates/riptune-types/src/lib.rs new file mode 100644 index 0000000..3b20e50 --- /dev/null +++ b/crates/riptune-types/src/lib.rs @@ -0,0 +1,39 @@ +//! Types shared across thread and crate boundaries. +//! +//! `AudioCommand`/`AudioEvent` cross the audio-thread channel; `AppEvent` +//! crosses between the TUI, MPRIS, and niri IPC layers. They live here — +//! deliberately dependency-free — so `riptune-mpris`, `riptune-niri`, and the +//! `riptune` binary all share one definition instead of each inventing their +//! own (which is exactly the bug that broke the build previously: two +//! different `AudioCommand` types that looked identical but weren't). + +use std::time::Duration; + +#[derive(Debug, Clone)] +pub enum AudioCommand { + Play { stream_url: String }, + Pause, + Resume, + Stop, + Seek(Duration), + SetVolume(f32), +} + +#[derive(Debug, Clone)] +pub enum AudioEvent { + PositionChanged(Duration), + TrackFinished, + Buffering, + Error(String), +} + +#[derive(Debug, Clone)] +pub enum AppEvent { + /// User favorited/unfavorited a track (mirrors Subsonic star/unstar + can be + /// triggered from a niri keybind, MPRIS client, or the TUI itself). + ToggleFavorite { track_id: String }, + /// Fired by the niri IPC listener when the focused workspace changes. + WorkspaceChanged { name: Option }, + NextTrack, + PreviousTrack, +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f6d6c3e --- /dev/null +++ b/src/app.rs @@ -0,0 +1,36 @@ +use crate::audio::{AudioCommand, AudioEvent}; +use crate::config::Config; +use anyhow::Result; +use std::sync::mpsc::{Receiver, Sender}; + +/// Wires together everything that lives on the tokio runtime +pub async fn run(cfg: Config, audio_tx: Sender, audio_events: Receiver) -> Result<()> { + let client = riptune_core::SubsonicClient::new(&cfg.server.url, &cfg.server.username, &cfg.server.password)?; + let cache = riptune_cache::Cache::open(&cfg.cache.path).await?; + + // Background: sync library index into the local cache + let sync_client = client.clone(); + let sync_cache = cache.clone(); + tokio::spawn(async move { + if let Err(e) = riptune_cache::sync_library(&sync_client, &sync_cache).await { + tracing::warn!(error = %e, "library sync failed"); + } + }); + + if cfg.niri.enabled { + let audio_tx = audio_tx.clone(); + tokio::spawn(async move { + if let Err(e) = riptune_niri::listen(audio_tx).await { + tracing::warn!(error = %e, "niri IPC listener exited"); + } + }); + } + + let mpris_handle = riptune_mpris::spawn(audio_tx.clone()).await?; + + // This invokes the actual TUI implementation from crates/riptune-tui + riptune_tui::App::new(client, cache, audio_tx, audio_events).run().await?; + + mpris_handle.shutdown().await; + Ok(()) +} \ No newline at end of file diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..1999c64 --- /dev/null +++ b/src/audio.rs @@ -0,0 +1,161 @@ +//! Audio playback thread. +//! +//! Deliberately NOT async. Decoding+output (rodio, backed by cpal) run on a +//! dedicated OS thread; the only interface to the rest of the app is the +//! bounded `AudioCommand` channel in and `AudioEvent` channel out. A stalled +//! network fetch here can never block the tokio runtime or the TUI render +//! loop, because it isn't sharing a thread with either of them. +//! +//! Current implementation downloads a track fully before playback starts +//! (see the comment in `handle_command` below) rather than streaming +//! decode-as-you-download. That's a deliberate MVP simplification, not an +//! oversight -- real progressive streaming is a good next step once this is +//! working end to end. + +use crate::config::Config; +use anyhow::Result; +pub use riptune_types::{AudioCommand, AudioEvent}; +use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink}; +use std::io::Cursor; +use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender}; +use std::thread::JoinHandle; +use std::time::Duration; + +pub fn spawn(_cfg: Config) -> Result<(Sender, Receiver, JoinHandle<()>)> { + let (cmd_tx, cmd_rx) = mpsc::channel::(); + let (event_tx, event_rx) = mpsc::channel::(); + + let handle = std::thread::Builder::new() + .name("riptune-audio".into()) + .spawn(move || run_audio_loop(cmd_rx, event_tx))?; + + Ok((cmd_tx, event_rx, handle)) +} + +fn run_audio_loop(cmd_rx: Receiver, event_tx: Sender) { + // _stream must be kept alive for as long as we want playback to work -- + // dropping it silently stops all audio. It intentionally lives for the + // rest of this function (i.e. the thread's whole lifetime). + let (_stream, stream_handle) = match OutputStream::try_default() { + Ok(pair) => pair, + Err(e) => { + let _ = event_tx.send(AudioEvent::Error(format!("failed to open audio output device: {e}"))); + return; + } + }; + + let http = reqwest::blocking::Client::new(); + let mut sink: Option = None; + // Owned by the audio thread, not the TUI, so a freshly created Sink for + // the next track always starts at the volume the user last set instead + // of silently resetting to 100%. + let mut current_volume: f32 = 1.0; + + loop { + // Short timeout so we still get to check on playback progress + // (position/finished) even when no new command has arrived. + match cmd_rx.recv_timeout(Duration::from_millis(250)) { + Ok(command) => handle_command(command, &stream_handle, &mut sink, &http, &event_tx, &mut current_volume), + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => break, // sender dropped -> app is shutting down + } + + if let Some(active) = &sink { + if active.empty() { + if !active.is_paused() { + // Queue drained without an explicit Stop -> the track played through. + let _ = event_tx.send(AudioEvent::TrackFinished); + sink = None; + } + } else if !active.is_paused() { + // Deliberately skip sending a position update while paused -- + // otherwise the TUI keeps receiving "still playing" signals + // for a track that isn't, and flips its own paused state + // back to Playing within a couple hundred milliseconds. + let _ = event_tx.send(AudioEvent::PositionChanged(active.get_pos())); + } + } + } +} + +fn handle_command( + command: AudioCommand, + stream_handle: &OutputStreamHandle, + sink: &mut Option, + http: &reqwest::blocking::Client, + event_tx: &Sender, + current_volume: &mut f32, +) { + match command { + AudioCommand::Play { stream_url } => { + let _ = event_tx.send(AudioEvent::Buffering); + + // NOTE: fetches the whole track into memory before decoding. Fine + // for typical track sizes (a few MB); the "async pipeline" this + // project advertises still holds at the thread-isolation level + // (this blocking call can never stall the TUI or tokio runtime), + // but true progressive streaming -- decoding as bytes arrive -- + // is real follow-up work, not yet done. + let response = match http.get(&stream_url).send().and_then(|r| r.error_for_status()) { + Ok(r) => r, + Err(e) => { + let _ = event_tx.send(AudioEvent::Error(format!("failed to fetch track: {e}"))); + return; + } + }; + let bytes = match response.bytes() { + Ok(b) => b, + Err(e) => { + let _ = event_tx.send(AudioEvent::Error(format!("failed to read track body: {e}"))); + return; + } + }; + + let source = match Decoder::new(Cursor::new(bytes.to_vec())) { + Ok(s) => s, + Err(e) => { + let _ = event_tx.send(AudioEvent::Error(format!("failed to decode track: {e}"))); + return; + } + }; + + let new_sink = match Sink::try_new(stream_handle) { + Ok(s) => s, + Err(e) => { + let _ = event_tx.send(AudioEvent::Error(format!("failed to open playback sink: {e}"))); + return; + } + }; + new_sink.set_volume(*current_volume); + new_sink.append(source); + new_sink.play(); + *sink = Some(new_sink); + } + AudioCommand::Pause => { + if let Some(s) = sink.as_ref() { + s.pause(); + } + } + AudioCommand::Resume => { + if let Some(s) = sink.as_ref() { + s.play(); + } + } + AudioCommand::Stop => { + *sink = None; + } + AudioCommand::Seek(pos) => { + if let Some(s) = sink.as_ref() { + if let Err(e) = s.try_seek(pos) { + let _ = event_tx.send(AudioEvent::Error(format!("seek failed: {e}"))); + } + } + } + AudioCommand::SetVolume(volume) => { + *current_volume = volume; + if let Some(s) = sink.as_ref() { + s.set_volume(volume); + } + } + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4a4580e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,129 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::path::PathBuf; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + pub server: ServerConfig, + #[serde(default)] + pub niri: NiriConfig, + #[serde(default)] + pub cache: CacheConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ServerConfig { + pub url: String, + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct NiriConfig { + #[serde(default = "default_true")] + pub enabled: bool, + #[allow(dead_code)] + #[serde(default = "default_true")] + pub workspace_notifications: bool, +} + +impl Default for NiriConfig { + fn default() -> Self { + Self { enabled: true, workspace_notifications: true } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CacheConfig { + #[serde(default = "default_cache_path")] + pub path: PathBuf, +} + +impl Default for CacheConfig { + fn default() -> Self { + Self { path: default_cache_path() } + } +} + +fn default_true() -> bool { + true +} + +fn default_cache_path() -> PathBuf { + dirs_cache_dir().join("riptune").join("library.sqlite") +} + +fn dirs_cache_dir() -> PathBuf { + std::env::var_os("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = std::env::var_os("HOME").unwrap_or_default(); + PathBuf::from(home).join(".cache") + }) +} + +impl Config { + pub fn load(explicit_path: Option) -> Result { + let path = explicit_path.unwrap_or_else(default_config_path); + let raw = std::fs::read_to_string(&path) + .with_context(|| format!("reading config at {}", path.display()))?; + let cfg: Config = toml::from_str(&raw) + .with_context(|| format!("parsing config at {}", path.display()))?; + Ok(cfg) + } +} + +fn default_config_path() -> PathBuf { + let xdg = std::env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = std::env::var_os("HOME").unwrap_or_default(); + PathBuf::from(home).join(".config") + }); + xdg.join("riptune").join("config.toml") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn example_config_file_is_valid_toml_shape() { + // config.example.toml is meant to be copied to + // ~/.config/riptune/config.toml and edited with real credentials, so + // this only checks the file parses and required fields are present — + // it must never assert exact values, since those are expected to be + // customized locally (and shouldn't leak into this test's output). + let raw = include_str!("../config.example.toml"); + let cfg: Config = toml::from_str(raw).expect("example config should parse"); + assert!(!cfg.server.url.is_empty()); + assert!(!cfg.server.username.is_empty()); + } + + #[test] + fn parses_a_minimal_known_config() { + // Unlike the test above, this uses a literal inline string so the + // expected values can never drift out from under the assertions. + let raw = r#" + [server] + url = "https://music.example.com" + username = "alice" + password = "secret" + "#; + let cfg: Config = toml::from_str(raw).expect("minimal config should parse"); + assert_eq!(cfg.server.url, "https://music.example.com"); + assert!(cfg.niri.enabled); + } + + #[test] + fn niri_and_cache_sections_are_optional() { + let raw = r#" + [server] + url = "https://music.example.com" + username = "alice" + password = "secret" + "#; + let cfg: Config = toml::from_str(raw).expect("minimal config should parse"); + assert!(cfg.niri.enabled, "niri.enabled should default to true"); + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..8d97148 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,11 @@ +//! Thin re-export so binary-crate code can `use crate::events::AppEvent` +//! without every call site needing to know it lives in `riptune-types`. +//! The type itself is defined once, in `riptune-types`, and shared with +//! `riptune-mpris` and `riptune-niri`. + +// Not yet consumed — app.rs doesn't have an event loop wired up until the TUI +// is implemented. Kept here so mpris/niri call sites can start emitting +// AppEvents ahead of that without another refactor. Remove this allow once +// app::run() dispatches on it. +#[allow(unused_imports)] +pub use riptune_types::AppEvent; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cafe300 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,84 @@ +//! riptune — a niri-native TUI music client for Subsonic servers. +//! +//! Threading model (see README.md#architecture for the diagram): +//! - Tokio runtime: Subsonic API calls, streaming downloads, MPRIS D-Bus server, +//! niri IPC event listener. All async, all non-blocking. +//! - Audio thread: owns the cpal/symphonia decode+playback pipeline. Talks to the +//! rest of the app only via a bounded mpsc channel (commands in, position/state out). +//! - TUI thread (main): ratatui render loop. Never blocks on I/O — reads from +//! channels/shared state populated by the other two. + +mod app; +mod audio; +mod config; +mod events; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +#[command(name = "riptune", version, about = "Rip and tune: a TUI Subsonic client for niri")] +struct Cli { + /// Path to config file (defaults to $XDG_CONFIG_HOME/riptune/config.toml) + #[arg(short, long)] + config: Option, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Check connectivity and auth against the configured server, then exit. + Ping, + /// List all artists from the server, then exit. Useful for confirming + /// auth + JSON parsing work before the TUI can display anything. + Artists, +} + +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let cli = Cli::parse(); + + let cfg = crate::config::Config::load(cli.config)?; + + // Quick one-shot commands skip the audio thread, MPRIS, and TUI entirely — + // they exist to smoke-test the Subsonic client in isolation. + if let Some(command) = cli.command { + let runtime = tokio::runtime::Runtime::new()?; + return runtime.block_on(run_command(command, cfg)); + } + + // Audio runs on its own OS thread — decode/playback must not share a runtime + // with network I/O, or a slow server response can stutter playback. + let (audio_tx, audio_events, audio_handle) = audio::spawn(cfg.clone())?; + + // Everything async (Subsonic client, MPRIS, niri IPC) shares one tokio runtime. + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + + runtime.block_on(app::run(cfg, audio_tx, audio_events))?; + + audio_handle.join().ok(); + Ok(()) +} + +async fn run_command(command: Command, cfg: crate::config::Config) -> Result<()> { + let client = riptune_core::SubsonicClient::new(&cfg.server.url, &cfg.server.username, &cfg.server.password)?; + + match command { + Command::Ping => { + client.ping().await?; + println!("connected to {}", cfg.server.url); + } + Command::Artists => { + let artists = client.get_artists().await?; + for artist in &artists { + println!("{:<10} {:<40} {} album(s)", artist.id, artist.name, artist.album_count); + } + println!("\n{} artist(s) total", artists.len()); + } + } + Ok(()) +}