├── .gitignore ├── LICENSE-MIT ├── README.md ├── LICENSE-APACHE ├── mpd_protocol ├── benches │ ├── .gitignore │ └── parse_response.rs ├── fuzz │ ├── .gitignore │ ├── fuzz_targets │ │ ├── sync_connect.rs │ │ └── sync_receive.rs │ └── Cargo.toml ├── README.md ├── LICENSE-MIT ├── Cargo.toml ├── src │ ├── lib.rs │ ├── parser.rs │ ├── response │ │ ├── frame.rs │ │ └── mod.rs │ ├── command.rs │ └── connection.rs ├── CHANGELOG.md └── LICENSE-APACHE ├── Cargo.toml ├── rustfmt.toml ├── mpd_client ├── src │ ├── lib.rs │ ├── responses │ │ ├── playlist.rs │ │ ├── timestamp.rs │ │ ├── sticker.rs │ │ ├── count.rs │ │ ├── list.rs │ │ ├── song.rs │ │ └── mod.rs │ ├── commands │ │ ├── command_list.rs │ │ └── mod.rs │ ├── filter.rs │ ├── tag.rs │ └── client │ │ └── connection.rs ├── Cargo.toml ├── README.md ├── LICENSE-MIT ├── examples │ └── state_changes.rs ├── CHANGELOG.md └── LICENSE-APACHE └── .github └── workflows └── rust.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | mpd_client/LICENSE-MIT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mpd_client/README.md -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | mpd_client/LICENSE-APACHE -------------------------------------------------------------------------------- /mpd_protocol/benches/.gitignore: -------------------------------------------------------------------------------- 1 | long.response 2 | -------------------------------------------------------------------------------- /mpd_protocol/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | coverage 4 | artifacts 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "mpd_protocol", 5 | "mpd_client", 6 | ] 7 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | group_imports = "StdExternalCrate" 3 | imports_granularity = "Crate" 4 | -------------------------------------------------------------------------------- /mpd_protocol/fuzz/fuzz_targets/sync_connect.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use mpd_protocol::Connection; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | let _ = Connection::connect(data); 8 | }); 9 | -------------------------------------------------------------------------------- /mpd_protocol/fuzz/fuzz_targets/sync_receive.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | use mpd_protocol::Connection; 5 | 6 | fuzz_target!(|data: &[u8]| { 7 | let mut connection = Connection::new_internal(data); 8 | let _ = connection.receive(); 9 | }); 10 | -------------------------------------------------------------------------------- /mpd_protocol/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "mpd_protocol-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2021" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.mpd_protocol] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "sync_receive" 24 | path = "fuzz_targets/sync_receive.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "sync_connect" 30 | path = "fuzz_targets/sync_connect.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /mpd_protocol/benches/parse_response.rs: -------------------------------------------------------------------------------- 1 | use std::hint::black_box; 2 | 3 | use criterion::{Criterion, criterion_group, criterion_main}; 4 | use mpd_protocol::Connection; 5 | 6 | // NOTE: Benchmark requires `--cfg criterion` to be set to build correctly. 7 | 8 | const LONG_RESPONSE: &[u8] = include_bytes!("long.response"); 9 | 10 | pub fn criterion_benchmark(c: &mut Criterion) { 11 | c.bench_function("long response", |b| { 12 | b.iter(|| { 13 | let mut connection = Connection::new_internal(black_box(LONG_RESPONSE)); 14 | let _ = connection.receive().unwrap(); 15 | }) 16 | }); 17 | } 18 | 19 | criterion_group!(benches, criterion_benchmark); 20 | criterion_main!(benches); 21 | -------------------------------------------------------------------------------- /mpd_client/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_debug_implementations, 3 | missing_docs, 4 | rust_2018_idioms, 5 | unreachable_pub, 6 | unused_import_braces, 7 | unused_qualifications 8 | )] 9 | #![deny(rustdoc::broken_intra_doc_links)] 10 | #![forbid(unsafe_code)] 11 | 12 | //! Asynchronous client for [MPD](https://musicpd.org). 13 | //! 14 | //! The [`Client`] type is the primary API. 15 | //! 16 | //! # Crate Features 17 | //! 18 | //! | Feature | Description | 19 | //! |----------|-----------------------------------| 20 | //! | `chrono` | Support for parsing [`Timestamp`] | 21 | //! 22 | //! [`Timestamp`]: responses::Timestamp 23 | 24 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 25 | 26 | pub mod client; 27 | pub mod commands; 28 | pub mod filter; 29 | pub mod responses; 30 | pub mod tag; 31 | 32 | pub use mpd_protocol as protocol; 33 | 34 | pub use self::client::Client; 35 | -------------------------------------------------------------------------------- /mpd_protocol/README.md: -------------------------------------------------------------------------------- 1 | # `mpd_protocol` 2 | 3 | Implementation of the client protocol for [MPD]. 4 | 5 | ## Features 6 | 7 | - Protocol support including binary responses and command lists 8 | - Support for traditional blocking IO as well as asynchronous IO (through [Tokio], requires the `async` feature flag) 9 | - Utilities for assembling commands and escaping arguments 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 16 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 17 | 18 | at your option. 19 | 20 | #### Contribution 21 | 22 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 23 | 24 | [MPD]: https://musicpd.org 25 | [Tokio]: https://tokio.rs 26 | -------------------------------------------------------------------------------- /mpd_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mpd_client" 3 | version = "1.4.1" 4 | edition = "2024" 5 | description = "Asynchronous user-friendly MPD client" 6 | repository = "https://github.com/elomatreb/mpd_client" 7 | keywords = ["mpd", "async", "client"] 8 | categories = ["network-programming"] 9 | license = "MIT OR Apache-2.0" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | bytes = "1.5.0" 15 | chrono = { version = "0.4.34", default-features = false, features = [ 16 | "std", 17 | ], optional = true } 18 | mpd_protocol = { version = "1.0.3", features = [ 19 | "async", 20 | ], path = "../mpd_protocol" } 21 | tokio = { version = "1.36.0", features = [ 22 | "rt", 23 | "net", 24 | "time", 25 | "sync", 26 | "macros", 27 | ] } 28 | tracing = "0.1.40" 29 | 30 | [dev-dependencies] 31 | assert_matches = "1.5.0" 32 | tokio-test = "0.4.3" 33 | tracing-subscriber = "0.3.18" 34 | 35 | [package.metadata.docs.rs] 36 | all-features = true 37 | rustdoc-args = ["--cfg", "docsrs"] 38 | -------------------------------------------------------------------------------- /mpd_client/README.md: -------------------------------------------------------------------------------- 1 | # `mpd_client` 2 | 3 | Asynchronous client for [MPD](https://musicpd.org). 4 | 5 | ## Features 6 | 7 | - Asynchronous, using [tokio](https://tokio.rs). 8 | - Supports protocol version 0.23 and binary responses (e.g. for loading album art). 9 | - Typed command API that automatically deals with converting the response into proper Rust structs. 10 | - API for programmatically generating filter expressions without string wrangling. 11 | 12 | ## Example 13 | 14 | See the `examples` directory for a demo of using printing the currently playing song whenever it changes. 15 | 16 | ## License 17 | 18 | Licensed under either of 19 | 20 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 21 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 22 | 23 | at your option. 24 | 25 | ## Contribution 26 | 27 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 28 | -------------------------------------------------------------------------------- /mpd_client/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ole Bertram 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /mpd_protocol/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Ole Bertram 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /mpd_protocol/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mpd_protocol" 3 | version = "1.0.3" 4 | edition = "2024" 5 | license = "MIT OR Apache-2.0" 6 | description = "Implementation of MPD client protocol" 7 | repository = "https://github.com/elomatreb/mpd_client" 8 | readme = "README.md" 9 | keywords = ["mpd", "protocol", "client"] 10 | categories = ["network-programming"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [features] 15 | async = ["tokio"] 16 | 17 | [dependencies] 18 | ahash = "0.8.9" 19 | bytes = "1.5.0" 20 | nom = "8.0.0" 21 | tokio = { version = "1.36.0", features = ["io-util"], optional = true } 22 | tracing = "0.1.40" 23 | 24 | [dev-dependencies] 25 | assert_matches = "1.5.0" 26 | criterion = "0.6.0" 27 | tokio = { version = "1.36.0", features = [ 28 | "io-util", 29 | "rt", 30 | "rt-multi-thread", 31 | "macros", 32 | "net", 33 | ] } 34 | tokio-test = "0.4.3" 35 | 36 | [package.metadata.docs.rs] 37 | all-features = true 38 | rustdoc-args = ["--cfg", "docsrs"] 39 | 40 | [lib] 41 | bench = false 42 | 43 | [[bench]] 44 | name = "parse_response" 45 | harness = false 46 | 47 | [lints.rust] 48 | unexpected_cfgs = { level = "warn", check-cfg = [ 49 | 'cfg(fuzzing)', 50 | 'cfg(criterion)', 51 | ] } 52 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | test: 7 | name: Test Suite 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: test 19 | args: --all-features 20 | 21 | fmt: 22 | name: Rustfmt 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | profile: minimal 29 | toolchain: nightly 30 | override: true 31 | components: rustfmt 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: fmt 35 | args: --all -- --check 36 | 37 | clippy: 38 | name: Clippy 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | toolchain: stable 46 | override: true 47 | components: clippy 48 | - uses: actions-rs/clippy-check@v1 49 | with: 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | args: --all-features 52 | 53 | docs: 54 | name: Rustdoc 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: stable 62 | override: true 63 | - uses: actions-rs/cargo@v1 64 | with: 65 | command: doc 66 | args: --all-features --no-deps 67 | -------------------------------------------------------------------------------- /mpd_client/src/responses/playlist.rs: -------------------------------------------------------------------------------- 1 | use mpd_protocol::response::Frame; 2 | 3 | use crate::responses::{FromFieldValue, Timestamp, TypedResponseError}; 4 | 5 | /// A stored playlist, as returned by [`listplaylists`]. 6 | /// 7 | /// [`listplaylists`]: crate::commands::definitions::GetPlaylists 8 | #[derive(Clone, Debug, PartialEq, Eq)] 9 | #[non_exhaustive] 10 | pub struct Playlist { 11 | /// Name of the playlist. 12 | pub name: String, 13 | /// Server timestamp of last modification. 14 | pub last_modified: Timestamp, 15 | } 16 | 17 | impl Playlist { 18 | pub(crate) fn parse_frame(frame: Frame) -> Result, TypedResponseError> { 19 | let mut out = Vec::with_capacity(frame.fields_len() / 2); 20 | 21 | let mut current_name: Option = None; 22 | 23 | for (key, value) in frame { 24 | if let Some(name) = current_name.take() { 25 | if key.as_ref() == "Last-Modified" { 26 | let last_modified = Timestamp::from_value(value, "Last-Modified")?; 27 | 28 | out.push(Playlist { 29 | name, 30 | last_modified, 31 | }); 32 | } else { 33 | return Err(TypedResponseError::unexpected_field( 34 | "Last-Modified", 35 | key.as_ref(), 36 | )); 37 | } 38 | } else if key.as_ref() == "playlist" { 39 | current_name = Some(value); 40 | } else { 41 | return Err(TypedResponseError::unexpected_field( 42 | "playlist", 43 | key.as_ref(), 44 | )); 45 | } 46 | } 47 | 48 | Ok(out) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mpd_client/examples/state_changes.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use mpd_client::{ 4 | Client, 5 | client::{ConnectionEvent, Subsystem}, 6 | commands, 7 | }; 8 | use tokio::net::TcpStream; 9 | 10 | #[tokio::main(flavor = "current_thread")] 11 | async fn main() -> Result<(), Box> { 12 | tracing_subscriber::fmt().init(); 13 | 14 | // Connect via TCP 15 | let connection = TcpStream::connect("localhost:6600").await?; 16 | // Or through a Unix socket 17 | // let connection = UnixStream::connect("/run/user/1000/mpd").await?; 18 | 19 | // The client is used to issue commands, and state_changes is an async stream of state change 20 | // notifications 21 | let (client, mut state_changes) = Client::connect(connection).await?; 22 | 23 | 'outer: loop { 24 | match client.command(commands::CurrentSong).await? { 25 | Some(song_in_queue) => { 26 | println!( 27 | "\"{}\" by \"{}\"", 28 | song_in_queue.song.title().unwrap_or(""), 29 | song_in_queue.song.artists().join(", "), 30 | ); 31 | } 32 | None => println!("(none)"), 33 | } 34 | 35 | loop { 36 | // wait for a state change notification in the player subsystem, which indicates a song 37 | // change among other things 38 | match state_changes.next().await { 39 | Some(ConnectionEvent::SubsystemChange(Subsystem::Player)) => break, /* something relevant changed */ 40 | Some(ConnectionEvent::SubsystemChange(_)) => continue, /* something changed but we don't care */ 41 | _ => break 'outer, // connection was closed by the server 42 | } 43 | } 44 | } 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /mpd_protocol/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | missing_debug_implementations, 3 | missing_docs, 4 | rust_2018_idioms, 5 | unreachable_pub 6 | )] 7 | #![deny(rustdoc::broken_intra_doc_links)] 8 | #![forbid(unsafe_code)] 9 | #![cfg_attr(docsrs, feature(doc_cfg))] 10 | 11 | //! Implementation of the client protocol for [MPD]. Supports binary responses and command lists. 12 | //! 13 | //! # Crate Features 14 | //! 15 | //! | Feature | Description | 16 | //! |---------|---------------------------------| 17 | //! | `async` | Async support, based on [Tokio] | 18 | //! 19 | //! [MPD]: https://musicpd.org 20 | //! [Tokio]: https://tokio.rs 21 | 22 | pub mod command; 23 | pub mod response; 24 | 25 | mod connection; 26 | mod parser; 27 | 28 | use std::{error::Error, fmt, io}; 29 | 30 | #[cfg(feature = "async")] 31 | pub use self::connection::AsyncConnection; 32 | pub use self::{ 33 | command::{Command, CommandList}, 34 | connection::Connection, 35 | }; 36 | 37 | /// Unrecoverable errors. 38 | #[derive(Debug)] 39 | pub enum MpdProtocolError { 40 | /// IO error occurred 41 | Io(io::Error), 42 | /// A message could not be parsed successfully. 43 | InvalidMessage, 44 | } 45 | 46 | impl fmt::Display for MpdProtocolError { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self { 49 | MpdProtocolError::Io(_) => write!(f, "IO error"), 50 | MpdProtocolError::InvalidMessage => write!(f, "invalid message"), 51 | } 52 | } 53 | } 54 | 55 | #[doc(hidden)] 56 | impl From for MpdProtocolError { 57 | fn from(e: io::Error) -> Self { 58 | MpdProtocolError::Io(e) 59 | } 60 | } 61 | 62 | impl Error for MpdProtocolError { 63 | fn source(&self) -> Option<&(dyn Error + 'static)> { 64 | match self { 65 | MpdProtocolError::Io(e) => Some(e), 66 | MpdProtocolError::InvalidMessage => None, 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mpd_client/src/responses/timestamp.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "chrono")] 2 | use chrono::{DateTime, FixedOffset}; 3 | 4 | use crate::responses::{FromFieldValue, TypedResponseError}; 5 | 6 | /// A timestamp, used for modification times. 7 | /// 8 | /// This is a newtype wrapper to allow the optional use of the `chrono` library. 9 | #[derive(Clone, Debug, Eq)] 10 | pub struct Timestamp { 11 | raw: String, 12 | #[cfg(feature = "chrono")] 13 | chrono: DateTime, 14 | } 15 | 16 | impl Timestamp { 17 | /// Returns the timestamp string as it was returned by the server. 18 | pub fn raw(&self) -> &str { 19 | &self.raw 20 | } 21 | 22 | /// Returns the timestamp string as it was returned by the server. 23 | #[cfg(feature = "chrono")] 24 | pub fn chrono_datetime(&self) -> DateTime { 25 | self.chrono 26 | } 27 | } 28 | 29 | impl PartialEq for Timestamp { 30 | fn eq(&self, other: &Self) -> bool { 31 | self.raw == other.raw 32 | } 33 | } 34 | 35 | #[cfg(feature = "chrono")] 36 | impl PartialEq> for Timestamp { 37 | fn eq(&self, other: &DateTime) -> bool { 38 | &self.chrono == other 39 | } 40 | } 41 | 42 | impl PartialOrd for Timestamp { 43 | fn partial_cmp(&self, other: &Self) -> Option { 44 | Some(self.cmp(other)) 45 | } 46 | } 47 | 48 | impl Ord for Timestamp { 49 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 50 | self.raw.cmp(&other.raw) 51 | } 52 | } 53 | 54 | #[cfg(feature = "chrono")] 55 | impl PartialOrd> for Timestamp { 56 | fn partial_cmp(&self, other: &DateTime) -> Option { 57 | self.chrono.partial_cmp(other) 58 | } 59 | } 60 | 61 | impl FromFieldValue for Timestamp { 62 | #[cfg_attr(not(feature = "chrono"), allow(unused_variables))] 63 | fn from_value(v: String, field: &str) -> Result { 64 | #[cfg(feature = "chrono")] 65 | let chrono = match DateTime::parse_from_rfc3339(&v) { 66 | Ok(v) => v, 67 | Err(e) => return Err(TypedResponseError::invalid_value(field, v).source(e)), 68 | }; 69 | 70 | Ok(Self { 71 | raw: v, 72 | #[cfg(feature = "chrono")] 73 | chrono, 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mpd_protocol/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.3 (2024-02-28) 2 | 3 | - Dependency updates. 4 | 5 | # 1.0.2 (2023-10-30) 6 | 7 | - Dependency updates 8 | 9 | # 1.0.1 (2023-07-01) 10 | 11 | - Internal improvements, dependency updates 12 | 13 | # 1.0.0 (2022-08-27) 14 | 15 | - Redesign and simplify the `Argument` trait 16 | - Make `CommandError` type opaque 17 | - API changes: 18 | - Rename `Frame::get_binary` to `Frame::take_binary` 19 | - Rename `Response::single_frame` to `Response::into_single_frame` 20 | - Remove `Response` root reexport 21 | 22 | # 0.13.0 (2021-12-09) 23 | 24 | - Redesign connection interface. 25 | - Instead of standalone functions for the synchronous API and a `Codec` implementation for the asynchronous API, connections are now represented as structs with either synchronous or asynchronous methods (`Connection` and `AsyncConnection`). 26 | 27 | As a result, the asynchronous API no longer consists of a `Sink` for commands and a corresponding `Stream` of responses, but individual methods that either write commands or read a response. 28 | 29 | # 0.12.1 (2021-05-13) 30 | 31 | - No external changes (only doc fixes) 32 | 33 | # 0.12.0 (2021-05-13) 34 | 35 | - Make async functionality optional by moving it behind the default-off `async` feature flag. Without it, the `tokio` dependencies are removed. 36 | - Rename error type from `MpdCodecError` to `MpdProtocolError` to reflect the above change. 37 | - Remove raw message contents from `InvalidMessage` error variant. 38 | - API changes: 39 | - Remove `Response::new()` and `Response::empty()` methods 40 | - Rename `Response::len()` to `Response::successful_frames()` 41 | - Remove `Frame::empty()` 42 | - Add `DoubleEndedIterator` implementations for response frame iterators 43 | - Internal improvements. 44 | 45 | # 0.11.0 (2021-01-01) 46 | 47 | - Update to `tokio` 1.0. 48 | 49 | # 0.10.1 (2020-11-20) 50 | 51 | - Update to `nom` 6. 52 | 53 | # 0.10.0 (2020-11-02) 54 | 55 | - Update `tokio-util` and `bytes` crates. 56 | 57 | # 0.9.0 (2020-10-23) 58 | 59 | - Update to tokio 0.3 60 | - Provide basic functions for sending and receiving using synchronous IO 61 | - Don't depend on nom features we don't actually use 62 | - Reword error messages to follow API guidelines 63 | 64 | # 0.8.1 (2020-08-05) 65 | 66 | - Change license to MIT or Apache 2.0 67 | 68 | # 0.8.0 (2020-08-02) 69 | 70 | - Rewritten parser that incrementally builds up a response 71 | - Explicit connection method that creates a codec instead of handling the handshake internally 72 | - Overhauled Frame APIs 73 | - Removed `command_list` macro 74 | - Many smaller changes 75 | -------------------------------------------------------------------------------- /mpd_client/src/commands/command_list.rs: -------------------------------------------------------------------------------- 1 | use mpd_protocol::{command::CommandList as RawCommandList, response::Frame}; 2 | 3 | use crate::{commands::Command, responses::TypedResponseError}; 4 | 5 | /// Types which can be used as a typed command list, using 6 | /// [`Client::command_list`][crate::Client::command_list]. 7 | /// 8 | /// This is implemented for tuples of [`Command`s][Command] where it returns a tuple of the same 9 | /// size of the responses corresponding to the commands, as well as for a vector of the same 10 | /// command type where it returns a vector of the same length of the responses. 11 | pub trait CommandList { 12 | /// The responses the list will result in. 13 | type Response; 14 | 15 | /// The command list that will be sent, or `None` if no commands. 16 | fn command_list(&self) -> Option; 17 | 18 | /// Convert the raw response frames into the proper response types(s). 19 | /// 20 | /// # Errors 21 | /// 22 | /// This should return an error if any of the responses were invalid. 23 | fn responses(self, frames: Vec) -> Result; 24 | } 25 | 26 | /// Arbitrarily long sequence of the same command. 27 | impl CommandList for Vec 28 | where 29 | C: Command, 30 | { 31 | type Response = Vec; 32 | 33 | fn command_list(&self) -> Option { 34 | let mut commands = self.iter().map(Command::command); 35 | let mut raw_commands = RawCommandList::new(commands.next()?); 36 | raw_commands.extend(commands); 37 | 38 | Some(raw_commands) 39 | } 40 | 41 | fn responses(self, frames: Vec) -> Result { 42 | assert_eq!(self.len(), frames.len()); 43 | let mut out = Vec::with_capacity(self.len()); 44 | 45 | for (command, frame) in self.into_iter().zip(frames) { 46 | out.push(command.response(frame)?); 47 | } 48 | 49 | Ok(out) 50 | } 51 | } 52 | 53 | macro_rules! impl_command_list_tuple { 54 | ($first_type:ident, $($further_type:ident => $further_idx:tt),*) => { 55 | impl<$first_type, $($further_type),*> CommandList for ($first_type, $($further_type),*) 56 | where 57 | $first_type: Command, 58 | $( 59 | $further_type: Command 60 | ),* 61 | { 62 | type Response = ($first_type::Response, $($further_type::Response),*); 63 | 64 | fn command_list(&self) -> Option { 65 | #[allow(unused_mut)] 66 | let mut commands = RawCommandList::new(self.0.command()); 67 | 68 | $( 69 | commands.add(self.$further_idx.command()); 70 | )* 71 | 72 | Some(commands) 73 | } 74 | 75 | fn responses(self, frames: Vec) -> Result { 76 | let mut frames = frames.into_iter(); 77 | 78 | Ok(( 79 | self.0.response(frames.next().unwrap())?, 80 | $( 81 | self.$further_idx.response(frames.next().unwrap())?, 82 | )* 83 | )) 84 | } 85 | } 86 | }; 87 | } 88 | 89 | impl_command_list_tuple!(A,); 90 | impl_command_list_tuple!(A, B => 1); 91 | impl_command_list_tuple!(A, B => 1, C => 2); 92 | impl_command_list_tuple!(A, B => 1, C => 2, D => 3); 93 | impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4); 94 | impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5); 95 | impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5, G => 6); 96 | impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5, G => 6, H => 7); 97 | -------------------------------------------------------------------------------- /mpd_client/src/responses/sticker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use mpd_protocol::response::Frame; 4 | 5 | use crate::responses::{KeyValuePair, TypedResponseError}; 6 | 7 | /// Response to the [`sticker get`] command. 8 | /// 9 | /// [`sticker get`]: crate::commands::definitions::StickerGet 10 | #[derive(Clone, Debug, PartialEq, Eq)] 11 | #[non_exhaustive] 12 | pub struct StickerGet { 13 | /// The sticker value 14 | pub value: String, 15 | } 16 | 17 | impl StickerGet { 18 | pub(crate) fn from_frame(frame: Frame) -> Result { 19 | let Some((key, field_value)) = frame.into_iter().next() else { 20 | return Err(TypedResponseError::missing("sticker")); 21 | }; 22 | 23 | if &*key != "sticker" { 24 | return Err(TypedResponseError::unexpected_field( 25 | "sticker", 26 | key.as_ref(), 27 | )); 28 | } 29 | 30 | let (_, sticker_value) = parse_sticker_value(field_value)?; 31 | 32 | Ok(StickerGet { 33 | value: sticker_value, 34 | }) 35 | } 36 | } 37 | 38 | impl From for String { 39 | fn from(sticker_get: StickerGet) -> Self { 40 | sticker_get.value 41 | } 42 | } 43 | 44 | /// Response to the [`sticker list`] command. 45 | /// 46 | /// [`sticker list`]: crate::commands::definitions::StickerList 47 | #[derive(Clone, Debug, PartialEq, Eq)] 48 | #[non_exhaustive] 49 | pub struct StickerList { 50 | /// A map of sticker names to their values 51 | pub value: HashMap, 52 | } 53 | 54 | impl StickerList { 55 | pub(crate) fn from_frame( 56 | raw: impl IntoIterator, 57 | ) -> Result { 58 | let value = raw 59 | .into_iter() 60 | .map(|(_, value)| parse_sticker_value(value)) 61 | .collect::>()?; 62 | 63 | Ok(Self { value }) 64 | } 65 | } 66 | 67 | impl From for HashMap { 68 | fn from(sticker_list: StickerList) -> Self { 69 | sticker_list.value 70 | } 71 | } 72 | 73 | /// Response to the [`sticker find`] command. 74 | /// 75 | /// [`sticker find`]: crate::commands::definitions::StickerFind 76 | #[derive(Clone, Debug, PartialEq, Eq)] 77 | #[non_exhaustive] 78 | pub struct StickerFind { 79 | /// A map of songs to their sticker values 80 | pub value: HashMap, 81 | } 82 | 83 | impl StickerFind { 84 | pub(crate) fn from_frame( 85 | raw: impl IntoIterator, 86 | ) -> Result { 87 | let mut value = HashMap::new(); 88 | 89 | let mut file = String::new(); 90 | 91 | for (key, tag) in raw { 92 | match &*key { 93 | "file" => file = tag, 94 | "sticker" => { 95 | let (_, sticker_value) = parse_sticker_value(tag)?; 96 | value.insert(file.clone(), sticker_value); 97 | } 98 | other => return Err(TypedResponseError::unexpected_field("sticker", other)), 99 | } 100 | } 101 | 102 | Ok(Self { value }) 103 | } 104 | } 105 | 106 | /// Parses a `key=value` tag into its key and value strings 107 | fn parse_sticker_value(mut tag: String) -> Result<(String, String), TypedResponseError> { 108 | match tag.split_once('=') { 109 | Some((key, value)) => { 110 | let value = String::from(value); 111 | tag.truncate(key.len()); 112 | Ok((tag, value)) 113 | } 114 | None => Err(TypedResponseError::invalid_value("sticker", tag)), 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /mpd_client/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | //! Strongly typed, pre-built commands. 2 | //! 3 | //! This module contains pre-made definitions of commands and responses, so you don't have to 4 | //! wrangle the stringly-typed raw responses if you don't want to. 5 | //! 6 | //! The fields on the contained structs are mostly undocumented, see the [MPD protocol 7 | //! documentation][mpd-docs] for details on their specific meaning. 8 | //! 9 | //! [mpd-docs]: https://www.musicpd.org/doc/html/protocol.html#command-reference 10 | 11 | pub mod definitions; 12 | 13 | mod command_list; 14 | 15 | use std::{fmt::Write, time::Duration}; 16 | 17 | use bytes::BytesMut; 18 | use mpd_protocol::{ 19 | command::{Argument, Command as RawCommand}, 20 | response::Frame, 21 | }; 22 | 23 | pub use self::{command_list::CommandList, definitions::*}; 24 | use crate::responses::TypedResponseError; 25 | 26 | /// Stable identifier of a song in the queue. 27 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 28 | pub struct SongId(pub u64); 29 | 30 | impl From for SongId { 31 | fn from(id: u64) -> Self { 32 | Self(id) 33 | } 34 | } 35 | 36 | impl Argument for SongId { 37 | fn render(&self, buf: &mut BytesMut) { 38 | write!(buf, "{}", self.0).unwrap(); 39 | } 40 | } 41 | 42 | /// Position of a song in the queue. 43 | /// 44 | /// This will change when the queue is modified. 45 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 46 | pub struct SongPosition(pub usize); 47 | 48 | impl From for SongPosition { 49 | fn from(pos: usize) -> Self { 50 | Self(pos) 51 | } 52 | } 53 | 54 | impl Argument for SongPosition { 55 | fn render(&self, buf: &mut BytesMut) { 56 | write!(buf, "{}", self.0).unwrap(); 57 | } 58 | } 59 | 60 | /// Possible ways to seek in the current song. 61 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 62 | pub enum SeekMode { 63 | /// Forwards from current position. 64 | Forward(Duration), 65 | /// Backwards from current position. 66 | Backward(Duration), 67 | /// To the absolute position in the current song. 68 | Absolute(Duration), 69 | } 70 | 71 | /// Possible `single` modes. 72 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 73 | #[allow(missing_docs)] 74 | pub enum SingleMode { 75 | Enabled, 76 | Disabled, 77 | Oneshot, 78 | } 79 | 80 | /// Possible `replay_gain_mode` modes. 81 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 82 | #[allow(missing_docs)] 83 | pub enum ReplayGainMode { 84 | /// Replay Gain off 85 | Off, 86 | /// Replay Gain Track mode 87 | Track, 88 | /// Replay Gain Album mode 89 | Album, 90 | /// Replay Gain Track if shuffle is on, Album otherwise 91 | Auto, 92 | } 93 | 94 | /// Modes to target a song with a command. 95 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 96 | pub enum Song { 97 | /// By ID 98 | Id(SongId), 99 | /// By position in the queue. 100 | Position(SongPosition), 101 | } 102 | 103 | impl From for Song { 104 | fn from(id: SongId) -> Self { 105 | Self::Id(id) 106 | } 107 | } 108 | 109 | impl From for Song { 110 | fn from(pos: SongPosition) -> Self { 111 | Self::Position(pos) 112 | } 113 | } 114 | 115 | /// Types which can be used as pre-built properly typed commands. 116 | pub trait Command { 117 | /// The response this command will return. 118 | type Response; 119 | 120 | /// Create the raw command representation for transmission. 121 | fn command(&self) -> RawCommand; 122 | 123 | /// Convert the raw response frame to the proper response type. 124 | /// 125 | /// # Errors 126 | /// 127 | /// This should return an error if the response was invalid. 128 | fn response(self, frame: Frame) -> Result; 129 | } 130 | -------------------------------------------------------------------------------- /mpd_client/src/responses/count.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use mpd_protocol::response::Frame; 4 | 5 | use crate::{ 6 | responses::{FromFieldValue, TypedResponseError, value}, 7 | tag::Tag, 8 | }; 9 | 10 | /// Response to the [`Count`][crate::commands::Count] command. 11 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 12 | #[non_exhaustive] 13 | pub struct Count { 14 | /// Number of songs 15 | pub songs: u64, 16 | /// Total playtime of the songs 17 | pub playtime: Duration, 18 | } 19 | 20 | impl Count { 21 | pub(crate) fn from_frame(mut frame: Frame) -> Result { 22 | Ok(Count { 23 | songs: value(&mut frame, "songs")?, 24 | playtime: value(&mut frame, "playtime")?, 25 | }) 26 | } 27 | 28 | pub(crate) fn from_frame_grouped( 29 | frame: Frame, 30 | group_by: &Tag, 31 | ) -> Result, TypedResponseError> { 32 | let mut out = Vec::with_capacity(frame.fields_len() / 3); 33 | build_grouped_values(&mut out, group_by, frame)?; 34 | Ok(out) 35 | } 36 | } 37 | 38 | fn build_grouped_values( 39 | out: &mut Vec<(String, Count)>, 40 | grouping_tag: &Tag, 41 | fields: I, 42 | ) -> Result<(), TypedResponseError> 43 | where 44 | I: IntoIterator, 45 | V: AsRef, 46 | { 47 | let mut fields = fields.into_iter(); 48 | while let Some((key, value)) = fields.next() { 49 | let mut songs: Option = None; 50 | let mut playtime: Option = None; 51 | 52 | if key.as_ref() != grouping_tag.as_str() { 53 | return Err(TypedResponseError::unexpected_field( 54 | grouping_tag.as_str(), 55 | key.as_ref(), 56 | )); 57 | } 58 | 59 | while songs.is_none() || playtime.is_none() { 60 | if let Some((key, value)) = fields.next() { 61 | match key.as_ref() { 62 | "songs" => { 63 | if songs.is_none() { 64 | songs = Some(u64::from_value(value, "songs")?); 65 | } else { 66 | return Err(TypedResponseError::unexpected_field("playtime", "songs")); 67 | } 68 | } 69 | "playtime" => { 70 | if playtime.is_none() { 71 | playtime = Some(Duration::from_value(value, "playtime")?); 72 | } else { 73 | return Err(TypedResponseError::unexpected_field("songs", "playtime")); 74 | } 75 | } 76 | other => { 77 | return Err(TypedResponseError::unexpected_field( 78 | if songs.is_some() { "playtime" } else { "songs" }, 79 | other, 80 | )); 81 | } 82 | } 83 | } else { 84 | return Err(TypedResponseError::missing(if songs.is_some() { 85 | "playtime" 86 | } else { 87 | "songs" 88 | })); 89 | } 90 | } 91 | 92 | out.push(( 93 | value, 94 | Count { 95 | songs: songs.unwrap(), 96 | playtime: playtime.unwrap(), 97 | }, 98 | )); 99 | } 100 | 101 | Ok(()) 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use assert_matches::assert_matches; 107 | 108 | use super::*; 109 | 110 | #[test] 111 | fn grouped_values_parsing() { 112 | let mut out = Vec::new(); 113 | 114 | build_grouped_values::<_, &str>(&mut out, &Tag::Album, vec![]).unwrap(); 115 | assert_eq!(out, &[]); 116 | 117 | build_grouped_values( 118 | &mut out, 119 | &Tag::Album, 120 | vec![ 121 | ("Album", String::from("hello")), 122 | ("songs", String::from("1234")), 123 | ("playtime", String::from("1234")), 124 | ("Album", String::from("world")), 125 | ("playtime", String::from("1")), 126 | ("songs", String::from("1")), 127 | ], 128 | ) 129 | .unwrap(); 130 | 131 | assert_eq!( 132 | out, 133 | &[ 134 | ( 135 | String::from("hello"), 136 | Count { 137 | songs: 1234, 138 | playtime: Duration::from_secs(1234) 139 | } 140 | ), 141 | ( 142 | String::from("world"), 143 | Count { 144 | songs: 1, 145 | playtime: Duration::from_secs(1) 146 | } 147 | ) 148 | ] 149 | ); 150 | out.clear(); 151 | 152 | let res = build_grouped_values( 153 | &mut out, 154 | &Tag::Album, 155 | vec![ 156 | ("Album", String::from("hello")), 157 | ("songs", String::from("1234")), 158 | ], 159 | ); 160 | 161 | assert_matches!(res, Err(_)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /mpd_client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.1 (2024-02-28) 2 | 3 | - Dependency updates. 4 | 5 | # 1.4.0 (2024-02-01) 6 | 7 | - Add commands for getting metadata about individual songs or subranges of the play queue ([#22](https://github.com/elomatreb/mpd_client/pull/22), thanks to kholthaus). 8 | - Fix a potential panic or overflow on commands that take ranges of song positions. 9 | - Dependency updates. 10 | 11 | # 1.3.0 (2023-10-30) 12 | 13 | - Add commands for interacting with the ReplayGain options (`ReplayGainStatus`, `SetReplayGainMode`) ([#19](https://github.com/elomatreb/mpd_client/issues/19), [#20](https://github.com/elomatreb/mpd_client/20), thanks to kholthaus). 14 | - Internal improvements and dependency updates. 15 | 16 | # 1.2.0 (2023-07-01) 17 | 18 | - Add commands for interacting with [client-to-client channels](https://mpd.readthedocs.io/en/latest/protocol.html#client-to-client) (`SubscribeToChannel`, `UnsubscribeFromChannel`, `ListChannels`, `ReadChannelMessages`, `SendChannelMessage`). 19 | - Internal improvements and dependency updates. 20 | 21 | # 1.1.0 (2023-03-13) 22 | 23 | - Add the `Update` and `Rescan` commands for managing updates to the MPD library (#8, thanks to pborzenkov). 24 | 25 | # 1.0.0 (2022-08-27) 26 | 27 | - Redesign the `Command` and `CommandList` traits 28 | - Remove trait seal, you can add your own impls now 29 | - Remove the `Response` trait, response creation is now handled by a method on the respective trait 30 | - Add public functions for constructing `TypedResponseError`s 31 | - Reorganize crate modules 32 | - Commands now live in their own top-level module 33 | - Error types now live in the modules where they are used 34 | - Make `chrono` dependency optional 35 | - Rename `StateChanges` to `ConnectionEvents` and return an enum of possible events. 36 | - Redesign commands to take references to their arguments where necessary instead of taking ownership. 37 | - Add commands for managing song stickers ([#14](https://github.com/elomatreb/mpd_client/pull/14), thanks to JakeStanger). 38 | - Add `Count` command (proposed by pborzenkov in [#15](https://github.com/elomatreb/mpd_client/pull/15)). 39 | - Reimplement `List` command to support type-safe grouping. 40 | - Bug fixes: 41 | - Missing `CommandList` impl for tuples of size 4 42 | - Missing argument rendering on `GetPlaylist` commnad 43 | - Other API changes: 44 | - Clean up crate reexports. Now simply reexports the entire `mpd_protocol` crate as `protocol`. 45 | - Add `Client::is_connection_closed` 46 | - `Status` response: Don't suppress the `default` partition name 47 | - `AlbumArt` response: Expose returned raw data as `BytesMut` 48 | - `Client::album_art`: Return loaded data as `BytesMut` 49 | 50 | # 0.7.5 51 | 52 | - Add `Ping` command. 53 | - Add commands for managing enabled metadata tags (`TagTypes` and `EnabledTagTypes`). 54 | 55 | # 0.7.4 (2022-06-04) 56 | 57 | - Fix `ListAllIn` error when response includes playlist objects. 58 | 59 | # 0.7.3 (2022-03-15) 60 | 61 | - Fix `List::group_by` generating invalid commands when used (due to missing keyword). 62 | 63 | # 0.7.2 (2022-02-20) 64 | 65 | - Add a utility method for connecting with an *optional* password (`Client::connect_with_password_opt`). 66 | - Require tokio 0.16.1. 67 | 68 | # 0.7.1 (2021-12-10) 69 | 70 | - Fix panic when parsing a `Song` response that contains negative or invalid duration values. 71 | 72 | # 0.7.0 (2021-12-09) 73 | 74 | - Response types for typed commands are now marked as `#[non_exhaustive]` where reasonable. 75 | 76 | This will allow future fields added to MPD to be added to the responses without breaking compatibility. As a result, the `Password` command and the `Client` method have been removed. 77 | - Rework connection password handling. 78 | 79 | Passwords are now specified on the initial connect and sent immediately after. This avoids issues where the `idle` command of the background task is sent before the password, resulting in spurious "permission denied" errors with restrictively configured MPD servers ([#10](https://github.com/elomatreb/mpd_client/issues/10)). 80 | - Added new features introduced in version 0.23 of MPD: 81 | - New tags (`ComposerSort`, `Ensemble`, `Location`, `Movement`, `MovementNumber`) 82 | - New position options for certain commands (`Add`, `AddToPlaylist`, `RemoveFromPlaylist`) 83 | - Rework `Move` command to use a builder 84 | - Command types are no longer `Copy` if they have private fields (to aid in forward compatibility). 85 | - The `Tag` enum now has forward-compatible equality based on the string representation. If a new variant is added, it will be equal to the `Other(_)` variant containing the same string. 86 | - Updated `mpd_protocol` dependency. 87 | 88 | # 0.6.1 (2021-08-21) 89 | 90 | - Add a limited degree of backwards compatibility for protocol versions older than 0.20 ([#9](https://github.com/elomatreb/mpd_client/pull/9), thanks to D3fus). 91 | Specifically, support parsing song durations with fallback to deprecated fields. 92 | **NOTE**: Other features still do **not** support these old protocols, notably the filter expressions used by certain commands. 93 | - Add a utility method for retrieving MPD subsystem protocol names. 94 | - Fix missing `Command` impl for `SetBinaryLimit` command. 95 | 96 | # 0.6.0 (2021-05-17) 97 | 98 | - Update `mpd_protocol` 99 | - Add `Client::album_art` method for loading album art 100 | - Add new MPD subsystems 101 | - API changes: 102 | - Remove `Client::connect_to` and `Client::connect_unix` methods 103 | - Rename `Command::to_command` to `Command::into_command` 104 | 105 | # 0.5.1 (2021-04-28) 106 | 107 | - Fix error when parsing list of songs response containing modified timestamps for directories ([#7](https://github.com/elomatreb/mpd_client/issues/7)) 108 | 109 | # 0.5.0 (2021-01-01) 110 | 111 | - Update to `tokio` 1.0. 112 | 113 | # 0.4.0 (2020-11-06) 114 | 115 | - Add typed commands and command list API 116 | - Update to tokio 0.3 117 | - Adapt to MPD 0.22 versions 118 | -------------------------------------------------------------------------------- /mpd_client/src/responses/list.rs: -------------------------------------------------------------------------------- 1 | use std::{slice::Iter, vec::IntoIter}; 2 | 3 | use mpd_protocol::response::Frame; 4 | 5 | use crate::tag::Tag; 6 | 7 | /// Response to the [`list`] command. 8 | /// 9 | /// [`list`]: crate::commands::definitions::List 10 | #[derive(Clone, Debug, PartialEq, Eq)] 11 | pub struct List { 12 | primary_tag: Tag, 13 | groupings: [Tag; N], 14 | fields: Vec<(Tag, String)>, 15 | } 16 | 17 | impl List<0> { 18 | /// Returns an iterator over all distinct values returned. 19 | pub fn values(&self) -> ListValuesIter<'_> { 20 | ListValuesIter(self.fields.iter()) 21 | } 22 | } 23 | 24 | impl List { 25 | pub(crate) fn from_frame(primary_tag: Tag, groupings: [Tag; N], frame: Frame) -> List { 26 | let fields = frame 27 | .into_iter() 28 | // Unwrapping here is fine because the parser already validated the fields 29 | .map(|(tag, value)| (Tag::try_from(tag.as_ref()).unwrap(), value)) 30 | .collect(); 31 | 32 | List { 33 | primary_tag, 34 | groupings, 35 | fields, 36 | } 37 | } 38 | 39 | /// Returns an iterator over the grouped combinations returned by the command. 40 | /// 41 | /// The grouped values are returned in the same order they were passed to [`group_by`]. 42 | /// 43 | /// [`group_by`]: crate::commands::definitions::List::group_by 44 | pub fn grouped_values(&self) -> GroupedListValuesIter<'_, N> { 45 | GroupedListValuesIter { 46 | primary_tag: &self.primary_tag, 47 | grouping_tags: &self.groupings, 48 | grouping_values: [""; N], 49 | fields: self.fields.iter(), 50 | } 51 | } 52 | 53 | /// Returns the tags the response was grouped by. 54 | pub fn grouped_by(&self) -> &[Tag; N] { 55 | &self.groupings 56 | } 57 | 58 | /// Get the raw fields as they were returned by the server. 59 | pub fn into_raw_values(self) -> Vec<(Tag, String)> { 60 | self.fields 61 | } 62 | } 63 | 64 | impl<'a> IntoIterator for &'a List<0> { 65 | type Item = &'a str; 66 | 67 | type IntoIter = ListValuesIter<'a>; 68 | 69 | fn into_iter(self) -> Self::IntoIter { 70 | self.values() 71 | } 72 | } 73 | 74 | impl IntoIterator for List<0> { 75 | type Item = String; 76 | 77 | type IntoIter = ListValuesIntoIter; 78 | 79 | fn into_iter(self) -> Self::IntoIter { 80 | ListValuesIntoIter(self.fields.into_iter()) 81 | } 82 | } 83 | 84 | /// Iterator over references to grouped values. 85 | /// 86 | /// Returned by [`List::grouped_values`]. 87 | #[derive(Clone, Debug)] 88 | pub struct GroupedListValuesIter<'a, const N: usize> { 89 | primary_tag: &'a Tag, 90 | grouping_tags: &'a [Tag; N], 91 | grouping_values: [&'a str; N], 92 | fields: Iter<'a, (Tag, String)>, 93 | } 94 | 95 | impl<'a, const N: usize> Iterator for GroupedListValuesIter<'a, N> { 96 | type Item = (&'a str, [&'a str; N]); 97 | 98 | fn next(&mut self) -> Option { 99 | loop { 100 | let (tag, value) = self.fields.next()?; 101 | 102 | if tag == self.primary_tag { 103 | break Some((value, self.grouping_values)); 104 | } 105 | 106 | let idx = self.grouping_tags.iter().position(|t| t == tag).unwrap(); 107 | self.grouping_values[idx] = value; 108 | } 109 | } 110 | } 111 | 112 | /// Iterator over references to ungrouped [`List`] values. 113 | #[derive(Clone, Debug)] 114 | pub struct ListValuesIter<'a>(Iter<'a, (Tag, String)>); 115 | 116 | impl<'a> Iterator for ListValuesIter<'a> { 117 | type Item = &'a str; 118 | 119 | fn next(&mut self) -> Option { 120 | self.0.next().map(|(_, v)| &**v) 121 | } 122 | 123 | fn size_hint(&self) -> (usize, Option) { 124 | self.0.size_hint() 125 | } 126 | 127 | fn count(self) -> usize { 128 | self.0.count() 129 | } 130 | 131 | fn nth(&mut self, n: usize) -> Option { 132 | self.0.nth(n).map(|(_, v)| &**v) 133 | } 134 | 135 | fn last(self) -> Option 136 | where 137 | Self: Sized, 138 | { 139 | self.0.last().map(|(_, v)| &**v) 140 | } 141 | } 142 | 143 | impl DoubleEndedIterator for ListValuesIter<'_> { 144 | fn next_back(&mut self) -> Option { 145 | self.0.next_back().map(|(_, v)| &**v) 146 | } 147 | 148 | fn nth_back(&mut self, n: usize) -> Option { 149 | self.0.nth_back(n).map(|(_, v)| &**v) 150 | } 151 | } 152 | 153 | impl ExactSizeIterator for ListValuesIter<'_> {} 154 | 155 | /// Iterator over ungrouped [`List`] values. 156 | #[derive(Debug)] 157 | pub struct ListValuesIntoIter(IntoIter<(Tag, String)>); 158 | 159 | impl Iterator for ListValuesIntoIter { 160 | type Item = String; 161 | 162 | fn next(&mut self) -> Option { 163 | self.0.next().map(|(_, v)| v) 164 | } 165 | 166 | fn size_hint(&self) -> (usize, Option) { 167 | self.0.size_hint() 168 | } 169 | 170 | fn count(self) -> usize { 171 | self.0.count() 172 | } 173 | 174 | fn nth(&mut self, n: usize) -> Option { 175 | self.0.nth(n).map(|(_, v)| v) 176 | } 177 | 178 | fn last(mut self) -> Option 179 | where 180 | Self: Sized, 181 | { 182 | self.0.next_back().map(|(_, v)| v) 183 | } 184 | } 185 | 186 | impl DoubleEndedIterator for ListValuesIntoIter { 187 | fn next_back(&mut self) -> Option { 188 | self.0.next_back().map(|(_, v)| v) 189 | } 190 | 191 | fn nth_back(&mut self, n: usize) -> Option { 192 | self.0.nth_back(n).map(|(_, v)| v) 193 | } 194 | } 195 | 196 | impl ExactSizeIterator for ListValuesIntoIter {} 197 | 198 | #[cfg(test)] 199 | mod tests { 200 | use super::*; 201 | 202 | #[test] 203 | fn grouped_iterator() { 204 | let fields = vec![ 205 | (Tag::AlbumArtist, String::from("Foo")), 206 | (Tag::Album, String::from("Bar")), 207 | (Tag::Title, String::from("Title 1")), 208 | (Tag::Title, String::from("Title 2")), 209 | (Tag::Album, String::from("Quz")), 210 | (Tag::Title, String::from("Title 3")), 211 | (Tag::AlbumArtist, String::from("Asdf")), 212 | (Tag::Album, String::from("Qwert")), 213 | (Tag::Title, String::from("Title 4")), 214 | ]; 215 | 216 | let mut iter = GroupedListValuesIter { 217 | primary_tag: &Tag::Title, 218 | grouping_tags: &[Tag::Album, Tag::AlbumArtist], 219 | grouping_values: [""; 2], 220 | fields: fields.iter(), 221 | }; 222 | 223 | assert_eq!(iter.next(), Some(("Title 1", ["Bar", "Foo"]))); 224 | assert_eq!(iter.next(), Some(("Title 2", ["Bar", "Foo"]))); 225 | assert_eq!(iter.next(), Some(("Title 3", ["Quz", "Foo"]))); 226 | assert_eq!(iter.next(), Some(("Title 4", ["Qwert", "Asdf"]))); 227 | assert_eq!(iter.next(), None); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /mpd_protocol/src/parser.rs: -------------------------------------------------------------------------------- 1 | //! Parser for MPD responses. 2 | 3 | use std::{ 4 | str::{self, FromStr, from_utf8}, 5 | sync::Arc, 6 | }; 7 | 8 | use nom::{ 9 | IResult, Parser, 10 | branch::alt, 11 | bytes::streaming::{tag, take, take_until, take_while, take_while1}, 12 | character::streaming::{char, digit1, newline}, 13 | combinator::{cut, map, map_res, opt}, 14 | sequence::{delimited, separated_pair, terminated}, 15 | }; 16 | 17 | use crate::response::{Error, ResponseFieldCache}; 18 | 19 | #[derive(Debug, PartialEq, Eq)] 20 | pub(crate) enum ParsedComponent { 21 | EndOfFrame, 22 | EndOfResponse, 23 | Error(Error), 24 | Field { key: Arc, value: String }, 25 | BinaryField { data_length: usize }, 26 | } 27 | 28 | #[derive(Debug, PartialEq, Eq)] 29 | struct RawError<'raw> { 30 | code: u64, 31 | command_index: u64, 32 | current_command: Option<&'raw str>, 33 | message: &'raw str, 34 | } 35 | 36 | impl ParsedComponent { 37 | pub(crate) fn parse<'i>( 38 | i: &'i [u8], 39 | field_cache: &'_ mut ResponseFieldCache, 40 | ) -> IResult<&'i [u8], ParsedComponent> { 41 | alt(( 42 | map(tag("OK\n"), |_| ParsedComponent::EndOfResponse), 43 | map(tag("list_OK\n"), |_| ParsedComponent::EndOfFrame), 44 | map(error, |e| ParsedComponent::Error(e.into_owned_error())), 45 | map(binary_field, |bin| ParsedComponent::BinaryField { 46 | data_length: bin.len(), 47 | }), 48 | map(key_value_field, |(k, v)| ParsedComponent::Field { 49 | key: field_cache.insert(k), 50 | value: String::from(v), 51 | }), 52 | )) 53 | .parse(i) 54 | } 55 | } 56 | 57 | impl RawError<'_> { 58 | fn into_owned_error(self) -> Error { 59 | Error { 60 | code: self.code, 61 | command_index: self.command_index, 62 | current_command: self.current_command.map(Box::from), 63 | message: Box::from(self.message), 64 | } 65 | } 66 | } 67 | 68 | /// Recognize a server greeting, returning the protocol version. 69 | pub(crate) fn greeting(i: &[u8]) -> IResult<&[u8], &str> { 70 | delimited( 71 | tag("OK MPD "), 72 | map_res(take_while1(|c| c != b'\n'), from_utf8), 73 | newline, 74 | ) 75 | .parse(i) 76 | } 77 | 78 | /// Recognize and parse an unsigned ASCII-encoded number 79 | fn number(i: &[u8]) -> IResult<&[u8], O> { 80 | map_res(map_res(digit1, from_utf8), str::parse).parse(i) 81 | } 82 | 83 | /// Parse an error response. 84 | fn error(i: &[u8]) -> IResult<&[u8], RawError<'_>> { 85 | let (remaining, ((code, index), command, message)) = delimited( 86 | tag("ACK "), 87 | ( 88 | terminated(error_code_and_index, char(' ')), 89 | terminated(error_current_command, char(' ')), 90 | map_res(take_while(|b| b != b'\n'), from_utf8), 91 | ), 92 | newline, 93 | ) 94 | .parse(i)?; 95 | 96 | Ok(( 97 | remaining, 98 | RawError { 99 | code, 100 | message, 101 | command_index: index, 102 | current_command: command, 103 | }, 104 | )) 105 | } 106 | 107 | /// Recognize `[@]`. 108 | fn error_code_and_index(i: &[u8]) -> IResult<&[u8], (u64, u64)> { 109 | delimited( 110 | char('['), 111 | separated_pair(number, char('@'), number), 112 | char(']'), 113 | ) 114 | .parse(i) 115 | } 116 | 117 | /// Recognize the current command in an error, `None` if empty. 118 | fn error_current_command(i: &[u8]) -> IResult<&[u8], Option<&str>> { 119 | delimited( 120 | char('{'), 121 | opt(map_res( 122 | take_while1(|b: u8| b.is_ascii_alphabetic() || b == b'_'), 123 | from_utf8, 124 | )), 125 | char('}'), 126 | ) 127 | .parse(i) 128 | } 129 | 130 | /// Recognize a single key-value pair 131 | fn key_value_field(i: &[u8]) -> IResult<&[u8], (&str, &str)> { 132 | separated_pair( 133 | map_res( 134 | take_while1(|b: u8| b.is_ascii_alphabetic() || b == b'_' || b == b'-'), 135 | from_utf8, 136 | ), 137 | tag(": "), 138 | map_res(field_value, from_utf8), 139 | ) 140 | .parse(i) 141 | } 142 | 143 | fn field_value(i: &[u8]) -> IResult<&[u8], &[u8]> { 144 | let (i, value) = take_until("\n")(i)?; 145 | Ok((&i[1..], value)) 146 | } 147 | 148 | /// Recognize the header of a binary section 149 | fn binary_prefix(i: &[u8]) -> IResult<&[u8], usize> { 150 | delimited(tag("binary: "), number, newline).parse(i) 151 | } 152 | 153 | /// Recognize a binary field 154 | fn binary_field(i: &[u8]) -> IResult<&[u8], &[u8]> { 155 | let (i, length) = binary_prefix(i)?; 156 | 157 | cut(terminated(take(length), newline)).parse(i) 158 | } 159 | 160 | #[cfg(test)] 161 | mod test { 162 | use nom::{Err as NomErr, Needed}; 163 | 164 | use super::*; 165 | 166 | const EMPTY: &[u8] = &[]; 167 | 168 | #[test] 169 | fn greeting() { 170 | assert_eq!(super::greeting(b"OK MPD 0.21.11\n"), Ok((EMPTY, "0.21.11"))); 171 | assert!( 172 | super::greeting(b"OK MPD 0.21.11") 173 | .unwrap_err() 174 | .is_incomplete() 175 | ); 176 | } 177 | 178 | #[test] 179 | fn end_markers() { 180 | let keys = &mut ResponseFieldCache::new(); 181 | 182 | assert_eq!( 183 | ParsedComponent::parse(b"OK\n", keys), 184 | Ok((EMPTY, ParsedComponent::EndOfResponse)) 185 | ); 186 | 187 | assert_eq!( 188 | ParsedComponent::parse(b"OK", keys), 189 | Err(NomErr::Incomplete(Needed::new(1))) 190 | ); 191 | 192 | assert_eq!( 193 | ParsedComponent::parse(b"list_OK\n", keys), 194 | Ok((EMPTY, ParsedComponent::EndOfFrame)) 195 | ); 196 | 197 | assert_eq!( 198 | ParsedComponent::parse(b"list_OK", keys), 199 | Err(NomErr::Incomplete(Needed::new(1))) 200 | ); 201 | } 202 | 203 | #[test] 204 | fn parse_error() { 205 | let keys = &mut ResponseFieldCache::new(); 206 | let no_command = b"ACK [5@0] {} unknown command \"foo\"\n"; 207 | let with_command = b"ACK [2@0] {random} Boolean (0/1) expected: foo\n"; 208 | 209 | assert_eq!( 210 | ParsedComponent::parse(no_command, keys), 211 | Ok(( 212 | EMPTY, 213 | ParsedComponent::Error(Error { 214 | code: 5, 215 | command_index: 0, 216 | current_command: None, 217 | message: Box::from("unknown command \"foo\""), 218 | }) 219 | )) 220 | ); 221 | 222 | assert_eq!( 223 | ParsedComponent::parse(with_command, keys), 224 | Ok(( 225 | EMPTY, 226 | ParsedComponent::Error(Error { 227 | code: 2, 228 | command_index: 0, 229 | current_command: Some(Box::from("random")), 230 | message: Box::from("Boolean (0/1) expected: foo"), 231 | }), 232 | )) 233 | ); 234 | } 235 | 236 | #[test] 237 | fn field() { 238 | let keys = &mut ResponseFieldCache::new(); 239 | 240 | assert_eq!( 241 | ParsedComponent::parse(b"foo: OK\n", keys), 242 | Ok(( 243 | EMPTY, 244 | ParsedComponent::Field { 245 | key: Arc::from("foo"), 246 | value: String::from("OK"), 247 | } 248 | )) 249 | ); 250 | 251 | assert_eq!( 252 | ParsedComponent::parse(b"foo_bar: hello world list_OK\n", keys), 253 | Ok(( 254 | EMPTY, 255 | ParsedComponent::Field { 256 | key: Arc::from("foo_bar"), 257 | value: String::from("hello world list_OK"), 258 | } 259 | )) 260 | ); 261 | 262 | assert!( 263 | ParsedComponent::parse(b"asdf: fooo", keys) 264 | .unwrap_err() 265 | .is_incomplete() 266 | ); 267 | } 268 | 269 | #[test] 270 | fn binary_field() { 271 | let keys = &mut ResponseFieldCache::new(); 272 | 273 | assert_eq!( 274 | ParsedComponent::parse(b"binary: 6\nFOOBAR\n", keys), 275 | Ok((EMPTY, ParsedComponent::BinaryField { data_length: 6 })) 276 | ); 277 | 278 | assert_eq!( 279 | ParsedComponent::parse(b"binary: 6\nF", keys), 280 | Err(NomErr::Incomplete(Needed::new(5))) 281 | ); 282 | 283 | assert_eq!( 284 | ParsedComponent::parse(b"binary: 12\n", keys), 285 | Err(NomErr::Incomplete(Needed::new(12))) 286 | ); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /mpd_client/src/filter.rs: -------------------------------------------------------------------------------- 1 | //! Tools for constructing [filter expressions], as used by e.g. the [`find`] command. 2 | //! 3 | //! [`find`]: crate::commands::definitions::Find 4 | //! [filter expressions]: https://www.musicpd.org/doc/html/protocol.html#filters 5 | 6 | use std::{borrow::Cow, fmt::Write, ops::Not}; 7 | 8 | use bytes::{BufMut, BytesMut}; 9 | use mpd_protocol::command::Argument; 10 | 11 | use crate::tag::Tag; 12 | 13 | const TAG_IS_ABSENT: &str = ""; 14 | 15 | /// A [filter expression]. 16 | /// 17 | /// [filter expression]: https://www.musicpd.org/doc/html/protocol.html#filters 18 | #[derive(Clone, Debug, PartialEq, Eq)] 19 | pub struct Filter(FilterType); 20 | 21 | /// Internal filter variant type 22 | #[derive(Clone, Debug, PartialEq, Eq)] 23 | enum FilterType { 24 | Tag { 25 | tag: Tag, 26 | operator: Operator, 27 | value: String, 28 | }, 29 | Not(Box), 30 | And(Vec), 31 | } 32 | 33 | impl Filter { 34 | /// Create a filter which selects on the given `tag`, using the given `operator`, for the 35 | /// given `value`. 36 | /// 37 | /// See also [`Tag::any()`]. 38 | pub fn new>(tag: Tag, operator: Operator, value: V) -> Self { 39 | Self(FilterType::Tag { 40 | tag, 41 | operator, 42 | value: value.into(), 43 | }) 44 | } 45 | 46 | /// Create a filter which checks where the given `tag` is equal to the given `value`. 47 | /// 48 | /// Shorthand method that always checks for equality. 49 | pub fn tag>(tag: Tag, value: V) -> Self { 50 | Filter::new(tag, Operator::Equal, value) 51 | } 52 | 53 | /// Create a filter which checks for the existence of `tag` (with any value). 54 | pub fn tag_exists(tag: Tag) -> Self { 55 | Filter::new(tag, Operator::NotEqual, String::from(TAG_IS_ABSENT)) 56 | } 57 | 58 | /// Create a filter which checks for the absence of `tag`. 59 | pub fn tag_absent(tag: Tag) -> Self { 60 | Filter::new(tag, Operator::Equal, String::from(TAG_IS_ABSENT)) 61 | } 62 | 63 | /// Negate the filter. 64 | /// 65 | /// You can also use the negation operator (`!`) if you prefer to negate at the start of an 66 | /// expression. 67 | pub fn negate(mut self) -> Self { 68 | self.0 = FilterType::Not(Box::new(self.0)); 69 | self 70 | } 71 | 72 | /// Chain the given filter onto this one with an `AND`. 73 | /// 74 | /// Automatically flattens nested `AND` conditions. 75 | pub fn and(self, other: Self) -> Self { 76 | let mut out = match self.0 { 77 | FilterType::And(inner) => inner, 78 | condition => { 79 | let mut out = Vec::with_capacity(2); 80 | out.push(condition); 81 | out 82 | } 83 | }; 84 | 85 | match other.0 { 86 | FilterType::And(inner) => { 87 | for c in inner { 88 | out.push(c); 89 | } 90 | } 91 | condition => out.push(condition), 92 | } 93 | 94 | Self(FilterType::And(out)) 95 | } 96 | 97 | fn render(&self, buf: &mut BytesMut) { 98 | buf.put_u8(b'"'); 99 | self.0.render(buf); 100 | buf.put_u8(b'"'); 101 | } 102 | } 103 | 104 | impl Argument for Filter { 105 | fn render(&self, buf: &mut BytesMut) { 106 | self.render(buf); 107 | } 108 | } 109 | 110 | impl Not for Filter { 111 | type Output = Self; 112 | 113 | fn not(self) -> Self::Output { 114 | self.negate() 115 | } 116 | } 117 | 118 | impl FilterType { 119 | fn render(&self, buf: &mut BytesMut) { 120 | match self { 121 | FilterType::Tag { 122 | tag, 123 | operator, 124 | value, 125 | } => { 126 | write!( 127 | buf, 128 | r#"({} {} \"{}\")"#, 129 | tag.as_str(), 130 | operator.as_str(), 131 | escape_filter_value(value) 132 | ) 133 | .unwrap(); 134 | } 135 | FilterType::Not(inner) => { 136 | buf.put_slice(b"(!"); 137 | inner.render(buf); 138 | buf.put_u8(b')'); 139 | } 140 | FilterType::And(inner) => { 141 | assert!(inner.len() >= 2); 142 | 143 | buf.put_u8(b'('); 144 | 145 | let mut first = true; 146 | for filter in inner { 147 | if first { 148 | first = false; 149 | } else { 150 | buf.put_slice(b" AND "); 151 | } 152 | 153 | filter.render(buf); 154 | } 155 | 156 | buf.put_u8(b')'); 157 | } 158 | } 159 | /* 160 | match self { 161 | FilterType::And(inner) => { 162 | assert!(inner.len() >= 2); 163 | let inner = inner.iter().map(|s| s.render()).collect::>(); 164 | 165 | // Wrapping parens 166 | let mut capacity = 2; 167 | // Lengths of the actual commands 168 | capacity += inner.iter().map(|s| s.len()).sum::(); 169 | // " AND " join operators 170 | capacity += (inner.len() - 1) * 5; 171 | 172 | let mut out = String::with_capacity(capacity); 173 | 174 | out.push('('); 175 | 176 | let mut first = true; 177 | for filter in inner { 178 | if first { 179 | first = false; 180 | } else { 181 | out.push_str(" AND "); 182 | } 183 | 184 | out.push_str(&filter); 185 | } 186 | 187 | out.push(')'); 188 | 189 | out 190 | } 191 | } 192 | */ 193 | } 194 | } 195 | 196 | /// Operators which can be used in filter expressions. 197 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 198 | pub enum Operator { 199 | /// Equality (`==`) 200 | Equal, 201 | /// Negated equality (`!=`) 202 | NotEqual, 203 | /// Substring matching (`contains`) 204 | Contain, 205 | /// Perl-style regex matching (`=~`) 206 | Match, 207 | /// Negated Perl-style regex matching (`!~`) 208 | NotMatch, 209 | } 210 | 211 | impl Operator { 212 | fn as_str(self) -> &'static str { 213 | match self { 214 | Operator::Equal => "==", 215 | Operator::NotEqual => "!=", 216 | Operator::Contain => "contains", 217 | Operator::Match => "=~", 218 | Operator::NotMatch => "!~", 219 | } 220 | } 221 | } 222 | 223 | fn escape_filter_value(value: &str) -> Cow<'_, str> { 224 | if value.contains('"') { 225 | Cow::Owned(value.replace('"', r#"\\""#)) 226 | } else { 227 | Cow::Borrowed(value) 228 | } 229 | } 230 | 231 | #[cfg(test)] 232 | mod tests { 233 | use super::*; 234 | 235 | #[test] 236 | fn filter_escaping() { 237 | let mut buf = BytesMut::new(); 238 | 239 | Filter::tag(Tag::Artist, "foo").render(&mut buf); 240 | assert_eq!(buf, r#""(Artist == \"foo\")""#); 241 | buf.clear(); 242 | 243 | Filter::tag(Tag::Artist, "foo\'s bar\"").render(&mut buf); 244 | assert_eq!(buf, r#""(Artist == \"foo's bar\\"\")""#); 245 | buf.clear(); 246 | } 247 | 248 | #[test] 249 | fn filter_other_operator() { 250 | let mut buf = BytesMut::new(); 251 | Filter::new(Tag::Artist, Operator::Contain, "mep mep").render(&mut buf); 252 | assert_eq!(buf, r#""(Artist contains \"mep mep\")""#); 253 | } 254 | 255 | #[test] 256 | fn filter_not() { 257 | let mut buf = BytesMut::new(); 258 | Filter::tag(Tag::Artist, "hello").negate().render(&mut buf); 259 | assert_eq!(buf, r#""(!(Artist == \"hello\"))""#); 260 | } 261 | 262 | #[test] 263 | fn filter_and() { 264 | let mut buf = BytesMut::new(); 265 | 266 | let first = Filter::tag(Tag::Artist, "hello"); 267 | let second = Filter::tag(Tag::Album, "world"); 268 | 269 | first.and(second).render(&mut buf); 270 | assert_eq!(buf, r#""((Artist == \"hello\") AND (Album == \"world\"))""#); 271 | } 272 | 273 | #[test] 274 | fn filter_and_multiple() { 275 | let mut buf = BytesMut::new(); 276 | 277 | let first = Filter::tag(Tag::Artist, "hello"); 278 | let second = Filter::tag(Tag::Album, "world"); 279 | let third = Filter::tag(Tag::Title, "foo"); 280 | 281 | first.and(second).and(third).render(&mut buf); 282 | assert_eq!( 283 | buf, 284 | r#""((Artist == \"hello\") AND (Album == \"world\") AND (Title == \"foo\"))""# 285 | ); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /mpd_client/src/tag.rs: -------------------------------------------------------------------------------- 1 | //! Metadata tags. 2 | 3 | use std::{ 4 | borrow::Cow, 5 | error::Error, 6 | fmt, 7 | hash::{Hash, Hasher}, 8 | }; 9 | 10 | use bytes::{BufMut, BytesMut}; 11 | use mpd_protocol::command::Argument; 12 | 13 | /// Tags which can be set on a [`Song`]. 14 | /// 15 | /// [MusicBrainz] tags are named differently from how they appear in the protocol to better 16 | /// reflect their actual purpose. 17 | /// 18 | /// # Tag validity 19 | /// 20 | /// **Manually** constructing a tag with the `Other` variant may result in protocols errors if the 21 | /// tag is invalid. Use the `TryFrom` implementation for checked conversion. 22 | /// 23 | /// # Unknown tags 24 | /// 25 | /// When parsing or constructing responses, tags not recognized by this type will be stored as they 26 | /// are encountered using the `Other` variant. Additionally the enum is marked as non-exhaustive, 27 | /// so additional tags may be added without breaking compatibility. 28 | /// 29 | /// The equality is checked using the string representation, so `Other` variants are 30 | /// forward-compatible with new variants being added. 31 | /// 32 | /// [`Song`]: crate::responses::Song 33 | /// [MusicBrainz]: https://musicbrainz.org 34 | #[derive(Clone, Debug)] 35 | #[allow(missing_docs)] 36 | #[non_exhaustive] 37 | pub enum Tag { 38 | Album, 39 | AlbumArtist, 40 | AlbumArtistSort, 41 | AlbumSort, 42 | Artist, 43 | ArtistSort, 44 | Comment, 45 | Composer, 46 | ComposerSort, 47 | Conductor, 48 | Date, 49 | Disc, 50 | Ensemble, 51 | Genre, 52 | Grouping, 53 | Label, 54 | Location, 55 | Movement, 56 | MovementNumber, 57 | MusicBrainzArtistId, 58 | MusicBrainzRecordingId, 59 | MusicBrainzReleaseArtistId, 60 | MusicBrainzReleaseId, 61 | MusicBrainzTrackId, 62 | MusicBrainzWorkId, 63 | Name, 64 | OriginalDate, 65 | Performer, 66 | Title, 67 | Track, 68 | Work, 69 | /// Catch-all variant that contains the raw tag string when it doesn't match any other 70 | /// variants, but is valid. 71 | Other(Box), 72 | } 73 | 74 | impl Tag { 75 | /// Creates a tag for [filtering] which will match *any* tag. 76 | /// 77 | /// [filtering]: crate::filter::Filter 78 | pub fn any() -> Self { 79 | Self::Other("any".into()) 80 | } 81 | 82 | pub(crate) fn as_str(&self) -> Cow<'static, str> { 83 | Cow::Borrowed(match self { 84 | Tag::Other(raw) => return Cow::Owned(raw.to_string()), 85 | Tag::Album => "Album", 86 | Tag::AlbumArtist => "AlbumArtist", 87 | Tag::AlbumArtistSort => "AlbumArtistSort", 88 | Tag::AlbumSort => "AlbumSort", 89 | Tag::Artist => "Artist", 90 | Tag::ArtistSort => "ArtistSort", 91 | Tag::Comment => "Comment", 92 | Tag::Composer => "Composer", 93 | Tag::ComposerSort => "ComposerSort", 94 | Tag::Conductor => "Conductor", 95 | Tag::Date => "Date", 96 | Tag::Disc => "Disc", 97 | Tag::Ensemble => "Ensemble", 98 | Tag::Genre => "Genre", 99 | Tag::Grouping => "Grouping", 100 | Tag::Label => "Label", 101 | Tag::Location => "Location", 102 | Tag::Movement => "Movement", 103 | Tag::MovementNumber => "MovementNumber", 104 | Tag::MusicBrainzArtistId => "MUSICBRAINZ_ARTISTID", 105 | Tag::MusicBrainzRecordingId => "MUSICBRAINZ_TRACKID", 106 | Tag::MusicBrainzReleaseArtistId => "MUSICBRAINZ_ALBUMARTISTID", 107 | Tag::MusicBrainzReleaseId => "MUSICBRAINZ_ALBUMID", 108 | Tag::MusicBrainzTrackId => "MUSICBRAINZ_RELEASETRACKID", 109 | Tag::MusicBrainzWorkId => "MUSICBRAINZ_WORKID", 110 | Tag::Name => "Name", 111 | Tag::OriginalDate => "OriginalDate", 112 | Tag::Performer => "Performer", 113 | Tag::Title => "Title", 114 | Tag::Track => "Track", 115 | Tag::Work => "Work", 116 | }) 117 | } 118 | } 119 | 120 | macro_rules! match_ignore_case { 121 | ($raw:ident, $($pattern:literal => $result:expr),+) => { 122 | $( 123 | if $raw.eq_ignore_ascii_case($pattern) { 124 | return Ok($result); 125 | } 126 | )+ 127 | }; 128 | } 129 | 130 | impl<'a> TryFrom<&'a str> for Tag { 131 | type Error = TagError; 132 | 133 | fn try_from(raw: &'a str) -> Result { 134 | if raw.is_empty() { 135 | return Err(TagError::Empty); 136 | } else if let Some((pos, chr)) = raw 137 | .char_indices() 138 | .find(|&(_, ch)| !(ch.is_ascii_alphabetic() || ch == '_' || ch == '-')) 139 | { 140 | return Err(TagError::InvalidCharacter { chr, pos }); 141 | } 142 | 143 | match_ignore_case! { 144 | raw, 145 | "Album" => Self::Album, 146 | "AlbumArtist" => Self::AlbumArtist, 147 | "AlbumArtistSort" => Self::AlbumArtistSort, 148 | "AlbumSort" => Self::AlbumSort, 149 | "Artist" => Self::Artist, 150 | "ArtistSort" => Self::ArtistSort, 151 | "Comment" => Self::Comment, 152 | "Composer" => Self::Composer, 153 | "ComposerSort" => Self::ComposerSort, 154 | "Conductor" => Self::Conductor, 155 | "Date" => Self::Date, 156 | "Disc" => Self::Disc, 157 | "Ensemble" => Self::Ensemble, 158 | "Genre" => Self::Genre, 159 | "Grouping" => Self::Grouping, 160 | "Label" => Self::Label, 161 | "Location" => Self::Location, 162 | "Movement" => Self::Movement, 163 | "MovementNumber" => Self::MovementNumber, 164 | "MUSICBRAINZ_ALBUMARTISTID" => Self::MusicBrainzReleaseArtistId, 165 | "MUSICBRAINZ_ALBUMID" => Self::MusicBrainzReleaseId, 166 | "MUSICBRAINZ_ARTISTID" => Self::MusicBrainzArtistId, 167 | "MUSICBRAINZ_RELEASETRACKID" => Self::MusicBrainzTrackId, 168 | "MUSICBRAINZ_TRACKID" => Self::MusicBrainzRecordingId, 169 | "MUSICBRAINZ_WORKID" => Self::MusicBrainzWorkId, 170 | "Name" => Self::Name, 171 | "OriginalDate" => Self::OriginalDate, 172 | "Performer" => Self::Performer, 173 | "Title" => Self::Title, 174 | "Track" => Self::Track, 175 | "Work" => Self::Work 176 | } 177 | 178 | Ok(Self::Other(raw.into())) 179 | } 180 | } 181 | 182 | impl PartialEq for Tag { 183 | fn eq(&self, other: &Tag) -> bool { 184 | self.as_str() == other.as_str() 185 | } 186 | } 187 | 188 | impl Eq for Tag {} 189 | 190 | impl<'a> PartialEq<&'a str> for Tag { 191 | fn eq(&self, other: &&'a str) -> bool { 192 | self.as_str() == *other 193 | } 194 | } 195 | 196 | impl PartialOrd for Tag { 197 | fn partial_cmp(&self, other: &Tag) -> Option { 198 | Some(self.cmp(other)) 199 | } 200 | } 201 | 202 | impl Ord for Tag { 203 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 204 | self.as_str().cmp(&other.as_str()) 205 | } 206 | } 207 | 208 | impl Hash for Tag { 209 | fn hash(&self, state: &mut H) { 210 | self.as_str().hash(state); 211 | } 212 | } 213 | 214 | impl Argument for Tag { 215 | fn render(&self, buf: &mut BytesMut) { 216 | buf.put_slice(self.as_str().as_bytes()); 217 | } 218 | } 219 | 220 | /// Errors that may occur when attempting to create a [`Tag`]. 221 | #[derive(Clone, Debug, PartialEq, Eq)] 222 | pub enum TagError { 223 | /// The raw tag was empty. 224 | Empty, 225 | /// The raw tag contained an invalid character. 226 | InvalidCharacter { 227 | /// The character. 228 | chr: char, 229 | /// Byte position of `chr`. 230 | pos: usize, 231 | }, 232 | } 233 | 234 | impl fmt::Display for TagError { 235 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 236 | match self { 237 | Self::Empty => write!(f, "empty tag"), 238 | Self::InvalidCharacter { chr, pos } => { 239 | write!(f, "invalid character {chr:?} at index {pos}") 240 | } 241 | } 242 | } 243 | } 244 | 245 | impl Error for TagError {} 246 | 247 | #[cfg(test)] 248 | mod tests { 249 | use super::*; 250 | 251 | #[test] 252 | fn try_from() { 253 | assert_eq!(Tag::try_from("Artist"), Ok(Tag::Artist)); 254 | 255 | // case-insensitive 256 | assert_eq!(Tag::try_from("artist"), Ok(Tag::Artist)); 257 | 258 | // unrecognized but valid tag 259 | assert_eq!(Tag::try_from("foo"), Ok(Tag::Other(Box::from("foo")))); 260 | } 261 | 262 | #[test] 263 | fn try_from_error() { 264 | assert_eq!(Tag::try_from(""), Err(TagError::Empty)); 265 | assert_eq!( 266 | Tag::try_from("foo bar"), 267 | Err(TagError::InvalidCharacter { chr: ' ', pos: 3 }) 268 | ); 269 | } 270 | 271 | #[test] 272 | fn as_arg() { 273 | assert_eq!(Tag::Album.as_str(), "Album"); 274 | assert_eq!(Tag::Other(Box::from("foo")).as_str(), "foo"); 275 | } 276 | 277 | #[test] 278 | fn equality() { 279 | assert_eq!(Tag::Album, Tag::Other(Box::from("Album"))); 280 | assert_eq!( 281 | Tag::Other(Box::from("Album")), 282 | Tag::Other(Box::from("Album")) 283 | ); 284 | assert_ne!(Tag::Other(Box::from("Foo")), Tag::Other(Box::from("Bar"))); 285 | 286 | assert_eq!(Tag::Artist, "Artist"); 287 | assert_eq!(Tag::Other(Box::from("Foo")), "Foo"); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /mpd_client/src/client/connection.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, time::Duration}; 2 | 3 | use mpd_protocol::{ 4 | AsyncConnection, MpdProtocolError, 5 | command::{Command as RawCommand, CommandList as RawCommandList}, 6 | response::Response, 7 | }; 8 | use tokio::{ 9 | io::{AsyncRead, AsyncWrite}, 10 | sync::mpsc::{UnboundedReceiver, UnboundedSender}, 11 | time::timeout, 12 | }; 13 | use tracing::{Instrument, Level, debug, error, span, trace}; 14 | 15 | use crate::client::{CommandResponder, ConnectionError, ConnectionEvent, Subsystem}; 16 | 17 | struct State { 18 | loop_state: LoopState, 19 | connection: AsyncConnection, 20 | commands: UnboundedReceiver<(RawCommandList, CommandResponder)>, 21 | events: UnboundedSender, 22 | } 23 | 24 | enum LoopState { 25 | Idling, 26 | WaitingForCommandReply(CommandResponder), 27 | } 28 | 29 | impl fmt::Debug for LoopState { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | // avoid Debug-printing the noisy internals of the contained channel type 32 | match self { 33 | LoopState::Idling => write!(f, "Idling"), 34 | LoopState::WaitingForCommandReply(_) => write!(f, "WaitingForCommandReply"), 35 | } 36 | } 37 | } 38 | 39 | fn idle() -> RawCommand { 40 | RawCommand::new("idle") 41 | } 42 | 43 | fn cancel_idle() -> RawCommand { 44 | RawCommand::new("noidle") 45 | } 46 | 47 | pub(super) async fn run_loop( 48 | mut connection: AsyncConnection, 49 | commands: UnboundedReceiver<(RawCommandList, CommandResponder)>, 50 | events: UnboundedSender, 51 | ) where 52 | C: AsyncRead + AsyncWrite + Unpin, 53 | { 54 | trace!("sending initial idle command"); 55 | if let Err(e) = connection.send(idle()).await { 56 | error!(error = ?e, "failed to send initial idle command"); 57 | let _ = events.send(ConnectionEvent::ConnectionClosed(e.into())); 58 | return; 59 | } 60 | 61 | let mut state = State { 62 | loop_state: LoopState::Idling, 63 | connection, 64 | commands, 65 | events, 66 | }; 67 | 68 | trace!("entering run loop"); 69 | 70 | loop { 71 | let span = span!(Level::TRACE, "iteration", state = ?state.loop_state); 72 | 73 | match run_loop_iteration(state).instrument(span).await { 74 | Ok(new_state) => state = new_state, 75 | Err(()) => break, 76 | } 77 | } 78 | 79 | trace!("exited run_loop"); 80 | } 81 | 82 | /// Time to wait for another command to send before starting the idle loop. 83 | const NEXT_COMMAND_IDLE_TIMEOUT: Duration = Duration::from_millis(100); 84 | 85 | async fn run_loop_iteration(mut state: State) -> Result, ()> 86 | where 87 | C: AsyncRead + AsyncWrite + Unpin, 88 | { 89 | match state.loop_state { 90 | LoopState::Idling => { 91 | // We are idling (the last command sent to the server was an IDLE). 92 | 93 | // Wait for either a command to send or a message from the server, which would be a 94 | // state change notification. 95 | tokio::select! { 96 | response = state.connection.receive() => { 97 | handle_idle_response(&mut state, response).await?; 98 | } 99 | command = state.commands.recv() => { 100 | handle_command(&mut state, command).await?; 101 | } 102 | } 103 | } 104 | LoopState::WaitingForCommandReply(responder) => { 105 | // We're waiting for the response to the command associated with `responder`. 106 | 107 | let response = state.connection.receive().await.transpose().ok_or(())?; 108 | trace!("response to command received"); 109 | 110 | let _ = responder.send(response.map_err(Into::into)); 111 | 112 | let next_command = timeout(NEXT_COMMAND_IDLE_TIMEOUT, state.commands.recv()); 113 | 114 | // See if we can immediately send the next command 115 | match next_command.await { 116 | Ok(Some((command, responder))) => { 117 | trace!(?command, "next command immediately available"); 118 | match state.connection.send_list(command).await { 119 | Ok(_) => state.loop_state = LoopState::WaitingForCommandReply(responder), 120 | Err(e) => { 121 | error!(error = ?e, "failed to send command"); 122 | let _ = responder.send(Err(e.into())); 123 | return Err(()); 124 | } 125 | } 126 | } 127 | Ok(None) => return Err(()), 128 | Err(_) => { 129 | trace!("reached next command timeout, idling"); 130 | 131 | // Start idling again 132 | state.loop_state = LoopState::Idling; 133 | if let Err(e) = state.connection.send(idle()).await { 134 | error!(error = ?e, "failed to start idling after receiving command response"); 135 | let _ = state 136 | .events 137 | .send(ConnectionEvent::ConnectionClosed(e.into())); 138 | return Err(()); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | Ok(state) 146 | } 147 | 148 | async fn handle_command( 149 | state: &mut State, 150 | command: Option<(RawCommandList, CommandResponder)>, 151 | ) -> Result<(), ()> 152 | where 153 | C: AsyncRead + AsyncWrite + Unpin, 154 | { 155 | let (command, responder) = command.ok_or(())?; 156 | trace!(?command, "command received"); 157 | 158 | // Cancel currently ongoing idle 159 | if let Err(e) = state.connection.send(cancel_idle()).await { 160 | error!(error = ?e, "failed to cancel idle prior to sending command"); 161 | let _ = responder.send(Err(e.into())); 162 | return Err(()); 163 | } 164 | 165 | // Receive the response to the cancellation 166 | match state.connection.receive().await { 167 | Ok(None) => return Err(()), 168 | Ok(Some(res)) => match res.into_single_frame() { 169 | Ok(f) => { 170 | if let Some(subsystem) = Subsystem::from_frame(f) { 171 | debug!(?subsystem, "state change"); 172 | let _ = state 173 | .events 174 | .send(ConnectionEvent::SubsystemChange(subsystem)); 175 | } 176 | } 177 | Err(e) => { 178 | error!( 179 | code = e.code, 180 | message = e.message, 181 | "idle cancel returned an error" 182 | ); 183 | let _ = state.events.send(ConnectionEvent::ConnectionClosed( 184 | ConnectionError::InvalidResponse, 185 | )); 186 | return Err(()); 187 | } 188 | }, 189 | Err(e) => { 190 | error!(error = ?e, "state change error prior to sending command"); 191 | let _ = responder.send(Err(e.into())); 192 | return Err(()); 193 | } 194 | } 195 | 196 | // Actually send the command. This sets the state for the next loop 197 | // iteration. 198 | match state.connection.send_list(command).await { 199 | Ok(_) => state.loop_state = LoopState::WaitingForCommandReply(responder), 200 | Err(e) => { 201 | error!(error = ?e, "failed to send command"); 202 | let _ = responder.send(Err(e.into())); 203 | return Err(()); 204 | } 205 | } 206 | 207 | trace!("command sent successfully"); 208 | Ok(()) 209 | } 210 | 211 | async fn handle_idle_response( 212 | state: &mut State, 213 | response: Result, MpdProtocolError>, 214 | ) -> Result<(), ()> 215 | where 216 | C: AsyncRead + AsyncWrite + Unpin, 217 | { 218 | trace!("handling idle response"); 219 | 220 | match response { 221 | Ok(Some(res)) => { 222 | match res.into_single_frame() { 223 | Ok(f) => { 224 | if let Some(subsystem) = Subsystem::from_frame(f) { 225 | debug!(?subsystem, "state change"); 226 | let _ = state 227 | .events 228 | .send(ConnectionEvent::SubsystemChange(subsystem)); 229 | } 230 | } 231 | Err(e) => { 232 | error!(code = e.code, message = e.message, "idle returned an error"); 233 | let _ = state.events.send(ConnectionEvent::ConnectionClosed( 234 | ConnectionError::InvalidResponse, 235 | )); 236 | return Err(()); 237 | } 238 | } 239 | 240 | if let Err(e) = state.connection.send(idle()).await { 241 | error!(error = ?e, "failed to start idling after state change"); 242 | let _ = state 243 | .events 244 | .send(ConnectionEvent::ConnectionClosed(e.into())); 245 | return Err(()); 246 | } 247 | } 248 | Ok(None) => return Err(()), // The connection was closed 249 | Err(e) => { 250 | error!(error = ?e, "state change error"); 251 | let _ = state 252 | .events 253 | .send(ConnectionEvent::ConnectionClosed(e.into())); 254 | return Err(()); 255 | } 256 | } 257 | 258 | Ok(()) 259 | } 260 | -------------------------------------------------------------------------------- /mpd_protocol/src/response/frame.rs: -------------------------------------------------------------------------------- 1 | //! A successful response to a command. 2 | 3 | use std::{fmt, iter::FusedIterator, slice, sync::Arc, vec}; 4 | 5 | use bytes::BytesMut; 6 | 7 | /// A successful response to a command. 8 | /// 9 | /// Consists of zero or more key-value pairs, where the keys are not unique, and optionally a 10 | /// single binary blob. 11 | #[derive(Clone, PartialEq, Eq)] 12 | pub struct Frame { 13 | pub(super) fields: FieldsContainer, 14 | pub(super) binary: Option, 15 | } 16 | 17 | impl Frame { 18 | /// Create an empty frame (0 key-value pairs). 19 | pub(crate) fn empty() -> Self { 20 | Self { 21 | fields: FieldsContainer(Vec::new()), 22 | binary: None, 23 | } 24 | } 25 | 26 | /// Get the number of key-value pairs in this response frame. 27 | pub fn fields_len(&self) -> usize { 28 | self.fields().count() 29 | } 30 | 31 | /// Returns `true` if the frame is entirely empty, i.e. contains 0 key-value pairs and no 32 | /// binary blob. 33 | pub fn is_empty(&self) -> bool { 34 | self.fields_len() == 0 && !self.has_binary() 35 | } 36 | 37 | /// Returns `true` if the frame contains a binary blob. 38 | /// 39 | /// If the binary blob has been removed using [`Frame::take_binary`], this will return `false`. 40 | pub fn has_binary(&self) -> bool { 41 | self.binary.is_some() 42 | } 43 | 44 | /// Returns an iterator over all key-value pairs in this frame, in the order they appear in the 45 | /// response. 46 | /// 47 | /// If keys have been removed using [`Frame::get`], they will not appear. 48 | pub fn fields(&self) -> Fields<'_> { 49 | Fields(self.fields.0.iter()) 50 | } 51 | 52 | /// Find the first key-value pair with the given key, and return a reference to its value. 53 | /// 54 | /// The key is case-sensitive. 55 | pub fn find(&self, key: K) -> Option<&str> 56 | where 57 | K: AsRef, 58 | { 59 | self.fields() 60 | .find_map(|(k, v)| if k == key.as_ref() { Some(v) } else { None }) 61 | } 62 | 63 | /// Returns a reference to the binary blob in this frame, if there is one. 64 | /// 65 | /// If the binary blob has been removed using [`Frame::take_binary`], this will return `None`. 66 | pub fn binary(&self) -> Option<&[u8]> { 67 | self.binary.as_deref() 68 | } 69 | 70 | /// Find the first key-value pair with the given key, and return its value. 71 | /// 72 | /// The key is case-sensitive. This removes it from the list of fields in this frame. 73 | pub fn get(&mut self, key: K) -> Option 74 | where 75 | K: AsRef, 76 | { 77 | self.fields.0.iter_mut().find_map(|field| { 78 | let k = match field.as_ref() { 79 | None => return None, 80 | Some((k, _)) => k, 81 | }; 82 | 83 | if k.as_ref() == key.as_ref() { 84 | field.take().map(|(_, v)| v) 85 | } else { 86 | None 87 | } 88 | }) 89 | } 90 | 91 | /// Get the binary blob contained in this frame, if present. 92 | /// 93 | /// This will remove it from the frame, future calls to this method will return `None`. 94 | pub fn take_binary(&mut self) -> Option { 95 | self.binary.take() 96 | } 97 | } 98 | 99 | impl fmt::Debug for Frame { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | write!(f, "Frame(")?; 102 | 103 | let alternate = f.alternate(); 104 | let mut map = f.debug_map(); 105 | let mut map = map.entries(self.fields()); 106 | 107 | if let Some(b) = &self.binary { 108 | map = if alternate { 109 | map.entry(&"debug", &b) 110 | } else { 111 | map.entry(&"debug", &format!("<{} bytes>", b.len())) 112 | }; 113 | } 114 | 115 | map.finish()?; 116 | 117 | write!(f, ")") 118 | } 119 | } 120 | 121 | #[derive(Clone, PartialEq, Eq)] 122 | pub(super) struct FieldsContainer(Vec, String)>>); 123 | 124 | impl FieldsContainer { 125 | pub(super) fn push_field(&mut self, key: Arc, value: String) { 126 | self.0.push(Some((key, value))); 127 | } 128 | } 129 | 130 | /// Iterator returned by the [`Frame::fields`] method. 131 | #[derive(Debug)] 132 | pub struct Fields<'a>(slice::Iter<'a, Option<(Arc, String)>>); 133 | 134 | impl<'a> Iterator for Fields<'a> { 135 | type Item = (&'a str, &'a str); 136 | 137 | fn next(&mut self) -> Option { 138 | match self.0.next() { 139 | None => None, 140 | Some(None) => self.next(), 141 | Some(Some((k, v))) => Some((k.as_ref(), v.as_ref())), 142 | } 143 | } 144 | } 145 | 146 | impl DoubleEndedIterator for Fields<'_> { 147 | fn next_back(&mut self) -> Option { 148 | match self.0.next_back() { 149 | None => None, 150 | Some(None) => self.next_back(), 151 | Some(Some((k, v))) => Some((k.as_ref(), v.as_ref())), 152 | } 153 | } 154 | } 155 | 156 | impl FusedIterator for Fields<'_> {} 157 | 158 | impl<'a> IntoIterator for &'a Frame { 159 | type Item = (&'a str, &'a str); 160 | type IntoIter = Fields<'a>; 161 | 162 | fn into_iter(self) -> Self::IntoIter { 163 | self.fields() 164 | } 165 | } 166 | 167 | /// Iterator returned by the [`IntoIterator`] implementation on [`Frame`]. 168 | #[derive(Debug)] 169 | pub struct IntoIter { 170 | iter: vec::IntoIter, String)>>, 171 | binary: Option, 172 | } 173 | 174 | impl IntoIter { 175 | /// Get the binary blob contained in this frame, if present. 176 | /// 177 | /// This will remove it from the frame, future calls to this method will return `None`. 178 | pub fn take_binary(&mut self) -> Option { 179 | self.binary.take() 180 | } 181 | } 182 | 183 | impl Iterator for IntoIter { 184 | type Item = (Arc, String); 185 | 186 | fn next(&mut self) -> Option { 187 | match self.iter.next() { 188 | None => None, 189 | Some(None) => self.next(), 190 | Some(value) => value, 191 | } 192 | } 193 | } 194 | 195 | impl DoubleEndedIterator for IntoIter { 196 | fn next_back(&mut self) -> Option { 197 | match self.iter.next_back() { 198 | None => None, 199 | Some(None) => self.next_back(), 200 | Some(value) => value, 201 | } 202 | } 203 | } 204 | 205 | impl FusedIterator for IntoIter {} 206 | 207 | impl IntoIterator for Frame { 208 | type Item = (Arc, String); 209 | type IntoIter = IntoIter; 210 | 211 | fn into_iter(self) -> Self::IntoIter { 212 | IntoIter { 213 | iter: self.fields.0.into_iter(), 214 | binary: self.binary, 215 | } 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use super::*; 222 | 223 | #[test] 224 | fn length() { 225 | let frame = Frame::empty(); 226 | assert_eq!(frame.fields_len(), 0); 227 | 228 | let frame = Frame { 229 | fields: FieldsContainer(vec![ 230 | Some((Arc::from("hello"), String::from("world"))), 231 | Some((Arc::from("foo"), String::from("bar"))), 232 | ]), 233 | binary: Some(BytesMut::from("hello world")), 234 | }; 235 | 236 | assert_eq!(frame.fields_len(), 2); 237 | } 238 | 239 | #[test] 240 | fn binary() { 241 | let mut frame = Frame { 242 | fields: FieldsContainer(Vec::new()), 243 | binary: Some(BytesMut::from("hello world")), 244 | }; 245 | 246 | assert!(frame.has_binary()); 247 | assert!(!frame.is_empty()); 248 | assert_eq!(frame.take_binary(), Some(BytesMut::from("hello world"))); 249 | assert_eq!(frame.take_binary(), None); 250 | assert!(!frame.has_binary()); 251 | } 252 | 253 | #[test] 254 | fn accessors() { 255 | let mut frame = Frame { 256 | fields: FieldsContainer(vec![ 257 | Some((Arc::from("hello"), String::from("first value"))), 258 | Some((Arc::from("foo"), String::from("bar"))), 259 | Some((Arc::from("hello"), String::from("second value"))), 260 | ]), 261 | binary: None, 262 | }; 263 | 264 | assert_eq!(frame.find("hello"), Some("first value")); 265 | assert_eq!(frame.find("404"), None); 266 | assert_eq!(frame.find("HELLO"), None); // case-sensitive 267 | 268 | assert_eq!(frame.get("hello"), Some(String::from("first value"))); 269 | assert_eq!(frame.get("hello"), Some(String::from("second value"))); 270 | assert_eq!(frame.get("hello"), None); 271 | assert_eq!(frame.get("Foo"), None); // case-sensitive 272 | } 273 | 274 | #[test] 275 | fn iter() { 276 | let frame = Frame { 277 | fields: FieldsContainer(vec![ 278 | Some((Arc::from("hello"), String::from("first value"))), 279 | Some((Arc::from("foo"), String::from("bar"))), 280 | Some((Arc::from("hello"), String::from("second value"))), 281 | ]), 282 | binary: None, 283 | }; 284 | let mut iter = frame.fields(); 285 | 286 | assert_eq!(iter.next(), Some(("hello", "first value"))); 287 | assert_eq!(iter.next(), Some(("foo", "bar"))); 288 | assert_eq!(iter.next(), Some(("hello", "second value"))); 289 | 290 | assert_eq!(iter.next(), None); 291 | assert_eq!(iter.next(), None); 292 | } 293 | 294 | #[test] 295 | fn owned_iter() { 296 | let frame = Frame { 297 | fields: FieldsContainer(vec![ 298 | Some((Arc::from("hello"), String::from("first value"))), 299 | Some((Arc::from("foo"), String::from("bar"))), 300 | Some((Arc::from("hello"), String::from("second value"))), 301 | ]), 302 | binary: None, 303 | }; 304 | let mut iter = frame.into_iter(); 305 | 306 | assert_eq!(iter.next(), Some(("hello".into(), "first value".into()))); 307 | assert_eq!(iter.next(), Some(("foo".into(), "bar".into()))); 308 | assert_eq!(iter.next(), Some(("hello".into(), "second value".into()))); 309 | 310 | assert_eq!(iter.next(), None); 311 | assert_eq!(iter.next(), None); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /mpd_client/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mpd_protocol/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mpd_protocol/src/command.rs: -------------------------------------------------------------------------------- 1 | //! Tools for constructing MPD commands. 2 | //! 3 | //! For an overview of available commands, see the [MPD documentation]. 4 | //! 5 | //! This does not perform any validations on commands beyond checking they appear well-formed, so 6 | //! it should not be tied to any particular protocol version. 7 | //! 8 | //! [MPD documentation]: https://www.musicpd.org/doc/html/protocol.html#command-reference 9 | 10 | use std::{ 11 | borrow::Cow, 12 | error::Error, 13 | fmt::{self, Debug}, 14 | time::Duration, 15 | }; 16 | 17 | use bytes::{BufMut, Bytes, BytesMut}; 18 | 19 | /// Start a command list, separated with list terminators. Our parser can't separate messages when 20 | /// the form of command list without terminators is used. 21 | const COMMAND_LIST_BEGIN: &[u8] = b"command_list_ok_begin\n"; 22 | 23 | /// End a command list. 24 | const COMMAND_LIST_END: &[u8] = b"command_list_end\n"; 25 | 26 | /// A single command, possibly including arguments. 27 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 28 | pub struct Command(pub(crate) BytesMut); 29 | 30 | impl Command { 31 | /// Start a new command. 32 | /// 33 | /// Same as [`Command::build`], but panics on error instead of returning a result. 34 | /// 35 | /// # Panics 36 | /// 37 | /// Panics where [`Command::build`] would return an error. 38 | #[track_caller] 39 | pub fn new(command: &str) -> Command { 40 | match Command::build(command) { 41 | Ok(c) => c, 42 | Err(e) => panic!("invalid command: {e}"), 43 | } 44 | } 45 | 46 | /// Start a new command. 47 | /// 48 | /// # Errors 49 | /// 50 | /// An error is returned when the command base is invalid. 51 | pub fn build(command: &str) -> Result { 52 | match validate_command_part(command) { 53 | Ok(()) => Ok(Command(BytesMut::from(command))), 54 | Err(kind) => Err(CommandError { 55 | data: Bytes::copy_from_slice(command.as_bytes()), 56 | kind, 57 | }), 58 | } 59 | } 60 | 61 | /// Add an argument to the command. 62 | /// 63 | /// Same as [`Command::add_argument`], but panics on error and allows chaining. 64 | /// 65 | /// # Panics 66 | /// 67 | /// Panics where [`Command::add_argument`] would return an error. 68 | #[track_caller] 69 | pub fn argument(mut self, argument: A) -> Command { 70 | if let Err(e) = self.add_argument(argument) { 71 | panic!("invalid argument: {e}"); 72 | } 73 | 74 | self 75 | } 76 | 77 | /// Add an argument to the command. 78 | /// 79 | /// # Errors 80 | /// 81 | /// An error is returned when the argument is invalid (e.g. empty string or containing invalid 82 | /// characters such as newlines). 83 | pub fn add_argument(&mut self, argument: A) -> Result<(), CommandError> { 84 | let len_without_arg = self.0.len(); 85 | 86 | self.0.put_u8(b' '); 87 | argument.render(&mut self.0); 88 | 89 | if let Err(kind) = validate_argument(&self.0[len_without_arg + 1..]) { 90 | // Remove added invalid part again 91 | let data = self.0.split_off(len_without_arg + 1).freeze(); 92 | self.0.truncate(len_without_arg); 93 | 94 | Err(CommandError { data, kind }) 95 | } else { 96 | Ok(()) 97 | } 98 | } 99 | } 100 | 101 | /// A non-empty list of commands. 102 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 103 | pub struct CommandList(pub(crate) Vec); 104 | 105 | #[allow(clippy::len_without_is_empty)] 106 | impl CommandList { 107 | /// Create a command list from the given single command. 108 | /// 109 | /// Unless further commands are added, the command will not be wrapped into a list. 110 | pub fn new(first: Command) -> Self { 111 | CommandList(vec![first]) 112 | } 113 | 114 | /// Add another command to the list. 115 | /// 116 | /// Same as [`CommandList::add`], but takes and returns `self` for chaining. 117 | pub fn command(mut self, command: Command) -> Self { 118 | self.add(command); 119 | self 120 | } 121 | 122 | /// Add another command to the list. 123 | pub fn add(&mut self, command: Command) { 124 | self.0.push(command); 125 | } 126 | 127 | /// Get the number of commands in this command list. 128 | /// 129 | /// This is never 0. 130 | pub fn len(&self) -> usize { 131 | self.0.len() 132 | } 133 | 134 | pub(crate) fn render(mut self) -> BytesMut { 135 | if self.len() == 1 { 136 | let mut buf = self.0.pop().unwrap().0; 137 | buf.put_u8(b'\n'); 138 | return buf; 139 | } 140 | 141 | // Calculate required length 142 | let required_length = COMMAND_LIST_BEGIN.len() 143 | + self.0.iter().map(|c| c.0.len() + 1).sum::() 144 | + COMMAND_LIST_END.len(); 145 | 146 | let mut buf = BytesMut::with_capacity(required_length); 147 | 148 | buf.put_slice(COMMAND_LIST_BEGIN); 149 | for command in self.0 { 150 | buf.put_slice(&command.0); 151 | buf.put_u8(b'\n'); 152 | } 153 | buf.put_slice(COMMAND_LIST_END); 154 | 155 | buf 156 | } 157 | } 158 | 159 | impl Extend for CommandList { 160 | fn extend>(&mut self, iter: T) { 161 | self.0.extend(iter); 162 | } 163 | } 164 | 165 | /// Escape a single argument, prefixing necessary characters (quotes and backslashes) with 166 | /// backslashes. 167 | /// 168 | /// Returns a borrowed [`Cow`] if the argument did not require escaping. 169 | /// 170 | /// ``` 171 | /// # use mpd_protocol::command::escape_argument; 172 | /// assert_eq!(escape_argument("foo'bar\""), "foo\\'bar\\\""); 173 | /// ``` 174 | pub fn escape_argument(argument: &str) -> Cow<'_, str> { 175 | let needs_quotes = argument.contains(&[' ', '\t'][..]); 176 | let escape_count = argument.chars().filter(|c| should_escape(*c)).count(); 177 | 178 | if escape_count == 0 && !needs_quotes { 179 | // The argument does not need to be quoted or escaped, return back an unmodified reference 180 | Cow::Borrowed(argument) 181 | } else { 182 | // The base length of the argument + a backslash for each escaped character + two quotes if 183 | // necessary 184 | let len = argument.len() + escape_count + if needs_quotes { 2 } else { 0 }; 185 | let mut out = String::with_capacity(len); 186 | 187 | if needs_quotes { 188 | out.push('"'); 189 | } 190 | 191 | for c in argument.chars() { 192 | if should_escape(c) { 193 | out.push('\\'); 194 | } 195 | 196 | out.push(c); 197 | } 198 | 199 | if needs_quotes { 200 | out.push('"'); 201 | } 202 | 203 | Cow::Owned(out) 204 | } 205 | } 206 | 207 | /// If the given character needs to be escaped 208 | fn should_escape(c: char) -> bool { 209 | c == '\\' || c == '"' || c == '\'' 210 | } 211 | 212 | fn validate_command_part(command: &str) -> Result<(), CommandErrorKind> { 213 | if command.is_empty() { 214 | return Err(CommandErrorKind::Empty); 215 | } 216 | 217 | if let Some((i, c)) = command 218 | .char_indices() 219 | .find(|(_, c)| !is_valid_command_char(*c)) 220 | { 221 | Err(CommandErrorKind::InvalidCharacter(i, c)) 222 | } else if is_command_list_command(command) { 223 | Err(CommandErrorKind::CommandList) 224 | } else { 225 | Ok(()) 226 | } 227 | } 228 | 229 | /// Validate an argument. 230 | fn validate_argument(argument: &[u8]) -> Result<(), CommandErrorKind> { 231 | match argument.iter().position(|&c| c == b'\n') { 232 | None => Ok(()), 233 | Some(i) => Err(CommandErrorKind::InvalidCharacter(i, '\n')), 234 | } 235 | } 236 | 237 | /// Commands can consist of alphabetic chars and underscores 238 | fn is_valid_command_char(c: char) -> bool { 239 | c.is_ascii_alphabetic() || c == '_' 240 | } 241 | 242 | /// Returns `true` if the given command would start or end a command list. 243 | fn is_command_list_command(command: &str) -> bool { 244 | command.starts_with("command_list") 245 | } 246 | 247 | /// Error returned when attempting to create invalid commands or arguments. 248 | #[derive(Debug)] 249 | pub struct CommandError { 250 | data: Bytes, 251 | kind: CommandErrorKind, 252 | } 253 | 254 | /// Error returned when attempting to construct an invalid command. 255 | #[derive(Debug)] 256 | enum CommandErrorKind { 257 | /// The command was empty (either an empty command or an empty list commands). 258 | Empty, 259 | /// The command string contained an invalid character at the contained position. This is 260 | /// context-dependent, as some characters are only invalid in certain sections of a command. 261 | InvalidCharacter(usize, char), 262 | /// Attempted to start or close a command list manually. 263 | CommandList, 264 | } 265 | 266 | impl Error for CommandError {} 267 | 268 | impl fmt::Display for CommandError { 269 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 270 | match &self.kind { 271 | CommandErrorKind::Empty => write!(f, "empty command"), 272 | CommandErrorKind::InvalidCharacter(i, c) => { 273 | write!( 274 | f, 275 | "invalid character {:?} at position {} in {:?}", 276 | c, i, self.data 277 | ) 278 | } 279 | CommandErrorKind::CommandList => write!( 280 | f, 281 | "attempted to open or close a command list: {:?}", 282 | self.data 283 | ), 284 | } 285 | } 286 | } 287 | 288 | /// Things which can be used as arguments for commands. 289 | pub trait Argument { 290 | /// Render the argument into the command buffer. 291 | /// 292 | /// Spaces before/after arguments are inserted automatically, but values need to be escaped 293 | /// manually. See [`escape_argument`]. 294 | fn render(&self, buf: &mut BytesMut); 295 | } 296 | 297 | impl Argument for &A 298 | where 299 | A: Argument + ?Sized, 300 | { 301 | fn render(&self, buf: &mut BytesMut) { 302 | (*self).render(buf); 303 | } 304 | } 305 | 306 | impl Argument for String { 307 | fn render(&self, buf: &mut BytesMut) { 308 | let arg = escape_argument(self); 309 | buf.put_slice(arg.as_bytes()); 310 | } 311 | } 312 | 313 | impl Argument for str { 314 | fn render(&self, buf: &mut BytesMut) { 315 | let arg = escape_argument(self); 316 | buf.put_slice(arg.as_bytes()); 317 | } 318 | } 319 | 320 | impl Argument for Cow<'_, str> { 321 | fn render(&self, buf: &mut BytesMut) { 322 | let arg = escape_argument(self); 323 | buf.put_slice(arg.as_bytes()); 324 | } 325 | } 326 | 327 | impl Argument for bool { 328 | fn render(&self, buf: &mut BytesMut) { 329 | buf.put_u8(if *self { b'1' } else { b'0' }); 330 | } 331 | } 332 | 333 | impl Argument for Duration { 334 | /// Song durations in the format MPD expects. Will round to third decimal place. 335 | fn render(&self, buf: &mut BytesMut) { 336 | use std::fmt::Write; 337 | write!(buf, "{:.3}", self.as_secs_f64()).unwrap(); 338 | } 339 | } 340 | 341 | macro_rules! implement_integer_arg { 342 | ($($type:ty),+) => { 343 | $( 344 | impl $crate::command::Argument for $type { 345 | fn render(&self, buf: &mut ::bytes::BytesMut) { 346 | use ::std::fmt::Write; 347 | ::std::write!(buf, "{}", self).unwrap(); 348 | } 349 | } 350 | )+ 351 | } 352 | } 353 | 354 | implement_integer_arg!(u8, u16, u32, u64, usize); 355 | 356 | #[cfg(test)] 357 | mod test { 358 | use super::*; 359 | 360 | #[test] 361 | fn arguments() { 362 | let mut command = Command::new("foo"); 363 | assert_eq!(command.0, "foo"); 364 | 365 | command.add_argument("bar").unwrap(); 366 | assert_eq!(command.0, "foo bar"); 367 | 368 | // Invalid argument does not change the command 369 | let _e = command.add_argument("foo\nbar").unwrap_err(); 370 | assert_eq!(command.0, "foo bar"); 371 | } 372 | 373 | #[test] 374 | fn argument_escaping() { 375 | assert_eq!(escape_argument("status"), "status"); 376 | assert_eq!(escape_argument("Joe's"), "Joe\\'s"); 377 | assert_eq!(escape_argument("hello\\world"), "hello\\\\world"); 378 | assert_eq!(escape_argument("foo bar"), r#""foo bar""#); 379 | } 380 | 381 | #[test] 382 | fn argument_rendering() { 383 | let mut buf = BytesMut::new(); 384 | 385 | "foo\"bar".render(&mut buf); 386 | assert_eq!(buf, "foo\\\"bar"); 387 | buf.clear(); 388 | 389 | true.render(&mut buf); 390 | assert_eq!(buf, "1"); 391 | buf.clear(); 392 | 393 | false.render(&mut buf); 394 | assert_eq!(buf, "0"); 395 | buf.clear(); 396 | 397 | Duration::from_secs(2).render(&mut buf); 398 | assert_eq!(buf, "2.000"); 399 | buf.clear(); 400 | 401 | Duration::from_secs_f64(2.34567).render(&mut buf); 402 | assert_eq!(buf, "2.346"); 403 | buf.clear(); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /mpd_client/src/responses/song.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, mem, path::Path, time::Duration}; 2 | 3 | use mpd_protocol::response::Frame; 4 | 5 | use crate::{ 6 | commands::{SongId, SongPosition}, 7 | responses::{FromFieldValue, Timestamp, TypedResponseError, parse_duration}, 8 | tag::Tag, 9 | }; 10 | 11 | /// A [`Song`] in the current queue, as returned by the [`playlistinfo`] command. 12 | /// 13 | /// [`playlistinfo`]: crate::commands::definitions::Queue 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | #[non_exhaustive] 16 | pub struct SongInQueue { 17 | /// Position in queue. 18 | pub position: SongPosition, 19 | /// ID in queue. 20 | pub id: SongId, 21 | /// The range of the song that will be played. 22 | pub range: Option, 23 | /// The priority. 24 | pub priority: u8, 25 | /// The song. 26 | pub song: Song, 27 | } 28 | 29 | impl SongInQueue { 30 | /// Convert the given frame into a single `SongInQueue`. 31 | pub(crate) fn from_frame_single( 32 | frame: Frame, 33 | ) -> Result, TypedResponseError> { 34 | let mut builder = SongBuilder::default(); 35 | 36 | for (key, value) in frame { 37 | builder.field(&key, value)?; 38 | } 39 | 40 | Ok(builder.finish()) 41 | } 42 | 43 | /// Convert the given frame into a list of `SongInQueue`s. 44 | pub(crate) fn from_frame_multi(frame: Frame) -> Result, TypedResponseError> { 45 | let mut out = Vec::new(); 46 | let mut builder = SongBuilder::default(); 47 | 48 | for (key, value) in frame { 49 | if let Some(song) = builder.field(&key, value)? { 50 | out.push(song); 51 | } 52 | } 53 | 54 | if let Some(song) = builder.finish() { 55 | out.push(song); 56 | } 57 | 58 | Ok(out) 59 | } 60 | } 61 | 62 | /// A single song, as returned by the [playlist] or [current song] commands. 63 | /// 64 | /// [playlist]: crate::commands::definitions::Queue 65 | /// [current song]: crate::commands::definitions::CurrentSong 66 | #[derive(Clone, Debug, PartialEq, Eq)] 67 | #[non_exhaustive] 68 | pub struct Song { 69 | /// Unique identifier of the song. May be a file path relative to the library root, or an URL 70 | /// to a remote resource. 71 | /// 72 | /// This is the `file` key as returned by MPD. 73 | pub url: String, 74 | /// The `duration` as returned by MPD. 75 | pub duration: Option, 76 | /// Tags in this response. 77 | pub tags: HashMap>, 78 | /// The `format` as returned by MPD. 79 | pub format: Option, 80 | /// Last modification date of the underlying file. 81 | pub last_modified: Option, 82 | } 83 | 84 | impl Song { 85 | /// Get the file as a `Path`. Note that if the file is a remote URL, operations on the result 86 | /// will give unexpected results. 87 | pub fn file_path(&self) -> &Path { 88 | Path::new(&self.url) 89 | } 90 | 91 | /// Get all artists of the song. 92 | pub fn artists(&self) -> &[String] { 93 | self.tag_values(&Tag::Artist) 94 | } 95 | 96 | /// Get all album artists of the song. 97 | pub fn album_artists(&self) -> &[String] { 98 | self.tag_values(&Tag::AlbumArtist) 99 | } 100 | 101 | /// Get the album of the song. 102 | pub fn album(&self) -> Option<&str> { 103 | self.single_tag_value(&Tag::Album) 104 | } 105 | 106 | /// Get the title of the song. 107 | pub fn title(&self) -> Option<&str> { 108 | self.single_tag_value(&Tag::Title) 109 | } 110 | 111 | /// Get the disc and track number of the song. 112 | /// 113 | /// If either are not set on the song, 0 is returned. This is a utility for sorting. 114 | pub fn number(&self) -> (u64, u64) { 115 | let disc = self.single_tag_value(&Tag::Disc); 116 | let track = self.single_tag_value(&Tag::Track); 117 | 118 | ( 119 | disc.and_then(|v| v.parse().ok()).unwrap_or(0), 120 | track.and_then(|v| v.parse().ok()).unwrap_or(0), 121 | ) 122 | } 123 | 124 | /// Convert the given frame into a list of `Song`s. 125 | pub(crate) fn from_frame_multi(frame: Frame) -> Result, TypedResponseError> { 126 | let mut out = Vec::new(); 127 | let mut builder = SongBuilder::default(); 128 | 129 | for (key, value) in frame { 130 | if let Some(SongInQueue { song, .. }) = builder.field(&key, value)? { 131 | out.push(song); 132 | } 133 | } 134 | 135 | if let Some(SongInQueue { song, .. }) = builder.finish() { 136 | out.push(song); 137 | } 138 | 139 | Ok(out) 140 | } 141 | 142 | fn tag_values(&self, tag: &Tag) -> &[String] { 143 | match self.tags.get(tag) { 144 | Some(v) => v.as_slice(), 145 | None => &[], 146 | } 147 | } 148 | 149 | fn single_tag_value(&self, tag: &Tag) -> Option<&str> { 150 | match self.tag_values(tag) { 151 | [] => None, 152 | [v, ..] => Some(v), 153 | } 154 | } 155 | } 156 | 157 | /// Range used when playing only part of a [`Song`]. 158 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 159 | pub struct SongRange { 160 | /// Start playback at this timestamp. 161 | pub from: Duration, 162 | /// End at this timestamp (if the end is known). 163 | pub to: Option, 164 | } 165 | 166 | impl FromFieldValue for SongRange { 167 | fn from_value(v: String, field: &str) -> Result { 168 | // The range follows the form "-" 169 | let Some((from, to)) = v.split_once('-') else { 170 | return Err(TypedResponseError::invalid_value(field, v)); 171 | }; 172 | 173 | let from = parse_duration(field, from)?; 174 | 175 | let to = if to.is_empty() { 176 | None 177 | } else { 178 | Some(parse_duration(field, to)?) 179 | }; 180 | 181 | Ok(SongRange { from, to }) 182 | } 183 | } 184 | 185 | #[derive(Debug, Default)] 186 | struct SongBuilder { 187 | url: String, 188 | position: usize, 189 | id: u64, 190 | range: Option, 191 | priority: u8, 192 | duration: Option, 193 | tags: HashMap>, 194 | format: Option, 195 | last_modified: Option, 196 | } 197 | 198 | impl SongBuilder { 199 | /// Handle a field from a song list. 200 | /// 201 | /// If this returns `Ok(Some(_))`, a song was completed and another one started. 202 | fn field( 203 | &mut self, 204 | key: &str, 205 | value: String, 206 | ) -> Result, TypedResponseError> { 207 | if self.url.is_empty() { 208 | // No song is currently in progress 209 | self.handle_start_field(key, value)?; 210 | Ok(None) 211 | } else { 212 | // Currently parsing a song 213 | self.handle_song_field(key, value) 214 | } 215 | } 216 | 217 | /// Handle a field that is expected to start a new song. 218 | fn handle_start_field(&mut self, key: &str, value: String) -> Result<(), TypedResponseError> { 219 | match key { 220 | // A `file` field starts a new song 221 | "file" => self.url = value, 222 | // Skip directory or playlist entries, encountered when using commands like 223 | // `listallinfo`, as well as the last modified date associated with these entries 224 | "directory" | "playlist" | "Last-Modified" => (), 225 | // Any other fields are invalid 226 | other => return Err(TypedResponseError::unexpected_field("file", other)), 227 | } 228 | 229 | Ok(()) 230 | } 231 | 232 | /// Handle a field that may be part of a song or may start a new one. 233 | fn handle_song_field( 234 | &mut self, 235 | key: &str, 236 | value: String, 237 | ) -> Result, TypedResponseError> { 238 | // If this field starts a new song, the current one is done 239 | if is_start_field(key) { 240 | // Reset the song builder and convert the existing data into a song 241 | let song = mem::take(self).into_song(); 242 | 243 | // Handle the current field 244 | self.handle_start_field(key, value)?; 245 | 246 | // Return the complete song 247 | return Ok(Some(song)); 248 | } 249 | 250 | // The field is a component of a song 251 | match key { 252 | "duration" => self.duration = Some(Duration::from_value(value, "duration")?), 253 | "Time" => { 254 | // Just a worse `duration` field, but retained for backwards compatibility with 255 | // protocol versions <0.20 256 | if self.duration.is_none() { 257 | self.duration = Some(Duration::from_value(value, "Time")?); 258 | } 259 | } 260 | "Range" => self.range = Some(SongRange::from_value(value, "Range")?), 261 | "Format" => self.format = Some(value), 262 | "Last-Modified" => { 263 | let lm = Timestamp::from_value(value, "Last-Modified")?; 264 | self.last_modified = Some(lm); 265 | } 266 | "Prio" => self.priority = u8::from_value(value, "Prio")?, 267 | "Pos" => self.position = usize::from_value(value, "Pos")?, 268 | "Id" => self.id = u64::from_value(value, "Id")?, 269 | tag => { 270 | // Anything else is a tag. 271 | // It's fine to unwrap here because the protocol implementation already validated 272 | // the field name 273 | let tag = Tag::try_from(tag).unwrap(); 274 | self.tags.entry(tag).or_default().push(value); 275 | } 276 | } 277 | 278 | Ok(None) 279 | } 280 | 281 | /// Finish the building process. This returns the final song, if there is one. 282 | fn finish(self) -> Option { 283 | if self.url.is_empty() { 284 | None 285 | } else { 286 | Some(self.into_song()) 287 | } 288 | } 289 | 290 | fn into_song(self) -> SongInQueue { 291 | assert!(!self.url.is_empty()); 292 | 293 | SongInQueue { 294 | position: SongPosition(self.position), 295 | id: SongId(self.id), 296 | range: self.range, 297 | priority: self.priority, 298 | song: Song { 299 | url: self.url, 300 | duration: self.duration, 301 | tags: self.tags, 302 | format: self.format, 303 | last_modified: self.last_modified, 304 | }, 305 | } 306 | } 307 | } 308 | 309 | /// Returns `true` if the given field name starts a new song entry. 310 | fn is_start_field(f: &str) -> bool { 311 | matches!(f, "file" | "directory" | "playlist") 312 | } 313 | 314 | #[cfg(test)] 315 | mod tests { 316 | use assert_matches::assert_matches; 317 | 318 | use super::*; 319 | 320 | const TEST_TIMESTAMP: &str = "2020-06-12T17:53:00Z"; 321 | 322 | #[test] 323 | fn song_builder() { 324 | let mut builder = SongBuilder::default(); 325 | 326 | assert_matches!(builder.field("file", String::from("test.flac")), Ok(None)); 327 | assert_matches!(builder.field("duration", String::from("123.456")), Ok(None)); 328 | assert_matches!( 329 | builder.field("Last-Modified", String::from(TEST_TIMESTAMP)), 330 | Ok(None) 331 | ); 332 | assert_matches!(builder.field("Title", String::from("Foo")), Ok(None)); 333 | assert_matches!(builder.field("Id", String::from("12")), Ok(None)); 334 | assert_matches!(builder.field("Pos", String::from("5")), Ok(None)); 335 | 336 | let song = builder 337 | .field("file", String::from("foo.flac")) 338 | .unwrap() 339 | .unwrap(); 340 | 341 | assert_eq!( 342 | song, 343 | SongInQueue { 344 | position: SongPosition(5), 345 | id: SongId(12), 346 | priority: 0, 347 | range: None, 348 | song: Song { 349 | url: String::from("test.flac"), 350 | duration: Some(Duration::from_secs_f64(123.456)), 351 | format: None, 352 | last_modified: Some(Timestamp::from_value(TEST_TIMESTAMP.into(), "").unwrap()), 353 | tags: [(Tag::Title, vec![String::from("Foo")])].into(), 354 | } 355 | } 356 | ); 357 | 358 | let song = builder.finish().unwrap(); 359 | 360 | assert_eq!( 361 | song, 362 | SongInQueue { 363 | position: SongPosition(0), 364 | id: SongId(0), 365 | priority: 0, 366 | range: None, 367 | song: Song { 368 | url: String::from("foo.flac"), 369 | duration: None, 370 | format: None, 371 | last_modified: None, 372 | tags: HashMap::new(), 373 | } 374 | } 375 | ); 376 | } 377 | 378 | #[test] 379 | fn song_builder_unrelated_entries() { 380 | let mut builder = SongBuilder::default(); 381 | 382 | assert_matches!(builder.field("playlist", String::from("foo.m3u")), Ok(None)); 383 | assert_matches!(builder.field("directory", String::from("foo")), Ok(None)); 384 | assert_matches!( 385 | builder.field("Last-Modified", String::from(TEST_TIMESTAMP)), 386 | Ok(None) 387 | ); 388 | assert_matches!(builder.field("file", String::from("foo.flac")), Ok(None)); 389 | 390 | let song = builder 391 | .field("directory", String::from("mep")) 392 | .unwrap() 393 | .unwrap(); 394 | 395 | assert_eq!( 396 | song, 397 | SongInQueue { 398 | position: SongPosition(0), 399 | id: SongId(0), 400 | priority: 0, 401 | range: None, 402 | song: Song { 403 | url: String::from("foo.flac"), 404 | duration: None, 405 | format: None, 406 | last_modified: None, 407 | tags: HashMap::new(), 408 | } 409 | } 410 | ); 411 | 412 | assert_matches!(builder.finish(), None); 413 | } 414 | 415 | #[test] 416 | fn song_builder_deprecated_time_field() { 417 | let mut builder = SongBuilder::default(); 418 | 419 | assert_matches!(builder.field("file", String::from("foo.flac")), Ok(None)); 420 | 421 | assert_matches!(builder.field("Time", String::from("123")), Ok(None)); 422 | assert_eq!(builder.duration, Some(Duration::from_secs(123))); 423 | 424 | assert_matches!(builder.field("duration", String::from("456.700")), Ok(None)); 425 | assert_eq!(builder.duration, Some(Duration::from_secs_f64(456.7))); 426 | 427 | assert_matches!(builder.field("Time", String::from("123")), Ok(None)); 428 | assert_eq!(builder.duration, Some(Duration::from_secs_f64(456.7))); 429 | 430 | let song = builder.finish().unwrap().song; 431 | 432 | assert_eq!( 433 | song, 434 | Song { 435 | url: String::from("foo.flac"), 436 | format: None, 437 | last_modified: None, 438 | duration: Some(Duration::from_secs_f64(456.7)), 439 | tags: HashMap::new(), 440 | } 441 | ); 442 | } 443 | 444 | #[test] 445 | fn parse_range() { 446 | assert_eq!( 447 | SongRange::from_value(String::from("1.500-5.642"), "Range").unwrap(), 448 | SongRange { 449 | from: Duration::from_secs_f64(1.5), 450 | to: Some(Duration::from_secs_f64(5.642)), 451 | } 452 | ); 453 | 454 | assert_eq!( 455 | SongRange::from_value(String::from("1.500-"), "Range").unwrap(), 456 | SongRange { 457 | from: Duration::from_secs_f64(1.5), 458 | to: None, 459 | } 460 | ); 461 | 462 | assert_matches!(SongRange::from_value(String::from("foo"), "Range"), Err(_)); 463 | 464 | assert_matches!( 465 | SongRange::from_value(String::from("1.000--5.000"), "Range"), 466 | Err(_) 467 | ); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /mpd_client/src/responses/mod.rs: -------------------------------------------------------------------------------- 1 | //! Typed responses to individual commands. 2 | 3 | mod count; 4 | mod list; 5 | mod playlist; 6 | mod song; 7 | mod sticker; 8 | mod timestamp; 9 | 10 | use std::{error::Error, fmt, num::ParseIntError, str::FromStr, sync::Arc, time::Duration}; 11 | 12 | use bytes::BytesMut; 13 | use mpd_protocol::response::Frame; 14 | 15 | pub use self::{ 16 | count::Count, 17 | list::{GroupedListValuesIter, List, ListValuesIntoIter, ListValuesIter}, 18 | playlist::Playlist, 19 | song::{Song, SongInQueue, SongRange}, 20 | sticker::{StickerFind, StickerGet, StickerList}, 21 | timestamp::Timestamp, 22 | }; 23 | use crate::commands::{ReplayGainMode, SingleMode, SongId, SongPosition}; 24 | 25 | type KeyValuePair = (Arc, String); 26 | 27 | /// Error returned when failing to convert a raw [`Frame`] into the proper typed response. 28 | #[derive(Debug)] 29 | pub struct TypedResponseError { 30 | kind: ErrorKind, 31 | source: Option>, 32 | } 33 | 34 | impl TypedResponseError { 35 | /// Construct a "Missing field" error. 36 | pub fn missing(field: F) -> TypedResponseError 37 | where 38 | F: Into, 39 | { 40 | TypedResponseError { 41 | kind: ErrorKind::Missing { 42 | field: field.into(), 43 | }, 44 | source: None, 45 | } 46 | } 47 | 48 | /// Construct an "Unexpected field" error. 49 | pub fn unexpected_field(expected: E, found: F) -> TypedResponseError 50 | where 51 | E: Into, 52 | F: Into, 53 | { 54 | TypedResponseError { 55 | kind: ErrorKind::UnexpectedField { 56 | expected: expected.into(), 57 | found: found.into(), 58 | }, 59 | source: None, 60 | } 61 | } 62 | 63 | /// Construct an "Invalid value" error. 64 | pub fn invalid_value(field: F, value: String) -> TypedResponseError 65 | where 66 | F: Into, 67 | { 68 | TypedResponseError { 69 | kind: ErrorKind::InvalidValue { 70 | field: field.into(), 71 | value, 72 | }, 73 | source: None, 74 | } 75 | } 76 | 77 | /// Construct a nonspecific error. 78 | pub fn other() -> TypedResponseError { 79 | TypedResponseError { 80 | kind: ErrorKind::Other, 81 | source: None, 82 | } 83 | } 84 | 85 | /// Set a source error. 86 | /// 87 | /// This is most useful with [invalid value][TypedResponseError::invalid_value] and 88 | /// [unspecified][TypedResponseError::other] errors. 89 | pub fn source(self, source: E) -> TypedResponseError 90 | where 91 | E: Error + Send + Sync + 'static, 92 | { 93 | let source = Some(Box::from(source)); 94 | TypedResponseError { source, ..self } 95 | } 96 | } 97 | 98 | #[derive(Debug)] 99 | enum ErrorKind { 100 | Missing { field: String }, 101 | UnexpectedField { expected: String, found: String }, 102 | InvalidValue { field: String, value: String }, 103 | Other, 104 | } 105 | 106 | impl fmt::Display for TypedResponseError { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | match &self.kind { 109 | ErrorKind::Missing { field } => write!(f, "field {field:?} is required but missing"), 110 | ErrorKind::UnexpectedField { expected, found } => { 111 | write!(f, "expected field {expected:?} but found {found:?}") 112 | } 113 | ErrorKind::InvalidValue { field, value } => { 114 | write!(f, "invalid value {value:?} for field {field:?}") 115 | } 116 | ErrorKind::Other => write!(f, "invalid response"), 117 | } 118 | } 119 | } 120 | 121 | impl Error for TypedResponseError { 122 | fn source(&self) -> Option<&(dyn Error + 'static)> { 123 | self.source.as_deref().map(|e| e as _) 124 | } 125 | } 126 | 127 | /// Types which can be converted from a field value. 128 | pub(crate) trait FromFieldValue: Sized { 129 | /// Convert the value. 130 | fn from_value(v: String, field: &str) -> Result; 131 | } 132 | 133 | impl FromFieldValue for bool { 134 | fn from_value(v: String, field: &str) -> Result { 135 | match &*v { 136 | "0" => Ok(false), 137 | "1" => Ok(true), 138 | _ => Err(TypedResponseError::invalid_value(field, v)), 139 | } 140 | } 141 | } 142 | 143 | impl FromFieldValue for Duration { 144 | fn from_value(v: String, field: &str) -> Result { 145 | parse_duration(field, v) 146 | } 147 | } 148 | 149 | impl FromFieldValue for PlayState { 150 | fn from_value(v: String, field: &str) -> Result { 151 | match &*v { 152 | "play" => Ok(PlayState::Playing), 153 | "pause" => Ok(PlayState::Paused), 154 | "stop" => Ok(PlayState::Stopped), 155 | _ => Err(TypedResponseError::invalid_value(field, v)), 156 | } 157 | } 158 | } 159 | 160 | impl FromFieldValue for ReplayGainMode { 161 | fn from_value(v: String, field: &str) -> Result { 162 | match &*v { 163 | "off" => Ok(ReplayGainMode::Off), 164 | "track" => Ok(ReplayGainMode::Track), 165 | "album" => Ok(ReplayGainMode::Album), 166 | "auto" => Ok(ReplayGainMode::Auto), 167 | _ => Err(TypedResponseError::invalid_value(field, v)), 168 | } 169 | } 170 | } 171 | 172 | fn parse_integer>( 173 | v: String, 174 | field: &str, 175 | ) -> Result { 176 | v.parse::() 177 | .map_err(|e| TypedResponseError::invalid_value(field, v).source(e)) 178 | } 179 | 180 | impl FromFieldValue for u8 { 181 | fn from_value(v: String, field: &str) -> Result { 182 | parse_integer(v, field) 183 | } 184 | } 185 | 186 | impl FromFieldValue for u32 { 187 | fn from_value(v: String, field: &str) -> Result { 188 | parse_integer(v, field) 189 | } 190 | } 191 | 192 | impl FromFieldValue for u64 { 193 | fn from_value(v: String, field: &str) -> Result { 194 | parse_integer(v, field) 195 | } 196 | } 197 | 198 | impl FromFieldValue for usize { 199 | fn from_value(v: String, field: &str) -> Result { 200 | parse_integer(v, field) 201 | } 202 | } 203 | 204 | /// Get a *required* value for the given field, as the given type. 205 | pub(crate) fn value( 206 | frame: &mut Frame, 207 | field: &'static str, 208 | ) -> Result { 209 | let value = frame 210 | .get(field) 211 | .ok_or_else(|| TypedResponseError::missing(field))?; 212 | V::from_value(value, field) 213 | } 214 | 215 | /// Get an *optional* value for the given field, as the given type. 216 | fn optional_value( 217 | frame: &mut Frame, 218 | field: &'static str, 219 | ) -> Result, TypedResponseError> { 220 | match frame.get(field) { 221 | None => Ok(None), 222 | Some(v) => { 223 | let v = V::from_value(v, field)?; 224 | Ok(Some(v)) 225 | } 226 | } 227 | } 228 | 229 | fn song_identifier( 230 | frame: &mut Frame, 231 | position_field: &'static str, 232 | id_field: &'static str, 233 | ) -> Result, TypedResponseError> { 234 | // The position field may or may not exist 235 | let position = match optional_value(frame, position_field)? { 236 | Some(p) => SongPosition(p), 237 | None => return Ok(None), 238 | }; 239 | 240 | // ... but if the position field existed, the ID field must exist too 241 | let id = value(frame, id_field).map(SongId)?; 242 | 243 | Ok(Some((position, id))) 244 | } 245 | 246 | fn parse_duration + Into>( 247 | field: &str, 248 | value: V, 249 | ) -> Result { 250 | let v = match value.as_ref().parse::() { 251 | Ok(v) => v, 252 | Err(e) => return Err(TypedResponseError::invalid_value(field, value.into()).source(e)), 253 | }; 254 | 255 | // Check if the parsed value is a reasonable duration, to avoid a panic from `from_secs_f64` 256 | if v >= 0.0 && v <= Duration::MAX.as_secs_f64() && v.is_finite() { 257 | Ok(Duration::from_secs_f64(v)) 258 | } else { 259 | Err(TypedResponseError::invalid_value(field, value.into())) 260 | } 261 | } 262 | 263 | /// Possible playback states. 264 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 265 | #[allow(missing_docs)] 266 | pub enum PlayState { 267 | Stopped, 268 | Playing, 269 | Paused, 270 | } 271 | 272 | /// Response to the [`replay_gain_status`] command. 273 | /// 274 | /// See the [MPD documentation][replay-gain-status-command] for the specific meanings of the fields. 275 | /// 276 | /// [`replay_gain_status`]: crate::commands::definitions::ReplayGainStatus 277 | /// [replay-gain-status-command]: https://www.musicpd.org/doc/html/protocol.html#command-replay-gain-status 278 | #[derive(Clone, Debug, PartialEq, Eq)] 279 | #[allow(missing_docs)] 280 | #[non_exhaustive] 281 | pub struct ReplayGainStatus { 282 | pub mode: ReplayGainMode, 283 | } 284 | 285 | impl ReplayGainStatus { 286 | pub(crate) fn from_frame(mut raw: Frame) -> Result { 287 | let f = &mut raw; 288 | Ok(Self { 289 | mode: value(f, "replay_gain_mode")?, 290 | }) 291 | } 292 | } 293 | 294 | /// Response to the [`status`] command. 295 | /// 296 | /// See the [MPD documentation][status-command] for the specific meanings of the fields. 297 | /// 298 | /// [`status`]: crate::commands::definitions::Status 299 | /// [status-command]: https://www.musicpd.org/doc/html/protocol.html#command-status 300 | #[derive(Clone, Debug, PartialEq, Eq)] 301 | #[allow(missing_docs)] 302 | #[non_exhaustive] 303 | pub struct Status { 304 | pub volume: u8, 305 | pub state: PlayState, 306 | pub repeat: bool, 307 | pub random: bool, 308 | pub consume: bool, 309 | pub single: SingleMode, 310 | pub playlist_version: u32, 311 | pub playlist_length: usize, 312 | pub current_song: Option<(SongPosition, SongId)>, 313 | pub next_song: Option<(SongPosition, SongId)>, 314 | pub elapsed: Option, 315 | pub duration: Option, 316 | pub bitrate: Option, 317 | pub crossfade: Duration, 318 | pub update_job: Option, 319 | pub error: Option, 320 | pub partition: Option, 321 | } 322 | 323 | impl Status { 324 | pub(crate) fn from_frame(mut raw: Frame) -> Result { 325 | let single = match raw.get("single") { 326 | None => SingleMode::Disabled, 327 | Some(val) => match val.as_str() { 328 | "0" => SingleMode::Disabled, 329 | "1" => SingleMode::Enabled, 330 | "oneshot" => SingleMode::Oneshot, 331 | _ => return Err(TypedResponseError::invalid_value("single", val)), 332 | }, 333 | }; 334 | 335 | let duration = if let Some(val) = raw.get("duration") { 336 | Some(Duration::from_value(val, "duration")?) 337 | } else if let Some(time) = raw.get("Time") { 338 | // Backwards compatibility with protocol versions <0.20 339 | if let Some((_, duration)) = time.split_once(':') { 340 | Some(Duration::from_value(duration.to_owned(), "Time")?) 341 | } else { 342 | // No separator 343 | return Err(TypedResponseError::invalid_value("Time", time)); 344 | } 345 | } else { 346 | None 347 | }; 348 | 349 | let f = &mut raw; 350 | 351 | Ok(Self { 352 | volume: optional_value(f, "volume")?.unwrap_or(0), 353 | state: value(f, "state")?, 354 | repeat: value(f, "repeat")?, 355 | random: value(f, "random")?, 356 | consume: value(f, "consume")?, 357 | single, 358 | playlist_length: optional_value(f, "playlistlength")?.unwrap_or(0), 359 | playlist_version: optional_value(f, "playlist")?.unwrap_or(0), 360 | current_song: song_identifier(f, "song", "songid")?, 361 | next_song: song_identifier(f, "nextsong", "nextsongid")?, 362 | elapsed: optional_value(f, "elapsed")?, 363 | duration, 364 | bitrate: optional_value(f, "bitrate")?, 365 | crossfade: optional_value(f, "xfade")?.unwrap_or(Duration::ZERO), 366 | update_job: optional_value(f, "update_job")?, 367 | error: f.get("error"), 368 | partition: f.get("partition"), 369 | }) 370 | } 371 | } 372 | 373 | /// Response to the [`stats`] command, containing general server statistics. 374 | /// 375 | /// [`stats`]: crate::commands::definitions::Stats 376 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 377 | #[allow(missing_docs)] 378 | #[non_exhaustive] 379 | pub struct Stats { 380 | pub artists: u64, 381 | pub albums: u64, 382 | pub songs: u64, 383 | pub uptime: Duration, 384 | pub playtime: Duration, 385 | pub db_playtime: Duration, 386 | /// Raw server UNIX timestamp of last database update. 387 | pub db_last_update: u64, 388 | } 389 | 390 | impl Stats { 391 | pub(crate) fn from_frame(mut f: Frame) -> Result { 392 | let f = &mut f; 393 | Ok(Self { 394 | artists: value(f, "artists")?, 395 | albums: value(f, "albums")?, 396 | songs: value(f, "songs")?, 397 | uptime: value(f, "uptime")?, 398 | playtime: value(f, "playtime")?, 399 | db_playtime: value(f, "db_playtime")?, 400 | db_last_update: value(f, "db_update")?, 401 | }) 402 | } 403 | } 404 | 405 | /// Response to the [`albumart`][crate::commands::AlbumArt] and 406 | /// [`readpicture`][crate::commands::AlbumArtEmbedded] commands. 407 | #[derive(Clone, Debug, PartialEq, Eq)] 408 | #[non_exhaustive] 409 | pub struct AlbumArt { 410 | /// The total size in bytes of the file. 411 | pub size: usize, 412 | /// The mime type, if known. 413 | pub mime: Option, 414 | /// The raw data. 415 | pub data: BytesMut, 416 | } 417 | 418 | impl AlbumArt { 419 | pub(crate) fn from_frame(mut frame: Frame) -> Result, TypedResponseError> { 420 | let Some(data) = frame.take_binary() else { 421 | return Ok(None); 422 | }; 423 | 424 | Ok(Some(AlbumArt { 425 | size: value(&mut frame, "size")?, 426 | mime: frame.get("type"), 427 | data, 428 | })) 429 | } 430 | } 431 | 432 | /// Parse response for the [`crate::commands::ReadChannelMessages`] command. 433 | pub(crate) fn parse_channel_messages( 434 | fields: F, 435 | ) -> Result, TypedResponseError> 436 | where 437 | F: IntoIterator, 438 | { 439 | let mut response = Vec::new(); 440 | let mut fields = fields.into_iter(); 441 | 442 | while let Some(channel) = fields.next() { 443 | if &*channel.0 != "channel" { 444 | return Err(TypedResponseError::unexpected_field("channel", &*channel.0)); 445 | } 446 | 447 | let Some(message) = fields.next() else { 448 | return Err(TypedResponseError::missing("message")); 449 | }; 450 | 451 | if &*message.0 != "message" { 452 | return Err(TypedResponseError::unexpected_field("message", &*message.0)); 453 | } 454 | 455 | response.push((channel.1, message.1)); 456 | } 457 | 458 | Ok(response) 459 | } 460 | 461 | #[cfg(test)] 462 | mod tests { 463 | use assert_matches::assert_matches; 464 | 465 | use super::*; 466 | 467 | #[test] 468 | fn duration_parsing() { 469 | assert_eq!( 470 | parse_duration("duration", "1.500").unwrap(), 471 | Duration::from_secs_f64(1.5) 472 | ); 473 | assert_eq!(parse_duration("Time", "3").unwrap(), Duration::from_secs(3)); 474 | 475 | // Error cases 476 | assert_matches!(parse_duration("duration", "asdf"), Err(_)); 477 | assert_matches!(parse_duration("duration", "-1"), Err(_)); 478 | assert_matches!(parse_duration("duration", "NaN"), Err(_)); 479 | assert_matches!(parse_duration("duration", "-1"), Err(_)); 480 | } 481 | 482 | #[test] 483 | fn channel_message_parsing() { 484 | assert_eq!(parse_channel_messages(Vec::new()).unwrap(), Vec::new()); 485 | 486 | let fields = vec![ 487 | (Arc::from("channel"), String::from("foo")), 488 | (Arc::from("message"), String::from("message 1")), 489 | (Arc::from("channel"), String::from("bar")), 490 | (Arc::from("message"), String::from("message 2")), 491 | ]; 492 | assert_eq!( 493 | parse_channel_messages(fields).unwrap(), 494 | vec![ 495 | (String::from("foo"), String::from("message 1")), 496 | (String::from("bar"), String::from("message 2")), 497 | ] 498 | ); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /mpd_protocol/src/response/mod.rs: -------------------------------------------------------------------------------- 1 | //! Complete responses. 2 | 3 | pub mod frame; 4 | 5 | use std::{collections::HashSet, fmt, iter::FusedIterator, mem, slice, sync::Arc, vec}; 6 | 7 | use bytes::{Buf, BytesMut}; 8 | use tracing::trace; 9 | 10 | pub use self::frame::Frame; 11 | use crate::{MpdProtocolError, parser::ParsedComponent}; 12 | 13 | /// Response to a command, consisting of an arbitrary amount of [frames][Frame], which are 14 | /// responses to individual commands, and optionally a single [error][Error]. 15 | /// 16 | /// Since an error terminates a command list, there can only be one error in a response. 17 | #[derive(Clone, PartialEq, Eq)] 18 | pub struct Response { 19 | /// The successful responses. 20 | frames: Vec, 21 | /// The error, if one occurred. 22 | error: Option, 23 | } 24 | 25 | impl Response { 26 | /// Construct a new "empty" response. This is the simplest possible successful response, 27 | /// consisting of a single empty frame. 28 | pub(crate) fn empty() -> Self { 29 | Self { 30 | frames: vec![Frame::empty()], 31 | error: None, 32 | } 33 | } 34 | 35 | /// Returns `true` if the response contains an error. 36 | /// 37 | /// Even if this returns `true`, there may still be successful frames in the response when the 38 | /// response is to a command list. 39 | pub fn is_error(&self) -> bool { 40 | self.error.is_some() 41 | } 42 | 43 | /// Returns `true` if the response was entirely successful (i.e. no errors). 44 | pub fn is_success(&self) -> bool { 45 | !self.is_error() 46 | } 47 | 48 | /// Get the number of successful frames in the response. 49 | /// 50 | /// May be 0 if the response only consists of an error. 51 | pub fn successful_frames(&self) -> usize { 52 | self.frames.len() 53 | } 54 | 55 | /// Create an iterator over references to the frames in the response. 56 | /// 57 | /// This yields `Result`s, with successful frames becoming `Ok()`s and an error becoming a 58 | /// (final) `Err()`. 59 | pub fn frames(&self) -> FramesRef<'_> { 60 | FramesRef { 61 | frames: self.frames.iter(), 62 | error: self.error.as_ref(), 63 | } 64 | } 65 | 66 | /// Extract the first frame or error from the response. 67 | /// 68 | /// Any additional frames are discarded. This is useful for responses to single commands. 69 | pub fn into_single_frame(self) -> Result { 70 | // There is always at least one frame 71 | self.into_iter().next().unwrap() 72 | } 73 | 74 | pub(crate) fn field_count(&self) -> usize { 75 | self.frames.iter().map(Frame::fields_len).sum() 76 | } 77 | } 78 | 79 | impl fmt::Debug for Response { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | write!(f, "Response(")?; 82 | 83 | f.debug_list() 84 | .entries(&self.frames) 85 | .entries(&self.error) 86 | .finish()?; 87 | 88 | write!(f, ")") 89 | } 90 | } 91 | 92 | /// A cache for field names used in responses. 93 | #[derive(Clone, Debug)] 94 | pub(crate) struct ResponseFieldCache(HashSet, ahash::RandomState>); 95 | 96 | impl ResponseFieldCache { 97 | /// Returns a new, empty cache. 98 | pub(crate) fn new() -> ResponseFieldCache { 99 | ResponseFieldCache(HashSet::default()) 100 | } 101 | 102 | /// Insert a field name into the cache or retrieve a reference to an already existing entry. 103 | pub(crate) fn insert(&mut self, key: &str) -> Arc { 104 | if let Some(k) = self.0.get(key) { 105 | Arc::clone(k) 106 | } else { 107 | let k = Arc::from(key); 108 | self.0.insert(Arc::clone(&k)); 109 | k 110 | } 111 | } 112 | } 113 | 114 | #[derive(Debug)] 115 | pub(crate) struct ResponseBuilder<'a> { 116 | field_cache: &'a mut ResponseFieldCache, 117 | state: ResponseState, 118 | } 119 | 120 | #[derive(Clone, Debug, PartialEq, Eq)] 121 | enum ResponseState { 122 | Initial, 123 | InProgress { 124 | current: Frame, 125 | }, 126 | ListInProgress { 127 | current: Frame, 128 | completed_frames: Vec, 129 | }, 130 | } 131 | 132 | impl<'a> ResponseBuilder<'a> { 133 | pub(crate) fn new(field_cache: &'a mut ResponseFieldCache) -> Self { 134 | Self { 135 | field_cache, 136 | state: ResponseState::Initial, 137 | } 138 | } 139 | 140 | pub(crate) fn parse( 141 | &mut self, 142 | src: &mut BytesMut, 143 | ) -> Result, MpdProtocolError> { 144 | while !src.is_empty() { 145 | let (remaining, component) = match ParsedComponent::parse(src, self.field_cache) { 146 | Err(e) if e.is_incomplete() => break, 147 | Err(_) => return Err(MpdProtocolError::InvalidMessage), 148 | Ok(p) => p, 149 | }; 150 | 151 | let msg_end = src.len() - remaining.len(); 152 | let mut msg = src.split_to(msg_end); 153 | 154 | match component { 155 | ParsedComponent::Field { key, value } => self.field(key, value), 156 | ParsedComponent::BinaryField { data_length } => { 157 | msg.advance(msg.len() - (data_length + 1)); 158 | msg.truncate(data_length); 159 | self.binary(msg); 160 | } 161 | ParsedComponent::Error(e) => return Ok(Some(self.error(e))), 162 | ParsedComponent::EndOfFrame => self.finish_frame(), 163 | ParsedComponent::EndOfResponse => return Ok(Some(self.finish())), 164 | } 165 | } 166 | 167 | Ok(None) 168 | } 169 | 170 | pub(crate) fn is_frame_in_progress(&self) -> bool { 171 | self.state != ResponseState::Initial 172 | } 173 | 174 | fn field(&mut self, key: Arc, value: String) { 175 | trace!(?key, ?value, "parsed field"); 176 | match &mut self.state { 177 | ResponseState::Initial => { 178 | let mut frame = Frame::empty(); 179 | frame.fields.push_field(key, value); 180 | self.state = ResponseState::InProgress { current: frame }; 181 | } 182 | ResponseState::InProgress { current } 183 | | ResponseState::ListInProgress { current, .. } => { 184 | current.fields.push_field(key, value); 185 | } 186 | } 187 | } 188 | 189 | fn binary(&mut self, binary: BytesMut) { 190 | trace!(length = binary.len(), "parsed binary field"); 191 | match &mut self.state { 192 | ResponseState::Initial => { 193 | let mut frame = Frame::empty(); 194 | frame.binary = Some(binary); 195 | self.state = ResponseState::InProgress { current: frame }; 196 | } 197 | ResponseState::InProgress { current } 198 | | ResponseState::ListInProgress { current, .. } => { 199 | current.binary = Some(binary); 200 | } 201 | } 202 | } 203 | 204 | fn finish_frame(&mut self) { 205 | trace!("finished command list frame"); 206 | let completed_frames = match mem::replace(&mut self.state, ResponseState::Initial) { 207 | ResponseState::Initial => vec![Frame::empty()], 208 | ResponseState::InProgress { current } => vec![current], 209 | ResponseState::ListInProgress { 210 | current, 211 | mut completed_frames, 212 | } => { 213 | completed_frames.push(current); 214 | completed_frames 215 | } 216 | }; 217 | 218 | self.state = ResponseState::ListInProgress { 219 | current: Frame::empty(), 220 | completed_frames, 221 | }; 222 | } 223 | 224 | fn finish(&mut self) -> Response { 225 | trace!("finished response"); 226 | match mem::replace(&mut self.state, ResponseState::Initial) { 227 | ResponseState::Initial => Response::empty(), 228 | ResponseState::InProgress { current } => Response { 229 | frames: vec![current], 230 | error: None, 231 | }, 232 | ResponseState::ListInProgress { 233 | completed_frames, .. 234 | } => Response { 235 | frames: completed_frames, 236 | error: None, 237 | }, 238 | } 239 | } 240 | 241 | fn error(&mut self, error: Error) -> Response { 242 | trace!(?error, "parsed error"); 243 | match mem::replace(&mut self.state, ResponseState::Initial) { 244 | ResponseState::Initial | ResponseState::InProgress { .. } => Response { 245 | frames: Vec::new(), 246 | error: Some(error), 247 | }, 248 | ResponseState::ListInProgress { 249 | completed_frames, .. 250 | } => Response { 251 | frames: completed_frames, 252 | error: Some(error), 253 | }, 254 | } 255 | } 256 | } 257 | 258 | /// Iterator over frames in a response, as returned by [`Response::frames`]. 259 | #[derive(Clone, Debug)] 260 | pub struct FramesRef<'a> { 261 | frames: slice::Iter<'a, Frame>, 262 | error: Option<&'a Error>, 263 | } 264 | 265 | impl<'a> Iterator for FramesRef<'a> { 266 | type Item = Result<&'a Frame, &'a Error>; 267 | 268 | fn next(&mut self) -> Option { 269 | if let Some(frame) = self.frames.next() { 270 | Some(Ok(frame)) 271 | } else { 272 | self.error.take().map(Err) 273 | } 274 | } 275 | 276 | fn size_hint(&self) -> (usize, Option) { 277 | // .len() returns the number of successful frames, add 1 if there is also an error 278 | let len = self.frames.len() + if self.error.is_some() { 1 } else { 0 }; 279 | 280 | (len, Some(len)) 281 | } 282 | } 283 | 284 | impl DoubleEndedIterator for FramesRef<'_> { 285 | fn next_back(&mut self) -> Option { 286 | if let Some(e) = self.error.take() { 287 | Some(Err(e)) 288 | } else { 289 | self.frames.next_back().map(Ok) 290 | } 291 | } 292 | } 293 | 294 | impl FusedIterator for FramesRef<'_> {} 295 | impl ExactSizeIterator for FramesRef<'_> {} 296 | 297 | impl<'a> IntoIterator for &'a Response { 298 | type Item = Result<&'a Frame, &'a Error>; 299 | type IntoIter = FramesRef<'a>; 300 | 301 | fn into_iter(self) -> Self::IntoIter { 302 | self.frames() 303 | } 304 | } 305 | 306 | /// Iterator over frames in a response, as returned by [`IntoIterator`] implementation on 307 | /// [`Response`]. 308 | #[derive(Clone, Debug)] 309 | pub struct Frames { 310 | frames: vec::IntoIter, 311 | error: Option, 312 | } 313 | 314 | impl Iterator for Frames { 315 | type Item = Result; 316 | 317 | fn next(&mut self) -> Option { 318 | if let Some(f) = self.frames.next() { 319 | Some(Ok(f)) 320 | } else { 321 | self.error.take().map(Err) 322 | } 323 | } 324 | 325 | fn size_hint(&self) -> (usize, Option) { 326 | // .len() returns the number of successful frames, add 1 if there is also an error 327 | let len = self.frames.len() + if self.error.is_some() { 1 } else { 0 }; 328 | 329 | (len, Some(len)) 330 | } 331 | } 332 | 333 | impl DoubleEndedIterator for Frames { 334 | fn next_back(&mut self) -> Option { 335 | if let Some(e) = self.error.take() { 336 | Some(Err(e)) 337 | } else { 338 | self.frames.next_back().map(Ok) 339 | } 340 | } 341 | } 342 | 343 | impl FusedIterator for Frames {} 344 | impl ExactSizeIterator for Frames {} 345 | 346 | impl IntoIterator for Response { 347 | type Item = Result; 348 | type IntoIter = Frames; 349 | 350 | fn into_iter(self) -> Self::IntoIter { 351 | Frames { 352 | frames: self.frames.into_iter(), 353 | error: self.error, 354 | } 355 | } 356 | } 357 | 358 | /// A response to a command indicating an error. 359 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 360 | pub struct Error { 361 | /// Error code. See [the MPD source][mpd-error-def] for a list of of possible values. 362 | /// 363 | /// [mpd-error-def]: https://github.com/MusicPlayerDaemon/MPD/blob/master/src/protocol/Ack.hxx#L30 364 | pub code: u64, 365 | /// Index of command in a command list that caused this error. 0 when not in a command list. 366 | pub command_index: u64, 367 | /// Command that returned the error, if applicable. 368 | pub current_command: Option>, 369 | /// Message describing the error. 370 | pub message: Box, 371 | } 372 | 373 | #[cfg(test)] 374 | mod test { 375 | use assert_matches::assert_matches; 376 | 377 | use super::*; 378 | 379 | fn frame(fields: [(&str, &str); N], binary: Option<&[u8]>) -> Frame { 380 | let mut out = Frame::empty(); 381 | 382 | for &(k, v) in &fields { 383 | out.fields.push_field(k.into(), v.into()); 384 | } 385 | 386 | out.binary = binary.map(BytesMut::from); 387 | 388 | out 389 | } 390 | 391 | #[test] 392 | fn owned_frames_iter() { 393 | let r = Response { 394 | frames: vec![Frame::empty(), Frame::empty(), Frame::empty()], 395 | error: Some(Error::default()), 396 | }; 397 | 398 | let mut iter = r.into_iter(); 399 | 400 | assert_eq!((4, Some(4)), iter.size_hint()); 401 | assert_eq!(Some(Ok(Frame::empty())), iter.next()); 402 | 403 | assert_eq!((3, Some(3)), iter.size_hint()); 404 | assert_eq!(Some(Ok(Frame::empty())), iter.next()); 405 | 406 | assert_eq!((2, Some(2)), iter.size_hint()); 407 | assert_eq!(Some(Ok(Frame::empty())), iter.next()); 408 | 409 | assert_eq!((1, Some(1)), iter.size_hint()); 410 | assert_eq!(Some(Err(Error::default())), iter.next()); 411 | 412 | assert_eq!((0, Some(0)), iter.size_hint()); 413 | } 414 | 415 | #[test] 416 | fn borrowed_frames_iter() { 417 | let r = Response { 418 | frames: vec![Frame::empty(), Frame::empty(), Frame::empty()], 419 | error: Some(Error::default()), 420 | }; 421 | 422 | let mut iter = r.frames(); 423 | 424 | assert_eq!((4, Some(4)), iter.size_hint()); 425 | assert_eq!(Some(Ok(&Frame::empty())), iter.next()); 426 | 427 | assert_eq!((3, Some(3)), iter.size_hint()); 428 | assert_eq!(Some(Ok(&Frame::empty())), iter.next()); 429 | 430 | assert_eq!((2, Some(2)), iter.size_hint()); 431 | assert_eq!(Some(Ok(&Frame::empty())), iter.next()); 432 | 433 | assert_eq!((1, Some(1)), iter.size_hint()); 434 | assert_eq!(Some(Err(&Error::default())), iter.next()); 435 | 436 | assert_eq!((0, Some(0)), iter.size_hint()); 437 | } 438 | 439 | #[test] 440 | fn simple_response() { 441 | let mut io = BytesMut::from("foo: bar\nOK"); 442 | 443 | let mut field_cache = ResponseFieldCache::new(); 444 | let mut builder = ResponseBuilder::new(&mut field_cache); 445 | assert_eq!(builder.state, ResponseState::Initial); 446 | 447 | // Consume fields 448 | assert_matches!(builder.parse(&mut io), Ok(None)); 449 | assert_eq!( 450 | builder.state, 451 | ResponseState::InProgress { 452 | current: frame([("foo", "bar")], None) 453 | } 454 | ); 455 | assert_eq!(io, "OK"); 456 | 457 | // No complete message, do not consume buffer 458 | assert_matches!(builder.parse(&mut io), Ok(None)); 459 | assert_eq!( 460 | builder.state, 461 | ResponseState::InProgress { 462 | current: frame([("foo", "bar")], None) 463 | } 464 | ); 465 | assert_eq!(io, "OK"); 466 | 467 | io.extend_from_slice(b"\n"); 468 | 469 | // Response now complete 470 | assert_eq!( 471 | builder.parse(&mut io).unwrap(), 472 | Some(Response { 473 | frames: vec![frame([("foo", "bar")], None)], 474 | error: None 475 | }) 476 | ); 477 | assert_eq!(builder.state, ResponseState::Initial); 478 | assert_eq!(io, ""); 479 | } 480 | 481 | #[test] 482 | fn response_with_binary() { 483 | let mut io = BytesMut::from("foo: bar\nbinary: 6\nOK\n"); 484 | let mut field_cache = ResponseFieldCache::new(); 485 | let mut builder = ResponseBuilder::new(&mut field_cache); 486 | 487 | assert_matches!(builder.parse(&mut io), Ok(None)); 488 | assert_eq!( 489 | builder.state, 490 | ResponseState::InProgress { 491 | current: frame([("foo", "bar")], None) 492 | } 493 | ); 494 | assert_eq!(io, "binary: 6\nOK\n"); 495 | 496 | io.extend_from_slice(b"OK\n\n"); 497 | 498 | assert_matches!(builder.parse(&mut io), Ok(None)); 499 | assert_eq!( 500 | builder.state, 501 | ResponseState::InProgress { 502 | current: frame([("foo", "bar")], Some(b"OK\nOK\n")), 503 | } 504 | ); 505 | assert_eq!(io, ""); 506 | 507 | io.extend_from_slice(b"OK\n"); 508 | assert_eq!( 509 | builder.parse(&mut io).unwrap(), 510 | Some(Response { 511 | frames: vec![frame([("foo", "bar")], Some(b"OK\nOK\n"))], 512 | error: None, 513 | }) 514 | ); 515 | assert_eq!(builder.state, ResponseState::Initial); 516 | } 517 | 518 | #[test] 519 | fn empty_response() { 520 | let mut io = BytesMut::from("OK"); 521 | let mut field_cache = ResponseFieldCache::new(); 522 | let mut builder = ResponseBuilder::new(&mut field_cache); 523 | 524 | assert_matches!(builder.parse(&mut io), Ok(None)); 525 | assert_eq!(builder.state, ResponseState::Initial); 526 | 527 | io.extend_from_slice(b"\n"); 528 | 529 | assert_eq!( 530 | builder.parse(&mut io).unwrap(), 531 | Some(Response { 532 | frames: vec![Frame::empty()], 533 | error: None, 534 | }) 535 | ); 536 | } 537 | 538 | #[test] 539 | fn error() { 540 | let mut io = BytesMut::from("ACK [5@0] {} unknown command \"foo\""); 541 | let mut field_cache = ResponseFieldCache::new(); 542 | let mut builder = ResponseBuilder::new(&mut field_cache); 543 | 544 | assert_matches!(builder.parse(&mut io), Ok(None)); 545 | assert_eq!(builder.state, ResponseState::Initial); 546 | 547 | io.extend_from_slice(b"\n"); 548 | 549 | assert_eq!( 550 | builder.parse(&mut io).unwrap(), 551 | Some(Response { 552 | frames: vec![], 553 | error: Some(Error { 554 | code: 5, 555 | command_index: 0, 556 | current_command: None, 557 | message: Box::from("unknown command \"foo\""), 558 | }), 559 | }) 560 | ); 561 | assert_eq!(builder.state, ResponseState::Initial); 562 | } 563 | 564 | #[test] 565 | fn multiple_messages() { 566 | let mut io = BytesMut::from("foo: bar\nOK\nhello: world\nOK\n"); 567 | let mut field_cache = ResponseFieldCache::new(); 568 | let mut builder = ResponseBuilder::new(&mut field_cache); 569 | 570 | assert_eq!( 571 | builder.parse(&mut io).unwrap(), 572 | Some(Response { 573 | frames: vec![frame([("foo", "bar")], None)], 574 | error: None 575 | }) 576 | ); 577 | assert_eq!(io, "hello: world\nOK\n"); 578 | 579 | assert_eq!( 580 | builder.parse(&mut io).unwrap(), 581 | Some(Response { 582 | frames: vec![frame([("hello", "world")], None)], 583 | error: None 584 | }) 585 | ); 586 | assert_eq!(io, ""); 587 | } 588 | 589 | #[test] 590 | fn command_list() { 591 | let mut io = BytesMut::from("foo: bar\n"); 592 | let mut field_cache = ResponseFieldCache::new(); 593 | let mut builder = ResponseBuilder::new(&mut field_cache); 594 | 595 | assert_matches!(builder.parse(&mut io), Ok(None)); 596 | assert_eq!( 597 | builder.state, 598 | ResponseState::InProgress { 599 | current: frame([("foo", "bar")], None) 600 | } 601 | ); 602 | 603 | io.extend_from_slice(b"list_OK\n"); 604 | 605 | assert_matches!(builder.parse(&mut io), Ok(None)); 606 | assert_eq!( 607 | builder.state, 608 | ResponseState::ListInProgress { 609 | current: Frame::empty(), 610 | completed_frames: vec![frame([("foo", "bar")], None)], 611 | } 612 | ); 613 | 614 | io.extend_from_slice(b"list_OK\n"); 615 | 616 | assert_matches!(builder.parse(&mut io), Ok(None)); 617 | assert_eq!( 618 | builder.state, 619 | ResponseState::ListInProgress { 620 | current: Frame::empty(), 621 | completed_frames: vec![frame([("foo", "bar")], None), Frame::empty()], 622 | } 623 | ); 624 | 625 | io.extend_from_slice(b"OK\n"); 626 | 627 | assert_eq!( 628 | builder.parse(&mut io).unwrap(), 629 | Some(Response { 630 | frames: vec![frame([("foo", "bar")], None), Frame::empty()], 631 | error: None 632 | }) 633 | ); 634 | assert_eq!(builder.state, ResponseState::Initial); 635 | } 636 | 637 | #[test] 638 | fn command_list_error() { 639 | let mut io = BytesMut::from("list_OK\n"); 640 | let mut field_cache = ResponseFieldCache::new(); 641 | let mut builder = ResponseBuilder::new(&mut field_cache); 642 | 643 | assert_matches!(builder.parse(&mut io), Ok(None)); 644 | assert_eq!( 645 | builder.state, 646 | ResponseState::ListInProgress { 647 | current: Frame::empty(), 648 | completed_frames: vec![Frame::empty()], 649 | } 650 | ); 651 | 652 | io.extend_from_slice(b"ACK [5@1] {} unknown command \"foo\"\n"); 653 | 654 | assert_eq!( 655 | builder.parse(&mut io).unwrap(), 656 | Some(Response { 657 | frames: vec![Frame::empty()], 658 | error: Some(Error { 659 | code: 5, 660 | command_index: 1, 661 | current_command: None, 662 | message: Box::from("unknown command \"foo\""), 663 | }), 664 | }) 665 | ); 666 | assert_eq!(builder.state, ResponseState::Initial); 667 | } 668 | 669 | #[test] 670 | fn key_interning() { 671 | let mut io = BytesMut::from("foo: bar\nfoo: baz\nOK\n"); 672 | 673 | let mut field_cache = ResponseFieldCache::new(); 674 | let mut resp = ResponseBuilder::new(&mut field_cache) 675 | .parse(&mut io) 676 | .expect("incomplete") 677 | .expect("invalid"); 678 | 679 | let mut fields = resp.frames.pop().unwrap().into_iter(); 680 | 681 | let (a, _) = fields.next().unwrap(); 682 | let (b, _) = fields.next().unwrap(); 683 | 684 | assert!(Arc::ptr_eq(&a, &b)); 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /mpd_protocol/src/connection.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Write}; 2 | 3 | use bytes::{BufMut, BytesMut}; 4 | #[cfg(feature = "async")] 5 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; 6 | use tracing::{debug, error, info, trace}; 7 | 8 | use crate::{ 9 | MpdProtocolError, 10 | command::{Command, CommandList}, 11 | parser, 12 | response::{Response, ResponseBuilder, ResponseFieldCache}, 13 | }; 14 | 15 | /// Default receive buffer size 16 | const DEFAULT_BUFFER_CAPACITY: usize = 4096; 17 | 18 | /// A **blocking** connection to an MPD server. 19 | #[derive(Debug)] 20 | pub struct Connection { 21 | io: IO, 22 | protocol_version: Box, 23 | field_cache: ResponseFieldCache, 24 | recv_buf: BytesMut, 25 | total_received: usize, 26 | } 27 | 28 | impl Connection { 29 | #[cfg(any(fuzzing, criterion))] 30 | #[allow(dead_code)] 31 | #[doc(hidden)] 32 | pub fn new_internal(io: IO) -> Connection { 33 | Connection { 34 | io, 35 | protocol_version: Box::from(""), 36 | field_cache: ResponseFieldCache::new(), 37 | recv_buf: BytesMut::zeroed(DEFAULT_BUFFER_CAPACITY), 38 | total_received: 0, 39 | } 40 | } 41 | 42 | /// Connect to an MPD server synchronously. 43 | #[tracing::instrument(skip_all, err)] 44 | pub fn connect(mut io: IO) -> Result, MpdProtocolError> 45 | where 46 | IO: Read, 47 | { 48 | let mut recv_buf = BytesMut::zeroed(DEFAULT_BUFFER_CAPACITY); 49 | let mut total_read = 0; 50 | 51 | let protocol_version = loop { 52 | let (data, amount_read) = read_to_buffer(&mut io, &mut recv_buf, &mut total_read)?; 53 | 54 | if amount_read == 0 { 55 | return Err(MpdProtocolError::Io(io::Error::new( 56 | io::ErrorKind::UnexpectedEof, 57 | "unexpected end of file while receiving greeting", 58 | ))); 59 | } 60 | 61 | match parser::greeting(data) { 62 | Ok((_, version)) => { 63 | info!(?version, "connected successfully"); 64 | break Box::from(version); 65 | } 66 | Err(e) if e.is_incomplete() => { 67 | // The response was valid *so far*, try another read 68 | trace!("greeting incomplete"); 69 | } 70 | Err(_) => { 71 | error!("invalid greeting"); 72 | return Err(MpdProtocolError::InvalidMessage); 73 | } 74 | } 75 | }; 76 | 77 | Ok(Connection { 78 | io, 79 | protocol_version, 80 | field_cache: ResponseFieldCache::new(), 81 | recv_buf, 82 | total_received: 0, 83 | }) 84 | } 85 | 86 | /// Send a command. 87 | /// 88 | /// # Errors 89 | /// 90 | /// This will return an error if writing to the given IO resource fails. 91 | #[tracing::instrument(skip(self), err)] 92 | pub fn send(&mut self, mut command: Command) -> Result<(), MpdProtocolError> 93 | where 94 | IO: Write, 95 | { 96 | command.0.put_u8(b'\n'); 97 | self.io.write_all(&command.0)?; 98 | debug!(length = command.0.len(), "sent command"); 99 | Ok(()) 100 | } 101 | 102 | /// Send a command list. 103 | /// 104 | /// # Errors 105 | /// 106 | /// This will return an error if writing to the given IO resource fails. 107 | #[tracing::instrument(skip(self), err)] 108 | pub fn send_list(&mut self, command_list: CommandList) -> Result<(), MpdProtocolError> 109 | where 110 | IO: Write, 111 | { 112 | let buf = command_list.render(); 113 | self.io.write_all(&buf)?; 114 | debug!(length = buf.len(), "sent command list"); 115 | 116 | Ok(()) 117 | } 118 | 119 | /// Receive a response from the server. 120 | /// 121 | /// This will return `Ok(Some(..))` when a complete response has been received, or `Ok(None)` if 122 | /// the connection is closed cleanly. 123 | /// 124 | /// # Errors 125 | /// 126 | /// This will return an error if: 127 | /// 128 | /// - Reading from the given IO resource returns an error 129 | /// - Malformed response data is received 130 | /// - The connection is closed while a response is in progress 131 | #[tracing::instrument(skip(self), err)] 132 | pub fn receive(&mut self) -> Result, MpdProtocolError> 133 | where 134 | IO: Read, 135 | { 136 | let mut response_builder = ResponseBuilder::new(&mut self.field_cache); 137 | 138 | loop { 139 | // Split off the read part of the receive buffer 140 | let buf_size = self.recv_buf.len(); 141 | let remaining = self.recv_buf.split_off(self.total_received); 142 | 143 | // Try to parse response data from the initialized section of the buffer, removing the 144 | // consumed parts from the buffer 145 | let maybe_parsed = response_builder.parse(&mut self.recv_buf)?; 146 | 147 | // Update the length of the initialized section to the remaining length 148 | self.total_received = self.recv_buf.len(); 149 | 150 | // Join back the remaining data with the main buffer, and readjust the length 151 | self.recv_buf.unsplit(remaining); 152 | self.recv_buf.resize(buf_size, 0); 153 | 154 | if let Some(response) = maybe_parsed { 155 | debug!( 156 | frames = response.successful_frames(), 157 | error = response.is_error(), 158 | fields = response.field_count(), 159 | "received complete response" 160 | ); 161 | break Ok(Some(response)); 162 | } 163 | 164 | let (_, amount_read) = 165 | read_to_buffer(&mut self.io, &mut self.recv_buf, &mut self.total_received)?; 166 | 167 | if amount_read == 0 { 168 | break if response_builder.is_frame_in_progress() || self.total_received != 0 { 169 | error!("EOF while receiving response"); 170 | Err(MpdProtocolError::Io(io::Error::new( 171 | io::ErrorKind::UnexpectedEof, 172 | "unexpected end of file while receiving response", 173 | ))) 174 | } else { 175 | debug!("clean EOF while receiving response"); 176 | Ok(None) 177 | }; 178 | } 179 | } 180 | } 181 | 182 | /// Send a command and receive its response. 183 | /// 184 | /// This is essentially a shorthand for [`Connection::send`] followed by [`Connection::receive`]. 185 | /// 186 | /// # Errors 187 | /// 188 | /// This will return an error if: 189 | /// 190 | /// - Writing to or reading from the connection returns an error 191 | /// - Malformed response data is received 192 | /// - The connection is closed 193 | #[tracing::instrument(skip(self), err)] 194 | pub fn command(&mut self, command: Command) -> Result 195 | where 196 | IO: Read + Write, 197 | { 198 | self.send(command)?; 199 | 200 | if let Some(r) = self.receive()? { 201 | Ok(r) 202 | } else { 203 | error!("connection was closed without a response to the command"); 204 | Err(MpdProtocolError::Io(io::Error::new( 205 | io::ErrorKind::UnexpectedEof, 206 | "connection was closed without a response to the command", 207 | ))) 208 | } 209 | } 210 | 211 | /// Send a command list and receive its response(s). 212 | /// 213 | /// This is essentially a shorthand for [`Connection::send_list`] followed by [`Connection::receive`]. 214 | /// 215 | /// # Errors 216 | /// 217 | /// This will return an error if: 218 | /// 219 | /// - Writing to or reading from the connection returns an error 220 | /// - Malformed response data is received 221 | /// - The connection is closed 222 | #[tracing::instrument(skip(self), err)] 223 | pub fn command_list(&mut self, command_list: CommandList) -> Result 224 | where 225 | IO: Read + Write, 226 | { 227 | self.send_list(command_list)?; 228 | 229 | if let Some(r) = self.receive()? { 230 | Ok(r) 231 | } else { 232 | error!("connection was closed without a response to the command"); 233 | Err(MpdProtocolError::Io(io::Error::new( 234 | io::ErrorKind::UnexpectedEof, 235 | "connection was closed without a response to the command", 236 | ))) 237 | } 238 | } 239 | 240 | /// Returns the protocol version the server is using. 241 | pub fn protocol_version(&self) -> &str { 242 | &self.protocol_version 243 | } 244 | 245 | /// Extract the connection instance. 246 | pub fn into_inner(self) -> IO { 247 | self.io 248 | } 249 | } 250 | 251 | fn read_to_buffer<'a, R: Read>( 252 | mut io: R, 253 | buf: &'a mut BytesMut, 254 | total: &mut usize, 255 | ) -> Result<(&'a [u8], usize), io::Error> { 256 | let read = io.read(&mut buf[*total..])?; 257 | trace!(read); 258 | *total += read; 259 | 260 | if buf.len() == *total { 261 | trace!("need to grow buffer"); 262 | buf.resize(buf.len() * 2, 0); 263 | } 264 | 265 | Ok((&buf[..*total], read)) 266 | } 267 | 268 | /// An **asynchronous** connection to an MPD server. 269 | #[cfg(feature = "async")] 270 | #[cfg_attr(docsrs, doc(cfg(feature = "async")))] 271 | #[derive(Debug)] 272 | pub struct AsyncConnection(Connection); 273 | 274 | #[cfg(feature = "async")] 275 | impl AsyncConnection { 276 | /// Connect to an MPD server asynchronously. 277 | /// 278 | /// # Errors 279 | /// 280 | /// This will return an error if: 281 | /// 282 | /// - Reading from the given IO resource returns an error 283 | /// - A malformed greeting is received 284 | /// - The connection is closed before a complete greeting could be read 285 | #[cfg_attr(docsrs, doc(cfg(feature = "async")))] 286 | #[tracing::instrument(skip_all, err)] 287 | pub async fn connect(mut io: IO) -> Result, MpdProtocolError> 288 | where 289 | IO: AsyncRead + Unpin, 290 | { 291 | let mut recv_buf = BytesMut::with_capacity(DEFAULT_BUFFER_CAPACITY); 292 | 293 | let protocol_version = loop { 294 | let read = io.read_buf(&mut recv_buf).await?; 295 | trace!(read); 296 | 297 | if read == 0 { 298 | return Err(MpdProtocolError::Io(io::Error::new( 299 | io::ErrorKind::UnexpectedEof, 300 | "unexpected end of file while receiving greeting", 301 | ))); 302 | } 303 | 304 | match parser::greeting(&recv_buf) { 305 | Ok((_, version)) => { 306 | info!(?version, "connected successfully"); 307 | break Box::from(version); 308 | } 309 | Err(e) if e.is_incomplete() => { 310 | // The response was valid *so far*, try another read 311 | trace!("greeting incomplete"); 312 | } 313 | Err(_) => { 314 | error!("invalid greeting"); 315 | return Err(MpdProtocolError::InvalidMessage); 316 | } 317 | } 318 | }; 319 | 320 | recv_buf.clear(); 321 | 322 | Ok(AsyncConnection(Connection { 323 | io, 324 | protocol_version, 325 | field_cache: ResponseFieldCache::new(), 326 | recv_buf, 327 | total_received: 0, 328 | })) 329 | } 330 | 331 | /// Send a command. 332 | /// 333 | /// # Errors 334 | /// 335 | /// This will return an error if writing to the given IO resource fails. 336 | #[cfg_attr(docsrs, doc(cfg(feature = "async")))] 337 | #[tracing::instrument(skip(self), err)] 338 | pub async fn send(&mut self, mut command: Command) -> Result<(), MpdProtocolError> 339 | where 340 | IO: AsyncWrite + Unpin, 341 | { 342 | command.0.put_u8(b'\n'); 343 | self.0.io.write_all(&command.0).await?; 344 | debug!(length = command.0.len(), "sent command"); 345 | Ok(()) 346 | } 347 | 348 | /// Send a command list. 349 | /// 350 | /// # Errors 351 | /// 352 | /// This will return an error if writing to the given IO resource fails. 353 | #[cfg_attr(docsrs, doc(cfg(feature = "async")))] 354 | #[tracing::instrument(skip(self), err)] 355 | pub async fn send_list(&mut self, command_list: CommandList) -> Result<(), MpdProtocolError> 356 | where 357 | IO: AsyncWrite + Unpin, 358 | { 359 | let buf = command_list.render(); 360 | self.0.io.write_all(&buf).await?; 361 | debug!(length = buf.len(), "sent command"); 362 | Ok(()) 363 | } 364 | 365 | /// Receive a response from the server. 366 | /// 367 | /// This will return `Ok(Some(..))` when a complete response has been received, or `Ok(None)` if 368 | /// the connection is closed cleanly. 369 | /// 370 | /// # Errors 371 | /// 372 | /// This will return an error if: 373 | /// 374 | /// - Reading from the given IO resource returns an error 375 | /// - Malformed response data is received 376 | /// - The connection is closed while a response is in progress 377 | #[cfg_attr(docsrs, doc(cfg(feature = "async")))] 378 | #[tracing::instrument(skip(self), err)] 379 | pub async fn receive(&mut self) -> Result, MpdProtocolError> 380 | where 381 | IO: AsyncRead + Unpin, 382 | { 383 | let mut response_builder = ResponseBuilder::new(&mut self.0.field_cache); 384 | 385 | loop { 386 | if let Some(response) = response_builder.parse(&mut self.0.recv_buf)? { 387 | debug!( 388 | frames = response.successful_frames(), 389 | fields = response.field_count(), 390 | error = response.is_error(), 391 | "received complete response" 392 | ); 393 | break Ok(Some(response)); 394 | } 395 | 396 | let read = self.0.io.read_buf(&mut self.0.recv_buf).await?; 397 | trace!(read); 398 | 399 | if read == 0 { 400 | break if response_builder.is_frame_in_progress() || !self.0.recv_buf.is_empty() { 401 | error!("EOF while receiving response"); 402 | Err(MpdProtocolError::Io(io::Error::new( 403 | io::ErrorKind::UnexpectedEof, 404 | "unexpected end of file while receiving response", 405 | ))) 406 | } else { 407 | debug!("clean EOF while receiving"); 408 | Ok(None) 409 | }; 410 | } 411 | } 412 | } 413 | 414 | /// Send a command and receive its response. 415 | /// 416 | /// This is essentially a shorthand for [`AsyncConnection::send`] followed by 417 | /// [`AsyncConnection::receive`]. 418 | /// 419 | /// # Errors 420 | /// 421 | /// This will return an error if: 422 | /// 423 | /// - Writing to or reading from the connection returns an error 424 | /// - Malformed response data is received 425 | /// - The connection is closed 426 | #[tracing::instrument(skip(self), err)] 427 | pub async fn command(&mut self, command: Command) -> Result 428 | where 429 | IO: AsyncRead + AsyncWrite + Unpin, 430 | { 431 | self.send(command).await?; 432 | 433 | if let Some(r) = self.receive().await? { 434 | Ok(r) 435 | } else { 436 | error!("connection was closed without a response to the command"); 437 | Err(MpdProtocolError::Io(io::Error::new( 438 | io::ErrorKind::UnexpectedEof, 439 | "connection was closed without a response to the command", 440 | ))) 441 | } 442 | } 443 | 444 | /// Send a command list and receive its response(s). 445 | /// 446 | /// This is essentially a shorthand for [`AsyncConnection::send_list`] followed by 447 | /// [`AsyncConnection::receive`]. 448 | /// 449 | /// # Errors 450 | /// 451 | /// This will return an error if: 452 | /// 453 | /// - Writing to or reading from the connection returns an error 454 | /// - Malformed response data is received 455 | /// - The connection is closed 456 | #[tracing::instrument(skip(self), err)] 457 | pub async fn command_list( 458 | &mut self, 459 | command_list: CommandList, 460 | ) -> Result 461 | where 462 | IO: AsyncRead + AsyncWrite + Unpin, 463 | { 464 | self.send_list(command_list).await?; 465 | 466 | if let Some(r) = self.receive().await? { 467 | Ok(r) 468 | } else { 469 | error!("connection was closed without a response to the command"); 470 | Err(MpdProtocolError::Io(io::Error::new( 471 | io::ErrorKind::UnexpectedEof, 472 | "connection was closed without a response to the command", 473 | ))) 474 | } 475 | } 476 | 477 | /// Returns the protocol version the server is using. 478 | pub fn protocol_version(&self) -> &str { 479 | &self.0.protocol_version 480 | } 481 | 482 | /// Extract the connection instance. 483 | pub fn into_inner(self) -> IO { 484 | self.0.io 485 | } 486 | } 487 | 488 | #[cfg(test)] 489 | mod tests_sync { 490 | use assert_matches::assert_matches; 491 | 492 | use super::*; 493 | 494 | fn new_conn(io: IO) -> Connection { 495 | Connection { 496 | io, 497 | field_cache: ResponseFieldCache::new(), 498 | protocol_version: Box::from(""), 499 | recv_buf: BytesMut::zeroed(DEFAULT_BUFFER_CAPACITY), 500 | total_received: 0, 501 | } 502 | } 503 | 504 | #[test] 505 | fn connect() { 506 | let io: &[u8] = b"OK MPD 0.23.3\n"; 507 | let connection = Connection::connect(io).unwrap(); 508 | assert_eq!(connection.protocol_version(), "0.23.3"); 509 | } 510 | 511 | #[test] 512 | fn connect_eof() { 513 | let io: &[u8] = b"OK MPD 0.23.3"; 514 | let connection = Connection::connect(io).unwrap_err(); 515 | assert_matches!(connection, MpdProtocolError::Io(e) if e.kind() == io::ErrorKind::UnexpectedEof); 516 | } 517 | 518 | #[test] 519 | fn connect_invalid() { 520 | let io: &[u8] = b"foobar\n"; 521 | let connection = Connection::connect(io).unwrap_err(); 522 | assert_matches!(connection, MpdProtocolError::InvalidMessage); 523 | } 524 | 525 | #[test] 526 | fn send() { 527 | let mut io = Vec::new(); 528 | let mut connection = new_conn(&mut io); 529 | 530 | connection 531 | .send(Command::new("foo").argument("bar")) 532 | .unwrap(); 533 | 534 | assert_eq!(io, b"foo bar\n"); 535 | } 536 | 537 | #[test] 538 | fn send_list() { 539 | let mut io = Vec::new(); 540 | let mut connection = new_conn(&mut io); 541 | 542 | let list = CommandList::new(Command::new("foo")).command(Command::new("bar")); 543 | 544 | connection.send_list(list).unwrap(); 545 | 546 | assert_eq!( 547 | io, 548 | b"command_list_ok_begin\n\ 549 | foo\n\ 550 | bar\n\ 551 | command_list_end\n" 552 | ); 553 | } 554 | 555 | #[test] 556 | fn receive() { 557 | let io: &[u8] = b"foo: bar\nOK\n"; 558 | let mut connection = new_conn(io); 559 | 560 | let response = connection.receive(); 561 | 562 | assert_matches!(response, Ok(Some(_))); 563 | } 564 | 565 | #[test] 566 | fn receive_eof() { 567 | let io: &[u8] = b"foo: bar\nOK"; 568 | let mut connection = new_conn(io); 569 | 570 | let response = connection.receive(); 571 | 572 | assert_matches!(response, Err(MpdProtocolError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof); 573 | } 574 | } 575 | 576 | #[cfg(test)] 577 | #[cfg(feature = "async")] 578 | mod tests_async { 579 | use assert_matches::assert_matches; 580 | use tokio_test::io::Builder as MockBuilder; 581 | 582 | use super::*; 583 | 584 | fn new_conn(io: IO) -> AsyncConnection { 585 | AsyncConnection(Connection { 586 | io, 587 | field_cache: ResponseFieldCache::new(), 588 | protocol_version: Box::from(""), 589 | recv_buf: BytesMut::new(), 590 | total_received: 0, 591 | }) 592 | } 593 | 594 | #[tokio::test] 595 | async fn connect() { 596 | let io = MockBuilder::new().read(b"OK MPD 0.23.3\n").build(); 597 | let connection = AsyncConnection::connect(io).await.unwrap(); 598 | assert_eq!(connection.protocol_version(), "0.23.3"); 599 | } 600 | 601 | #[tokio::test] 602 | async fn connect_split_read() { 603 | let io = MockBuilder::new() 604 | .read(b"OK MPD 0.23.3") 605 | .read(b"\n") 606 | .build(); 607 | let connection = AsyncConnection::connect(io).await.unwrap(); 608 | assert_eq!(connection.protocol_version(), "0.23.3"); 609 | } 610 | 611 | #[tokio::test] 612 | async fn connect_eof() { 613 | let io = MockBuilder::new().read(b"OK MPD 0.23.3").build(); // no newline 614 | let connection = AsyncConnection::connect(io).await.unwrap_err(); 615 | assert_matches!(connection, MpdProtocolError::Io(e) if e.kind() == io::ErrorKind::UnexpectedEof); 616 | } 617 | 618 | #[tokio::test] 619 | async fn connect_invalid() { 620 | let io = MockBuilder::new().read(b"OK foobar\n").build(); 621 | let connection = AsyncConnection::connect(io).await.unwrap_err(); 622 | assert_matches!(connection, MpdProtocolError::InvalidMessage); 623 | } 624 | 625 | #[tokio::test] 626 | async fn send_single() { 627 | let io = MockBuilder::new().write(b"status\n").build(); 628 | let mut connection = new_conn(io); 629 | 630 | connection.send(Command::new("status")).await.unwrap(); 631 | } 632 | 633 | #[tokio::test] 634 | async fn send_list() { 635 | let list = CommandList::new(Command::new("foo")).command(Command::new("bar")); 636 | let io = MockBuilder::new() 637 | .write( 638 | b"command_list_ok_begin\n\ 639 | foo\n\ 640 | bar\n\ 641 | command_list_end\n", 642 | ) 643 | .build(); 644 | let mut connection = new_conn(io); 645 | 646 | connection.send_list(list).await.unwrap(); 647 | } 648 | 649 | #[tokio::test] 650 | async fn send_list_single() { 651 | let list = CommandList::new(Command::new("foo")); 652 | let io = MockBuilder::new().write(b"foo\n").build(); // skips command list wrapping 653 | let mut connection = new_conn(io); 654 | 655 | connection.send_list(list).await.unwrap(); 656 | } 657 | 658 | #[tokio::test] 659 | async fn receive() { 660 | let io = MockBuilder::new().read(b"foo: bar\nOK\n").build(); 661 | let mut connection = new_conn(io); 662 | 663 | let response = connection.receive().await.unwrap(); 664 | 665 | assert_matches!(response, Some(response) if response.is_success()); 666 | } 667 | 668 | #[tokio::test] 669 | async fn receive_split_read() { 670 | let io = MockBuilder::new().read(b"foo: bar\nOK").read(b"\n").build(); 671 | let mut connection = new_conn(io); 672 | 673 | let response = connection.receive().await.unwrap(); 674 | 675 | assert_matches!(response, Some(response) if response.is_success()); 676 | } 677 | 678 | #[tokio::test] 679 | async fn receive_eof_clean() { 680 | let io = MockBuilder::new().build(); 681 | let mut connection = new_conn(io); 682 | 683 | let response = connection.receive().await.unwrap(); 684 | 685 | assert_eq!(response, None); 686 | } 687 | 688 | #[tokio::test] 689 | async fn receive_eof() { 690 | let io = MockBuilder::new().read(b"foo: bar\n").build(); 691 | let mut connection = new_conn(io); 692 | 693 | let error = connection.receive().await.unwrap_err(); 694 | 695 | assert_matches!(error, MpdProtocolError::Io(e) if e.kind() == io::ErrorKind::UnexpectedEof); 696 | } 697 | 698 | #[tokio::test] 699 | async fn receive_multiple() { 700 | let io = MockBuilder::new().read(b"OK\nOK\n").build(); 701 | let mut connection = new_conn(io); 702 | 703 | let response = connection.receive().await.unwrap(); 704 | assert_matches!(response, Some(response) if response.is_success()); 705 | 706 | let response = connection.receive().await.unwrap(); 707 | assert_matches!(response, Some(response) if response.is_success()); 708 | 709 | let response = connection.receive().await.unwrap(); 710 | assert_matches!(response, None); 711 | } 712 | 713 | #[tokio::test] 714 | async fn command() { 715 | let io = MockBuilder::new() 716 | .write(b"foo\n") 717 | .read(b"bar: baz\nOK\n") 718 | .build(); 719 | let mut connection = new_conn(io); 720 | 721 | let resp = connection.command(Command::new("foo")).await.unwrap(); 722 | assert_eq!(resp.field_count(), 1); 723 | } 724 | } 725 | --------------------------------------------------------------------------------