├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── options.rs ├── examples └── example.rs ├── rustfmt.toml ├── src ├── client.rs ├── convert.rs ├── error.rs ├── idle.rs ├── lib.rs ├── macros.rs ├── message.rs ├── mount.rs ├── output.rs ├── playlist.rs ├── plugin.rs ├── proto.rs ├── reply.rs ├── search.rs ├── song.rs ├── stats.rs ├── status.rs ├── sticker.rs └── version.rs └── tests ├── data └── silence.flac ├── helpers ├── daemon.rs └── mod.rs ├── idle.rs ├── options.rs ├── outputs.rs ├── playback.rs ├── playlist.rs ├── reflect.rs ├── search.rs ├── song.rs └── stickers.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | # Make sure CI fails on all warnings, including Clippy lints 10 | RUSTFLAGS: "-Dwarnings" 11 | 12 | jobs: 13 | build_and_test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | toolchain: 18 | - stable 19 | - beta 20 | - nightly 21 | steps: 22 | - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 23 | - uses: awalsh128/cache-apt-pkgs-action@1850ee53f6e706525805321a3f2f863dcf73c962 24 | with: 25 | packages: mpd 26 | - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} 27 | - run: cargo build --verbose 28 | - run: cargo test --verbose 29 | - if: matrix.toolchain == 'nightly' 30 | run: rustup component add rustfmt && cargo fmt --all -- --check 31 | - if: matrix.toolchain == 'stable' 32 | run: cargo clippy --all-features 33 | - if: matrix.toolchain == 'stable' 34 | run: cargo doc 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Konstantin Stepanov "] 3 | description = "A client library for MPD (music player daemon), like libmpdclient but in Rust" 4 | documentation = "http://kstep.me/rust-mpd/mpd/index.html" 5 | homepage = "https://github.com/kstep/rust-mpd" 6 | license = "MIT/Apache-2.0" 7 | name = "mpd" 8 | repository = "https://github.com/kstep/rust-mpd.git" 9 | version = "0.1.0" 10 | edition = "2018" 11 | 12 | [dependencies] 13 | bufstream = { version = "0.1", default-features = false } 14 | serde = { version = "1", features = ["derive"], optional = true } 15 | serde_repr = { version = "0.1", optional = true } 16 | 17 | [dev-dependencies] 18 | tempfile = "3.8.1" 19 | 20 | [features] 21 | serde = ["dep:serde", "dep:serde_repr"] 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Konstantin Stepanov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-mpd 2 | 3 | Pure Rust version of [libmpdclient](http://www.musicpd.org/libs/libmpdclient/). 4 | 5 | [Full documentation](http://docs.rs/mpd/) 6 | 7 | ## Example 8 | 9 | Add to `Cargo.toml`: 10 | 11 | ```toml 12 | [dependencies] 13 | mpd = "*" 14 | ``` 15 | 16 | Then just use: 17 | 18 | ```rust 19 | extern crate mpd; 20 | 21 | use mpd::Client; 22 | 23 | let mut conn = Client::connect("127.0.0.1:6600").unwrap(); 24 | conn.volume(100).unwrap(); 25 | conn.load("My Lounge Playlist", ..).unwrap(); 26 | conn.play().unwrap(); 27 | println!("Status: {:?}", conn.status()); 28 | ``` 29 | 30 | ## License 31 | 32 | Licensed under either of 33 | 34 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 35 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 36 | 37 | at your option. 38 | 39 | ### Contribution 40 | 41 | Unless you explicitly state otherwise, any contribution intentionally submitted 42 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 43 | additional terms or conditions. 44 | -------------------------------------------------------------------------------- /benches/options.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate mpd; 4 | extern crate test; 5 | 6 | use std::os::unix::net::UnixStream; 7 | use test::{black_box, Bencher}; 8 | 9 | #[bench] 10 | fn status(b: &mut Bencher) { 11 | let mut mpd = mpd::Client::::new(UnixStream::connect("/var/run/mpd/socket").unwrap()).unwrap(); 12 | b.iter(|| { 13 | black_box(mpd.status()).unwrap(); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /examples/example.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | use mpd::{Client, Query}; 4 | 5 | fn main() -> Result<(), Box> { 6 | let mut c = Client::connect("127.0.0.1:6600").unwrap(); 7 | println!("version: {:?}", c.version); 8 | println!("status: {:?}", c.status()); 9 | println!("stuff: {:?}", c.find(&Query::new(), (1, 2))); 10 | 11 | let now_playing = c.currentsong()?; 12 | if let Some(song) = now_playing { 13 | println!("Metadata:"); 14 | for (k, v) in (c.readcomments(song)?).flatten() { 15 | println!("{}: {}", k, v); 16 | } 17 | } else { 18 | println!("No song playing."); 19 | } 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | max_width = 140 3 | fn_call_width = 120 4 | tab_spaces = 4 5 | fn_params_layout = "Compressed" 6 | where_single_line = true 7 | reorder_imports = true 8 | indent_style = "Block" 9 | chain_width = 120 10 | reorder_modules = false 11 | use_small_heuristics = "Max" 12 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! This module defines client data structure — the main entry point to MPD communication 2 | //! 3 | //! Almost every method of the [`Client`] structure corresponds to some command in [MPD protocol][proto]. 4 | //! 5 | //! [proto]: https://www.musicpd.org/doc/protocol/ 6 | 7 | use bufstream::BufStream; 8 | 9 | use crate::convert::*; 10 | use crate::error::{Error, ParseError, ProtoError, Result}; 11 | use crate::message::{Channel, Message}; 12 | use crate::mount::{Mount, Neighbor}; 13 | use crate::output::Output; 14 | use crate::playlist::Playlist; 15 | use crate::plugin::Plugin; 16 | use crate::proto::*; 17 | use crate::search::{Query, Term, Window}; 18 | use crate::song::{Id, Song}; 19 | use crate::stats::Stats; 20 | use crate::status::{ReplayGain, Status}; 21 | use crate::sticker::Sticker; 22 | use crate::version::Version; 23 | 24 | use std::collections::HashMap; 25 | use std::convert::From; 26 | use std::io::{BufRead, Lines, Read, Write}; 27 | use std::net::{TcpStream, ToSocketAddrs}; 28 | 29 | // Client {{{ 30 | 31 | /// Client connection 32 | #[derive(Debug)] 33 | pub struct Client 34 | where S: Read + Write 35 | { 36 | socket: BufStream, 37 | /// MPD protocol version 38 | pub version: Version, 39 | } 40 | 41 | impl Default for Client { 42 | fn default() -> Client { 43 | Client::::connect("127.0.0.1:6600").unwrap() 44 | } 45 | } 46 | 47 | impl Client { 48 | /// Connect client to some IP address 49 | pub fn connect(addr: A) -> Result> { 50 | TcpStream::connect(addr).map_err(Error::Io).and_then(Client::new) 51 | } 52 | } 53 | 54 | impl Client { 55 | // Constructors {{{ 56 | /// Create client from some arbitrary pre-connected socket 57 | pub fn new(socket: S) -> Result> { 58 | let mut socket = BufStream::new(socket); 59 | 60 | let mut banner = String::new(); 61 | socket.read_line(&mut banner)?; 62 | 63 | if !banner.starts_with("OK MPD ") { 64 | return Err(From::from(ProtoError::BadBanner)); 65 | } 66 | 67 | let version = banner[7..].trim().parse::()?; 68 | 69 | Ok(Client { socket, version }) 70 | } 71 | // }}} 72 | 73 | // Playback options & status {{{ 74 | /// Get MPD status 75 | pub fn status(&mut self) -> Result { 76 | self.run_command("command_list_begin", ()) 77 | .and_then(|_| self.run_command("status", ())) 78 | .and_then(|_| self.run_command("replay_gain_status", ())) 79 | .and_then(|_| self.run_command("command_list_end", ())) 80 | .and_then(|_| self.read_struct()) 81 | } 82 | 83 | /// Get MPD playing statistics 84 | pub fn stats(&mut self) -> Result { 85 | self.run_command("stats", ()).and_then(|_| self.read_struct()) 86 | } 87 | 88 | /// Clear error state 89 | pub fn clearerror(&mut self) -> Result<()> { 90 | self.run_command("clearerror", ()).and_then(|_| self.expect_ok()) 91 | } 92 | 93 | /// Set volume 94 | pub fn volume(&mut self, volume: i8) -> Result<()> { 95 | self.run_command("setvol", volume).and_then(|_| self.expect_ok()) 96 | } 97 | 98 | /// Set repeat state 99 | pub fn repeat(&mut self, value: bool) -> Result<()> { 100 | self.run_command("repeat", value as u8).and_then(|_| self.expect_ok()) 101 | } 102 | 103 | /// Set random state 104 | pub fn random(&mut self, value: bool) -> Result<()> { 105 | self.run_command("random", value as u8).and_then(|_| self.expect_ok()) 106 | } 107 | 108 | /// Set single state 109 | pub fn single(&mut self, value: bool) -> Result<()> { 110 | self.run_command("single", value as u8).and_then(|_| self.expect_ok()) 111 | } 112 | 113 | /// Set consume state 114 | pub fn consume(&mut self, value: bool) -> Result<()> { 115 | self.run_command("consume", value as u8).and_then(|_| self.expect_ok()) 116 | } 117 | 118 | /// Set crossfade time in seconds 119 | pub fn crossfade(&mut self, value: T) -> Result<()> { 120 | self.run_command("crossfade", value.to_seconds()).and_then(|_| self.expect_ok()) 121 | } 122 | 123 | /// Set mixramp level in dB 124 | pub fn mixrampdb(&mut self, value: f32) -> Result<()> { 125 | self.run_command("mixrampdb", value).and_then(|_| self.expect_ok()) 126 | } 127 | 128 | /// Set mixramp delay in seconds 129 | pub fn mixrampdelay(&mut self, value: T) -> Result<()> { 130 | self.run_command("mixrampdelay", value.to_seconds()).and_then(|_| self.expect_ok()) 131 | } 132 | 133 | /// Set replay gain mode 134 | pub fn replaygain(&mut self, gain: ReplayGain) -> Result<()> { 135 | self.run_command("replay_gain_mode", gain).and_then(|_| self.expect_ok()) 136 | } 137 | // }}} 138 | 139 | // Playback control {{{ 140 | /// Start playback 141 | pub fn play(&mut self) -> Result<()> { 142 | self.run_command("play", ()).and_then(|_| self.expect_ok()) 143 | } 144 | 145 | /// Start playback from given song in a queue 146 | pub fn switch(&mut self, place: T) -> Result<()> { 147 | let command = if T::is_id() { "playid" } else { "play" }; 148 | self.run_command(command, place.to_place()).and_then(|_| self.expect_ok()) 149 | } 150 | 151 | /// Switch to a next song in queue 152 | #[cfg_attr(feature = "cargo-clippy", allow(clippy::should_implement_trait))] 153 | pub fn next(&mut self) -> Result<()> { 154 | self.run_command("next", ()).and_then(|_| self.expect_ok()) 155 | } 156 | 157 | /// Switch to a previous song in queue 158 | pub fn prev(&mut self) -> Result<()> { 159 | self.run_command("previous", ()).and_then(|_| self.expect_ok()) 160 | } 161 | 162 | /// Stop playback 163 | pub fn stop(&mut self) -> Result<()> { 164 | self.run_command("stop", ()).and_then(|_| self.expect_ok()) 165 | } 166 | 167 | /// Toggle pause state 168 | pub fn toggle_pause(&mut self) -> Result<()> { 169 | self.run_command("pause", ()).and_then(|_| self.expect_ok()) 170 | } 171 | 172 | /// Set pause state 173 | pub fn pause(&mut self, value: bool) -> Result<()> { 174 | self.run_command("pause", value as u8).and_then(|_| self.expect_ok()) 175 | } 176 | 177 | /// Seek to a given place (in seconds) in a given song 178 | pub fn seek(&mut self, place: P, pos: T) -> Result<()> { 179 | let command = if P::is_id() { "seekid" } else { "seek" }; 180 | self.run_command(command, (place.to_place(), pos.to_seconds())).and_then(|_| self.expect_ok()) 181 | } 182 | 183 | /// Seek to a given place (in seconds) in the current song 184 | pub fn rewind(&mut self, pos: T) -> Result<()> { 185 | self.run_command("seekcur", pos.to_seconds()).and_then(|_| self.expect_ok()) 186 | } 187 | // }}} 188 | 189 | // Queue control {{{ 190 | /// List given song or range of songs in a play queue 191 | pub fn songs(&mut self, pos: T) -> Result> { 192 | let command = if T::is_id() { "playlistid" } else { "playlistinfo" }; 193 | self.run_command(command, pos.to_range()).and_then(|_| self.read_structs("file")) 194 | } 195 | 196 | /// List all songs in a play queue 197 | pub fn queue(&mut self) -> Result> { 198 | self.run_command("playlistinfo", ()).and_then(|_| self.read_structs("file")) 199 | } 200 | 201 | /// Lists all songs in the database 202 | pub fn listall(&mut self) -> Result> { 203 | self.run_command("listall", ()).and_then(|_| self.read_structs("file")) 204 | } 205 | 206 | /// Lists all songs in the database with metadata 207 | pub fn listallinfo(&mut self) -> Result> { 208 | self.run_command("listallinfo", ()).and_then(|_| self.read_structs("file")) 209 | } 210 | 211 | /// Get current playing song 212 | pub fn currentsong(&mut self) -> Result> { 213 | self.run_command("currentsong", ()) 214 | .and_then(|_| self.read_struct::()) 215 | .map(|s| if s.place.is_none() { None } else { Some(s) }) 216 | } 217 | 218 | /// gets the song wrt to songid in the playlist 219 | pub fn playlistid(&mut self, id: Id) -> Result> { 220 | self.run_command("playlistid", id) 221 | .and_then(|_| self.read_struct::()) 222 | .map(|s| if s.place.is_none() { None } else { Some(s) }) 223 | } 224 | 225 | /// Clear current queue 226 | pub fn clear(&mut self) -> Result<()> { 227 | self.run_command("clear", ()).and_then(|_| self.expect_ok()) 228 | } 229 | 230 | /// List all changes in a queue since given version 231 | pub fn changes(&mut self, version: u32) -> Result> { 232 | self.run_command("plchanges", version).and_then(|_| self.read_structs("file")) 233 | } 234 | 235 | /// Append a song into a queue 236 | pub fn push(&mut self, path: P) -> Result { 237 | self.run_command("addid", path).and_then(|_| self.read_field("Id")).map(Id) 238 | } 239 | 240 | /// Insert a song into a given position in a queue 241 | pub fn insert(&mut self, path: P, pos: usize) -> Result { 242 | self.run_command("addid", (path, pos)).and_then(|_| self.read_field("Id")) 243 | } 244 | 245 | /// Delete a song (at some position) or several songs (in a range) from a queue 246 | pub fn delete(&mut self, pos: T) -> Result<()> { 247 | let command = if T::is_id() { "deleteid" } else { "delete" }; 248 | self.run_command(command, pos.to_range()).and_then(|_| self.expect_ok()) 249 | } 250 | 251 | /// Move a song (at a some position) or several songs (in a range) to other position in queue 252 | pub fn shift(&mut self, from: T, to: usize) -> Result<()> { 253 | let command = if T::is_id() { "moveid" } else { "move" }; 254 | self.run_command(command, (from.to_range(), to)).and_then(|_| self.expect_ok()) 255 | } 256 | 257 | /// Swap to songs in a queue 258 | pub fn swap(&mut self, one: T, two: T) -> Result<()> { 259 | let command = if T::is_id() { "swapid" } else { "swap" }; 260 | self.run_command(command, (one.to_place(), two.to_place())).and_then(|_| self.expect_ok()) 261 | } 262 | 263 | /// Shuffle queue in a given range (use `..` to shuffle full queue) 264 | pub fn shuffle(&mut self, range: T) -> Result<()> { 265 | self.run_command("shuffle", range.to_range()).and_then(|_| self.expect_ok()) 266 | } 267 | 268 | /// Set song priority in a queue 269 | pub fn priority(&mut self, pos: T, prio: u8) -> Result<()> { 270 | let command = if T::is_id() { "prioid" } else { "prio" }; 271 | self.run_command(command, (prio, pos.to_range())).and_then(|_| self.expect_ok()) 272 | } 273 | 274 | /// Set song range (in seconds) to play 275 | /// 276 | /// Doesn't work for currently playing song. 277 | pub fn range(&mut self, song: T, range: R) -> Result<()> { 278 | self.run_command("rangeid", (song.to_song_id(), range.to_range())).and_then(|_| self.expect_ok()) 279 | } 280 | 281 | /// Add tag to a song 282 | pub fn tag(&mut self, song: T, tag: &str, value: &str) -> Result<()> { 283 | self.run_command("addtagid", (song.to_song_id(), tag, value)).and_then(|_| self.expect_ok()) 284 | } 285 | 286 | /// Delete tag from a song 287 | pub fn untag(&mut self, song: T, tag: &str) -> Result<()> { 288 | self.run_command("cleartagid", (song.to_song_id(), tag)).and_then(|_| self.expect_ok()) 289 | } 290 | // }}} 291 | 292 | // Connection settings {{{ 293 | /// Just pings MPD server, does nothing 294 | pub fn ping(&mut self) -> Result<()> { 295 | self.run_command("ping", ()).and_then(|_| self.expect_ok()) 296 | } 297 | 298 | /// Close MPD connection 299 | pub fn close(&mut self) -> Result<()> { 300 | self.run_command("close", ()).and_then(|_| self.expect_ok()) 301 | } 302 | 303 | /// Kill MPD server 304 | pub fn kill(&mut self) -> Result<()> { 305 | self.run_command("kill", ()).and_then(|_| self.expect_ok()) 306 | } 307 | 308 | /// Login to MPD server with given password 309 | pub fn login(&mut self, password: &str) -> Result<()> { 310 | self.run_command("password", password).and_then(|_| self.expect_ok()) 311 | } 312 | // }}} 313 | 314 | // Playlist methods {{{ 315 | /// List all playlists 316 | pub fn playlists(&mut self) -> Result> { 317 | self.run_command("listplaylists", ()).and_then(|_| self.read_structs("playlist")) 318 | } 319 | 320 | /// List all songs in a playlist 321 | pub fn playlist(&mut self, name: N) -> Result> { 322 | self.run_command("listplaylistinfo", name.to_name()).and_then(|_| self.read_structs("file")) 323 | } 324 | 325 | /// Load playlist into queue 326 | /// 327 | /// You can give either full range (`..`) to load all songs in a playlist, 328 | /// or some partial range to load only part of playlist. 329 | pub fn load(&mut self, name: N, range: T) -> Result<()> { 330 | self.run_command("load", (name.to_name(), range.to_range())).and_then(|_| self.expect_ok()) 331 | } 332 | 333 | /// Save current queue into playlist 334 | /// 335 | /// If playlist with given name doesn't exist, create new one. 336 | pub fn save(&mut self, name: N) -> Result<()> { 337 | self.run_command("save", name.to_name()).and_then(|_| self.expect_ok()) 338 | } 339 | 340 | /// Rename playlist 341 | pub fn pl_rename(&mut self, name: N, newname: &str) -> Result<()> { 342 | self.run_command("rename", (name.to_name(), newname)).and_then(|_| self.expect_ok()) 343 | } 344 | 345 | /// Clear playlist 346 | pub fn pl_clear(&mut self, name: N) -> Result<()> { 347 | self.run_command("playlistclear", name.to_name()).and_then(|_| self.expect_ok()) 348 | } 349 | 350 | /// Delete playlist 351 | pub fn pl_remove(&mut self, name: N) -> Result<()> { 352 | self.run_command("rm", name.to_name()).and_then(|_| self.expect_ok()) 353 | } 354 | 355 | /// Add new songs to a playlist 356 | pub fn pl_push(&mut self, name: N, path: P) -> Result<()> { 357 | self.run_command("playlistadd", (name.to_name(), path)).and_then(|_| self.expect_ok()) 358 | } 359 | 360 | /// Delete a song at a given position in a playlist 361 | pub fn pl_delete(&mut self, name: N, pos: u32) -> Result<()> { 362 | self.run_command("playlistdelete", (name.to_name(), pos)).and_then(|_| self.expect_ok()) 363 | } 364 | 365 | /// Move song in a playlist from one position into another 366 | pub fn pl_shift(&mut self, name: N, from: u32, to: u32) -> Result<()> { 367 | self.run_command("playlistmove", (name.to_name(), from, to)).and_then(|_| self.expect_ok()) 368 | } 369 | // }}} 370 | 371 | // Database methods {{{ 372 | /// Run database rescan, i.e. remove non-existing files from DB 373 | /// as well as add new files to DB 374 | pub fn rescan(&mut self) -> Result { 375 | self.run_command("rescan", ()).and_then(|_| self.read_field("updating_db")) 376 | } 377 | 378 | /// Run database update, i.e. remove non-existing files from DB 379 | pub fn update(&mut self) -> Result { 380 | self.run_command("update", ()).and_then(|_| self.read_field("updating_db")) 381 | } 382 | // }}} 383 | 384 | // Database search {{{ 385 | // TODO: count tag needle [...] [group] [grouptag], find type what [...] [window start:end] 386 | // TODO: search type what [...] [window start:end], searchadd type what [...] 387 | // TODO: listfiles [uri] 388 | // TODO: list type [filtertype] [filterwhat] [...] [group] [grouptype] [...] 389 | // TODO: searchaddpl name type what [...] 390 | 391 | /// List all songs/directories in directory 392 | pub fn listfiles(&mut self, song_path: &str) -> Result> { 393 | self.run_command("listfiles", song_path).and_then(|_| self.read_pairs().collect()) 394 | } 395 | 396 | /// Find songs matching Query conditions. 397 | pub fn find(&mut self, query: &Query, window: W) -> Result> 398 | where W: Into { 399 | self.find_generic("find", query, window.into()) 400 | } 401 | 402 | /// Find album art for file 403 | pub fn albumart(&mut self, path: &P) -> Result> { 404 | let mut buf = vec![]; 405 | loop { 406 | self.run_command("albumart", (path, &*format!("{}", buf.len())))?; 407 | let (_, size) = self.read_pair()?; 408 | let (_, bytes) = self.read_pair()?; 409 | let mut chunk = self.read_bytes(bytes.parse()?)?; 410 | buf.append(&mut chunk); 411 | // Read empty newline 412 | let _ = self.read_line()?; 413 | let result = self.read_line()?; 414 | if result != "OK" { 415 | return Err(ProtoError::NotOk)?; 416 | } 417 | 418 | if size.parse::()? == buf.len() { 419 | break; 420 | } 421 | } 422 | Ok(buf) 423 | } 424 | 425 | /// Case-insensitively search for songs matching Query conditions. 426 | pub fn search(&mut self, query: &Query, window: W) -> Result> 427 | where W: Into { 428 | self.find_generic("search", query, window.into()) 429 | } 430 | 431 | fn find_generic(&mut self, cmd: &str, query: &Query, window: Window) -> Result> { 432 | self.run_command(cmd, (query, window)).and_then(|_| self.read_structs("file")) 433 | } 434 | 435 | /// Lists unique tags values of the specified type for songs matching the given query. 436 | // TODO: list type [filtertype] [filterwhat] [...] [group] [grouptype] [...] 437 | // It isn't clear if or how `group` works 438 | pub fn list(&mut self, term: &Term, query: &Query) -> Result> { 439 | self.run_command("list", (term, query)).and_then(|_| self.read_pairs().map(|p| p.map(|p| p.1)).collect()) 440 | } 441 | 442 | /// Find all songs in the db that match query and adds them to current playlist. 443 | pub fn findadd(&mut self, query: &Query) -> Result<()> { 444 | self.run_command("findadd", query).and_then(|_| self.expect_ok()) 445 | } 446 | 447 | /// Lists the contents of a directory. 448 | pub fn lsinfo(&mut self, path: P) -> Result> { 449 | self.run_command("lsinfo", path).and_then(|_| self.read_structs("file")) 450 | } 451 | 452 | /// Returns raw metadata for file 453 | pub fn readcomments(&mut self, path: P) -> Result> + '_> { 454 | self.run_command("readcomments", path)?; 455 | Ok(self.read_pairs()) 456 | } 457 | 458 | // }}} 459 | 460 | // Output methods {{{ 461 | /// List all outputs 462 | pub fn outputs(&mut self) -> Result> { 463 | self.run_command("outputs", ()).and_then(|_| self.read_structs("outputid")) 464 | } 465 | 466 | /// Set given output enabled state 467 | pub fn output(&mut self, id: T, state: bool) -> Result<()> { 468 | if state { 469 | self.out_enable(id) 470 | } else { 471 | self.out_disable(id) 472 | } 473 | } 474 | 475 | /// Disable given output 476 | pub fn out_disable(&mut self, id: T) -> Result<()> { 477 | self.run_command("disableoutput", id.to_output_id()).and_then(|_| self.expect_ok()) 478 | } 479 | 480 | /// Enable given output 481 | pub fn out_enable(&mut self, id: T) -> Result<()> { 482 | self.run_command("enableoutput", id.to_output_id()).and_then(|_| self.expect_ok()) 483 | } 484 | 485 | /// Toggle given output 486 | pub fn out_toggle(&mut self, id: T) -> Result<()> { 487 | self.run_command("toggleoutput", id.to_output_id()).and_then(|_| self.expect_ok()) 488 | } 489 | // }}} 490 | 491 | // Reflection methods {{{ 492 | /// Get current music directory 493 | pub fn music_directory(&mut self) -> Result { 494 | self.run_command("config", ()).and_then(|_| self.read_field("music_directory")) 495 | } 496 | 497 | /// List all available commands 498 | pub fn commands(&mut self) -> Result> { 499 | self.run_command("commands", ()).and_then(|_| self.read_list("command")) 500 | } 501 | 502 | /// List all forbidden commands 503 | pub fn notcommands(&mut self) -> Result> { 504 | self.run_command("notcommands", ()).and_then(|_| self.read_list("command")) 505 | } 506 | 507 | /// List all available URL handlers 508 | pub fn urlhandlers(&mut self) -> Result> { 509 | self.run_command("urlhandlers", ()).and_then(|_| self.read_list("handler")) 510 | } 511 | 512 | /// List all supported tag types 513 | pub fn tagtypes(&mut self) -> Result> { 514 | self.run_command("tagtypes", ()).and_then(|_| self.read_list("tagtype")) 515 | } 516 | 517 | /// List all available decoder plugins 518 | pub fn decoders(&mut self) -> Result> { 519 | self.run_command("decoders", ()).and_then(|_| self.read_struct()) 520 | } 521 | // }}} 522 | 523 | // Messaging {{{ 524 | /// List all channels available for current connection 525 | pub fn channels(&mut self) -> Result> { 526 | self.run_command("channels", ()) 527 | .and_then(|_| self.read_list("channel")) 528 | .map(|v| v.into_iter().map(|b| unsafe { Channel::new_unchecked(b) }).collect()) 529 | } 530 | 531 | /// Read queued messages from subscribed channels 532 | pub fn readmessages(&mut self) -> Result> { 533 | self.run_command("readmessages", ()).and_then(|_| self.read_structs("channel")) 534 | } 535 | 536 | /// Send a message to a channel 537 | pub fn sendmessage(&mut self, channel: Channel, message: &str) -> Result<()> { 538 | self.run_command("sendmessage", (channel, message)).and_then(|_| self.expect_ok()) 539 | } 540 | 541 | /// Subscribe to a channel 542 | pub fn subscribe(&mut self, channel: Channel) -> Result<()> { 543 | self.run_command("subscribe", channel).and_then(|_| self.expect_ok()) 544 | } 545 | 546 | /// Unsubscribe to a channel 547 | pub fn unsubscribe(&mut self, channel: Channel) -> Result<()> { 548 | self.run_command("unsubscribe", channel).and_then(|_| self.expect_ok()) 549 | } 550 | // }}} 551 | 552 | // Mount methods {{{ 553 | /// List all (virtual) mounts 554 | /// 555 | /// These mounts exist inside MPD process only, thus they can work without root permissions. 556 | pub fn mounts(&mut self) -> Result> { 557 | self.run_command("listmounts", ()).and_then(|_| self.read_structs("mount")) 558 | } 559 | 560 | /// List all network neighbors, which can be potentially mounted 561 | pub fn neighbors(&mut self) -> Result> { 562 | self.run_command("listneighbors", ()).and_then(|_| self.read_structs("neighbor")) 563 | } 564 | 565 | /// Mount given neighbor to a mount point 566 | /// 567 | /// The mount exists inside MPD process only, thus it can work without root permissions. 568 | pub fn mount(&mut self, path: &str, uri: &str) -> Result<()> { 569 | self.run_command("mount", (path, uri)).and_then(|_| self.expect_ok()) 570 | } 571 | 572 | /// Unmount given active (virtual) mount 573 | /// 574 | /// The mount exists inside MPD process only, thus it can work without root permissions. 575 | pub fn unmount(&mut self, path: &str) -> Result<()> { 576 | self.run_command("unmount", path).and_then(|_| self.expect_ok()) 577 | } 578 | // }}} 579 | 580 | // Sticker methods {{{ 581 | /// Show sticker value for a given object, identified by type and uri 582 | pub fn sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result { 583 | self.run_command("sticker get", (typ, uri, name)) 584 | // TODO: This should parse to a `Sticker` type. 585 | .and_then(|_| self.read_field::("sticker")) 586 | .map(|s| s.value) 587 | } 588 | 589 | /// Set sticker value for a given object, identified by type and uri 590 | pub fn set_sticker(&mut self, typ: &str, uri: &str, name: &str, value: &str) -> Result<()> { 591 | self.run_command("sticker set", (typ, uri, name, value)).and_then(|_| self.expect_ok()) 592 | } 593 | 594 | /// Delete sticker from a given object, identified by type and uri 595 | pub fn delete_sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result<()> { 596 | self.run_command("sticker delete", (typ, uri, name)).and_then(|_| self.expect_ok()) 597 | } 598 | 599 | /// Remove all stickers from a given object, identified by type and uri 600 | pub fn clear_stickers(&mut self, typ: &str, uri: &str) -> Result<()> { 601 | self.run_command("sticker delete", (typ, uri)).and_then(|_| self.expect_ok()) 602 | } 603 | 604 | /// List all stickers from a given object, identified by type and uri 605 | pub fn stickers(&mut self, typ: &str, uri: &str) -> Result> { 606 | self.run_command("sticker list", (typ, uri)) 607 | .and_then(|_| self.read_list("sticker")) 608 | .map(|v| v.into_iter().map(|b| b.split_once('=').map(|x| x.1.to_owned()).unwrap()).collect()) 609 | } 610 | 611 | /// List all stickers from a given object in a map, identified by type and uri 612 | pub fn stickers_map(&mut self, typ: &str, uri: &str) -> Result> { 613 | self.run_command("sticker list", (typ, uri)).and_then(|_| self.read_list("sticker")).map(|v| { 614 | v.into_iter() 615 | .map(|b| { 616 | let mut iter = b.splitn(2, '='); 617 | 618 | (iter.next().unwrap().to_owned(), iter.next().unwrap().to_owned()) 619 | }) 620 | .collect() 621 | }) 622 | } 623 | 624 | /// List all (file, sticker) pairs for sticker name and objects of given type 625 | /// from given directory (identified by uri) 626 | pub fn find_sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result> { 627 | self.run_command("sticker find", (typ, uri, name)).and_then(|_| { 628 | self.read_pairs() 629 | .split("file") 630 | .map(|rmap| { 631 | rmap.map(|map| { 632 | ( 633 | map.iter().find_map(|(k, v)| if k == "file" { Some(v.to_owned()) } else { None }).unwrap(), 634 | map.iter() 635 | .find_map(|(k, v)| if k == "sticker" { Some(v.to_owned()) } else { None }) 636 | .and_then(|s| s.split_once('=').map(|x| x.1.to_owned())) 637 | .unwrap(), 638 | ) 639 | }) 640 | }) 641 | .collect() 642 | }) 643 | } 644 | 645 | /// List all files of a given type under given directory (identified by uri) 646 | /// with a tag set to given value 647 | pub fn find_sticker_eq(&mut self, typ: &str, uri: &str, name: &str, value: &str) -> Result> { 648 | self.run_command("sticker find", (typ, uri, name, "=", value)).and_then(|_| self.read_list("file")) 649 | } 650 | // }}} 651 | } 652 | 653 | // Helper methods {{{ 654 | impl Proto for Client { 655 | type Stream = S; 656 | 657 | fn read_bytes(&mut self, bytes: usize) -> Result> { 658 | let mut buf = Vec::with_capacity(bytes); 659 | let mut chunk = (&mut self.socket).take(bytes as u64); 660 | chunk.read_to_end(&mut buf)?; 661 | Ok(buf) 662 | } 663 | 664 | fn read_line(&mut self) -> Result { 665 | let mut buf = Vec::new(); 666 | self.socket.read_until(b'\n', &mut buf)?; 667 | if buf.ends_with(&[b'\n']) { 668 | buf.pop(); 669 | } 670 | let str = String::from_utf8(buf) 671 | .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "stream did not contain valid UTF-8"))?; 672 | Ok(str) 673 | } 674 | 675 | fn read_pairs(&mut self) -> Pairs>> { 676 | Pairs((&mut self.socket).lines()) 677 | } 678 | 679 | fn read_pair(&mut self) -> Result<(String, String)> { 680 | let line = self.read_line()?; 681 | let mut split = line.split(": "); 682 | let key = split.next().ok_or(ParseError::BadPair)?; 683 | let val = split.next().ok_or(ParseError::BadPair)?; 684 | Ok((key.to_string(), val.to_string())) 685 | } 686 | 687 | fn run_command(&mut self, command: &str, arguments: I) -> Result<()> 688 | where I: ToArguments { 689 | self.socket 690 | .write_all(command.as_bytes()) 691 | .and_then(|_| arguments.to_arguments(&mut |arg| write!(self.socket, " {}", Quoted(arg)))) 692 | .and_then(|_| self.socket.write(&[0x0a])) 693 | .and_then(|_| self.socket.flush()) 694 | .map_err(From::from) 695 | } 696 | } 697 | // }}} 698 | 699 | // }}} 700 | -------------------------------------------------------------------------------- /src/convert.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | //! These are inner traits to support methods overloading for the `Client` 3 | 4 | use crate::error::{Error, ProtoError}; 5 | use crate::output::Output; 6 | use crate::playlist::Playlist; 7 | use crate::proto::ToArguments; 8 | use crate::song::{self, Id, Song}; 9 | use std::collections::BTreeMap; 10 | use std::ops::{Range, RangeFrom, RangeFull, RangeTo}; 11 | 12 | use std::time::Duration; 13 | 14 | #[doc(hidden)] 15 | pub trait FromMap: Sized { 16 | fn from_map(map: BTreeMap) -> Result; 17 | } 18 | 19 | #[doc(hidden)] 20 | pub trait FromIter: Sized { 21 | fn from_iter>>(iter: I) -> Result; 22 | } 23 | 24 | impl FromIter for T { 25 | fn from_iter>>(iter: I) -> Result { 26 | iter.collect::, _>>().and_then(FromMap::from_map) 27 | } 28 | } 29 | 30 | // Playlist name polymorphisms {{{ 31 | pub trait ToPlaylistName { 32 | fn to_name(&self) -> &str; 33 | } 34 | 35 | impl ToPlaylistName for Playlist { 36 | fn to_name(&self) -> &str { 37 | &self.name 38 | } 39 | } 40 | 41 | impl<'a> ToPlaylistName for &'a Playlist { 42 | fn to_name(&self) -> &str { 43 | &self.name 44 | } 45 | } 46 | 47 | impl<'a> ToPlaylistName for &'a String { 48 | fn to_name(&self) -> &str { 49 | self 50 | } 51 | } 52 | 53 | impl<'a> ToPlaylistName for &'a str { 54 | fn to_name(&self) -> &str { 55 | self 56 | } 57 | } 58 | 59 | impl ToPlaylistName for str { 60 | fn to_name(&self) -> &str { 61 | self 62 | } 63 | } 64 | 65 | impl ToPlaylistName for String { 66 | fn to_name(&self) -> &str { 67 | self 68 | } 69 | } 70 | // }}} 71 | 72 | // Seconds polymorphisms {{{ 73 | pub trait ToSeconds { 74 | fn to_seconds(self) -> f64; 75 | } 76 | 77 | impl ToSeconds for i64 { 78 | fn to_seconds(self) -> f64 { 79 | self as f64 80 | } 81 | } 82 | 83 | impl ToSeconds for f64 { 84 | fn to_seconds(self) -> f64 { 85 | self 86 | } 87 | } 88 | 89 | impl ToSeconds for Duration { 90 | fn to_seconds(self) -> f64 { 91 | self.as_secs_f64() 92 | } 93 | } 94 | // }}} 95 | 96 | // Queue place polymorphisms {{{ 97 | 98 | pub trait IsId { 99 | fn is_id() -> bool { 100 | false 101 | } 102 | } 103 | 104 | pub trait ToQueueRangeOrPlace: IsId { 105 | fn to_range(self) -> String; 106 | } 107 | 108 | pub trait ToQueueRange { 109 | fn to_range(self) -> String; 110 | } 111 | 112 | impl ToQueueRangeOrPlace for T { 113 | fn to_range(self) -> String { 114 | format!("{}", self.to_place()) 115 | } 116 | } 117 | 118 | impl ToQueueRange for Range { 119 | fn to_range(self) -> String { 120 | format!("{}:{}", self.start, self.end) 121 | } 122 | } 123 | 124 | impl ToQueueRangeOrPlace for Range { 125 | fn to_range(self) -> String { 126 | ToQueueRange::to_range(self) 127 | } 128 | } 129 | 130 | impl ToQueueRange for RangeTo { 131 | fn to_range(self) -> String { 132 | format!(":{}", self.end) 133 | } 134 | } 135 | 136 | impl ToQueueRangeOrPlace for RangeTo { 137 | fn to_range(self) -> String { 138 | ToQueueRange::to_range(self) 139 | } 140 | } 141 | 142 | impl ToQueueRange for RangeFrom { 143 | fn to_range(self) -> String { 144 | format!("{}:", self.start) 145 | } 146 | } 147 | 148 | impl ToQueueRangeOrPlace for RangeFrom { 149 | fn to_range(self) -> String { 150 | ToQueueRange::to_range(self) 151 | } 152 | } 153 | 154 | impl ToQueueRange for RangeFull { 155 | fn to_range(self) -> String { 156 | ToQueueRange::to_range(0..) 157 | } 158 | } 159 | 160 | impl ToQueueRangeOrPlace for RangeFull { 161 | fn to_range(self) -> String { 162 | ToQueueRange::to_range(self) 163 | } 164 | } 165 | 166 | pub trait ToQueuePlace: IsId { 167 | fn to_place(self) -> u32; 168 | } 169 | 170 | impl ToQueuePlace for Id { 171 | fn to_place(self) -> u32 { 172 | self.0 173 | } 174 | } 175 | 176 | impl ToQueuePlace for u32 { 177 | fn to_place(self) -> u32 { 178 | self 179 | } 180 | } 181 | 182 | impl IsId for u32 {} 183 | impl IsId for Range {} 184 | impl IsId for RangeTo {} 185 | impl IsId for RangeFrom {} 186 | impl IsId for RangeFull {} 187 | impl IsId for Id { 188 | fn is_id() -> bool { 189 | true 190 | } 191 | } 192 | 193 | pub trait ToSongId { 194 | fn to_song_id(&self) -> Id; 195 | } 196 | 197 | impl ToSongId for Song { 198 | fn to_song_id(&self) -> Id { 199 | self.place.unwrap().id 200 | } 201 | } 202 | 203 | impl ToSongId for u32 { 204 | fn to_song_id(&self) -> Id { 205 | Id(*self) 206 | } 207 | } 208 | 209 | impl ToSongId for Id { 210 | fn to_song_id(&self) -> Id { 211 | *self 212 | } 213 | } 214 | // }}} 215 | 216 | // Output id polymorphisms {{{ 217 | pub trait ToOutputId { 218 | fn to_output_id(self) -> u32; 219 | } 220 | 221 | impl ToOutputId for u32 { 222 | fn to_output_id(self) -> u32 { 223 | self 224 | } 225 | } 226 | impl ToOutputId for Output { 227 | fn to_output_id(self) -> u32 { 228 | self.id 229 | } 230 | } 231 | // }}} 232 | 233 | // Song play range polymorphisms {{{ 234 | pub trait ToSongRange { 235 | fn to_range(self) -> song::Range; 236 | } 237 | 238 | impl ToSongRange for Range { 239 | fn to_range(self) -> song::Range { 240 | song::Range(self.start, Some(self.end)) 241 | } 242 | } 243 | 244 | impl ToSongRange for Range { 245 | fn to_range(self) -> song::Range { 246 | song::Range(Duration::from_secs(self.start as u64), Some(Duration::from_secs(self.end as u64))) 247 | } 248 | } 249 | 250 | impl ToSongRange for RangeFrom { 251 | fn to_range(self) -> song::Range { 252 | song::Range(self.start, None) 253 | } 254 | } 255 | 256 | impl ToSongRange for RangeFrom { 257 | fn to_range(self) -> song::Range { 258 | song::Range(Duration::from_secs(self.start as u64), None) 259 | } 260 | } 261 | 262 | impl ToSongRange for RangeTo { 263 | fn to_range(self) -> song::Range { 264 | song::Range(Duration::from_secs(0), Some(self.end)) 265 | } 266 | } 267 | 268 | impl ToSongRange for RangeTo { 269 | fn to_range(self) -> song::Range { 270 | song::Range(Duration::from_secs(0), Some(Duration::from_secs(self.end as u64))) 271 | } 272 | } 273 | 274 | impl ToSongRange for RangeFull { 275 | fn to_range(self) -> song::Range { 276 | song::Range(Duration::from_secs(0), None) 277 | } 278 | } 279 | 280 | impl ToSongRange for song::Range { 281 | fn to_range(self) -> song::Range { 282 | self 283 | } 284 | } 285 | 286 | // }}} 287 | 288 | pub trait ToSongPath { 289 | fn to_path(&self) -> &str; 290 | } 291 | 292 | impl ToSongPath for Song { 293 | fn to_path(&self) -> &str { 294 | &self.file 295 | } 296 | } 297 | 298 | impl<'a, T: ToSongPath> ToSongPath for &'a T { 299 | fn to_path(&self) -> &str { 300 | (*self).to_path() 301 | } 302 | } 303 | 304 | impl ToSongPath for dyn AsRef { 305 | fn to_path(&self) -> &str { 306 | self.as_ref() 307 | } 308 | } 309 | 310 | impl ToArguments for T { 311 | fn to_arguments(&self, f: &mut F) -> Result<(), E> 312 | where F: FnMut(&str) -> Result<(), E> { 313 | self.to_path().to_arguments(f) 314 | } 315 | } 316 | 317 | impl FromIter for String { 318 | fn from_iter>>(iter: I) -> Result { 319 | for res in iter { 320 | let line = res?; 321 | if line.0 == "file" { 322 | return Ok(line.1); 323 | } 324 | } 325 | Err(Error::Proto(ProtoError::NoField("songname"))) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! This module defines different errors occurring during communication with MPD. 2 | //! 3 | //! There're following kinds of possible errors: 4 | //! 5 | //! - IO errors (due to network communication failures), 6 | //! - parsing errors (because of bugs in parsing server response), 7 | //! - protocol errors (happen when we get unexpected data from server, 8 | //! mostly because protocol version mismatch, network data corruption 9 | //! or just bugs in the client), 10 | //! - server errors (run-time errors coming from MPD due to some MPD 11 | //! errors, like database failures or sound problems) 12 | //! 13 | //! This module defines all necessary infrastructure to represent these kinds or errors. 14 | 15 | use std::convert::From; 16 | use std::error::Error as StdError; 17 | use std::fmt; 18 | use std::io::Error as IoError; 19 | use std::num::{ParseFloatError, ParseIntError}; 20 | use std::result; 21 | use std::str::FromStr; 22 | use std::string::ParseError as StringParseError; 23 | use std::time::TryFromFloatSecsError; 24 | 25 | // Server errors {{{ 26 | /// Server error codes, as defined in [libmpdclient](https://www.musicpd.org/doc/libmpdclient/protocol_8h_source.html) 27 | #[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] 28 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] 29 | #[repr(u8)] 30 | pub enum ErrorCode { 31 | /// not a list 32 | NotList = 1, 33 | /// bad command arguments 34 | Argument = 2, 35 | /// invalid password 36 | Password = 3, 37 | /// insufficient permissions 38 | Permission = 4, 39 | /// unknown command 40 | UnknownCmd = 5, 41 | /// object doesn't exist 42 | NoExist = 50, 43 | /// maximum playlist size exceeded 44 | PlaylistMax = 51, 45 | /// general system error 46 | System = 52, 47 | /// error loading playlist 48 | PlaylistLoad = 53, 49 | /// update database is already in progress 50 | UpdateAlready = 54, 51 | /// player synchronization error 52 | PlayerSync = 55, 53 | /// object already exists 54 | Exist = 56, 55 | } 56 | 57 | impl FromStr for ErrorCode { 58 | type Err = ParseError; 59 | fn from_str(s: &str) -> result::Result { 60 | use self::ErrorCode::*; 61 | match s.parse()? { 62 | 1 => Ok(NotList), 63 | 2 => Ok(Argument), 64 | 3 => Ok(Password), 65 | 4 => Ok(Permission), 66 | 5 => Ok(UnknownCmd), 67 | 68 | 50 => Ok(NoExist), 69 | 51 => Ok(PlaylistMax), 70 | 52 => Ok(System), 71 | 53 => Ok(PlaylistLoad), 72 | 54 => Ok(UpdateAlready), 73 | 55 => Ok(PlayerSync), 74 | 56 => Ok(Exist), 75 | 76 | v => Err(ParseError::BadErrorCode(v)), 77 | } 78 | } 79 | } 80 | 81 | impl StdError for ErrorCode {} 82 | 83 | impl fmt::Display for ErrorCode { 84 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | use self::ErrorCode as E; 86 | 87 | let desc = match *self { 88 | E::NotList => "not a list", 89 | E::Argument => "invalid argument", 90 | E::Password => "invalid password", 91 | E::Permission => "permission", 92 | E::UnknownCmd => "unknown command", 93 | 94 | E::NoExist => "item not found", 95 | E::PlaylistMax => "playlist overflow", 96 | E::System => "system", 97 | E::PlaylistLoad => "playload load", 98 | E::UpdateAlready => "already updating", 99 | E::PlayerSync => "player syncing", 100 | E::Exist => "already exists", 101 | }; 102 | 103 | f.write_str(desc) 104 | } 105 | } 106 | 107 | /// Server error 108 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 109 | #[derive(Debug, Clone, PartialEq)] 110 | pub struct ServerError { 111 | /// server error code 112 | pub code: ErrorCode, 113 | /// command position in command list 114 | pub pos: u16, 115 | /// command name, which caused the error 116 | pub command: String, 117 | /// detailed error description 118 | pub detail: String, 119 | } 120 | 121 | impl StdError for ServerError {} 122 | 123 | impl fmt::Display for ServerError { 124 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 125 | write!(f, "{} error (`{}') at {}", self.code, self.detail, self.pos) 126 | } 127 | } 128 | 129 | impl FromStr for ServerError { 130 | type Err = ParseError; 131 | fn from_str(s: &str) -> result::Result { 132 | // ACK [@] {} 133 | if let Some(s) = s.strip_prefix("ACK [") { 134 | if let (Some(atsign), Some(right_bracket)) = (s.find('@'), s.find(']')) { 135 | match (s[..atsign].parse(), s[atsign + 1..right_bracket].parse()) { 136 | (Ok(code), Ok(pos)) => { 137 | let s = &s[right_bracket + 1..]; 138 | if let (Some(left_brace), Some(right_brace)) = (s.find('{'), s.find('}')) { 139 | let command = s[left_brace + 1..right_brace].to_string(); 140 | let detail = s[right_brace + 1..].trim().to_string(); 141 | Ok(ServerError { code, pos, command, detail }) 142 | } else { 143 | Err(ParseError::NoMessage) 144 | } 145 | } 146 | (Err(_), _) => Err(ParseError::BadCode), 147 | (_, Err(_)) => Err(ParseError::BadPos), 148 | } 149 | } else { 150 | Err(ParseError::NoCodePos) 151 | } 152 | } else { 153 | Err(ParseError::NotAck) 154 | } 155 | } 156 | } 157 | // }}} 158 | 159 | // Error {{{ 160 | /// Main error type, describing all possible error classes for the crate 161 | #[derive(Debug)] 162 | pub enum Error { 163 | /// IO errors (low-level network communication failures) 164 | Io(IoError), 165 | /// parsing errors (unknown data came from server) 166 | Parse(ParseError), 167 | /// protocol errors (e.g. missing required fields in server response, no handshake message etc.) 168 | Proto(ProtoError), 169 | /// server errors (a.k.a. `ACK` responses from server) 170 | Server(ServerError), 171 | } 172 | 173 | /// Shortcut type for MPD results 174 | pub type Result = result::Result; 175 | 176 | impl StdError for Error { 177 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 178 | match *self { 179 | Error::Io(ref err) => Some(err), 180 | Error::Parse(ref err) => Some(err), 181 | Error::Proto(ref err) => Some(err), 182 | Error::Server(ref err) => Some(err), 183 | } 184 | } 185 | } 186 | 187 | impl fmt::Display for Error { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | match *self { 190 | Error::Io(ref err) => err.fmt(f), 191 | Error::Parse(ref err) => err.fmt(f), 192 | Error::Proto(ref err) => err.fmt(f), 193 | Error::Server(ref err) => err.fmt(f), 194 | } 195 | } 196 | } 197 | 198 | impl From for Error { 199 | fn from(e: IoError) -> Error { 200 | Error::Io(e) 201 | } 202 | } 203 | impl From for Error { 204 | fn from(e: ParseError) -> Error { 205 | Error::Parse(e) 206 | } 207 | } 208 | impl From for Error { 209 | fn from(e: ProtoError) -> Error { 210 | Error::Proto(e) 211 | } 212 | } 213 | impl From for Error { 214 | fn from(e: ParseIntError) -> Error { 215 | Error::Parse(ParseError::BadInteger(e)) 216 | } 217 | } 218 | impl From for Error { 219 | fn from(e: ParseFloatError) -> Error { 220 | Error::Parse(ParseError::BadFloat(e)) 221 | } 222 | } 223 | impl From for Error { 224 | fn from(e: TryFromFloatSecsError) -> Error { 225 | Error::Parse(ParseError::BadDuration(e)) 226 | } 227 | } 228 | impl From for Error { 229 | fn from(e: ServerError) -> Error { 230 | Error::Server(e) 231 | } 232 | } 233 | 234 | // }}} 235 | 236 | // Parse errors {{{ 237 | /// Parsing error kinds 238 | #[derive(Debug, Clone, PartialEq)] 239 | pub enum ParseError { 240 | /// invalid integer 241 | BadInteger(ParseIntError), 242 | /// invalid float 243 | BadFloat(ParseFloatError), 244 | /// invalid duration (negative, too big or not a number) 245 | BadDuration(TryFromFloatSecsError), 246 | /// some other invalid value 247 | BadValue(String), 248 | /// invalid version format (should be x.y.z) 249 | BadVersion, 250 | /// the response is not an `ACK` (not an error) 251 | /// (this is not actually an error, just a marker 252 | /// to try to parse the response as some other type, 253 | /// like a pair) 254 | NotAck, 255 | /// invalid pair 256 | BadPair, 257 | /// invalid error code in `ACK` response 258 | BadCode, 259 | /// invalid command position in `ACK` response 260 | BadPos, 261 | /// missing command position and/or error code in `ACK` response 262 | NoCodePos, 263 | /// missing error message in `ACK` response 264 | NoMessage, 265 | /// missing bitrate in audio format field 266 | NoRate, 267 | /// missing bits in audio format field 268 | NoBits, 269 | /// missing channels in audio format field 270 | NoChans, 271 | /// invalid bitrate in audio format field 272 | BadRate(ParseIntError), 273 | /// invalid bits in audio format field 274 | BadBits(ParseIntError), 275 | /// invalid channels in audio format field 276 | BadChans(ParseIntError), 277 | /// unknown state in state status field 278 | BadState(String), 279 | /// unknown error code in `ACK` response 280 | BadErrorCode(usize), 281 | } 282 | 283 | impl StdError for ParseError {} 284 | 285 | impl fmt::Display for ParseError { 286 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 287 | use self::ParseError as E; 288 | 289 | let desc = match *self { 290 | E::BadInteger(_) => "invalid integer", 291 | E::BadFloat(_) => "invalid float", 292 | E::BadDuration(_) => "invalid duration", 293 | E::BadValue(_) => "invalid value", 294 | E::BadVersion => "invalid version", 295 | E::NotAck => "not an ACK", 296 | E::BadPair => "invalid pair", 297 | E::BadCode => "invalid code", 298 | E::BadPos => "invalid position", 299 | E::NoCodePos => "missing code and position", 300 | E::NoMessage => "missing position", 301 | E::NoRate => "missing audio format rate", 302 | E::NoBits => "missing audio format bits", 303 | E::NoChans => "missing audio format channels", 304 | E::BadRate(_) => "invalid audio format rate", 305 | E::BadBits(_) => "invalid audio format bits", 306 | E::BadChans(_) => "invalid audio format channels", 307 | E::BadState(_) => "invalid playing state", 308 | E::BadErrorCode(_) => "unknown error code", 309 | }; 310 | 311 | write!(f, "{}", desc) 312 | } 313 | } 314 | 315 | impl From for ParseError { 316 | fn from(e: ParseIntError) -> ParseError { 317 | ParseError::BadInteger(e) 318 | } 319 | } 320 | 321 | impl From for ParseError { 322 | fn from(e: ParseFloatError) -> ParseError { 323 | ParseError::BadFloat(e) 324 | } 325 | } 326 | 327 | impl From for ParseError { 328 | fn from(e: TryFromFloatSecsError) -> ParseError { 329 | ParseError::BadDuration(e) 330 | } 331 | } 332 | 333 | impl From for ParseError { 334 | fn from(e: StringParseError) -> ParseError { 335 | match e {} 336 | } 337 | } 338 | // }}} 339 | 340 | // Protocol errors {{{ 341 | /// Protocol errors 342 | /// 343 | /// They usually occur when server violate expected command response format, 344 | /// like missing fields in answer to some command, missing closing `OK` 345 | /// line after data stream etc. 346 | #[derive(Debug, Clone, PartialEq)] 347 | pub enum ProtoError { 348 | /// `OK` was expected, but it was missing 349 | NotOk, 350 | /// a data pair was expected 351 | NotPair, 352 | /// invalid handshake banner received 353 | BadBanner, 354 | /// expected some field, but it was missing 355 | NoField(&'static str), 356 | /// expected sticker value, but didn't find it 357 | BadSticker, 358 | } 359 | 360 | impl StdError for ProtoError {} 361 | 362 | impl fmt::Display for ProtoError { 363 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 364 | let desc = match *self { 365 | ProtoError::NotOk => "OK expected", 366 | ProtoError::NotPair => "pair expected", 367 | ProtoError::BadBanner => "banner error", 368 | ProtoError::NoField(_) => "missing field", 369 | ProtoError::BadSticker => "sticker error", 370 | }; 371 | 372 | write!(f, "{}", desc) 373 | } 374 | } 375 | // }}} 376 | -------------------------------------------------------------------------------- /src/idle.rs: -------------------------------------------------------------------------------- 1 | //! The module defines structures and protocols for asynchronous MPD communication 2 | //! 3 | //! The MPD supports very simple protocol for asynchronous client notifications about 4 | //! different player events. First user issues [`idle`](Client::idle) command 5 | //! with optional argument to filter events by source subsystem (like 6 | //! "database", "player", "mixer" etc.) 7 | //! 8 | //! Once in "idle" mode, client connection timeout is disabled, and MPD will notify 9 | //! client about next event when one occurs (if originated from one of designated 10 | //! subsystems, if specified). 11 | //! 12 | //! (Actually MPD notifies only about general subsystem source of event, e.g. 13 | //! if user changed volume, client will get [`mixer`](Subsystem::Mixer) event 14 | //! in idle mode, so it should issue [`status`](Client::status) command then and 15 | //! check for any mixer-related field changes.) 16 | //! 17 | //! Once some such event occurs, and client is notified about it, idle mode is interrupted, 18 | //! and client must issue another `idle` command to continue listening for interesting 19 | //! events. 20 | //! 21 | //! While in "idle" mode, client can't issue any commands, except for special `noidle` 22 | //! command, which interrupts "idle" mode, and provides a list queued events 23 | //! since last `idle` command, if they occurred. 24 | //! 25 | //! The module describes subsystems enum only, but the main workflow is determined by 26 | //! [`IdleGuard`] struct, which catches mutable reference 27 | //! to original [`Client`] struct, thus enforcing MPD contract in regards of (im)possibility 28 | //! to send commands while in "idle" mode. 29 | 30 | use crate::client::Client; 31 | use crate::error::{Error, ParseError}; 32 | use crate::proto::Proto; 33 | 34 | use std::fmt; 35 | use std::io::{Read, Write}; 36 | use std::mem::forget; 37 | use std::str::FromStr; 38 | 39 | /// Subsystems for `idle` command 40 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 41 | #[derive(Clone, Copy, Debug, PartialEq)] 42 | pub enum Subsystem { 43 | /// database: the song database has been modified after update. 44 | Database, 45 | /// update: a database update has started or finished. 46 | /// If the database was modified during the update, the database event is also emitted. 47 | Update, 48 | /// stored_playlist: a stored playlist has been modified, renamed, created or deleted 49 | Playlist, 50 | /// playlist: the current playlist has been modified 51 | Queue, 52 | /// player: the player has been started, stopped or seeked 53 | Player, 54 | /// mixer: the volume has been changed 55 | Mixer, 56 | /// output: an audio output has been enabled or disabled 57 | Output, 58 | /// options: options like repeat, random, crossfade, replay gain 59 | Options, 60 | /// partition: a partition was added, removed or changed 61 | Partition, 62 | /// sticker: the sticker database has been modified. 63 | Sticker, 64 | /// subscription: a client has subscribed or unsubscribed to a channel 65 | Subscription, 66 | /// message: a message was received on a channel this client is subscribed to; this event is only emitted when the queue is empty 67 | Message, 68 | /// neighbor: a neighbor was found or lost 69 | Neighbor, 70 | /// mount: the mount list has changed 71 | Mount, 72 | } 73 | 74 | impl FromStr for Subsystem { 75 | type Err = ParseError; 76 | fn from_str(s: &str) -> Result { 77 | use self::Subsystem::*; 78 | match s { 79 | "database" => Ok(Database), 80 | "update" => Ok(Update), 81 | "stored_playlist" => Ok(Playlist), 82 | "playlist" => Ok(Queue), 83 | "player" => Ok(Player), 84 | "mixer" => Ok(Mixer), 85 | "output" => Ok(Output), 86 | "options" => Ok(Options), 87 | "partition" => Ok(Partition), 88 | "sticker" => Ok(Sticker), 89 | "subscription" => Ok(Subscription), 90 | "message" => Ok(Message), 91 | "neighbor" => Ok(Neighbor), 92 | "mount" => Ok(Mount), 93 | _ => Err(ParseError::BadValue(s.to_owned())), 94 | } 95 | } 96 | } 97 | 98 | impl Subsystem { 99 | fn to_str(self) -> &'static str { 100 | use self::Subsystem as S; 101 | match self { 102 | S::Database => "database", 103 | S::Update => "update", 104 | S::Playlist => "stored_playlist", 105 | S::Queue => "playlist", 106 | S::Player => "player", 107 | S::Mixer => "mixer", 108 | S::Output => "output", 109 | S::Options => "options", 110 | S::Partition => "partition", 111 | S::Sticker => "sticker", 112 | S::Subscription => "subscription", 113 | S::Message => "message", 114 | S::Neighbor => "neighbor", 115 | S::Mount => "mount", 116 | } 117 | } 118 | } 119 | 120 | impl fmt::Display for Subsystem { 121 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 122 | f.write_str(self.to_str()) 123 | } 124 | } 125 | 126 | use std::result::Result as StdResult; 127 | impl crate::proto::ToArguments for Subsystem { 128 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 129 | where F: FnMut(&str) -> StdResult<(), E> { 130 | f(self.to_str()) 131 | } 132 | } 133 | 134 | /// "Idle" mode guard enforcing MPD asynchronous events protocol 135 | pub struct IdleGuard<'a, S: 'a + Read + Write>(&'a mut Client); 136 | 137 | impl<'a, S: 'a + Read + Write> IdleGuard<'a, S> { 138 | /// Get list of subsystems with new events, interrupting idle mode in process 139 | pub fn get(self) -> Result, Error> { 140 | let result = self.0.read_list("changed").and_then(|v| v.into_iter().map(|b| b.parse().map_err(From::from)).collect()); 141 | forget(self); 142 | result 143 | } 144 | } 145 | 146 | impl<'a, S: 'a + Read + Write> Drop for IdleGuard<'a, S> { 147 | fn drop(&mut self) { 148 | let _ = self.0.run_command("noidle", ()).map(|_| self.0.drain()); 149 | } 150 | } 151 | 152 | /// This trait implements `idle` command of MPD protocol 153 | /// 154 | /// See module's documentation for details. 155 | pub trait Idle { 156 | /// Stream type of a client 157 | type Stream: Read + Write; 158 | 159 | /// Start listening for events from a set of subsystems 160 | /// 161 | /// If empty subsystems slice is given, wait for all event from any subsystem. 162 | /// 163 | /// This method returns `IdleGuard`, which takes mutable reference of an initial client, 164 | /// thus disallowing any operations on this mpd connection. 165 | /// 166 | /// You can call `.get()` method of this struct to stop waiting and get all queued events 167 | /// matching given subsystems filter. This call consumes a guard, stops waiting 168 | /// and releases client object. 169 | /// 170 | /// If the guard goes out of scope, wait lock is released as well, but all queued events 171 | /// will be silently ignored. 172 | fn idle<'a>(&'a mut self, subsystems: &[Subsystem]) -> Result, Error>; 173 | 174 | /// Wait for events from a set of subsystems and return list of affected subsystems 175 | /// 176 | /// This is a blocking operation. If empty subsystems slice is given, 177 | /// wait for all event from any subsystem. 178 | fn wait(&mut self, subsystems: &[Subsystem]) -> Result, Error> { 179 | self.idle(subsystems).and_then(IdleGuard::get) 180 | } 181 | } 182 | 183 | impl Idle for Client { 184 | type Stream = S; 185 | fn idle<'a>(&'a mut self, subsystems: &[Subsystem]) -> Result, Error> { 186 | self.run_command("idle", subsystems)?; 187 | Ok(IdleGuard(self)) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! MPD client for Rust 4 | //! 5 | //! This crate tries to provide idiomatic Rust API for [Music Player Daemon][mpd]. 6 | //! The main entry point to the API is [`Client`] struct, and inherent methods 7 | //! of the struct follow [MPD protocol][proto] for most part, making use of 8 | //! traits to overload different parameters for convenience. 9 | //! 10 | //! [mpd]: https://www.musicpd.org/ 11 | //! [proto]: https://www.musicpd.org/doc/protocol/ 12 | //! 13 | //! # Usage 14 | //! 15 | //! ```text 16 | //! [dependencies] 17 | //! mpd = "*" 18 | //! ``` 19 | //! 20 | //! ```rust,no_run 21 | //! extern crate mpd; 22 | //! 23 | //! use mpd::Client; 24 | //! use std::net::TcpStream; 25 | //! 26 | //! # fn main() { 27 | //! let mut conn = Client::connect("127.0.0.1:6600").unwrap(); 28 | //! conn.volume(100).unwrap(); 29 | //! conn.load("My Lounge Playlist", ..).unwrap(); 30 | //! conn.play().unwrap(); 31 | //! println!("Status: {:?}", conn.status()); 32 | //! # } 33 | //! ``` 34 | 35 | mod macros; 36 | mod convert; 37 | pub mod error; 38 | pub mod version; 39 | pub mod reply; 40 | pub mod status; 41 | pub mod song; 42 | pub mod output; 43 | pub mod playlist; 44 | pub mod plugin; 45 | pub mod stats; 46 | pub mod search; 47 | pub mod message; 48 | pub mod idle; 49 | pub mod mount; 50 | mod sticker; 51 | 52 | mod proto; 53 | pub mod client; 54 | 55 | pub use client::Client; 56 | pub use idle::{Idle, Subsystem}; 57 | pub use message::{Channel, Message}; 58 | pub use mount::{Mount, Neighbor}; 59 | pub use output::Output; 60 | pub use playlist::Playlist; 61 | pub use plugin::Plugin; 62 | pub use search::{Query, Term}; 63 | pub use song::{Id, Song}; 64 | pub use stats::Stats; 65 | pub use status::{ReplayGain, State, Status}; 66 | pub use version::Version; 67 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #![macro_use] 2 | 3 | macro_rules! get_field_impl { 4 | ($op:ident, $map:expr, bool $name:expr) => { 5 | $map.$op($name).ok_or(Error::Proto(ProtoError::NoField($name))).map(|v| v == "1")? 6 | }; 7 | ($op:ident, $map:expr, opt $name:expr) => { 8 | $map.$op($name).map(|v| v.parse().map(Some)).unwrap_or(Ok(None))? 9 | }; 10 | ($op:ident, $map:expr, $name:expr) => { 11 | $map.$op($name) 12 | .ok_or(Error::Proto(ProtoError::NoField($name))) 13 | .and_then(|v| v.parse().map_err(|e| Error::Parse(From::from(e))))? 14 | }; 15 | } 16 | 17 | macro_rules! get_field { 18 | ($map:expr, bool $name:expr) => { get_field_impl!(get, $map, bool $name) }; 19 | ($map:expr, opt $name:expr) => { get_field_impl!(get, $map, opt $name) }; 20 | ($map:expr, $name:expr) => { get_field_impl!(get, $map, $name) } 21 | } 22 | 23 | #[allow(unused_macros)] 24 | macro_rules! pop_field { 25 | ($map:expr, bool $name:expr) => { get_field_impl!(remove, $map, bool $name) }; 26 | ($map:expr, opt $name:expr) => { get_field_impl!(remove, $map, opt $name) }; 27 | ($map:expr, $name:expr) => { get_field_impl!(remove, $map, $name) } 28 | } 29 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | //! The module defines structures for MPD client-to-client messaging/subscription protocol 2 | //! 3 | //! The MPD client-to-client messaging protocol is fairly easy one, and is based on channels. 4 | //! Any client can subscribe to arbitrary number of channels, and some other client 5 | //! can send messages to a channel by name. Then, at some point of time, subscribed 6 | //! client can read all queued messages for all channels, it was subscribed to. 7 | //! 8 | //! Also client can get asynchronous notifications about new messages from subscribed 9 | //! channels with `idle` command, by waiting for `message` subsystem events. 10 | 11 | use crate::convert::FromMap; 12 | use crate::error::{Error, ProtoError}; 13 | 14 | use std::collections::BTreeMap; 15 | use std::fmt; 16 | 17 | /// Message 18 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 19 | #[derive(Debug, PartialEq, Clone)] 20 | pub struct Message { 21 | /// channel 22 | pub channel: Channel, 23 | /// message payload 24 | pub message: String, 25 | } 26 | 27 | impl FromMap for Message { 28 | fn from_map(map: BTreeMap) -> Result { 29 | Ok(Message { 30 | channel: Channel(map.get("channel").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("channel")))?), 31 | message: map.get("message").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("message")))?, 32 | }) 33 | } 34 | } 35 | 36 | /// Channel 37 | #[derive(Debug, PartialEq, PartialOrd, Clone)] 38 | pub struct Channel(String); 39 | 40 | #[cfg(feature = "serde")] 41 | impl<'de> serde::Deserialize<'de> for Channel { 42 | fn deserialize(deserializer: D) -> Result 43 | where D: serde::Deserializer<'de> { 44 | Ok(Channel(String::deserialize(deserializer)?)) 45 | } 46 | } 47 | 48 | #[cfg(feature = "serde")] 49 | impl serde::Serialize for Channel { 50 | fn serialize(&self, serializer: S) -> Result 51 | where S: serde::Serializer { 52 | serializer.serialize_str(&self.0) 53 | } 54 | } 55 | 56 | impl fmt::Display for Channel { 57 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 58 | fmt::Display::fmt(&self.0, f) 59 | } 60 | } 61 | 62 | impl Channel { 63 | /// Create channel with given name 64 | pub fn new(name: &str) -> Option { 65 | if Channel::is_valid_name(name) { 66 | Some(Channel(name.to_owned())) 67 | } else { 68 | None 69 | } 70 | } 71 | 72 | /// Create channel with arbitrary name, bypassing name validity checks 73 | /// 74 | /// Not recommened! Use [`new()`](Channel::new) method above instead. 75 | /// # Safety 76 | /// Only if [`Channel::is_valid_name(name)`](Channel::is_valid_name) 77 | pub unsafe fn new_unchecked(name: String) -> Channel { 78 | Channel(name) 79 | } 80 | 81 | /// Check if given name is a valid channel name 82 | /// 83 | /// Valid channel name can contain only English letters (`A`-`Z`, `a`-`z`), 84 | /// numbers (`0`-`9`), underscore, forward slash, dot and colon (`_`, `/`, `.`, `:`) 85 | pub fn is_valid_name(name: &str) -> bool { 86 | name.bytes().all(|b| { 87 | (0x61..=0x7a).contains(&b) 88 | || (0x41..=0x5a).contains(&b) 89 | || (0x30..=0x39).contains(&b) 90 | || (b == 0x5f || b == 0x2f || b == 0x2e || b == 0x3a) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/mount.rs: -------------------------------------------------------------------------------- 1 | //! The module describes data structures for MPD (virtual) mounts system 2 | //! 3 | //! This mounts has nothing to do with system-wide Unix mounts, as they are 4 | //! implemented inside MPD only, so they doesn't require root access. 5 | //! 6 | //! The MPD mounts are plugin-based, so MPD can mount any resource as 7 | //! a source of songs for its database (like network shares). 8 | //! 9 | //! Possible, but inactive, mounts are named "neighbors" and can be 10 | //! listed with [`neighbors()`](crate::Client::neighbors) method. 11 | 12 | use crate::convert::FromMap; 13 | use crate::error::{Error, ProtoError}; 14 | 15 | use std::collections::BTreeMap; 16 | 17 | /// Mount point 18 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 19 | #[derive(Clone, Debug, PartialEq)] 20 | pub struct Mount { 21 | /// mount point name 22 | pub name: String, 23 | /// mount storage URI 24 | pub storage: String, 25 | } 26 | 27 | impl FromMap for Mount { 28 | fn from_map(map: BTreeMap) -> Result { 29 | Ok(Mount { 30 | name: map.get("mount").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("mount")))?, 31 | storage: map.get("storage").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("storage")))?, 32 | }) 33 | } 34 | } 35 | 36 | /// Neighbor 37 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 38 | #[derive(Clone, Debug, PartialEq)] 39 | pub struct Neighbor { 40 | /// neighbor name 41 | pub name: String, 42 | /// neighbor storage URI 43 | pub storage: String, 44 | } 45 | 46 | impl FromMap for Neighbor { 47 | fn from_map(map: BTreeMap) -> Result { 48 | Ok(Neighbor { 49 | name: map.get("name").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("name")))?, 50 | storage: map.get("neighbor").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("neighbor")))?, 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | //! The module describes output 2 | 3 | use crate::convert::FromIter; 4 | use crate::error::{Error, ProtoError}; 5 | 6 | /// Sound output 7 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 8 | #[derive(Clone, Debug, PartialEq)] 9 | pub struct Output { 10 | /// id 11 | pub id: u32, 12 | /// name of the output plugin 13 | pub plugin: String, 14 | /// name 15 | pub name: String, 16 | /// enabled state 17 | pub enabled: bool, 18 | /// Runtime-configurable, plugin-specific attributes, such as "dop" for ALSA 19 | pub attributes: Vec<(String, String)> 20 | } 21 | 22 | impl FromIter for Output { 23 | // Implement FromIter directly so that we can parse plugin-specific attributes 24 | fn from_iter>>(iter: I) -> Result { 25 | let mut attributes = Vec::new(); 26 | let mut name: Option = None; // panic if unnamed 27 | let mut plugin: Option = None; // panic if not found 28 | let mut id: u32 = 0; 29 | let mut enabled: bool = false; 30 | 31 | for res in iter { 32 | let line = res?; 33 | match &*line.0 { 34 | "outputid" => { id = line.1.parse::()? }, 35 | "outputname" => { name.replace(line.1); }, 36 | "plugin" => { plugin.replace(line.1); }, 37 | "outputenabled" => enabled = line.1 == "1", 38 | "attribute" => { 39 | let terms: Vec<&str> = line.1.split("=").collect(); 40 | if terms.len() != 2 { 41 | return Err(Error::Proto(ProtoError::NotPair)); 42 | } 43 | attributes.push((terms[0].to_owned(), terms[1].to_owned())); 44 | }, 45 | _ => {} 46 | } 47 | } 48 | 49 | if name.is_none() { 50 | return Err(Error::Proto(ProtoError::NoField("outputname"))); 51 | } 52 | 53 | if plugin.is_none() { 54 | return Err(Error::Proto(ProtoError::NoField("plugin"))); 55 | } 56 | 57 | Ok(Self { 58 | id, 59 | plugin: plugin.unwrap(), 60 | name: name.unwrap(), 61 | enabled, 62 | attributes 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/playlist.rs: -------------------------------------------------------------------------------- 1 | //! The module defines playlist data structures 2 | 3 | use crate::convert::FromMap; 4 | use crate::error::{Error, ProtoError}; 5 | 6 | use std::collections::BTreeMap; 7 | 8 | /// Playlist 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 10 | #[derive(Clone, Debug, PartialEq)] 11 | pub struct Playlist { 12 | /// name 13 | pub name: String, 14 | /// last modified 15 | pub last_mod: String, 16 | } 17 | 18 | impl FromMap for Playlist { 19 | fn from_map(map: BTreeMap) -> Result { 20 | Ok(Playlist { 21 | name: map.get("playlist").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("playlist")))?, 22 | last_mod: map.get("Last-Modified").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("Last-Modified")))?, 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | //! The module defines decoder plugin data structures 2 | 3 | use crate::convert::FromIter; 4 | use crate::error::Error; 5 | 6 | /// Decoder plugin 7 | #[derive(Clone, Debug, PartialEq)] 8 | pub struct Plugin { 9 | /// name 10 | pub name: String, 11 | /// supported file suffixes (extensions) 12 | pub suffixes: Vec, 13 | /// supported MIME-types 14 | pub mime_types: Vec, 15 | } 16 | 17 | impl FromIter for Vec { 18 | fn from_iter>>(iter: I) -> Result { 19 | let mut result = Vec::new(); 20 | let mut plugin: Option = None; 21 | for reply in iter { 22 | let (a, b) = reply?; 23 | match &*a { 24 | "plugin" => { 25 | if let Some(p) = plugin { 26 | result.push(p) 27 | } 28 | 29 | plugin = Some(Plugin { name: b, suffixes: Vec::new(), mime_types: Vec::new() }); 30 | } 31 | "mime_type" => { 32 | if let Some(p) = plugin.as_mut() { 33 | p.mime_types.push(b) 34 | } 35 | } 36 | "suffix" => { 37 | if let Some(p) = plugin.as_mut() { 38 | p.suffixes.push(b) 39 | } 40 | } 41 | _ => unreachable!(), 42 | } 43 | } 44 | if let Some(p) = plugin { 45 | result.push(p) 46 | } 47 | Ok(result) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/proto.rs: -------------------------------------------------------------------------------- 1 | // Hidden internal interface 2 | #![allow(missing_docs)] 3 | 4 | use bufstream::BufStream; 5 | 6 | use crate::convert::FromIter; 7 | use crate::error::{Error, ParseError, ProtoError, Result}; 8 | use crate::reply::Reply; 9 | 10 | use std::fmt; 11 | use std::io::{self, Lines, Read, Write}; 12 | use std::result::Result as StdResult; 13 | use std::str::FromStr; 14 | 15 | pub struct Pairs(pub I); 16 | 17 | impl Iterator for Pairs 18 | where I: Iterator> 19 | { 20 | type Item = Result<(String, String)>; 21 | fn next(&mut self) -> Option> { 22 | let reply: Option> = 23 | self.0.next().map(|v| v.map_err(Error::Io).and_then(|s| s.parse::().map_err(Error::Parse))); 24 | match reply { 25 | Some(Ok(Reply::Pair(a, b))) => Some(Ok((a, b))), 26 | None | Some(Ok(Reply::Ok)) => None, 27 | Some(Ok(Reply::Ack(e))) => Some(Err(Error::Server(e))), 28 | Some(Err(e)) => Some(Err(e)), 29 | } 30 | } 31 | } 32 | 33 | pub struct Maps<'a, I: 'a> { 34 | pairs: &'a mut Pairs, 35 | sep: &'a str, 36 | value: Option, 37 | done: bool, 38 | first: bool, 39 | } 40 | 41 | impl<'a, I> Iterator for Maps<'a, I> 42 | where I: Iterator> 43 | { 44 | type Item = Result>; 45 | fn next(&mut self) -> Option>> { 46 | if self.done { 47 | return None; 48 | } 49 | 50 | let mut map = Vec::new(); 51 | 52 | if let Some(b) = self.value.take() { 53 | map.push((self.sep.to_owned(), b)); 54 | } 55 | 56 | loop { 57 | match self.pairs.next() { 58 | Some(Ok((a, b))) => { 59 | if &*a == self.sep { 60 | self.value = Some(b); 61 | if self.first { 62 | self.first = false; 63 | return self.next(); 64 | } 65 | break; 66 | } else { 67 | map.push((a, b)); 68 | } 69 | } 70 | Some(Err(e)) => return Some(Err(e)), 71 | None => { 72 | self.done = true; 73 | break; 74 | } 75 | } 76 | } 77 | 78 | if map.is_empty() { 79 | None 80 | } else { 81 | Some(Ok(map)) 82 | } 83 | } 84 | } 85 | 86 | impl Pairs 87 | where I: Iterator> 88 | { 89 | pub fn split<'a, 'b: 'a>(&'a mut self, f: &'b str) -> Maps<'a, I> { 90 | Maps { pairs: self, sep: f, value: None, done: false, first: true } 91 | } 92 | } 93 | 94 | // Client inner communication methods {{{ 95 | #[doc(hidden)] 96 | pub trait Proto { 97 | type Stream: Read + Write; 98 | 99 | fn read_bytes(&mut self, bytes: usize) -> Result>; 100 | fn read_line(&mut self) -> Result; 101 | fn read_pairs(&mut self) -> Pairs>>; 102 | 103 | fn run_command(&mut self, command: &str, arguments: I) -> Result<()> 104 | where I: ToArguments; 105 | 106 | fn read_structs<'a, T>(&'a mut self, key: &'static str) -> Result> 107 | where T: 'a + FromIter { 108 | self.read_pairs().split(key).map(|v| v.and_then(|v| FromIter::from_iter(v.into_iter().map(Ok)))).collect() 109 | } 110 | 111 | fn read_list(&mut self, key: &'static str) -> Result> { 112 | self.read_pairs().filter(|r| r.as_ref().map(|(a, _)| *a == key).unwrap_or(true)).map(|r| r.map(|(_, b)| b)).collect() 113 | } 114 | 115 | fn read_struct<'a, T>(&'a mut self) -> Result 116 | where 117 | T: 'a + FromIter, 118 | Self::Stream: 'a, 119 | { 120 | FromIter::from_iter(self.read_pairs()) 121 | } 122 | 123 | fn drain(&mut self) -> Result<()> { 124 | loop { 125 | let reply = self.read_line()?; 126 | match &*reply { 127 | "OK" | "list_OK" => break, 128 | _ => (), 129 | } 130 | } 131 | Ok(()) 132 | } 133 | 134 | fn expect_ok(&mut self) -> Result<()> { 135 | let line = self.read_line()?; 136 | 137 | match line.parse::() { 138 | Ok(Reply::Ok) => Ok(()), 139 | Ok(Reply::Ack(e)) => Err(Error::Server(e)), 140 | Ok(_) => Err(Error::Proto(ProtoError::NotOk)), 141 | Err(e) => Err(From::from(e)), 142 | } 143 | } 144 | 145 | fn read_pair(&mut self) -> Result<(String, String)> { 146 | let line = self.read_line()?; 147 | 148 | match line.parse::() { 149 | Ok(Reply::Pair(a, b)) => Ok((a, b)), 150 | Ok(Reply::Ok) => Err(Error::Proto(ProtoError::NotPair)), 151 | Ok(Reply::Ack(e)) => Err(Error::Server(e)), 152 | Err(e) => Err(Error::Parse(e)), 153 | } 154 | } 155 | 156 | fn read_field(&mut self, field: &'static str) -> Result 157 | where ParseError: From { 158 | let (a, b) = self.read_pair()?; 159 | self.expect_ok()?; 160 | if &*a == field { 161 | Ok(b.parse::().map_err(Into::::into)?) 162 | } else { 163 | Err(Error::Proto(ProtoError::NoField(field))) 164 | } 165 | } 166 | } 167 | 168 | pub trait ToArguments { 169 | fn to_arguments(&self, _: &mut F) -> StdResult<(), E> 170 | where F: FnMut(&str) -> StdResult<(), E>; 171 | } 172 | 173 | impl ToArguments for () { 174 | fn to_arguments(&self, _: &mut F) -> StdResult<(), E> 175 | where F: FnMut(&str) -> StdResult<(), E> { 176 | Ok(()) 177 | } 178 | } 179 | 180 | impl<'a> ToArguments for &'a str { 181 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 182 | where F: FnMut(&str) -> StdResult<(), E> { 183 | f(self) 184 | } 185 | } 186 | 187 | macro_rules! argument_for_display { 188 | ( $x:path ) => { 189 | impl ToArguments for $x { 190 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 191 | where F: FnMut(&str) -> StdResult<(), E> { 192 | f(&self.to_string()) 193 | } 194 | } 195 | }; 196 | } 197 | argument_for_display! {i8} 198 | argument_for_display! {u8} 199 | argument_for_display! {u32} 200 | argument_for_display! {f32} 201 | argument_for_display! {f64} 202 | argument_for_display! {usize} 203 | argument_for_display! {crate::status::ReplayGain} 204 | argument_for_display! {String} 205 | argument_for_display! {crate::song::Id} 206 | argument_for_display! {crate::song::Range} 207 | argument_for_display! {crate::message::Channel} 208 | 209 | macro_rules! argument_for_tuple { 210 | ( $($t:ident: $T: ident),+ ) => { 211 | impl<$($T : ToArguments,)*> ToArguments for ($($T,)*) { 212 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 213 | where F: FnMut(&str) -> StdResult<(), E> 214 | { 215 | let ($(ref $t,)*) = *self; 216 | $( 217 | $t.to_arguments(f)?; 218 | )* 219 | Ok(()) 220 | } 221 | } 222 | }; 223 | } 224 | argument_for_tuple! {t0: T0} 225 | argument_for_tuple! {t0: T0, t1: T1} 226 | argument_for_tuple! {t0: T0, t1: T1, t2: T2} 227 | argument_for_tuple! {t0: T0, t1: T1, t2: T2, t3: T3} 228 | argument_for_tuple! {t0: T0, t1: T1, t2: T2, t3:T3, t4: T4} 229 | 230 | impl<'a, T: ToArguments> ToArguments for &'a [T] { 231 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 232 | where F: FnMut(&str) -> StdResult<(), E> { 233 | for arg in *self { 234 | arg.to_arguments(f)? 235 | } 236 | Ok(()) 237 | } 238 | } 239 | 240 | pub struct Quoted<'a, D: fmt::Display + 'a + ?Sized>(pub &'a D); 241 | 242 | impl<'a, D: fmt::Display + 'a + ?Sized> fmt::Display for Quoted<'a, D> { 243 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 244 | let unquoted = format!("{}", self.0); 245 | if unquoted.is_empty() { 246 | // return Ok(()); 247 | } 248 | let quoted = unquoted.replace('\\', r"\\").replace('"', r#"\""#); 249 | formatter.write_fmt(format_args!("\"{}\"", "ed)) 250 | } 251 | } 252 | 253 | // }}} 254 | -------------------------------------------------------------------------------- /src/reply.rs: -------------------------------------------------------------------------------- 1 | //! The module describes all possible replies from MPD server. 2 | //! 3 | //! Also it contains most generic parser, which can handle 4 | //! all possible server replies. 5 | 6 | use crate::error::{ParseError, ServerError}; 7 | use std::str::FromStr; 8 | 9 | /// All possible MPD server replies 10 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub enum Reply { 13 | /// `OK` and `list_OK` replies 14 | Ok, 15 | /// `ACK` reply (server error) 16 | Ack(ServerError), 17 | /// a data pair reply (in `field: value` format) 18 | Pair(String, String), 19 | } 20 | 21 | impl FromStr for Reply { 22 | type Err = ParseError; 23 | fn from_str(s: &str) -> Result { 24 | if s == "OK" || s == "list_OK" { 25 | Ok(Reply::Ok) 26 | } else if let Ok(ack) = s.parse::() { 27 | Ok(Reply::Ack(ack)) 28 | } else { 29 | let mut splits = s.splitn(2, ':'); 30 | match (splits.next(), splits.next()) { 31 | (Some(a), Some(b)) => Ok(Reply::Pair(a.to_owned(), b.trim().to_owned())), 32 | _ => Err(ParseError::BadPair), 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | //! The module defines data structures used in MPD search queries. 3 | // TODO: unfinished functionality 4 | 5 | use crate::proto::{Quoted, ToArguments}; 6 | use std::{ 7 | io::Write, // implements write for Vec 8 | borrow::Cow 9 | }; 10 | use std::convert::Into; 11 | use std::fmt; 12 | use std::result::Result as StdResult; 13 | 14 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged, rename_all = "lowercase"))] 15 | pub enum Term<'a> { 16 | Any, 17 | File, 18 | Base, 19 | #[cfg_attr(feature = "serde", serde(rename = "modified-since"))] 20 | LastMod, 21 | Tag(Cow<'a, str>), 22 | } 23 | 24 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged, rename_all = "lowercase"))] 25 | pub enum Operation { 26 | Equals, 27 | NotEquals, 28 | Contains, 29 | #[cfg_attr(feature = "serde", serde(rename = "starts_with"))] 30 | StartsWith 31 | } 32 | 33 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 34 | pub struct Filter<'a> { 35 | typ: Term<'a>, 36 | what: Cow<'a, str>, 37 | how: Operation 38 | } 39 | 40 | impl<'a> Filter<'a> { 41 | pub fn new(typ: Term<'a>, what: W) -> Filter 42 | where W: 'a + Into> { 43 | Filter { 44 | typ, 45 | what: what.into(), 46 | how: Operation::Equals 47 | } 48 | } 49 | 50 | pub fn new_with_op(typ: Term<'a>, what: W, how: Operation) -> Filter 51 | where W: 'a + Into> { 52 | Filter { 53 | typ, 54 | what: what.into(), 55 | how 56 | } 57 | } 58 | } 59 | 60 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 61 | pub struct Window(Option<(u32, u32)>); 62 | 63 | impl From<(u32, u32)> for Window { 64 | fn from(window: (u32, u32)) -> Window { 65 | Window(Some(window)) 66 | } 67 | } 68 | 69 | impl From> for Window { 70 | fn from(window: Option<(u32, u32)>) -> Window { 71 | Window(window) 72 | } 73 | } 74 | 75 | #[derive(Default)] 76 | pub struct Query<'a> { 77 | filters: Vec>, 78 | } 79 | 80 | impl<'a> Query<'a> { 81 | pub fn new() -> Query<'a> { 82 | Query { filters: Vec::new() } 83 | } 84 | 85 | pub fn and<'b: 'a, V: 'b + Into>>(&mut self, term: Term<'b>, value: V) -> &mut Query<'a> { 86 | self.filters.push(Filter::new(term, value)); 87 | self 88 | } 89 | 90 | pub fn and_with_op<'b: 'a, V: 'b + Into>>(&mut self, term: Term<'b>, op: Operation, value: V) -> &mut Query<'a> { 91 | self.filters.push(Filter::new_with_op(term, value, op)); 92 | self 93 | } 94 | } 95 | 96 | impl<'a> fmt::Display for Term<'a> { 97 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 | f.write_str(match *self { 99 | Term::Any => "any", 100 | Term::File => "file", 101 | Term::Base => "base", 102 | Term::LastMod => "modified-since", 103 | Term::Tag(ref tag) => tag, 104 | }) 105 | } 106 | } 107 | 108 | impl<'a> ToArguments for &'a Term<'a> { 109 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 110 | where F: FnMut(&str) -> StdResult<(), E> { 111 | f(&self.to_string()) 112 | } 113 | } 114 | 115 | impl fmt::Display for Operation { 116 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 117 | f.write_str(match *self { 118 | Operation::Equals => "==", 119 | Operation::NotEquals => "!=", 120 | Operation::Contains => "contains", 121 | Operation::StartsWith => "starts_with" 122 | }) 123 | } 124 | } 125 | 126 | impl ToArguments for Operation { 127 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 128 | where F: FnMut(&str) -> StdResult<(), E> { 129 | f(&self.to_string()) 130 | } 131 | } 132 | 133 | impl<'a> ToArguments for &'a Filter<'a> { 134 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 135 | where F: FnMut(&str) -> StdResult<(), E> { 136 | match self.typ { 137 | // For some terms, the filter clause cannot have an operation 138 | Term::Base | Term::LastMod => { 139 | f(&format!( 140 | "({} {})", 141 | &self.typ, 142 | &Quoted(&self.what).to_string() 143 | )) 144 | } 145 | _ => { 146 | f(&format!( 147 | "({} {} {})", 148 | &self.typ, 149 | &self.how, 150 | &Quoted(&self.what).to_string()) 151 | ) 152 | } 153 | } 154 | } 155 | } 156 | 157 | impl<'a> ToArguments for &'a Query<'a> { 158 | // Use MPD 0.21+ filter syntax 159 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 160 | where F: FnMut(&str) -> StdResult<(), E> { 161 | // Construct the query string in its entirety first before escaping 162 | if !self.filters.is_empty() { 163 | let mut qs = String::new(); 164 | for (i, filter) in self.filters.iter().enumerate() { 165 | if i > 0 { 166 | qs.push_str(" AND "); 167 | } 168 | // Leave escaping to the filter since terms should not be escaped or quoted 169 | filter.to_arguments(&mut |arg| { 170 | qs.push_str(arg); 171 | Ok(()) 172 | })?; 173 | } 174 | // println!("Singly escaped query string: {}", &qs); 175 | f(&qs) 176 | } else { 177 | Ok(()) 178 | } 179 | } 180 | } 181 | 182 | impl ToArguments for Window { 183 | fn to_arguments(&self, f: &mut F) -> StdResult<(), E> 184 | where F: FnMut(&str) -> StdResult<(), E> { 185 | if let Some(window) = self.0 { 186 | f("window")?; 187 | f(&format! {"{}:{}", window.0, window.1})?; 188 | } 189 | Ok(()) 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod test { 195 | use super::*; 196 | use crate::proto::ToArguments; 197 | 198 | fn collect(arguments: I) -> Vec { 199 | let mut output = Vec::::new(); 200 | arguments 201 | .to_arguments::<_, ()>(&mut |arg| { 202 | output.push(arg.to_string()); 203 | Ok(()) 204 | }) 205 | .unwrap(); 206 | output 207 | } 208 | 209 | #[test] 210 | fn find_window_format() { 211 | let window: Window = (0, 2).into(); 212 | let output = collect(window); 213 | assert_eq!(output, vec!["window", "0:2"]); 214 | } 215 | 216 | #[test] 217 | fn find_query_format() { 218 | let mut query = Query::new(); 219 | let finished = query.and(Term::Tag("albumartist".into()), "Mac DeMarco").and(Term::Tag("album".into()), "Salad Days"); 220 | let output = collect(&*finished); 221 | assert_eq!(output, vec!["albumartist", "Mac DeMarco", "album", "Salad Days"]); 222 | } 223 | 224 | #[test] 225 | fn multiple_and() { 226 | let mut query = Query::new(); 227 | query.and(Term::Tag("albumartist".into()), "Mac DeMarco"); 228 | query.and(Term::Tag("album".into()), "Salad Days"); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/song.rs: -------------------------------------------------------------------------------- 1 | //! The module defines song structs and methods. 2 | 3 | use crate::convert::FromIter; 4 | use crate::error::{Error, ParseError}; 5 | 6 | use std::fmt; 7 | use std::str::FromStr; 8 | use std::time::Duration; 9 | 10 | /// Song ID 11 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Default)] 12 | pub struct Id(pub u32); 13 | 14 | impl fmt::Display for Id { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | self.0.fmt(f) 17 | } 18 | } 19 | 20 | #[cfg(feature = "serde")] 21 | impl<'de> serde::Deserialize<'de> for Id { 22 | fn deserialize(deserializer: D) -> Result 23 | where D: serde::Deserializer<'de> { 24 | Ok(Id(u32::deserialize(deserializer)?)) 25 | } 26 | } 27 | 28 | #[cfg(feature = "serde")] 29 | impl serde::Serialize for Id { 30 | fn serialize(&self, serializer: S) -> Result 31 | where S: serde::Serializer { 32 | serializer.serialize_u32(self.0) 33 | } 34 | } 35 | 36 | /// Song place in the queue 37 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 38 | #[derive(Debug, Copy, Clone, PartialEq, Default)] 39 | pub struct QueuePlace { 40 | /// song ID 41 | pub id: Id, 42 | /// absolute zero-based song position 43 | pub pos: u32, 44 | /// song priority, if present, defaults to 0 45 | pub prio: u8, 46 | } 47 | 48 | /// Song range 49 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 50 | #[derive(Debug, Copy, Clone, PartialEq)] 51 | pub struct Range(pub Duration, pub Option); 52 | 53 | impl Default for Range { 54 | fn default() -> Range { 55 | Range(Duration::from_secs(0), None) 56 | } 57 | } 58 | 59 | impl fmt::Display for Range { 60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 61 | self.0.as_secs().fmt(f)?; 62 | f.write_str(":")?; 63 | if let Some(v) = self.1 { 64 | v.as_secs().fmt(f)?; 65 | } 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl FromStr for Range { 71 | type Err = ParseError; 72 | fn from_str(s: &str) -> Result { 73 | let mut splits = s.split('-').flat_map(|v| v.parse().into_iter()); 74 | match (splits.next(), splits.next()) { 75 | (Some(s), Some(e)) => Ok(Range(Duration::from_secs(s), Some(Duration::from_secs(e)))), 76 | (None, Some(e)) => Ok(Range(Duration::from_secs(0), Some(Duration::from_secs(e)))), 77 | (Some(s), None) => Ok(Range(Duration::from_secs(s), None)), 78 | (None, None) => Ok(Range(Duration::from_secs(0), None)), 79 | } 80 | } 81 | } 82 | 83 | /// Song data 84 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 85 | #[derive(Debug, Clone, PartialEq, Default)] 86 | pub struct Song { 87 | /// filename 88 | pub file: String, 89 | /// name (for streams) 90 | pub name: Option, 91 | /// title 92 | pub title: Option, 93 | /// last modification time 94 | pub last_mod: Option, 95 | /// artist 96 | pub artist: Option, 97 | /// duration (in seconds resolution) 98 | pub duration: Option, 99 | /// place in the queue (if queued for playback) 100 | pub place: Option, 101 | /// range to play (if queued for playback and range was set) 102 | pub range: Option, 103 | /// arbitrary tags, like album, artist etc 104 | pub tags: Vec<(String, String)>, 105 | } 106 | 107 | impl FromIter for Song { 108 | /// build song from map 109 | fn from_iter>>(iter: I) -> Result { 110 | let mut result = Song::default(); 111 | 112 | for res in iter { 113 | let line = res?; 114 | match &*line.0 { 115 | "file" => result.file = line.1.to_owned(), 116 | "Title" => result.title = Some(line.1.to_owned()), 117 | "Last-Modified" => result.last_mod = Some(line.1.to_owned()), 118 | "Artist" => result.artist = Some(line.1.to_owned()), 119 | "Name" => result.name = Some(line.1.to_owned()), 120 | // Deprecated in MPD. 121 | "Time" => (), 122 | "duration" => result.duration = Some(Duration::try_from_secs_f64(line.1.parse()?)?), 123 | "Range" => result.range = Some(line.1.parse()?), 124 | "Id" => match result.place { 125 | None => result.place = Some(QueuePlace { id: Id(line.1.parse()?), pos: 0, prio: 0 }), 126 | Some(ref mut place) => place.id = Id(line.1.parse()?), 127 | }, 128 | "Pos" => match result.place { 129 | None => result.place = Some(QueuePlace { pos: line.1.parse()?, id: Id(0), prio: 0 }), 130 | Some(ref mut place) => place.pos = line.1.parse()?, 131 | }, 132 | "Prio" => match result.place { 133 | None => result.place = Some(QueuePlace { prio: line.1.parse()?, id: Id(0), pos: 0 }), 134 | Some(ref mut place) => place.prio = line.1.parse()?, 135 | }, 136 | _ => { 137 | result.tags.push((line.0, line.1)); 138 | } 139 | } 140 | } 141 | 142 | Ok(result) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | //! The module describes DB and playback statistics 2 | 3 | use crate::convert::FromIter; 4 | use crate::error::Error; 5 | 6 | use std::time::Duration; 7 | 8 | /// DB and playback statistics 9 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | pub struct Stats { 12 | /// number of artists in DB 13 | pub artists: u32, 14 | /// number of albums in DB 15 | pub albums: u32, 16 | /// number of songs in DB 17 | pub songs: u32, 18 | /// total MPD uptime, seconds resolution 19 | pub uptime: Duration, 20 | /// total playback time, seconds resolution 21 | pub playtime: Duration, 22 | /// total playback time for all songs in DB, seconds resolution 23 | pub db_playtime: Duration, 24 | /// last DB update timestamp in seconds since Epoch, seconds resolution 25 | pub db_update: Duration, 26 | } 27 | 28 | impl Default for Stats { 29 | fn default() -> Stats { 30 | Stats { 31 | artists: 0, 32 | albums: 0, 33 | songs: 0, 34 | uptime: Duration::from_secs(0), 35 | playtime: Duration::from_secs(0), 36 | db_playtime: Duration::from_secs(0), 37 | db_update: Duration::from_secs(0), 38 | } 39 | } 40 | } 41 | 42 | impl FromIter for Stats { 43 | /// build stats from iterator 44 | fn from_iter>>(iter: I) -> Result { 45 | let mut result = Stats::default(); 46 | 47 | for res in iter { 48 | let line = res?; 49 | match &*line.0 { 50 | "artists" => result.artists = line.1.parse()?, 51 | "albums" => result.albums = line.1.parse()?, 52 | "songs" => result.songs = line.1.parse()?, 53 | "uptime" => result.uptime = Duration::from_secs(line.1.parse()?), 54 | "playtime" => result.playtime = Duration::from_secs(line.1.parse()?), 55 | "db_playtime" => result.db_playtime = Duration::from_secs(line.1.parse()?), 56 | "db_update" => result.db_update = Duration::from_secs(line.1.parse()?), 57 | _ => (), 58 | } 59 | } 60 | 61 | Ok(result) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/status.rs: -------------------------------------------------------------------------------- 1 | //! The module defines MPD status data structures 2 | 3 | use crate::convert::FromIter; 4 | use crate::error::{Error, ParseError}; 5 | use crate::song::{Id, QueuePlace}; 6 | 7 | use std::fmt; 8 | use std::str::FromStr; 9 | use std::time::Duration; 10 | 11 | /// MPD status 12 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 13 | #[derive(Debug, PartialEq, Clone, Default)] 14 | pub struct Status { 15 | /// volume (0-100, or -1 if volume is unavailable (e.g. for HTTPD output type) 16 | pub volume: i8, 17 | /// repeat mode 18 | pub repeat: bool, 19 | /// random mode 20 | pub random: bool, 21 | /// single mode 22 | pub single: bool, 23 | /// consume mode 24 | pub consume: bool, 25 | /// queue version number 26 | pub queue_version: u32, 27 | /// queue length 28 | pub queue_len: u32, 29 | /// playback state 30 | pub state: State, 31 | /// currently playing song place in the queue 32 | pub song: Option, 33 | /// next song to play place in the queue 34 | pub nextsong: Option, 35 | /// time current song played, and total song duration (in seconds resolution) 36 | pub time: Option<(Duration, Duration)>, 37 | /// elapsed play time current song played (in milliseconds resolution) 38 | pub elapsed: Option, 39 | /// current song duration 40 | pub duration: Option, 41 | /// current song bitrate, kbps 42 | pub bitrate: Option, 43 | /// crossfade timeout, seconds 44 | pub crossfade: Option, 45 | /// mixramp threshold, dB 46 | pub mixrampdb: f32, 47 | /// mixramp duration, seconds 48 | pub mixrampdelay: Option, 49 | /// current audio playback format 50 | pub audio: Option, 51 | /// current DB updating job number (if DB updating is in progress) 52 | pub updating_db: Option, 53 | /// last player error (if happened, can be reset with 54 | /// [`clearerror()`](crate::Client::clearerror) method) 55 | pub error: Option, 56 | /// replay gain mode 57 | pub replaygain: Option, 58 | } 59 | 60 | impl FromIter for Status { 61 | fn from_iter>>(iter: I) -> Result { 62 | let mut result = Status::default(); 63 | 64 | for res in iter { 65 | let line = res?; 66 | match &*line.0 { 67 | "volume" => result.volume = line.1.parse()?, 68 | 69 | "repeat" => result.repeat = &*line.1 == "1", 70 | "random" => result.random = &*line.1 == "1", 71 | "single" => result.single = &*line.1 == "1", 72 | "consume" => result.consume = &*line.1 == "1", 73 | 74 | "playlist" => result.queue_version = line.1.parse()?, 75 | "playlistlength" => result.queue_len = line.1.parse()?, 76 | "state" => result.state = line.1.parse()?, 77 | "songid" => match result.song { 78 | None => result.song = Some(QueuePlace { id: Id(line.1.parse()?), pos: 0, prio: 0 }), 79 | Some(ref mut place) => place.id = Id(line.1.parse()?), 80 | }, 81 | "song" => match result.song { 82 | None => result.song = Some(QueuePlace { pos: line.1.parse()?, id: Id(0), prio: 0 }), 83 | Some(ref mut place) => place.pos = line.1.parse()?, 84 | }, 85 | "nextsongid" => match result.nextsong { 86 | None => result.nextsong = Some(QueuePlace { id: Id(line.1.parse()?), pos: 0, prio: 0 }), 87 | Some(ref mut place) => place.id = Id(line.1.parse()?), 88 | }, 89 | "nextsong" => match result.nextsong { 90 | None => result.nextsong = Some(QueuePlace { pos: line.1.parse()?, id: Id(0), prio: 0 }), 91 | Some(ref mut place) => place.pos = line.1.parse()?, 92 | }, 93 | "time" => { 94 | let mut splits = line.1.splitn(2, ':').map(|v| v.parse().map_err(ParseError::BadInteger).map(Duration::from_secs)); 95 | result.time = match (splits.next(), splits.next()) { 96 | (Some(Ok(a)), Some(Ok(b))) => Ok(Some((a, b))), 97 | (Some(Err(e)), _) | (_, Some(Err(e))) => Err(e), 98 | _ => Ok(None), 99 | }?; 100 | } 101 | "elapsed" => result.elapsed = Some(Duration::try_from_secs_f64(line.1.parse()?)?), 102 | "duration" => result.duration = Some(Duration::try_from_secs_f64(line.1.parse()?)?), 103 | "bitrate" => result.bitrate = Some(line.1.parse()?), 104 | "xfade" => result.crossfade = Some(Duration::from_secs(line.1.parse()?)), 105 | "mixrampdb" => result.mixrampdb = line.1.parse::()?, 106 | "mixrampdelay" => result.mixrampdelay = Some(Duration::from_secs_f64(line.1.parse()?)), 107 | "audio" => result.audio = Some(line.1.parse()?), 108 | "updating_db" => result.updating_db = Some(line.1.parse()?), 109 | "error" => result.error = Some(line.1.to_owned()), 110 | "replay_gain_mode" => result.replaygain = Some(line.1.parse()?), 111 | _ => (), 112 | } 113 | } 114 | 115 | Ok(result) 116 | } 117 | } 118 | 119 | /// Audio playback format 120 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 121 | #[derive(Debug, Copy, Clone, PartialEq)] 122 | pub struct AudioFormat { 123 | /// Sample rate, kbps. 124 | /// For DSD, to align with MPD's internal handling, the returned rate will be in kilobytes per second instead. 125 | /// See https://mpd.readthedocs.io/en/latest/user.html#audio-output-format. 126 | pub rate: u32, 127 | /// Sample resolution in bits, can be 0 for floating point resolution or 1 for DSD. 128 | pub bits: u8, 129 | /// Number of channels. 130 | pub chans: u8, 131 | } 132 | 133 | impl FromStr for AudioFormat { 134 | type Err = ParseError; 135 | fn from_str(s: &str) -> Result { 136 | if s.contains("dsd") { 137 | // DSD format string only contains two terms: "dsd..." and number of channels. 138 | // To shoehorn into our current AudioFormat struct, use the following conversion: 139 | // - Sample rate: 44100 * the DSD multiplier / 8. For example, DSD64 is sampled at 2.8224MHz. 140 | // - Bits: 1 (DSD is a sequence of single-bit values, or PDM). 141 | // - Channels: as usual. 142 | let mut it = s.split(':'); 143 | let dsd_mul: u32 = it.next().ok_or(ParseError::NoRate).and_then(|v| v[3..].parse().map_err(ParseError::BadRate))?; 144 | return Ok(AudioFormat { 145 | rate: dsd_mul * 44100 / 8, 146 | bits: 1, 147 | chans: it.next().ok_or(ParseError::NoChans).and_then(|v| v.parse().map_err(ParseError::BadChans))?, 148 | }); 149 | } 150 | let mut it = s.split(':'); 151 | Ok(AudioFormat { 152 | rate: it.next().ok_or(ParseError::NoRate).and_then(|v| v.parse().map_err(ParseError::BadRate))?, 153 | bits: it.next().ok_or(ParseError::NoBits).and_then( 154 | |v| { 155 | if v == "f" { 156 | Ok(0) 157 | } else { 158 | v.parse().map_err(ParseError::BadBits) 159 | } 160 | }, 161 | )?, 162 | chans: it.next().ok_or(ParseError::NoChans).and_then(|v| v.parse().map_err(ParseError::BadChans))?, 163 | }) 164 | } 165 | } 166 | 167 | /// Playback state 168 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "lowercase"))] 169 | #[derive(Default, Debug, Copy, Clone, PartialEq)] 170 | pub enum State { 171 | /// player stopped 172 | #[default] 173 | Stop, 174 | /// player is playing 175 | Play, 176 | /// player paused 177 | Pause, 178 | } 179 | 180 | impl FromStr for State { 181 | type Err = ParseError; 182 | fn from_str(s: &str) -> Result { 183 | match s { 184 | "stop" => Ok(State::Stop), 185 | "play" => Ok(State::Play), 186 | "pause" => Ok(State::Pause), 187 | _ => Err(ParseError::BadState(s.to_owned())), 188 | } 189 | } 190 | } 191 | 192 | /// Replay gain mode 193 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(rename_all = "lowercase"))] 194 | #[derive(Debug, Clone, Copy, PartialEq)] 195 | pub enum ReplayGain { 196 | /// off 197 | Off, 198 | /// track 199 | Track, 200 | /// album 201 | Album, 202 | /// auto 203 | Auto, 204 | } 205 | 206 | impl FromStr for ReplayGain { 207 | type Err = ParseError; 208 | fn from_str(s: &str) -> Result { 209 | use self::ReplayGain::*; 210 | match s { 211 | "off" => Ok(Off), 212 | "track" => Ok(Track), 213 | "album" => Ok(Album), 214 | "auto" => Ok(Auto), 215 | _ => Err(ParseError::BadValue(s.to_owned())), 216 | } 217 | } 218 | } 219 | 220 | impl fmt::Display for ReplayGain { 221 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 222 | use self::ReplayGain as R; 223 | f.write_str(match *self { 224 | R::Off => "off", 225 | R::Track => "track", 226 | R::Album => "album", 227 | R::Auto => "auto", 228 | }) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/sticker.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ParseError; 2 | use std::str::FromStr; 3 | 4 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 5 | pub struct Sticker { 6 | pub name: String, 7 | pub value: String, 8 | } 9 | 10 | impl FromStr for Sticker { 11 | type Err = ParseError; 12 | fn from_str(s: &str) -> Result { 13 | let mut parts = s.splitn(2, '='); 14 | match (parts.next(), parts.next()) { 15 | (Some(name), Some(value)) => Ok(Sticker { name: name.to_owned(), value: value.to_owned() }), 16 | _ => Err(ParseError::BadValue(s.to_owned())), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | //! This module defines MPD version type and parsing code 2 | 3 | use crate::error::ParseError; 4 | use std::str::FromStr; 5 | 6 | // Version {{{ 7 | /// MPD version 8 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 9 | pub struct Version(pub u16, pub u16, pub u16); 10 | 11 | impl FromStr for Version { 12 | type Err = ParseError; 13 | fn from_str(s: &str) -> Result { 14 | let mut splits = s.splitn(3, '.').map(FromStr::from_str); 15 | match (splits.next(), splits.next(), splits.next()) { 16 | (Some(Ok(a)), Some(Ok(b)), Some(Ok(c))) => Ok(Version(a, b, c)), 17 | (Some(Err(e)), _, _) | (_, Some(Err(e)), _) | (_, _, Some(Err(e))) => Err(ParseError::BadInteger(e)), 18 | _ => Err(ParseError::BadVersion), 19 | } 20 | } 21 | } 22 | 23 | #[cfg(feature = "serde")] 24 | impl<'de> serde::Deserialize<'de> for Version { 25 | fn deserialize(deserializer: D) -> Result 26 | where D: serde::Deserializer<'de> { 27 | let s = String::deserialize(deserializer)?; 28 | FromStr::from_str(&s).map_err(serde::de::Error::custom) 29 | } 30 | } 31 | 32 | #[cfg(feature = "serde")] 33 | impl serde::Serialize for Version { 34 | fn serialize(&self, serializer: S) -> Result 35 | where S: serde::Serializer { 36 | serializer.serialize_str(&format!("{}.{}.{}", self.0, self.1, self.2)) 37 | } 38 | } 39 | // }}} 40 | -------------------------------------------------------------------------------- /tests/data/silence.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kstep/rust-mpd/8927921dce2714227cf7e6e3b8e773f4fbd53ebe/tests/data/silence.flac -------------------------------------------------------------------------------- /tests/helpers/daemon.rs: -------------------------------------------------------------------------------- 1 | extern crate tempfile; 2 | 3 | use self::tempfile::TempDir; 4 | use super::mpd; 5 | use std::fs::{create_dir, File}; 6 | use std::io::{Read, Write}; 7 | use std::os::unix::net::UnixStream; 8 | use std::path::{Path, PathBuf}; 9 | use std::process::{Child, Command, Stdio}; 10 | 11 | struct MpdConfig { 12 | db_file: PathBuf, 13 | music_directory: PathBuf, 14 | playlist_directory: PathBuf, 15 | sticker_file: PathBuf, 16 | config_path: PathBuf, 17 | sock_path: PathBuf, 18 | } 19 | 20 | impl MpdConfig { 21 | pub fn new

