mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Discovery: Refactor and add Avahi DBus backend (#1347)
* discovery: use opaque error type for DnsSdError This helps to decouple discovery and core by not leaking implementation details of the zeroconf backend into Error conversion impls in core. * discovery: map all MDNS/DNS-SD errors to DiscoveryError::DnsSdError previously, libmdns errors would use a generic conversion from std::io::Error to core::Error * discovery: use an opaque type for the handle to the DNS-SD service * discovery: make features additive i.e. add with-libmdns instead of using not(with-dns-sd). The logic is such that enabling with-dns-sd in addition to the default with-libmdns will still end up using dns-sd, as before. If only with-libmdns is enabled, that will be the default. If none of the features is enabled, attempting to build a `Discovery` will yield an error. * discovery: add --zeroconf-backend CLI flag * discovery: Add minimal Avahi zeroconf backend * bump MSRV to 1.75 required by zbus >= 4 * discovery: ensure that server and dns-sd backend shutdown gracefully Previously, on drop the the shutdown_tx/close_tx, it wasn't guaranteed the corresponding tasks would continue to be polled until they actually completed their shutdown. Since dns_sd::Service is not Send and non-async, and because libmdns is non-async, put them on their own threads. * discovery: use a shared channel for server and zeroconf status messages * discovery: add Avahi reconnection logic This deals gracefully with the case where the Avahi daemon is restarted or not running initially. * discovery: allow running when compiled without zeroconf backend... ...but exit with an error if there's no way to authenticate * better error messages for invalid options with no short flag
This commit is contained in:
parent
d2324ddd1b
commit
94d174c33d
15 changed files with 1156 additions and 129 deletions
|
@ -1,6 +1,6 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
ARG debian_version=slim-bookworm
|
||||
ARG rust_version=1.74.0
|
||||
ARG rust_version=1.75.0
|
||||
FROM rust:${rust_version}-${debian_version}
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
|
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
@ -109,7 +109,7 @@ jobs:
|
|||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
toolchain:
|
||||
- "1.74" # MSRV (Minimum supported rust version)
|
||||
- "1.75" # MSRV (Minimum supported rust version)
|
||||
- stable
|
||||
experimental: [false]
|
||||
# Ignore failures in beta
|
||||
|
@ -164,7 +164,7 @@ jobs:
|
|||
matrix:
|
||||
os: [windows-latest]
|
||||
toolchain:
|
||||
- "1.74" # MSRV (Minimum supported rust version)
|
||||
- "1.75" # MSRV (Minimum supported rust version)
|
||||
- stable
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
@ -215,7 +215,7 @@ jobs:
|
|||
- aarch64-unknown-linux-gnu
|
||||
- riscv64gc-unknown-linux-gnu
|
||||
toolchain:
|
||||
- "1.74" # MSRV (Minimum supported rust version)
|
||||
- "1.75" # MSRV (Minimum supported rust version)
|
||||
- stable
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
|
@ -10,11 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- [core] The `access_token` for http requests is now acquired by `login5`
|
||||
- [core] MSRV is now 1.75 (breaking)
|
||||
- [discovery] librespot can now be compiled with multiple MDNS/DNS-SD backends
|
||||
(avahi, dns_sd, libmdns) which can be selected using a CLI flag. The defaults
|
||||
are unchanged (breaking).
|
||||
|
||||
### Added
|
||||
|
||||
- [core] Add `login` (mobile) and `auth_token` retrieval via login5
|
||||
- [core] Add `OS` and `os_version` to `config.rs`
|
||||
- [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus.
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
11
COMPILING.md
11
COMPILING.md
|
@ -56,6 +56,17 @@ On Fedora systems:
|
|||
sudo dnf install alsa-lib-devel
|
||||
```
|
||||
|
||||
### Zeroconf library dependencies
|
||||
Depending on the chosen backend, specific development libraries are required.
|
||||
|
||||
*_Note this is an non-exhaustive list, open a PR to add to it!_*
|
||||
|
||||
| Zeroconf backend | Debian/Ubuntu | Fedora | macOS |
|
||||
|--------------------|------------------------------|-----------------------------------|-------------|
|
||||
|avahi | | | |
|
||||
|dns_sd | `libavahi-compat-libdnssd-dev pkg-config` | `avahi-compat-libdns_sd-devel` | |
|
||||
|libmdns (default) | | | |
|
||||
|
||||
### Getting the Source
|
||||
|
||||
The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, it’s a simple case of cloning your fork.
|
||||
|
|
444
Cargo.lock
generated
444
Cargo.lock
generated
|
@ -135,6 +135,114 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.83"
|
||||
|
@ -292,6 +400,19 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
|
@ -358,6 +479,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
|
@ -419,6 +546,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
@ -494,6 +630,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
|
@ -654,6 +796,33 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.2"
|
||||
|
@ -692,6 +861,27 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
|
@ -773,6 +963,19 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
|
@ -1152,6 +1355,12 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
|
@ -1753,7 +1962,6 @@ dependencies = [
|
|||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"dns-sd",
|
||||
"form_urlencoded",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
|
@ -1821,10 +2029,13 @@ dependencies = [
|
|||
"librespot-core",
|
||||
"log",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1931,6 +2142,15 @@ version = "2.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
@ -1958,7 +2178,7 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.52.0",
|
||||
|
@ -2014,6 +2234,19 @@ dependencies = [
|
|||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
|
@ -2253,6 +2486,22 @@ dependencies = [
|
|||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
|
@ -2332,6 +2581,17 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
|
@ -2359,6 +2619,21 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.4.0",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.9.0"
|
||||
|
@ -2442,9 +2717,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "3.6.0"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3018844a02746180074f621e847703737d27d89d7f0721a7a4da317f88b16385"
|
||||
checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"protobuf-support",
|
||||
|
@ -2453,9 +2728,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "protobuf-codegen"
|
||||
version = "3.6.0"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411c15a212b4de05eb8bc989fd066a74c86bd3c04e27d6e86bd7703b806d7734"
|
||||
checksum = "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"once_cell",
|
||||
|
@ -2468,9 +2743,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "protobuf-parse"
|
||||
version = "3.6.0"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06f45f16b522d92336e839b5e40680095a045e36a1e7f742ba682ddc85236772"
|
||||
checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
|
@ -2484,9 +2759,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "protobuf-support"
|
||||
version = "3.6.0"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf96d872914fcda2b66d66ea3fff2be7c66865d31c7bb2790cff32c0e714880"
|
||||
checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
@ -2946,6 +3221,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
|
@ -3079,6 +3365,12 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
@ -3362,6 +3654,7 @@ dependencies = [
|
|||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
|
@ -3495,9 +3788,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
|
@ -3539,6 +3844,17 @@ version = "1.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.17"
|
||||
|
@ -3598,9 +3914,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"rand",
|
||||
|
@ -4148,6 +4464,73 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.52.0",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
@ -4174,3 +4557,40 @@ name = "zeroize"
|
|||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "librespot"
|
||||
version = "0.5.0"
|
||||
rust-version = "1.74"
|
||||
rust-version = "1.75"
|
||||
authors = ["Librespot Org"]
|
||||
license = "MIT"
|
||||
description = "An open source client library for Spotify, with support for Spotify Connect"
|
||||
|
@ -36,6 +36,7 @@ version = "0.5.0"
|
|||
[dependencies.librespot-discovery]
|
||||
path = "discovery"
|
||||
version = "0.5.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.librespot-metadata]
|
||||
path = "metadata"
|
||||
|
@ -62,7 +63,7 @@ log = "0.4"
|
|||
sha1 = "0.10"
|
||||
sysinfo = { version = "0.31.3", default-features = false, features = ["system"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] }
|
||||
tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] }
|
||||
url = "2.2"
|
||||
|
||||
[features]
|
||||
|
@ -75,11 +76,13 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"]
|
|||
sdl-backend = ["librespot-playback/sdl-backend"]
|
||||
gstreamer-backend = ["librespot-playback/gstreamer-backend"]
|
||||
|
||||
with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"]
|
||||
with-avahi = ["librespot-discovery/with-avahi"]
|
||||
with-dns-sd = ["librespot-discovery/with-dns-sd"]
|
||||
with-libmdns = ["librespot-discovery/with-libmdns"]
|
||||
|
||||
passthrough-decoder = ["librespot-playback/passthrough-decoder"]
|
||||
|
||||
default = ["rodio-backend"]
|
||||
default = ["rodio-backend", "with-libmdns"]
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "librespot-org"
|
||||
|
|
|
@ -29,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross
|
|||
RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf
|
||||
RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf
|
||||
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.74 -y
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.75 -y
|
||||
ENV PATH="/root/.cargo/bin/:${PATH}"
|
||||
RUN rustup target add aarch64-unknown-linux-gnu
|
||||
RUN rustup target add arm-unknown-linux-gnueabi
|
||||
|
|
|
@ -22,7 +22,6 @@ aes = "0.8"
|
|||
base64 = "0.22"
|
||||
byteorder = "1.4"
|
||||
bytes = "1"
|
||||
dns-sd = { version = "0.1", optional = true }
|
||||
form_urlencoded = "1.0"
|
||||
futures-core = "0.3"
|
||||
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
||||
|
@ -71,6 +70,3 @@ vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build
|
|||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["macros", "parking_lot"] }
|
||||
|
||||
[features]
|
||||
with-dns-sd = ["dns-sd"]
|
||||
|
|
|
@ -21,9 +21,6 @@ use url::ParseError;
|
|||
|
||||
use librespot_oauth::OAuthError;
|
||||
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
use dns_sd::DNSError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
|
@ -314,13 +311,6 @@ impl From<DecodeError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
impl From<DNSError> for Error {
|
||||
fn from(err: DNSError) -> Self {
|
||||
Self::new(ErrorKind::Unavailable, err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::Error> for Error {
|
||||
fn from(err: http::Error) -> Self {
|
||||
if err.is::<InvalidHeaderName>()
|
||||
|
|
|
@ -21,13 +21,16 @@ hmac = "0.12"
|
|||
hyper = { version = "1.3", features = ["http1"] }
|
||||
hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] }
|
||||
http-body-util = "0.1.1"
|
||||
libmdns = "0.9"
|
||||
libmdns = { version = "0.9", optional = true }
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||
serde_repr = "0.1"
|
||||
serde_json = "1.0"
|
||||
sha1 = "0.10"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
|
||||
zbus = { version = "4", default-features = false, features = ["tokio"], optional = true }
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
|
@ -39,4 +42,8 @@ hex = "0.4"
|
|||
tokio = { version = "1", features = ["macros", "parking_lot", "rt"] }
|
||||
|
||||
[features]
|
||||
with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"]
|
||||
with-avahi = ["zbus", "serde"]
|
||||
with-dns-sd = ["dns-sd"]
|
||||
with-libmdns = ["libmdns"]
|
||||
|
||||
default = ["with-libmdns"]
|
||||
|
|
151
discovery/src/avahi.rs
Normal file
151
discovery/src/avahi.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
#![cfg(feature = "with-avahi")]
|
||||
|
||||
#[allow(unused)]
|
||||
pub use server::ServerProxy;
|
||||
|
||||
#[allow(unused)]
|
||||
pub use entry_group::{
|
||||
EntryGroupProxy, EntryGroupState, StateChangedStream as EntryGroupStateChangedStream,
|
||||
};
|
||||
|
||||
mod server {
|
||||
// This is not the full interface, just the methods we need!
|
||||
// Avahi also implements a newer version of the interface ("org.freedesktop.Avahi.Server2"), but
|
||||
// the additions are not relevant for us, and the older version is not intended to be deprecated.
|
||||
// cf. the release notes for 0.8 at https://github.com/avahi/avahi/blob/master/docs/NEWS
|
||||
#[zbus::proxy(
|
||||
interface = "org.freedesktop.Avahi.Server",
|
||||
default_service = "org.freedesktop.Avahi",
|
||||
default_path = "/",
|
||||
gen_blocking = false
|
||||
)]
|
||||
trait Server {
|
||||
/// EntryGroupNew method
|
||||
#[zbus(object = "super::entry_group::EntryGroup")]
|
||||
fn entry_group_new(&self);
|
||||
|
||||
/// GetState method
|
||||
fn get_state(&self) -> zbus::Result<i32>;
|
||||
|
||||
/// StateChanged signal
|
||||
#[zbus(signal)]
|
||||
fn state_changed(&self, state: i32, error: &str) -> zbus::Result<()>;
|
||||
}
|
||||
}
|
||||
|
||||
mod entry_group {
|
||||
use serde_repr::Deserialize_repr;
|
||||
use zbus::zvariant;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize_repr)]
|
||||
#[repr(i32)]
|
||||
pub enum EntryGroupState {
|
||||
// The group has not yet been committed, the user must still call avahi_entry_group_commit()
|
||||
Uncommited = 0,
|
||||
// The entries of the group are currently being registered
|
||||
Registering = 1,
|
||||
// The entries have successfully been established
|
||||
Established = 2,
|
||||
// A name collision for one of the entries in the group has been detected, the entries have been withdrawn
|
||||
Collision = 3,
|
||||
// Some kind of failure happened, the entries have been withdrawn
|
||||
Failure = 4,
|
||||
}
|
||||
|
||||
impl zvariant::Type for EntryGroupState {
|
||||
fn signature() -> zvariant::Signature<'static> {
|
||||
zvariant::Signature::try_from("i").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::proxy(
|
||||
interface = "org.freedesktop.Avahi.EntryGroup",
|
||||
default_service = "org.freedesktop.Avahi",
|
||||
gen_blocking = false
|
||||
)]
|
||||
trait EntryGroup {
|
||||
/// AddAddress method
|
||||
fn add_address(
|
||||
&self,
|
||||
interface: i32,
|
||||
protocol: i32,
|
||||
flags: u32,
|
||||
name: &str,
|
||||
address: &str,
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
/// AddRecord method
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_record(
|
||||
&self,
|
||||
interface: i32,
|
||||
protocol: i32,
|
||||
flags: u32,
|
||||
name: &str,
|
||||
clazz: u16,
|
||||
type_: u16,
|
||||
ttl: u32,
|
||||
rdata: &[u8],
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
/// AddService method
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_service(
|
||||
&self,
|
||||
interface: i32,
|
||||
protocol: i32,
|
||||
flags: u32,
|
||||
name: &str,
|
||||
type_: &str,
|
||||
domain: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
txt: &[&[u8]],
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
/// AddServiceSubtype method
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn add_service_subtype(
|
||||
&self,
|
||||
interface: i32,
|
||||
protocol: i32,
|
||||
flags: u32,
|
||||
name: &str,
|
||||
type_: &str,
|
||||
domain: &str,
|
||||
subtype: &str,
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
/// Commit method
|
||||
fn commit(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Free method
|
||||
fn free(&self) -> zbus::Result<()>;
|
||||
|
||||
/// GetState method
|
||||
fn get_state(&self) -> zbus::Result<EntryGroupState>;
|
||||
|
||||
/// IsEmpty method
|
||||
fn is_empty(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// Reset method
|
||||
fn reset(&self) -> zbus::Result<()>;
|
||||
|
||||
/// UpdateServiceTxt method
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_service_txt(
|
||||
&self,
|
||||
interface: i32,
|
||||
protocol: i32,
|
||||
flags: u32,
|
||||
name: &str,
|
||||
type_: &str,
|
||||
domain: &str,
|
||||
txt: &[&[u8]],
|
||||
) -> zbus::Result<()>;
|
||||
|
||||
/// StateChanged signal
|
||||
#[zbus(signal)]
|
||||
fn state_changed(&self, state: EntryGroupState, error: &str) -> zbus::Result<()>;
|
||||
}
|
||||
}
|
|
@ -7,17 +7,19 @@
|
|||
//! This library uses mDNS and DNS-SD so that other devices can find it,
|
||||
//! and spawns an http server to answer requests of Spotify clients.
|
||||
|
||||
mod avahi;
|
||||
mod server;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io,
|
||||
error::Error as StdError,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_core::Stream;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use self::server::DiscoveryServer;
|
||||
|
||||
|
@ -30,6 +32,88 @@ pub use crate::core::authentication::Credentials;
|
|||
/// Determining the icon in the list of available devices.
|
||||
pub use crate::core::config::DeviceType;
|
||||
|
||||
pub enum DiscoveryEvent {
|
||||
Credentials(Credentials),
|
||||
ServerError(DiscoveryError),
|
||||
ZeroconfError(DiscoveryError),
|
||||
}
|
||||
|
||||
enum ZeroconfCmd {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct DnsSdHandle {
|
||||
task_handle: tokio::task::JoinHandle<()>,
|
||||
shutdown_tx: oneshot::Sender<ZeroconfCmd>,
|
||||
}
|
||||
|
||||
impl DnsSdHandle {
|
||||
async fn shutdown(self) {
|
||||
log::debug!("Shutting down zeroconf responder");
|
||||
let Self {
|
||||
task_handle,
|
||||
shutdown_tx,
|
||||
} = self;
|
||||
if shutdown_tx.send(ZeroconfCmd::Shutdown).is_err() {
|
||||
log::warn!("Zeroconf responder unexpectedly disappeared");
|
||||
} else {
|
||||
let _ = task_handle.await;
|
||||
log::debug!("Zeroconf responder stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DnsSdServiceBuilder = fn(
|
||||
Cow<'static, str>,
|
||||
Vec<std::net::IpAddr>,
|
||||
u16,
|
||||
mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
) -> Result<DnsSdHandle, Error>;
|
||||
|
||||
// Default goes first: This matches the behaviour when feature flags were exlusive, i.e. when there
|
||||
// was only `feature = "with-dns-sd"` or `not(feature = "with-dns-sd")`
|
||||
pub const BACKENDS: &[(
|
||||
&str,
|
||||
// If None, the backend is known but wasn't compiled.
|
||||
Option<DnsSdServiceBuilder>,
|
||||
)] = &[
|
||||
#[cfg(feature = "with-avahi")]
|
||||
("avahi", Some(launch_avahi)),
|
||||
#[cfg(not(feature = "with-avahi"))]
|
||||
("avahi", None),
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
("dns-sd", Some(launch_dns_sd)),
|
||||
#[cfg(not(feature = "with-dns-sd"))]
|
||||
("dns-sd", None),
|
||||
#[cfg(feature = "with-libmdns")]
|
||||
("libmdns", Some(launch_libmdns)),
|
||||
#[cfg(not(feature = "with-libmdns"))]
|
||||
("libmdns", None),
|
||||
];
|
||||
|
||||
pub fn find(name: Option<&str>) -> Result<DnsSdServiceBuilder, Error> {
|
||||
if let Some(ref name) = name {
|
||||
match BACKENDS.iter().find(|(id, _)| name == id) {
|
||||
Some((_id, Some(launch_svc))) => Ok(*launch_svc),
|
||||
Some((_id, None)) => Err(Error::unavailable(format!(
|
||||
"librespot built without '{}' support",
|
||||
name
|
||||
))),
|
||||
None => Err(Error::not_found(format!(
|
||||
"unknown zeroconf backend '{}'",
|
||||
name
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
BACKENDS
|
||||
.iter()
|
||||
.find_map(|(_, launch_svc)| *launch_svc)
|
||||
.ok_or(Error::unavailable(
|
||||
"librespot built without zeroconf backends",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes this device visible to Spotify clients in the local network.
|
||||
///
|
||||
/// `Discovery` implements the [`Stream`] trait. Every time this device
|
||||
|
@ -37,10 +121,11 @@ pub use crate::core::config::DeviceType;
|
|||
pub struct Discovery {
|
||||
server: DiscoveryServer,
|
||||
|
||||
#[cfg(not(feature = "with-dns-sd"))]
|
||||
_svc: libmdns::Service,
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
_svc: dns_sd::DNSService,
|
||||
/// An opaque handle to the DNS-SD service. Dropping this will unregister the service.
|
||||
#[allow(unused)]
|
||||
svc: DnsSdHandle,
|
||||
|
||||
event_rx: mpsc::UnboundedReceiver<DiscoveryEvent>,
|
||||
}
|
||||
|
||||
/// A builder for [`Discovery`].
|
||||
|
@ -48,6 +133,7 @@ pub struct Builder {
|
|||
server_config: server::Config,
|
||||
port: u16,
|
||||
zeroconf_ip: Vec<std::net::IpAddr>,
|
||||
zeroconf_backend: Option<DnsSdServiceBuilder>,
|
||||
}
|
||||
|
||||
/// Errors that can occur while setting up a [`Discovery`] instance.
|
||||
|
@ -55,16 +141,27 @@ pub struct Builder {
|
|||
pub enum DiscoveryError {
|
||||
#[error("Creating SHA1 block cipher failed")]
|
||||
AesError(#[from] aes::cipher::InvalidLength),
|
||||
|
||||
#[error("Setting up dns-sd failed: {0}")]
|
||||
DnsSdError(#[from] io::Error),
|
||||
DnsSdError(#[source] Box<dyn StdError + Send + Sync>),
|
||||
|
||||
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
|
||||
HmacError(Vec<u8>),
|
||||
|
||||
#[error("Setting up the HTTP server failed: {0}")]
|
||||
HttpServerError(#[from] hyper::Error),
|
||||
|
||||
#[error("Missing params for key {0}")]
|
||||
ParamsError(&'static str),
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-avahi")]
|
||||
impl From<zbus::Error> for DiscoveryError {
|
||||
fn from(error: zbus::Error) -> Self {
|
||||
Self::DnsSdError(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiscoveryError> for Error {
|
||||
fn from(err: DiscoveryError) -> Self {
|
||||
match err {
|
||||
|
@ -77,6 +174,264 @@ impl From<DiscoveryError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
const DNS_SD_SERVICE_NAME: &str = "_spotify-connect._tcp";
|
||||
#[allow(unused)]
|
||||
const TXT_RECORD: [&str; 2] = ["VERSION=1.0", "CPath=/"];
|
||||
|
||||
#[cfg(feature = "with-avahi")]
|
||||
async fn avahi_task(
|
||||
name: Cow<'static, str>,
|
||||
port: u16,
|
||||
entry_group: &mut Option<avahi::EntryGroupProxy<'_>>,
|
||||
) -> Result<(), DiscoveryError> {
|
||||
use self::avahi::{EntryGroupState, ServerProxy};
|
||||
use futures_util::StreamExt;
|
||||
|
||||
let conn = zbus::Connection::system().await?;
|
||||
|
||||
// Wait for the daemon to show up.
|
||||
// On error: Failed to listen for NameOwnerChanged signal => Fatal DBus issue
|
||||
let bus = zbus::fdo::DBusProxy::new(&conn).await?;
|
||||
let mut stream = bus
|
||||
.receive_name_owner_changed_with_args(&[(0, "org.freedesktop.Avahi")])
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
// Wait for Avahi daemon to be started
|
||||
'wait_avahi: {
|
||||
while let Poll::Ready(Some(_)) = futures_util::poll!(stream.next()) {
|
||||
// Drain queued name owner changes, since we're going to connect in a second
|
||||
}
|
||||
|
||||
// Ping after we connected to the signal since it might have shown up in the meantime
|
||||
if let Ok(avahi_peer) =
|
||||
zbus::fdo::PeerProxy::new(&conn, "org.freedesktop.Avahi", "/").await
|
||||
{
|
||||
if avahi_peer.ping().await.is_ok() {
|
||||
log::debug!("Pinged Avahi: Available");
|
||||
break 'wait_avahi;
|
||||
}
|
||||
}
|
||||
log::warn!("Failed to connect to Avahi, zeroconf discovery will not work until avahi-daemon is started. Check that it is installed and running");
|
||||
|
||||
// If it didn't, wait for the signal
|
||||
match stream.next().await {
|
||||
Some(_signal) => {
|
||||
log::debug!("Avahi appeared");
|
||||
break 'wait_avahi;
|
||||
}
|
||||
// The stream ended, but this should never happen
|
||||
None => {
|
||||
return Err(zbus::Error::Failure("DBus disappeared".to_owned()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to Avahi and publish the service
|
||||
let avahi_server = ServerProxy::new(&conn).await?;
|
||||
log::trace!("Connected to Avahi");
|
||||
|
||||
*entry_group = Some(avahi_server.entry_group_new().await?);
|
||||
|
||||
let mut entry_group_state_stream = entry_group
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.receive_state_changed()
|
||||
.await?;
|
||||
|
||||
entry_group
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.add_service(
|
||||
-1, // AVAHI_IF_UNSPEC
|
||||
-1, // IPv4 and IPv6
|
||||
0, // flags
|
||||
&name,
|
||||
DNS_SD_SERVICE_NAME, // type
|
||||
"", // domain: let the server choose
|
||||
"", // host: let the server choose
|
||||
port,
|
||||
&TXT_RECORD.map(|s| s.as_bytes()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
entry_group.as_mut().unwrap().commit().await?;
|
||||
log::debug!("Commited zeroconf service with name {}", &name);
|
||||
|
||||
'monitor_service: loop {
|
||||
tokio::select! {
|
||||
Some(state_changed) = entry_group_state_stream.next() => {
|
||||
let (state, error) = match state_changed.args() {
|
||||
Ok(sc) => (sc.state, sc.error),
|
||||
Err(e) => {
|
||||
log::warn!("Error on receiving EntryGroup state from Avahi: {}", e);
|
||||
continue 'monitor_service;
|
||||
}
|
||||
};
|
||||
match state {
|
||||
EntryGroupState::Uncommited | EntryGroupState::Registering => {
|
||||
// Not yet registered, ignore.
|
||||
}
|
||||
EntryGroupState::Established => {
|
||||
log::info!("Published zeroconf service");
|
||||
}
|
||||
EntryGroupState::Collision => {
|
||||
// This most likely means that librespot has unintentionally been started twice.
|
||||
// Thus, don't retry with a new name, but abort.
|
||||
//
|
||||
// Note that the error would usually already be returned by
|
||||
// entry_group.add_service above, so this state_changed handler
|
||||
// won't be hit.
|
||||
//
|
||||
// EntryGroup has been withdrawn at this point already!
|
||||
log::error!("zeroconf collision for name '{}'", &name);
|
||||
return Err(zbus::Error::Failure(format!("zeroconf collision for name: {}", name)).into());
|
||||
}
|
||||
EntryGroupState::Failure => {
|
||||
// TODO: Back off/treat as fatal?
|
||||
// EntryGroup has been withdrawn at this point already!
|
||||
// There seems to be no code in Avahi that actually sets this state.
|
||||
log::error!("zeroconf failure: {}", error);
|
||||
return Err(zbus::Error::Failure(format!("zeroconf failure: {}", error)).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_name_owner_change = stream.next() => {
|
||||
break 'monitor_service;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Avahi disappeared (or the service was immediately taken over by a
|
||||
// new daemon) => drop all handles, and reconnect
|
||||
log::info!("Avahi disappeared, trying to reconnect");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-avahi")]
|
||||
fn launch_avahi(
|
||||
name: Cow<'static, str>,
|
||||
_zeroconf_ip: Vec<std::net::IpAddr>,
|
||||
port: u16,
|
||||
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
) -> Result<DnsSdHandle, Error> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
|
||||
let task_handle = tokio::spawn(async move {
|
||||
let mut entry_group = None;
|
||||
tokio::select! {
|
||||
res = avahi_task(name, port, &mut entry_group) => {
|
||||
if let Err(e) = res {
|
||||
log::error!("Avahi error: {}", e);
|
||||
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
|
||||
}
|
||||
},
|
||||
_ = shutdown_rx => {
|
||||
if let Some(entry_group) = entry_group.as_mut() {
|
||||
if let Err(e) = entry_group.free().await {
|
||||
log::warn!("Failed to un-publish zeroconf service: {}", e);
|
||||
} else {
|
||||
log::debug!("Un-published zeroconf service");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DnsSdHandle {
|
||||
task_handle,
|
||||
shutdown_tx,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
fn launch_dns_sd(
|
||||
name: Cow<'static, str>,
|
||||
_zeroconf_ip: Vec<std::net::IpAddr>,
|
||||
port: u16,
|
||||
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
) -> Result<DnsSdHandle, Error> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
|
||||
let task_handle = tokio::task::spawn_blocking(move || {
|
||||
let inner = move || -> Result<(), DiscoveryError> {
|
||||
let svc = dns_sd::DNSService::register(
|
||||
Some(name.as_ref()),
|
||||
DNS_SD_SERVICE_NAME,
|
||||
None,
|
||||
None,
|
||||
port,
|
||||
&TXT_RECORD,
|
||||
)
|
||||
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?;
|
||||
|
||||
let _ = shutdown_rx.blocking_recv();
|
||||
|
||||
std::mem::drop(svc);
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = inner() {
|
||||
log::error!("dns_sd error: {}", e);
|
||||
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DnsSdHandle {
|
||||
shutdown_tx,
|
||||
task_handle,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-libmdns")]
|
||||
fn launch_libmdns(
|
||||
name: Cow<'static, str>,
|
||||
zeroconf_ip: Vec<std::net::IpAddr>,
|
||||
port: u16,
|
||||
status_tx: mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
) -> Result<DnsSdHandle, Error> {
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
|
||||
let task_handle = tokio::task::spawn_blocking(move || {
|
||||
let inner = move || -> Result<(), DiscoveryError> {
|
||||
let svc = if !zeroconf_ip.is_empty() {
|
||||
libmdns::Responder::spawn_with_ip_list(
|
||||
&tokio::runtime::Handle::current(),
|
||||
zeroconf_ip,
|
||||
)
|
||||
} else {
|
||||
libmdns::Responder::spawn(&tokio::runtime::Handle::current())
|
||||
}
|
||||
.map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))
|
||||
.unwrap()
|
||||
.register(
|
||||
DNS_SD_SERVICE_NAME.to_owned(),
|
||||
name.into_owned(),
|
||||
port,
|
||||
&TXT_RECORD,
|
||||
);
|
||||
|
||||
let _ = shutdown_rx.blocking_recv();
|
||||
|
||||
std::mem::drop(svc);
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = inner() {
|
||||
log::error!("libmdns error: {}", e);
|
||||
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(DnsSdHandle {
|
||||
shutdown_tx,
|
||||
task_handle,
|
||||
})
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Starts a new builder using the provided device and client IDs.
|
||||
pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Self {
|
||||
|
@ -90,6 +445,7 @@ impl Builder {
|
|||
},
|
||||
port: 0,
|
||||
zeroconf_ip: vec![],
|
||||
zeroconf_backend: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +473,12 @@ impl Builder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the zeroconf (MDNS and DNS-SD) implementation to use.
|
||||
pub fn zeroconf_backend(mut self, zeroconf_backend: DnsSdServiceBuilder) -> Self {
|
||||
self.zeroconf_backend = Some(zeroconf_backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the port on which it should listen to incoming connections.
|
||||
/// The default value `0` means any port.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
|
@ -129,43 +491,21 @@ impl Builder {
|
|||
/// # Errors
|
||||
/// If setting up the mdns service or creating the server fails, this function returns an error.
|
||||
pub fn launch(self) -> Result<Discovery, Error> {
|
||||
let name = self.server_config.name.clone();
|
||||
let zeroconf_ip = self.zeroconf_ip;
|
||||
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let mut port = self.port;
|
||||
let name = self.server_config.name.clone().into_owned();
|
||||
let server = DiscoveryServer::new(self.server_config, &mut port)?;
|
||||
let _zeroconf_ip = self.zeroconf_ip;
|
||||
let svc;
|
||||
let server = DiscoveryServer::new(self.server_config, &mut port, event_tx.clone())?;
|
||||
|
||||
#[cfg(feature = "with-dns-sd")]
|
||||
{
|
||||
svc = dns_sd::DNSService::register(
|
||||
Some(name.as_ref()),
|
||||
"_spotify-connect._tcp",
|
||||
None,
|
||||
None,
|
||||
port,
|
||||
&["VERSION=1.0", "CPath=/"],
|
||||
)?;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "with-dns-sd"))]
|
||||
{
|
||||
let _svc = if !_zeroconf_ip.is_empty() {
|
||||
libmdns::Responder::spawn_with_ip_list(
|
||||
&tokio::runtime::Handle::current(),
|
||||
_zeroconf_ip,
|
||||
)?
|
||||
} else {
|
||||
libmdns::Responder::spawn(&tokio::runtime::Handle::current())?
|
||||
};
|
||||
svc = _svc.register(
|
||||
"_spotify-connect._tcp".to_owned(),
|
||||
name,
|
||||
port,
|
||||
&["VERSION=1.0", "CPath=/"],
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Discovery { server, _svc: svc })
|
||||
let launch_svc = self.zeroconf_backend.unwrap_or(find(None)?);
|
||||
let svc = launch_svc(name, zeroconf_ip, port, event_tx)?;
|
||||
Ok(Discovery {
|
||||
server,
|
||||
svc,
|
||||
event_rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,12 +519,25 @@ impl Discovery {
|
|||
pub fn new<T: Into<String>>(device_id: T, client_id: T) -> Result<Self, Error> {
|
||||
Self::builder(device_id, client_id).launch()
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) {
|
||||
tokio::join!(self.server.shutdown(), self.svc.shutdown(),);
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for Discovery {
|
||||
type Item = Credentials;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.server).poll_next(cx)
|
||||
match Pin::new(&mut self.event_rx).poll_recv(cx) {
|
||||
// Yields credentials
|
||||
Poll::Ready(Some(DiscoveryEvent::Credentials(creds))) => Poll::Ready(Some(creds)),
|
||||
// Also terminate the stream on fatal server or MDNS/DNS-SD errors.
|
||||
Poll::Ready(Some(
|
||||
DiscoveryEvent::ServerError(_) | DiscoveryEvent::ZeroconfError(_),
|
||||
)) => Poll::Ready(None),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
convert::Infallible,
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener},
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use base64::engine::Engine as _;
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use futures_util::{FutureExt, TryFutureExt};
|
||||
use hmac::{Hmac, Mac};
|
||||
use http_body_util::{BodyExt, Full};
|
||||
|
@ -24,7 +20,7 @@ use serde_json::json;
|
|||
use sha1::{Digest, Sha1};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use super::DiscoveryError;
|
||||
use super::{DiscoveryError, DiscoveryEvent};
|
||||
|
||||
use crate::{
|
||||
core::config::DeviceType,
|
||||
|
@ -47,21 +43,17 @@ struct RequestHandler {
|
|||
config: Config,
|
||||
username: Mutex<Option<String>>,
|
||||
keys: DhLocalKeys,
|
||||
tx: mpsc::UnboundedSender<Credentials>,
|
||||
event_tx: mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
}
|
||||
|
||||
impl RequestHandler {
|
||||
fn new(config: Config) -> (Self, mpsc::UnboundedReceiver<Credentials>) {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
let discovery = Self {
|
||||
fn new(config: Config, event_tx: mpsc::UnboundedSender<DiscoveryEvent>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
username: Mutex::new(None),
|
||||
keys: DhLocalKeys::random(&mut rand::thread_rng()),
|
||||
tx,
|
||||
};
|
||||
|
||||
(discovery, rx)
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_user(&self) -> String {
|
||||
|
@ -202,7 +194,8 @@ impl RequestHandler {
|
|||
|
||||
{
|
||||
let maybe_username = self.username.lock();
|
||||
self.tx.send(credentials)?;
|
||||
self.event_tx
|
||||
.send(DiscoveryEvent::Credentials(credentials))?;
|
||||
if let Ok(mut username_field) = maybe_username {
|
||||
*username_field = Some(String::from(username));
|
||||
} else {
|
||||
|
@ -258,14 +251,22 @@ impl RequestHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) enum DiscoveryServerCmd {
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct DiscoveryServer {
|
||||
cred_rx: mpsc::UnboundedReceiver<Credentials>,
|
||||
_close_tx: oneshot::Sender<Infallible>,
|
||||
close_tx: oneshot::Sender<DiscoveryServerCmd>,
|
||||
task_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DiscoveryServer {
|
||||
pub fn new(config: Config, port: &mut u16) -> Result<Self, Error> {
|
||||
let (discovery, cred_rx) = RequestHandler::new(config);
|
||||
pub fn new(
|
||||
config: Config,
|
||||
port: &mut u16,
|
||||
event_tx: mpsc::UnboundedSender<DiscoveryEvent>,
|
||||
) -> Result<Self, Error> {
|
||||
let discovery = RequestHandler::new(config, event_tx);
|
||||
let address = if cfg!(windows) {
|
||||
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port)
|
||||
} else {
|
||||
|
@ -297,7 +298,7 @@ impl DiscoveryServer {
|
|||
}
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let task_handle = tokio::spawn(async move {
|
||||
let discovery = Arc::new(discovery);
|
||||
|
||||
let server = hyper::server::conn::http1::Builder::new();
|
||||
|
@ -326,27 +327,32 @@ impl DiscoveryServer {
|
|||
});
|
||||
}
|
||||
_ = &mut close_rx => {
|
||||
debug!("Shutting down discovery server");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graceful.shutdown().await;
|
||||
debug!("Discovery server stopped");
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
cred_rx,
|
||||
_close_tx: close_tx,
|
||||
close_tx,
|
||||
task_handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for DiscoveryServer {
|
||||
type Item = Credentials;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Credentials>> {
|
||||
self.cred_rx.poll_recv(cx)
|
||||
pub async fn shutdown(self) {
|
||||
let Self {
|
||||
close_tx,
|
||||
task_handle,
|
||||
..
|
||||
} = self;
|
||||
log::debug!("Shutting down discovery server");
|
||||
if close_tx.send(DiscoveryServerCmd::Shutdown).is_err() {
|
||||
log::warn!("Discovery server unexpectedly disappeared");
|
||||
} else {
|
||||
let _ = task_handle.await;
|
||||
log::debug!("Discovery server stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
117
src/main.rs
117
src/main.rs
|
@ -22,6 +22,7 @@ use librespot::{
|
|||
authentication::Credentials, cache::Cache, config::DeviceType, version, Session,
|
||||
SessionConfig,
|
||||
},
|
||||
discovery::DnsSdServiceBuilder,
|
||||
playback::{
|
||||
audio_backend::{self, SinkBuilder, BACKENDS},
|
||||
config::{
|
||||
|
@ -212,11 +213,11 @@ struct Setup {
|
|||
credentials: Option<Credentials>,
|
||||
enable_oauth: bool,
|
||||
oauth_port: Option<u16>,
|
||||
enable_discovery: bool,
|
||||
zeroconf_port: u16,
|
||||
player_event_program: Option<String>,
|
||||
emit_sink_events: bool,
|
||||
zeroconf_ip: Vec<std::net::IpAddr>,
|
||||
zeroconf_backend: Option<DnsSdServiceBuilder>,
|
||||
}
|
||||
|
||||
fn get_setup() -> Setup {
|
||||
|
@ -277,6 +278,7 @@ fn get_setup() -> Setup {
|
|||
const VOLUME_RANGE: &str = "volume-range";
|
||||
const ZEROCONF_PORT: &str = "zeroconf-port";
|
||||
const ZEROCONF_INTERFACE: &str = "zeroconf-interface";
|
||||
const ZEROCONF_BACKEND: &str = "zeroconf-backend";
|
||||
|
||||
// Mostly arbitrary.
|
||||
const AP_PORT_SHORT: &str = "a";
|
||||
|
@ -327,6 +329,7 @@ fn get_setup() -> Setup {
|
|||
const NORMALISATION_RELEASE_SHORT: &str = "y";
|
||||
const NORMALISATION_THRESHOLD_SHORT: &str = "Z";
|
||||
const ZEROCONF_PORT_SHORT: &str = "z";
|
||||
const ZEROCONF_BACKEND_SHORT: &str = ""; // no short flag
|
||||
|
||||
// Options that have different descriptions
|
||||
// depending on what backends were enabled at build time.
|
||||
|
@ -638,6 +641,12 @@ fn get_setup() -> Setup {
|
|||
ZEROCONF_INTERFACE,
|
||||
"Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.",
|
||||
"IP"
|
||||
)
|
||||
.optopt(
|
||||
ZEROCONF_BACKEND_SHORT,
|
||||
ZEROCONF_BACKEND,
|
||||
"Zeroconf (MDNS/DNS-SD) backend to use. Valid values are 'avahi', 'dns-sd' and 'libmdns', if librespot is compiled with the corresponding feature flags.",
|
||||
"BACKEND"
|
||||
);
|
||||
|
||||
#[cfg(feature = "passthrough-decoder")]
|
||||
|
@ -803,12 +812,22 @@ fn get_setup() -> Setup {
|
|||
exit(0);
|
||||
}
|
||||
|
||||
// Can't use `-> fmt::Arguments` due to https://github.com/rust-lang/rust/issues/92698
|
||||
fn format_flag(long: &str, short: &str) -> String {
|
||||
if short.is_empty() {
|
||||
format!("`--{long}`")
|
||||
} else {
|
||||
format!("`--{long}` / `-{short}`")
|
||||
}
|
||||
}
|
||||
|
||||
let invalid_error_msg =
|
||||
|long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| {
|
||||
error!("Invalid `--{long}` / `-{short}`: \"{invalid}\"");
|
||||
let flag = format_flag(long, short);
|
||||
error!("Invalid {flag}: \"{invalid}\"");
|
||||
|
||||
if !valid_values.is_empty() {
|
||||
println!("Valid `--{long}` / `-{short}` values: {valid_values}");
|
||||
println!("Valid {flag} values: {valid_values}");
|
||||
}
|
||||
|
||||
if !default_value.is_empty() {
|
||||
|
@ -1190,9 +1209,22 @@ fn get_setup() -> Setup {
|
|||
}
|
||||
};
|
||||
|
||||
let enable_discovery = !opt_present(DISABLE_DISCOVERY);
|
||||
let no_discovery_reason = if !cfg!(any(
|
||||
feature = "with-libmdns",
|
||||
feature = "with-dns-sd",
|
||||
feature = "with-avahi"
|
||||
)) {
|
||||
Some("librespot compiled without zeroconf backend".to_owned())
|
||||
} else if opt_present(DISABLE_DISCOVERY) {
|
||||
Some(format!(
|
||||
"the `--{}` / `-{}` flag set",
|
||||
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if credentials.is_none() && !enable_discovery && !enable_oauth {
|
||||
if credentials.is_none() && no_discovery_reason.is_some() && !enable_oauth {
|
||||
error!("Credentials are required if discovery and oauth login are disabled.");
|
||||
exit(1);
|
||||
}
|
||||
|
@ -1225,14 +1257,16 @@ fn get_setup() -> Setup {
|
|||
Some(5588)
|
||||
};
|
||||
|
||||
if !enable_discovery && opt_present(ZEROCONF_PORT) {
|
||||
if let Some(reason) = no_discovery_reason.as_deref() {
|
||||
if opt_present(ZEROCONF_PORT) {
|
||||
warn!(
|
||||
"With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.",
|
||||
DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
|
||||
"With {} `--{}` / `-{}` has no effect.",
|
||||
reason, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let zeroconf_port = if enable_discovery {
|
||||
let zeroconf_port = if no_discovery_reason.is_none() {
|
||||
opt_str(ZEROCONF_PORT)
|
||||
.map(|port| match port.parse::<u16>() {
|
||||
Ok(value) if value != 0 => value,
|
||||
|
@ -1268,6 +1302,16 @@ fn get_setup() -> Setup {
|
|||
None => SessionConfig::default().autoplay,
|
||||
};
|
||||
|
||||
if let Some(reason) = no_discovery_reason.as_deref() {
|
||||
if opt_present(ZEROCONF_INTERFACE) {
|
||||
warn!(
|
||||
"With {} {} has no effect.",
|
||||
reason,
|
||||
format_flag(ZEROCONF_INTERFACE, ZEROCONF_INTERFACE_SHORT),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let zeroconf_ip: Vec<std::net::IpAddr> = if opt_present(ZEROCONF_INTERFACE) {
|
||||
if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) {
|
||||
zeroconf_ip
|
||||
|
@ -1293,6 +1337,39 @@ fn get_setup() -> Setup {
|
|||
vec![]
|
||||
};
|
||||
|
||||
if let Some(reason) = no_discovery_reason.as_deref() {
|
||||
if opt_present(ZEROCONF_BACKEND) {
|
||||
warn!(
|
||||
"With {} `--{}` / `-{}` has no effect.",
|
||||
reason, ZEROCONF_BACKEND, ZEROCONF_BACKEND_SHORT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let zeroconf_backend_name = opt_str(ZEROCONF_BACKEND);
|
||||
let zeroconf_backend = no_discovery_reason.is_none().then(|| {
|
||||
librespot::discovery::find(zeroconf_backend_name.as_deref()).unwrap_or_else(|_| {
|
||||
let available_backends: Vec<_> = librespot::discovery::BACKENDS
|
||||
.iter()
|
||||
.filter_map(|(id, launch_svc)| launch_svc.map(|_| *id))
|
||||
.collect();
|
||||
let default_backend = librespot::discovery::BACKENDS
|
||||
.iter()
|
||||
.find_map(|(id, launch_svc)| launch_svc.map(|_| *id))
|
||||
.unwrap_or("<none>");
|
||||
|
||||
invalid_error_msg(
|
||||
ZEROCONF_BACKEND,
|
||||
ZEROCONF_BACKEND_SHORT,
|
||||
&zeroconf_backend_name.unwrap_or_default(),
|
||||
&available_backends.join(", "),
|
||||
default_backend,
|
||||
);
|
||||
|
||||
exit(1);
|
||||
})
|
||||
});
|
||||
|
||||
let connect_config = {
|
||||
let connect_default_config = ConnectConfig::default();
|
||||
|
||||
|
@ -1734,11 +1811,11 @@ fn get_setup() -> Setup {
|
|||
credentials,
|
||||
enable_oauth,
|
||||
oauth_port,
|
||||
enable_discovery,
|
||||
zeroconf_port,
|
||||
player_event_program,
|
||||
emit_sink_events,
|
||||
zeroconf_ip,
|
||||
zeroconf_backend,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1767,7 +1844,7 @@ async fn main() {
|
|||
|
||||
let mut sys = System::new();
|
||||
|
||||
if setup.enable_discovery {
|
||||
if let Some(zeroconf_backend) = setup.zeroconf_backend {
|
||||
// When started at boot as a service discovery may fail due to it
|
||||
// trying to bind to interfaces before the network is actually up.
|
||||
// This could be prevented in systemd by starting the service after
|
||||
|
@ -1787,6 +1864,7 @@ async fn main() {
|
|||
.is_group(setup.connect_config.is_group)
|
||||
.port(setup.zeroconf_port)
|
||||
.zeroconf_ip(setup.zeroconf_ip.clone())
|
||||
.zeroconf_backend(zeroconf_backend)
|
||||
.launch()
|
||||
{
|
||||
Ok(d) => break Some(d),
|
||||
|
@ -1955,18 +2033,25 @@ async fn main() {
|
|||
|
||||
info!("Gracefully shutting down");
|
||||
|
||||
let mut shutdown_tasks = tokio::task::JoinSet::new();
|
||||
|
||||
// Shutdown spirc if necessary
|
||||
if let Some(spirc) = spirc {
|
||||
if let Err(e) = spirc.shutdown() {
|
||||
error!("error sending spirc shutdown message: {}", e);
|
||||
}
|
||||
|
||||
if let Some(mut spirc_task) = spirc_task {
|
||||
if let Some(spirc_task) = spirc_task {
|
||||
shutdown_tasks.spawn(spirc_task);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(discovery) = discovery {
|
||||
shutdown_tasks.spawn(discovery.shutdown());
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => (),
|
||||
_ = spirc_task.as_mut() => (),
|
||||
else => (),
|
||||
}
|
||||
}
|
||||
_ = shutdown_tasks.join_all() => (),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue