diff --git a/CHANGELOG.md b/CHANGELOG.md index cca75abdc..96b4efcab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added (spotty feature) +- [lms-connect] New `lms-connect` cargo feature: LMS glue for Spotify Connect event dispatch + to Lyrion Music Server via JSON-RPC, streaming pipe output, and token helpers +- [lms-connect] `--lms` / `--player-mac` CLI flags: connect spotty to LMS endpoint for + real-time PlayerEvent forwarding (TrackChanged, PlaybackStart, etc.) +- [lms-connect] `--keymaster-token` CLI flag: binary-assisted OAuth token refresh for LMS +- [lms-connect] `--get-token` / `--save-token` CLI flags: token persistence for LMS +- [lms-connect] `ConnectNullSink` audio backend module for pure-control-plane operation + ## [0.8.0] - 2025-11-10 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2a68e2e82..4255b82cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -53,7 +53,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -94,44 +94,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayvec" @@ -181,9 +181,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atomic_refcell" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" [[package]] name = "autocfg" @@ -199,9 +199,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -211,9 +211,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -226,15 +226,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -244,15 +244,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.45" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -266,9 +266,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-expr" -version = "0.20.4" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acd0bdbbf4b2612d09f52ba61da432140cb10930354079d0d53fafc12968726" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -288,9 +288,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -434,9 +434,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -494,9 +494,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "der" @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -563,11 +563,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "objc2", ] @@ -594,9 +594,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encoding_rs" @@ -609,9 +609,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -636,18 +636,18 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -695,21 +695,21 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -727,6 +727,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -753,9 +759,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -768,9 +774,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -778,15 +784,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -795,9 +801,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -814,9 +820,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -825,27 +831,27 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -855,15 +861,14 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -880,9 +885,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -900,16 +905,29 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gio-sys" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" dependencies = [ "glib-sys", "gobject-sys", @@ -920,11 +938,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9dbecb1c33e483a98be4acfea2ab369e1c28f517c6eadb674537409c25c4b2" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -941,9 +959,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "880e524e0085f3546cfb38532b2c202c0d64741d9977a6e4aa24704bfc9f19fb" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" dependencies = [ "heck", "proc-macro-crate", @@ -954,9 +972,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" dependencies = [ "libc", "system-deps", @@ -964,9 +982,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" dependencies = [ "glib-sys", "libc", @@ -975,15 +993,15 @@ dependencies = [ [[package]] name = "governor" -version = "0.10.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", "futures-sink", "futures-timer", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "nonzero_ext", "parking_lot", "portable-atomic", @@ -994,9 +1012,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ac2f12970a2f85a681d2ceaa40c32fe86cc202ead315e0dfa2223a1217cd24" +checksum = "1e8251db223ca38d9aefaf3d19f6f11581a9123cd12dacebd8b9e182da965023" dependencies = [ "cfg-if", "futures-channel", @@ -1014,14 +1032,14 @@ dependencies = [ "pastey", "pin-project-lite", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "gstreamer-app" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" +checksum = "3da7017b2a2fa5cdf9123b1603947ea24174f6d8cea0ea673411df824c811921" dependencies = [ "futures-core", "futures-sink", @@ -1034,9 +1052,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.24.0" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" +checksum = "4fa9f1b12b546aea543c15a0fdbc5a53617902a74f6d357a32b6a9fb4bc4725c" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1047,9 +1065,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8" +checksum = "76c058cce8d32bfb6dd578a3d6d1d874b855a638b738d8bb34cd7aedd65aeddd" dependencies = [ "cfg-if", "glib", @@ -1062,9 +1080,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.24.0" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" +checksum = "807e476f555c4e7409d8c8fe4fd10fa9989b8e8f2898762d9551e1adfde98a2d" dependencies = [ "glib-sys", "gobject-sys", @@ -1076,9 +1094,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4" +checksum = "9375f9a12120a8ee17b765c816c9b23861ce258def77b0ee40a05acb00c74972" dependencies = [ "atomic_refcell", "cfg-if", @@ -1090,9 +1108,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e" +checksum = "1b844f3559b6ab0379b4b771261643783ae4e0ffa71d5f5f46e33b7acf66b752" dependencies = [ "glib-sys", "gobject-sys", @@ -1103,9 +1121,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.24.2" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377" +checksum = "b5d37c1a599ae57b8186948bd5699f2dbfc044baea9d400228b489a85bcf2759" dependencies = [ "cfg-if", "glib-sys", @@ -1116,9 +1134,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -1138,17 +1156,26 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "headers" @@ -1206,23 +1233,22 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1263,9 +1289,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1278,7 +1304,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1331,20 +1356,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.35", - "rustls-native-certs 0.8.2", - "rustls-pki-types", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.7", ] [[package]] @@ -1365,14 +1389,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1391,9 +1414,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1461,9 +1484,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1475,9 +1498,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1494,6 +1517,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1533,12 +1562,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1552,19 +1583,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is-docker" @@ -1602,17 +1623,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jack" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" +checksum = "f7811b07bcac5dafabf814ab52c4b0ca9b7948aa1e279f572f03aa6544d47d27" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "jack-sys", "lazy_static", "libc", @@ -1635,9 +1656,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -1648,9 +1669,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -1666,7 +1687,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -1675,16 +1696,40 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +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", +] [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1707,11 +1752,17 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1725,9 +1776,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmdns" @@ -1741,9 +1792,9 @@ dependencies = [ "if-addrs", "log", "multimap", - "rand 0.9.2", + "rand 0.9.4", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] @@ -1753,7 +1804,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "libpulse-sys", "num-derive", @@ -1809,7 +1860,7 @@ dependencies = [ "librespot-core", "log", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] @@ -1823,9 +1874,9 @@ dependencies = [ "librespot-protocol", "log", "protobuf", - "rand 0.9.2", + "rand 0.9.4", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "uuid", @@ -1851,7 +1902,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy2", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", "librespot-oauth", @@ -1868,7 +1919,7 @@ dependencies = [ "protobuf", "protobuf-json-mapping", "quick-xml", - "rand 0.9.2", + "rand 0.9.4", "rand_distr", "rsa", "serde", @@ -1876,7 +1927,7 @@ dependencies = [ "sha1", "shannon", "sysinfo", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -1908,12 +1959,12 @@ dependencies = [ "libmdns", "librespot-core", "log", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_json", "serde_repr", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "zbus", ] @@ -1930,7 +1981,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", ] @@ -1943,7 +1994,7 @@ dependencies = [ "oauth2", "open", "reqwest", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "url", ] @@ -1969,13 +2020,13 @@ dependencies = [ "ogg", "portable-atomic", "portaudio-rs", - "rand 0.9.2", + "rand 0.9.4", "rand_distr", "rodio", "sdl2", "shell-words", "symphonia", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "zerocopy", ] @@ -1996,15 +2047,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2017,9 +2068,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-slab" @@ -2038,9 +2089,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2069,9 +2120,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2092,17 +2143,17 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] @@ -2113,8 +2164,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.11.1", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2133,20 +2184,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", + "jni-sys 0.3.1", ] [[package]] @@ -2157,9 +2195,9 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -2176,16 +2214,16 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -2250,9 +2288,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2260,9 +2298,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2287,9 +2325,9 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64", "chrono", - "getrandom 0.2.16", + "getrandom 0.2.17", "http", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "serde", "serde_json", @@ -2301,9 +2339,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2314,7 +2352,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "objc2", "objc2-core-audio", @@ -2341,7 +2379,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "objc2", ] @@ -2351,7 +2389,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] @@ -2392,9 +2430,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2404,9 +2442,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.2" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "is-wsl", "libc", @@ -2415,15 +2453,14 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -2445,11 +2482,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -2459,9 +2502,9 @@ dependencies = [ [[package]] name = "option-operations" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689" +checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754" dependencies = [ "pastey", ] @@ -2507,9 +2550,9 @@ dependencies = [ [[package]] name = "pastey" -version = "0.1.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pathdiff" @@ -2544,15 +2587,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -2577,21 +2614,21 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -2619,9 +2656,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2641,6 +2678,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "priority-queue" version = "2.7.0" @@ -2654,18 +2701,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2734,9 +2781,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", "serde", @@ -2754,9 +2801,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.40", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2764,20 +2811,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2799,9 +2846,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2812,11 +2859,17 @@ 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.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2825,12 +2878,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2850,7 +2903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2859,14 +2912,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2878,7 +2931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -2887,14 +2940,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2904,9 +2957,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2915,15 +2968,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2934,7 +2987,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-tls", "hyper-util", "js-sys", @@ -2943,8 +2996,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", - "rustls-native-certs 0.8.2", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-pki-types", "serde", "serde_json", @@ -2960,7 +3013,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.7", ] [[package]] @@ -2971,7 +3024,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2990,9 +3043,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -3010,9 +3063,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -3020,7 +3073,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3029,14 +3082,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3056,14 +3109,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -3074,7 +3127,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile", "rustls-pki-types", "schannel", @@ -3083,14 +3136,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework 3.7.0", ] [[package]] @@ -3104,9 +3157,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -3125,9 +3178,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -3142,9 +3195,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3157,9 +3210,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3199,7 +3252,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3208,11 +3261,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3221,14 +3274,20 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -3261,15 +3320,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3296,9 +3355,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3348,9 +3407,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -3360,10 +3419,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -3379,15 +3439,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3397,12 +3457,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3434,10 +3494,14 @@ dependencies = [ name = "spotty" version = "0.8.0" dependencies = [ + "bytes", "data-encoding", "env_logger", "futures-util", "getopts", + "http-body-util", + "hyper", + "hyper-util", "librespot-audio", "librespot-connect", "librespot-core", @@ -3450,8 +3514,9 @@ dependencies = [ "serde_json", "sha1", "sysinfo", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", + "tokio-stream", "url", ] @@ -3578,9 +3643,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3623,11 +3688,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3644,9 +3709,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.7" +version = "7.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ "cfg-expr", "heck", @@ -3663,14 +3728,14 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -3685,11 +3750,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -3705,9 +3770,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -3716,9 +3781,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -3726,22 +3791,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -3749,9 +3814,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3759,9 +3824,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3774,9 +3839,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3791,9 +3856,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3827,15 +3892,15 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.40", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -3851,8 +3916,8 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.35", - "rustls-native-certs 0.8.2", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-native-tls", @@ -3863,9 +3928,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3876,9 +3941,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -3886,50 +3951,50 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -3942,20 +4007,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3972,9 +4037,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3983,9 +4048,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3994,9 +4059,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -4019,36 +4084,36 @@ dependencies = [ "httparse", "log", "native-tls", - "rand 0.9.2", - "rustls 0.23.35", + "rand 0.9.4", + "rustls 0.23.40", "rustls-pki-types", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -4056,6 +4121,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -4064,14 +4135,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4094,13 +4166,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -4197,14 +4269,23 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -4215,22 +4296,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4238,9 +4316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -4251,18 +4329,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -4294,14 +4406,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -4474,13 +4586,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -4770,9 +4882,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -4783,17 +4904,105 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4802,9 +5011,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4814,9 +5023,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast", "async-recursion", @@ -4826,8 +5035,9 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix 1.1.4", "serde", "serde_repr", "tokio", @@ -4835,7 +5045,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -4843,9 +5053,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4858,30 +5068,29 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4890,18 +5099,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4917,9 +5126,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4928,9 +5137,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4939,34 +5148,40 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zvariant" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4977,13 +5192,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", "syn", - "winnow", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index c83c9927b..b6365ce6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,9 @@ repository = "https://github.com/michaelherger/librespot" edition = "2024" [features] -default = ["native-tls", "spotty", "with-libmdns"] -default-linux = ["rustls-tls-webpki-roots", "spotty", "with-libmdns"] +lms-connect = [] # LMS-Glue feature: enables LMS notification dispatch and streaming pipeline +default = ["native-tls", "spotty", "with-libmdns", "lms-connect"] +default-linux = ["rustls-tls-webpki-roots", "spotty", "with-libmdns", "lms-connect"] # TLS backends (mutually exclusive - compile-time checks in oauth/src/lib.rs) # Note: Feature validation is in oauth crate since it's compiled first in the dependency tree. @@ -184,8 +185,15 @@ tokio = { version = "1", features = [ "signal", "sync", "process", + "net", + "io-util", ] } url = "2.2" +bytes = "1" +http-body-util = "0.1" +hyper = { version = "1", features = ["http1", "server"] } +hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service", "tokio"] } +tokio-stream = "0.1" [package.metadata.deb] maintainer = "Librespot Organization " diff --git a/src/lib.rs b/src/lib.rs index f6a176548..da087b13c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,6 @@ pub use librespot_metadata as metadata; pub use librespot_oauth as oauth; pub use librespot_playback as playback; pub use librespot_protocol as protocol; + +#[cfg(feature = "lms-connect")] +pub mod spotty; diff --git a/src/main.rs b/src/main.rs index b8c2997e3..74235d3eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -253,6 +253,14 @@ struct Setup { client_id: Option, get_token: bool, save_token: Option, + #[cfg(feature = "lms-connect")] + lms: Option, + #[cfg(feature = "lms-connect")] + lms_auth: Option, + #[cfg(feature = "lms-connect")] + player_mac: Option, + #[cfg(feature = "lms-connect")] + connect_stream: bool, } async fn get_setup() -> Setup { @@ -317,6 +325,8 @@ async fn get_setup() -> Setup { const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; const ZEROCONF_BACKEND: &str = "zeroconf-backend"; const LOCAL_FILE_DIR: &str = "local-file-dir"; + const KEYMASTER_TOKEN: &str = "keymaster-token"; + const CONNECT_STREAM: &str = "connect-stream"; // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; @@ -787,6 +797,16 @@ async fn get_setup() -> Setup { PLAYER_MAC, "MAC address of the Squeezebox to be controlled", "MAC" + ) + .optflag( + "", + KEYMASTER_TOKEN, + "Get a fresh access token from stored credentials via Spotify login5, print JSON to stdout, then exit." + ) + .optflag( + "", + CONNECT_STREAM, + "[S] Run as a continuous Spotify Connect audio stream to stdout (S16LE PCM, 44100 Hz, stereo)." ); let args: Vec<_> = std::env::args_os() @@ -865,6 +885,61 @@ async fn get_setup() -> Setup { spotty::check(get_version_string()); } + if opt_present(KEYMASTER_TOKEN) { + let cache_dir = opt_str(CACHE) + .or_else(|| opt_str(SYSTEM_CACHE)) + .unwrap_or_else(|| { + error!("--{KEYMASTER_TOKEN} requires --{CACHE}/-{CACHE_SHORT}"); + exit(1); + }); + + let cache = match Cache::new( + Some(PathBuf::from(&cache_dir)), + Some(PathBuf::from(&cache_dir)), + None, + None, + ) { + Ok(cache) => cache, + Err(e) => { + error!("Cannot create cache: {e}"); + exit(1); + } + }; + + let credentials = cache.credentials().unwrap_or_else(|| { + error!("No stored credentials in {cache_dir}"); + exit(1); + }); + + let session_config = SessionConfig::default(); + let session = Session::new(session_config, Some(cache)); + + match session.connect(credentials, true).await { + Ok(()) => { + match session.login5().auth_token().await { + Ok(token) => { + let json = serde_json::json!({ + "accessToken": token.access_token, + "expiresIn": token.expires_in.as_secs(), + "tokenType": token.token_type, + "scope": token.scopes, + }); + println!("{json}"); + exit(0); + } + Err(e) => { + error!("Failed to get auth token via login5: {e}"); + exit(1); + } + } + } + Err(e) => { + error!("Failed to connect with stored credentials: {e}"); + exit(1); + } + } + } + #[cfg(debug_assertions)] setup_logging(opt_present(QUIET), opt_present(VERBOSE)); @@ -1399,9 +1474,15 @@ async fn get_setup() -> Setup { )) { Some("librespot compiled without zeroconf backend".to_owned()) } else if cfg!(feature = "spotty") - && (opt_present(SINGLE_TRACK) || opt_present(SAVE_TOKEN) || opt_present(GET_TOKEN)) + && (opt_present(SINGLE_TRACK) || opt_present(SAVE_TOKEN) || opt_present(GET_TOKEN) + || opt_present(CONNECT_STREAM)) { - Some("we don't need discovery in spotty mode".to_owned()) + // WR-05: suppress Zeroconf in --connect-stream mode. The HTTP stream + // server is a tightly controlled Connect receiver managed by LMS; + // uncontrolled Zeroconf advertisement would allow Spotify apps on the LAN + // to discover the device independently, potentially creating a second + // Spirc instance that clobbers spirc_active. + Some("we don't need discovery in spotty connect-stream mode".to_owned()) } else if opt_present(DISABLE_DISCOVERY) { Some(format!( "the `--{DISABLE_DISCOVERY}` / `-{DISABLE_DISCOVERY_SHORT}` flag set", @@ -2022,6 +2103,13 @@ async fn get_setup() -> Setup { let save_token = opt_str(SAVE_TOKEN).unwrap_or_else(|| "".to_string()); let client_id = opt_str(CLIENT_ID).unwrap_or_else(|| include_str!("client_id.txt").to_string()); + #[cfg(feature = "lms-connect")] + let lms = opt_str(LYRION_MUSIC_SERVER); + #[cfg(feature = "lms-connect")] + let lms_auth = opt_str(LMS_AUTH); + #[cfg(feature = "lms-connect")] + let player_mac = opt_str(PLAYER_MAC); + Setup { format, backend, @@ -2060,6 +2148,14 @@ async fn get_setup() -> Setup { } else { Some(client_id) }, + #[cfg(feature = "lms-connect")] + lms, + #[cfg(feature = "lms-connect")] + lms_auth, + #[cfg(feature = "lms-connect")] + player_mac, + #[cfg(feature = "lms-connect")] + connect_stream: opt_present(CONNECT_STREAM), } } @@ -2093,6 +2189,17 @@ async fn main() { let setup = get_setup().await; + // WR-04: --authenticate and --connect-stream are mutually exclusive. + // --authenticate exits after saving credentials; --connect-stream starts + // the HTTP stream server and prints stream_port=N. Combining them causes + // the daemon to print stream_port=N, then immediately exit after "authorized", + // leaving LMS with a port that refuses connections. + #[cfg(feature = "lms-connect")] + if setup.authenticate && setup.connect_stream { + error!("--authenticate and --connect-stream are mutually exclusive"); + exit(1); + } + let mut last_credentials = None; let mut spirc: Option = None; let mut spirc_task: Option> = None; @@ -2220,10 +2327,147 @@ async fn main() { let format = setup.format; let backend = setup.backend; let device = setup.device.clone(); + + // W6-locked: when the lms-connect feature is on, select the appropriate sink + // (HttpStreamSink in --connect-stream mode, ConnectNullSink otherwise). + // Non-feature builds fall through to the upstream `(backend)(device, format)` + // closure unchanged. + #[cfg(feature = "lms-connect")] + let spirc_active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + + // flush_tx_for_lms is set to Some(flush_tx.clone()) when connect_stream is true + // so the LMS event dispatcher can fire seek-flush signals. It is None in + // headless mode (no HTTP relay to drain). + #[cfg(feature = "lms-connect")] + let flush_tx_for_lms: Option>; + + #[cfg(feature = "lms-connect")] + let (player, pcm_rx_opt) = { + let _ = backend; // keep `setup.backend` selection valid for non-feature builds + if setup.connect_stream { + // HTTP streaming mode: create the PCM channel + flush watch-channel, + // wire HttpStreamSink, keep pcm_rx/flush_rx for http_stream_server + // spawning below. + let (pcm_tx, pcm_rx) = + tokio::sync::mpsc::channel::(256); + let (flush_tx, flush_rx) = tokio::sync::watch::channel::(0); + // Clone flush_tx for the LMS event dispatcher; sink holds the other copy. + flush_tx_for_lms = Some(flush_tx.clone()); + let player = Player::new( + player_config, + session.clone(), + soft_volume, + move || { + librespot::spotty::lms_connect::HttpStreamSink::open( + device.clone(), + format, + pcm_tx, + flush_tx, + ) + }, + ); + (player, Some((pcm_rx, flush_rx))) + } else { + // Headless Connect mode: discard PCM, no HTTP server needed. + flush_tx_for_lms = None; + let player = Player::new( + player_config, + session.clone(), + soft_volume, + move || { + librespot::spotty::lms_connect::ConnectNullSink::open( + device.clone(), + format, + ) + }, + ); + (player, None::<(tokio::sync::mpsc::Receiver, tokio::sync::watch::Receiver)>) + } + }; + + // Bind TcpListener, announce stream_port, spawn http_stream_server. + // All within #[cfg(feature = "lms-connect")] and guarded by pcm_rx_opt. + #[cfg(feature = "lms-connect")] + let (http_shutdown_tx_opt, http_handle_opt) = { + if let Some((pcm_rx, flush_rx)) = pcm_rx_opt { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind HTTP stream port"); + let port = listener + .local_addr() + .expect("Failed to get local addr") + .port(); + // Print port announcement BEFORE entering the event loop so + // Daemon.pm can read it synchronously (BIN-01 / D-02). + println!("stream_port={port}"); + // Explicit flush — stdout may be pipe-buffered (Pitfall 3). + use std::io::Write as _; + std::io::stdout().flush().expect("stdout flush failed"); + + let (http_shutdown_tx, http_shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + let spirc_active_clone = spirc_active.clone(); + let http_handle = tokio::spawn( + librespot::spotty::lms_connect::http_stream_server( + listener, + pcm_rx, + spirc_active_clone, + http_shutdown_rx, + flush_rx, + ), + ); + (Some(http_shutdown_tx), Some(http_handle)) + } else { + (None, None) + } + }; + #[cfg(not(feature = "lms-connect"))] let player = Player::new(player_config, session.clone(), soft_volume, move || { (backend)(device, format) }); + // Spawn the LMS PlayerEvent dispatcher loop. Requires --lms AND + // --player-mac to be configured; otherwise no-op. + // Pass flush_tx_for_lms so the dispatcher can fire seek-flush signals on + // PlayerEvent::Seeked (Some when connect_stream is true, None otherwise). + #[cfg(feature = "lms-connect")] + { + let lms = librespot::spotty::lms_connect::LMS::new( + setup.lms.clone(), + setup.player_mac.clone(), + setup.lms_auth.clone(), + flush_tx_for_lms, + ); + if lms.is_configured() { + let mut event_chan = player.get_player_event_channel(); + tokio::spawn(async move { + let mut current_track: Option = None; + while let Some(event) = event_chan.recv().await { + lms.handle_player_event(&event, &mut current_track).await; + } + }); + } + } + + // CR-03: Spawn a dedicated task that listens for PlayerEvent::SessionConnected + // and sets spirc_active=true only after the Spotify handshake completes — + // not immediately after Spirc::new returns. This prevents the HTTP stream + // server from returning 200 before audio is actually flowing. + // spirc_active=false paths (discovery replacement, spirc_task exit) remain + // in the main select! loop below because those are synchronous transitions + // that must happen before `connecting` is set. + #[cfg(feature = "lms-connect")] + if setup.connect_stream { + let mut session_event_chan = player.get_player_event_channel(); + let spirc_active_clone = spirc_active.clone(); + tokio::spawn(async move { + while let Some(event) = session_event_chan.recv().await { + if matches!(event, librespot_playback::player::PlayerEvent::SessionConnected { .. }) { + spirc_active_clone.store(true, std::sync::atomic::Ordering::SeqCst); + } + } + }); + } + #[cfg(not(feature = "spotty"))] if let Some(player_event_program) = setup.player_event_program.clone() { _event_handler = Some(EventHandler::new( @@ -2252,6 +2496,9 @@ async fn main() { auto_connect_times.clear(); if let Some(spirc) = spirc.take() { + // Session is being replaced — old Spirc shutting down. + #[cfg(feature = "lms-connect")] + spirc_active.store(false, std::sync::atomic::Ordering::SeqCst); if let Err(e) = spirc.shutdown() { error!("error sending spirc shutdown message: {e}"); } @@ -2299,6 +2546,11 @@ async fn main() { }; spirc = Some(spirc_); spirc_task = Some(Box::pin(spirc_task_)); + // CR-03: spirc_active is set to true only after + // PlayerEvent::SessionConnected fires (see the dedicated task + // spawned above), not here. Setting it here would race with + // Spirc's initial Hello/Welcome handshake and allow LMS to + // connect before audio is flowing. connecting = false; }, @@ -2308,6 +2560,9 @@ async fn main() { } }, if spirc_task.is_some() && !connecting => { spirc_task = None; + // Mark Spirc as inactive so http_stream_server returns 503. + #[cfg(feature = "lms-connect")] + spirc_active.store(false, std::sync::atomic::Ordering::SeqCst); warn!("Spirc shut down unexpectedly"); @@ -2357,6 +2612,15 @@ async fn main() { shutdown_tasks.spawn(discovery.shutdown()); } + // Shut down the HTTP stream server (BIN-06: graceful shutdown via JoinSet). + #[cfg(feature = "lms-connect")] + if let (Some(tx), Some(handle)) = (http_shutdown_tx_opt, http_handle_opt) { + shutdown_tasks.spawn(async move { + let _ = tx.send(()); + let _ = handle.await; + }); + } + tokio::select! { _ = tokio::signal::ctrl_c() => (), _ = shutdown_tasks.join_all() => (), diff --git a/src/spotty.rs b/src/spotty.rs index 351ddc905..1126911e3 100644 --- a/src/spotty.rs +++ b/src/spotty.rs @@ -1,3 +1,45 @@ +//! Spotty helper module. +//! +//! This file contains two distinct sections: +//! +//! 1. **Herger's Spotty functions** (`check`, `get_token`, `write_response`, `play_track`) — +//! used for the standalone streaming/token helpers. These are compiled when the `spotty` +//! Cargo feature is active (the default). +//! +//! 2. **LMS-Connect glue layer** (gated behind `#[cfg(feature = "lms-connect")]`) — +//! the JSON-RPC notification bridge that forwards librespot's `PlayerEvent` stream +//! into Lyrion Music Server's `spottyconnect` CLI command, plus a real-time-rate-limited +//! null audio sink for headless Connect-receiver mode. +//! +//! ## LMS-Connect architecture +//! +//! - [`lms_connect::LMS`] holds the wiring (LMS host, target player MAC, optional +//! HTTP-Basic auth) and the `suppress_next_volume` flag used to swallow the spurious +//! `VolumeChanged` Spotify pushes immediately after a `SessionConnected`. +//! - [`lms_connect::ConnectNullSink`] implements the playback `Sink` trait; it discards +//! decoded PCM frames but pseudo-rate-limits the call site so Spirc reports realistic +//! playback positions back to the Spotify cloud. +//! - [`lms_connect::LMS::handle_player_event`] consumes a `PlayerEvent` and emits a +//! `spottyconnect ` JSON-RPC dispatch into LMS. +//! +//! ## Wire vocabulary (the Phase-8 Perl handler must match) +//! +//! Five commands are emitted: `start`, `change`, `stop`, `volume`, `seek`. +//! `pause` is *not* emitted — the dispatcher collapses Paused and Stopped +//! variants into a single `stop` event, mirroring the contract that the +//! original hansherlighed-era plugin already speaks. +//! +//! ## Authorship of LMS-Connect section +//! +//! Written from scratch against librespot-org HEAD's `PlayerEvent` API. +//! The hansherlighed `fcdeecc` reference was read for *contract* (struct +//! field names, event name vocabulary, suppress-next-volume semantics) but +//! no code was byte-copied — see Phase 7 plan 04 deviation D-10. + +// --------------------------------------------------------------------------- +// Herger's Spotty helpers (check / get_token / write_response / play_track) +// --------------------------------------------------------------------------- + #[allow(unused)] use log::{error, info, warn}; @@ -5,28 +47,31 @@ use serde_json::{Value, json}; use std::fs; use std::process::exit; -use librespot::core::authentication::Credentials; -use librespot::core::session::Session; -use librespot::core::spotify_uri::SpotifyUri; +use librespot_core::authentication::Credentials; +use librespot_core::session::Session; +use librespot_core::spotify_uri::SpotifyUri; -use librespot::playback::audio_backend; -use librespot::playback::config::{AudioFormat, PlayerConfig}; -use librespot::playback::mixer::NoOpVolume; -use librespot::playback::player::Player; +use librespot_playback::audio_backend; +use librespot_playback::config::{AudioFormat, PlayerConfig}; +use librespot_playback::mixer::NoOpVolume; +use librespot_playback::player::Player; #[cfg(debug_assertions)] const DEBUGMODE: bool = true; #[cfg(not(debug_assertions))] const DEBUGMODE: bool = false; -pub const VERSION: &str = "2.1.0"; +pub const VERSION: &str = "2.2.0"; pub fn check(version_info: String) { println!("ok {}", version_info); - let capabilities = json!({ + let mut capabilities = json!({ "autoplay": true, + "connect-stream": true, "debug": DEBUGMODE, + "http-stream": true, + "keymaster-token": true, "lms-auth": true, "no-ap-port": true, "oauth": true, @@ -145,3 +190,799 @@ pub async fn play_track( } } } + +// --------------------------------------------------------------------------- +// LMS-Connect glue layer (feature = "lms-connect") +// --------------------------------------------------------------------------- + +#[cfg(feature = "lms-connect")] +pub mod lms_connect { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + use std::time::{Duration, Instant}; + + use bytes::Bytes; + use futures_util::StreamExt; + use http_body_util::{BodyExt, Full, StreamBody}; + use hyper::body::Frame; + use hyper::server::conn::http1; + use hyper::{Response, StatusCode}; + use hyper_util::rt::TokioIo; + use hyper_util::server::graceful::GracefulShutdown; + use log::{info, warn}; + use serde_json::json; + use tokio::io::AsyncWriteExt; + use tokio::net::TcpStream; + use tokio::sync::{mpsc, watch}; + use tokio_stream::wrappers::ReceiverStream; + + use librespot_playback::audio_backend::{Sink, SinkError, SinkResult}; + use librespot_playback::config::AudioFormat; + use librespot_playback::convert::Converter; + use librespot_playback::decoder::AudioPacket; + use librespot_playback::player::PlayerEvent; + use librespot_playback::{NUM_CHANNELS, SAMPLE_RATE}; + + // ------------------------------------------------------------------------- + // LMS struct + // ------------------------------------------------------------------------- + + /// LMS-side notification target. + /// + /// `host_port` is `":"` (typically `"localhost:9000"`). + /// `player_mac` is the colon-separated MAC of the target LMS player. + /// `auth` is an optional pre-base64-encoded `user:pass` string. + /// + /// `suppress_next_volume` is set the moment Spirc fires `SessionConnected`, + /// then consumed (cleared) on the very next `VolumeChanged`. This avoids + /// pushing Spotify's stored device volume back to LMS immediately after a + /// transfer-to-player handshake — that initial push is a Spotify-cloud + /// echo, not a user action, and would otherwise clobber LMS-side volume. + pub struct LMS { + pub host_port: Option, + pub player_mac: Option, + pub auth: Option, + pub suppress_next_volume: Arc, + /// Sender half of the watch channel used to signal the relay task to drain + /// pre-seek PCM bytes. Incremented on every `PlayerEvent::Seeked`. + pub flush_tx: Option>, + /// Monotonically increasing generation counter; incremented on seek. + pub seek_gen: Arc, + /// Set after TrackChanged emits `start`; the next same-id `Playing` + /// event will send a `seek` notification with the actual position so + /// LMS can sync its progress bar for mid-song connects. + pub needs_position_sync: Arc, + } + + impl LMS { + pub fn new( + host_port: Option, + player_mac: Option, + auth: Option, + flush_tx: Option>, + ) -> Self { + Self { + host_port, + player_mac, + // CR-02: sanitize `--lms-auth` credential to prevent CRLF injection + // into the hand-rolled HTTP/1.0 notify request. `trim()` removes + // leading/trailing whitespace; `replace` strips any embedded CR/LF + // that would let an attacker inject arbitrary request headers. + auth: auth.map(|raw| raw.trim().replace(['\r', '\n'], "").to_owned()), + suppress_next_volume: Arc::new(AtomicBool::new(false)), + flush_tx, + seek_gen: Arc::new(AtomicU64::new(0)), + needs_position_sync: Arc::new(AtomicBool::new(false)), + } + } + + /// True iff both the LMS host and the player MAC have been configured. + /// Without either, `notify` is a no-op and the dispatcher short-circuits. + pub fn is_configured(&self) -> bool { + self.host_port.is_some() && self.player_mac.is_some() + } + } + + impl Clone for LMS { + fn clone(&self) -> Self { + Self { + host_port: self.host_port.clone(), + player_mac: self.player_mac.clone(), + auth: self.auth.clone(), + // Important: clone the Arc so all clones share the same flag. + // The wiring spawns a Tokio task that takes ownership of one + // clone; the suppression flag must remain a single shared cell. + suppress_next_volume: Arc::clone(&self.suppress_next_volume), + // flush_tx is not Clone (watch::Sender doesn't implement Clone), + // so clones do not hold a sender. Only the original LMS instance + // (the one passed to the event dispatcher) fires flush signals. + flush_tx: None, + seek_gen: Arc::clone(&self.seek_gen), + needs_position_sync: Arc::clone(&self.needs_position_sync), + } + } + } + + // ------------------------------------------------------------------------- + // PlayerEvent dispatcher + JSON-RPC notifier + // ------------------------------------------------------------------------- + + impl LMS { + /// Consume one [`PlayerEvent`] and emit zero-or-one matching + /// `spottyconnect ` JSON-RPC dispatches. + /// + /// `current_track` is the dispatch loop's persistent cursor: the base62 + /// id of whatever track we last saw `Playing`. It is mutated in place. + /// + /// The five emitted command names — `start`, `change`, `stop`, + /// `volume`, `seek` — are the wire vocabulary the Spotty-Plugin's + /// `Connect::_connectEvent` Perl handler will match in Phase 8. + /// `pause` is intentionally *not* emitted; Paused and Stopped both + /// collapse into a single `stop` event (per existing plugin contract). + pub async fn handle_player_event( + &self, + event: &PlayerEvent, + current_track: &mut Option, + ) { + if !self.is_configured() { + return; + } + + match event { + // Playing fires for: track-start, un-pause, post-seek, and + // buffer-underrun re-emit. We emit `start` only on a clean + // None -> Some transition; same-id re-emits are no-ops, and + // a different id replaces the cursor with `change`. + // Exception: after TrackChanged sent `start`, the next same-id + // Playing carries position_ms — send it as `seek` so the Perl + // side can sync the progress bar for mid-song connects. + PlayerEvent::Playing { track_id, position_ms, .. } => { + let new_id = track_id.to_id().unwrap_or_default(); + match current_track.as_deref() { + Some(prev) if prev == new_id.as_str() => { + if self.needs_position_sync.load(Ordering::Acquire) { + self.needs_position_sync.store(false, Ordering::Release); + let secs = f64::from(*position_ms) / 1000.0; + if secs > 1.0 { + self.notify("seek", &format!("{secs:.3}"), "").await; + } + } + } + Some(_) => { + self.needs_position_sync.store(false, Ordering::Release); + let prev = current_track.replace(new_id.clone()).unwrap_or_default(); + self.notify("change", &new_id, &prev).await; + } + None => { + *current_track = Some(new_id.clone()); + self.notify("start", &new_id, "").await; + } + } + } + + // Both Paused and Stopped collapse into `stop`. Only fire if + // we actually had an active track — guards against duplicate + // stop events on idle daemon. + PlayerEvent::Paused { .. } | PlayerEvent::Stopped { .. } => { + if current_track.take().is_some() { + self.notify("stop", "", "").await; + } + } + + // VolumeChanged: librespot reports 0..=65535; LMS speaks + // 0..=100. The first event after SessionConnected is a + // Spotify-cloud echo, not a user action — see suppress flag. + PlayerEvent::VolumeChanged { volume } => { + if self.suppress_next_volume.swap(false, Ordering::Relaxed) { + info!( + "lms-connect: suppressed activation-time volume push from Spotify (raw={})", + volume + ); + return; + } + let pct = u32::from(*volume) * 100 / 65535; + self.notify("volume", &pct.to_string(), "").await; + } + + // Seeked: report position in seconds (3 decimals). Only valid + // mid-playback — without an active track, the seek vocabulary + // has no LMS-side referent. + // Also fires the watch-channel flush signal so the relay task + // drains pre-seek PCM bytes (D-03 in-place drain variant). + // Pitfall 5: only fire on Seeked, never on Playing (which also + // fires on resume) to avoid draining during gapless transitions. + PlayerEvent::Seeked { position_ms, .. } => { + if current_track.is_some() { + let secs = f64::from(*position_ms) / 1000.0; + self.notify("seek", &format!("{secs:.3}"), "").await; + } + // Fire flush regardless of whether a track is active: the + // relay must drain even if current_track is not yet set. + if let Some(tx) = &self.flush_tx { + let new_gen = self.seek_gen.fetch_add(1, Ordering::Release) + 1; + tx.send(new_gen).ok(); + info!("lms-connect: seek-flush signal sent (gen={})", new_gen); + } + } + + // Spirc just connected to Spotify. The next VolumeChanged is + // Spotify's stored device volume being pushed back; flag it + // for suppression so we don't clobber LMS-side volume. + PlayerEvent::SessionConnected { .. } => { + self.suppress_next_volume.store(true, Ordering::Relaxed); + } + + // TrackChanged fires when librespot loads a new track (e.g. + // playlist jump via Spotify app). Update the cursor and emit + // `change` so the Perl side can switch. `Playing` may follow + // later and will be a same-id no-op. + PlayerEvent::TrackChanged { audio_item } => { + let new_id = audio_item.track_id.to_id().unwrap_or_default(); + match current_track.as_deref() { + Some(prev) if prev == new_id.as_str() => { /* same track */ } + Some(_) => { + self.needs_position_sync.store(false, Ordering::Release); + let prev = current_track.replace(new_id.clone()).unwrap_or_default(); + self.notify("change", &new_id, &prev).await; + } + None => { + self.needs_position_sync.store(true, Ordering::Release); + *current_track = Some(new_id.clone()); + self.notify("start", &new_id, "").await; + } + } + } + + // Everything else (Loading, Preloading, EndOfTrack, + // SetQueue, PositionChanged, ...) — no LMS equivalent. + _ => {} + } + } + + /// POST a `spottyconnect ` JSON-RPC slim.request to LMS. + /// + /// Opens a fresh TCP connection per event (no keep-alive). Errors are + /// logged at WARN; the daemon must never panic on a transient LMS + /// outage. If `auth` is set, an `Authorization: Basic ` header + /// is added; the value is sent verbatim (caller pre-encoded it). + async fn notify(&self, cmd: &str, p1: &str, p2: &str) { + let host_port = match self.host_port.as_deref() { + Some(h) => h, + None => return, + }; + let player_mac = match self.player_mac.as_deref() { + Some(m) => m, + None => return, + }; + + // Build the variadic spottyconnect command array. Empty trailing + // params are dropped — matches the over-the-wire shape the Perl + // _connectEvent handler historically expects. + let mut params: Vec = Vec::with_capacity(4); + params.push(json!("spottyconnect")); + params.push(json!(cmd)); + if !p1.is_empty() { + params.push(json!(p1)); + } + if !p2.is_empty() { + params.push(json!(p2)); + } + let body = json!({ + "id": 1, + "method": "slim.request", + "params": [player_mac, params], + }) + .to_string(); + + let auth_header = match self.auth.as_deref() { + Some(creds) => format!("Authorization: Basic {creds}\r\n"), + None => String::new(), + }; + + let request = format!( + "POST /jsonrpc.js HTTP/1.0\r\n\ + Host: {host_port}\r\n\ + Content-Type: application/json\r\n\ + Content-Length: {len}\r\n\ + {auth_header}\ + \r\n\ + {body}", + len = body.len(), + ); + + match TcpStream::connect(host_port).await { + Ok(mut stream) => { + if let Err(e) = stream.write_all(request.as_bytes()).await { + warn!("lms-connect: write_all to {host_port} failed for {cmd}: {e}"); + } + } + Err(e) => { + warn!("lms-connect: TcpStream::connect({host_port}) failed for {cmd}: {e}"); + } + } + } + } + + // ------------------------------------------------------------------------- + // ConnectNullSink + // ------------------------------------------------------------------------- + + /// Audio sink for headless Connect-receiver builds. + /// + /// We don't actually emit audio — LMS owns the audio path — but we still + /// need to *consume* librespot's decoded PCM at roughly real-time pace so + /// Spirc's playback-position reports stay believable. A naïve "drop every + /// packet immediately" sink would let the player race ahead of wall-clock + /// time, which makes Spotify clients (phone app, Connect API) display + /// nonsensical seek positions. + /// + /// The implementation tracks how many stereo frames have been consumed + /// since `start()` and parks the calling thread until wall-clock time has + /// caught up to the implied PCM duration. + pub struct ConnectNullSink { + began_at: Instant, + frames_consumed: u64, + } + + impl ConnectNullSink { + /// Constructor matching the `SinkBuilder` signature so this can be + /// passed straight to `Player::new`. + pub fn open(_device: Option, _format: AudioFormat) -> Box { + Box::new(Self { + began_at: Instant::now(), + frames_consumed: 0, + }) + } + } + + impl Sink for ConnectNullSink { + fn start(&mut self) -> SinkResult<()> { + // Reset the wall-clock anchor every time playback begins so the + // rate-limiter doesn't drift across pause/resume cycles. + self.began_at = Instant::now(); + self.frames_consumed = 0; + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + // CRITICAL: do NOT exit() the process here. The pipe/StdoutSink + // backend in librespot terminates the daemon at end-of-stream; + // a Connect-receiver must outlive individual track stops to + // handle pause/resume and track changes. + self.frames_consumed = 0; + Ok(()) + } + + fn write(&mut self, packet: AudioPacket, _converter: &mut Converter) -> SinkResult<()> { + let AudioPacket::Samples(samples) = packet else { + // Raw passthrough variant — not in scope for this sink; just + // accept and discard. Spirc still progresses its position. + return Ok(()); + }; + + let frames_in_packet = (samples.len() / NUM_CHANNELS as usize) as u64; + self.frames_consumed = self.frames_consumed.saturating_add(frames_in_packet); + + // expected_ns = frames_consumed * 1e9 / SAMPLE_RATE + // u128 prevents overflow at multi-hour playback durations. + let expected_ns: u128 = + u128::from(self.frames_consumed) * 1_000_000_000u128 / u128::from(SAMPLE_RATE); + let elapsed_ns: u128 = self.began_at.elapsed().as_nanos(); + + if expected_ns > elapsed_ns { + let park_ns = (expected_ns - elapsed_ns) as u64; + std::thread::sleep(Duration::from_nanos(park_ns)); + } + Ok(()) + } + } + + // ------------------------------------------------------------------------- + // HttpStreamSink + // ------------------------------------------------------------------------- + + /// Audio sink for `--connect-stream` mode. + /// + /// Unlike [`ConnectNullSink`] (which discards decoded PCM), this sink sends + /// a continuous S16LE stereo stream over an mpsc channel to the HTTP stream + /// server, allowing LMS to consume it via `canDirectStream` as a plain HTTP + /// audio source. + /// + /// Unlike `pipe.rs::StdoutSink` (which calls `exit(0)` in `stop()`), this + /// sink's `stop()` only resets counters. The process outlives individual + /// track boundaries so Spotify Connect can deliver gapless playback across + /// the LMS player's lifetime. + /// + /// Rate-limiting follows the same nanosecond wall-clock math as + /// [`ConnectNullSink`], using plain `std::thread::sleep` since the Player + /// runs `Sink::write` on a dedicated OS thread (std::thread::spawn in + /// player.rs), not on a Tokio worker. `blocking_send` is therefore safe + /// here (BIN-03). + pub struct HttpStreamSink { + pcm_tx: mpsc::Sender, + /// Sender half of the flush watch-channel. Held here purely for ownership + /// (keeping the channel open); the actual flush signals are fired from + /// `LMS::handle_player_event` on `PlayerEvent::Seeked`. + #[allow(dead_code)] + flush_tx: watch::Sender, + began_at: Instant, + frames_consumed: u64, + } + + impl HttpStreamSink { + /// Constructor for use in the `--connect-stream` wiring. + /// + /// `pcm_tx` is the sending half of the channel that connects this sink + /// (OS thread) to `http_stream_server` (Tokio task). + /// + /// `flush_tx` is held for ownership only; flush signals are sent by + /// `LMS::handle_player_event` on `PlayerEvent::Seeked`. + /// + /// Panics if `format != AudioFormat::S16` — only S16LE is supported + /// (pitfall S-03: format continuity). + pub fn open( + _device: Option, + format: AudioFormat, + pcm_tx: mpsc::Sender, + flush_tx: watch::Sender, + ) -> Box { + if format != AudioFormat::S16 { + panic!( + "HttpStreamSink: only AudioFormat::S16 supported, got {:?}", + format + ); + } + Box::new(Self { + pcm_tx, + flush_tx, + began_at: Instant::now(), + frames_consumed: 0, + }) + } + } + + impl Sink for HttpStreamSink { + fn start(&mut self) -> SinkResult<()> { + // Reset the wall-clock anchor every time playback begins so the + // rate-limiter doesn't drift across pause/resume cycles. + self.began_at = Instant::now(); + self.frames_consumed = 0; + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + // CRITICAL: do NOT exit() the process here. The pipe/StdoutSink + // backend in librespot terminates the daemon at end-of-stream under + // #[cfg(feature = "spotty")]; that is designed for --single-track. + // HttpStreamSink must survive track boundaries for gapless Connect + // playback (BIN-03). No stdout flush needed — we write to an mpsc + // channel, not stdout. + self.frames_consumed = 0; + self.began_at = Instant::now(); + Ok(()) + } + + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { + let AudioPacket::Samples(samples) = packet else { + // Raw passthrough variant — not in scope for this sink; skip. + return Ok(()); + }; + + // Convert f64 samples to S16LE and reinterpret as a byte slice. + // SAFETY: i16 has alignment 2 and size 2; the resulting byte slice + // has len = samples_s16.len() * 2 and points to valid memory for + // the lifetime of `samples_s16`. This is equivalent to zerocopy's + // IntoBytes::as_bytes() but avoids adding zerocopy as a dependency + // to the spotty binary crate (zerocopy lives in the playback crate). + let samples_s16 = converter.f64_to_s16(&samples); + // SAFETY: `i16` values are valid to view as two `u8` bytes; pointer + // and length are derived from the valid Vec allocation. + let bytes: &[u8] = unsafe { + std::slice::from_raw_parts( + samples_s16.as_ptr().cast::(), + samples_s16.len() * std::mem::size_of::(), + ) + }; + + // Rate-limiter — identical to ConnectNullSink (pitfall S-01). + // Without this sleep the decoder races ahead of wall-clock time, + // making Spotify clients show nonsensical seek positions. + let frames_in_packet = (samples.len() / NUM_CHANNELS as usize) as u64; + self.frames_consumed = self.frames_consumed.saturating_add(frames_in_packet); + let expected_ns: u128 = + u128::from(self.frames_consumed) * 1_000_000_000u128 / u128::from(SAMPLE_RATE); + let elapsed_ns: u128 = self.began_at.elapsed().as_nanos(); + + if expected_ns > elapsed_ns { + let park_ns = (expected_ns - elapsed_ns) as u64; + std::thread::sleep(Duration::from_nanos(park_ns)); + } + + // Send PCM bytes over the channel to the HTTP stream server. + // Player::new spawns a std::thread but runs block_on() with its + // own tokio Runtime inside it (player.rs:517), so Sink::write + // executes within a tokio context. blocking_send would panic. + // Use try_send with a spin-retry: the channel has 256 slots + // (~1.5s of audio) and a single consumer, so contention is + // transient — the rate-limiter above already paces us to + // real-time, keeping the channel nearly empty. + let chunk = Bytes::copy_from_slice(bytes); + loop { + match self.pcm_tx.try_send(chunk.clone()) { + Ok(()) => break, + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + std::thread::sleep(Duration::from_millis(1)); + } + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + return Err(SinkError::OnWrite( + "HTTP stream server shut down".into(), + )); + } + } + } + + Ok(()) + } + } + + // ------------------------------------------------------------------------- + // http_stream_server + // ------------------------------------------------------------------------- + + /// HTTP server that streams PCM audio to LMS via a single persistent + /// GET /stream endpoint. + /// + /// Listens on the supplied `listener` (bound to 127.0.0.1 before calling + /// this function). Receives decoded S16LE PCM from [`HttpStreamSink`] via + /// `pcm_rx`. Uses a relay-per-connection pattern so the single + /// `mpsc::Receiver` is shared across sequential connections without cloning. + /// + /// ## Connection lifecycle + /// + /// 1. Accept incoming TCP connection. + /// 2. If `spirc_active` is false → return 503 with `Retry-After: 2`. Close. + /// 3. Otherwise: + /// a. Drain stale PCM chunks from `pcm_rx` via `try_recv` (D-03). + /// b. Spawn a relay task that forwards from `pcm_rx` to a per-connection + /// bounded channel (`conn_tx` / `conn_rx`). + /// c. Serve a 200 response with `Content-Type: audio/L16;rate=44100;channels=2` + /// and a streaming body backed by `ReceiverStream(conn_rx)`. + /// d. When LMS disconnects, `conn_tx.send` in the relay fails → relay + /// exits → the Mutex over `pcm_rx` is released for the next connection. + /// + /// ## Shutdown + /// + /// When `shutdown_rx` fires, the accept loop breaks and + /// `graceful.shutdown().await` drains in-flight connections. + pub async fn http_stream_server( + listener: tokio::net::TcpListener, + pcm_rx: mpsc::Receiver, + spirc_active: Arc, + shutdown_rx: tokio::sync::oneshot::Receiver<()>, + flush_rx: watch::Receiver, + ) { + use std::sync::Mutex; + + let server = http1::Builder::new(); + let graceful = GracefulShutdown::new(); + let mut shutdown_rx = std::pin::pin!(shutdown_rx); + + // Wrap pcm_rx in Arc so the relay task can acquire it + // exclusively without requiring pcm_rx: Clone (it isn't). + let pcm_rx = Arc::new(Mutex::new(pcm_rx)); + + // Wrap flush_rx in Arc so it can be shared across connections. + let flush_rx = Arc::new(Mutex::new(flush_rx)); + + // CR-01: guard against concurrent relay tasks that would split the PCM + // stream across two competing connections. When a relay is already active + // a second HTTP connection gets 503 with Retry-After: 1 and should retry + // once the first relay exits and clears this flag. + let relay_active = Arc::new(AtomicBool::new(false)); + + loop { + tokio::select! { + accept_result = listener.accept() => { + let (stream, _addr) = match accept_result { + Ok(pair) => pair, + Err(e) => { + warn!("http_stream_server: accept error: {e}"); + continue; + } + }; + + let spirc_active = Arc::clone(&spirc_active); + let pcm_rx = Arc::clone(&pcm_rx); + let flush_rx = Arc::clone(&flush_rx); + let relay_active = Arc::clone(&relay_active); + + // Build the service function for this single connection. + // Both response paths (503 and 200-stream) are erased to + // BoxBody so the return type is uniform. + let svc = hyper::service::service_fn(move |req: hyper::Request| { + let spirc_active = Arc::clone(&spirc_active); + let pcm_rx = Arc::clone(&pcm_rx); + let flush_rx = Arc::clone(&flush_rx); + let relay_active = Arc::clone(&relay_active); + async move { + // WR-02: validate request method and path. Only GET /stream + // is served as audio; anything else gets 404/405. + if req.method() != hyper::Method::GET { + let body = Full::new(Bytes::new()) + .map_err(|e| match e {}) + .boxed(); + let resp = Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header("Content-Length", "0") + .body(body) + .expect("BUG: static 405 response builder failed"); + return Ok::>, hyper::Error>(resp); + } + if req.uri().path() != "/stream" { + let body = Full::new(Bytes::new()) + .map_err(|e| match e {}) + .boxed(); + let resp = Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Length", "0") + .body(body) + .expect("BUG: static 404 response builder failed"); + return Ok::>, hyper::Error>(resp); + } + + if !spirc_active.load(Ordering::SeqCst) { + // Spirc not active yet — tell LMS to retry shortly. + let body = Full::new(Bytes::new()) + .map_err(|e| match e {}) + .boxed(); + let resp = Response::builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("Retry-After", "2") + .header("Content-Length", "0") + .body(body) + .expect("BUG: static 503 (spirc inactive) response builder failed"); + return Ok::>, hyper::Error>(resp); + } + + // CR-01: reject concurrent relay attempts — the PCM stream + // must flow to exactly one connection at a time. The old + // relay task clears the flag on exit. + if relay_active.swap(true, Ordering::AcqRel) { + let body = Full::new(Bytes::new()) + .map_err(|e| match e {}) + .boxed(); + let resp = Response::builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .header("Retry-After", "1") + .header("Content-Length", "0") + .body(body) + .expect("BUG: static 503 (relay busy) response builder failed"); + return Ok::>, hyper::Error>(resp); + } + + // Drain stale pre-seek audio from the channel (D-03). + { + let mut rx = pcm_rx.lock().unwrap(); + while rx.try_recv().is_ok() {} + } + + // Per-connection relay channel (capacity 64 frames). + let (conn_tx, conn_rx) = mpsc::channel::(64); + + // Relay task: acquires exclusive access to pcm_rx via + // the Mutex and forwards chunks to conn_tx. Uses + // recv().await (WR-01: no busy-poll) rather than + // try_recv/yield_now. Clears relay_active on exit so + // the next connection can start a new relay (CR-01). + // + // At the top of each loop iteration, checks flush_rx + // for a new seek generation. If one is found, drains + // the pcm_rx of pre-seek bytes before forwarding + // post-seek audio (in-place relay drain, Approach D). + let pcm_rx_clone = Arc::clone(&pcm_rx); + let flush_rx_clone = Arc::clone(&flush_rx); + let relay_active_clone = Arc::clone(&relay_active); + tokio::spawn(async move { + // Take exclusive ownership of the receiver for the + // duration of this relay. The Mutex ensures only one + // relay task holds the receiver at a time. + // + // Initialise last_seen_gen from the current value so + // we don't drain on the very first iteration. + let mut last_seen_gen: u64 = { + let rx = flush_rx_clone.lock().unwrap(); + *rx.borrow() + }; + + loop { + // --- Seek-flush drain (top of loop, before read) --- + // Poll has_changed() without holding the mutex + // across an await point. + let flush_pending = { + let rx = flush_rx_clone.lock().unwrap(); + rx.has_changed().unwrap_or(false) + }; + if flush_pending { + let new_gen = { + let mut rx = flush_rx_clone.lock().unwrap(); + *rx.borrow_and_update() + }; + if new_gen > last_seen_gen { + // Drain all queued pre-seek PCM bytes. + let mut count: u64 = 0; + let mut rx = pcm_rx_clone.lock().unwrap(); + while rx.try_recv().is_ok() { + count += 1; + } + last_seen_gen = new_gen; + info!( + "lms-connect: relay flushed {} pre-seek chunks (gen={})", + count, new_gen + ); + } + } + + // --- Normal relay --- + // WR-01: poll once and release the lock before + // yielding to the runtime (must not hold mutex + // across await point). + let chunk = { + let mut rx = pcm_rx_clone.lock().unwrap(); + rx.try_recv().ok() + }; + match chunk { + Some(bytes) => { + if conn_tx.send(bytes).await.is_err() { + // Client disconnected — relay done. + break; + } + } + None => { + // No data available right now. Sleep + // briefly (1 ms) instead of a hot + // yield_now loop. At 44100 Hz / 2ch + // packets arrive ~every 10 ms so 1 ms + // adds negligible latency. + tokio::time::sleep( + std::time::Duration::from_millis(1), + ) + .await; + } + } + } + // Clear the relay-active flag so the next LMS + // reconnect can start a fresh relay (CR-01). + relay_active_clone.store(false, Ordering::Release); + }); + + // Build streaming response body, erased to BoxBody. + let stream = ReceiverStream::new(conn_rx) + .map(|chunk| Ok::, hyper::Error>(Frame::data(chunk))); + let body = BodyExt::boxed(StreamBody::new(stream)); + + let resp = Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "audio/L16;rate=44100;channels=2") + .body(body) + .expect("BUG: static 200 response builder failed"); + + Ok::>, hyper::Error>(resp) + } + }); + + let io = TokioIo::new(stream); + let conn = server.serve_connection(io, svc); + let fut = graceful.watch(conn); + tokio::spawn(async move { + let _ = fut.await; + }); + } + _ = &mut shutdown_rx => { + break; + } + } + } + + graceful.shutdown().await; + } +}