(base: P) -> MpdConfig 22 | where P: AsRef { 23 | let base = base.as_ref(); 24 | MpdConfig { 25 | db_file: base.join("db"), 26 | music_directory: base.join("music"), 27 | playlist_directory: base.join("playlists"), 28 | sticker_file: base.join("sticker_file"), 29 | config_path: base.join("config"), 30 | sock_path: base.join("sock"), 31 | } 32 | } 33 | 34 | fn config_text(&self) -> String { 35 | format!( 36 | r#" 37 | db_file "{db_file}" 38 | music_directory "{music_directory}" 39 | playlist_directory "{playlist_directory}" 40 | sticker_file "{sticker_file}" 41 | bind_to_address "{sock_path}" 42 | audio_output {{ 43 | type "null" 44 | name "null" 45 | mixer_type "null" 46 | }} 47 | "#, 48 | db_file = self.db_file.display(), 49 | music_directory = self.music_directory.display(), 50 | playlist_directory = self.playlist_directory.display(), 51 | sticker_file = self.sticker_file.display(), 52 | sock_path = self.sock_path.display(), 53 | ) 54 | } 55 | 56 | fn generate(&self) { 57 | create_dir(&self.music_directory).expect("Could not create music directory."); 58 | create_dir(&self.playlist_directory).expect("Could not create playlist directory."); 59 | let mut file = File::create(&self.config_path).expect("Could not create config file."); 60 | file.write_all(self.config_text().as_bytes()).expect("Could not write config file."); 61 | } 62 | } 63 | 64 | pub struct Daemon { 65 | // Saved here so it gets dropped when this does. 66 | _temp_dir: TempDir, 67 | config: MpdConfig, 68 | process: Child, 69 | } 70 | 71 | impl Drop for Daemon { 72 | fn drop(&mut self) { 73 | self.process.kill().expect("Could not kill mpd daemon."); 74 | self.process.wait().expect("Could not wait for mpd daemon to shutdown."); 75 | if let Some(ref mut stderr) = self.process.stderr { 76 | let mut output = String::new(); 77 | stderr.read_to_string(&mut output).expect("Could not collect output from mpd."); 78 | println!("Output from mpd:\n{output}"); 79 | } 80 | } 81 | } 82 | 83 | fn sleep() { 84 | use std::{thread, time}; 85 | let ten_millis = time::Duration::from_millis(10); 86 | thread::sleep(ten_millis); 87 | } 88 | 89 | static EMPTY_FLAC_BYTES: &[u8] = include_bytes!("../data/silence.flac"); 90 | 91 | impl Daemon { 92 | pub fn start() -> Daemon { 93 | let temp_dir = TempDir::with_prefix("mpd-test").unwrap(); 94 | let config = MpdConfig::new(&temp_dir); 95 | config.generate(); 96 | 97 | // TODO: Factor out putting files in the music directory. 98 | File::create(config.music_directory.join("silence.flac")).unwrap().write_all(EMPTY_FLAC_BYTES).unwrap(); 99 | 100 | let process = Command::new("mpd") 101 | .arg("--no-daemon") 102 | .arg("--verbose") 103 | .arg("--stderr") 104 | .arg(&config.config_path) 105 | .stdin(Stdio::null()) 106 | .stdout(Stdio::null()) 107 | .stderr(Stdio::piped()) 108 | .spawn() 109 | .expect("Could not create mpd daemon."); 110 | 111 | let daemon = Daemon { _temp_dir: temp_dir, config, process }; 112 | 113 | // Wait until we can connect to the daemon 114 | let mut client; 115 | loop { 116 | if let Ok(c) = daemon.maybe_connect() { 117 | client = c; 118 | break; 119 | } 120 | sleep() 121 | } 122 | while client.status().expect("Couldn't get status.").updating_db.is_some() { 123 | sleep() 124 | } 125 | 126 | daemon 127 | } 128 | 129 | fn maybe_connect(&self) -> Result, mpd::error::Error> { 130 | let stream = UnixStream::connect(&self.config.sock_path)?; 131 | mpd::Client::new(stream) 132 | } 133 | 134 | pub fn connect(&self) -> mpd::Client { 135 | self.maybe_connect().expect("Could not connect to daemon.") 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod daemon; 4 | 5 | pub use self::daemon::Daemon; 6 | use std::os::unix::net::UnixStream; 7 | 8 | pub struct DaemonClient { 9 | _daemon: Daemon, 10 | client: mpd::Client, 11 | } 12 | 13 | use std::ops::{Deref, DerefMut}; 14 | 15 | impl Deref for DaemonClient { 16 | type Target = mpd::Client; 17 | fn deref(&self) -> &Self::Target { 18 | &self.client 19 | } 20 | } 21 | 22 | impl DerefMut for DaemonClient { 23 | fn deref_mut(&mut self) -> &mut Self::Target { 24 | &mut self.client 25 | } 26 | } 27 | 28 | #[allow(dead_code)] 29 | pub fn connect() -> DaemonClient { 30 | let daemon = Daemon::start(); 31 | let client = daemon.connect(); 32 | DaemonClient { _daemon: daemon, client } 33 | } 34 | -------------------------------------------------------------------------------- /tests/idle.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::Daemon; 5 | 6 | use mpd::Idle; 7 | 8 | #[test] 9 | fn idle() { 10 | let daemon = Daemon::start(); 11 | let mut mpd = daemon.connect(); 12 | let idle = mpd.idle(&[]).unwrap(); 13 | 14 | let mut mpd1 = daemon.connect(); 15 | mpd1.consume(true).unwrap(); 16 | mpd1.consume(false).unwrap(); 17 | 18 | let sys = idle.get().unwrap(); 19 | assert_eq!(&*sys, &[mpd::Subsystem::Options]); 20 | } 21 | -------------------------------------------------------------------------------- /tests/options.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | use mpd::{Idle, Song, State, Subsystem}; 6 | use std::time::Duration; 7 | 8 | #[test] 9 | fn status() { 10 | let mut mpd = connect(); 11 | let status = mpd.status().unwrap(); 12 | 13 | assert_eq!(status.song, None); 14 | assert_eq!(status.state, State::Stop); 15 | } 16 | 17 | #[test] 18 | fn status_during_playback() { 19 | let mut mpd = connect(); 20 | mpd.push(Song { file: "silence.flac".to_string(), ..Song::default() }).expect("adding song to queue should not fail"); 21 | mpd.play().expect("starting playback should not fail"); 22 | mpd.idle(&[Subsystem::Player]).expect("waiting for playback should not fail"); 23 | 24 | let status = mpd.status().expect("getting status should not fail"); 25 | 26 | assert!(status.song.is_some()); 27 | assert_eq!(status.state, State::Play); 28 | assert_eq!(status.duration, Some(Duration::from_millis(500))); 29 | } 30 | 31 | #[test] 32 | fn stats() { 33 | let mut mpd = connect(); 34 | let stats = mpd.stats().unwrap(); 35 | println!("{:?}", stats); 36 | } 37 | 38 | macro_rules! test_options_impl { 39 | ($name:ident, $val1:expr, $tval1:expr, $val2:expr, $tval2:expr) => { 40 | #[test] 41 | fn $name() { 42 | let mut mpd = connect(); 43 | mpd.$name($val1).unwrap(); 44 | assert_eq!(mpd.status().unwrap().$name, $tval1); 45 | mpd.$name($val2).unwrap(); 46 | assert_eq!(mpd.status().unwrap().$name, $tval2); 47 | } 48 | }; 49 | } 50 | 51 | macro_rules! test_option { 52 | ($name:ident, $val1:expr, $val2:expr) => { 53 | test_options_impl!($name, $val1, $val1, $val2, $val2); 54 | }; 55 | ($name:ident, $val1:expr => $tval1:expr, $val2:expr => $tval2:expr) => { 56 | test_options_impl!($name, $val1, $tval1, $val2, $tval2); 57 | }; 58 | } 59 | 60 | test_option!(consume, true, false); 61 | test_option!(single, true, false); 62 | test_option!(random, true, false); 63 | test_option!(repeat, true, false); 64 | // test_option!(mixrampdb, 1.0f32, 0.0f32); 65 | // test_option!(mixrampdelay, 1 => Some(Duration::from_secs(1)), 0 => None); 66 | 67 | #[test] 68 | fn volume() { 69 | let mut mpd = connect(); 70 | if mpd.status().unwrap().volume >= 0 { 71 | mpd.volume(100).unwrap(); 72 | assert_eq!(mpd.status().unwrap().volume, 100); 73 | mpd.volume(0).unwrap(); 74 | assert_eq!(mpd.status().unwrap().volume, 0); 75 | } 76 | } 77 | 78 | #[test] 79 | fn crossfade() { 80 | let mut mpd = connect(); 81 | mpd.crossfade(1000).unwrap(); 82 | assert_eq!(mpd.status().unwrap().crossfade, Some(Duration::from_secs(1000))); 83 | mpd.crossfade(0).unwrap(); 84 | assert_eq!(mpd.status().unwrap().crossfade, if mpd.version >= mpd::Version(0, 19, 0) { None } else { Some(Duration::from_secs(0)) }); 85 | } 86 | -------------------------------------------------------------------------------- /tests/outputs.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | 6 | #[test] 7 | fn outputs() { 8 | let mut mpd = connect(); 9 | 10 | let outputs = mpd.outputs().unwrap(); 11 | assert_eq!(outputs.len(), 1); 12 | 13 | let null_output = outputs.first().unwrap(); 14 | assert_eq!(null_output.id, 0); 15 | assert_eq!(null_output.plugin, "null"); 16 | assert_eq!(null_output.name, "null"); 17 | assert!(null_output.enabled); 18 | } 19 | 20 | #[test] 21 | fn out_toggle() { 22 | let mut mpd = connect(); 23 | 24 | mpd.out_disable(0).unwrap(); 25 | mpd.out_enable(0).unwrap(); 26 | 27 | if mpd.version >= mpd::Version(0, 17, 0) { 28 | mpd.out_toggle(0).unwrap(); 29 | } 30 | 31 | mpd.output(0, true).unwrap(); 32 | } 33 | -------------------------------------------------------------------------------- /tests/playback.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | 5 | #[test] 6 | fn playback() { 7 | let mut mpd = helpers::connect(); 8 | mpd.play().unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /tests/playlist.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | 6 | #[test] 7 | fn playlists() { 8 | let mut mpd = connect(); 9 | let pls = mpd.playlists().unwrap(); 10 | println!("{:?}", pls); 11 | 12 | for pl in &pls { 13 | println!("{}: {:?}", pl.name, mpd.playlist(&pl.name).unwrap()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/reflect.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | 6 | #[test] 7 | fn commands() { 8 | let mut mpd = connect(); 9 | println!("{:?}", mpd.commands().unwrap()); 10 | } 11 | 12 | #[test] 13 | fn urlhandlers() { 14 | let mut mpd = connect(); 15 | println!("{:?}", mpd.urlhandlers().unwrap()); 16 | } 17 | 18 | #[test] 19 | fn decoders() { 20 | let mut mpd = connect(); 21 | println!("{:?}", mpd.decoders().unwrap()); 22 | } 23 | 24 | #[test] 25 | fn tagtypes() { 26 | let mut mpd = connect(); 27 | println!("{:?}", mpd.tagtypes().unwrap()); 28 | } 29 | -------------------------------------------------------------------------------- /tests/search.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | use mpd::Query; 6 | 7 | #[test] 8 | fn search() { 9 | let mut mpd = connect(); 10 | let mut query = Query::new(); 11 | let query = query.and(mpd::Term::Any, "Soul"); 12 | let songs = mpd.find(query, None); 13 | println!("{:?}", songs); 14 | assert!(songs.is_ok()); 15 | } 16 | -------------------------------------------------------------------------------- /tests/song.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use std::time::Duration; 5 | 6 | use helpers::connect; 7 | use mpd::Song; 8 | 9 | #[test] 10 | fn currentsong() { 11 | let mut mpd = connect(); 12 | let song = mpd.currentsong().unwrap(); 13 | println!("{:?}", song); 14 | } 15 | 16 | #[test] 17 | fn queue() { 18 | let mut mpd = connect(); 19 | let queue = mpd.queue().unwrap(); 20 | println!("{:?}", queue); 21 | 22 | let songs = mpd.songs(..).unwrap(); 23 | assert_eq!(songs, queue); 24 | } 25 | 26 | #[test] 27 | fn lsinfo() { 28 | let mut mpd = connect(); 29 | let songs = mpd.lsinfo(Song { file: "silence.flac".into(), ..Default::default() }).unwrap(); 30 | assert_eq!(songs.len(), 1); 31 | 32 | let song = songs.get(0).unwrap(); 33 | assert_eq!(song.file, "silence.flac"); 34 | assert_eq!(song.duration.expect("song should have duration"), Duration::from_millis(500)); 35 | } 36 | 37 | #[test] 38 | fn rescan_update() { 39 | let mut mpd = connect(); 40 | println!("update: {:?}", mpd.update()); 41 | println!("rescan: {:?}", mpd.rescan()); 42 | } 43 | -------------------------------------------------------------------------------- /tests/stickers.rs: -------------------------------------------------------------------------------- 1 | extern crate mpd; 2 | 3 | mod helpers; 4 | use helpers::connect; 5 | 6 | #[test] 7 | /// Creating a sticker and then getting that sticker returns the value that was set. 8 | fn set_sticker() { 9 | let mut mpd = connect(); 10 | 11 | static VALUE: &str = "value"; 12 | 13 | mpd.set_sticker("song", "silence.flac", "test_sticker", VALUE).unwrap(); 14 | 15 | let sticker = mpd.sticker("song", "silence.flac", "test_sticker").unwrap(); 16 | assert_eq!(sticker, VALUE); 17 | } 18 | --------------------------------------------------------------------------------