diff options
Diffstat (limited to 'vendored')
40 files changed, 3773 insertions, 0 deletions
diff --git a/vendored/mpd/.gitignore b/vendored/mpd/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/vendored/mpd/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/vendored/mpd/.travis.yml b/vendored/mpd/.travis.yml new file mode 100644 index 0000000..5e221d0 --- /dev/null +++ b/vendored/mpd/.travis.yml @@ -0,0 +1,43 @@ +sudo: required +language: rust +cache: cargo +addons: + apt: + packages: + - libcurl4-openssl-dev + - libelf-dev + - libdw-dev + - binutils-dev +rust: + - stable + - nightly-2017-02-05 +env: + global: + # override the default `--features unstable` used for the nightly branch + - TRAVIS_CARGO_NIGHTLY_FEATURE="" + # Version of clippy known to work with pinned nightly. + - CLIPPY_VERSION=0.0.113 +before_script: + - sudo apt-get update -qq + - sudo apt-get install -y mpd + # Stop mpd service to ensure we use the test-started ones. + - sudo /etc/init.d/mpd stop + - /usr/bin/mpd --version + - export PATH=$HOME/.cargo/bin:$HOME/.local/bin:$PATH + - | + pip install 'travis-cargo<0.2' --user && + travis-cargo --only nightly install -- --force clippy --vers CLIPPY_VERSION + - | + cargo install --force rustfmt +script: + - | + RUSTFLAGS=-Dwarnings travis-cargo build && + travis-cargo test + - | + cargo fmt -- --write-mode diff + - | + travis-cargo --only nightly clippy + - | + travis-cargo --only stable doc +after_success: + - travis-cargo coveralls --no-sudo --verify diff --git a/vendored/mpd/Cargo.toml b/vendored/mpd/Cargo.toml new file mode 100644 index 0000000..e3a104f --- /dev/null +++ b/vendored/mpd/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["Konstantin Stepanov <me@kstep.me>"] +description = "A client library for MPD (music player daemon), like libmpdclient but in Rust" +documentation = "http://kstep.me/rust-mpd/mpd/index.html" +homepage = "https://github.com/kstep/rust-mpd" +license = "MIT/Apache-2.0" +name = "mpd" +repository = "https://github.com/kstep/rust-mpd.git" +version = "0.1.0" +edition = "2018" + +[dependencies] +bufstream = "0.1.1" +rustc-serialize = "0.3.16" +time = "0.2" + +[dev-dependencies] +tempdir = "0.3.5" +unix_socket = "0.5.0" diff --git a/vendored/mpd/LICENSE-APACHE b/vendored/mpd/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/vendored/mpd/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendored/mpd/LICENSE-MIT b/vendored/mpd/LICENSE-MIT new file mode 100644 index 0000000..fddc895 --- /dev/null +++ b/vendored/mpd/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2015-2016 Konstantin Stepanov <me@kstep.me> + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendored/mpd/README.md b/vendored/mpd/README.md new file mode 100644 index 0000000..0fae261 --- /dev/null +++ b/vendored/mpd/README.md @@ -0,0 +1,44 @@ +# rust-mpd <a href="https://travis-ci.org/kstep/rust-mpd"><img src="https://img.shields.io/travis/kstep/rust-mpd.png?style=flat-square" /></a> <a href="https://crates.io/crates/mpd"><img src="https://img.shields.io/crates/d/mpd.png?style=flat-square" /></a> <a href="https://crates.io/crates/mpd"><img src="https://img.shields.io/crates/v/mpd.png?style=flat-square" /></a> <a href="https://crates.io/crates/mpd"><img src="https://img.shields.io/crates/l/mpd.png?style=flat-square" /></a><a href=http://docs.rs/mpd/><img src="https://docs.rs/mpd/badge.svg" /></a> + +Pure Rust version of [libmpdclient](http://www.musicpd.org/libs/libmpdclient/). + +[Full documentation](http://docs.rs/mpd/) + +## Example + +Add to `Cargo.toml`: + +```toml +[dependencies] +mpd = "*" +``` + +Then just use: + +```rust +extern crate mpd; + +use mpd::Client; +use std::net::TcpStream; + +let mut conn = Client::connect("127.0.0.1:6600").unwrap(); +conn.volume(100).unwrap(); +conn.load("My Lounge Playlist", ..).unwrap(); +conn.play().unwrap(); +println!("Status: {:?}", conn.status()); +``` + +## License + +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. diff --git a/vendored/mpd/benches/options.rs b/vendored/mpd/benches/options.rs new file mode 100644 index 0000000..cd04418 --- /dev/null +++ b/vendored/mpd/benches/options.rs @@ -0,0 +1,15 @@ +#![feature(test)] + +extern crate mpd; +extern crate time; +extern crate test; +extern crate unix_socket; + +use test::{Bencher, black_box}; +use unix_socket::UnixStream; + +#[bench] +fn status(b: &mut Bencher) { + let mut mpd = mpd::Client::<UnixStream>::new(UnixStream::connect("/var/run/mpd/socket").unwrap()).unwrap(); + b.iter(|| { black_box(mpd.status()).unwrap(); }); +} diff --git a/vendored/mpd/examples/example.rs b/vendored/mpd/examples/example.rs new file mode 100644 index 0000000..f4ae079 --- /dev/null +++ b/vendored/mpd/examples/example.rs @@ -0,0 +1,24 @@ +extern crate mpd; + +use mpd::{Client, Query}; +use std::net::TcpStream; + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let mut c = Client::new(TcpStream::connect("127.0.0.1:6600").unwrap()).unwrap(); + println!("version: {:?}", c.version); + println!("status: {:?}", c.status()); + println!("stuff: {:?}", c.find(&Query::new(), (1, 2))); + + let now_playing = c.currentsong()?; + if let Some(song) = now_playing { + println!("Metadata:"); + for row in c.readcomments(song)? { + if let Ok((k, v)) = row { + println!("{}: {}", k, v); + } + } + } else { + println!("No song playing."); + } + Ok(()) +} diff --git a/vendored/mpd/rustfmt.toml b/vendored/mpd/rustfmt.toml new file mode 100644 index 0000000..1091732 --- /dev/null +++ b/vendored/mpd/rustfmt.toml @@ -0,0 +1,12 @@ +newline_style = "Unix" +max_width = 140 +fn_call_width = 120 +ideal_width = 120 +tab_spaces = 4 +fn_args_density = "Compressed" +fn_arg_indent = "Tabbed" +single_line_if_else = true +reorder_imports = true +chain_base_indent = "Tabbed" +chain_indent = "Block" +chain_one_line_max = 160 diff --git a/vendored/mpd/src/client.rs b/vendored/mpd/src/client.rs new file mode 100644 index 0000000..ac5c834 --- /dev/null +++ b/vendored/mpd/src/client.rs @@ -0,0 +1,689 @@ +//! This module defines client data structure — the main entry point to MPD communication +//! +//! Almost every method of the `Client` structure corresponds to some command in [MPD protocol][proto]. +//! +//! [proto]: http://www.musicpd.org/doc/protocol/ + + +use bufstream::BufStream; + +use crate::convert::*; +use crate::error::{Error, ParseError, ProtoError, Result}; +use crate::message::{Channel, Message}; +use crate::mount::{Mount, Neighbor}; +use crate::output::Output; +use crate::playlist::Playlist; +use crate::plugin::Plugin; +use crate::proto::*; +use crate::search::{Query, Window, Term}; +use crate::song::{Id, Song}; +use crate::stats::Stats; +use crate::status::{ReplayGain, Status}; +use crate::sticker::Sticker; +use crate::version::Version; + +use std::convert::From; +use std::io::{BufRead, Lines, Read, Write}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::collections::HashMap; + +// Client {{{ + +/// Client connection +#[derive(Debug)] +pub struct Client<S = TcpStream> + where S: Read + Write +{ + socket: BufStream<S>, + /// MPD version + pub version: Version, +} + +impl Default for Client<TcpStream> { + fn default() -> Client<TcpStream> { + Client::<TcpStream>::connect("127.0.0.1:6600").unwrap() + } +} + +impl Client<TcpStream> { + /// Connect client to some IP address + pub fn connect<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream>> { + TcpStream::connect(addr).map_err(Error::Io).and_then(Client::new) + } +} + +impl<S: Read + Write> Client<S> { + // Constructors {{{ + /// Create client from some arbitrary pre-connected socket + pub fn new(socket: S) -> Result<Client<S>> { + let mut socket = BufStream::new(socket); + + let mut banner = String::new(); + socket.read_line(&mut banner)?; + + if !banner.starts_with("OK MPD ") { + return Err(From::from(ProtoError::BadBanner)); + } + + let version = banner[7..].trim().parse::<Version>()?; + + Ok(Client { + socket: socket, + version: version, + }) + } + // }}} + + // Playback options & status {{{ + /// Get MPD status + pub fn status(&mut self) -> Result<Status> { + self.run_command("command_list_begin", ()) + .and_then(|_| self.run_command("status", ())) + .and_then(|_| self.run_command("replay_gain_status", ())) + .and_then(|_| self.run_command("command_list_end", ())) + .and_then(|_| self.read_struct()) + } + + /// Get MPD playing statistics + pub fn stats(&mut self) -> Result<Stats> { + self.run_command("stats", ()).and_then(|_| self.read_struct()) + } + + /// Clear error state + pub fn clearerror(&mut self) -> Result<()> { + self.run_command("clearerror", ()).and_then(|_| self.expect_ok()) + } + + /// Set volume + pub fn volume(&mut self, volume: i8) -> Result<()> { + self.run_command("setvol", volume).and_then(|_| self.expect_ok()) + } + + /// Set repeat state + pub fn repeat(&mut self, value: bool) -> Result<()> { + self.run_command("repeat", value as u8).and_then(|_| self.expect_ok()) + } + + /// Set random state + pub fn random(&mut self, value: bool) -> Result<()> { + self.run_command("random", value as u8).and_then(|_| self.expect_ok()) + } + + /// Set single state + pub fn single(&mut self, value: bool) -> Result<()> { + self.run_command("single", value as u8).and_then(|_| self.expect_ok()) + } + + /// Set consume state + pub fn consume(&mut self, value: bool) -> Result<()> { + self.run_command("consume", value as u8).and_then(|_| self.expect_ok()) + } + + /// Set crossfade time in seconds + pub fn crossfade<T: ToSeconds>(&mut self, value: T) -> Result<()> { + self.run_command("crossfade", value.to_seconds()).and_then(|_| self.expect_ok()) + } + + /// Set mixramp level in dB + pub fn mixrampdb(&mut self, value: f32) -> Result<()> { + self.run_command("mixrampdb", value).and_then(|_| self.expect_ok()) + } + + /// Set mixramp delay in seconds + pub fn mixrampdelay<T: ToSeconds>(&mut self, value: T) -> Result<()> { + self.run_command("mixrampdelay", value.to_seconds()).and_then(|_| self.expect_ok()) + } + + /// Set replay gain mode + pub fn replaygain(&mut self, gain: ReplayGain) -> Result<()> { + self.run_command("replay_gain_mode", gain).and_then(|_| self.expect_ok()) + } + // }}} + + // Playback control {{{ + /// Start playback + pub fn play(&mut self) -> Result<()> { + self.run_command("play", ()).and_then(|_| self.expect_ok()) + } + + /// Start playback from given song in a queue + pub fn switch<T: ToQueuePlace>(&mut self, place: T) -> Result<()> { + let command = if T::is_id() { "playid" } else { "play" }; + self.run_command(command, place.to_place()).and_then(|_| self.expect_ok()) + } + + /// Switch to a next song in queue + #[cfg_attr(feature = "cargo-clippy", allow(should_implement_trait))] + pub fn next(&mut self) -> Result<()> { + self.run_command("next", ()).and_then(|_| self.expect_ok()) + } + + /// Switch to a previous song in queue + pub fn prev(&mut self) -> Result<()> { + self.run_command("previous", ()).and_then(|_| self.expect_ok()) + } + + /// Stop playback + pub fn stop(&mut self) -> Result<()> { + self.run_command("stop", ()).and_then(|_| self.expect_ok()) + } + + /// Toggle pause state + pub fn toggle_pause(&mut self) -> Result<()> { + self.run_command("pause", ()).and_then(|_| self.expect_ok()) + } + + /// Set pause state + pub fn pause(&mut self, value: bool) -> Result<()> { + self.run_command("pause", value as u8).and_then(|_| self.expect_ok()) + } + + /// Seek to a given place (in seconds) in a given song + pub fn seek<T: ToSeconds, P: ToQueuePlace>(&mut self, place: P, pos: T) -> Result<()> { + let command = if P::is_id() { "seekid" } else { "seek" }; + self.run_command(command, (place.to_place(), pos.to_seconds())).and_then(|_| self.expect_ok()) + } + + /// Seek to a given place (in seconds) in the current song + pub fn rewind<T: ToSeconds>(&mut self, pos: T) -> Result<()> { + self.run_command("seekcur", pos.to_seconds()).and_then(|_| self.expect_ok()) + } + // }}} + + // Queue control {{{ + /// List given song or range of songs in a play queue + pub fn songs<T: ToQueueRangeOrPlace>(&mut self, pos: T) -> Result<Vec<Song>> { + let command = if T::is_id() { + "playlistid" + } else { + "playlistinfo" + }; + self.run_command(command, pos.to_range()).and_then(|_| self.read_structs("file")) + } + + /// List all songs in a play queue + pub fn queue(&mut self) -> Result<Vec<Song>> { + self.run_command("playlistinfo", ()).and_then(|_| self.read_structs("file")) + } + + /// Get current playing song + pub fn currentsong(&mut self) -> Result<Option<Song>> { + self.run_command("currentsong", ()).and_then(|_| self.read_struct::<Song>()).map(|s| if s.place.is_none() { None } else { Some(s) }) + } + + /// Clear current queue + pub fn clear(&mut self) -> Result<()> { + self.run_command("clear", ()).and_then(|_| self.expect_ok()) + } + + /// List all changes in a queue since given version + pub fn changes(&mut self, version: u32) -> Result<Vec<Song>> { + self.run_command("plchanges", version).and_then(|_| self.read_structs("file")) + } + + /// Append a song into a queue + pub fn push_str(&mut self, path: String) -> Result<Id> { + self.run_command("addid", path).and_then(|_| self.read_field("Id")).map(Id) + } + + /// Append a song into a queue + pub fn push<P: ToSongPath>(&mut self, path: P) -> Result<Id> { + self.run_command("addid", path).and_then(|_| self.read_field("Id")).map(Id) + } + + /// Insert a song into a given position in a queue + pub fn insert<P: ToSongPath>(&mut self, path: P, pos: usize) -> Result<usize> { + self.run_command("addid", (path, pos)).and_then(|_| self.read_field("Id")) + } + + /// Delete a song (at some position) or several songs (in a range) from a queue + pub fn delete<T: ToQueueRangeOrPlace>(&mut self, pos: T) -> Result<()> { + let command = if T::is_id() { "deleteid" } else { "delete" }; + self.run_command(command, pos.to_range()).and_then(|_| self.expect_ok()) + } + + /// Move a song (at a some position) or several songs (in a range) to other position in queue + pub fn shift<T: ToQueueRangeOrPlace>(&mut self, from: T, to: usize) -> Result<()> { + let command = if T::is_id() { "moveid" } else { "move" }; + self.run_command(command, (from.to_range(), to)).and_then(|_| self.expect_ok()) + } + + /// Swap to songs in a queue + pub fn swap<T: ToQueuePlace>(&mut self, one: T, two: T) -> Result<()> { + let command = if T::is_id() { "swapid" } else { "swap" }; + self.run_command(command, (one.to_place(), two.to_place())).and_then(|_| self.expect_ok()) + } + + /// Shuffle queue in a given range (use `..` to shuffle full queue) + pub fn shuffle<T: ToQueueRange>(&mut self, range: T) -> Result<()> { + self.run_command("shuffle", range.to_range()).and_then(|_| self.expect_ok()) + } + + /// Set song priority in a queue + pub fn priority<T: ToQueueRangeOrPlace>(&mut self, pos: T, prio: u8) -> Result<()> { + let command = if T::is_id() { "prioid" } else { "prio" }; + self.run_command(command, (prio, pos.to_range())).and_then(|_| self.expect_ok()) + } + + /// Set song range (in seconds) to play + /// + /// Doesn't work for currently playing song. + pub fn range<T: ToSongId, R: ToSongRange>(&mut self, song: T, range: R) -> Result<()> { + self.run_command("rangeid", (song.to_song_id(), range.to_range())).and_then(|_| self.expect_ok()) + } + + /// Add tag to a song + pub fn tag<T: ToSongId>(&mut self, song: T, tag: &str, value: &str) -> Result<()> { + self.run_command("addtagid", (song.to_song_id(), tag, value)).and_then(|_| self.expect_ok()) + } + + /// Delete tag from a song + pub fn untag<T: ToSongId>(&mut self, song: T, tag: &str) -> Result<()> { + self.run_command("cleartagid", (song.to_song_id(), tag)).and_then(|_| self.expect_ok()) + } + // }}} + + // Connection settings {{{ + /// Just pings MPD server, does nothing + pub fn ping(&mut self) -> Result<()> { + self.run_command("ping", ()).and_then(|_| self.expect_ok()) + } + + /// Close MPD connection + pub fn close(&mut self) -> Result<()> { + self.run_command("close", ()).and_then(|_| self.expect_ok()) + } + + /// Kill MPD server + pub fn kill(&mut self) -> Result<()> { + self.run_command("kill", ()).and_then(|_| self.expect_ok()) + } + + /// Login to MPD server with given password + pub fn login(&mut self, password: &str) -> Result<()> { + self.run_command("password", password).and_then(|_| self.expect_ok()) + } + // }}} + + // Playlist methods {{{ + /// List all playlists + pub fn playlists(&mut self) -> Result<Vec<Playlist>> { + self.run_command("listplaylists", ()).and_then(|_| self.read_structs("playlist")) + } + + /// List all songs in a playlist + pub fn playlist<N: ToPlaylistName>(&mut self, name: N) -> Result<Vec<Song>> { + self.run_command("listplaylistinfo", name.to_name()).and_then(|_| self.read_structs("file")) + } + + /// Load playlist into queue + /// + /// You can give either full range (`..`) to load all songs in a playlist, + /// or some partial range to load only part of playlist. + pub fn load<T: ToQueueRange, N: ToPlaylistName>(&mut self, name: N, range: T) -> Result<()> { + self.run_command("load", (name.to_name(), range.to_range())).and_then(|_| self.expect_ok()) + } + + /// Save current queue into playlist + /// + /// If playlist with given name doesn't exist, create new one. + pub fn save<N: ToPlaylistName>(&mut self, name: N) -> Result<()> { + self.run_command("save", name.to_name()).and_then(|_| self.expect_ok()) + } + + /// Rename playlist + pub fn pl_rename<N: ToPlaylistName>(&mut self, name: N, newname: &str) -> Result<()> { + self.run_command("rename", (name.to_name(), newname)).and_then(|_| self.expect_ok()) + } + + /// Clear playlist + pub fn pl_clear<N: ToPlaylistName>(&mut self, name: N) -> Result<()> { + self.run_command("playlistclear", name.to_name()).and_then(|_| self.expect_ok()) + } + + /// Delete playlist + pub fn pl_remove<N: ToPlaylistName>(&mut self, name: N) -> Result<()> { + self.run_command("rm", name.to_name()).and_then(|_| self.expect_ok()) + } + + /// Add new songs to a playlist + pub fn pl_push<N: ToPlaylistName, P: ToSongPath>(&mut self, name: N, path: P) -> Result<()> { + self.run_command("playlistadd", (name.to_name(), path)).and_then(|_| self.expect_ok()) + } + + /// Delete a song at a given position in a playlist + pub fn pl_delete<N: ToPlaylistName>(&mut self, name: N, pos: u32) -> Result<()> { + self.run_command("playlistdelete", (name.to_name(), pos)).and_then(|_| self.expect_ok()) + } + + /// Move song in a playlist from one position into another + pub fn pl_shift<N: ToPlaylistName>(&mut self, name: N, from: u32, to: u32) -> Result<()> { + self.run_command("playlistmove", (name.to_name(), from, to)).and_then(|_| self.expect_ok()) + } + // }}} + + // Database methods {{{ + /// Run database rescan, i.e. remove non-existing files from DB + /// as well as add new files to DB + pub fn rescan(&mut self) -> Result<u32> { + self.run_command("rescan", ()).and_then(|_| self.read_field("updating_db")) + } + + /// Run database update, i.e. remove non-existing files from DB + pub fn update(&mut self) -> Result<u32> { + self.run_command("update", ()).and_then(|_| self.read_field("updating_db")) + } + // }}} + + // Database search {{{ + // TODO: count tag needle [...] [group] [grouptag], find type what [...] [window start:end] + // TODO: search type what [...] [window start:end], searchadd type what [...] + // TODO: listallinfo [uri], listfiles [uri] + // TODO: list type [filtertype] [filterwhat] [...] [group] [grouptype] [...] + // TODO: searchaddpl name type what [...] + + /// Find songs matching Query conditions. + pub fn find<W>(&mut self, query: &Query, window: W) -> Result<Vec<Song>> + where W: Into<Window> + { + self.find_generic("find", query, window.into()) + } + + /// Find album art for file + pub fn albumart<P: ToSongPath>(&mut self, path: &P) -> Result<Vec<u8>> { + let mut buf = vec![]; + loop { + self.run_command("albumart", (path, &*format!("{}", buf.len())))?; + let (_, size) = self.read_pair()?; + let (_, bytes) = self.read_pair()?; + let mut chunk = self.read_bytes(bytes.parse()?)?; + buf.append(&mut chunk); + // Read empty newline + let _ = self.read_line()?; + let result = self.read_line()?; + if result != "OK" { + return Err(ProtoError::NotOk)?; + } + + if size.parse::<usize>()? == buf.len() { + break; + } + } + Ok(buf) + } + + /// Case-insensitively search for songs matching Query conditions. + pub fn search<W>(&mut self, query: &Query, window: W) -> Result<Vec<Song>> + where W: Into<Window> + { + self.find_generic("search", query, window.into()) + } + + fn find_generic(&mut self, cmd: &str, query: &Query, window: Window) -> Result<Vec<Song>> { + self.run_command(cmd, (query, window)).and_then(|_| self.read_structs("file")) + } + + /// Lists unique tags values of the specified type for songs matching the given query. + // TODO: list type [filtertype] [filterwhat] [...] [group] [grouptype] [...] + // It isn't clear if or how `group` works + pub fn list(&mut self, term: &Term, query: &Query) -> Result<Vec<String>> { + self.run_command("list", (term, query)).and_then(|_| self.read_pairs().map(|p| p.map(|p| p.1)).collect()) + } + + /// Find all songs in the db that match query and adds them to current playlist. + pub fn findadd(&mut self, query: &Query) -> Result<()> { + self.run_command("findadd", query).and_then(|_| self.expect_ok()) + } + + /// Lists the contents of a directory. + pub fn lsinfo<P: ToSongPath>(&mut self, path: P) -> Result<Song> { + self.run_command("lsinfo", path).and_then(|_| self.read_struct()) + } + + /// Returns raw metadata for file + pub fn readcomments<'a, P: ToSongPath>(&'a mut self, path: P) -> Result<impl Iterator<Item = Result<(String, String)>> + 'a> { + self.run_command("readcomments", path)?; + Ok(self.read_pairs()) + } + + // }}} + + // Output methods {{{ + /// List all outputs + pub fn outputs(&mut self) -> Result<Vec<Output>> { + self.run_command("outputs", ()).and_then(|_| self.read_structs("outputid")) + } + + /// Set given output enabled state + pub fn output<T: ToOutputId>(&mut self, id: T, state: bool) -> Result<()> { + if state { + self.out_enable(id) + } else { + self.out_disable(id) + } + } + + /// Disable given output + pub fn out_disable<T: ToOutputId>(&mut self, id: T) -> Result<()> { + self.run_command("disableoutput", id.to_output_id()).and_then(|_| self.expect_ok()) + } + + /// Enable given output + pub fn out_enable<T: ToOutputId>(&mut self, id: T) -> Result<()> { + self.run_command("enableoutput", id.to_output_id()).and_then(|_| self.expect_ok()) + } + + /// Toggle given output + pub fn out_toggle<T: ToOutputId>(&mut self, id: T) -> Result<()> { + self.run_command("toggleoutput", id.to_output_id()).and_then(|_| self.expect_ok()) + } + // }}} + + // Reflection methods {{{ + /// Get current music directory + pub fn music_directory(&mut self) -> Result<String> { + self.run_command("config", ()).and_then(|_| self.read_field("music_directory")) + } + + /// List all available commands + pub fn commands(&mut self) -> Result<Vec<String>> { + self.run_command("commands", ()).and_then(|_| self.read_list("command")) + } + + /// List all forbidden commands + pub fn notcommands(&mut self) -> Result<Vec<String>> { + self.run_command("notcommands", ()).and_then(|_| self.read_list("command")) + } + + /// List all available URL handlers + pub fn urlhandlers(&mut self) -> Result<Vec<String>> { + self.run_command("urlhandlers", ()).and_then(|_| self.read_list("handler")) + } + + /// List all supported tag types + pub fn tagtypes(&mut self) -> Result<Vec<String>> { + self.run_command("tagtypes", ()).and_then(|_| self.read_list("tagtype")) + } + + /// List all available decoder plugins + pub fn decoders(&mut self) -> Result<Vec<Plugin>> { + self.run_command("decoders", ()).and_then(|_| self.read_struct()) + } + // }}} + + // Messaging {{{ + /// List all channels available for current connection + pub fn channels(&mut self) -> Result<Vec<Channel>> { + self.run_command("channels", ()).and_then(|_| self.read_list("channel")).map(|v| { + v.into_iter() + .map(|b| unsafe { + Channel::new_unchecked(b) + }) + .collect() + }) + } + + /// Read queued messages from subscribed channels + pub fn readmessages(&mut self) -> Result<Vec<Message>> { + self.run_command("readmessages", ()).and_then(|_| self.read_structs("channel")) + } + + /// Send a message to a channel + pub fn sendmessage(&mut self, channel: Channel, message: &str) -> Result<()> { + self.run_command("sendmessage", (channel, message)).and_then(|_| self.expect_ok()) + } + + /// Subscribe to a channel + pub fn subscribe(&mut self, channel: Channel) -> Result<()> { + self.run_command("subscribe", channel).and_then(|_| self.expect_ok()) + } + + /// Unsubscribe to a channel + pub fn unsubscribe(&mut self, channel: Channel) -> Result<()> { + self.run_command("unsubscribe", channel).and_then(|_| self.expect_ok()) + } + // }}} + + // Mount methods {{{ + /// List all (virtual) mounts + /// + /// These mounts exist inside MPD process only, thus they can work without root permissions. + pub fn mounts(&mut self) -> Result<Vec<Mount>> { + self.run_command("listmounts", ()).and_then(|_| self.read_structs("mount")) + } + + /// List all network neighbors, which can be potentially mounted + pub fn neighbors(&mut self) -> Result<Vec<Neighbor>> { + self.run_command("listneighbors", ()).and_then(|_| self.read_structs("neighbor")) + } + + /// Mount given neighbor to a mount point + /// + /// The mount exists inside MPD process only, thus it can work without root permissions. + pub fn mount(&mut self, path: &str, uri: &str) -> Result<()> { + self.run_command("mount", (path, uri)).and_then(|_| self.expect_ok()) + } + + /// Unmount given active (virtual) mount + /// + /// The mount exists inside MPD process only, thus it can work without root permissions. + pub fn unmount(&mut self, path: &str) -> Result<()> { + self.run_command("unmount", path).and_then(|_| self.expect_ok()) + } + // }}} + + // Sticker methods {{{ + /// Show sticker value for a given object, identified by type and uri + pub fn sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result<String> { + self.run_command("sticker get", (typ, uri, name)) + // TODO: This should parse to a `Sticker` type. + .and_then(|_| self.read_field::<Sticker>("sticker")) + .map(|s| s.value) + } + + /// Set sticker value for a given object, identified by type and uri + pub fn set_sticker(&mut self, typ: &str, uri: &str, name: &str, value: &str) -> Result<()> { + self.run_command("sticker set", (typ, uri, name, value)).and_then(|_| self.expect_ok()) + } + + /// Delete sticker from a given object, identified by type and uri + pub fn delete_sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result<()> { + self.run_command("sticker delete", (typ, uri, name)).and_then(|_| self.expect_ok()) + } + + /// Remove all stickers from a given object, identified by type and uri + pub fn clear_stickers(&mut self, typ: &str, uri: &str) -> Result<()> { + self.run_command("sticker delete", (typ, uri)).and_then(|_| self.expect_ok()) + } + + /// List all stickers from a given object, identified by type and uri + pub fn stickers(&mut self, typ: &str, uri: &str) -> Result<Vec<String>> { + self.run_command("sticker list", (typ, uri)) + .and_then(|_| self.read_list("sticker")) + .map(|v| v.into_iter().map(|b| b.splitn(2, '=').nth(1).map(|s| s.to_owned()).unwrap()).collect()) + } + + /// List all stickers from a given object in a map, identified by type and uri + pub fn stickers_map(&mut self, typ: &str, uri: &str) -> Result<HashMap<String, String>> { + self.run_command("sticker list", (typ, uri)) + .and_then(|_| self.read_list("sticker")) + .map(|v| v.into_iter().map(|b| { + let mut iter = b.splitn(2, '='); + + (iter.next().unwrap().to_owned(), iter.next().unwrap().to_owned()) + }).collect()) + } + + /// List all (file, sticker) pairs for sticker name and objects of given type + /// from given directory (identified by uri) + pub fn find_sticker(&mut self, typ: &str, uri: &str, name: &str) -> Result<Vec<(String, String)>> { + self.run_command("sticker find", (typ, uri, name)) + .and_then(|_| { + self.read_pairs() + .split("file") + .map(|rmap| { + rmap.map(|mut map| { + (map.remove("file").unwrap(), + map.remove("sticker").and_then(|s| s.splitn(2, '=').nth(1).map(|s| s.to_owned())).unwrap()) + }) + }) + .collect() + }) + } + + /// List all files of a given type under given directory (identified by uri) + /// with a tag set to given value + pub fn find_sticker_eq(&mut self, typ: &str, uri: &str, name: &str, value: &str) -> Result<Vec<String>> { + self.run_command("sticker find", (typ, uri, name, value)).and_then(|_| self.read_list("file")) + } + // }}} +} + +// Helper methods {{{ +impl<S: Read + Write> Proto for Client<S> { + type Stream = S; + + fn read_bytes(&mut self, bytes: usize) -> Result<Vec<u8>> { + let mut buf = Vec::with_capacity(bytes); + let mut chunk = (&mut self.socket).take(bytes as u64); + chunk.read_to_end(&mut buf)?; + Ok(buf) + } + + fn read_line(&mut self) -> Result<String> { + let mut buf = Vec::new(); + self.socket.read_until(b'\n', &mut buf)?; + if buf.ends_with(&[b'\n']) { + buf.pop(); + } + let str = String::from_utf8(buf) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "stream did not contain valid UTF-8"))?; + Ok(str) + } + + fn read_pairs(&mut self) -> Pairs<Lines<&mut BufStream<S>>> { + Pairs((&mut self.socket).lines()) + } + + fn read_pair(&mut self) -> Result<(String, String)> { + let line = self.read_line()?; + let mut split = line.split(": "); + let key = split.next().ok_or(ParseError::BadPair)?; + let val = split.next().ok_or(ParseError::BadPair)?; + Ok((key.to_string(), val.to_string())) + } + + fn run_command<I>(&mut self, command: &str, arguments: I) -> Result<()> + where I: ToArguments + { + self.socket + .write_all(command.as_bytes()) + .and_then(|_| arguments.to_arguments(&mut |arg| write!(self.socket, " {}", Quoted(arg)))) + .and_then(|_| self.socket.write(&[0x0a])) + .and_then(|_| self.socket.flush()) + .map_err(From::from) + } +} +// }}} + +// }}} diff --git a/vendored/mpd/src/convert.rs b/vendored/mpd/src/convert.rs new file mode 100644 index 0000000..7e554bb --- /dev/null +++ b/vendored/mpd/src/convert.rs @@ -0,0 +1,316 @@ +#![allow(missing_docs)] +//! These are inner traits to support methods overloading for the `Client` + +use crate::error::Error; +use crate::output::Output; +use crate::playlist::Playlist; +use crate::proto::ToArguments; +use crate::song::{self, Id, Song}; +use std::collections::BTreeMap; +use std::ops::{Range, RangeFrom, RangeFull, RangeTo}; + +use std::time::Duration; + +#[doc(hidden)] +pub trait FromMap: Sized { + fn from_map(map: BTreeMap<String, String>) -> Result<Self, Error>; +} + +#[doc(hidden)] +pub trait FromIter: Sized { + fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Self, Error>; +} + +impl<T: FromIter> FromMap for T { + fn from_map(map: BTreeMap<String, String>) -> Result<Self, Error> { + FromIter::from_iter(map.into_iter().map(Ok)) + } +} + +// Playlist name polymorphisms {{{ +pub trait ToPlaylistName { + fn to_name(&self) -> &str; +} + +impl ToPlaylistName for Playlist { + fn to_name(&self) -> &str { + &*self.name + } +} + +impl<'a> ToPlaylistName for &'a Playlist { + fn to_name(&self) -> &str { + &*self.name + } +} + +impl<'a> ToPlaylistName for &'a String { + fn to_name(&self) -> &str { + self + } +} + +impl<'a> ToPlaylistName for &'a str { + fn to_name(&self) -> &str { + *self + } +} + +impl ToPlaylistName for str { + fn to_name(&self) -> &str { + self + } +} + +impl ToPlaylistName for String { + fn to_name(&self) -> &str { + &*self + } +} +// }}} + +// Seconds polymorphisms {{{ +pub trait ToSeconds { + fn to_seconds(self) -> f64; +} + +impl ToSeconds for i64 { + fn to_seconds(self) -> f64 { + self as f64 + } +} + +impl ToSeconds for f64 { + fn to_seconds(self) -> f64 { + self + } +} + +impl ToSeconds for Duration { + fn to_seconds(self) -> f64 { + self.as_secs_f64() + } +} +// }}} + +// Queue place polymorphisms {{{ + +pub trait IsId { + fn is_id() -> bool { + false + } +} + +pub trait ToQueueRangeOrPlace: IsId { + fn to_range(self) -> String; +} + +pub trait ToQueueRange { + fn to_range(self) -> String; +} + +impl<T: ToQueuePlace> ToQueueRangeOrPlace for T { + fn to_range(self) -> String { + format!("{}", self.to_place()) + } +} + +impl ToQueueRange for Range<u32> { + fn to_range(self) -> String { + format!("{}:{}", self.start, self.end) + } +} + +impl ToQueueRangeOrPlace for Range<u32> { + fn to_range(self) -> String { + ToQueueRange::to_range(self) + } +} + +impl ToQueueRange for RangeTo<u32> { + fn to_range(self) -> String { + format!(":{}", self.end) + } +} + +impl ToQueueRangeOrPlace for RangeTo<u32> { + fn to_range(self) -> String { + ToQueueRange::to_range(self) + } +} + +impl ToQueueRange for RangeFrom<u32> { + fn to_range(self) -> String { + format!("{}:", self.start) + } +} + +impl ToQueueRangeOrPlace for RangeFrom<u32> { + fn to_range(self) -> String { + ToQueueRange::to_range(self) + } +} + +impl ToQueueRange for RangeFull { + fn to_range(self) -> String { + ToQueueRange::to_range(0..) + } +} + +impl ToQueueRangeOrPlace for RangeFull { + fn to_range(self) -> String { + ToQueueRange::to_range(self) + } +} + +pub trait ToQueuePlace: IsId { + fn to_place(self) -> u32; +} + +impl ToQueuePlace for Id { + fn to_place(self) -> u32 { + self.0 + } +} + +impl ToQueuePlace for u32 { + fn to_place(self) -> u32 { + self + } +} + +impl IsId for u32 {} +impl IsId for Range<u32> {} +impl IsId for RangeTo<u32> {} +impl IsId for RangeFrom<u32> {} +impl IsId for RangeFull {} +impl IsId for Id { + fn is_id() -> bool { + true + } +} + +pub trait ToSongId { + fn to_song_id(&self) -> Id; +} + +impl ToSongId for Song { + fn to_song_id(&self) -> Id { + self.place.unwrap().id + } +} + +impl ToSongId for u32 { + fn to_song_id(&self) -> Id { + Id(*self) + } +} + +impl ToSongId for Id { + fn to_song_id(&self) -> Id { + *self + } +} +// }}} + +// Output id polymorphisms {{{ +pub trait ToOutputId { + fn to_output_id(self) -> u32; +} + +impl ToOutputId for u32 { + fn to_output_id(self) -> u32 { + self + } +} +impl ToOutputId for Output { + fn to_output_id(self) -> u32 { + self.id + } +} +// }}} + +// Song play range polymorphisms {{{ +pub trait ToSongRange { + fn to_range(self) -> song::Range; +} + +impl ToSongRange for Range<Duration> { + fn to_range(self) -> song::Range { + song::Range(self.start, Some(self.end)) + } +} + +impl ToSongRange for Range<u32> { + fn to_range(self) -> song::Range { + song::Range(Duration::from_secs(self.start as u64), Some(Duration::from_secs(self.end as u64))) + } +} + +impl ToSongRange for RangeFrom<Duration> { + fn to_range(self) -> song::Range { + song::Range(self.start, None) + } +} + +impl ToSongRange for RangeFrom<u32> { + fn to_range(self) -> song::Range { + song::Range(Duration::from_secs(self.start as u64), None) + } +} + +impl ToSongRange for RangeTo<Duration> { + fn to_range(self) -> song::Range { + song::Range(Duration::from_secs(0), Some(self.end)) + } +} + +impl ToSongRange for RangeTo<u32> { + fn to_range(self) -> song::Range { + song::Range(Duration::from_secs(0), Some(Duration::from_secs(self.end as u64))) + } +} + +impl ToSongRange for RangeFull { + fn to_range(self) -> song::Range { + song::Range(Duration::from_secs(0), None) + } +} + +impl ToSongRange for song::Range { + fn to_range(self) -> song::Range { + self + } +} + +// }}} + +pub trait ToSongPath { + fn to_path(&self) -> &str; +} + +impl ToSongPath for Song { + fn to_path(&self) -> &str { + &self.file + } +} + +impl<'a, T: ToSongPath> ToSongPath for &'a T { + fn to_path(&self) -> &str { + (*self).to_path() + } +} + +impl ToSongPath for dyn AsRef<str> { + fn to_path(&self) -> &str { + self.as_ref() + } +} + +impl<T: ToSongPath> ToArguments for T { + fn to_arguments<F, E>(&self, f: &mut F) -> Result<(), E> + where F: FnMut(&str) -> Result<(), E> + { + self.to_path().to_arguments(f) + } +} diff --git a/vendored/mpd/src/error.rs b/vendored/mpd/src/error.rs new file mode 100644 index 0000000..b58f8dc --- /dev/null +++ b/vendored/mpd/src/error.rs @@ -0,0 +1,394 @@ +//! This module defines different errors occurring during communication with MPD. +//! +//! There're following kinds of possible errors: +//! +//! - IO errors (due to network communication failures), +//! - parsing errors (because of bugs in parsing server response), +//! - protocol errors (happen when we get unexpected data from server, +//! mostly because protocol version mismatch, network data corruption +//! or just bugs in the client), +//! - server errors (run-time errors coming from MPD due to some MPD +//! errors, like database failures or sound problems) +//! +//! This module defines all necessary infrastructure to represent these kinds or errors. + +use std::convert::From; +use std::error::Error as StdError; +use std::fmt; +use std::io::Error as IoError; +use std::num::{ParseFloatError, ParseIntError}; +use std::result; +use std::str::FromStr; +use std::string::ParseError as StringParseError; +use time::ParseError as TimeParseError; +use time::ConversionRangeError as TimeConversionRangeError; + +// Server errors {{{ +/// Server error codes, as defined in [libmpdclient](http://www.musicpd.org/doc/libmpdclient/protocol_8h_source.html) +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum ErrorCode { + /// not a list + NotList = 1, + /// bad command arguments + Argument = 2, + /// invalid password + Password = 3, + /// insufficient permissions + Permission = 4, + /// unknown command + UnknownCmd = 5, + /// object doesn't exist + NoExist = 50, + /// maximum playlist size exceeded + PlaylistMax = 51, + /// general system error + System = 52, + /// error loading playlist + PlaylistLoad = 53, + /// update database is already in progress + UpdateAlready = 54, + /// player synchronization error + PlayerSync = 55, + /// object already exists + Exist = 56, +} + +impl FromStr for ErrorCode { + type Err = ParseError; + fn from_str(s: &str) -> result::Result<ErrorCode, ParseError> { + use self::ErrorCode::*; + match s.parse()? { + 1 => Ok(NotList), + 2 => Ok(Argument), + 3 => Ok(Password), + 4 => Ok(Permission), + 5 => Ok(UnknownCmd), + + 50 => Ok(NoExist), + 51 => Ok(PlaylistMax), + 52 => Ok(System), + 53 => Ok(PlaylistLoad), + 54 => Ok(UpdateAlready), + 55 => Ok(PlayerSync), + 56 => Ok(Exist), + + v => Err(ParseError::BadErrorCode(v)), + } + } +} + +impl StdError for ErrorCode { } + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ErrorCode::*; + + let desc = match *self { + NotList => "not a list", + Argument => "invalid argument", + Password => "invalid password", + Permission => "permission", + UnknownCmd => "unknown command", + + NoExist => "item not found", + PlaylistMax => "playlist overflow", + System => "system", + PlaylistLoad => "playload load", + UpdateAlready => "already updating", + PlayerSync => "player syncing", + Exist => "already exists", + }; + + f.write_str(desc) + } +} + +/// Server error +#[derive(Debug, Clone, PartialEq)] +pub struct ServerError { + /// server error code + pub code: ErrorCode, + /// command position in command list + pub pos: u16, + /// command name, which caused the error + pub command: String, + /// detailed error description + pub detail: String, +} + +impl StdError for ServerError { } + +impl fmt::Display for ServerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} error (`{}') at {}", self.code, self.detail, self.pos) + } +} + +impl FromStr for ServerError { + type Err = ParseError; + fn from_str(s: &str) -> result::Result<ServerError, ParseError> { + // ACK [<code>@<index>] {<command>} <description> + if s.starts_with("ACK [") { + let s = &s[5..]; + if let (Some(atsign), Some(right_bracket)) = (s.find('@'), s.find(']')) { + match (s[..atsign].parse(), s[atsign + 1..right_bracket].parse()) { + (Ok(code), Ok(pos)) => { + let s = &s[right_bracket + 1..]; + if let (Some(left_brace), Some(right_brace)) = (s.find('{'), s.find('}')) { + let command = s[left_brace + 1..right_brace].to_string(); + let detail = s[right_brace + 1..].trim().to_string(); + Ok(ServerError { + code: code, + pos: pos, + command: command, + detail: detail, + }) + } else { + Err(ParseError::NoMessage) + } + } + (Err(_), _) => Err(ParseError::BadCode), + (_, Err(_)) => Err(ParseError::BadPos), + } + } else { + Err(ParseError::NoCodePos) + } + } else { + Err(ParseError::NotAck) + } + } +} +// }}} + +// Error {{{ +/// Main error type, describing all possible error classes for the crate +#[derive(Debug)] +pub enum Error { + /// IO errors (low-level network communication failures) + Io(IoError), + /// parsing errors (unknown data came from server) + Parse(ParseError), + /// protocol errors (e.g. missing required fields in server response, no handshake message etc.) + Proto(ProtoError), + /// server errors (a.k.a. `ACK` responses from server) + Server(ServerError), +} + +/// Shortcut type for MPD results +pub type Result<T> = result::Result<T, Error>; + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match *self { + Error::Io(ref err) => Some(err), + Error::Parse(ref err) => Some(err), + Error::Proto(ref err) => Some(err), + Error::Server(ref err) => Some(err), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Io(ref err) => err.fmt(f), + Error::Parse(ref err) => err.fmt(f), + Error::Proto(ref err) => err.fmt(f), + Error::Server(ref err) => err.fmt(f), + } + } +} + +impl From<IoError> for Error { + fn from(e: IoError) -> Error { + Error::Io(e) + } +} +impl From<ParseError> for Error { + fn from(e: ParseError) -> Error { + Error::Parse(e) + } +} +impl From<ProtoError> for Error { + fn from(e: ProtoError) -> Error { + Error::Proto(e) + } +} +impl From<ParseIntError> for Error { + fn from(e: ParseIntError) -> Error { + Error::Parse(ParseError::BadInteger(e)) + } +} +impl From<ParseFloatError> for Error { + fn from(e: ParseFloatError) -> Error { + Error::Parse(ParseError::BadFloat(e)) + } +} +impl From<TimeParseError> for Error { + fn from(e: TimeParseError) -> Error { + Error::Parse(ParseError::BadTime(e)) + } +} + +impl From<ServerError> for Error { + fn from(e: ServerError) -> Error { + Error::Server(e) + } +} + +impl From<TimeConversionRangeError> for Error { + fn from(e: TimeConversionRangeError) -> Error { + Error::Parse(ParseError::BadTimeConversion(e)) + } +} +// }}} + +// Parse errors {{{ +/// Parsing error kinds +#[derive(Debug, Clone, PartialEq)] +pub enum ParseError { + /// invalid integer + BadInteger(ParseIntError), + /// invalid float + BadFloat(ParseFloatError), + /// some other invalid value + BadValue(String), + /// date/time parsing error + BadTime(TimeParseError), + /// date/time to duration (Unix time) conversion error + BadTimeConversion(TimeConversionRangeError), + /// invalid version format (should be x.y.z) + BadVersion, + /// the response is not an `ACK` (not an error) + /// (this is not actually an error, just a marker + /// to try to parse the response as some other type, + /// like a pair) + NotAck, + /// invalid pair + BadPair, + /// invalid error code in `ACK` response + BadCode, + /// invalid command position in `ACK` response + BadPos, + /// missing command position and/or error code in `ACK` response + NoCodePos, + /// missing error message in `ACK` response + NoMessage, + /// missing bitrate in audio format field + NoRate, + /// missing bits in audio format field + NoBits, + /// missing channels in audio format field + NoChans, + /// invalid bitrate in audio format field + BadRate(ParseIntError), + /// invalid bits in audio format field + BadBits(ParseIntError), + /// invalid channels in audio format field + BadChans(ParseIntError), + /// unknown state in state status field + BadState(String), + /// unknown error code in `ACK` response + BadErrorCode(usize), +} + +impl StdError for ParseError { } + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ParseError::*; + + let desc = match *self { + BadInteger(_) => "invalid integer", + BadFloat(_) => "invalid float", + BadValue(_) => "invalid value", + BadTime(_) => "invalid date/time", + BadTimeConversion(_) => "invalid date/time conversion", + BadVersion => "invalid version", + NotAck => "not an ACK", + BadPair => "invalid pair", + BadCode => "invalid code", + BadPos => "invalid position", + NoCodePos => "missing code and position", + NoMessage => "missing position", + NoRate => "missing audio format rate", + NoBits => "missing audio format bits", + NoChans => "missing audio format channels", + BadRate(_) => "invalid audio format rate", + BadBits(_) => "invalid audio format bits", + BadChans(_) => "invalid audio format channels", + BadState(_) => "invalid playing state", + BadErrorCode(_) => "unknown error code", + }; + + write!(f, "{}", desc) + } +} + +impl From<TimeParseError> for ParseError { + fn from(e: TimeParseError) -> ParseError { + ParseError::BadTime(e) + } +} + +impl From<TimeConversionRangeError> for ParseError { + fn from(e: TimeConversionRangeError) -> ParseError { + ParseError::BadTimeConversion(e) + } +} + +impl From<ParseIntError> for ParseError { + fn from(e: ParseIntError) -> ParseError { + ParseError::BadInteger(e) + } +} + +impl From<ParseFloatError> for ParseError { + fn from(e: ParseFloatError) -> ParseError { + ParseError::BadFloat(e) + } +} + +impl From<StringParseError> for ParseError { + fn from(e: StringParseError) -> ParseError { + match e {} + } +} +// }}} + +// Protocol errors {{{ +/// Protocol errors +/// +/// They usually occur when server violate expected command response format, +/// like missing fields in answer to some command, missing closing `OK` +/// line after data stream etc. +#[derive(Debug, Clone, PartialEq)] +pub enum ProtoError { + /// `OK` was expected, but it was missing + NotOk, + /// a data pair was expected + NotPair, + /// invalid handshake banner received + BadBanner, + /// expected some field, but it was missing + NoField(&'static str), + /// expected sticker value, but didn't find it + BadSticker, +} + +impl StdError for ProtoError { } + +impl fmt::Display for ProtoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let desc = match *self { + ProtoError::NotOk => "OK expected", + ProtoError::NotPair => "pair expected", + ProtoError::BadBanner => "banner error", + ProtoError::NoField(_) => "missing field", + ProtoError::BadSticker => "sticker error", + }; + + write!(f, "{}", desc) + } +} +// }}} diff --git a/vendored/mpd/src/idle.rs b/vendored/mpd/src/idle.rs new file mode 100644 index 0000000..6cb3881 --- /dev/null +++ b/vendored/mpd/src/idle.rs @@ -0,0 +1,176 @@ +//! The module defines structures and protocols for asynchronous MPD communication +//! +//! The MPD supports very simple protocol for asynchronous client notifications about +//! different player events. First user issues `idle` command with optional argument +//! to filter events by source subsystem (like "database", "player", "mixer" etc.) +//! +//! Once in "idle" mode, client connection timeout is disabled, and MPD will notify +//! client about next event when one occurs (if originated from one of designated +//! subsystems, if specified). +//! +//! (Actually MPD notifies only about general subsystem source of event, e.g. +//! if user changed volume, client will get `mixer` event in idle mode, so +//! it should issue `status` command then and check for any mixer-related field +//! changes.) +//! +//! Once some such event occurs, and client is notified about it, idle mode is interrupted, +//! and client must issue another `idle` command to continue listening for interesting +//! events. +//! +//! While in "idle" mode, client can't issue any commands, except for special `noidle` +//! command, which interrupts "idle" mode, and provides a list queued events +//! since last `idle` command, if they occurred. +//! +//! The module describes subsystems enum only, but the main workflow is determined by +//! [`IdleGuard`](struct.IdleGuard.html) struct, which catches mutable reference +//! to original `Client` struct, thus enforcing MPD contract in regards of (im)possibility +//! to send commands while in "idle" mode. + +use crate::client::Client; +use crate::error::{Error, ParseError}; +use crate::proto::Proto; + +use std::fmt; +use std::io::{Read, Write}; +use std::mem::forget; +use std::str::FromStr; + +/// Subsystems for `idle` command +#[derive(Clone, Copy, Debug, PartialEq, RustcEncodable)] +pub enum Subsystem { + /// database: the song database has been modified after update. + Database, + /// update: a database update has started or finished. + /// If the database was modified during the update, the database event is also emitted. + Update, + /// stored_playlist: a stored playlist has been modified, renamed, created or deleted + Playlist, + /// playlist: the current playlist has been modified + Queue, + /// player: the player has been started, stopped or seeked + Player, + /// mixer: the volume has been changed + Mixer, + /// output: an audio output has been enabled or disabled + Output, + /// options: options like repeat, random, crossfade, replay gain + Options, + /// sticker: the sticker database has been modified. + Sticker, + /// subscription: a client has subscribed or unsubscribed to a channel + Subscription, + /// message: a message was received on a channel this client is subscribed to; this event is only emitted when the queue is empty + Message, +} + +impl FromStr for Subsystem { + type Err = ParseError; + fn from_str(s: &str) -> Result<Subsystem, ParseError> { + use self::Subsystem::*; + match s { + "database" => Ok(Database), + "update" => Ok(Update), + "stored_playlist" => Ok(Playlist), + "playlist" => Ok(Queue), + "player" => Ok(Player), + "mixer" => Ok(Mixer), + "output" => Ok(Output), + "options" => Ok(Options), + "sticker" => Ok(Sticker), + "subscription" => Ok(Subscription), + "message" => Ok(Message), + _ => Err(ParseError::BadValue(s.to_owned())), + } + } +} + +impl Subsystem { + fn to_str(self) -> &'static str { + use self::Subsystem::*; + match self { + Database => "database", + Update => "update", + Playlist => "stored_playlist", + Queue => "playlist", + Player => "player", + Mixer => "mixer", + Output => "output", + Options => "options", + Sticker => "sticker", + Subscription => "subscription", + Message => "message", + } + } +} + +impl fmt::Display for Subsystem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.to_str()) + } +} + +use std::result::Result as StdResult; +impl<'a> crate::proto::ToArguments for Subsystem { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + f(self.to_str()) + } +} + +/// "Idle" mode guard enforcing MPD asynchronous events protocol +pub struct IdleGuard<'a, S: 'a + Read + Write>(&'a mut Client<S>); + +impl<'a, S: 'a + Read + Write> IdleGuard<'a, S> { + /// Get list of subsystems with new events, interrupting idle mode in process + pub fn get(self) -> Result<Vec<Subsystem>, Error> { + let result = self.0.read_list("changed").and_then(|v| v.into_iter().map(|b| b.parse().map_err(From::from)).collect()); + forget(self); + result + } +} + +impl<'a, S: 'a + Read + Write> Drop for IdleGuard<'a, S> { + fn drop(&mut self) { + let _ = self.0.run_command("noidle", ()).map(|_| self.0.drain()); + } +} + +/// This trait implements `idle` command of MPD protocol +/// +/// See module's documentation for details. +pub trait Idle { + /// Stream type of a client + type Stream: Read + Write; + + /// Start listening for events from a set of subsystems + /// + /// If empty subsystems slice is given, wait for all event from any subsystem. + /// + /// This method returns `IdleGuard`, which takes mutable reference of an initial client, + /// thus disallowing any operations on this mpd connection. + /// + /// You can call `.get()` method of this struct to stop waiting and get all queued events + /// matching given subsystems filter. This call consumes a guard, stops waiting + /// and releases client object. + /// + /// If the guard goes out of scope, wait lock is released as well, but all queued events + /// will be silently ignored. + fn idle<'a>(&'a mut self, subsystems: &[Subsystem]) -> Result<IdleGuard<'a, Self::Stream>, Error>; + + /// Wait for events from a set of subsystems and return list of affected subsystems + /// + /// This is a blocking operation. If empty subsystems slice is given, + /// wait for all event from any subsystem. + fn wait(&mut self, subsystems: &[Subsystem]) -> Result<Vec<Subsystem>, Error> { + self.idle(subsystems).and_then(IdleGuard::get) + } +} + +impl<S: Read + Write> Idle for Client<S> { + type Stream = S; + fn idle<'a>(&'a mut self, subsystems: &[Subsystem]) -> Result<IdleGuard<'a, S>, Error> { + self.run_command("idle", subsystems)?; + Ok(IdleGuard(self)) + } +} diff --git a/vendored/mpd/src/lib.rs b/vendored/mpd/src/lib.rs new file mode 100644 index 0000000..ccc9c01 --- /dev/null +++ b/vendored/mpd/src/lib.rs @@ -0,0 +1,66 @@ +#![warn(missing_docs)] + +//! MPD client for Rust +//! +//! This crate tries to provide idiomatic Rust API for [Music Player Daemon][mpd]. +//! The main entry point to the API is [`Client`](client/struct.Client.html) struct, +//! and inherent methods of the struct follow [MPD protocol][proto] for most part, +//! making use of traits to overload different parameters for convenience. +//! +//! [mpd]: http://www.musicpd.org/ +//! [proto]: http://www.musicpd.org/doc/protocol/ +//! +//! # Usage +//! +//! ```text +//! [dependencies] +//! mpd = "*" +//! ``` +//! +//! ```rust,no_run +//! extern crate mpd; +//! +//! use mpd::Client; +//! use std::net::TcpStream; +//! +//! # fn main() { +//! let mut conn = Client::connect("127.0.0.1:6600").unwrap(); +//! conn.volume(100).unwrap(); +//! conn.load("My Lounge Playlist", ..).unwrap(); +//! conn.play().unwrap(); +//! println!("Status: {:?}", conn.status()); +//! # } +//! ``` + +mod macros; +mod convert; +pub mod error; +pub mod version; +pub mod reply; +pub mod status; +pub mod song; +pub mod output; +pub mod playlist; +pub mod plugin; +pub mod stats; +pub mod search; +pub mod message; +pub mod idle; +pub mod mount; +mod sticker; + +mod proto; +pub mod client; + +pub use client::Client; +pub use idle::{Idle, Subsystem}; +pub use message::{Channel, Message}; +pub use mount::{Mount, Neighbor}; +pub use output::Output; +pub use playlist::Playlist; +pub use plugin::Plugin; +pub use search::{Query, Term}; +pub use song::{Id, Song}; +pub use stats::Stats; +pub use status::{ReplayGain, State, Status}; +pub use version::Version; diff --git a/vendored/mpd/src/macros.rs b/vendored/mpd/src/macros.rs new file mode 100644 index 0000000..76690db --- /dev/null +++ b/vendored/mpd/src/macros.rs @@ -0,0 +1,27 @@ +#![macro_use] + +macro_rules! get_field_impl { + ($op:ident, $map:expr, bool $name:expr) => { + $map.$op($name).ok_or(Error::Proto(ProtoError::NoField($name))) + .map(|v| v == "1")? + }; + ($op:ident, $map:expr, opt $name:expr) => { + $map.$op($name).map(|v| v.parse().map(Some)).unwrap_or(Ok(None))? + }; + ($op:ident, $map:expr, $name:expr) => { + $map.$op($name).ok_or(Error::Proto(ProtoError::NoField($name))) + .and_then(|v| v.parse().map_err(|e| Error::Parse(From::from(e))))? + }; +} + +macro_rules! get_field { + ($map:expr, bool $name:expr) => { get_field_impl!(get, $map, bool $name) }; + ($map:expr, opt $name:expr) => { get_field_impl!(get, $map, opt $name) }; + ($map:expr, $name:expr) => { get_field_impl!(get, $map, $name) } +} + +macro_rules! pop_field { + ($map:expr, bool $name:expr) => { get_field_impl!(remove, $map, bool $name) }; + ($map:expr, opt $name:expr) => { get_field_impl!(remove, $map, opt $name) }; + ($map:expr, $name:expr) => { get_field_impl!(remove, $map, $name) } +} diff --git a/vendored/mpd/src/message.rs b/vendored/mpd/src/message.rs new file mode 100644 index 0000000..8be122a --- /dev/null +++ b/vendored/mpd/src/message.rs @@ -0,0 +1,73 @@ +//! The module defines structures for MPD client-to-client messaging/subscription protocol +//! +//! The MPD client-to-client messaging protocol is fairly easy one, and is based on channels. +//! Any client can subscribe to arbitrary number of channels, and some other client +//! can send messages to a channel by name. Then, at some point of time, subscribed +//! client can read all queued messages for all channels, it was subscribed to. +//! +//! Also client can get asynchronous notifications about new messages from subscribed +//! channels with `idle` command, by waiting for `message` subsystem events. + +use crate::convert::FromMap; +use crate::error::{Error, ProtoError}; + +use std::collections::BTreeMap; +use std::fmt; + +/// Message +#[derive(Debug, PartialEq, Clone, RustcEncodable)] +pub struct Message { + /// channel + pub channel: Channel, + /// message payload + pub message: String, +} + +impl FromMap for Message { + fn from_map(map: BTreeMap<String, String>) -> Result<Message, Error> { + Ok(Message { + channel: Channel(map.get("channel").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("channel")))?), + message: map.get("message").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("message")))?, + }) + } +} + +/// Channel +#[derive(Debug, PartialEq, PartialOrd, Clone, RustcEncodable)] +pub struct Channel(String); + +impl fmt::Display for Channel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Channel { + /// Create channel with given name + pub fn new(name: &str) -> Option<Channel> { + if Channel::is_valid_name(name) { + Some(Channel(name.to_owned())) + } else { + None + } + } + + /// Create channel with arbitrary name, bypassing name validity checks + /// + /// Not recommened! Use `new()` method above instead. + pub unsafe fn new_unchecked(name: String) -> Channel { + Channel(name) + } + + /// Check if given name is a valid channel name + /// + /// Valid channel name can contain only English letters (`A`-`Z`, `a`-`z`), + /// numbers (`0`-`9`), underscore, forward slash, dot and colon (`_`, `/`, `.`, `:`) + pub fn is_valid_name(name: &str) -> bool { + name.bytes() + .all(|b| { + (0x61 <= b && b <= 0x7a) || (0x41 <= b && b <= 0x5a) || (0x30 <= b && b <= 0x39) || + (b == 0x5f || b == 0x2f || b == 0x2e || b == 0x3a) + }) + } +} diff --git a/vendored/mpd/src/mount.rs b/vendored/mpd/src/mount.rs new file mode 100644 index 0000000..2c9558e --- /dev/null +++ b/vendored/mpd/src/mount.rs @@ -0,0 +1,51 @@ +//! The module describes data structures for MPD (virtual) mounts system +//! +//! This mounts has nothing to do with system-wide Unix mounts, as they are +//! implemented inside MPD only, so they doesn't require root access. +//! +//! The MPD mounts are plugin-based, so MPD can mount any resource as +//! a source of songs for its database (like network shares). +//! +//! Possible, but inactive, mounts are named "neighbors" and can be +//! listed with `neighbors()` method. + +use crate::convert::FromMap; +use crate::error::{Error, ProtoError}; + +use std::collections::BTreeMap; + +/// Mount point +#[derive(Clone, Debug, PartialEq, RustcEncodable)] +pub struct Mount { + /// mount point name + pub name: String, + /// mount storage URI + pub storage: String, +} + +impl FromMap for Mount { + fn from_map(map: BTreeMap<String, String>) -> Result<Mount, Error> { + Ok(Mount { + name: map.get("mount").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("mount")))?, + storage: map.get("storage").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("storage")))?, + }) + } +} + +/// Neighbor +#[derive(Clone, Debug, PartialEq, RustcEncodable)] +pub struct Neighbor { + /// neighbor name + pub name: String, + /// neighbor storage URI + pub storage: String, +} + +impl FromMap for Neighbor { + fn from_map(map: BTreeMap<String, String>) -> Result<Neighbor, Error> { + Ok(Neighbor { + name: map.get("name").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("name")))?, + storage: map.get("neighbor").map(|s| s.to_owned()).ok_or(Error::Proto(ProtoError::NoField("neighbor")))?, + }) + } +} diff --git a/vendored/mpd/src/output.rs b/vendored/mpd/src/output.rs new file mode 100644 index 0000000..d781511 --- /dev/null +++ b/vendored/mpd/src/output.rs @@ -0,0 +1,28 @@ +//! The module describes output + + +use crate::convert::FromMap; +use crate::error::{Error, ProtoError}; +use std::collections::BTreeMap; +use std::convert::From; + +/// Sound output +#[derive(Clone, Debug, PartialEq, RustcEncodable)] +pub struct Output { + /// id + pub id: u32, + /// name + pub name: String, + /// enabled state + pub enabled: bool, +} + +impl FromMap for Output { + fn from_map(map: BTreeMap<String, String>) -> Result<Output, Error> { + Ok(Output { + id: get_field!(map, "outputid"), + name: map.get("outputname").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("outputname")))?, + enabled: get_field!(map, bool "outputenabled"), + }) + } +} diff --git a/vendored/mpd/src/playlist.rs b/vendored/mpd/src/playlist.rs new file mode 100644 index 0000000..3d246c9 --- /dev/null +++ b/vendored/mpd/src/playlist.rs @@ -0,0 +1,33 @@ +//! The module defines playlist data structures + +use crate::convert::FromMap; +use crate::error::{Error, ProtoError, ParseError}; + +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::time::Duration; + +/// Playlist +#[derive(Clone, Debug, PartialEq)] +pub struct Playlist { + /// name + pub name: String, + /// last modified + pub last_mod: Duration, +} + +impl FromMap for Playlist { + fn from_map(map: BTreeMap<String, String>) -> Result<Playlist, Error> { + Ok(Playlist { + name: map.get("playlist").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("playlist")))?, + last_mod: map.get("Last-Modified") + .ok_or(Error::Proto(ProtoError::NoField("Last-Modified"))) + .and_then(|v| { + let parsed: time::Date = time::parse(&*v, "%Y-%m-%dT%H:%M:%SZ") + .map_err(ParseError::BadTime)?; + Ok(std::time::Duration::try_from(parsed - time::date!(1970-01-01)) + .map_err(ParseError::BadTimeConversion)?) + })? + }) + } +} diff --git a/vendored/mpd/src/plugin.rs b/vendored/mpd/src/plugin.rs new file mode 100644 index 0000000..c490d02 --- /dev/null +++ b/vendored/mpd/src/plugin.rs @@ -0,0 +1,45 @@ +//! The module defines decoder plugin data structures + +use crate::convert::FromIter; +use crate::error::Error; + +/// Decoder plugin +#[derive(Clone, Debug, PartialEq, RustcEncodable)] +pub struct Plugin { + /// name + pub name: String, + /// supported file suffixes (extensions) + pub suffixes: Vec<String>, + /// supported MIME-types + pub mime_types: Vec<String>, +} + +impl FromIter for Vec<Plugin> { + fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Self, Error> { + let mut result = Vec::new(); + let mut plugin: Option<Plugin> = None; + for reply in iter { + let (a, b) = reply?; + match &*a { + "plugin" => { + plugin.map(|p| result.push(p)); + + plugin = Some(Plugin { + name: b, + suffixes: Vec::new(), + mime_types: Vec::new(), + }); + } + "mime_type" => { + plugin.as_mut().map(|p| p.mime_types.push(b)); + } + "suffix" => { + plugin.as_mut().map(|p| p.suffixes.push(b)); + } + _ => unreachable!(), + } + } + plugin.map(|p| result.push(p)); + Ok(result) + } +} diff --git a/vendored/mpd/src/proto.rs b/vendored/mpd/src/proto.rs new file mode 100644 index 0000000..af97d58 --- /dev/null +++ b/vendored/mpd/src/proto.rs @@ -0,0 +1,260 @@ +// Hidden internal interface +#![allow(missing_docs)] + +use bufstream::BufStream; + +use crate::convert::{FromIter, FromMap}; +use crate::error::{Error, ProtoError, Result, ParseError}; +use crate::reply::Reply; + +use std::collections::BTreeMap; +use std::fmt; +use std::io::{self, Lines, Read, Write}; +use std::result::Result as StdResult; +use std::str::FromStr; + +pub struct Pairs<I>(pub I); + +impl<I> Iterator for Pairs<I> + where I: Iterator<Item = io::Result<String>> +{ + type Item = Result<(String, String)>; + fn next(&mut self) -> Option<Result<(String, String)>> { + let reply: Option<Result<Reply>> = + self.0.next().map(|v| v.map_err(Error::Io).and_then(|s| s.parse::<Reply>().map_err(Error::Parse))); + match reply { + Some(Ok(Reply::Pair(a, b))) => Some(Ok((a, b))), + None | + Some(Ok(Reply::Ok)) => None, + Some(Ok(Reply::Ack(e))) => Some(Err(Error::Server(e))), + Some(Err(e)) => Some(Err(e)), + } + } +} + +pub struct Maps<'a, I: 'a> { + pairs: &'a mut Pairs<I>, + sep: &'a str, + value: Option<String>, + done: bool, + first: bool, +} + +impl<'a, I> Iterator for Maps<'a, I> + where I: Iterator<Item = io::Result<String>> +{ + type Item = Result<BTreeMap<String, String>>; + fn next(&mut self) -> Option<Result<BTreeMap<String, String>>> { + if self.done { + return None; + } + + let mut map = BTreeMap::new(); + + if let Some(b) = self.value.take() { + map.insert(self.sep.to_owned(), b); + } + + loop { + match self.pairs.next() { + Some(Ok((a, b))) => { + if &*a == self.sep { + self.value = Some(b); + if self.first { + self.first = false; + return self.next(); + } + break; + } else { + map.insert(a, b); + } + } + Some(Err(e)) => return Some(Err(e)), + None => { + self.done = true; + break; + } + } + } + + if map.is_empty() { None } else { Some(Ok(map)) } + } +} + +impl<I> Pairs<I> + where I: Iterator<Item = io::Result<String>> +{ + pub fn split<'a, 'b: 'a>(&'a mut self, f: &'b str) -> Maps<'a, I> { + Maps { + pairs: self, + sep: f, + value: None, + done: false, + first: true, + } + } +} + +// Client inner communication methods {{{ +#[doc(hidden)] +pub trait Proto { + type Stream: Read + Write; + + fn read_bytes(&mut self, bytes: usize) -> Result<Vec<u8>>; + fn read_line(&mut self) -> Result<String>; + fn read_pairs(&mut self) -> Pairs<Lines<&mut BufStream<Self::Stream>>>; + + fn run_command<I>(&mut self, command: &str, arguments: I) -> Result<()> where I: ToArguments; + + fn read_structs<'a, T>(&'a mut self, key: &'static str) -> Result<Vec<T>> + where T: 'a + FromMap + { + self.read_pairs().split(key).map(|v| v.and_then(FromMap::from_map)).collect() + } + + fn read_list(&mut self, key: &'static str) -> Result<Vec<String>> { + self.read_pairs().filter(|r| r.as_ref().map(|&(ref a, _)| *a == key).unwrap_or(true)).map(|r| r.map(|(_, b)| b)).collect() + } + + fn read_struct<'a, T>(&'a mut self) -> Result<T> + where T: 'a + FromIter, + Self::Stream: 'a + { + FromIter::from_iter(self.read_pairs()) + } + + fn drain(&mut self) -> Result<()> { + loop { + let reply = self.read_line()?; + match &*reply { + "OK" | "list_OK" => break, + _ => (), + } + } + Ok(()) + } + + fn expect_ok(&mut self) -> Result<()> { + let line = self.read_line()?; + + match line.parse::<Reply>() { + Ok(Reply::Ok) => Ok(()), + Ok(Reply::Ack(e)) => Err(Error::Server(e)), + Ok(_) => Err(Error::Proto(ProtoError::NotOk)), + Err(e) => Err(From::from(e)), + } + } + + fn read_pair(&mut self) -> Result<(String, String)> { + let line = self.read_line()?; + + match line.parse::<Reply>() { + Ok(Reply::Pair(a, b)) => Ok((a, b)), + Ok(Reply::Ok) => Err(Error::Proto(ProtoError::NotPair)), + Ok(Reply::Ack(e)) => Err(Error::Server(e)), + Err(e) => Err(Error::Parse(e)), + } + } + + fn read_field<T: FromStr>(&mut self, field: &'static str) -> Result<T> + where ParseError: From<T::Err> + { + let (a, b) = self.read_pair()?; + self.expect_ok()?; + if &*a == field { + Ok(b.parse::<T>().map_err(Into::<ParseError>::into)?) + } else { + Err(Error::Proto(ProtoError::NoField(field))) + } + } +} + + +pub trait ToArguments { + fn to_arguments<F, E>(&self, _: &mut F) -> StdResult<(), E> where F: FnMut(&str) -> StdResult<(), E>; +} + +impl ToArguments for () { + fn to_arguments<F, E>(&self, _: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + Ok(()) + } +} + +impl<'a> ToArguments for &'a str { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + f(self) + } +} + +macro_rules! argument_for_display { + ( $x:path ) => { + impl ToArguments for $x { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + f(&self.to_string()) + } + } + }; +} +argument_for_display!{i8} +argument_for_display!{u8} +argument_for_display!{u32} +argument_for_display!{f32} +argument_for_display!{f64} +argument_for_display!{usize} +argument_for_display!{crate::status::ReplayGain} +argument_for_display!{String} +argument_for_display!{crate::song::Id} +argument_for_display!{crate::song::Range} +argument_for_display!{crate::message::Channel} + +macro_rules! argument_for_tuple { + ( $($t:ident: $T: ident),+ ) => { + impl<$($T : ToArguments,)*> ToArguments for ($($T,)*) { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + let ($(ref $t,)*) = *self; + $( + $t.to_arguments(f)?; + )* + Ok(()) + } + } + }; +} +argument_for_tuple!{t0: T0} +argument_for_tuple!{t0: T0, t1: T1} +argument_for_tuple!{t0: T0, t1: T1, t2: T2} +argument_for_tuple!{t0: T0, t1: T1, t2: T2, t3:T3} + +impl<'a, T: ToArguments> ToArguments for &'a [T] { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + for arg in *self { + arg.to_arguments(f)? + } + Ok(()) + } +} + +pub struct Quoted<'a, D: fmt::Display + 'a + ?Sized>(pub &'a D); + +impl<'a, D: fmt::Display + 'a + ?Sized> fmt::Display for Quoted<'a, D> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let unquoted = format!("{}", self.0); + if &unquoted == "" { + // return Ok(()); + } + let quoted = unquoted.replace('\\', r"\\").replace('"', r#"\""#); + formatter.write_fmt(format_args!("\"{}\"", "ed)) + } +} + +// }}} diff --git a/vendored/mpd/src/reply.rs b/vendored/mpd/src/reply.rs new file mode 100644 index 0000000..b288259 --- /dev/null +++ b/vendored/mpd/src/reply.rs @@ -0,0 +1,36 @@ +//! The module describes all possible replies from MPD server. +//! +//! Also it contains most generic parser, which can handle +//! all possible server replies. + + +use crate::error::{ParseError, ServerError}; +use std::str::FromStr; + +/// All possible MPD server replies +#[derive(Debug, Clone, PartialEq)] +pub enum Reply { + /// `OK` and `list_OK` replies + Ok, + /// `ACK` reply (server error) + Ack(ServerError), + /// a data pair reply (in `field: value` format) + Pair(String, String), +} + +impl FromStr for Reply { + type Err = ParseError; + fn from_str(s: &str) -> Result<Reply, ParseError> { + if s == "OK" || s == "list_OK" { + Ok(Reply::Ok) + } else if let Ok(ack) = s.parse::<ServerError>() { + Ok(Reply::Ack(ack)) + } else { + let mut splits = s.splitn(2, ':'); + match (splits.next(), splits.next()) { + (Some(a), Some(b)) => Ok(Reply::Pair(a.to_owned(), b.trim().to_owned())), + _ => Err(ParseError::BadPair), + } + } + } +} diff --git a/vendored/mpd/src/search.rs b/vendored/mpd/src/search.rs new file mode 100644 index 0000000..c800c38 --- /dev/null +++ b/vendored/mpd/src/search.rs @@ -0,0 +1,141 @@ +#![allow(missing_docs)] +// TODO: unfinished functionality + +use crate::proto::ToArguments; +use std::borrow::Cow; +use std::convert::Into; +use std::fmt; +use std::result::Result as StdResult; + +pub enum Term<'a> { + Any, + File, + Base, + LastMod, + Tag(Cow<'a, str>), +} + +pub struct Filter<'a> { + typ: Term<'a>, + what: Cow<'a, str>, +} + +impl<'a> Filter<'a> { + fn new<W>(typ: Term<'a>, what: W) -> Filter + where W: 'a + Into<Cow<'a, str>> + { + Filter { + typ: typ, + what: what.into(), + } + } +} + +pub struct Window(Option<(u32, u32)>); + +impl From<(u32, u32)> for Window { + fn from(window: (u32, u32)) -> Window { + Window(Some(window)) + } +} + +impl From<Option<(u32, u32)>> for Window { + fn from(window: Option<(u32, u32)>) -> Window { + Window(window) + } +} + +#[derive(Default)] +pub struct Query<'a> { + filters: Vec<Filter<'a>>, +} + +impl<'a> Query<'a> { + pub fn new() -> Query<'a> { + Query { filters: Vec::new() } + } + + pub fn and<'b: 'a, V: 'b + Into<Cow<'b, str>>>(&mut self, term: Term<'b>, value: V) -> &mut Query<'a> { + self.filters.push(Filter::new(term, value)); + self + } +} + +impl<'a> fmt::Display for Term<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match *self { + Term::Any => "any", + Term::File => "file", + Term::Base => "base", + Term::LastMod => "modified-since", + Term::Tag(ref tag) => &*tag, + }) + } +} + +impl<'a> ToArguments for &'a Term<'a> { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + f(&self.to_string()) + } +} + +impl<'a> ToArguments for &'a Filter<'a> { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + (&self.typ).to_arguments(f)?; + f(&self.what) + } +} + +impl<'a> ToArguments for &'a Query<'a> { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + for filter in &self.filters { + filter.to_arguments(f)? + } + Ok(()) + } +} + +impl ToArguments for Window { + fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E> + where F: FnMut(&str) -> StdResult<(), E> + { + if let Some(window) = self.0 { + f("window")?; + f(&format!{"{}:{}", window.0, window.1})?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::proto::ToArguments; + + fn collect<I: ToArguments>(arguments: I) -> Vec<String> { + let mut output = Vec::<String>::new(); + arguments.to_arguments::<_, ()>(&mut |arg| Ok(output.push(arg.to_string()))).unwrap(); + output + } + + #[test] + fn find_window_format() { + let window: Window = (0, 2).into(); + let output = collect(window); + assert_eq!(output, vec!["window", "0:2"]); + } + + #[test] + fn find_query_format() { + let mut query = Query::new(); + let finished = query.and(Term::Tag("albumartist".into()), "Mac DeMarco").and(Term::Tag("album".into()), "Salad Days"); + let output = collect(&*finished); + assert_eq!(output, vec!["albumartist", "Mac DeMarco", "album", "Salad Days"]); + } +} diff --git a/vendored/mpd/src/song.rs b/vendored/mpd/src/song.rs new file mode 100644 index 0000000..8730025 --- /dev/null +++ b/vendored/mpd/src/song.rs @@ -0,0 +1,210 @@ +//! The module defines song structs and methods. + +use crate::convert::FromIter; +use crate::error::{Error, ParseError}; + +use rustc_serialize::{Decodable, Decoder, Encodable, Encoder}; + +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; +use std::convert::TryFrom; + +/// Song ID +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Default)] +pub struct Id(pub u32); + +impl Encodable for Id { + fn encode<S: Encoder>(&self, e: &mut S) -> Result<(), S::Error> { + self.0.encode(e) + } +} + +impl Decodable for Id { + fn decode<S: Decoder>(d: &mut S) -> Result<Id, S::Error> { + d.read_u32().map(Id) + } +} + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Song place in the queue +#[derive(Debug, Copy, Clone, PartialEq, Default, RustcEncodable)] +pub struct QueuePlace { + /// song ID + pub id: Id, + /// absolute zero-based song position + pub pos: u32, + /// song priority, if present, defaults to 0 + pub prio: u8, +} + +/// Song range +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Range(pub Duration, pub Option<Duration>); + +impl Encodable for Range { + fn encode<S: Encoder>(&self, e: &mut S) -> Result<(), S::Error> { + e.emit_tuple(2, |e| { + e.emit_tuple_arg(0, |e| e.emit_u64(self.0.as_secs()))?; + e.emit_tuple_arg(1, |e| { + e.emit_option(|e| match self.1 { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + }) + }) + } +} + +impl Default for Range { + fn default() -> Range { + Range(Duration::from_secs(0), None) + } +} + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.as_secs().fmt(f)?; + f.write_str(":")?; + if let Some(v) = self.1 { + v.as_secs().fmt(f)?; + } + Ok(()) + } +} + +impl FromStr for Range { + type Err = ParseError; + fn from_str(s: &str) -> Result<Range, ParseError> { + let mut splits = s.split('-').flat_map(|v| v.parse().into_iter()); + match (splits.next(), splits.next()) { + (Some(s), Some(e)) => Ok(Range(Duration::from_secs(s), Some(Duration::from_secs(e)))), + (None, Some(e)) => Ok(Range(Duration::from_secs(0), Some(Duration::from_secs(e)))), + (Some(s), None) => Ok(Range(Duration::from_secs(s), None)), + (None, None) => Ok(Range(Duration::from_secs(0), None)), + } + } +} + +/// Song data +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Song { + /// filename + pub file: String, + /// name (for streams) + pub name: Option<String>, + /// title + pub title: Option<String>, + /// last modification time + pub last_mod: Option<Duration>, + /// artist + pub artist: Option<String>, + /// duration (in seconds resolution) + pub duration: Option<Duration>, + /// place in the queue (if queued for playback) + pub place: Option<QueuePlace>, + /// range to play (if queued for playback and range was set) + pub range: Option<Range>, + /// arbitrary tags, like album, artist etc + pub tags: BTreeMap<String, String>, +} + +impl Encodable for Song { + fn encode<S: Encoder>(&self, e: &mut S) -> Result<(), S::Error> { + e.emit_struct("Song", 8, |e| { + e.emit_struct_field("file", 0, |e| self.file.encode(e))?; + e.emit_struct_field("name", 1, |e| self.name.encode(e))?; + e.emit_struct_field("title", 2, |e| self.title.encode(e))?; + e.emit_struct_field("last_mod", 3, |e| { + e.emit_option(|e| match self.last_mod { + Some(m) => e.emit_option_some(|e| m.as_secs().encode(e)), + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("artist", 4, |e| self.artist.encode(e))?; + e.emit_struct_field("duration", 5, |e| { + e.emit_option(|e| match self.duration { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("place", 6, |e| self.place.encode(e))?; + e.emit_struct_field("range", 7, |e| self.range.encode(e))?; + e.emit_struct_field("tags", 8, |e| self.tags.encode(e))?; + Ok(()) + }) + } +} + +impl FromIter for Song { + /// build song from map + fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Song, Error> { + let mut result = Song::default(); + + for res in iter { + let line = res?; + match &*line.0 { + "file" => result.file = line.1.to_owned(), + "Title" => result.title = Some(line.1.to_owned()), + "Last-Modified" => { + let parsed: time::Date = time::parse(&*line.1, "%Y-%m-%dT%H:%M:%SZ") + .map_err(ParseError::BadTime)?; + let stamp = std::time::Duration::try_from(parsed - time::date!(1970-01-01))?; + + result.last_mod = Some(stamp); + }, + "Artist" => result.artist = Some(line.1.to_owned()), + "Name" => result.name = Some(line.1.to_owned()), + "Time" => result.duration = Some(Duration::from_secs(line.1.parse()?)), + "Range" => result.range = Some(line.1.parse()?), + "Id" => { + match result.place { + None => { + result.place = Some(QueuePlace { + id: Id(line.1.parse()?), + pos: 0, + prio: 0, + }) + } + Some(ref mut place) => place.id = Id(line.1.parse()?), + } + } + "Pos" => { + match result.place { + None => { + result.place = Some(QueuePlace { + pos: line.1.parse()?, + id: Id(0), + prio: 0, + }) + } + Some(ref mut place) => place.pos = line.1.parse()?, + } + } + "Prio" => { + match result.place { + None => { + result.place = Some(QueuePlace { + prio: line.1.parse()?, + id: Id(0), + pos: 0, + }) + } + Some(ref mut place) => place.prio = line.1.parse()?, + } + } + _ => { + result.tags.insert(line.0, line.1); + } + } + } + + Ok(result) + } +} diff --git a/vendored/mpd/src/stats.rs b/vendored/mpd/src/stats.rs new file mode 100644 index 0000000..3829381 --- /dev/null +++ b/vendored/mpd/src/stats.rs @@ -0,0 +1,78 @@ +//! The module describes DB and playback statistics + +use crate::convert::FromIter; +use crate::error::Error; + +use rustc_serialize::{Encodable, Encoder}; +use std::time::Duration; + +/// DB and playback statistics +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Stats { + /// number of artists in DB + pub artists: u32, + /// number of albums in DB + pub albums: u32, + /// number of songs in DB + pub songs: u32, + /// total MPD uptime, seconds resolution + pub uptime: Duration, + /// total playback time, seconds resolution + pub playtime: Duration, + /// total playback time for all songs in DB, seconds resolution + pub db_playtime: Duration, + /// last DB update timestamp in seconds since Epoch, seconds resolution + pub db_update: Duration, +} + +impl Encodable for Stats { + fn encode<S: Encoder>(&self, e: &mut S) -> Result<(), S::Error> { + e.emit_struct("Stats", 7, |e| { + e.emit_struct_field("artists", 0, |e| self.artists.encode(e))?; + e.emit_struct_field("albums", 1, |e| self.albums.encode(e))?; + e.emit_struct_field("songs", 2, |e| self.songs.encode(e))?; + e.emit_struct_field("uptime", 3, |e| self.uptime.as_secs().encode(e))?; + e.emit_struct_field("playtime", 4, |e| self.playtime.as_secs().encode(e))?; + e.emit_struct_field("db_playtime", 5, |e| self.db_playtime.as_secs().encode(e))?; + e.emit_struct_field("db_update", 6, |e| self.db_update.as_secs().encode(e))?; + Ok(()) + }) + } +} + +impl Default for Stats { + fn default() -> Stats { + Stats { + artists: 0, + albums: 0, + songs: 0, + uptime: Duration::from_secs(0), + playtime: Duration::from_secs(0), + db_playtime: Duration::from_secs(0), + db_update: Duration::from_secs(0), + } + } +} + +impl FromIter for Stats { + /// build stats from iterator + fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Stats, Error> { + let mut result = Stats::default(); + + for res in iter { + let line = res?; + match &*line.0 { + "artists" => result.artists = line.1.parse()?, + "albums" => result.albums = line.1.parse()?, + "songs" => result.songs = line.1.parse()?, + "uptime" => result.uptime = Duration::from_secs(line.1.parse()?), + "playtime" => result.playtime = Duration::from_secs(line.1.parse()?), + "db_playtime" => result.db_playtime = Duration::from_secs(line.1.parse()?), + "db_update" => result.db_update = Duration::from_secs(line.1.parse()?), + _ => (), + } + } + + Ok(result) + } +} diff --git a/vendored/mpd/src/status.rs b/vendored/mpd/src/status.rs new file mode 100644 index 0000000..08c7278 --- /dev/null +++ b/vendored/mpd/src/status.rs @@ -0,0 +1,310 @@ +//! The module defines MPD status data structures + +use crate::convert::FromIter; +use crate::error::{Error, ParseError}; +use crate::song::{Id, QueuePlace}; + +use rustc_serialize::{Encodable, Encoder}; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; + +/// MPD status +#[derive(Debug, PartialEq, Clone, Default)] +pub struct Status { + /// volume (0-100, or -1 if volume is unavailable (e.g. for HTTPD output type) + pub volume: i8, + /// repeat mode + pub repeat: bool, + /// random mode + pub random: bool, + /// single mode + pub single: bool, + /// consume mode + pub consume: bool, + /// queue version number + pub queue_version: u32, + /// queue length + pub queue_len: u32, + /// playback state + pub state: State, + /// currently playing song place in the queue + pub song: Option<QueuePlace>, + /// next song to play place in the queue + pub nextsong: Option<QueuePlace>, + /// time current song played, and total song duration (in seconds resolution) + pub time: Option<(Duration, Duration)>, + /// elapsed play time current song played (in milliseconds resolution) + pub elapsed: Option<Duration>, + /// current song duration + pub duration: Option<Duration>, + /// current song bitrate, kbps + pub bitrate: Option<u32>, + /// crossfade timeout, seconds + pub crossfade: Option<Duration>, + /// mixramp threshold, dB + pub mixrampdb: f32, + /// mixramp duration, seconds + pub mixrampdelay: Option<Duration>, + /// current audio playback format + pub audio: Option<AudioFormat>, + /// current DB updating job number (if DB updating is in progress) + pub updating_db: Option<u32>, + /// last player error (if happened, can be reset with `clearerror()` method) + pub error: Option<String>, + /// replay gain mode + pub replaygain: Option<ReplayGain>, +} + +impl Encodable for Status { + fn encode<S: Encoder>(&self, e: &mut S) -> Result<(), S::Error> { + e.emit_struct("Status", 21, |e| { + e.emit_struct_field("volume", 0, |e| self.volume.encode(e))?; + e.emit_struct_field("repeat", 1, |e| self.repeat.encode(e))?; + e.emit_struct_field("random", 2, |e| self.random.encode(e))?; + e.emit_struct_field("single", 3, |e| self.single.encode(e))?; + e.emit_struct_field("consume", 4, |e| self.consume.encode(e))?; + e.emit_struct_field("queue_version", 5, |e| self.queue_version.encode(e))?; + e.emit_struct_field("queue_len", 6, |e| self.queue_len.encode(e))?; + e.emit_struct_field("state", 7, |e| self.state.encode(e))?; + e.emit_struct_field("song", 8, |e| self.song.encode(e))?; + e.emit_struct_field("nextsong", 9, |e| self.nextsong.encode(e))?; + e.emit_struct_field("time", 10, |e| { + e.emit_option(|e| match self.time { + Some(p) => { + e.emit_option_some(|e| { + e.emit_tuple(2, |e| { + e.emit_tuple_arg(0, |e| p.0.as_secs().encode(e))?; + e.emit_tuple_arg(1, |e| p.1.as_secs().encode(e))?; + Ok(()) + }) + }) + } + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("elapsed", 11, |e| { + e.emit_option(|e| match self.elapsed { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + + })?; + e.emit_struct_field("duration", 12, |e| { + e.emit_option(|e| match self.duration { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("bitrate", 13, |e| self.bitrate.encode(e))?; + e.emit_struct_field("crossfade", 14, |e| { + e.emit_option(|e| match self.crossfade { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("mixrampdb", 15, |e| self.mixrampdb.encode(e))?; + e.emit_struct_field("mixrampdelay", 16, |e| { + e.emit_option(|e| match self.mixrampdelay { + Some(d) => e.emit_option_some(|e| d.as_secs().encode(e)), + None => e.emit_option_none(), + }) + })?; + e.emit_struct_field("audio", 17, |e| self.audio.encode(e))?; + e.emit_struct_field("updating_db", 18, |e| self.updating_db.encode(e))?; + e.emit_struct_field("error", 19, |e| self.error.encode(e))?; + e.emit_struct_field("replaygain", 20, |e| self.replaygain.encode(e))?; + Ok(()) + }) + + } +} + +impl FromIter for Status { + fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Status, Error> { + let mut result = Status::default(); + + for res in iter { + let line = res?; + match &*line.0 { + "volume" => result.volume = line.1.parse()?, + + "repeat" => result.repeat = &*line.1 == "1", + "random" => result.random = &*line.1 == "1", + "single" => result.single = &*line.1 == "1", + "consume" => result.consume = &*line.1 == "1", + + "playlist" => result.queue_version = line.1.parse()?, + "playlistlength" => result.queue_len = line.1.parse()?, + "state" => result.state = line.1.parse()?, + "songid" => { + match result.song { + None => { + result.song = Some(QueuePlace { + id: Id(line.1.parse()?), + pos: 0, + prio: 0, + }) + } + Some(ref mut place) => place.id = Id(line.1.parse()?), + } + } + "song" => { + match result.song { + None => { + result.song = Some(QueuePlace { + pos: line.1.parse()?, + id: Id(0), + prio: 0, + }) + } + Some(ref mut place) => place.pos = line.1.parse()?, + } + } + "nextsongid" => { + match result.nextsong { + None => { + result.nextsong = Some(QueuePlace { + id: Id(line.1.parse()?), + pos: 0, + prio: 0, + }) + } + Some(ref mut place) => place.id = Id(line.1.parse()?), + } + } + "nextsong" => { + match result.nextsong { + None => { + result.nextsong = Some(QueuePlace { + pos: line.1.parse()?, + id: Id(0), + prio: 0, + }) + } + Some(ref mut place) => place.pos = line.1.parse()?, + } + } + "time" => { + let mut splits = line.1.splitn(2, ':').map(|v| v.parse().map_err(ParseError::BadInteger).map(Duration::from_secs)); + result.time = match (splits.next(), splits.next()) { + (Some(Ok(a)), Some(Ok(b))) => Ok(Some((a, b))), + (Some(Err(e)), _) | + (_, Some(Err(e))) => Err(e), + _ => Ok(None), + }?; + } + // TODO" => float errors don't work on stable + "elapsed" => result.elapsed = line.1.parse::<f32>().ok().map(|v| Duration::from_millis((v * 1000.0) as u64)), + "duration" => result.duration = line.1.parse::<f32>().ok().map(|v| Duration::from_millis((v * 1000.0) as u64)), + "bitrate" => result.bitrate = Some(line.1.parse()?), + "xfade" => result.crossfade = Some(Duration::from_secs(line.1.parse()?)), + // "mixrampdb" => 0.0, //get_field!(map, "mixrampdb"), + // "mixrampdelay" => None, //get_field!(map, opt "mixrampdelay").map(|v: f64| Duration::milliseconds((v * 1000.0) as i64)), + "audio" => result.audio = Some(line.1.parse()?), + "updating_db" => result.updating_db = Some(line.1.parse()?), + "error" => result.error = Some(line.1.to_owned()), + "replay_gain_mode" => result.replaygain = Some(line.1.parse()?), + _ => (), + } + } + + Ok(result) + } +} + +/// Audio playback format +#[derive(Debug, Copy, Clone, PartialEq, RustcEncodable)] +pub struct AudioFormat { + /// sample rate, kbps + pub rate: u32, + /// sample resolution in bits, can be 0 for floating point resolution + pub bits: u8, + /// number of channels + pub chans: u8, +} + +impl FromStr for AudioFormat { + type Err = ParseError; + fn from_str(s: &str) -> Result<AudioFormat, ParseError> { + let mut it = s.split(':'); + Ok(AudioFormat { + rate: it.next().ok_or(ParseError::NoRate).and_then(|v| v.parse().map_err(ParseError::BadRate))?, + bits: it.next().ok_or(ParseError::NoBits) + .and_then(|v| if &*v == "f" { + Ok(0) + } else { + v.parse().map_err(ParseError::BadBits) + })?, + chans: it.next().ok_or(ParseError::NoChans).and_then(|v| v.parse().map_err(ParseError::BadChans))?, + }) + } +} + +/// Playback state +#[derive(Debug, Copy, Clone, PartialEq, RustcEncodable, RustcDecodable)] +pub enum State { + /// player stopped + Stop, + /// player is playing + Play, + /// player paused + Pause, +} + +impl Default for State { + fn default() -> State { + State::Stop + } +} + +impl FromStr for State { + type Err = ParseError; + fn from_str(s: &str) -> Result<State, ParseError> { + match s { + "stop" => Ok(State::Stop), + "play" => Ok(State::Play), + "pause" => Ok(State::Pause), + _ => Err(ParseError::BadState(s.to_owned())), + } + } +} + +/// Replay gain mode +#[derive(Debug, Clone, Copy, PartialEq, RustcEncodable, RustcDecodable)] +pub enum ReplayGain { + /// off + Off, + /// track + Track, + /// album + Album, + /// auto + Auto, +} + +impl FromStr for ReplayGain { + type Err = ParseError; + fn from_str(s: &str) -> Result<ReplayGain, ParseError> { + use self::ReplayGain::*; + match s { + "off" => Ok(Off), + "track" => Ok(Track), + "album" => Ok(Album), + "auto" => Ok(Auto), + _ => Err(ParseError::BadValue(s.to_owned())), + } + } +} + +impl fmt::Display for ReplayGain { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::ReplayGain::*; + f.write_str(match *self { + Off => "off", + Track => "track", + Album => "album", + Auto => "auto", + }) + } +} diff --git a/vendored/mpd/src/sticker.rs b/vendored/mpd/src/sticker.rs new file mode 100644 index 0000000..b1a2dc3 --- /dev/null +++ b/vendored/mpd/src/sticker.rs @@ -0,0 +1,23 @@ +use crate::error::ParseError; +use std::str::FromStr; + +pub struct Sticker { + pub name: String, + pub value: String, +} + +impl FromStr for Sticker { + type Err = ParseError; + fn from_str(s: &str) -> Result<Sticker, ParseError> { + let mut parts = s.splitn(2, '='); + match (parts.next(), parts.next()) { + (Some(name), Some(value)) => { + Ok(Sticker { + name: name.to_owned(), + value: value.to_owned(), + }) + } + _ => Err(ParseError::BadValue(s.to_owned())), + } + } +} diff --git a/vendored/mpd/src/version.rs b/vendored/mpd/src/version.rs new file mode 100644 index 0000000..7a62d6a --- /dev/null +++ b/vendored/mpd/src/version.rs @@ -0,0 +1,25 @@ +//! This module defines MPD version type and parsing code + + +use crate::error::ParseError; +use std::str::FromStr; + +// Version {{{ +/// MPD version +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, RustcEncodable)] +pub struct Version(pub u16, pub u16, pub u16); + +impl FromStr for Version { + type Err = ParseError; + fn from_str(s: &str) -> Result<Version, ParseError> { + let mut splits = s.splitn(3, '.').map(FromStr::from_str); + match (splits.next(), splits.next(), splits.next()) { + (Some(Ok(a)), Some(Ok(b)), Some(Ok(c))) => Ok(Version(a, b, c)), + (Some(Err(e)), _, _) | + (_, Some(Err(e)), _) | + (_, _, Some(Err(e))) => Err(ParseError::BadInteger(e)), + _ => Err(ParseError::BadVersion), + } + } +} +// }}} diff --git a/vendored/mpd/tests/data/empty.flac b/vendored/mpd/tests/data/empty.flac Binary files differnew file mode 100644 index 0000000..4407f5b --- /dev/null +++ b/vendored/mpd/tests/data/empty.flac diff --git a/vendored/mpd/tests/helpers/daemon.rs b/vendored/mpd/tests/helpers/daemon.rs new file mode 100644 index 0000000..a8e8b6c --- /dev/null +++ b/vendored/mpd/tests/helpers/daemon.rs @@ -0,0 +1,140 @@ +extern crate tempdir; + +use self::tempdir::TempDir; +use super::mpd; +use std::fs::{File, create_dir}; +use std::io::{Write, Read}; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; +use std::process::{Command, Child, Stdio}; + +struct MpdConfig { + db_file: PathBuf, + music_directory: PathBuf, + playlist_directory: PathBuf, + sticker_file: PathBuf, + config_path: PathBuf, + sock_path: PathBuf, +} + +impl MpdConfig { + pub fn new<P>(base: P) -> MpdConfig + where P: AsRef<Path> + { + let base = base.as_ref(); + MpdConfig { + db_file: base.join("db"), + music_directory: base.join("music"), + playlist_directory: base.join("playlists"), + sticker_file: base.join("sticker_file"), + config_path: base.join("config"), + sock_path: base.join("sock"), + } + } + + fn config_text(&self) -> String { + format!(r#" +db_file "{db_file}" +log_file "/dev/null" +music_directory "{music_directory}" +playlist_directory "{playlist_directory}" +sticker_file "{sticker_file}" +bind_to_address "{sock_path}" +audio_output {{ + type "null" + name "null" +}} +"#, + db_file=self.db_file.display(), + music_directory=self.music_directory.display(), + playlist_directory=self.playlist_directory.display(), + sticker_file=self.sticker_file.display(), + sock_path=self.sock_path.display(), + ) + } + + fn generate(&self) { + create_dir(&self.music_directory).expect("Could not create music directory."); + create_dir(&self.playlist_directory).expect("Could not create playlist directory."); + let mut file = File::create(&self.config_path).expect("Could not create config file."); + file.write_all(self.config_text().as_bytes()).expect("Could not write config file."); + } +} + +pub struct Daemon { + // Saved here so it gets dropped when this does. + _temp_dir: TempDir, + config: MpdConfig, + process: Child, +} + +impl Drop for Daemon { + fn drop(&mut self) { + self.process.kill().expect("Could not kill mpd daemon."); + self.process.wait().expect("Could not wait for mpd daemon to shutdown."); + if let Some(ref mut stderr) = self.process.stderr { + let mut output = String::new(); + stderr.read_to_string(&mut output).expect("Could not collect output from mpd."); + println!{"Output from mpd:"} + println!{"{}", output}; + } + } +} + +fn sleep() { + use std::{thread, time}; + let ten_millis = time::Duration::from_millis(10); + thread::sleep(ten_millis); +} + +static EMPTY_FLAC_BYTES: &'static [u8] = include_bytes!("../data/empty.flac"); + +impl Daemon { + pub fn start() -> Daemon { + let temp_dir = TempDir::new("mpd-test").unwrap(); + let config = MpdConfig::new(&temp_dir); + config.generate(); + + // TODO: Factor out putting files in the music directory. + File::create(config.music_directory.join("empty.flac")).unwrap().write_all(EMPTY_FLAC_BYTES).unwrap(); + + let process = Command::new("mpd") + .arg("--no-daemon") + .arg(&config.config_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .expect("Could not create mpd daemon."); + + let daemon = Daemon { + _temp_dir: temp_dir, + config: config, + process: process, + }; + + // Wait until we can connect to the daemon + let mut client; + loop { + if let Ok(c) = daemon.maybe_connect() { + client = c; + break; + } + sleep() + } + while let Some(_) = client.status().expect("Couldn't get status.").updating_db { + sleep() + } + + daemon + } + + fn maybe_connect(&self) -> Result<mpd::Client<UnixStream>, mpd::error::Error> { + let stream = UnixStream::connect(&self.config.sock_path)?; + mpd::Client::new(stream) + } + + pub fn connect(&self) -> mpd::Client<UnixStream> { + self.maybe_connect().expect("Could not connect to daemon.") + } +} diff --git a/vendored/mpd/tests/helpers/mod.rs b/vendored/mpd/tests/helpers/mod.rs new file mode 100644 index 0000000..e331433 --- /dev/null +++ b/vendored/mpd/tests/helpers/mod.rs @@ -0,0 +1,36 @@ +extern crate mpd; + +mod daemon; + +pub use self::daemon::Daemon; +use std::os::unix::net::UnixStream; + +pub struct DaemonClient { + _daemon: Daemon, + client: mpd::Client<UnixStream>, +} + +use std::ops::{Deref, DerefMut}; + +impl Deref for DaemonClient { + type Target = mpd::Client<UnixStream>; + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl DerefMut for DaemonClient { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} + +#[allow(dead_code)] +pub fn connect() -> DaemonClient { + let daemon = Daemon::start(); + let client = daemon.connect(); + DaemonClient { + _daemon: daemon, + client: client, + } +} diff --git a/vendored/mpd/tests/idle.rs b/vendored/mpd/tests/idle.rs new file mode 100644 index 0000000..d510aa2 --- /dev/null +++ b/vendored/mpd/tests/idle.rs @@ -0,0 +1,20 @@ +extern crate mpd; + +mod helpers; +use helpers::Daemon; + +use mpd::Idle; + +#[test] +fn idle() { + let daemon = Daemon::start(); + let mut mpd = daemon.connect(); + let idle = mpd.idle(&[]).unwrap(); + + let mut mpd1 = daemon.connect(); + mpd1.consume(true).unwrap(); + mpd1.consume(false).unwrap(); + + let sys = idle.get().unwrap(); + assert_eq!(&*sys, &[mpd::Subsystem::Options]); +} diff --git a/vendored/mpd/tests/options.rs b/vendored/mpd/tests/options.rs new file mode 100644 index 0000000..1bfa861 --- /dev/null +++ b/vendored/mpd/tests/options.rs @@ -0,0 +1,73 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; +use std::time::Duration; + +#[test] +fn status() { + let mut mpd = connect(); + let status = mpd.status().unwrap(); + println!("{:?}", status); +} + +#[test] +fn stats() { + let mut mpd = connect(); + let stats = mpd.stats().unwrap(); + println!("{:?}", stats); +} + +macro_rules! test_options_impl { + ($name:ident, $val1:expr, $tval1:expr, $val2:expr, $tval2:expr) => { + #[test] + fn $name() { + let mut mpd = connect(); + mpd.$name($val1).unwrap(); + assert_eq!(mpd.status().unwrap().$name, $tval1); + mpd.$name($val2).unwrap(); + assert_eq!(mpd.status().unwrap().$name, $tval2); + } + } +} + +macro_rules! test_option { + ($name:ident, $val1:expr, $val2:expr) => { + test_options_impl!($name, $val1, $val1, $val2, $val2); + }; + ($name:ident, $val1:expr => $tval1:expr, $val2:expr => $tval2:expr) => { + test_options_impl!($name, $val1, $tval1, $val2, $tval2); + }; +} + +test_option!(consume, true, false); +test_option!(single, true, false); +test_option!(random, true, false); +test_option!(repeat, true, false); +// test_option!(mixrampdb, 1.0f32, 0.0f32); +// test_option!(mixrampdelay, 1 => Some(Duration::from_secs(1)), 0 => None); + +#[test] +fn volume() { + let mut mpd = connect(); + if mpd.status().unwrap().volume >= 0 { + mpd.volume(100).unwrap(); + assert_eq!(mpd.status().unwrap().volume, 100); + mpd.volume(0).unwrap(); + assert_eq!(mpd.status().unwrap().volume, 0); + } +} + +#[test] +fn crossfade() { + let mut mpd = connect(); + mpd.crossfade(1000).unwrap(); + assert_eq!(mpd.status().unwrap().crossfade, Some(Duration::from_secs(1000))); + mpd.crossfade(0).unwrap(); + assert_eq!(mpd.status().unwrap().crossfade, + if mpd.version >= mpd::Version(0, 19, 0) { + None + } else { + Some(Duration::from_secs(0)) + }); +} diff --git a/vendored/mpd/tests/outputs.rs b/vendored/mpd/tests/outputs.rs new file mode 100644 index 0000000..078aa94 --- /dev/null +++ b/vendored/mpd/tests/outputs.rs @@ -0,0 +1,25 @@ +extern crate mpd; +extern crate time; + +mod helpers; +use helpers::connect; + +#[test] +fn outputs() { + let mut mpd = connect(); + println!("{:?}", mpd.outputs()); +} + +#[test] +fn out_toggle() { + let mut mpd = connect(); + + mpd.out_disable(0).unwrap(); + mpd.out_enable(0).unwrap(); + + if mpd.version >= mpd::Version(0, 17, 0) { + mpd.out_toggle(0).unwrap(); + } + + mpd.output(0, true).unwrap(); +} diff --git a/vendored/mpd/tests/playback.rs b/vendored/mpd/tests/playback.rs new file mode 100644 index 0000000..4a51eb8 --- /dev/null +++ b/vendored/mpd/tests/playback.rs @@ -0,0 +1,10 @@ +extern crate mpd; +extern crate time; + +mod helpers; + +#[test] +fn playback() { + let mut mpd = helpers::connect(); + mpd.play().unwrap(); +} diff --git a/vendored/mpd/tests/playlist.rs b/vendored/mpd/tests/playlist.rs new file mode 100644 index 0000000..c8442c2 --- /dev/null +++ b/vendored/mpd/tests/playlist.rs @@ -0,0 +1,15 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; + +#[test] +fn playlists() { + let mut mpd = connect(); + let pls = mpd.playlists().unwrap(); + println!("{:?}", pls); + + for pl in &pls { + println!("{}: {:?}", pl.name, mpd.playlist(&pl.name).unwrap()); + } +} diff --git a/vendored/mpd/tests/reflect.rs b/vendored/mpd/tests/reflect.rs new file mode 100644 index 0000000..f0d0858 --- /dev/null +++ b/vendored/mpd/tests/reflect.rs @@ -0,0 +1,28 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; + +#[test] +fn commands() { + let mut mpd = connect(); + println!("{:?}", mpd.commands().unwrap()); +} + +#[test] +fn urlhandlers() { + let mut mpd = connect(); + println!("{:?}", mpd.urlhandlers().unwrap()); +} + +#[test] +fn decoders() { + let mut mpd = connect(); + println!("{:?}", mpd.decoders().unwrap()); +} + +#[test] +fn tagtypes() { + let mut mpd = connect(); + println!("{:?}", mpd.tagtypes().unwrap()); +} diff --git a/vendored/mpd/tests/search.rs b/vendored/mpd/tests/search.rs new file mode 100644 index 0000000..56c544d --- /dev/null +++ b/vendored/mpd/tests/search.rs @@ -0,0 +1,15 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; +use mpd::Query; + +#[test] +fn search() { + let mut mpd = connect(); + let mut query = Query::new(); + let query = query.and(mpd::Term::Any, "Soul"); + let songs = mpd.find(query, None); + println!("{:?}", songs); + assert!(songs.is_ok()); +} diff --git a/vendored/mpd/tests/song.rs b/vendored/mpd/tests/song.rs new file mode 100644 index 0000000..3510f99 --- /dev/null +++ b/vendored/mpd/tests/song.rs @@ -0,0 +1,28 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; + +#[test] +fn currentsong() { + let mut mpd = connect(); + let song = mpd.currentsong().unwrap(); + println!("{:?}", song); +} + +#[test] +fn queue() { + let mut mpd = connect(); + let queue = mpd.queue().unwrap(); + println!("{:?}", queue); + + let songs = mpd.songs(..).unwrap(); + assert_eq!(songs, queue); +} + +#[test] +fn rescan_update() { + let mut mpd = connect(); + println!("update: {:?}", mpd.update()); + println!("rescan: {:?}", mpd.rescan()); +} diff --git a/vendored/mpd/tests/stickers.rs b/vendored/mpd/tests/stickers.rs new file mode 100644 index 0000000..61aead6 --- /dev/null +++ b/vendored/mpd/tests/stickers.rs @@ -0,0 +1,17 @@ +extern crate mpd; + +mod helpers; +use helpers::connect; + +#[test] +/// Creating a sticker and then getting that sticker returns the value that was set. +fn set_sticker() { + let mut mpd = connect(); + + static VALUE: &'static str = "value"; + + mpd.set_sticker("song", "empty.flac", "test_sticker", VALUE).unwrap(); + + let sticker = mpd.sticker("song", "empty.flac", "test_sticker").unwrap(); + assert_eq!(sticker, VALUE); +} |