diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index 2d949ad6..08e0f07d 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -ARG alpine_version=alpine3.19 +ARG alpine_version=alpine3.20 ARG rust_version=1.85.0 FROM rust:${rust_version}-${alpine_version} @@ -15,6 +15,7 @@ RUN apk add --no-cache \ pkgconf \ musl-dev \ # developer dependencies + openssl-dev \ libunwind-dev \ pulseaudio-dev \ portaudio-dev \ diff --git a/CHANGELOG.md b/CHANGELOG.md index c89b12c3..a11d932e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. +## [Unreleased] + +### Added + +- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can + +### Changed + +- [playback] Changed type of `SpotifyId` fields in `PlayerEvent` members to `SpotifyUri` (breaking) +- [metadata] Changed arguments for `Metadata` trait from `&SpotifyId` to `&SpotifyUri` (breaking) +- [player] `load` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) +- [player] `preload` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) +- [spclient] `get_radio_for_track` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) + + +### Removed + +- [core] Removed `SpotifyItemType` enum; the new `SpotifyUri` is an enum over all item types and so which variant it is + describes its item type (breaking) +- [core] Removed `NamedSpotifyId` struct; it was made obsolete by `SpotifyUri` (breaking) +- [core] The following methods have been removed from `SpotifyId` and moved to `SpotifyUri` (breaking): + - `is_playable` + - `from_uri` + - `to_uri` + ## [v0.7.1] - 2025-08-31 ### Changed diff --git a/Cargo.lock b/Cargo.lock index f2bd9aef..0ad27773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -62,7 +62,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" dependencies = [ "alsa-sys", - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -77,12 +77,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -144,9 +138,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayvec" @@ -241,9 +235,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -280,10 +274,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.34" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -295,9 +290,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-expr" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" +checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", "target-lexicon", @@ -317,17 +312,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -541,9 +535,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -597,7 +591,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "objc2", ] @@ -694,12 +688,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -730,10 +724,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "fixedbitset" -version = "0.4.2" +name = "find-msvc-tools" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" @@ -931,7 +925,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -943,24 +937,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" +checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "glib" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" +checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "futures-channel", "futures-core", "futures-executor", @@ -977,9 +971,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" +checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f" dependencies = [ "heck", "proc-macro-crate", @@ -990,9 +984,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" +checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" dependencies = [ "libc", "system-deps", @@ -1000,9 +994,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" +checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" dependencies = [ "glib-sys", "libc", @@ -1019,12 +1013,10 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "getrandom 0.3.3", - "hashbrown", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", - "rand 0.9.2", "smallvec", "spinning_top", "web-time", @@ -1032,9 +1024,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f5db514ad5ccf70ad35485058aa8b894bb81cfcf76bb994af135d9789427c6" +checksum = "3e7ba7a2584e31927b7fec6a32737b57dc991b55253c9bb7c2c8eddb5a4cb345" dependencies = [ "cfg-if", "futures-channel", @@ -1049,7 +1041,7 @@ dependencies = [ "num-integer", "num-rational", "option-operations", - "paste", + "pastey", "pin-project-lite", "smallvec", "thiserror 2.0.16", @@ -1057,9 +1049,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad8ae64a7af6d1aa04e96db085a0cbd64a6b838d85c115c99fa053ab8902d98" +checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" dependencies = [ "futures-core", "futures-sink", @@ -1085,9 +1077,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404c5d0cbb2189e6a10d05801e93f47fe60b195e4d73dd1c540d055f7b340b8" +checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8" dependencies = [ "cfg-if", "glib", @@ -1114,9 +1106,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34745d3726a080e0d57e402a314e37073d0b341f3a5754258550311ca45e4754" +checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4" dependencies = [ "atomic_refcell", "cfg-if", @@ -1128,9 +1120,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfad00fa63ddd8132306feef9d5095a3636192f09d925adfd0a9be0d82b9ea91" +checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e" dependencies = [ "glib-sys", "gobject-sys", @@ -1141,9 +1133,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f46b35f9dc4b5a0dca3f19d2118bb5355c3112f228a99a84ed555f48ce5cf9" +checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377" dependencies = [ "cfg-if", "glib-sys", @@ -1182,6 +1174,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "headers" version = "0.4.1" @@ -1244,7 +1242,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1370,11 +1368,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tower-service", "webpki-roots 1.0.2", ] @@ -1397,9 +1395,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -1413,7 +1411,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "system-configuration", "tokio", "tower-service", @@ -1423,9 +1421,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1433,7 +1431,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.0", ] [[package]] @@ -1560,22 +1558,22 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8" +checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -1593,7 +1591,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1660,7 +1658,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "jack-sys", "lazy_static", "libc", @@ -1729,9 +1727,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -1779,9 +1777,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmdns" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862" +checksum = "a00dbe871d2cf9df13f68d152b949fca8cafc854b60ffd259fc6df6e8663d8d7" dependencies = [ "byteorder", "futures-util", @@ -1789,9 +1787,9 @@ dependencies = [ "if-addrs", "log", "multimap", - "rand 0.8.5", - "socket2 0.5.10", - "thiserror 1.0.69", + "rand 0.9.2", + "socket2", + "thiserror 2.0.16", "tokio", ] @@ -1801,7 +1799,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "libc", "libpulse-sys", "num-derive", @@ -1880,7 +1878,6 @@ dependencies = [ "hyper-util", "librespot-core", "log", - "parking_lot", "tempfile", "thiserror 2.0.16", "tokio", @@ -1935,7 +1932,6 @@ dependencies = [ "num-derive", "num-integer", "num-traits", - "parking_lot", "pbkdf2", "pin-project-lite", "priority-queue", @@ -2040,7 +2036,6 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot", "portable-atomic", "portaudio-rs", "rand 0.9.2", @@ -2070,9 +2065,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2092,9 +2087,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-slab" @@ -2163,9 +2158,6 @@ name = "multimap" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -dependencies = [ - "serde", -] [[package]] name = "native-tls" @@ -2190,7 +2182,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "jni-sys", "log", "ndk-sys", @@ -2219,7 +2211,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -2392,7 +2384,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "libc", "objc2", "objc2-core-audio", @@ -2419,7 +2411,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "objc2", ] @@ -2429,7 +2421,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "dispatch2", "objc2", ] @@ -2506,7 +2498,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2546,11 +2538,11 @@ dependencies = [ [[package]] name = "option-operations" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689" dependencies = [ - "paste", + "pastey", ] [[package]] @@ -2585,21 +2577,18 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "backtrace", "cfg-if", "libc", - "petgraph", "redox_syscall", "smallvec", - "thread-id", "windows-targets 0.52.6", ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pathdiff" @@ -2632,16 +2621,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2743,22 +2722,21 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" +checksum = "3e7f4ffd8645efad783fc2844ac842367aa2e912d484950192564d57dc039a3a" dependencies = [ - "autocfg", "equivalent", "indexmap", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] @@ -2854,8 +2832,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.31", - "socket2 0.6.0", + "rustls 0.23.32", + "socket2", "thiserror 2.0.16", "tokio", "tracing", @@ -2874,7 +2852,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-pki-types", "slab", "thiserror 2.0.16", @@ -2892,7 +2870,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -2987,7 +2965,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", ] [[package]] @@ -3043,7 +3021,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", @@ -3052,7 +3030,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tower", "tower-http", "tower-service", @@ -3126,7 +3104,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3135,15 +3113,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -3162,14 +3140,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.6", "subtle", "zeroize", ] @@ -3196,7 +3174,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.4.0", ] [[package]] @@ -3231,9 +3209,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -3263,11 +3241,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3305,7 +3283,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3314,11 +3292,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3327,9 +3305,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3337,18 +3315,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -3357,24 +3345,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -3483,16 +3473,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3687,7 +3667,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3723,15 +3703,15 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -3774,21 +3754,11 @@ dependencies = [ "syn", ] -[[package]] -name = "thread-id" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3803,15 +3773,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3853,11 +3823,10 @@ dependencies = [ "io-uring", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "tracing", "windows-sys 0.59.0", @@ -3897,11 +3866,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls 0.23.31", + "rustls 0.23.32", "tokio", ] @@ -3925,12 +3894,12 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tungstenite", "webpki-roots 0.26.11", ] @@ -3956,8 +3925,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -3969,6 +3938,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3978,7 +3956,28 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -4003,7 +4002,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -4077,7 +4076,7 @@ dependencies = [ "log", "native-tls", "rand 0.9.2", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-pki-types", "sha1", "thiserror 2.0.16", @@ -4103,9 +4102,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" @@ -4151,13 +4150,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", - "js-sys", - "wasm-bindgen", ] [[package]] @@ -4249,30 +4246,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -4284,9 +4291,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -4297,9 +4304,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4307,9 +4314,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -4320,18 +4327,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4405,11 +4412,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -4437,7 +4444,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -4468,9 +4475,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -4480,7 +4500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -4512,6 +4532,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -4519,7 +4545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4528,9 +4554,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -4548,7 +4574,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4557,7 +4592,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4596,6 +4640,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4633,7 +4686,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4650,7 +4703,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4802,9 +4855,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4838,9 +4891,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" dependencies = [ "async-broadcast", "async-recursion", @@ -4866,9 +4919,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4893,18 +4946,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bcb3664a..63a5927a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,7 +179,6 @@ tokio = { version = "1", features = [ "macros", "signal", "sync", - "parking_lot", "process", ] } url = "2.2" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5a57ed76..3ff3aac1 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -23,12 +23,11 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false aes = "0.8" bytes = "1" ctr = "0.9" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } http-body-util = "0.1" hyper = { version = "1.6", features = ["http1", "http2"] } hyper-util = { version = "0.1", features = ["client", "http2"] } log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" thiserror = "2" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } +tokio = { version = "1", features = ["macros", "sync"] } diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 781e8a32..6a6379b9 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -8,13 +8,14 @@ use std::{ Arc, OnceLock, atomic::{AtomicBool, AtomicUsize, Ordering}, }, + sync::{Condvar, Mutex}, time::Duration, }; use futures_util::{StreamExt, TryFutureExt, future::IntoStream}; use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE}; use hyper_util::client::legacy::ResponseFuture; -use parking_lot::{Condvar, Mutex}; + use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{Semaphore, mpsc, oneshot}; @@ -27,6 +28,8 @@ use crate::range_set::{Range, RangeSet}; pub type AudioFileResult = Result<(), librespot_core::Error>; +const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned"; + #[derive(Error, Debug)] pub enum AudioFileError { #[error("other end of channel disconnected")] @@ -163,7 +166,10 @@ impl StreamLoaderController { pub fn range_available(&self, range: Range) -> bool { if let Some(ref shared) = self.stream_shared { - let download_status = shared.download_status.lock(); + let download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); range.length <= download_status @@ -214,7 +220,10 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); let download_timeout = AudioFetchParams::get().download_timeout; while range.length @@ -222,11 +231,13 @@ impl StreamLoaderController { .downloaded .contained_length_from_value(range.start) { - if shared + let (new_download_status, wait_result) = shared .cond - .wait_for(&mut download_status, download_timeout) - .timed_out() - { + .wait_timeout(download_status, download_timeout) + .expect(DOWNLOAD_STATUS_POISON_MSG); + + download_status = new_download_status; + if wait_result.timed_out() { return Err(AudioFileError::WaitTimeout.into()); } @@ -558,7 +569,11 @@ impl Read for AudioFileStreaming { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length_to_request)); - let mut download_status = self.shared.download_status.lock(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -571,12 +586,14 @@ impl Read for AudioFileStreaming { let download_timeout = AudioFetchParams::get().download_timeout; while !download_status.downloaded.contains(offset) { - if self + let (new_download_status, wait_result) = self .shared .cond - .wait_for(&mut download_status, download_timeout) - .timed_out() - { + .wait_timeout(download_status, download_timeout) + .expect(DOWNLOAD_STATUS_POISON_MSG); + + download_status = new_download_status; + if wait_result.timed_out() { return Err(io::Error::new( io::ErrorKind::TimedOut, Error::deadline_exceeded(AudioFileError::WaitTimeout), @@ -619,6 +636,7 @@ impl Seek for AudioFileStreaming { .shared .download_status .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG) .downloaded .contains(requested_pos as usize); diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 3d7dfa64..4c894cf6 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -33,6 +33,7 @@ enum ReceivedData { } const ONE_SECOND: Duration = Duration::from_secs(1); +const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned"; async fn receive_data( shared: Arc, @@ -124,7 +125,10 @@ async fn receive_data( if bytes_remaining > 0 { { let missing_range = Range::new(offset, bytes_remaining); - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); } @@ -189,7 +193,11 @@ impl AudioFileFetch { // The iteration that follows spawns streamers fast, without awaiting them, // so holding the lock for the entire scope of this function should be faster // then locking and unlocking multiple times. - let mut download_status = self.shared.download_status.lock(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -227,7 +235,11 @@ impl AudioFileFetch { let mut missing_data = RangeSet::new(); missing_data.add_range(&Range::new(0, self.shared.file_size)); { - let download_status = self.shared.download_status.lock(); + let download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -349,7 +361,11 @@ impl AudioFileFetch { let received_range = Range::new(data.offset, data.data.len()); let full = { - let mut download_status = self.shared.download_status.lock(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.downloaded.add_range(&received_range); self.shared.cond.notify_all(); @@ -415,7 +431,10 @@ pub(super) async fn audio_file_fetch( initial_request.offset + initial_request.length, ); - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.requested.add_range(&requested_range); } @@ -466,7 +485,11 @@ pub(super) async fn audio_file_fetch( if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() { let bytes_pending: usize = { - let download_status = fetch.shared.download_status.lock(); + let download_status = fetch + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status .requested diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 9e7b231b..08d24f66 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -22,12 +22,12 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false librespot-playback = { version = "0.7.1", path = "../playback", default-features = false } librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false } -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } log = "0.4" protobuf = "3.7" rand = { version = "0.9", default-features = false, features = ["small_rng"] } serde_json = "1.0" thiserror = "2" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } -tokio-stream = "0.1" -uuid = { version = "1.18", features = ["v4"] } +tokio = { version = "1", features = ["macros", "sync"] } +tokio-stream = { version = "0.1", default-features = false } +uuid = { version = "1.18", default-features = false, features = ["v4"] } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 087384e9..43702d8a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -2,7 +2,7 @@ use crate::{ LoadContextOptions, LoadRequestOptions, PlayContext, context_resolver::{ContextAction, ContextResolver, ResolveContext}, core::{ - Error, Session, SpotifyId, + Error, Session, SpotifyUri, authentication::Credentials, dealer::{ manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply}, @@ -778,7 +778,7 @@ impl SpircTask { return Ok(()); } PlayerEvent::Unavailable { track_id, .. } => { - self.handle_unavailable(track_id)?; + self.handle_unavailable(&track_id)?; if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? { self.handle_next(None)? } @@ -1499,7 +1499,7 @@ impl SpircTask { } // Mark unavailable tracks so we can skip them later - fn handle_unavailable(&mut self, track_id: SpotifyId) -> Result<(), Error> { + fn handle_unavailable(&mut self, track_id: &SpotifyUri) -> Result<(), Error> { self.connect_state.mark_unavailable(track_id)?; self.handle_preload_next_track(); @@ -1704,7 +1704,7 @@ impl SpircTask { } let current_uri = self.connect_state.current_track(|t| &t.uri); - let id = SpotifyId::from_uri(current_uri)?; + let id = SpotifyUri::from_uri(current_uri)?; self.player.load(id, start_playing, position_ms); self.connect_state diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 7f0fc640..e2b78720 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -1,5 +1,5 @@ use crate::{ - core::{Error, SpotifyId}, + core::{Error, SpotifyId, SpotifyUri}, protocol::{ context::Context, context_page::ContextPage, @@ -449,8 +449,10 @@ impl ConnectState { (Some(uri), _) if uri.contains(['?', '%']) => { Err(StateError::InvalidTrackUri(Some(uri.clone())))? } - (Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?, - (_, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?, + (Some(uri), _) if !uri.is_empty() => SpotifyUri::from_uri(uri)?, + (_, Some(gid)) if !gid.is_empty() => SpotifyUri::Track { + id: SpotifyId::from_raw(gid)?, + }, _ => Err(StateError::InvalidTrackUri(None))?, }; diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 94bde92b..8619035c 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -1,5 +1,5 @@ use crate::{ - core::{Error, SpotifyId}, + core::{Error, SpotifyUri}, protocol::player::ProvidedTrack, state::{ ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError, @@ -352,14 +352,14 @@ impl<'ct> ConnectState { Ok(()) } - pub fn preview_next_track(&mut self) -> Option { + pub fn preview_next_track(&mut self) -> Option { let next = if self.repeat_track() { self.current_track(|t| &t.uri) } else { &self.next_tracks().first()?.uri }; - SpotifyId::from_uri(next).ok() + SpotifyUri::from_uri(next).ok() } pub fn has_next_tracks(&self, min: Option) -> bool { @@ -381,7 +381,7 @@ impl<'ct> ConnectState { prev } - pub fn mark_unavailable(&mut self, id: SpotifyId) -> Result<(), Error> { + pub fn mark_unavailable(&mut self, id: &SpotifyUri) -> Result<(), Error> { let uri = id.to_uri()?; debug!("marking {uri} as unavailable"); diff --git a/contrib/Dockerfile b/contrib/Dockerfile index cae7a6d7..9b3a5a81 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -8,10 +8,10 @@ # The compiled binaries will be located in /tmp/librespot-build # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" FROM debian:bookworm diff --git a/contrib/cross-compile-armv6hf/docker-build.sh b/contrib/cross-compile-armv6hf/docker-build.sh index 76158e44..08386186 100755 --- a/contrib/cross-compile-armv6hf/docker-build.sh +++ b/contrib/cross-compile-armv6hf/docker-build.sh @@ -14,4 +14,4 @@ PI1_LIB_DIRS=( export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}" export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$PI1_TOOLS_SYSROOT_DIR -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" diff --git a/contrib/docker-build.sh b/contrib/docker-build.sh index 50b6b3e1..84131e07 100755 --- a/contrib/docker-build.sh +++ b/contrib/docker-build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eux -cargo build --release --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" diff --git a/core/Cargo.toml b/core/Cargo.toml index 4f5e79cc..f91a1b38 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -51,16 +51,12 @@ data-encoding = "2.9" flate2 = "1.1" form_urlencoded = "1.2" futures-core = "0.3" -futures-util = { version = "0.3", features = [ +futures-util = { version = "0.3", default-features = false, features = [ "alloc", "bilock", - "sink", "unstable", ] } -governor = { version = "0.10", default-features = false, features = [ - "std", - "jitter", -] } +governor = { version = "0.10", default-features = false, features = ["std"] } hmac = "0.12" httparse = "1.10" http = "1.3" @@ -84,14 +80,13 @@ num-bigint = "0.4" num-derive = "0.4" num-integer = "0.1" num-traits = "0.2" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pin-project-lite = "0.2" priority-queue = "2.5" protobuf = "3.7" protobuf-json-mapping = "3.7" quick-xml = { version = "0.38", features = ["serialize"] } -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } rsa = "0.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -104,23 +99,22 @@ tokio = { version = "1", features = [ "io-util", "macros", "net", - "parking_lot", "rt", "sync", "time", ] } -tokio-stream = "0.1" +tokio-stream = { version = "0.1", default-features = false } tokio-tungstenite = { version = "0.27", default-features = false } -tokio-util = { version = "0.7", features = ["codec"] } +tokio-util = { version = "0.7", default-features = false } url = "2" uuid = { version = "1", default-features = false, features = ["v4"] } [build-dependencies] -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } rand_distr = "0.5" vergen-gitcl = { version = "1.0", default-features = false, features = [ "build", ] } [dev-dependencies] -tokio = { version = "1", features = ["macros", "parking_lot"] } +tokio = { version = "1", features = ["macros"] } diff --git a/core/src/cache.rs b/core/src/cache.rs index 2d2ef53d..15e35d21 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -4,16 +4,17 @@ use std::{ fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Mutex}, time::SystemTime, }; -use parking_lot::Mutex; use priority_queue::PriorityQueue; use thiserror::Error; use crate::{Error, FileId, authentication::Credentials, error::ErrorKind}; +const CACHE_LIMITER_POISON_MSG: &str = "cache limiter mutex should not be poisoned"; + #[derive(Debug, Error)] pub enum CacheError { #[error("audio cache location is not configured")] @@ -189,15 +190,24 @@ impl FsSizeLimiter { } fn add(&self, file: &Path, size: u64) { - self.limiter.lock().add(file, size, SystemTime::now()) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .add(file, size, SystemTime::now()) } fn touch(&self, file: &Path) -> bool { - self.limiter.lock().update(file, SystemTime::now()) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .update(file, SystemTime::now()) } fn remove(&self, file: &Path) -> bool { - self.limiter.lock().remove(file) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .remove(file) } fn prune_internal Option>(mut pop: F) -> Result<(), Error> { @@ -232,7 +242,7 @@ impl FsSizeLimiter { } fn prune(&self) -> Result<(), Error> { - Self::prune_internal(|| self.limiter.lock().pop()) + Self::prune_internal(|| self.limiter.lock().expect(CACHE_LIMITER_POISON_MSG).pop()) } fn new(path: &Path, limit: u64) -> Result { diff --git a/core/src/component.rs b/core/src/component.rs index ebe42e8d..75387ae5 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -1,20 +1,23 @@ +pub(crate) const COMPONENT_POISON_MSG: &str = "component mutex should not be poisoned"; + macro_rules! component { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { #[derive(Clone)] - pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>); + pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>); impl $name { #[allow(dead_code)] pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { debug!(target:"librespot::component", "new {}", stringify!($name)); - $name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner { + $name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { $($key : $value,)* })))) } #[allow(dead_code)] fn lock R, R>(&self, f: F) -> R { - let mut inner = (self.0).1.lock(); + let mut inner = (self.0).1.lock() + .expect($crate::component::COMPONENT_POISON_MSG); f(&mut inner) } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 4f738403..63ee6e72 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -6,7 +6,7 @@ use std::{ iter, pin::Pin, sync::{ - Arc, + Arc, Mutex, atomic::{self, AtomicBool}, }, task::Poll, @@ -15,7 +15,6 @@ use std::{ use futures_core::{Future, Stream}; use futures_util::{SinkExt, StreamExt, future::join_all}; -use parking_lot::Mutex; use thiserror::Error; use tokio::{ select, @@ -57,6 +56,11 @@ const PING_TIMEOUT: Duration = Duration::from_secs(3); const RECONNECT_INTERVAL: Duration = Duration::from_secs(10); +const DEALER_REQUEST_HANDLERS_POISON_MSG: &str = + "dealer request handlers mutex should not be poisoned"; +const DEALER_MESSAGE_HANDLERS_POISON_MSG: &str = + "dealer message handlers mutex should not be poisoned"; + struct Response { pub success: bool, } @@ -350,6 +354,7 @@ impl DealerShared { if self .message_handlers .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG) .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()) { return; @@ -387,7 +392,10 @@ impl DealerShared { return; }; - let handler_map = self.request_handlers.lock(); + let handler_map = self + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG); if let Some(handler) = handler_map.get(split) { handler.handle_request(payload_request, responder); @@ -425,21 +433,51 @@ impl Dealer { where H: RequestHandler, { - add_handler(&mut self.shared.request_handlers.lock(), uri, handler) + add_handler( + &mut self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + uri, + handler, + ) } pub fn remove_handler(&self, uri: &str) -> Option> { - remove_handler(&mut self.shared.request_handlers.lock(), uri) + remove_handler( + &mut self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + uri, + ) } pub fn subscribe(&self, uris: &[&str]) -> Result { - subscribe(&mut self.shared.message_handlers.lock(), uris) + subscribe( + &mut self + .shared + .message_handlers + .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG), + uris, + ) } pub fn handles(&self, uri: &str) -> bool { handles( - &self.shared.request_handlers.lock(), - &self.shared.message_handlers.lock(), + &self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + &self + .shared + .message_handlers + .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG), uri, ) } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 9d5d54fc..bbd63838 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, sync::OnceLock, time::{Duration, Instant}, }; @@ -7,7 +6,8 @@ use std::{ use bytes::Bytes; use futures_util::{FutureExt, future::IntoStream}; use governor::{ - Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, + Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, + state::keyed::DefaultKeyedStateStore, }; use http::{Uri, header::HeaderValue}; use http_body_util::{BodyExt, Full}; @@ -18,7 +18,6 @@ use hyper_util::{ rt::TokioExecutor, }; use nonzero_ext::nonzero; -use parking_lot::Mutex; use thiserror::Error; use url::Url; @@ -100,10 +99,8 @@ pub struct HttpClient { proxy_url: Option, hyper_client: OnceLock, - // while the DashMap variant is more performant, our level of concurrency - // is pretty low so we can save pulling in that extra dependency rate_limiter: - RateLimiter>, MonotonicClock, NoOpMiddleware>, + RateLimiter, MonotonicClock, NoOpMiddleware>, } impl HttpClient { diff --git a/core/src/lib.rs b/core/src/lib.rs index f2d6587e..f4ead234 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -32,6 +32,7 @@ mod socket; #[allow(dead_code)] pub mod spclient; pub mod spotify_id; +pub mod spotify_uri; pub mod token; #[doc(hidden)] pub mod util; @@ -42,3 +43,4 @@ pub use error::Error; pub use file_id::FileId; pub use session::Session; pub use spotify_id::SpotifyId; +pub use spotify_uri::SpotifyUri; diff --git a/core/src/session.rs b/core/src/session.rs index 91c41781..333678fd 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -4,8 +4,7 @@ use std::{ io, pin::Pin, process::exit, - sync::OnceLock, - sync::{Arc, Weak}, + sync::{Arc, OnceLock, RwLock, Weak}, task::{Context, Poll}, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -34,7 +33,6 @@ use futures_core::TryStream; use futures_util::StreamExt; use librespot_protocol::authentication::AuthenticationType; use num_traits::FromPrimitive; -use parking_lot::RwLock; use pin_project_lite::pin_project; use quick_xml::events::Event; use thiserror::Error; @@ -45,6 +43,8 @@ use tokio::{ use tokio_stream::wrappers::UnboundedReceiverStream; use uuid::Uuid; +const SESSION_DATA_POISON_MSG: &str = "session data rwlock should not be poisoned"; + #[derive(Debug, Error)] pub enum SessionError { #[error(transparent)] @@ -338,7 +338,11 @@ impl Session { } pub fn time_delta(&self) -> i64 { - self.0.data.read().time_delta + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .time_delta } pub fn spawn(&self, task: T) @@ -388,15 +392,32 @@ impl Session { // you need more fields at once, in which case this can spare multiple `read` // locks. pub fn user_data(&self) -> UserData { - self.0.data.read().user_data.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .clone() } pub fn session_id(&self) -> String { - self.0.data.read().session_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .session_id + .clone() } pub fn set_session_id(&self, session_id: &str) { - session_id.clone_into(&mut self.0.data.write().session_id); + session_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .session_id, + ); } pub fn device_id(&self) -> &str { @@ -404,63 +425,155 @@ impl Session { } pub fn client_id(&self) -> String { - self.0.data.read().client_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_id + .clone() } pub fn set_client_id(&self, client_id: &str) { - client_id.clone_into(&mut self.0.data.write().client_id); + client_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_id, + ); } pub fn client_name(&self) -> String { - self.0.data.read().client_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_name + .clone() } pub fn set_client_name(&self, client_name: &str) { - client_name.clone_into(&mut self.0.data.write().client_name); + client_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_name, + ); } pub fn client_brand_name(&self) -> String { - self.0.data.read().client_brand_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_brand_name + .clone() } pub fn set_client_brand_name(&self, client_brand_name: &str) { - client_brand_name.clone_into(&mut self.0.data.write().client_brand_name); + client_brand_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_brand_name, + ); } pub fn client_model_name(&self) -> String { - self.0.data.read().client_model_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_model_name + .clone() } pub fn set_client_model_name(&self, client_model_name: &str) { - client_model_name.clone_into(&mut self.0.data.write().client_model_name); + client_model_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_model_name, + ); } pub fn connection_id(&self) -> String { - self.0.data.read().connection_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .connection_id + .clone() } pub fn set_connection_id(&self, connection_id: &str) { - connection_id.clone_into(&mut self.0.data.write().connection_id); + connection_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .connection_id, + ); } pub fn username(&self) -> String { - self.0.data.read().user_data.canonical_username.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .canonical_username + .clone() } pub fn set_username(&self, username: &str) { - username.clone_into(&mut self.0.data.write().user_data.canonical_username); + username.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .canonical_username, + ); } pub fn auth_data(&self) -> Vec { - self.0.data.read().auth_data.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .auth_data + .clone() } pub fn set_auth_data(&self, auth_data: &[u8]) { - auth_data.clone_into(&mut self.0.data.write().auth_data); + auth_data.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .auth_data, + ); } pub fn country(&self) -> String { - self.0.data.read().user_data.country.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .country + .clone() } pub fn filter_explicit_content(&self) -> bool { @@ -489,6 +602,7 @@ impl Session { self.0 .data .write() + .expect(SESSION_DATA_POISON_MSG) .user_data .attributes .insert(key.to_owned(), value.to_owned()) @@ -497,11 +611,24 @@ impl Session { pub fn set_user_attributes(&self, attributes: UserAttributes) { Self::check_catalogue(&attributes); - self.0.data.write().user_data.attributes.extend(attributes) + self.0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes + .extend(attributes) } pub fn get_user_attribute(&self, key: &str) -> Option { - self.0.data.read().user_data.attributes.get(key).cloned() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes + .get(key) + .cloned() } fn weak(&self) -> SessionWeak { @@ -510,13 +637,13 @@ impl Session { pub fn shutdown(&self) { debug!("Shutdown: Invalidating session"); - self.0.data.write().invalid = true; + self.0.data.write().expect(SESSION_DATA_POISON_MSG).invalid = true; self.mercury().shutdown(); self.channel().shutdown(); } pub fn is_invalid(&self) -> bool { - self.0.data.read().invalid + self.0.data.read().expect(SESSION_DATA_POISON_MSG).invalid } } @@ -643,7 +770,7 @@ where .unwrap_or(Duration::ZERO) .as_secs() as i64; { - let mut data = session.0.data.write(); + let mut data = session.0.data.write().expect(SESSION_DATA_POISON_MSG); data.time_delta = server_timestamp.saturating_sub(timestamp); } @@ -668,7 +795,13 @@ where Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned())?; info!("Country: {country:?}"); - session.0.data.write().user_data.country = country; + session + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .country = country; Ok(()) } Some(StreamChunkRes) | Some(ChannelError) => session.channel().dispatch(cmd, data), @@ -713,7 +846,13 @@ where trace!("Received product info: {user_attributes:#?}"); Session::check_catalogue(&user_attributes); - session.0.data.write().user_data.attributes = user_attributes; + session + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes = user_attributes; Ok(()) } Some(SecretBlock) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 7d3f39e9..7bc9d0b5 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -5,7 +5,7 @@ use std::{ use crate::config::{OS, os_version}; use crate::{ - Error, FileId, SpotifyId, + Error, FileId, SpotifyId, SpotifyUri, apresolve::SocketAddress, config::SessionConfig, error::ErrorKind, @@ -676,10 +676,10 @@ impl SpClient { .await } - pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult { + pub async fn get_radio_for_track(&self, track_uri: &SpotifyUri) -> SpClientResult { let endpoint = format!( "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", - track_id.to_uri()? + track_uri.to_uri()? ); self.request_as_json(&Method::GET, &endpoint, None, None) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index f7478f54..c627b551 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,70 +1,23 @@ -use std::{fmt, ops::Deref}; +use std::fmt; use thiserror::Error; -use crate::Error; - -use librespot_protocol as protocol; +use crate::{Error, SpotifyUri}; // re-export FileId for historic reasons, when it was part of this mod pub use crate::FileId; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SpotifyItemType { - Album, - Artist, - Episode, - Playlist, - Show, - Track, - Local, - Unknown, -} - -impl From<&str> for SpotifyItemType { - fn from(v: &str) -> Self { - match v { - "album" => Self::Album, - "artist" => Self::Artist, - "episode" => Self::Episode, - "playlist" => Self::Playlist, - "show" => Self::Show, - "track" => Self::Track, - "local" => Self::Local, - _ => Self::Unknown, - } - } -} - -impl From for &str { - fn from(item_type: SpotifyItemType) -> &'static str { - match item_type { - SpotifyItemType::Album => "album", - SpotifyItemType::Artist => "artist", - SpotifyItemType::Episode => "episode", - SpotifyItemType::Playlist => "playlist", - SpotifyItemType::Show => "show", - SpotifyItemType::Track => "track", - SpotifyItemType::Local => "local", - _ => "unknown", - } - } -} - #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, - pub item_type: SpotifyItemType, } #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum SpotifyIdError { #[error("ID cannot be parsed")] InvalidId, - #[error("not a valid Spotify URI")] + #[error("not a valid Spotify ID")] InvalidFormat, - #[error("URI does not belong to Spotify")] - InvalidRoot, } impl From for Error { @@ -74,7 +27,6 @@ impl From for Error { } pub type SpotifyIdResult = Result; -pub type NamedSpotifyIdResult = Result; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; @@ -84,14 +36,6 @@ impl SpotifyId { const SIZE_BASE16: usize = 32; const SIZE_BASE62: usize = 22; - /// Returns whether this `SpotifyId` is for a playable audio item, if known. - pub fn is_playable(&self) -> bool { - matches!( - self.item_type, - SpotifyItemType::Episode | SpotifyItemType::Track - ) - } - /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. /// /// `src` is expected to be 32 bytes long and encoded using valid characters. @@ -114,10 +58,7 @@ impl SpotifyId { dst += p; } - Ok(Self { - id: dst, - item_type: SpotifyItemType::Unknown, - }) + Ok(Self { id: dst }) } /// Parses a base62 encoded [Spotify ID] into a `u128`. @@ -126,7 +67,7 @@ impl SpotifyId { /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base62(src: &str) -> SpotifyIdResult { - if src.len() != 22 { + if src.len() != Self::SIZE_BASE62 { return Err(SpotifyIdError::InvalidId.into()); } let mut dst: u128 = 0; @@ -143,10 +84,7 @@ impl SpotifyId { dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?; } - Ok(Self { - id: dst, - item_type: SpotifyItemType::Unknown, - }) + Ok(Self { id: dst }) } /// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. @@ -156,65 +94,11 @@ impl SpotifyId { match src.try_into() { Ok(dst) => Ok(Self { id: u128::from_be_bytes(dst), - item_type: SpotifyItemType::Unknown, }), Err(_) => Err(SpotifyIdError::InvalidId.into()), } } - /// Parses a [Spotify URI] into a `SpotifyId`. - /// - /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` - /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID. - /// - /// Note that this should not be used for playlists, which have the form of - /// `spotify:playlist:{id}`. - /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids - pub fn from_uri(src: &str) -> SpotifyIdResult { - // Basic: `spotify:{type}:{id}` - // Named: `spotify:user:{user}:{type}:{id}` - // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}` - let mut parts = src.split(':'); - - let scheme = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - - let item_type = { - let next = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - if next == "user" { - let _username = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - parts.next().ok_or(SpotifyIdError::InvalidFormat)? - } else { - next - } - }; - - let id = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - - if scheme != "spotify" { - return Err(SpotifyIdError::InvalidRoot.into()); - } - - let item_type = item_type.into(); - - // Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/ - // TODO: find a way to add this local file ID to SpotifyId. - // One possible solution would be to copy the contents of `id` to a new String field in SpotifyId, - // but then we would need to remove the derived Copy trait, which would be a breaking change. - if item_type == SpotifyItemType::Local { - return Ok(Self { item_type, id: 0 }); - } - - if id.len() != Self::SIZE_BASE62 { - return Err(SpotifyIdError::InvalidId.into()); - } - - Ok(Self { - item_type, - ..Self::from_base62(id)? - }) - } - /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. #[allow(clippy::wrong_self_convention)] @@ -274,124 +158,19 @@ impl SpotifyId { pub fn to_raw(&self) -> [u8; Self::SIZE] { self.id.to_be_bytes() } - - /// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`, - /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded - /// Spotify ID. - /// - /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will - /// be encoded as `unknown`. - /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids - #[allow(clippy::wrong_self_convention)] - pub fn to_uri(&self) -> Result { - // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 - // + unknown size item_type. - let item_type: &str = self.item_type.into(); - let mut dst = String::with_capacity(31 + item_type.len()); - dst.push_str("spotify:"); - dst.push_str(item_type); - dst.push(':'); - let base_62 = self.to_base62()?; - dst.push_str(&base_62); - - Ok(dst) - } } impl fmt::Debug for SpotifyId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SpotifyId") - .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + .field(&self.to_base62().unwrap_or_else(|_| "invalid uri".into())) .finish() } } impl fmt::Display for SpotifyId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) - } -} - -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct NamedSpotifyId { - pub inner_id: SpotifyId, - pub username: String, -} - -impl NamedSpotifyId { - pub fn from_uri(src: &str) -> NamedSpotifyIdResult { - let uri_parts: Vec<&str> = src.split(':').collect(); - - // At minimum, should be `spotify:user:{username}:{type}:{id}` - if uri_parts.len() < 5 { - return Err(SpotifyIdError::InvalidFormat.into()); - } - - if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot.into()); - } - - if uri_parts[1] != "user" { - return Err(SpotifyIdError::InvalidFormat.into()); - } - - Ok(Self { - inner_id: SpotifyId::from_uri(src)?, - username: uri_parts[2].to_owned(), - }) - } - - pub fn to_uri(&self) -> Result { - let item_type: &str = self.inner_id.item_type.into(); - let mut dst = String::with_capacity(37 + self.username.len() + item_type.len()); - dst.push_str("spotify:user:"); - dst.push_str(&self.username); - dst.push(':'); - dst.push_str(item_type); - dst.push(':'); - let base_62 = self.to_base62()?; - dst.push_str(&base_62); - - Ok(dst) - } - - pub fn from_spotify_id(id: SpotifyId, username: &str) -> Self { - Self { - inner_id: id, - username: username.to_owned(), - } - } -} - -impl Deref for NamedSpotifyId { - type Target = SpotifyId; - fn deref(&self) -> &Self::Target { - &self.inner_id - } -} - -impl fmt::Debug for NamedSpotifyId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("NamedSpotifyId") - .field( - &self - .inner_id - .to_uri() - .unwrap_or_else(|_| "invalid id".into()), - ) - .finish() - } -} - -impl fmt::Display for NamedSpotifyId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str( - &self - .inner_id - .to_uri() - .unwrap_or_else(|_| "invalid id".into()), - ) + f.write_str(&self.to_base62().unwrap_or_else(|_| "invalid uri".into())) } } @@ -423,104 +202,20 @@ impl TryFrom<&Vec> for SpotifyId { } } -impl TryFrom<&protocol::metadata::Album> for SpotifyId { +impl TryFrom<&SpotifyUri> for SpotifyId { type Error = crate::Error; - fn try_from(album: &protocol::metadata::Album) -> Result { - Ok(Self { - item_type: SpotifyItemType::Album, - ..Self::from_raw(album.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Artist> for SpotifyId { - type Error = crate::Error; - fn try_from(artist: &protocol::metadata::Artist) -> Result { - Ok(Self { - item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Episode> for SpotifyId { - type Error = crate::Error; - fn try_from(episode: &protocol::metadata::Episode) -> Result { - Ok(Self { - item_type: SpotifyItemType::Episode, - ..Self::from_raw(episode.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Track> for SpotifyId { - type Error = crate::Error; - fn try_from(track: &protocol::metadata::Track) -> Result { - Ok(Self { - item_type: SpotifyItemType::Track, - ..Self::from_raw(track.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Show> for SpotifyId { - type Error = crate::Error; - fn try_from(show: &protocol::metadata::Show) -> Result { - Ok(Self { - item_type: SpotifyItemType::Show, - ..Self::from_raw(show.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { - type Error = crate::Error; - fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { - Ok(Self { - item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.artist_gid())? - }) - } -} - -impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { - type Error = crate::Error; - fn try_from(item: &protocol::playlist4_external::Item) -> Result { - Ok(Self { - item_type: SpotifyItemType::Track, - ..Self::from_uri(item.uri())? - }) - } -} - -// Note that this is the unique revision of an item's metadata on a playlist, -// not the ID of that item or playlist. -impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { - type Error = crate::Error; - fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { - Self::try_from(item.revision()) - } -} - -// Note that this is the unique revision of a playlist, not the ID of that playlist. -impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { - type Error = crate::Error; - fn try_from( - playlist: &protocol::playlist4_external::SelectedListContent, - ) -> Result { - Self::try_from(playlist.revision()) - } -} - -// TODO: check meaning and format of this field in the wild. This might be a FileId, -// which is why we now don't create a separate `Playlist` enum value yet and choose -// to discard any item type. -impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { - type Error = crate::Error; - fn try_from( - picture: &protocol::playlist_annotate3::TranscodedPicture, - ) -> Result { - Self::from_base62(picture.uri()) + fn try_from(value: &SpotifyUri) -> Result { + match value { + SpotifyUri::Album { id } + | SpotifyUri::Artist { id } + | SpotifyUri::Episode { id } + | SpotifyUri::Playlist { id, .. } + | SpotifyUri::Show { id } + | SpotifyUri::Track { id } => Ok(*id), + SpotifyUri::Local { .. } | SpotifyUri::Unknown { .. } => { + Err(SpotifyIdError::InvalidFormat.into()) + } + } } } @@ -541,8 +236,6 @@ mod tests { struct ConversionCase { id: u128, - kind: SpotifyItemType, - uri: &'static str, base16: &'static str, base62: &'static str, raw: &'static [u8], @@ -551,8 +244,6 @@ mod tests { static CONV_VALID: [ConversionCase; 5] = [ ConversionCase { id: 238762092608182713602505436543891614649, - kind: SpotifyItemType::Track, - uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base62: "5sWHDYs0csV6RS48xBl0tH", raw: &[ @@ -561,8 +252,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Track, - uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -571,8 +260,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Episode, - uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -581,8 +268,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Show, - uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -591,8 +276,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Local, - uri: "spotify:local:0000000000000000000000", base16: "00000000000000000000000000000000", base62: "0000000000000000000000", raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -602,9 +285,6 @@ mod tests { static CONV_INVALID: [ConversionCase; 5] = [ ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Invalid ID in the URI. - uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", raw: &[ @@ -614,9 +294,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Missing colon between ID and type. - uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", raw: &[ @@ -626,9 +303,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Uri too short - uri: "spotify:azb:aRS48xBl0tH", // too long, should return error but not panic overflow base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // too long, should return error but not panic overflow @@ -640,9 +314,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Uri too short - uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", // too short to encode a 128 bits int base62: "aa", @@ -653,8 +324,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - uri: "cleary invalid uri", base16: "--------------------", // too high of a value, this would need a 132 bits int base62: "ZZZZZZZZZZZZZZZZZZZZZZ", @@ -679,10 +348,7 @@ mod tests { #[test] fn to_base62() { for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; + let id = SpotifyId { id: c.id }; assert_eq!(id.to_base62().unwrap(), c.base62); } @@ -702,60 +368,12 @@ mod tests { #[test] fn to_base16() { for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; + let id = SpotifyId { id: c.id }; assert_eq!(id.to_base16().unwrap(), c.base16); } } - #[test] - fn from_uri() { - for c in &CONV_VALID { - let actual = SpotifyId::from_uri(c.uri).unwrap(); - - assert_eq!(actual.id, c.id); - assert_eq!(actual.item_type, c.kind); - } - - for c in &CONV_INVALID { - assert!(SpotifyId::from_uri(c.uri).is_err()); - } - } - - #[test] - fn from_local_uri() { - let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap(); - - assert_eq!(actual.id, 0); - assert_eq!(actual.item_type, SpotifyItemType::Local); - } - - #[test] - fn from_named_uri() { - let actual = - NamedSpotifyId::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI") - .unwrap(); - - assert_eq!(actual.id, 136159921382084734723401526672209703396); - assert_eq!(actual.item_type, SpotifyItemType::Playlist); - assert_eq!(actual.username, "spotify"); - } - - #[test] - fn to_uri() { - for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; - - assert_eq!(id.to_uri().unwrap(), c.uri); - } - } - #[test] fn from_raw() { for c in &CONV_VALID { diff --git a/core/src/spotify_uri.rs b/core/src/spotify_uri.rs new file mode 100644 index 00000000..647ec652 --- /dev/null +++ b/core/src/spotify_uri.rs @@ -0,0 +1,583 @@ +use crate::{Error, SpotifyId}; +use std::{borrow::Cow, fmt}; +use thiserror::Error; + +use librespot_protocol as protocol; + +const SPOTIFY_ITEM_TYPE_ALBUM: &str = "album"; +const SPOTIFY_ITEM_TYPE_ARTIST: &str = "artist"; +const SPOTIFY_ITEM_TYPE_EPISODE: &str = "episode"; +const SPOTIFY_ITEM_TYPE_PLAYLIST: &str = "playlist"; +const SPOTIFY_ITEM_TYPE_SHOW: &str = "show"; +const SPOTIFY_ITEM_TYPE_TRACK: &str = "track"; +const SPOTIFY_ITEM_TYPE_LOCAL: &str = "local"; +const SPOTIFY_ITEM_TYPE_UNKNOWN: &str = "unknown"; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +pub enum SpotifyUriError { + #[error("not a valid Spotify URI")] + InvalidFormat, + #[error("URI does not belong to Spotify")] + InvalidRoot, +} + +impl From for Error { + fn from(err: SpotifyUriError) -> Self { + Error::invalid_argument(err) + } +} + +pub type SpotifyUriResult = Result; + +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum SpotifyUri { + Album { + id: SpotifyId, + }, + Artist { + id: SpotifyId, + }, + Episode { + id: SpotifyId, + }, + Playlist { + user: Option, + id: SpotifyId, + }, + Show { + id: SpotifyId, + }, + Track { + id: SpotifyId, + }, + Local { + artist: String, + album_title: String, + track_title: String, + duration: std::time::Duration, + }, + Unknown { + kind: Cow<'static, str>, + id: String, + }, +} + +impl SpotifyUri { + /// Returns whether this `SpotifyUri` is for a playable audio item, if known. + pub fn is_playable(&self) -> bool { + matches!(self, SpotifyUri::Episode { .. } | SpotifyUri::Track { .. }) + } + + /// Gets the item type of this URI as a static string + pub fn item_type(&self) -> &'static str { + match &self { + SpotifyUri::Album { .. } => SPOTIFY_ITEM_TYPE_ALBUM, + SpotifyUri::Artist { .. } => SPOTIFY_ITEM_TYPE_ARTIST, + SpotifyUri::Episode { .. } => SPOTIFY_ITEM_TYPE_EPISODE, + SpotifyUri::Playlist { .. } => SPOTIFY_ITEM_TYPE_PLAYLIST, + SpotifyUri::Show { .. } => SPOTIFY_ITEM_TYPE_SHOW, + SpotifyUri::Track { .. } => SPOTIFY_ITEM_TYPE_TRACK, + SpotifyUri::Local { .. } => SPOTIFY_ITEM_TYPE_LOCAL, + SpotifyUri::Unknown { .. } => SPOTIFY_ITEM_TYPE_UNKNOWN, + } + } + + /// Gets the ID of this URI. The resource ID is the component of the URI that identifies + /// the resource after its type label. If `self` is a named ID, the user will be omitted. + pub fn to_id(&self) -> Result { + match &self { + SpotifyUri::Album { id } + | SpotifyUri::Artist { id } + | SpotifyUri::Episode { id } + | SpotifyUri::Playlist { id, .. } + | SpotifyUri::Show { id } + | SpotifyUri::Track { id } => id.to_base62(), + SpotifyUri::Local { + artist, + album_title, + track_title, + duration, + } => { + let duration_secs = duration.as_secs(); + Ok(format!( + "{artist}:{album_title}:{track_title}:{duration_secs}" + )) + } + SpotifyUri::Unknown { id, .. } => Ok(id.clone()), + } + } + + /// Parses a [Spotify URI] into a `SpotifyUri`. + /// + /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` + /// can be arbitrary while `{id}` is in a format that varies based on the `{type}`: + /// + /// - For most item types, a 22-character long, base62 encoded Spotify ID is expected. + /// - For local files, an arbitrary length string with the fields + /// `{artist}:{album_title}:{track_title}:{duration_in_seconds}` is expected. + /// + /// Spotify URI: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids + pub fn from_uri(src: &str) -> SpotifyUriResult { + // Basic: `spotify:{type}:{id}` + // Named: `spotify:user:{user}:{type}:{id}` + // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}` + let mut parts = src.split(':'); + + let scheme = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + + if scheme != "spotify" { + return Err(SpotifyUriError::InvalidRoot.into()); + } + + let mut username: Option = None; + + let item_type = { + let next = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + if next == "user" { + username.replace( + parts + .next() + .ok_or(SpotifyUriError::InvalidFormat)? + .to_owned(), + ); + parts.next().ok_or(SpotifyUriError::InvalidFormat)? + } else { + next + } + }; + + let name = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + match item_type { + SPOTIFY_ITEM_TYPE_ALBUM => Ok(Self::Album { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_ARTIST => Ok(Self::Artist { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_EPISODE => Ok(Self::Episode { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_PLAYLIST => Ok(Self::Playlist { + id: SpotifyId::from_base62(name)?, + user: username, + }), + SPOTIFY_ITEM_TYPE_SHOW => Ok(Self::Show { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_TRACK => Ok(Self::Track { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_LOCAL => Ok(Self::Local { + artist: "unimplemented".to_owned(), + album_title: "unimplemented".to_owned(), + track_title: "unimplemented".to_owned(), + duration: Default::default(), + }), + _ => Ok(Self::Unknown { + kind: item_type.to_owned().into(), + id: name.to_owned(), + }), + } + } + + /// Returns the `SpotifyUri` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`, + /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded + /// Spotify ID. + /// + /// If the `SpotifyUri` has an associated type unrecognized by the library, `{type}` will + /// be encoded as `unknown`. + /// + /// If the `SpotifyUri` is named, it will be returned in the form + /// `spotify:user:{user}:{type}:{id}`. + /// + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids + pub fn to_uri(&self) -> Result { + let item_type = self.item_type(); + let name = self.to_id()?; + + if let SpotifyUri::Playlist { + id, + user: Some(user), + } = self + { + Ok(format!("spotify:user:{user}:{item_type}:{id}")) + } else { + Ok(format!("spotify:{item_type}:{name}")) + } + } + + /// Gets the name of this URI. The resource name is the component of the URI that identifies + /// the resource after its type label. If `self` is a named ID, the user will be omitted. + /// + /// Deprecated: not all IDs can be represented in Base62, so this function has been renamed to + /// [SpotifyUri::to_id], which this implementation forwards to. + #[deprecated(since = "0.8.0", note = "use to_name instead")] + pub fn to_base62(&self) -> Result { + self.to_id() + } +} + +impl fmt::Debug for SpotifyUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("SpotifyUri") + .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + .finish() + } +} + +impl fmt::Display for SpotifyUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + } +} + +impl TryFrom<&protocol::metadata::Album> for SpotifyUri { + type Error = crate::Error; + fn try_from(album: &protocol::metadata::Album) -> Result { + Ok(Self::Album { + id: SpotifyId::from_raw(album.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Artist> for SpotifyUri { + type Error = crate::Error; + fn try_from(artist: &protocol::metadata::Artist) -> Result { + Ok(Self::Artist { + id: SpotifyId::from_raw(artist.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Episode> for SpotifyUri { + type Error = crate::Error; + fn try_from(episode: &protocol::metadata::Episode) -> Result { + Ok(Self::Episode { + id: SpotifyId::from_raw(episode.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Track> for SpotifyUri { + type Error = crate::Error; + fn try_from(track: &protocol::metadata::Track) -> Result { + Ok(Self::Track { + id: SpotifyId::from_raw(track.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Show> for SpotifyUri { + type Error = crate::Error; + fn try_from(show: &protocol::metadata::Show) -> Result { + Ok(Self::Show { + id: SpotifyId::from_raw(show.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyUri { + type Error = crate::Error; + fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { + Ok(Self::Artist { + id: SpotifyId::from_raw(artist.artist_gid())?, + }) + } +} + +impl TryFrom<&protocol::playlist4_external::Item> for SpotifyUri { + type Error = crate::Error; + fn try_from(item: &protocol::playlist4_external::Item) -> Result { + Self::from_uri(item.uri()) + } +} + +// Note that this is the unique revision of an item's metadata on a playlist, +// not the ID of that item or playlist. +impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyUri { + type Error = crate::Error; + fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { + Ok(Self::Unknown { + kind: "MetaItem".into(), + id: SpotifyId::try_from(item.revision())?.to_base62()?, + }) + } +} + +// Note that this is the unique revision of a playlist, not the ID of that playlist. +impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyUri { + type Error = crate::Error; + fn try_from( + playlist: &protocol::playlist4_external::SelectedListContent, + ) -> Result { + Ok(Self::Unknown { + kind: "SelectedListContent".into(), + id: SpotifyId::try_from(playlist.revision())?.to_base62()?, + }) + } +} + +// TODO: check meaning and format of this field in the wild. This might be a FileId, +// which is why we now don't create a separate `Playlist` enum value yet and choose +// to discard any item type. +impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyUri { + type Error = crate::Error; + fn try_from( + picture: &protocol::playlist_annotate3::TranscodedPicture, + ) -> Result { + Ok(Self::Unknown { + kind: "TranscodedPicture".into(), + id: picture.uri().to_owned(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct ConversionCase { + parsed: SpotifyUri, + uri: &'static str, + base62: &'static str, + } + + static CONV_VALID: [ConversionCase; 4] = [ + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { + id: 238762092608182713602505436543891614649, + }, + }, + uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", + base62: "5sWHDYs0csV6RS48xBl0tH", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ConversionCase { + parsed: SpotifyUri::Episode { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ConversionCase { + parsed: SpotifyUri::Show { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ]; + + static CONV_INVALID: [ConversionCase; 5] = [ + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Invalid ID in the URI. + uri: "spotify:track:5sWHDYs0Bl0tH", + base62: "!!!!!Ys0csV6RS48xBl0tH", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Missing colon between ID and type. + uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", + base62: "....................", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Uri too short + uri: "spotify:track:aRS48xBl0tH", + // too long, should return error but not panic overflow + base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Uri too short + uri: "spotify:track:aRS48xBl0tH", + // too short to encode a 128 bits int + base62: "aa", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + uri: "cleary invalid uri", + // too high of a value, this would need a 132 bits int + base62: "ZZZZZZZZZZZZZZZZZZZZZZ", + }, + ]; + + struct ItemTypeCase { + uri: SpotifyUri, + expected_type: &'static str, + } + + static ITEM_TYPES: [ItemTypeCase; 6] = [ + ItemTypeCase { + uri: SpotifyUri::Album { + id: SpotifyId { id: 0 }, + }, + expected_type: "album", + }, + ItemTypeCase { + uri: SpotifyUri::Artist { + id: SpotifyId { id: 0 }, + }, + expected_type: "artist", + }, + ItemTypeCase { + uri: SpotifyUri::Episode { + id: SpotifyId { id: 0 }, + }, + expected_type: "episode", + }, + ItemTypeCase { + uri: SpotifyUri::Playlist { + user: None, + id: SpotifyId { id: 0 }, + }, + expected_type: "playlist", + }, + ItemTypeCase { + uri: SpotifyUri::Show { + id: SpotifyId { id: 0 }, + }, + expected_type: "show", + }, + ItemTypeCase { + uri: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + expected_type: "track", + }, + ]; + + #[test] + fn to_id() { + for c in &CONV_VALID { + assert_eq!(c.parsed.to_id().unwrap(), c.base62); + } + } + + #[test] + fn item_type() { + for i in &ITEM_TYPES { + assert_eq!(i.uri.item_type(), i.expected_type); + } + + // These need to use methods that can't be used in the static context like to_owned() and + // into(). + + let local_file = SpotifyUri::Local { + artist: "".to_owned(), + album_title: "".to_owned(), + track_title: "".to_owned(), + duration: Default::default(), + }; + + assert_eq!(local_file.item_type(), "local"); + + let unknown = SpotifyUri::Unknown { + kind: "not used".into(), + id: "".to_owned(), + }; + + assert_eq!(unknown.item_type(), "unknown"); + } + + #[test] + fn from_uri() { + for c in &CONV_VALID { + let actual = SpotifyUri::from_uri(c.uri).unwrap(); + + assert_eq!(actual, c.parsed); + } + + for c in &CONV_INVALID { + assert!(SpotifyUri::from_uri(c.uri).is_err()); + } + } + + #[test] + fn from_invalid_type_uri() { + let actual = + SpotifyUri::from_uri("spotify:arbitrarywhatever:5sWHDYs0csV6RS48xBl0tH").unwrap(); + + assert_eq!( + actual, + SpotifyUri::Unknown { + kind: "arbitrarywhatever".into(), + id: "5sWHDYs0csV6RS48xBl0tH".to_owned() + } + ) + } + + #[test] + fn from_local_uri() { + let actual = SpotifyUri::from_uri("spotify:local:xyz:123").unwrap(); + + assert_eq!( + actual, + SpotifyUri::Local { + artist: "unimplemented".to_owned(), + album_title: "unimplemented".to_owned(), + track_title: "unimplemented".to_owned(), + duration: Default::default(), + } + ); + } + + #[test] + fn from_named_uri() { + let actual = + SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap(); + + let SpotifyUri::Playlist { ref user, id } = actual else { + panic!("wrong id type"); + }; + + assert_eq!(*user, Some("spotify".to_owned())); + assert_eq!( + id, + SpotifyId { + id: 136159921382084734723401526672209703396 + }, + ); + } + + #[test] + fn to_uri() { + for c in &CONV_VALID { + assert_eq!(c.parsed.to_uri().unwrap(), c.uri); + } + } + + #[test] + fn to_named_uri() { + let string = "spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI"; + + let actual = + SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap(); + + assert_eq!(actual.to_uri().unwrap(), string); + } +} diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 6285b1a4..2f86d5ac 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -32,7 +32,7 @@ ctr = "0.9" dns-sd = { version = "0.1", optional = true } form_urlencoded = "1.2" futures-core = "0.3" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } hmac = "0.12" http-body-util = "0.1" hyper = { version = "1.6", features = ["http1"] } @@ -41,9 +41,9 @@ hyper-util = { version = "0.1", features = [ "server-graceful", "service", ] } -libmdns = { version = "0.9", optional = true } +libmdns = { version = "0.10", optional = true } log = "0.4" -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } @@ -51,7 +51,7 @@ serde_repr = "0.1" serde_json = "1.0" sha1 = "0.10" thiserror = "2" -tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } +tokio = { version = "1", features = ["sync", "rt"] } zbus = { version = "5", default-features = false, features = [ "tokio", ], optional = true } @@ -59,4 +59,4 @@ zbus = { version = "5", default-features = false, features = [ [dev-dependencies] futures = "0.3" hex = "0.4" -tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index fb0c1f63..e440c67f 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -406,12 +406,7 @@ fn launch_libmdns( } .map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?; - let svc = responder.register( - DNS_SD_SERVICE_NAME.to_owned(), - name.into_owned(), - port, - &TXT_RECORD, - ); + let svc = responder.register(&DNS_SD_SERVICE_NAME, &name, port, &TXT_RECORD); let _ = shutdown_rx.blocking_recv(); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..9a5e794b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,36 @@ +# Examples + +This folder contains examples of how to use the `librespot` library for various purposes. + +## How to run the examples + +In general, to invoke an example, clone down the repo and use `cargo` as follows: + +``` +cargo run --example [filename] +``` + +in which `filename` is the file name of the example, for instance `get_token` or `play`. + +### Acquiring an access token + +Most examples require an access token as the first positional argument. **Note that an access token +gained by the client credentials flow will not work**. `librespot-oauth` provides a utility to +acquire an access token using an OAuth flow, which will be able to run the examples. To invoke this, +run: + +``` +cargo run --package librespot-oauth --example oauth_sync +``` + +A browser window will open and prompt you to authorize with Spotify. Once done, take the +`access_token` property from the dumped object response and proceed to use it in examples. You may +find it convenient to save it in a shell variable like `$ACCESS_TOKEN`. + +Once you have obtained the token you can proceed to run the example. Check each individual +file to see what arguments are expected. As a demonstration, here is how to invoke the `play` +example to play a song -- the second argument is the URI of the track to play. + +``` +cargo run --example play "$ACCESS_TOKEN" 2WUy2Uywcj5cP0IXQagO3z +``` \ No newline at end of file diff --git a/examples/play.rs b/examples/play.rs index fa751cbb..32a86069 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -2,10 +2,8 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, - config::SessionConfig, - session::Session, - spotify_id::{SpotifyId, SpotifyItemType}, + SpotifyUri, authentication::Credentials, config::SessionConfig, session::Session, + spotify_id::SpotifyId, }, playback::{ audio_backend, @@ -28,8 +26,9 @@ async fn main() { } let credentials = Credentials::with_access_token(&args[1]); - let mut track = SpotifyId::from_base62(&args[2]).unwrap(); - track.item_type = SpotifyItemType::Track; + let track = SpotifyUri::Track { + id: SpotifyId::from_base62(&args[2]).unwrap(), + }; let backend = audio_backend::find(None).unwrap(); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 1d6a4266..a1b5cad5 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -2,7 +2,8 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + authentication::Credentials, config::SessionConfig, session::Session, + spotify_uri::SpotifyUri, }, metadata::{Metadata, Playlist, Track}, }; @@ -19,7 +20,7 @@ async fn main() { } let credentials = Credentials::with_access_token(&args[1]); - let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| { + let plist_uri = SpotifyUri::from_uri(&args[2]).unwrap_or_else(|_| { eprintln!( "PLAYLIST should be a playlist URI such as: \ \"spotify:playlist:37i9dQZF1DXec50AjHrNTq\"" diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 9be9364c..b1b26468 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -17,7 +17,7 @@ use crate::{ util::{impl_deref_wrapped, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; use protocol::metadata::Disc as DiscMessage; @@ -25,7 +25,7 @@ pub use protocol::metadata::album::Type as AlbumType; #[derive(Debug, Clone)] pub struct Album { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub artists: Artists, pub album_type: AlbumType, @@ -48,9 +48,9 @@ pub struct Album { } #[derive(Debug, Clone, Default)] -pub struct Albums(pub Vec); +pub struct Albums(pub Vec); -impl_deref_wrapped!(Albums, Vec); +impl_deref_wrapped!(Albums, Vec); #[derive(Debug, Clone)] pub struct Disc { @@ -65,7 +65,7 @@ pub struct Discs(pub Vec); impl_deref_wrapped!(Discs, Vec); impl Album { - pub fn tracks(&self) -> impl Iterator { + pub fn tracks(&self) -> impl Iterator { self.discs.iter().flat_map(|disc| disc.tracks.iter()) } } @@ -74,11 +74,15 @@ impl Album { impl Metadata for Album { type Message = protocol::metadata::Album; - async fn request(session: &Session, album_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, album_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Album { id: album_id } = album_uri else { + return Err(Error::invalid_argument("album_uri")); + }; + session.spclient().get_album_metadata(album_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index e875a985..5f443719 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -16,7 +16,7 @@ use crate::{ util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::metadata::artist_with_role::ArtistRole; @@ -29,7 +29,7 @@ use protocol::metadata::TopTracks as TopTracksMessage; #[derive(Debug, Clone)] pub struct Artist { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub popularity: i32, pub top_tracks: CountryTopTracks, @@ -56,7 +56,7 @@ impl_deref_wrapped!(Artists, Vec); #[derive(Debug, Clone)] pub struct ArtistWithRole { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub role: ArtistRole, } @@ -140,14 +140,14 @@ impl Artist { /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn albums_current(&self) -> impl Iterator { + pub fn albums_current(&self) -> impl Iterator { self.albums.current_releases() } /// Get the full list of singles, not containing duplicate variants of the same singles. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn singles_current(&self) -> impl Iterator { + pub fn singles_current(&self) -> impl Iterator { self.singles.current_releases() } @@ -155,14 +155,14 @@ impl Artist { /// compilations. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn compilations_current(&self) -> impl Iterator { + pub fn compilations_current(&self) -> impl Iterator { self.compilations.current_releases() } /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn appears_on_albums_current(&self) -> impl Iterator { + pub fn appears_on_albums_current(&self) -> impl Iterator { self.appears_on_albums.current_releases() } } @@ -171,11 +171,15 @@ impl Artist { impl Metadata for Artist { type Message = protocol::metadata::Artist; - async fn request(session: &Session, artist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, artist_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Artist { id: artist_id } = artist_uri else { + return Err(Error::invalid_argument("artist_uri")); + }; + session.spclient().get_artist_metadata(artist_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } @@ -249,7 +253,7 @@ impl AlbumGroups { /// Get the contained albums. This will only use the latest release / variant of an album if /// multiple variants are available. This should be used if multiple variants of the same album /// are not explicitely desired. - pub fn current_releases(&self) -> impl Iterator { + pub fn current_releases(&self) -> impl Iterator { self.iter().filter_map(|agrp| agrp.first()) } } diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index d398c8a0..3df63d9e 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -13,9 +13,7 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::{ - Error, Session, SpotifyId, date::Date, session::UserData, spotify_id::SpotifyItemType, -}; +use librespot_core::{Error, Session, SpotifyUri, date::Date, session::UserData}; pub type AudioItemResult = Result; @@ -29,7 +27,7 @@ pub struct CoverImage { #[derive(Debug, Clone)] pub struct AudioItem { - pub track_id: SpotifyId, + pub track_id: SpotifyUri, pub uri: String, pub files: AudioFiles, pub name: String, @@ -60,14 +58,14 @@ pub enum UniqueFields { } impl AudioItem { - pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult { + pub async fn get_file(session: &Session, uri: SpotifyUri) -> AudioItemResult { let image_url = session .get_user_attribute("image-url") .unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}")); - match id.item_type { - SpotifyItemType::Track => { - let track = Track::get(session, &id).await?; + match uri { + SpotifyUri::Track { .. } => { + let track = Track::get(session, &uri).await?; if track.duration <= 0 { return Err(Error::unavailable(MetadataError::InvalidDuration( @@ -79,8 +77,7 @@ impl AudioItem { return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); } - let track_id = track.id; - let uri = track_id.to_uri()?; + let uri_string = uri.to_uri()?; let album = track.album.name; let album_artists = track @@ -123,8 +120,8 @@ impl AudioItem { }; Ok(Self { - track_id, - uri, + track_id: uri, + uri: uri_string, files: track.files, name: track.name, covers, @@ -136,8 +133,8 @@ impl AudioItem { unique_fields, }) } - SpotifyItemType::Episode => { - let episode = Episode::get(session, &id).await?; + SpotifyUri::Episode { .. } => { + let episode = Episode::get(session, &uri).await?; if episode.duration <= 0 { return Err(Error::unavailable(MetadataError::InvalidDuration( @@ -149,8 +146,7 @@ impl AudioItem { return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); } - let track_id = episode.id; - let uri = track_id.to_uri()?; + let uri_string = uri.to_uri()?; let covers = get_covers(episode.covers, image_url); @@ -167,8 +163,8 @@ impl AudioItem { }; Ok(Self { - track_id, - uri, + track_id: uri, + uri: uri_string, files: episode.audio, name: episode.name, covers, diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 4ba0a0da..847e8941 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -15,14 +15,14 @@ use crate::{ video::VideoFiles, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; pub use protocol::metadata::episode::EpisodeType; #[derive(Debug, Clone)] pub struct Episode { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub duration: i32, pub audio: AudioFiles, @@ -49,19 +49,23 @@ pub struct Episode { } #[derive(Debug, Clone, Default)] -pub struct Episodes(pub Vec); +pub struct Episodes(pub Vec); -impl_deref_wrapped!(Episodes, Vec); +impl_deref_wrapped!(Episodes, Vec); #[async_trait] impl Metadata for Episode { type Message = protocol::metadata::Episode; - async fn request(session: &Session, episode_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, episode_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Episode { id: episode_id } = episode_uri else { + return Err(Error::invalid_argument("episode_uri")); + }; + session.spclient().get_episode_metadata(episode_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 30a1f4ed..4d201218 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -5,7 +5,7 @@ use std::{ use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}; -use librespot_core::{FileId, SpotifyId}; +use librespot_core::{FileId, SpotifyUri}; use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; @@ -47,7 +47,7 @@ impl_deref_wrapped!(PictureSizes, Vec); #[derive(Debug, Clone)] pub struct TranscodedPicture { pub target_name: String, - pub uri: SpotifyId, + pub uri: SpotifyUri, } #[derive(Debug, Clone)] diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 1bb5b9f3..b097f98c 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -6,7 +6,7 @@ extern crate async_trait; use protobuf::Message; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; pub mod album; pub mod artist; @@ -44,15 +44,15 @@ pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message + std::fmt::Debug; // Request a protobuf - async fn request(session: &Session, id: &SpotifyId) -> RequestResult; + async fn request(session: &Session, id: &SpotifyUri) -> RequestResult; // Request a metadata struct - async fn get(session: &Session, id: &SpotifyId) -> Result { + async fn get(session: &Session, id: &SpotifyUri) -> Result { let response = Self::request(session, id).await?; let msg = Self::Message::parse_from_bytes(&response)?; trace!("Received metadata: {msg:#?}"); Self::parse(&msg, id) } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result; + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result; } diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index bd703ee2..b11d34da 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -8,8 +8,7 @@ use crate::{ request::{MercuryRequest, RequestResult}, }; -use librespot_core::{Error, Session, SpotifyId}; - +use librespot_core::{Error, Session, SpotifyId, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::playlist_annotate3::AbuseReportState; @@ -26,12 +25,20 @@ pub struct PlaylistAnnotation { impl Metadata for PlaylistAnnotation { type Message = protocol::playlist_annotate3::PlaylistAnnotation; - async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult { let current_user = session.username(); + + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + Self::request_for_user(session, ¤t_user, playlist_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Ok(Self { description: msg.description().to_owned(), picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI? @@ -60,11 +67,18 @@ impl PlaylistAnnotation { async fn get_for_user( session: &Session, username: &str, - playlist_id: &SpotifyId, + playlist_uri: &SpotifyUri, ) -> Result { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; - Self::parse(&msg, playlist_id) + Self::parse(&msg, playlist_uri) } } diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index ce03f0de..1746857b 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -10,7 +10,7 @@ use super::{ permission::Capabilities, }; -use librespot_core::{SpotifyId, date::Date}; +use librespot_core::{SpotifyUri, date::Date}; use librespot_protocol as protocol; use protocol::playlist4_external::Item as PlaylistItemMessage; @@ -19,7 +19,7 @@ use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; #[derive(Debug, Clone)] pub struct PlaylistItem { - pub id: SpotifyId, + pub id: SpotifyUri, pub attributes: PlaylistItemAttributes, } @@ -38,7 +38,7 @@ pub struct PlaylistItemList { #[derive(Debug, Clone)] pub struct PlaylistMetaItem { - pub revision: SpotifyId, + pub revision: SpotifyUri, pub attributes: PlaylistAttributes, pub length: i32, pub timestamp: Date, diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 49ff1188..1052afd8 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -14,12 +14,7 @@ use super::{ permission::Capabilities, }; -use librespot_core::{ - Error, Session, - date::Date, - spotify_id::{NamedSpotifyId, SpotifyId}, -}; - +use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId}; use librespot_protocol as protocol; use protocol::playlist4_external::GeoblockBlockingType as Geoblock; @@ -30,7 +25,7 @@ impl_deref_wrapped!(Geoblocks, Vec); #[derive(Debug, Clone)] pub struct Playlist { - pub id: NamedSpotifyId, + pub id: SpotifyUri, pub revision: Vec, pub length: i32, pub attributes: PlaylistAttributes, @@ -72,7 +67,7 @@ pub struct SelectedListContent { } impl Playlist { - pub fn tracks(&self) -> impl ExactSizeIterator { + pub fn tracks(&self) -> impl ExactSizeIterator { let tracks = self.contents.items.iter().map(|item| &item.id); let length = tracks.len(); @@ -93,17 +88,35 @@ impl Playlist { impl Metadata for Playlist { type Message = protocol::playlist4_external::SelectedListContent; - async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + session.spclient().get_playlist(playlist_id).await } - fn parse(msg: &Self::Message, id: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + // the playlist proto doesn't contain the id so we decorate it let playlist = SelectedListContent::try_from(msg)?; - let id = NamedSpotifyId::from_spotify_id(*id, &playlist.owner_username); + + let new_uri = SpotifyUri::Playlist { + id: *playlist_id, + user: Some(playlist.owner_username), + }; Ok(Self { - id, + id: new_uri, revision: playlist.revision, length: playlist.length, attributes: playlist.attributes, diff --git a/metadata/src/show.rs b/metadata/src/show.rs index b326c652..01a55c2d 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -5,7 +5,7 @@ use crate::{ episode::Episodes, image::Images, restriction::Restrictions, }; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder; @@ -13,7 +13,7 @@ pub use protocol::metadata::show::MediaType as ShowMediaType; #[derive(Debug, Clone)] pub struct Show { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub description: String, pub publisher: String, @@ -27,7 +27,7 @@ pub struct Show { pub media_type: ShowMediaType, pub consumption_order: ShowConsumptionOrder, pub availability: Availabilities, - pub trailer_uri: Option, + pub trailer_uri: Option, pub has_music_and_talk: bool, pub is_audiobook: bool, } @@ -36,11 +36,15 @@ pub struct Show { impl Metadata for Show { type Message = protocol::metadata::Show; - async fn request(session: &Session, show_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, show_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Show { id: show_id } = show_uri else { + return Err(Error::invalid_argument("show_uri")); + }; + session.spclient().get_show_metadata(show_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } @@ -67,7 +71,7 @@ impl TryFrom<&::Message> for Show { .trailer_uri .as_deref() .filter(|s| !s.is_empty()) - .map(SpotifyId::from_uri) + .map(SpotifyUri::from_uri) .transpose()?, has_music_and_talk: show.music_and_talk(), is_audiobook: show.is_audiobook(), diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 78ea5481..5893ca15 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -17,12 +17,12 @@ use crate::{ util::{impl_deref_wrapped, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; #[derive(Debug, Clone)] pub struct Track { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub album: Album, pub artists: Artists, @@ -50,19 +50,23 @@ pub struct Track { } #[derive(Debug, Clone, Default)] -pub struct Tracks(pub Vec); +pub struct Tracks(pub Vec); -impl_deref_wrapped!(Tracks, Vec); +impl_deref_wrapped!(Tracks, Vec); #[async_trait] impl Metadata for Track { type Message = protocol::metadata::Track; - async fn request(session: &Session, track_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, track_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Track { id: track_id } = track_uri else { + return Err(Error::invalid_argument("track_uri")); + }; + session.spclient().get_track_metadata(track_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index dca23297..2001c680 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -51,18 +51,12 @@ librespot-audio = { version = "0.7.1", path = "../audio", default-features = fal librespot-core = { version = "0.7.1", path = "../core", default-features = false } librespot-metadata = { version = "0.7.1", path = "../metadata", default-features = false } -portable-atomic = "1" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } +portable-atomic = "1" shell-words = "1.1" thiserror = "2" -tokio = { version = "1", features = [ - "parking_lot", - "rt", - "rt-multi-thread", - "sync", -] } +tokio = { version = "1", features = ["rt-multi-thread", "sync"] } zerocopy = { version = "0.8", features = ["derive"] } # Backends @@ -97,5 +91,5 @@ symphonia = { version = "0.5", default-features = false, features = [ ogg = { version = "0.9", optional = true } # Dithering -rand = { version = "0.9", features = ["small_rng"] } +rand = { version = "0.9", default-features = false, features = ["small_rng"] } rand_distr = "0.5" diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index b9087e3d..f41d4333 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use gstreamer::{ State, event::{FlushStart, FlushStop}, @@ -8,8 +10,7 @@ use gstreamer as gst; use gstreamer_app as gst_app; use gstreamer_audio as gst_audio; -use parking_lot::Mutex; -use std::sync::Arc; +const GSTREAMER_ASYNC_ERROR_POISON_MSG: &str = "gstreamer async error mutex should not be poisoned"; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; @@ -97,7 +98,9 @@ impl Open for GstreamerSink { gst::MessageView::Eos(_) => { println!("gst signaled end of stream"); - let mut async_error_storage = async_error_clone.lock(); + let mut async_error_storage = async_error_clone + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG); *async_error_storage = Some(String::from("gst signaled end of stream")); } gst::MessageView::Error(err) => { @@ -108,7 +111,9 @@ impl Open for GstreamerSink { err.debug() ); - let mut async_error_storage = async_error_clone.lock(); + let mut async_error_storage = async_error_clone + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG); *async_error_storage = Some(format!( "Error from {:?}: {} ({:?})", err.src().map(|s| s.path_string()), @@ -138,7 +143,10 @@ impl Open for GstreamerSink { impl Sink for GstreamerSink { fn start(&mut self) -> SinkResult<()> { - *self.async_error.lock() = None; + *self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None; self.appsrc.send_event(FlushStop::new(true)); self.bufferpool .set_active(true) @@ -150,7 +158,10 @@ impl Sink for GstreamerSink { } fn stop(&mut self) -> SinkResult<()> { - *self.async_error.lock() = None; + *self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None; self.appsrc.send_event(FlushStart::new()); self.pipeline .set_state(State::Paused) @@ -173,7 +184,11 @@ impl Drop for GstreamerSink { impl SinkAsBytes for GstreamerSink { #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - if let Some(async_error) = &*self.async_error.lock() { + if let Some(async_error) = &*self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) + { return Err(SinkError::OnWrite(async_error.to_string())); } diff --git a/playback/src/player.rs b/playback/src/player.rs index ba2e5a4c..a4a03ca3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -6,6 +6,7 @@ use std::{ mem, pin::Pin, process::exit, + sync::Mutex, sync::{ Arc, atomic::{AtomicUsize, Ordering}, @@ -15,27 +16,25 @@ use std::{ time::{Duration, Instant}, }; -use futures_util::{ - StreamExt, TryFutureExt, future, future::FusedFuture, - stream::futures_unordered::FuturesUnordered, -}; -use parking_lot::Mutex; -use symphonia::core::io::MediaSource; -use tokio::sync::{mpsc, oneshot}; - +#[cfg(feature = "passthrough-decoder")] +use crate::decoder::PassthroughDecoder; use crate::{ audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController}, audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, - core::{Error, Session, SpotifyId, util::SeqGenerator}, + core::{Error, Session, SpotifyId, SpotifyUri, util::SeqGenerator}, decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::VolumeGetter, }; - -#[cfg(feature = "passthrough-decoder")] -use crate::decoder::PassthroughDecoder; +use futures_util::{ + StreamExt, TryFutureExt, future, future::FusedFuture, + stream::futures_unordered::FuturesUnordered, +}; +use librespot_metadata::track::Tracks; +use symphonia::core::io::MediaSource; +use tokio::sync::{mpsc, oneshot}; use crate::SAMPLES_PER_SECOND; @@ -47,6 +46,8 @@ pub const PCM_AT_0DBFS: f64 = 1.0; // otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it. const SPOTIFY_OGG_HEADER_END: u64 = 0xa7; +const LOAD_HANDLES_POISON_MSG: &str = "load handles mutex should not be poisoned"; + pub type PlayerResult = Result<(), Error>; pub struct Player { @@ -94,12 +95,12 @@ static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); enum PlayerCommand { Load { - track_id: SpotifyId, + track_id: SpotifyUri, play: bool, position_ms: u32, }, Preload { - track_id: SpotifyId, + track_id: SpotifyUri, }, Play, Pause, @@ -142,17 +143,17 @@ pub enum PlayerEvent { // Fired when the player is stopped (e.g. by issuing a "stop" command to the player). Stopped { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player is delayed by loading a track. Loading { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player is preloading a track. Preloading { - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player is playing a track. // This event is issued at the start of playback of whenever the position must be communicated @@ -163,31 +164,31 @@ pub enum PlayerEvent { // after a buffer-underrun Playing { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player entered a paused state. Paused { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player thinks it's a good idea to issue a preload command for the next track now. // This event is intended for use within spirc. TimeToPreloadNextTrack { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player reached the end of a track. // This event is intended for use within spirc. Spirc will respond by issuing another command. EndOfTrack { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player was unable to load the requested track. Unavailable { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The mixer volume was set to a new level. VolumeChanged { @@ -195,7 +196,7 @@ pub enum PlayerEvent { }, PositionCorrection { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, /// Requires `PlayerConfig::position_update_interval` to be set to Some. @@ -203,12 +204,12 @@ pub enum PlayerEvent { /// current playback position PositionChanged { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, Seeked { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, TrackChanged { @@ -526,7 +527,7 @@ impl Player { } } - pub fn load(&self, track_id: SpotifyId, start_playing: bool, position_ms: u32) { + pub fn load(&self, track_id: SpotifyUri, start_playing: bool, position_ms: u32) { self.command(PlayerCommand::Load { track_id, play: start_playing, @@ -534,7 +535,7 @@ impl Player { }); } - pub fn preload(&self, track_id: SpotifyId) { + pub fn preload(&self, track_id: SpotifyUri) { self.command(PlayerCommand::Preload { track_id }); } @@ -660,11 +661,11 @@ struct PlayerLoadedTrackData { enum PlayerPreload { None, Loading { - track_id: SpotifyId, + track_id: SpotifyUri, loader: Pin> + Send>>, }, Ready { - track_id: SpotifyId, + track_id: SpotifyUri, loaded_track: Box, }, } @@ -674,13 +675,13 @@ type Decoder = Box; enum PlayerState { Stopped, Loading { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, start_playback: bool, loader: Pin> + Send>>, }, Paused { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, decoder: Decoder, audio_item: AudioItem, @@ -694,7 +695,7 @@ enum PlayerState { is_explicit: bool, }, Playing { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, decoder: Decoder, normalisation_data: NormalisationData, @@ -709,7 +710,7 @@ enum PlayerState { is_explicit: bool, }, EndOfTrack { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, loaded_track: PlayerLoadedTrackData, }, @@ -893,10 +894,12 @@ impl PlayerTrackLoader { None } else if !audio_item.files.is_empty() { Some(audio_item) - } else if let Some(alternatives) = &audio_item.alternatives { - let alternatives: FuturesUnordered<_> = alternatives - .iter() - .map(|alt_id| AudioItem::get_file(&self.session, *alt_id)) + } else if let Some(alternatives) = audio_item.alternatives { + let Tracks(alternatives_vec) = alternatives; // required to make `into_iter` able to move + + let alternatives: FuturesUnordered<_> = alternatives_vec + .into_iter() + .map(|alt_id| AudioItem::get_file(&self.session, alt_id)) .collect(); alternatives @@ -938,16 +941,40 @@ impl PlayerTrackLoader { async fn load_track( &self, - spotify_id: SpotifyId, + track_uri: SpotifyUri, position_ms: u32, ) -> Option { - let audio_item = match AudioItem::get_file(&self.session, spotify_id).await { + match track_uri { + SpotifyUri::Track { .. } | SpotifyUri::Episode { .. } => { + self.load_remote_track(track_uri, position_ms).await + } + _ => { + error!("Cannot handle load of track with URI: <{track_uri}>",); + None + } + } + } + + async fn load_remote_track( + &self, + track_uri: SpotifyUri, + position_ms: u32, + ) -> Option { + let track_id: SpotifyId = match (&track_uri).try_into() { + Ok(id) => id, + Err(_) => { + warn!("<{track_uri}> could not be converted to a base62 ID"); + return None; + } + }; + + let audio_item = match AudioItem::get_file(&self.session, track_uri).await { Ok(audio) => match self.find_available_alternative(audio).await { Some(audio) => audio, None => { warn!( - "<{}> is not available", - spotify_id.to_uri().unwrap_or_default() + "spotify:track:<{}> is not available", + track_id.to_base62().unwrap_or_default() ); return None; } @@ -1033,13 +1060,14 @@ impl PlayerTrackLoader { // Not all audio files are encrypted. If we can't get a key, try loading the track // without decryption. If the file was encrypted after all, the decoder will fail // parsing and bail out, so we should be safe from outputting ear-piercing noise. - let key = match self.session.audio_key().request(spotify_id, file_id).await { + let key = match self.session.audio_key().request(track_id, file_id).await { Ok(key) => Some(key), Err(e) => { warn!("Unable to load key, continuing without decryption: {e}"); None } }; + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format); @@ -1195,13 +1223,15 @@ impl Future for PlayerInternal { // Handle loading of a new track to play if let PlayerState::Loading { ref mut loader, - track_id, + ref track_id, start_playback, play_request_id, } = self.state { // The loader may be terminated if we are trying to load the same track // as before, and that track failed to open before. + let track_id = track_id.clone(); + if !loader.as_mut().is_terminated() { match loader.as_mut().poll(cx) { Poll::Ready(Ok(loaded_track)) => { @@ -1233,12 +1263,15 @@ impl Future for PlayerInternal { // handle pending preload requests. if let PlayerPreload::Loading { ref mut loader, - track_id, + ref track_id, } = self.preload { + let track_id = track_id.clone(); match loader.as_mut().poll(cx) { Poll::Ready(Ok(loaded_track)) => { - self.send_event(PlayerEvent::Preloading { track_id }); + self.send_event(PlayerEvent::Preloading { + track_id: track_id.clone(), + }); self.preload = PlayerPreload::Ready { track_id, loaded_track: Box::new(loaded_track), @@ -1269,7 +1302,7 @@ impl Future for PlayerInternal { self.ensure_sink_running(); if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, ref mut decoder, normalisation_factor, @@ -1278,6 +1311,7 @@ impl Future for PlayerInternal { .. } = self.state { + let track_id = track_id.clone(); match decoder.next_packet() { Ok(result) => { if let Some((ref packet_position, ref packet)) = result { @@ -1338,7 +1372,7 @@ impl Future for PlayerInternal { now.checked_sub(new_stream_position); self.send_event(PlayerEvent::PositionCorrection { play_request_id, - track_id, + track_id: track_id.clone(), position_ms: new_stream_position_ms, }); } @@ -1391,7 +1425,7 @@ impl Future for PlayerInternal { } if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, duration_ms, stream_position_ms, @@ -1400,7 +1434,7 @@ impl Future for PlayerInternal { .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, duration_ms, stream_position_ms, @@ -1409,6 +1443,8 @@ impl Future for PlayerInternal { .. } = self.state { + let track_id = track_id.clone(); + if (!*suggested_to_preload_next_track) && ((duration_ms as i64 - stream_position_ms as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) @@ -1482,25 +1518,27 @@ impl PlayerInternal { fn handle_player_stop(&mut self) { match self.state { PlayerState::Playing { - track_id, + ref track_id, play_request_id, .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, .. } | PlayerState::EndOfTrack { - track_id, + ref track_id, play_request_id, .. } | PlayerState::Loading { - track_id, + ref track_id, play_request_id, .. } => { + let track_id = track_id.clone(); + self.ensure_sink_stopped(false); self.send_event(PlayerEvent::Stopped { track_id, @@ -1519,11 +1557,13 @@ impl PlayerInternal { fn handle_play(&mut self) { match self.state { PlayerState::Paused { - track_id, + ref track_id, play_request_id, stream_position_ms, .. } => { + let track_id = track_id.clone(); + self.state.paused_to_playing(); self.send_event(PlayerEvent::Playing { track_id, @@ -1546,11 +1586,13 @@ impl PlayerInternal { match self.state { PlayerState::Paused { .. } => self.ensure_sink_stopped(false), PlayerState::Playing { - track_id, + ref track_id, play_request_id, stream_position_ms, .. } => { + let track_id = track_id.clone(); + self.state.playing_to_paused(); self.ensure_sink_stopped(false); @@ -1681,13 +1723,13 @@ impl PlayerInternal { None => { self.state.playing_to_end_of_track(); if let PlayerState::EndOfTrack { - track_id, + ref track_id, play_request_id, .. } = self.state { self.send_event(PlayerEvent::EndOfTrack { - track_id, + track_id: track_id.clone(), play_request_id, }) } else { @@ -1700,7 +1742,7 @@ impl PlayerInternal { fn start_playback( &mut self, - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, loaded_track: PlayerLoadedTrackData, start_playback: bool, @@ -1725,7 +1767,7 @@ impl PlayerInternal { if start_playback { self.ensure_sink_running(); self.send_event(PlayerEvent::Playing { - track_id, + track_id: track_id.clone(), play_request_id, position_ms, }); @@ -1750,7 +1792,7 @@ impl PlayerInternal { self.ensure_sink_stopped(false); self.state = PlayerState::Paused { - track_id, + track_id: track_id.clone(), play_request_id, decoder: loaded_track.decoder, audio_item: loaded_track.audio_item, @@ -1774,7 +1816,7 @@ impl PlayerInternal { fn handle_command_load( &mut self, - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id_option: Option, play: bool, position_ms: u32, @@ -1803,9 +1845,9 @@ impl PlayerInternal { if let PlayerState::EndOfTrack { track_id: previous_track_id, .. - } = self.state + } = &self.state { - if previous_track_id == track_id { + if *previous_track_id == track_id { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, _ => { @@ -1834,19 +1876,19 @@ impl PlayerInternal { // Check if we are already playing the track. If so, just do a seek and update our info. if let PlayerState::Playing { - track_id: current_track_id, + track_id: ref current_track_id, ref mut stream_position_ms, ref mut decoder, .. } | PlayerState::Paused { - track_id: current_track_id, + track_id: ref current_track_id, ref mut stream_position_ms, ref mut decoder, .. } = self.state { - if current_track_id == track_id { + if *current_track_id == track_id { // we can use the current decoder. Ensure it's at the correct position. if position_ms != *stream_position_ms { // This may be blocking. @@ -1915,9 +1957,9 @@ impl PlayerInternal { if let PlayerPreload::Ready { track_id: loaded_track_id, .. - } = self.preload + } = &self.preload { - if track_id == loaded_track_id { + if track_id == *loaded_track_id { let preload = std::mem::replace(&mut self.preload, PlayerPreload::None); if let PlayerPreload::Ready { track_id, @@ -1940,7 +1982,7 @@ impl PlayerInternal { } self.send_event(PlayerEvent::Loading { - track_id, + track_id: track_id.clone(), play_request_id, position_ms, }); @@ -1949,9 +1991,9 @@ impl PlayerInternal { let loader = if let PlayerPreload::Loading { track_id: loaded_track_id, .. - } = self.preload + } = &self.preload { - if (track_id == loaded_track_id) && (position_ms == 0) { + if (track_id == *loaded_track_id) && (position_ms == 0) { let mut preload = PlayerPreload::None; std::mem::swap(&mut preload, &mut self.preload); if let PlayerPreload::Loading { loader, .. } = preload { @@ -1969,7 +2011,8 @@ impl PlayerInternal { self.preload = PlayerPreload::None; // If we don't have a loader yet, create one from scratch. - let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms))); + let loader = + loader.unwrap_or_else(|| Box::pin(self.load_track(track_id.clone(), position_ms))); // Set ourselves to a loading state. self.state = PlayerState::Loading { @@ -1982,7 +2025,7 @@ impl PlayerInternal { Ok(()) } - fn handle_command_preload(&mut self, track_id: SpotifyId) { + fn handle_command_preload(&mut self, track_id: SpotifyUri) { debug!("Preloading track"); let mut preload_track = true; // check whether the track is already loaded somewhere or being loaded. @@ -1993,9 +2036,9 @@ impl PlayerInternal { | PlayerPreload::Ready { track_id: currently_loading, .. - } = self.preload + } = &self.preload { - if currently_loading == track_id { + if *currently_loading == track_id { // we're already preloading the requested track. preload_track = false; } else { @@ -2015,9 +2058,9 @@ impl PlayerInternal { | PlayerState::EndOfTrack { track_id: current_track_id, .. - } = self.state + } = &self.state { - if current_track_id == track_id { + if *current_track_id == track_id { // we already have the requested track loaded. preload_track = false; } @@ -2025,7 +2068,7 @@ impl PlayerInternal { // schedule the preload of the current track if desired. if preload_track { - let loader = self.load_track(track_id, 0); + let loader = self.load_track(track_id.clone(), 0); self.preload = PlayerPreload::Loading { track_id, loader: Box::pin(loader), @@ -2039,14 +2082,14 @@ impl PlayerInternal { // that. In this case just restart the loading process but // with the requested position. if let PlayerState::Loading { - track_id, + ref track_id, play_request_id, start_playback, .. } = self.state { return self.handle_command_load( - track_id, + track_id.clone(), Some(play_request_id), start_playback, position_ms, @@ -2058,13 +2101,13 @@ impl PlayerInternal { Ok(new_position_ms) => { if let PlayerState::Playing { ref mut stream_position_ms, - track_id, + ref track_id, play_request_id, .. } | PlayerState::Paused { ref mut stream_position_ms, - track_id, + ref track_id, play_request_id, .. } = self.state @@ -2073,7 +2116,7 @@ impl PlayerInternal { self.send_event(PlayerEvent::Seeked { play_request_id, - track_id, + track_id: track_id.clone(), position_ms: new_position_ms, }); } @@ -2177,18 +2220,20 @@ impl PlayerInternal { if filter { if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, is_explicit, .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, is_explicit, .. } = self.state { + let track_id = track_id.clone(); + if is_explicit { warn!( "Currently loaded track is explicit, which client setting forbids -- skipping to next track." @@ -2213,7 +2258,7 @@ impl PlayerInternal { fn load_track( &mut self, - spotify_id: SpotifyId, + spotify_uri: SpotifyUri, position_ms: u32, ) -> impl FusedFuture> + Send + 'static { // This method creates a future that returns the loaded stream and associated info. @@ -2231,17 +2276,18 @@ impl PlayerInternal { let load_handles_clone = self.load_handles.clone(); let handle = tokio::runtime::Handle::current(); + let load_handle = thread::spawn(move || { - let data = handle.block_on(loader.load_track(spotify_id, position_ms)); + let data = handle.block_on(loader.load_track(spotify_uri, position_ms)); if let Some(data) = data { let _ = result_tx.send(data); } - let mut load_handles = load_handles_clone.lock(); + let mut load_handles = load_handles_clone.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles.remove(&thread::current().id()); }); - let mut load_handles = self.load_handles.lock(); + let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles.insert(load_handle.thread().id(), load_handle); result_rx.map_err(|_| ()) @@ -2276,7 +2322,7 @@ impl Drop for PlayerInternal { let handles: Vec> = { // waiting for the thread while holding the mutex would result in a deadlock - let mut load_handles = self.load_handles.lock(); + let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles .drain() @@ -2376,7 +2422,7 @@ impl fmt::Debug for PlayerCommand { impl fmt::Debug for PlayerState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use PlayerState::*; - match *self { + match self { Stopped => f.debug_struct("Stopped").finish(), Loading { track_id, diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..367cc1fc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +components = ["rustfmt", "clippy"] diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 36695c99..51495932 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -28,7 +28,7 @@ impl EventHandler { env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); } PlayerEvent::TrackChanged { audio_item } => { - match audio_item.track_id.to_base62() { + match audio_item.track_id.to_id() { Err(e) => { warn!("PlayerEvent::TrackChanged: Invalid track id: {e}") } @@ -104,7 +104,7 @@ impl EventHandler { } } } - PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + PlayerEvent::Stopped { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "stopped".to_string()); @@ -115,7 +115,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "playing".to_string()); @@ -127,7 +127,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "paused".to_string()); @@ -135,26 +135,24 @@ impl EventHandler { env_vars.insert("POSITION_MS", position_ms.to_string()); } }, - PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { + PlayerEvent::Loading { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "loading".to_string()); env_vars.insert("TRACK_ID", id); } }, - PlayerEvent::Preloading { track_id, .. } => { - match track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::Preloading: Invalid track id: {e}") - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "preloading".to_string()); - env_vars.insert("TRACK_ID", id); - } + PlayerEvent::Preloading { track_id, .. } => match track_id.to_id() { + Err(e) => { + warn!("PlayerEvent::Preloading: Invalid track id: {e}") } - } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { - match track_id.to_base62() { + match track_id.to_id() { Err(e) => warn!( "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}" ), @@ -164,19 +162,16 @@ impl EventHandler { } } } - PlayerEvent::EndOfTrack { track_id, .. } => { - match track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}") - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); - env_vars.insert("TRACK_ID", id); - } + PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_id() { + Err(e) => { + warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}") } - } - PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() - { + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Unavailable { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); @@ -191,7 +186,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "seeked".to_string()); @@ -203,7 +198,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => { warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}") }