mpd_client-1.4.1/.cargo_vcs_info.json0000644000000001500000000000100131510ustar { "git": { "sha1": "c56e165ced2147a6227ec78cca708991c0f9c280" }, "path_in_vcs": "mpd_client" }mpd_client-1.4.1/CHANGELOG.md000064400000000000000000000126651046102023000135700ustar 00000000000000# 1.4.1 (2024-02-28) - Dependency updates. # 1.4.0 (2024-02-01) - Add commands for getting metadata about individual songs or subranges of the play queue ([#22](https://github.com/elomatreb/mpd_client/pull/22), thanks to kholthaus). - Fix a potential panic or overflow on commands that take ranges of song positions. - Dependency updates. # 1.3.0 (2023-10-30) - Add commands for interacting with the ReplayGain options (`ReplayGainStatus`, `SetReplayGainMode`) ([#19](https://github.com/elomatreb/mpd_client/issues/19), [#20](https://github.com/elomatreb/mpd_client/20), thanks to kholthaus). - Internal improvements and dependency updates. # 1.2.0 (2023-07-01) - Add commands for interacting with [client-to-client channels](https://mpd.readthedocs.io/en/latest/protocol.html#client-to-client) (`SubscribeToChannel`, `UnsubscribeFromChannel`, `ListChannels`, `ReadChannelMessages`, `SendChannelMessage`). - Internal improvements and dependency updates. # 1.1.0 (2023-03-13) - Add the `Update` and `Rescan` commands for managing updates to the MPD library (#8, thanks to pborzenkov). # 1.0.0 (2022-08-27) - Redesign the `Command` and `CommandList` traits - Remove trait seal, you can add your own impls now - Remove the `Response` trait, response creation is now handled by a method on the respective trait - Add public functions for constructing `TypedResponseError`s - Reorganize crate modules - Commands now live in their own top-level module - Error types now live in the modules where they are used - Make `chrono` dependency optional - Rename `StateChanges` to `ConnectionEvents` and return an enum of possible events. - Redesign commands to take references to their arguments where necessary instead of taking ownership. - Add commands for managing song stickers ([#14](https://github.com/elomatreb/mpd_client/pull/14), thanks to JakeStanger). - Add `Count` command (proposed by pborzenkov in [#15](https://github.com/elomatreb/mpd_client/pull/15)). - Reimplement `List` command to support type-safe grouping. - Bug fixes: - Missing `CommandList` impl for tuples of size 4 - Missing argument rendering on `GetPlaylist` commnad - Other API changes: - Clean up crate reexports. Now simply reexports the entire `mpd_protocol` crate as `protocol`. - Add `Client::is_connection_closed` - `Status` response: Don't suppress the `default` partition name - `AlbumArt` response: Expose returned raw data as `BytesMut` - `Client::album_art`: Return loaded data as `BytesMut` # 0.7.5 - Add `Ping` command. - Add commands for managing enabled metadata tags (`TagTypes` and `EnabledTagTypes`). # 0.7.4 (2022-06-04) - Fix `ListAllIn` error when response includes playlist objects. # 0.7.3 (2022-03-15) - Fix `List::group_by` generating invalid commands when used (due to missing keyword). # 0.7.2 (2022-02-20) - Add a utility method for connecting with an *optional* password (`Client::connect_with_password_opt`). - Require tokio 0.16.1. # 0.7.1 (2021-12-10) - Fix panic when parsing a `Song` response that contains negative or invalid duration values. # 0.7.0 (2021-12-09) - Response types for typed commands are now marked as `#[non_exhaustive]` where reasonable. This will allow future fields added to MPD to be added to the responses without breaking compatibility. As a result, the `Password` command and the `Client` method have been removed. - Rework connection password handling. Passwords are now specified on the initial connect and sent immediately after. This avoids issues where the `idle` command of the background task is sent before the password, resulting in spurious "permission denied" errors with restrictively configured MPD servers ([#10](https://github.com/elomatreb/mpd_client/issues/10)). - Added new features introduced in version 0.23 of MPD: - New tags (`ComposerSort`, `Ensemble`, `Location`, `Movement`, `MovementNumber`) - New position options for certain commands (`Add`, `AddToPlaylist`, `RemoveFromPlaylist`) - Rework `Move` command to use a builder - Command types are no longer `Copy` if they have private fields (to aid in forward compatibility). - The `Tag` enum now has forward-compatible equality based on the string representation. If a new variant is added, it will be equal to the `Other(_)` variant containing the same string. - Updated `mpd_protocol` dependency. # 0.6.1 (2021-08-21) - Add a limited degree of backwards compatibility for protocol versions older than 0.20 ([#9](https://github.com/elomatreb/mpd_client/pull/9), thanks to D3fus). Specifically, support parsing song durations with fallback to deprecated fields. **NOTE**: Other features still do **not** support these old protocols, notably the filter expressions used by certain commands. - Add a utility method for retrieving MPD subsystem protocol names. - Fix missing `Command` impl for `SetBinaryLimit` command. # 0.6.0 (2021-05-17) - Update `mpd_protocol` - Add `Client::album_art` method for loading album art - Add new MPD subsystems - API changes: - Remove `Client::connect_to` and `Client::connect_unix` methods - Rename `Command::to_command` to `Command::into_command` # 0.5.1 (2021-04-28) - Fix error when parsing list of songs response containing modified timestamps for directories ([#7](https://github.com/elomatreb/mpd_client/issues/7)) # 0.5.0 (2021-01-01) - Update to `tokio` 1.0. # 0.4.0 (2020-11-06) - Add typed commands and command list API - Update to tokio 0.3 - Adapt to MPD 0.22 versions mpd_client-1.4.1/Cargo.lock0000644000000416300000000000100111340ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "async-stream" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", "pin-project-lite", ] [[package]] name = "async-stream-impl" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "num-traits", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", "windows-sys 0.48.0", ] [[package]] name = "mpd_client" version = "1.4.1" dependencies = [ "assert_matches", "bytes", "chrono", "mpd_protocol", "tokio", "tokio-test", "tracing", "tracing-subscriber", ] [[package]] name = "mpd_protocol" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40964143b62ea7878011861ff31b157dbafd5a3785c6d8d3b07f065a7dcbacc2" dependencies = [ "ahash", "bytes", "nom", "tokio", "tracing", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "proc-macro2" version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "smallvec" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "syn" version = "2.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tokio" version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-stream" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-test" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89b3cbabd3ae862100094ae433e1def582cf86451b4e9bf83aa7ac1d8a7d719" dependencies = [ "async-stream", "bytes", "futures-core", "tokio", "tokio-stream", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.3", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" dependencies = [ "windows_aarch64_gnullvm 0.52.3", "windows_aarch64_msvc 0.52.3", "windows_i686_gnu 0.52.3", "windows_i686_msvc 0.52.3", "windows_x86_64_gnu 0.52.3", "windows_x86_64_gnullvm 0.52.3", "windows_x86_64_msvc 0.52.3", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" [[package]] name = "zerocopy" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", "syn", ] mpd_client-1.4.1/Cargo.toml0000644000000025740000000000100111630ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "mpd_client" version = "1.4.1" description = "Asynchronous user-friendly MPD client" readme = "README.md" keywords = [ "mpd", "async", "client", ] categories = ["network-programming"] license = "MIT OR Apache-2.0" repository = "https://github.com/elomatreb/mpd_client" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.bytes] version = "1.5.0" [dependencies.chrono] version = "0.4.34" features = ["std"] optional = true default-features = false [dependencies.mpd_protocol] version = "1.0.3" features = ["async"] [dependencies.tokio] version = "1.36.0" features = [ "rt", "net", "time", "sync", "macros", ] [dependencies.tracing] version = "0.1.40" [dev-dependencies.assert_matches] version = "1.5.0" [dev-dependencies.tokio-test] version = "0.4.3" [dev-dependencies.tracing-subscriber] version = "0.3.18" mpd_client-1.4.1/Cargo.toml.orig000064400000000000000000000016131046102023000146350ustar 00000000000000[package] name = "mpd_client" version = "1.4.1" edition = "2021" description = "Asynchronous user-friendly MPD client" repository = "https://github.com/elomatreb/mpd_client" keywords = ["mpd", "async", "client"] categories = ["network-programming"] license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bytes = "1.5.0" chrono = { version = "0.4.34", default-features = false, features = [ "std", ], optional = true } mpd_protocol = { version = "1.0.3", features = [ "async", ], path = "../mpd_protocol" } tokio = { version = "1.36.0", features = [ "rt", "net", "time", "sync", "macros", ] } tracing = "0.1.40" [dev-dependencies] assert_matches = "1.5.0" tokio-test = "0.4.3" tracing-subscriber = "0.3.18" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] mpd_client-1.4.1/LICENSE-APACHE000064400000000000000000000251371046102023000137010ustar 00000000000000 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. mpd_client-1.4.1/LICENSE-MIT000064400000000000000000000020371046102023000134030ustar 00000000000000Copyright (c) 2020 Ole Bertram 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. mpd_client-1.4.1/README.md000064400000000000000000000017721046102023000132330ustar 00000000000000# `mpd_client` Asynchronous client for [MPD](https://musicpd.org). ## Features - Asynchronous, using [tokio](https://tokio.rs). - Supports protocol version 0.23 and binary responses (e.g. for loading album art). - Typed command API that automatically deals with converting the response into proper Rust structs. - API for programmatically generating filter expressions without string wrangling. ## Example See the `examples` directory for a demo of using printing the currently playing song whenever it changes. ## 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. mpd_client-1.4.1/examples/state_changes.rs000064400000000000000000000030751046102023000167460ustar 00000000000000use std::error::Error; use mpd_client::{ client::{ConnectionEvent, Subsystem}, commands, Client, }; use tokio::net::TcpStream; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { tracing_subscriber::fmt().init(); // Connect via TCP let connection = TcpStream::connect("localhost:6600").await?; // Or through a Unix socket // let connection = UnixStream::connect("/run/user/1000/mpd").await?; // The client is used to issue commands, and state_changes is an async stream of state change // notifications let (client, mut state_changes) = Client::connect(connection).await?; 'outer: loop { match client.command(commands::CurrentSong).await? { Some(song_in_queue) => { println!( "\"{}\" by \"{}\"", song_in_queue.song.title().unwrap_or(""), song_in_queue.song.artists().join(", "), ); } None => println!("(none)"), } loop { // wait for a state change notification in the player subsystem, which indicates a song // change among other things match state_changes.next().await { Some(ConnectionEvent::SubsystemChange(Subsystem::Player)) => break, /* something relevant changed */ Some(ConnectionEvent::SubsystemChange(_)) => continue, /* something changed but we don't care */ _ => break 'outer, // connection was closed by the server } } } Ok(()) } mpd_client-1.4.1/src/client/connection.rs000064400000000000000000000206741046102023000165300ustar 00000000000000use std::{fmt, time::Duration}; use mpd_protocol::{ command::{Command as RawCommand, CommandList as RawCommandList}, response::Response, AsyncConnection, MpdProtocolError, }; use tokio::{ io::{AsyncRead, AsyncWrite}, sync::mpsc::{UnboundedReceiver, UnboundedSender}, time::timeout, }; use tracing::{debug, error, span, trace, Instrument, Level}; use crate::client::{CommandResponder, ConnectionError, ConnectionEvent, Subsystem}; struct State { loop_state: LoopState, connection: AsyncConnection, commands: UnboundedReceiver<(RawCommandList, CommandResponder)>, events: UnboundedSender, } enum LoopState { Idling, WaitingForCommandReply(CommandResponder), } impl fmt::Debug for LoopState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // avoid Debug-printing the noisy internals of the contained channel type match self { LoopState::Idling => write!(f, "Idling"), LoopState::WaitingForCommandReply(_) => write!(f, "WaitingForCommandReply"), } } } fn idle() -> RawCommand { RawCommand::new("idle") } fn cancel_idle() -> RawCommand { RawCommand::new("noidle") } pub(super) async fn run_loop( mut connection: AsyncConnection, commands: UnboundedReceiver<(RawCommandList, CommandResponder)>, events: UnboundedSender, ) where C: AsyncRead + AsyncWrite + Unpin, { trace!("sending initial idle command"); if let Err(e) = connection.send(idle()).await { error!(error = ?e, "failed to send initial idle command"); let _ = events.send(ConnectionEvent::ConnectionClosed(e.into())); return; } let mut state = State { loop_state: LoopState::Idling, connection, commands, events, }; trace!("entering run loop"); loop { let span = span!(Level::TRACE, "iteration", state = ?state.loop_state); match run_loop_iteration(state).instrument(span).await { Ok(new_state) => state = new_state, Err(()) => break, } } trace!("exited run_loop"); } /// Time to wait for another command to send before starting the idle loop. const NEXT_COMMAND_IDLE_TIMEOUT: Duration = Duration::from_millis(100); async fn run_loop_iteration(mut state: State) -> Result, ()> where C: AsyncRead + AsyncWrite + Unpin, { match state.loop_state { LoopState::Idling => { // We are idling (the last command sent to the server was an IDLE). // Wait for either a command to send or a message from the server, which would be a // state change notification. tokio::select! { response = state.connection.receive() => { handle_idle_response(&mut state, response).await?; } command = state.commands.recv() => { handle_command(&mut state, command).await?; } } } LoopState::WaitingForCommandReply(responder) => { // We're waiting for the response to the command associated with `responder`. let response = state.connection.receive().await.transpose().ok_or(())?; trace!("response to command received"); let _ = responder.send(response.map_err(Into::into)); let next_command = timeout(NEXT_COMMAND_IDLE_TIMEOUT, state.commands.recv()); // See if we can immediately send the next command match next_command.await { Ok(Some((command, responder))) => { trace!(?command, "next command immediately available"); match state.connection.send_list(command).await { Ok(_) => state.loop_state = LoopState::WaitingForCommandReply(responder), Err(e) => { error!(error = ?e, "failed to send command"); let _ = responder.send(Err(e.into())); return Err(()); } } } Ok(None) => return Err(()), Err(_) => { trace!("reached next command timeout, idling"); // Start idling again state.loop_state = LoopState::Idling; if let Err(e) = state.connection.send(idle()).await { error!(error = ?e, "failed to start idling after receiving command response"); let _ = state .events .send(ConnectionEvent::ConnectionClosed(e.into())); return Err(()); } } } } } Ok(state) } async fn handle_command( state: &mut State, command: Option<(RawCommandList, CommandResponder)>, ) -> Result<(), ()> where C: AsyncRead + AsyncWrite + Unpin, { let (command, responder) = command.ok_or(())?; trace!(?command, "command received"); // Cancel currently ongoing idle if let Err(e) = state.connection.send(cancel_idle()).await { error!(error = ?e, "failed to cancel idle prior to sending command"); let _ = responder.send(Err(e.into())); return Err(()); } // Receive the response to the cancellation match state.connection.receive().await { Ok(None) => return Err(()), Ok(Some(res)) => match res.into_single_frame() { Ok(f) => { if let Some(subsystem) = Subsystem::from_frame(f) { debug!(?subsystem, "state change"); let _ = state .events .send(ConnectionEvent::SubsystemChange(subsystem)); } } Err(e) => { error!( code = e.code, message = e.message, "idle cancel returned an error" ); let _ = state.events.send(ConnectionEvent::ConnectionClosed( ConnectionError::InvalidResponse, )); return Err(()); } }, Err(e) => { error!(error = ?e, "state change error prior to sending command"); let _ = responder.send(Err(e.into())); return Err(()); } } // Actually send the command. This sets the state for the next loop // iteration. match state.connection.send_list(command).await { Ok(_) => state.loop_state = LoopState::WaitingForCommandReply(responder), Err(e) => { error!(error = ?e, "failed to send command"); let _ = responder.send(Err(e.into())); return Err(()); } } trace!("command sent successfully"); Ok(()) } async fn handle_idle_response( state: &mut State, response: Result, MpdProtocolError>, ) -> Result<(), ()> where C: AsyncRead + AsyncWrite + Unpin, { trace!("handling idle response"); match response { Ok(Some(res)) => { match res.into_single_frame() { Ok(f) => { if let Some(subsystem) = Subsystem::from_frame(f) { debug!(?subsystem, "state change"); let _ = state .events .send(ConnectionEvent::SubsystemChange(subsystem)); } } Err(e) => { error!(code = e.code, message = e.message, "idle returned an error"); let _ = state.events.send(ConnectionEvent::ConnectionClosed( ConnectionError::InvalidResponse, )); return Err(()); } } if let Err(e) = state.connection.send(idle()).await { error!(error = ?e, "failed to start idling after state change"); let _ = state .events .send(ConnectionEvent::ConnectionClosed(e.into())); return Err(()); } } Ok(None) => return Err(()), // The connection was closed Err(e) => { error!(error = ?e, "state change error"); let _ = state .events .send(ConnectionEvent::ConnectionClosed(e.into())); return Err(()); } } Ok(()) } mpd_client-1.4.1/src/client/mod.rs000064400000000000000000000734041046102023000151470ustar 00000000000000//! The client implementation. mod connection; use std::{ fmt, hash::{Hash, Hasher}, io, sync::Arc, }; use bytes::BytesMut; use mpd_protocol::{ command::{Command as RawCommand, CommandList as RawCommandList}, response::{Error, Frame, Response as RawResponse}, AsyncConnection, MpdProtocolError, }; use tokio::{ io::{AsyncRead, AsyncWrite}, sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, oneshot, }, }; use tracing::{debug, error, span, trace, warn, Instrument, Level}; use crate::{ commands::{self as cmds, Command, CommandList}, responses::TypedResponseError, }; type CommandResponder = oneshot::Sender>; /// Components of a connection. /// /// This contains a [`Client`], which you can use to issue commands, and a [`ConnectionEvents`] value, /// which is a stream that receives connection events. pub type Connection = (Client, ConnectionEvents); /// A client connected to an MPD server. /// /// You can use this to send commands to the MPD server. Cloning the /// `Client` will reuse the connection, similar to how a channel sender works. /// /// # Sending Commands /// /// The main way to send commands is through the [`Client::command`] method. This method allows you /// to use one of the predefined commands from the [`commands`][crate::commands] module to /// automatically send the proper command and get a strongly typed parsed response back. /// /// ## Example /// /// ```no_run /// use mpd_client::{commands::Status, Client}; /// use tokio::net::TcpStream; /// /// async fn print_play_state() { /// let connection = TcpStream::connect("localhost:6600").await.unwrap(); /// let (client, _) = Client::connect(connection).await.unwrap(); /// /// let status = client.command(Status).await.unwrap(); /// /// println!("The play state is: {:?}", status.state); /// } /// ``` /// /// # Sending Commands Lists /// /// Command lists are used similarly through the [`Client::command_list`] method. You can issue /// multiple separate commands by constructing a tuple of commands, or a dynamically sized list of /// the same type of command with a [`Vec`] of commands. /// /// ## Example /// /// ```no_run /// use mpd_client::{ /// commands::{Stats, Status}, /// Client, /// }; /// use tokio::net::TcpStream; /// /// async fn print_play_state_and_stats() { /// let connection = TcpStream::connect("localhost:6600").await.unwrap(); /// let (client, _) = Client::connect(connection).await.unwrap(); /// /// let (status, stats) = client.command_list((Status, Stats)).await.unwrap(); /// /// println!( /// "The play state is: {:?} and we have {} songs in the library", /// status.state, stats.songs /// ); /// } /// ``` /// /// # Sending Raw Commands /// /// Alteratively to the typed command interface described in the previous sections, you can use the /// [`Client::raw_command`] and [`Client::raw_command_list`] methods to send [raw /// commands][mpd_protocol::command::Command]. /// /// # Connection Management /// /// Cloning the `Client` is cheap and reuses the same connection it was initially given. Dropping /// the last clone of a particular `Client` will close the connection automatically. #[derive(Clone)] pub struct Client { commands_sender: UnboundedSender<(RawCommandList, CommandResponder)>, protocol_version: Arc, } impl Client { /// Connect to the MPD server using the given connection. /// /// Commonly used with [TCP connections](tokio::net::TcpStream) or [Unix /// sockets](tokio::net::UnixStream). /// /// # Panics /// /// Since this spawns a task internally, this will panic when called outside a Tokio runtime. /// /// # Errors /// /// This will return an error if sending the initial commands over the given transport fails. pub async fn connect(connection: C) -> Result where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { do_connect(connection, None).await.map_err(|e| match e { ConnectWithPasswordError::ProtocolError(e) => e, ConnectWithPasswordError::IncorrectPassword => unreachable!(), }) } /// Connect to the password-protected MPD server using the given connection and password. /// /// Commonly used with [TCP connections](tokio::net::TcpStream) or [Unix /// sockets](tokio::net::UnixStream). /// /// # Panics /// /// Since this spawns a task internally, this will panic when called outside a Tokio runtime. /// /// # Errors /// /// This will return an error if sending the initial commands over the given transport fails, /// or if the password is incorrect. pub async fn connect_with_password( connection: C, password: &str, ) -> Result where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { do_connect(connection, Some(password)).await } /// Connect to the possibly password-protected MPD server using the given connection and password. /// /// Commonly used with [TCP connections](tokio::net::TcpStream) or [Unix /// sockets](tokio::net::UnixStream). /// /// # Panics /// /// Since this spawns a task internally, this will panic when called outside a Tokio runtime. /// /// # Errors /// /// This will return an error if sending the initial commands over the given transport fails, /// or if the password is incorrect. pub async fn connect_with_password_opt( connection: C, password: Option<&str>, ) -> Result where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { do_connect(connection, password).await } /// Send a [command]. /// /// This will automatically parse the response to a proper type. /// /// # Errors /// /// This returns errors in the same conditions as [`Client::raw_command`], and additionally if the /// response fails to convert to the expected type. /// /// [command]: super::commands pub async fn command(&self, cmd: C) -> Result where C: Command, { let command = cmd.command(); let frame = self.raw_command(command).await?; let response = cmd.response(frame)?; Ok(response) } /// Send the given command list, and return the (typed) responses. /// /// # Errors /// /// This returns errors in the same conditions as [`Client::raw_command_list`], and /// additionally if the response type conversion fails. pub async fn command_list(&self, list: L) -> Result where L: CommandList, { let frames = match list.command_list() { Some(cmds) => self.raw_command_list(cmds).await?, None => Vec::new(), }; list.responses(frames).map_err(Into::into) } /// Send the given command, and return the response to it. /// /// # Errors /// /// This will return an error if the connection to MPD is closed (cleanly) or a protocol error /// occurs (including IO errors), or if the command results in an MPD error. pub async fn raw_command(&self, command: RawCommand) -> Result { self.do_send(RawCommandList::new(command)) .await? .into_single_frame() .map_err(|error| CommandError::ErrorResponse { error, succesful_frames: Vec::new(), }) } /// Send the given command list, and return the raw response frames to the contained commands. /// /// # Errors /// /// Errors will be returned in the same conditions as with [`Client::raw_command`], but if /// *any* of the commands in the list return an error condition, the entire list will be /// treated as an error. /// /// You may recover possible successful fields in a response from the [error]. /// /// [error]: CommandError::ErrorResponse pub async fn raw_command_list( &self, commands: RawCommandList, ) -> Result, CommandError> { debug!(?commands, "sending command"); let res = self.do_send(commands).await?; let mut frames = Vec::with_capacity(res.successful_frames()); for frame in res { match frame { Ok(f) => frames.push(f), Err(error) => { return Err(CommandError::ErrorResponse { error, succesful_frames: frames, }); } } } Ok(frames) } /// Load album art for the given URI. /// /// # Behavior /// /// This first tries to use the [`readpicture`][cmds::AlbumArtEmbedded] command to load /// embedded data, before falling back to reading from a separate file using the /// [`albumart`](cmds::AlbumArt) command. /// /// **Note**: Due to the default binary size limit of MPD being quite low, loading larger art /// will issue many commands and can be slow. Consider increasing the /// [binary size limit][cmds::SetBinaryLimit]. /// /// # Return value /// /// If this method returns successfully, a return value of `None` indicates that no album art /// for the given URI was found. Otherwise, you will get a tuple consisting of the raw binary /// data, and an optional string value that contains a MIME type for the data, if one was /// provided by the server. /// /// # Errors /// /// This returns errors in the same conditions as [`Client::command`]. #[tracing::instrument(skip(self))] pub async fn album_art( &self, uri: &str, ) -> Result)>, CommandError> { debug!("loading album art"); let mut out = BytesMut::new(); let mut expected_size = 0; let mut embedded = false; let mut mime = None; // Try loadding embedded album art first match self.command(cmds::AlbumArtEmbedded::new(uri)).await { Ok(Some(resp)) => { out = resp.data; expected_size = resp.size; out.reserve(expected_size); embedded = true; mime = resp.mime; debug!(length = resp.size, ?mime, "found embedded album art"); } Ok(None) => { debug!("readpicture command gave no result, falling back"); } Err(e) => match e { CommandError::ErrorResponse { error, .. } if error.code == 5 => { debug!("readpicture command unsupported, falling back"); } e => return Err(e), }, } if !embedded { if let Some(resp) = self.command(cmds::AlbumArt::new(uri)).await? { out = resp.data; expected_size = resp.size; out.reserve(expected_size); debug!(length = expected_size, "found separate file album art"); } else { debug!("no embedded or separate album art found"); return Ok(None); } } while out.len() < expected_size { let resp = if embedded { self.command(cmds::AlbumArtEmbedded::new(uri).offset(out.len())) .await? } else { self.command(cmds::AlbumArt::new(uri).offset(out.len())) .await? }; if let Some(resp) = resp { trace!(received = resp.data.len(), progress = out.len()); out.extend_from_slice(&resp.data); } else { warn!(progress = out.len(), "incomplete cover art response"); return Ok(None); } } debug!(length = expected_size, "finished loading"); Ok(Some((out, mime))) } /// Get the protocol version the underlying connection is using. pub fn protocol_version(&self) -> &str { self.protocol_version.as_ref() } /// Returns `true` if the connection to the server has been closed (by the server or due to an /// error). pub fn is_connection_closed(&self) -> bool { self.commands_sender.is_closed() } async fn do_send(&self, commands: RawCommandList) -> Result { let (tx, rx) = oneshot::channel(); self.commands_sender .send((commands, tx)) .map_err(|_| CommandError::ConnectionClosed)?; rx.await.map_err(|_| CommandError::ConnectionClosed)? } } impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Client") .field("protocol_version", &self.protocol_version) .finish_non_exhaustive() } } /// Perform the initial handshake to the server. async fn do_connect( io: IO, password: Option<&str>, ) -> Result { let span = span!(Level::DEBUG, "client connection"); let (state_changes_sender, state_changes) = unbounded_channel(); let (commands_sender, commands_receiver) = unbounded_channel(); let mut connection = match AsyncConnection::connect(io).instrument(span.clone()).await { Ok(c) => c, Err(e) => { error!(error = ?e, "failed to perform initial handshake"); return Err(e.into()); } }; let protocol_version = Arc::from(connection.protocol_version()); if let Some(password) = password { trace!(parent: &span, "sending password"); if let Err(e) = connection .send(RawCommand::new("password").argument(password.to_owned())) .instrument(span.clone()) .await { error!(parent: &span, error = ?e, "failed to send password"); return Err(e.into()); } match connection.receive().instrument(span.clone()).await { Err(e) => { error!(parent: &span, error = ?e, "failed to receive reply to password"); return Err(e.into()); } Ok(None) => { error!( parent: &span, "unexpected end of stream after sending password" ); return Err(MpdProtocolError::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "connection closed while waiting for reply to password", )) .into()); } Ok(Some(response)) if response.is_error() => { error!(parent: &span, "incorrect password"); return Err(ConnectWithPasswordError::IncorrectPassword); } Ok(Some(_)) => { trace!(parent: &span, "password accepted"); } } } tokio::spawn( connection::run_loop(connection, commands_receiver, state_changes_sender) .instrument(span!(parent: &span, Level::TRACE, "run loop")), ); let state_changes = ConnectionEvents(state_changes); let client = Client { commands_sender, protocol_version, }; Ok((client, state_changes)) } /// Errors which can occur when issuing a command. #[derive(Debug)] pub enum CommandError { /// The connection to MPD was closed cleanly ConnectionClosed, /// An underlying protocol error occurred, including IO errors Protocol(MpdProtocolError), /// Command returned an error ErrorResponse { /// The error error: Error, /// Possible successful frames in the same response, empty if not in a command list succesful_frames: Vec, }, /// A [typed command](crate::commands) failed to convert its response. InvalidTypedResponse(TypedResponseError), } impl fmt::Display for CommandError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CommandError::ConnectionClosed => write!(f, "the connection is closed"), CommandError::Protocol(_) => write!(f, "protocol error"), CommandError::InvalidTypedResponse(_) => { write!(f, "response was invalid for typed command") } CommandError::ErrorResponse { error, succesful_frames, } => { write!( f, "command returned an error [code {}]: {}", error.code, error.message, )?; if !succesful_frames.is_empty() { write!(f, " (after {} succesful frames)", succesful_frames.len())?; } Ok(()) } } } } impl std::error::Error for CommandError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { CommandError::Protocol(e) => Some(e), CommandError::InvalidTypedResponse(e) => Some(e), _ => None, } } } #[doc(hidden)] impl From for CommandError { fn from(e: MpdProtocolError) -> Self { CommandError::Protocol(e) } } #[doc(hidden)] impl From for CommandError { fn from(e: TypedResponseError) -> Self { CommandError::InvalidTypedResponse(e) } } /// Error returned when [connecting with a password][Client::connect_with_password] fails. #[derive(Debug)] pub enum ConnectWithPasswordError { /// The provided password was not accepted by the server. IncorrectPassword, /// An unrelated protocol error occurred. ProtocolError(MpdProtocolError), } impl fmt::Display for ConnectWithPasswordError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ConnectWithPasswordError::IncorrectPassword => write!(f, "incorrect password"), ConnectWithPasswordError::ProtocolError(_) => write!(f, "protocol error"), } } } impl std::error::Error for ConnectWithPasswordError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { ConnectWithPasswordError::ProtocolError(e) => Some(e), ConnectWithPasswordError::IncorrectPassword => None, } } } #[doc(hidden)] impl From for ConnectWithPasswordError { fn from(e: MpdProtocolError) -> Self { ConnectWithPasswordError::ProtocolError(e) } } /// Receiver for [connection events][ConnectionEvent]. /// /// This includes notifications about state changes as well as the connection being closed, /// possibly due to an error. If you don't care about these, you can just drop this receiver. #[derive(Debug)] pub struct ConnectionEvents(pub(crate) UnboundedReceiver); impl ConnectionEvents { /// Wait for the next connection event. /// /// If this returns `None`, the connection was closed cleanly. pub async fn next(&mut self) -> Option { self.0.recv().await } } /// Events that occur during connection life cycle. #[derive(Debug)] pub enum ConnectionEvent { /// A change event in one of the subsystems of the server occurred. SubsystemChange(Subsystem), /// The connection was closed because of an error. ConnectionClosed(ConnectionError), } /// Subsystems of MPD which can receive state change notifications. /// /// Derived from [the documentation](https://www.musicpd.org/doc/html/protocol.html#command-idle), /// but also includes a catch-all to remain forward-compatible. #[allow(missing_docs)] #[non_exhaustive] #[derive(Clone, Debug)] pub enum Subsystem { Database, Message, Mixer, Options, Output, Partition, Player, /// Called `playlist` in the protocol. Queue, Sticker, StoredPlaylist, Subscription, Update, Neighbor, Mount, /// Catch-all variant used when the above variants do not match. Includes the raw subsystem /// from the MPD response. Other(Box), } impl Subsystem { fn from_frame(mut r: Frame) -> Option { r.get("changed").map(|raw| match &*raw { "database" => Subsystem::Database, "message" => Subsystem::Message, "mixer" => Subsystem::Mixer, "options" => Subsystem::Options, "output" => Subsystem::Output, "partition" => Subsystem::Partition, "player" => Subsystem::Player, "playlist" => Subsystem::Queue, "sticker" => Subsystem::Sticker, "stored_playlist" => Subsystem::StoredPlaylist, "subscription" => Subsystem::Subscription, "update" => Subsystem::Update, "neighbor" => Subsystem::Neighbor, "mount" => Subsystem::Mount, _ => Subsystem::Other(raw.into()), }) } /// Returns the raw protocol name used for this subsystem. pub fn as_str(&self) -> &str { match self { Subsystem::Database => "database", Subsystem::Message => "message", Subsystem::Mixer => "mixer", Subsystem::Options => "options", Subsystem::Output => "output", Subsystem::Partition => "partition", Subsystem::Player => "player", Subsystem::Queue => "playlist", Subsystem::Sticker => "sticker", Subsystem::StoredPlaylist => "stored_playlist", Subsystem::Subscription => "subscription", Subsystem::Update => "update", Subsystem::Neighbor => "neighbor", Subsystem::Mount => "mount", Subsystem::Other(r) => r, } } } impl PartialEq for Subsystem { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } impl Eq for Subsystem {} impl Hash for Subsystem { fn hash(&self, state: &mut H) { self.as_str().hash(state); } } /// Errors which result in the connection being closed. #[derive(Debug)] pub enum ConnectionError { /// An underlying protocol error occurred, including IO errors. Protocol(MpdProtocolError), /// An invalid response was received (such as in response to the `idle` commands). InvalidResponse, } impl fmt::Display for ConnectionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ConnectionError::Protocol(_) => write!(f, "protocol error"), ConnectionError::InvalidResponse => write!(f, "invalid response"), } } } impl std::error::Error for ConnectionError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { ConnectionError::Protocol(e) => Some(e), ConnectionError::InvalidResponse => None, } } } impl From for ConnectionError { fn from(e: MpdProtocolError) -> Self { ConnectionError::Protocol(e) } } #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; use assert_matches::assert_matches; use tokio_test::io::Builder as MockBuilder; use super::*; static GREETING: &[u8] = b"OK MPD 0.21.11\n"; #[tokio::test] async fn single_state_change() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .read(b"changed: player\nOK\n") .write(b"idle\n") .build(); let (_client, mut state_changes) = Client::connect(io).await.expect("connect failed"); assert_matches!( state_changes.next().await, Some(ConnectionEvent::SubsystemChange(Subsystem::Player)) ); } #[tokio::test] async fn command() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"changed: playlist\nOK\n") .write(b"hello\n") .read(b"foo: bar\nOK\n") .write(b"idle\n") .build(); let (client, mut state_changes) = Client::connect(io).await.expect("connect failed"); let response = client .raw_command(RawCommand::new("hello")) .await .expect("command failed"); assert_eq!(response.find("foo"), Some("bar")); assert_matches!( state_changes.next().await, Some(ConnectionEvent::SubsystemChange(Subsystem::Queue)) ); assert!(state_changes.next().await.is_none()); } #[tokio::test] async fn incomplete_response() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"hello\n") .read(b"foo: bar\n") .read(b"baz: qux\nOK\n") .write(b"idle\n") .build(); let (client, _state_changes) = Client::connect(io).await.expect("connect failed"); let response = client .raw_command(RawCommand::new("hello")) .await .expect("command failed"); assert_eq!(response.find("foo"), Some("bar")); } #[tokio::test] async fn command_list() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"command_list_ok_begin\nfoo\nbar\ncommand_list_end\n") .read(b"foo: asdf\nlist_OK\n") .read(b"baz: qux\nlist_OK\nOK\n") .write(b"idle\n") .build(); let (client, _state_changes) = Client::connect(io).await.expect("connect failed"); let mut commands = RawCommandList::new(RawCommand::new("foo")); commands.add(RawCommand::new("bar")); let responses = client .raw_command_list(commands) .await .expect("command failed"); assert_eq!(responses.len(), 2); assert_eq!(responses[0].find("foo"), Some("asdf")); } #[tokio::test] async fn dropping_client() { let io = MockBuilder::new().read(GREETING).write(b"idle\n").build(); let (client, mut state_changes) = Client::connect(io).await.expect("connect failed"); drop(client); assert!(state_changes.next().await.is_none()); } #[tokio::test] async fn album_art() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"readpicture foo/bar.mp3 0\n") .read(b"size: 6\ntype: image/jpeg\nbinary: 3\nFOO\nOK\n") .write(b"readpicture foo/bar.mp3 3\n") .read(b"size: 6\ntype: image/jpeg\nbinary: 3\nBAR\nOK\n") .build(); let (client, _) = Client::connect(io).await.expect("connect failed"); let x = client .album_art("foo/bar.mp3") .await .expect("command failed"); assert_eq!( x, Some((BytesMut::from("FOOBAR"), Some(String::from("image/jpeg")))) ); } #[tokio::test] async fn album_art_fallback() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"readpicture foo/bar.mp3 0\n") .read(b"OK\n") .write(b"albumart foo/bar.mp3 0\n") .read(b"size: 6\nbinary: 3\nFOO\nOK\n") .write(b"albumart foo/bar.mp3 3\n") .read(b"size: 6\nbinary: 3\nBAR\nOK\n") .build(); let (client, _) = Client::connect(io).await.expect("connect failed"); let x = client .album_art("foo/bar.mp3") .await .expect("command failed"); assert_eq!(x, Some((BytesMut::from("FOOBAR"), None))); } #[tokio::test] async fn album_art_fallback_error() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"readpicture foo/bar.mp3 0\n") .read(b"ACK [5@0] {} unknown command \"readpicture\"\n") .write(b"albumart foo/bar.mp3 0\n") .read(b"size: 6\nbinary: 3\nFOO\nOK\n") .write(b"albumart foo/bar.mp3 3\n") .read(b"size: 6\nbinary: 3\nBAR\nOK\n") .build(); let (client, _) = Client::connect(io).await.expect("connect failed"); let x = client .album_art("foo/bar.mp3") .await .expect("command failed"); assert_eq!(x, Some((BytesMut::from("FOOBAR"), None))); } #[tokio::test] async fn album_art_none() { let io = MockBuilder::new() .read(GREETING) .write(b"idle\n") .write(b"noidle\n") .read(b"OK\n") .write(b"readpicture foo/bar.mp3 0\n") .read(b"OK\n") .write(b"albumart foo/bar.mp3 0\n") .read(b"OK\n") .build(); let (client, _) = Client::connect(io).await.expect("connect failed"); let x = client .album_art("foo/bar.mp3") .await .expect("command failed"); assert_eq!(x, None); } #[tokio::test] async fn protocol_version() { let io = MockBuilder::new().read(GREETING).write(b"idle\n").build(); let (client, _state_changes) = Client::connect(io).await.expect("connect failed"); assert_eq!(client.protocol_version(), "0.21.11"); } #[test] fn subsystem_equality() { assert_eq!(Subsystem::Player, Subsystem::Other("player".into())); let mut a = DefaultHasher::new(); Subsystem::Player.hash(&mut a); let mut b = DefaultHasher::new(); Subsystem::Other("player".into()).hash(&mut b); assert_eq!(a.finish(), b.finish()); } } mpd_client-1.4.1/src/commands/command_list.rs000064400000000000000000000064671046102023000173710ustar 00000000000000use mpd_protocol::{command::CommandList as RawCommandList, response::Frame}; use crate::{commands::Command, responses::TypedResponseError}; /// Types which can be used as a typed command list, using /// [`Client::command_list`][crate::Client::command_list]. /// /// This is implemented for tuples of [`Command`s][Command] where it returns a tuple of the same /// size of the responses corresponding to the commands, as well as for a vector of the same /// command type where it returns a vector of the same length of the responses. pub trait CommandList { /// The responses the list will result in. type Response; /// The command list that will be sent, or `None` if no commands. fn command_list(&self) -> Option; /// Convert the raw response frames into the proper response types(s). /// /// # Errors /// /// This should return an error if any of the responses were invalid. fn responses(self, frames: Vec) -> Result; } /// Arbitrarily long sequence of the same command. impl CommandList for Vec where C: Command, { type Response = Vec; fn command_list(&self) -> Option { let mut commands = self.iter().map(Command::command); let mut raw_commands = RawCommandList::new(commands.next()?); raw_commands.extend(commands); Some(raw_commands) } fn responses(self, frames: Vec) -> Result { assert_eq!(self.len(), frames.len()); let mut out = Vec::with_capacity(self.len()); for (command, frame) in self.into_iter().zip(frames) { out.push(command.response(frame)?); } Ok(out) } } macro_rules! impl_command_list_tuple { ($first_type:ident, $($further_type:ident => $further_idx:tt),*) => { impl<$first_type, $($further_type),*> CommandList for ($first_type, $($further_type),*) where $first_type: Command, $( $further_type: Command ),* { type Response = ($first_type::Response, $($further_type::Response),*); fn command_list(&self) -> Option { #[allow(unused_mut)] let mut commands = RawCommandList::new(self.0.command()); $( commands.add(self.$further_idx.command()); )* Some(commands) } fn responses(self, frames: Vec) -> Result { let mut frames = frames.into_iter(); Ok(( self.0.response(frames.next().unwrap())?, $( self.$further_idx.response(frames.next().unwrap())?, )* )) } } }; } impl_command_list_tuple!(A,); impl_command_list_tuple!(A, B => 1); impl_command_list_tuple!(A, B => 1, C => 2); impl_command_list_tuple!(A, B => 1, C => 2, D => 3); impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4); impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5); impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5, G => 6); impl_command_list_tuple!(A, B => 1, C => 2, D => 3, E => 4, F => 5, G => 6, H => 7); mpd_client-1.4.1/src/commands/definitions.rs000064400000000000000000001603051046102023000172230ustar 00000000000000//! Definitions of commands. use std::{ cmp::min, fmt::Write, ops::{Bound, RangeBounds}, time::Duration, }; use bytes::BytesMut; use mpd_protocol::{ command::{Argument, Command as RawCommand}, response::Frame, }; use crate::{ commands::{Command, ReplayGainMode, SeekMode, SingleMode, Song, SongId, SongPosition}, filter::Filter, responses::{self as res, value, TypedResponseError}, tag::Tag, }; macro_rules! argless_command { // Utility branch to generate struct with doc expression (#[doc = $doc:expr], $item:item) => { #[doc = $doc] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] $item }; ($name:ident, $command:literal) => { argless_command!( #[doc = concat!("`", $command, "` command.")], pub struct $name; ); impl Command for $name { type Response = (); fn command(&self) -> RawCommand { RawCommand::new($command) } fn response(self, _: Frame) -> Result { Ok(()) } } }; } macro_rules! single_arg_command { // Utility branch to generate struct with doc expression (#[doc = $doc:expr], $item:item) => { #[doc = $doc] #[derive(Clone, Debug, PartialEq, Eq)] #[allow(missing_copy_implementations)] $item }; ($name:ident $(<$lt:lifetime>)?, $argtype:ty, $command:literal) => { single_arg_command!( #[doc = concat!("`", $command, "` command.")], pub struct $name $(<$lt>)? (pub $argtype); ); impl $(<$lt>)? Command for $name $(<$lt>)? { type Response = (); fn command(&self) -> RawCommand { RawCommand::new($command) .argument(&self.0) } fn response(self, _: Frame) -> Result { Ok(()) } } }; } argless_command!(ClearQueue, "clear"); argless_command!(Next, "next"); argless_command!(Ping, "ping"); argless_command!(Previous, "previous"); argless_command!(Stop, "stop"); single_arg_command!(ClearPlaylist<'a>, &'a str, "playlistclear"); single_arg_command!(DeletePlaylist<'a>, &'a str, "rm"); single_arg_command!(SaveQueueAsPlaylist<'a>, &'a str, "save"); single_arg_command!(SetConsume, bool, "consume"); single_arg_command!(SetPause, bool, "pause"); single_arg_command!(SetRandom, bool, "random"); single_arg_command!(SetRepeat, bool, "repeat"); single_arg_command!(SubscribeToChannel<'a>, &'a str, "subscribe"); single_arg_command!(UnsubscribeFromChannel<'a>, &'a str, "unsubscribe"); /// `replay_gain_status` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ReplayGainStatus; impl Command for ReplayGainStatus { type Response = res::ReplayGainStatus; fn command(&self) -> RawCommand { RawCommand::new("replay_gain_status") } fn response(self, frame: Frame) -> Result { res::ReplayGainStatus::from_frame(frame) } } /// `status` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Status; impl Command for Status { type Response = res::Status; fn command(&self) -> RawCommand { RawCommand::new("status") } fn response(self, frame: Frame) -> Result { res::Status::from_frame(frame) } } /// `stats` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Stats; impl Command for Stats { type Response = res::Stats; fn command(&self) -> RawCommand { RawCommand::new("stats") } fn response(self, frame: Frame) -> Result { res::Stats::from_frame(frame) } } /// `playlistinfo` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Queue; impl Command for Queue { type Response = Vec; fn command(&self) -> RawCommand { RawCommand::new("playlistinfo") } fn response(self, frame: Frame) -> Result { res::SongInQueue::from_frame_multi(frame) } } impl Queue { /// Get the metadata about the entire queue. pub fn all() -> Queue { Queue } /// Get the metadata for a specific song in the queue. pub fn song(song: S) -> QueueRange where S: Into, { QueueRange(SongOrSongRange::Single(song.into())) } /// Get the metadata for a range of songs in the queue. pub fn range(range: R) -> QueueRange where R: RangeBounds, { QueueRange(SongOrSongRange::Range(SongRange::new(range))) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct SongRange { from: usize, to: Option, } impl SongRange { fn new_usize>(range: R) -> Self { let from = match range.start_bound() { Bound::Excluded(pos) => pos.saturating_add(1), Bound::Included(pos) => *pos, Bound::Unbounded => 0, }; let to = match range.end_bound() { Bound::Excluded(pos) => Some(*pos), Bound::Included(pos) => Some(pos.saturating_add(1)), Bound::Unbounded => None, }; Self { from, to } } fn new>(range: R) -> Self { let from = match range.start_bound() { Bound::Excluded(pos) => Bound::Excluded(pos.0), Bound::Included(pos) => Bound::Included(pos.0), Bound::Unbounded => Bound::Unbounded, }; let to = match range.end_bound() { Bound::Excluded(pos) => Bound::Excluded(pos.0), Bound::Included(pos) => Bound::Included(pos.0), Bound::Unbounded => Bound::Unbounded, }; Self::new_usize((from, to)) } } impl Argument for SongRange { fn render(&self, buf: &mut BytesMut) { if let Some(to) = self.to { write!(buf, "{}:{}", self.from, to).unwrap(); } else { write!(buf, "{}:", self.from).unwrap(); } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum SongOrSongRange { /// Single Song Single(Song), /// Song Range Range(SongRange), } /// `playlistinfo` / `playlistid` commands. /// /// These return the metadata of specific individual songs or subranges of the queue. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct QueueRange(SongOrSongRange); impl QueueRange { /// Get the metadata for a specific song in the queue. pub fn song(song: S) -> Self where S: Into, { Self(SongOrSongRange::Single(song.into())) } /// Get the metadata for a range of songs in the queue. pub fn range(range: R) -> Self where R: RangeBounds, { Self(SongOrSongRange::Range(SongRange::new(range))) } } impl Command for QueueRange { type Response = Vec; fn command(&self) -> RawCommand { match self.0 { SongOrSongRange::Single(Song::Id(id)) => RawCommand::new("playlistid").argument(id), SongOrSongRange::Single(Song::Position(pos)) => { RawCommand::new("playlistinfo").argument(pos) } SongOrSongRange::Range(range) => RawCommand::new("playlistinfo").argument(range), } } fn response(self, frame: Frame) -> Result { res::SongInQueue::from_frame_multi(frame) } } /// `currentsong` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CurrentSong; impl Command for CurrentSong { type Response = Option; fn command(&self) -> RawCommand { RawCommand::new("currentsong") } fn response(self, frame: Frame) -> Result { res::SongInQueue::from_frame_single(frame) } } /// `listplaylists` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GetPlaylists; impl Command for GetPlaylists { type Response = Vec; fn command(&self) -> RawCommand { RawCommand::new("listplaylists") } fn response(self, frame: Frame) -> Result { res::Playlist::parse_frame(frame) } } /// `tagtypes` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GetEnabledTagTypes; impl Command for GetEnabledTagTypes { type Response = Vec; fn command(&self) -> RawCommand { RawCommand::new("tagtypes") } fn response(self, frame: Frame) -> Result { let mut out = Vec::with_capacity(frame.fields_len()); for (key, value) in frame { if &*key != "tagtype" { return Err(TypedResponseError::unexpected_field( "tagtype", key.as_ref(), )); } let tag = Tag::try_from(&*value) .map_err(|e| TypedResponseError::invalid_value("tagtype", value).source(e))?; out.push(tag); } Ok(out) } } /// `listplaylistinfo` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct GetPlaylist<'a>(pub &'a str); impl<'a> Command for GetPlaylist<'a> { type Response = Vec; fn command(&self) -> RawCommand { RawCommand::new("listplaylistinfo").argument(self.0) } fn response(self, frame: Frame) -> Result { res::Song::from_frame_multi(frame) } } /// `setvol` command. /// /// Set the volume. The value is truncated to fit in the range `0..=100`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SetVolume(pub u8); impl Command for SetVolume { type Response = (); fn command(&self) -> RawCommand { let volume = min(self.0, 100); RawCommand::new("setvol").argument(volume) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `single` command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SetSingle(pub SingleMode); impl Command for SetSingle { type Response = (); fn command(&self) -> RawCommand { let single = match self.0 { SingleMode::Disabled => "0", SingleMode::Enabled => "1", SingleMode::Oneshot => "oneshot", }; RawCommand::new("single").argument(single) } fn response(self, _: Frame) -> Result { Ok(()) } } /// 'replay_gain_mode' command #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SetReplayGainMode(pub ReplayGainMode); impl Command for SetReplayGainMode { type Response = (); fn command(&self) -> RawCommand { let rgm = match self.0 { ReplayGainMode::Off => "off", ReplayGainMode::Track => "track", ReplayGainMode::Album => "album", ReplayGainMode::Auto => "auto", }; RawCommand::new("replay_gain_mode").argument(rgm) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `crossfade` command. /// /// The given duration is rounded down to whole seconds. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Crossfade(pub Duration); impl Command for Crossfade { type Response = (); fn command(&self) -> RawCommand { let seconds = self.0.as_secs(); RawCommand::new("crossfade").argument(seconds) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `seek` and `seekid` commands. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SeekTo(pub Song, pub Duration); impl Command for SeekTo { type Response = (); fn command(&self) -> RawCommand { let command = match self.0 { Song::Position(pos) => RawCommand::new("seek").argument(pos), Song::Id(id) => RawCommand::new("seekid").argument(id), }; command.argument(self.1) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `seekcur` command. /// /// Seek in the current song. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Seek(pub SeekMode); impl Command for Seek { type Response = (); fn command(&self) -> RawCommand { let time = match self.0 { SeekMode::Absolute(pos) => format!("{:.3}", pos.as_secs_f64()), SeekMode::Forward(time) => format!("+{:.3}", time.as_secs_f64()), SeekMode::Backward(time) => format!("-{:.3}", time.as_secs_f64()), }; RawCommand::new("seekcur").argument(time) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `shuffle` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct Shuffle(Option); impl Shuffle { /// Shuffle entire queue pub fn all() -> Self { Self(None) } /// Shuffle a range of songs /// /// The range must have at least a lower bound. pub fn range(range: R) -> Self where R: RangeBounds, { Self(Some(SongRange::new(range))) } } impl Command for Shuffle { type Response = (); fn command(&self) -> RawCommand { match self.0 { None => RawCommand::new("shuffle"), Some(range) => RawCommand::new("shuffle").argument(range), } } fn response(self, _: Frame) -> Result { Ok(()) } } /// `play` and `playid` commands. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Play(Option); impl Play { /// Play the current song (when paused or stopped). pub fn current() -> Self { Self(None) } /// Play the given song. pub fn song(song: S) -> Self where S: Into, { Self(Some(song.into())) } } impl Command for Play { type Response = (); fn command(&self) -> RawCommand { match self.0 { None => RawCommand::new("play"), Some(Song::Position(pos)) => RawCommand::new("play").argument(pos), Some(Song::Id(id)) => RawCommand::new("playid").argument(id), } } fn response(self, _: Frame) -> Result { Ok(()) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PositionOrRelative { Absolute(SongPosition), BeforeCurrent(usize), AfterCurrent(usize), } impl Argument for PositionOrRelative { fn render(&self, buf: &mut BytesMut) { match self { PositionOrRelative::Absolute(pos) => pos.render(buf), PositionOrRelative::AfterCurrent(x) => write!(buf, "+{x}").unwrap(), PositionOrRelative::BeforeCurrent(x) => write!(buf, "-{x}").unwrap(), } } } /// `addid` command. /// /// Add a song to the queue, returning its ID. If neither of [`Add::at`], [`Add::before_current`], /// or [`Add::after_current`] is used, the song will be appended to the queue. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Add<'a> { uri: &'a str, position: Option, } impl<'a> Add<'a> { /// Add the song with the given URI. /// /// Only individual files are supported. pub fn uri(uri: &'a str) -> Self { Self { uri, position: None, } } /// Add the URI at the given position in the queue. pub fn at>(mut self, position: P) -> Self { self.position = Some(PositionOrRelative::Absolute(position.into())); self } /// Add the URI `delta` positions before the current song. /// /// A `delta` of 0 is immediately before the current song. /// /// **NOTE**: Supported on protocol versions later than 0.23. pub fn before_current(mut self, delta: usize) -> Self { self.position = Some(PositionOrRelative::BeforeCurrent(delta)); self } /// Add the URI `delta` positions after the current song. /// /// A `delta` of 0 is immediately after the current song. /// /// **NOTE**: Supported on protocol versions later than 0.23. pub fn after_current(mut self, delta: usize) -> Self { self.position = Some(PositionOrRelative::AfterCurrent(delta)); self } } impl<'a> Command for Add<'a> { type Response = SongId; fn command(&self) -> RawCommand { let mut command = RawCommand::new("addid").argument(self.uri); if let Some(pos) = self.position { command.add_argument(pos).unwrap(); } command } fn response(self, mut frame: Frame) -> Result { value(&mut frame, "Id").map(SongId) } } /// `delete` and `deleteid` commands. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Delete(Target); #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Target { Id(SongId), Range(SongRange), } impl Delete { /// Remove the given ID from the queue. pub fn id(id: SongId) -> Self { Self(Target::Id(id)) } /// Remove the song at the given position from the queue. pub fn position(pos: SongPosition) -> Self { let range = SongRange::new(pos..=pos); Self(Target::Range(range)) } /// Remove the given range from the queue. /// /// The range must have at least a lower bound. pub fn range(range: R) -> Self where R: RangeBounds, { Self(Target::Range(SongRange::new(range))) } } impl Command for Delete { type Response = (); fn command(&self) -> RawCommand { match self.0 { Target::Id(id) => RawCommand::new("deleteid").argument(id), Target::Range(range) => RawCommand::new("delete").argument(range), } } fn response(self, _: Frame) -> Result { Ok(()) } } /// `move` and `moveid` commands. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Move { from: Target, to: PositionOrRelative, } impl Move { /// Move the song with the given ID. pub fn id(id: SongId) -> MoveBuilder { MoveBuilder(Target::Id(id)) } /// Move the song at the given position. pub fn position(position: SongPosition) -> MoveBuilder { MoveBuilder(Target::Range(SongRange::new(position..=position))) } /// Move the given range of song positions. /// /// # Panics /// /// The given range must have an end. If a range with an open end is passed, this will panic. pub fn range(range: R) -> MoveBuilder where R: RangeBounds, { if let Bound::Unbounded = range.end_bound() { panic!("move commands must not have an open end"); } MoveBuilder(Target::Range(SongRange::new(range))) } } /// Builder for `move` or `moveid` commands. /// /// Returned by methods on [`Move`]. #[must_use] #[derive(Clone, Debug, PartialEq, Eq)] pub struct MoveBuilder(Target); impl MoveBuilder { /// Move the selection to the given absolute queue position. pub fn to_position(self, position: SongPosition) -> Move { Move { from: self.0, to: PositionOrRelative::Absolute(position), } } /// Move the selection to the given `delta` after the current song. /// /// A `delta` of 0 means immediately after the current song. pub fn after_current(self, delta: usize) -> Move { Move { from: self.0, to: PositionOrRelative::AfterCurrent(delta), } } /// Move the selection to the given `delta` before the current song. /// /// A `delta` of 0 means immediately before the current song. pub fn before_current(self, delta: usize) -> Move { Move { from: self.0, to: PositionOrRelative::BeforeCurrent(delta), } } } impl Command for Move { type Response = (); fn command(&self) -> RawCommand { let command = match self.from { Target::Id(id) => RawCommand::new("moveid").argument(id), Target::Range(range) => RawCommand::new("move").argument(range), }; command.argument(self.to) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `find` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Find { filter: Filter, sort: Option, window: Option, } impl Find { /// Find all songs matching `filter`. pub fn new(filter: Filter) -> Self { Self { filter, sort: None, window: None, } } /// Sort the result by the given tag. /// /// This does some special-casing for certain tags, see the [MPD documentation][0] for details. /// /// # Panics /// /// This will panic when sending the command if you pass a malformed value using the /// [`Other`][error] variant. /// /// [0]: https://www.musicpd.org/doc/html/protocol.html#command-find /// [error]: crate::tag::Tag::Other pub fn sort(mut self, sort_by: Tag) -> Self { self.sort = Some(sort_by); self } /// Limit the result to the given window. pub fn window(mut self, window: R) -> Self where R: RangeBounds, { self.window = Some(SongRange::new_usize(window)); self } } impl Command for Find { type Response = Vec; fn command(&self) -> RawCommand { let mut command = RawCommand::new("find").argument(&self.filter); if let Some(sort) = &self.sort { command.add_argument("sort").unwrap(); command .add_argument(sort.as_str()) .expect("Invalid sort value"); } if let Some(window) = self.window { command.add_argument("window").unwrap(); command.add_argument(window).unwrap(); } command } fn response(self, frame: Frame) -> Result { res::Song::from_frame_multi(frame) } } /// `list` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct List { tag: Tag, filter: Option, group_by: [Tag; N], } impl List<0> { /// List distinct values of `tag`. pub fn new(tag: Tag) -> List<0> { List { tag, filter: None, group_by: [], } } } impl List { /// Filter the songs being considered using the given `filter`. /// /// This will overwrite the filter if called multiple times. pub fn filter(mut self, filter: Filter) -> Self { self.filter = Some(filter); self } /// Group results by the given tag. /// /// This will overwrite the grouping if called multiple times. pub fn group_by(self, group_by: [Tag; M]) -> List { List { tag: self.tag, filter: self.filter, group_by, } } } impl Command for List { type Response = res::List; fn command(&self) -> RawCommand { let mut command = RawCommand::new("list").argument(&self.tag); if let Some(filter) = self.filter.as_ref() { command.add_argument(filter).unwrap(); } for group_by in &self.group_by { command.add_argument("group").unwrap(); command.add_argument(group_by).unwrap(); } command } fn response(self, frame: Frame) -> Result { Ok(res::List::from_frame(self.tag, self.group_by, frame)) } } /// `count` command without grouping. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Count { filter: Filter, } impl Count { /// Count the number and total playtime of all songs matching the given filter. pub fn new(filter: Filter) -> Count { Count { filter } } /// Group the results by the given tag. pub fn group_by(self, group_by: Tag) -> CountGrouped { CountGrouped { filter: Some(self.filter), group_by, } } } impl Command for Count { type Response = res::Count; fn command(&self) -> RawCommand { RawCommand::new("count").argument(&self.filter) } fn response(self, frame: Frame) -> Result { res::Count::from_frame(frame) } } /// `count` command with grouping. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CountGrouped { group_by: Tag, filter: Option, } impl CountGrouped { /// Count the number and total playtime of songs grouped by the given tag. pub fn new(group_by: Tag) -> CountGrouped { CountGrouped { group_by, filter: None, } } /// Only consider songs matching the given filter. /// /// If called multiple times, this will overwrite the filter. pub fn filter(mut self, filter: Filter) -> CountGrouped { self.filter = Some(filter); self } } impl Command for CountGrouped { type Response = Vec<(String, res::Count)>; fn command(&self) -> RawCommand { let mut cmd = RawCommand::new("count"); if let Some(filter) = &self.filter { cmd.add_argument(filter).unwrap(); } cmd.argument("group").argument(&self.group_by) } fn response(self, frame: Frame) -> Result { res::Count::from_frame_grouped(frame, &self.group_by) } } /// `rename` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct RenamePlaylist<'a> { from: &'a str, to: &'a str, } impl<'a> RenamePlaylist<'a> { /// Rename the playlist named `from` to `to`. pub fn new(from: &'a str, to: &'a str) -> Self { Self { from, to } } } impl<'a> Command for RenamePlaylist<'a> { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("rename") .argument(self.from) .argument(self.to) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `load` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LoadPlaylist<'a> { name: &'a str, range: Option, } impl<'a> LoadPlaylist<'a> { /// Load the playlist with the given name into the queue. pub fn name(name: &'a str) -> Self { Self { name, range: None } } /// Limit the loaded playlist to the given window. pub fn range(mut self, range: R) -> Self where R: RangeBounds, { self.range = Some(SongRange::new_usize(range)); self } } impl<'a> Command for LoadPlaylist<'a> { type Response = (); fn command(&self) -> RawCommand { let mut command = RawCommand::new("load").argument(self.name); if let Some(range) = self.range { command.add_argument(range).unwrap(); } command } fn response(self, _: Frame) -> Result { Ok(()) } } /// `playlistadd` command. /// /// If [`AddToPlaylist::at`] is not used, the song will be appended to the playlist. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AddToPlaylist<'a> { playlist: &'a str, song_url: &'a str, position: Option, } impl<'a> AddToPlaylist<'a> { /// Add `song_url` to `playlist`. pub fn new(playlist: &'a str, song_url: &'a str) -> Self { Self { playlist, song_url, position: None, } } /// Add the URI at the given position in the queue. /// /// **NOTE**: Supported on protocol versions later than 0.23.3. pub fn at>(mut self, position: P) -> Self { self.position = Some(position.into()); self } } impl<'a> Command for AddToPlaylist<'a> { type Response = (); fn command(&self) -> RawCommand { let mut command = RawCommand::new("playlistadd") .argument(self.playlist) .argument(self.song_url); if let Some(pos) = self.position { command.add_argument(pos).unwrap(); } command } fn response(self, _: Frame) -> Result { Ok(()) } } /// `playlistdelete` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct RemoveFromPlaylist<'a> { playlist: &'a str, target: PositionOrRange, } #[derive(Clone, Debug, PartialEq, Eq)] enum PositionOrRange { Position(usize), Range(SongRange), } impl<'a> RemoveFromPlaylist<'a> { /// Delete the song at `position` from `playlist`. pub fn position(playlist: &'a str, position: usize) -> Self { RemoveFromPlaylist { playlist, target: PositionOrRange::Position(position), } } /// Delete the specified range of songs from `playlist`. pub fn range(playlist: &'a str, range: R) -> Self where R: RangeBounds, { RemoveFromPlaylist { playlist, target: PositionOrRange::Range(SongRange::new(range)), } } } impl<'a> Command for RemoveFromPlaylist<'a> { type Response = (); fn command(&self) -> RawCommand { let command = RawCommand::new("playlistdelete").argument(self.playlist); match self.target { PositionOrRange::Position(p) => command.argument(p), PositionOrRange::Range(r) => command.argument(r), } } fn response(self, _: Frame) -> Result { Ok(()) } } /// `playlistmove` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct MoveInPlaylist<'a> { playlist: &'a str, from: usize, to: usize, } impl<'a> MoveInPlaylist<'a> { /// Move the song at `from` to `to` in the playlist named `playlist`. pub fn new(playlist: &'a str, from: usize, to: usize) -> Self { Self { playlist, from, to } } } impl<'a> Command for MoveInPlaylist<'a> { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("playlistmove") .argument(self.playlist) .argument(self.from) .argument(self.to) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `listallinfo` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ListAllIn<'a> { directory: &'a str, } impl<'a> ListAllIn<'a> { /// List all songs in the library. pub fn root() -> ListAllIn<'static> { ListAllIn { directory: "" } } /// List all songs beneath the given directory. pub fn directory(directory: &'a str) -> Self { Self { directory } } } impl<'a> Command for ListAllIn<'a> { type Response = Vec; fn command(&self) -> RawCommand { let mut command = RawCommand::new("listallinfo"); if !self.directory.is_empty() { command.add_argument(self.directory).unwrap(); } command } fn response(self, frame: Frame) -> Result { res::Song::from_frame_multi(frame) } } /// Set the response binary length limit, in bytes. /// /// This can dramatically speed up operations like [loading album art][crate::Client::album_art], /// but may cause undesirable latency when using MPD over a slow connection. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SetBinaryLimit(pub usize); impl Command for SetBinaryLimit { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("binarylimit").argument(self.0) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `albumart` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AlbumArt<'a> { uri: &'a str, offset: usize, } impl<'a> AlbumArt<'a> { /// Get the separate file album art for the given URI. pub fn new(uri: &'a str) -> Self { Self { uri, offset: 0 } } /// Load the resulting data starting from the given offset. pub fn offset(self, offset: usize) -> Self { Self { offset, ..self } } } impl<'a> Command for AlbumArt<'a> { type Response = Option; fn command(&self) -> RawCommand { RawCommand::new("albumart") .argument(self.uri) .argument(self.offset) } fn response(self, frame: Frame) -> Result { res::AlbumArt::from_frame(frame) } } /// `readpicture` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct AlbumArtEmbedded<'a> { uri: &'a str, offset: usize, } impl<'a> AlbumArtEmbedded<'a> { /// Get the separate file album art for the given URI. pub fn new(uri: &'a str) -> Self { Self { uri, offset: 0 } } /// Load the resulting data starting from the given offset. pub fn offset(self, offset: usize) -> Self { Self { offset, ..self } } } impl<'a> Command for AlbumArtEmbedded<'a> { type Response = Option; fn command(&self) -> RawCommand { RawCommand::new("readpicture") .argument(self.uri) .argument(self.offset) } fn response(self, frame: Frame) -> Result { res::AlbumArt::from_frame(frame) } } /// Manage enabled tag types. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TagTypes<'a>(TagTypesAction<'a>); impl<'a> TagTypes<'a> { /// Enable all tags. pub fn enable_all() -> TagTypes<'static> { TagTypes(TagTypesAction::EnableAll) } /// Disable all tags. pub fn disable_all() -> TagTypes<'static> { TagTypes(TagTypesAction::Clear) } /// Disable the given list of tags. /// /// # Panics /// /// Panics if called with an empty list of tags. pub fn disable(tags: &'a [Tag]) -> TagTypes<'a> { assert_ne!(tags.len(), 0, "The list of tags must not be empty"); TagTypes(TagTypesAction::Disable(tags)) } /// Enable the given list of tags. /// /// # Panics /// /// Panics if called with an empty list of tags. pub fn enable(tags: &'a [Tag]) -> TagTypes<'a> { assert_ne!(tags.len(), 0, "The list of tags must not be empty"); TagTypes(TagTypesAction::Enable(tags)) } } impl<'a> Command for TagTypes<'a> { type Response = (); fn command(&self) -> RawCommand { let mut cmd = RawCommand::new("tagtypes"); match &self.0 { TagTypesAction::EnableAll => cmd.add_argument("all").unwrap(), TagTypesAction::Clear => cmd.add_argument("clear").unwrap(), TagTypesAction::Disable(tags) => { cmd.add_argument("disable").unwrap(); for tag in tags.iter() { cmd.add_argument(tag).unwrap(); } } TagTypesAction::Enable(tags) => { cmd.add_argument("enable").unwrap(); for tag in tags.iter() { cmd.add_argument(tag).unwrap(); } } } cmd } fn response(self, _: Frame) -> Result { Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] enum TagTypesAction<'a> { EnableAll, Clear, Disable(&'a [Tag]), Enable(&'a [Tag]), } /// `sticker get` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct StickerGet<'a> { uri: &'a str, name: &'a str, } impl<'a> StickerGet<'a> { /// Get the sticker `name` for the song at `uri` pub fn new(uri: &'a str, name: &'a str) -> Self { Self { uri, name } } } impl<'a> Command for StickerGet<'a> { type Response = res::StickerGet; fn command(&self) -> RawCommand { RawCommand::new("sticker") .argument("get") .argument("song") .argument(self.uri) .argument(self.name) } fn response(self, frame: Frame) -> Result { res::StickerGet::from_frame(frame) } } /// `sticker set` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct StickerSet<'a> { uri: &'a str, name: &'a str, value: &'a str, } impl<'a> StickerSet<'a> { /// Set the sticker `name` to `value` for the song at `uri` pub fn new(uri: &'a str, name: &'a str, value: &'a str) -> Self { Self { uri, name, value } } } impl<'a> Command for StickerSet<'a> { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("sticker") .argument("set") .argument("song") .argument(self.uri) .argument(self.name) .argument(self.value) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `sticker delete` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct StickerDelete<'a> { uri: &'a str, name: &'a str, } impl<'a> StickerDelete<'a> { /// Delete the sticker `name` for the song at `uri` pub fn new(uri: &'a str, name: &'a str) -> Self { Self { uri, name } } } impl<'a> Command for StickerDelete<'a> { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("sticker") .argument("delete") .argument("song") .argument(self.uri) .argument(self.name) } fn response(self, _: Frame) -> Result { Ok(()) } } /// `sticker list` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct StickerList<'a> { uri: &'a str, } impl<'a> StickerList<'a> { /// Lists all stickers on the song at `uri` pub fn new(uri: &'a str) -> Self { Self { uri } } } impl<'a> Command for StickerList<'a> { type Response = res::StickerList; fn command(&self) -> RawCommand { RawCommand::new("sticker") .argument("list") .argument("song") .argument(self.uri) } fn response(self, frame: Frame) -> Result { res::StickerList::from_frame(frame) } } /// Operator for full (filtered) version /// of `sticker find` command #[derive(Clone, Debug, PartialEq, Eq)] enum StickerFindOperator { /// = operator Equals, /// < operator LessThan, /// > operator GreaterThan, } /// `sticker find` command #[derive(Clone, Debug, PartialEq, Eq)] pub struct StickerFind<'a> { uri: &'a str, name: &'a str, filter: Option<(StickerFindOperator, &'a str)>, } impl<'a> StickerFind<'a> { /// Lists all stickers on the song at `uri` pub fn new(uri: &'a str, name: &'a str) -> Self { Self { uri, name, filter: None, } } /// Find stickers where their value is equal to `value` pub fn where_eq(self, value: &'a str) -> Self { self.add_filter(StickerFindOperator::Equals, value) } /// Find stickers where their value is greater than `value` pub fn where_gt(self, value: &'a str) -> Self { self.add_filter(StickerFindOperator::GreaterThan, value) } /// Find stickers where their value is less than `value` pub fn where_lt(self, value: &'a str) -> Self { self.add_filter(StickerFindOperator::LessThan, value) } fn add_filter(self, operator: StickerFindOperator, value: &'a str) -> Self { Self { name: self.name, uri: self.uri, filter: Some((operator, value)), } } } impl<'a> Command for StickerFind<'a> { type Response = res::StickerFind; fn command(&self) -> RawCommand { let base = RawCommand::new("sticker") .argument("find") .argument("song") .argument(self.uri) .argument(self.name); if let Some((operator, value)) = self.filter.as_ref() { match operator { StickerFindOperator::Equals => base.argument("=").argument(value), StickerFindOperator::GreaterThan => base.argument(">").argument(value), StickerFindOperator::LessThan => base.argument("<").argument(value), } } else { base } } fn response(self, frame: Frame) -> Result { res::StickerFind::from_frame(frame) } } /// `update` command. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Update<'a>(Option<&'a str>); impl<'a> Update<'a> { /// Update the entire music database. pub fn new() -> Self { Update(None) } /// Restrict the update to the files below the given path. pub fn uri(self, uri: &'a str) -> Self { Self(Some(uri)) } } impl<'a> Command for Update<'a> { type Response = u64; fn command(&self) -> RawCommand { let mut command = RawCommand::new("update"); if let Some(uri) = self.0 { command.add_argument(uri).unwrap(); } command } fn response(self, mut frame: Frame) -> Result { value(&mut frame, "updating_db") } } impl<'a> Default for Update<'a> { fn default() -> Self { Update::new() } } /// `rescan` command. /// /// Unlike the [`Update`] command, this will also scan files that don't appear to have changed /// based on their modification time. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Rescan<'a>(Option<&'a str>); impl<'a> Rescan<'a> { /// Rescan the entire music database. pub fn new() -> Self { Rescan(None) } /// Restrict the rescan to the files below the given path. pub fn uri(self, uri: &'a str) -> Self { Self(Some(uri)) } } impl<'a> Command for Rescan<'a> { type Response = u64; fn command(&self) -> RawCommand { let mut command = RawCommand::new("rescan"); if let Some(uri) = self.0 { command.add_argument(uri).unwrap(); } command } fn response(self, mut frame: Frame) -> Result { value(&mut frame, "updating_db") } } impl<'a> Default for Rescan<'a> { fn default() -> Self { Rescan::new() } } /// `readmessage` command. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ReadChannelMessages; impl Command for ReadChannelMessages { type Response = Vec<(String, String)>; fn command(&self) -> RawCommand { RawCommand::new("readmessages") } fn response(self, frame: Frame) -> Result { res::parse_channel_messages(frame) } } /// `channels` command. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ListChannels; impl Command for ListChannels { type Response = Vec; fn command(&self) -> RawCommand { RawCommand::new("channels") } fn response(self, frame: Frame) -> Result { let mut response = Vec::with_capacity(frame.fields_len()); for (key, value) in frame { if &*key != "channel" { return Err(TypedResponseError::unexpected_field("channel", &*key)); } response.push(value); } Ok(response) } } /// `sendmessage` command. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SendChannelMessage<'a> { channel: &'a str, message: &'a str, } impl<'a> SendChannelMessage<'a> { /// Send the given message to the given channel. pub fn new(channel: &'a str, message: &'a str) -> SendChannelMessage<'a> { SendChannelMessage { channel, message } } } impl<'a> Command for SendChannelMessage<'a> { type Response = (); fn command(&self) -> RawCommand { RawCommand::new("sendmessage") .argument(self.channel) .argument(self.message) } fn response(self, _frame: Frame) -> Result { Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn range_arg() { let mut buf = BytesMut::new(); SongRange::new_usize(2..4).render(&mut buf); assert_eq!(buf, "2:4"); buf.clear(); SongRange::new_usize(3..).render(&mut buf); assert_eq!(buf, "3:"); buf.clear(); SongRange::new_usize(2..=5).render(&mut buf); assert_eq!(buf, "2:6"); buf.clear(); SongRange::new_usize(..5).render(&mut buf); assert_eq!(buf, "0:5"); buf.clear(); SongRange::new_usize(..).render(&mut buf); assert_eq!(buf, "0:"); buf.clear(); SongRange::new_usize(1..=1).render(&mut buf); assert_eq!(buf, "1:2"); buf.clear(); SongRange::new_usize(..usize::MAX).render(&mut buf); assert_eq!(buf, format!("0:{}", usize::MAX)); buf.clear(); SongRange::new_usize(..=usize::MAX).render(&mut buf); assert_eq!(buf, format!("0:{}", usize::MAX)); buf.clear(); } #[test] fn command_queue() { assert_eq!(Queue.command(), RawCommand::new("playlistinfo")); assert_eq!( Queue::song(Song::Position(SongPosition(1))).command(), RawCommand::new("playlistinfo").argument("1") ); assert_eq!( Queue::song(Song::Id(SongId(7))).command(), RawCommand::new("playlistid").argument("7") ); assert_eq!( Queue::range(SongPosition(3)..SongPosition(18)).command(), RawCommand::new("playlistinfo").argument("3:18") ); } #[test] fn command_crossfade() { assert_eq!( Crossfade(Duration::from_secs_f64(2.345)).command(), RawCommand::new("crossfade").argument("2") ); } #[test] fn command_getplaylist() { assert_eq!( GetPlaylist("foo").command(), RawCommand::new("listplaylistinfo").argument("foo") ); } #[test] fn command_volume() { assert_eq!( SetVolume(150).command(), RawCommand::new("setvol").argument("100") ); } #[test] fn command_seek_to() { let duration = Duration::from_secs(2); assert_eq!( SeekTo(SongId(2).into(), duration).command(), RawCommand::new("seekid") .argument(SongId(2)) .argument(duration) ); assert_eq!( SeekTo(SongPosition(2).into(), duration).command(), RawCommand::new("seek") .argument(SongPosition(2)) .argument(duration) ); } #[test] fn command_seek() { let duration = Duration::from_secs(1); assert_eq!( Seek(SeekMode::Absolute(duration)).command(), RawCommand::new("seekcur").argument("1.000") ); assert_eq!( Seek(SeekMode::Forward(duration)).command(), RawCommand::new("seekcur").argument("+1.000") ); assert_eq!( Seek(SeekMode::Backward(duration)).command(), RawCommand::new("seekcur").argument("-1.000") ); } #[test] fn command_shuffle() { assert_eq!(Shuffle::all().command(), RawCommand::new("shuffle")); assert_eq!( Shuffle::range(SongPosition(0)..SongPosition(2)).command(), RawCommand::new("shuffle").argument("0:2") ); } #[test] fn command_play() { assert_eq!(Play::current().command(), RawCommand::new("play")); assert_eq!( Play::song(SongPosition(2)).command(), RawCommand::new("play").argument(SongPosition(2)) ); assert_eq!( Play::song(SongId(2)).command(), RawCommand::new("playid").argument(SongId(2)) ); } #[test] fn command_add() { let uri = "foo/bar.mp3"; assert_eq!( Add::uri(uri).command(), RawCommand::new("addid").argument(uri) ); assert_eq!( Add::uri(uri).at(5).command(), RawCommand::new("addid").argument(uri).argument("5") ); assert_eq!( Add::uri(uri).before_current(5).command(), RawCommand::new("addid").argument(uri).argument("-5") ); assert_eq!( Add::uri(uri).after_current(5).command(), RawCommand::new("addid").argument(uri).argument("+5") ); } #[test] fn command_delete() { assert_eq!( Delete::id(SongId(2)).command(), RawCommand::new("deleteid").argument(SongId(2)) ); assert_eq!( Delete::position(SongPosition(2)).command(), RawCommand::new("delete").argument("2:3") ); assert_eq!( Delete::range(SongPosition(2)..SongPosition(4)).command(), RawCommand::new("delete").argument("2:4") ); } #[test] fn command_move() { assert_eq!( Move::position(SongPosition(2)) .to_position(SongPosition(4)) .command(), RawCommand::new("move").argument("2:3").argument("4") ); assert_eq!( Move::id(SongId(2)).to_position(SongPosition(4)).command(), RawCommand::new("moveid") .argument(SongId(2)) .argument(SongPosition(4)) ); assert_eq!( Move::range(SongPosition(3)..SongPosition(5)) .to_position(SongPosition(4)) .command(), RawCommand::new("move") .argument("3:5") .argument(SongPosition(4)) ); assert_eq!( Move::position(SongPosition(2)).after_current(3).command(), RawCommand::new("move").argument("2:3").argument("+3") ); assert_eq!( Move::position(SongPosition(2)).before_current(3).command(), RawCommand::new("move").argument("2:3").argument("-3") ); } #[test] fn command_find() { let filter = Filter::tag(Tag::Artist, "Foo"); assert_eq!( Find::new(filter.clone()).command(), RawCommand::new("find").argument(filter.clone()) ); assert_eq!( Find::new(filter.clone()).window(..3).command(), RawCommand::new("find") .argument(filter.clone()) .argument("window") .argument("0:3"), ); assert_eq!( Find::new(filter.clone()) .window(3..) .sort(Tag::Artist) .command(), RawCommand::new("find") .argument(filter) .argument("sort") .argument("Artist") .argument("window") .argument("3:") ); } #[test] fn command_list() { assert_eq!( List::new(Tag::Album).command(), RawCommand::new("list").argument("Album") ); let filter = Filter::tag(Tag::Artist, "Foo"); assert_eq!( List::new(Tag::Album).filter(filter.clone()).command(), RawCommand::new("list").argument("Album").argument(filter) ); let filter = Filter::tag(Tag::Artist, "Foo"); assert_eq!( List::new(Tag::Title) .filter(filter.clone()) .group_by([Tag::AlbumArtist, Tag::Album]) .command(), RawCommand::new("list") .argument("Title") .argument(filter) .argument("group") .argument("AlbumArtist") .argument("group") .argument("Album") ); } #[test] fn command_listallinfo() { assert_eq!(ListAllIn::root().command(), RawCommand::new("listallinfo")); assert_eq!( ListAllIn::directory("foo").command(), RawCommand::new("listallinfo").argument("foo") ); } #[test] fn command_playlistdelete() { assert_eq!( RemoveFromPlaylist::position("foo", 5).command(), RawCommand::new("playlistdelete") .argument("foo") .argument("5"), ); assert_eq!( RemoveFromPlaylist::range("foo", SongPosition(3)..SongPosition(6)).command(), RawCommand::new("playlistdelete") .argument("foo") .argument("3:6"), ); } #[test] fn command_tagtypes() { assert_eq!( TagTypes::enable_all().command(), RawCommand::new("tagtypes").argument("all"), ); assert_eq!( TagTypes::disable_all().command(), RawCommand::new("tagtypes").argument("clear"), ); assert_eq!( TagTypes::disable(&[Tag::Album, Tag::Title]).command(), RawCommand::new("tagtypes") .argument("disable") .argument("Album") .argument("Title") ); assert_eq!( TagTypes::enable(&[Tag::Album, Tag::Title]).command(), RawCommand::new("tagtypes") .argument("enable") .argument("Album") .argument("Title") ); } #[test] fn command_get_enabled_tagtypes() { assert_eq!(GetEnabledTagTypes.command(), RawCommand::new("tagtypes")); } #[test] fn command_sticker_get() { assert_eq!( StickerGet::new("foo", "bar").command(), RawCommand::new("sticker") .argument("get") .argument("song") .argument("foo") .argument("bar") ); } #[test] fn command_sticker_set() { assert_eq!( StickerSet::new("foo", "bar", "baz").command(), RawCommand::new("sticker") .argument("set") .argument("song") .argument("foo") .argument("bar") .argument("baz") ); } #[test] fn command_sticker_delete() { assert_eq!( StickerDelete::new("foo", "bar").command(), RawCommand::new("sticker") .argument("delete") .argument("song") .argument("foo") .argument("bar") ); } #[test] fn command_sticker_list() { assert_eq!( StickerList::new("foo").command(), RawCommand::new("sticker") .argument("list") .argument("song") .argument("foo") ); } #[test] fn command_sticker_find() { assert_eq!( StickerFind::new("foo", "bar").command(), RawCommand::new("sticker") .argument("find") .argument("song") .argument("foo") .argument("bar") ); assert_eq!( StickerFind::new("foo", "bar").where_eq("baz").command(), RawCommand::new("sticker") .argument("find") .argument("song") .argument("foo") .argument("bar") .argument("=") .argument("baz") ); } #[test] fn command_update() { assert_eq!(Update::new().command(), RawCommand::new("update")); assert_eq!( Update::new().uri("folder").command(), RawCommand::new("update").argument("folder") ) } #[test] fn command_rescan() { assert_eq!(Rescan::new().command(), RawCommand::new("rescan")); assert_eq!( Rescan::new().uri("folder").command(), RawCommand::new("rescan").argument("folder") ) } #[test] fn command_subscribe() { assert_eq!( SubscribeToChannel("hello").command(), RawCommand::new("subscribe").argument("hello") ); } #[test] fn command_unsubscribe() { assert_eq!( UnsubscribeFromChannel("hello").command(), RawCommand::new("unsubscribe").argument("hello") ); } #[test] fn command_read_messages() { assert_eq!( ReadChannelMessages.command(), RawCommand::new("readmessages") ); } #[test] fn command_list_channels() { assert_eq!(ListChannels.command(), RawCommand::new("channels")); } #[test] fn command_send_message() { assert_eq!( SendChannelMessage::new("foo", "bar").command(), RawCommand::new("sendmessage") .argument("foo") .argument("bar") ); } } mpd_client-1.4.1/src/commands/mod.rs000064400000000000000000000062441046102023000154700ustar 00000000000000//! Strongly typed, pre-built commands. //! //! This module contains pre-made definitions of commands and responses, so you don't have to //! wrangle the stringly-typed raw responses if you don't want to. //! //! The fields on the contained structs are mostly undocumented, see the [MPD protocol //! documentation][mpd-docs] for details on their specific meaning. //! //! [mpd-docs]: https://www.musicpd.org/doc/html/protocol.html#command-reference pub mod definitions; mod command_list; use std::{fmt::Write, time::Duration}; use bytes::BytesMut; use mpd_protocol::{ command::{Argument, Command as RawCommand}, response::Frame, }; pub use self::{command_list::CommandList, definitions::*}; use crate::responses::TypedResponseError; /// Stable identifier of a song in the queue. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SongId(pub u64); impl From for SongId { fn from(id: u64) -> Self { Self(id) } } impl Argument for SongId { fn render(&self, buf: &mut BytesMut) { write!(buf, "{}", self.0).unwrap(); } } /// Position of a song in the queue. /// /// This will change when the queue is modified. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SongPosition(pub usize); impl From for SongPosition { fn from(pos: usize) -> Self { Self(pos) } } impl Argument for SongPosition { fn render(&self, buf: &mut BytesMut) { write!(buf, "{}", self.0).unwrap(); } } /// Possible ways to seek in the current song. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SeekMode { /// Forwards from current position. Forward(Duration), /// Backwards from current position. Backward(Duration), /// To the absolute position in the current song. Absolute(Duration), } /// Possible `single` modes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(missing_docs)] pub enum SingleMode { Enabled, Disabled, Oneshot, } /// Possible `replay_gain_mode` modes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(missing_docs)] pub enum ReplayGainMode { /// Replay Gain off Off, /// Replay Gain Track mode Track, /// Replay Gain Album mode Album, /// Replay Gain Track if shuffle is on, Album otherwise Auto, } /// Modes to target a song with a command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Song { /// By ID Id(SongId), /// By position in the queue. Position(SongPosition), } impl From for Song { fn from(id: SongId) -> Self { Self::Id(id) } } impl From for Song { fn from(pos: SongPosition) -> Self { Self::Position(pos) } } /// Types which can be used as pre-built properly typed commands. pub trait Command { /// The response this command will return. type Response; /// Create the raw command representation for transmission. fn command(&self) -> RawCommand; /// Convert the raw response frame to the proper response type. /// /// # Errors /// /// This should return an error if the response was invalid. fn response(self, frame: Frame) -> Result; } mpd_client-1.4.1/src/filter.rs000064400000000000000000000170231046102023000143720ustar 00000000000000//! Tools for constructing [filter expressions], as used by e.g. the [`find`] command. //! //! [`find`]: crate::commands::definitions::Find //! [filter expressions]: https://www.musicpd.org/doc/html/protocol.html#filters use std::{borrow::Cow, fmt::Write, ops::Not}; use bytes::{BufMut, BytesMut}; use mpd_protocol::command::Argument; use crate::tag::Tag; const TAG_IS_ABSENT: &str = ""; /// A [filter expression]. /// /// [filter expression]: https://www.musicpd.org/doc/html/protocol.html#filters #[derive(Clone, Debug, PartialEq, Eq)] pub struct Filter(FilterType); /// Internal filter variant type #[derive(Clone, Debug, PartialEq, Eq)] enum FilterType { Tag { tag: Tag, operator: Operator, value: String, }, Not(Box), And(Vec), } impl Filter { /// Create a filter which selects on the given `tag`, using the given `operator`, for the /// given `value`. /// /// See also [`Tag::any()`]. pub fn new>(tag: Tag, operator: Operator, value: V) -> Self { Self(FilterType::Tag { tag, operator, value: value.into(), }) } /// Create a filter which checks where the given `tag` is equal to the given `value`. /// /// Shorthand method that always checks for equality. pub fn tag>(tag: Tag, value: V) -> Self { Filter::new(tag, Operator::Equal, value) } /// Create a filter which checks for the existence of `tag` (with any value). pub fn tag_exists(tag: Tag) -> Self { Filter::new(tag, Operator::NotEqual, String::from(TAG_IS_ABSENT)) } /// Create a filter which checks for the absence of `tag`. pub fn tag_absent(tag: Tag) -> Self { Filter::new(tag, Operator::Equal, String::from(TAG_IS_ABSENT)) } /// Negate the filter. /// /// You can also use the negation operator (`!`) if you prefer to negate at the start of an /// expression. pub fn negate(mut self) -> Self { self.0 = FilterType::Not(Box::new(self.0)); self } /// Chain the given filter onto this one with an `AND`. /// /// Automatically flattens nested `AND` conditions. pub fn and(self, other: Self) -> Self { let mut out = match self.0 { FilterType::And(inner) => inner, condition => { let mut out = Vec::with_capacity(2); out.push(condition); out } }; match other.0 { FilterType::And(inner) => { for c in inner { out.push(c); } } condition => out.push(condition), } Self(FilterType::And(out)) } fn render(&self, buf: &mut BytesMut) { buf.put_u8(b'"'); self.0.render(buf); buf.put_u8(b'"'); } } impl Argument for Filter { fn render(&self, buf: &mut BytesMut) { self.render(buf); } } impl Not for Filter { type Output = Self; fn not(self) -> Self::Output { self.negate() } } impl FilterType { fn render(&self, buf: &mut BytesMut) { match self { FilterType::Tag { tag, operator, value, } => { write!( buf, r#"({} {} \"{}\")"#, tag.as_str(), operator.as_str(), escape_filter_value(value) ) .unwrap(); } FilterType::Not(inner) => { buf.put_slice(b"(!"); inner.render(buf); buf.put_u8(b')'); } FilterType::And(inner) => { assert!(inner.len() >= 2); buf.put_u8(b'('); let mut first = true; for filter in inner { if first { first = false; } else { buf.put_slice(b" AND "); } filter.render(buf); } buf.put_u8(b')'); } } /* match self { FilterType::And(inner) => { assert!(inner.len() >= 2); let inner = inner.iter().map(|s| s.render()).collect::>(); // Wrapping parens let mut capacity = 2; // Lengths of the actual commands capacity += inner.iter().map(|s| s.len()).sum::(); // " AND " join operators capacity += (inner.len() - 1) * 5; let mut out = String::with_capacity(capacity); out.push('('); let mut first = true; for filter in inner { if first { first = false; } else { out.push_str(" AND "); } out.push_str(&filter); } out.push(')'); out } } */ } } /// Operators which can be used in filter expressions. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Operator { /// Equality (`==`) Equal, /// Negated equality (`!=`) NotEqual, /// Substring matching (`contains`) Contain, /// Perl-style regex matching (`=~`) Match, /// Negated Perl-style regex matching (`!~`) NotMatch, } impl Operator { fn as_str(self) -> &'static str { match self { Operator::Equal => "==", Operator::NotEqual => "!=", Operator::Contain => "contains", Operator::Match => "=~", Operator::NotMatch => "!~", } } } fn escape_filter_value(value: &str) -> Cow<'_, str> { if value.contains('"') { Cow::Owned(value.replace('"', r#"\\""#)) } else { Cow::Borrowed(value) } } #[cfg(test)] mod tests { use super::*; #[test] fn filter_escaping() { let mut buf = BytesMut::new(); Filter::tag(Tag::Artist, "foo").render(&mut buf); assert_eq!(buf, r#""(Artist == \"foo\")""#); buf.clear(); Filter::tag(Tag::Artist, "foo\'s bar\"").render(&mut buf); assert_eq!(buf, r#""(Artist == \"foo's bar\\"\")""#); buf.clear(); } #[test] fn filter_other_operator() { let mut buf = BytesMut::new(); Filter::new(Tag::Artist, Operator::Contain, "mep mep").render(&mut buf); assert_eq!(buf, r#""(Artist contains \"mep mep\")""#); } #[test] fn filter_not() { let mut buf = BytesMut::new(); Filter::tag(Tag::Artist, "hello").negate().render(&mut buf); assert_eq!(buf, r#""(!(Artist == \"hello\"))""#); } #[test] fn filter_and() { let mut buf = BytesMut::new(); let first = Filter::tag(Tag::Artist, "hello"); let second = Filter::tag(Tag::Album, "world"); first.and(second).render(&mut buf); assert_eq!(buf, r#""((Artist == \"hello\") AND (Album == \"world\"))""#); } #[test] fn filter_and_multiple() { let mut buf = BytesMut::new(); let first = Filter::tag(Tag::Artist, "hello"); let second = Filter::tag(Tag::Album, "world"); let third = Filter::tag(Tag::Title, "foo"); first.and(second).and(third).render(&mut buf); assert_eq!( buf, r#""((Artist == \"hello\") AND (Album == \"world\") AND (Title == \"foo\"))""# ); } } mpd_client-1.4.1/src/lib.rs000064400000000000000000000013651046102023000136550ustar 00000000000000#![warn( missing_debug_implementations, missing_docs, rust_2018_idioms, unreachable_pub, unused_import_braces, unused_qualifications )] #![deny(rustdoc::broken_intra_doc_links)] #![forbid(unsafe_code)] //! Asynchronous client for [MPD](https://musicpd.org). //! //! The [`Client`] type is the primary API. //! //! # Crate Features //! //! | Feature | Description | //! |----------|-----------------------------------| //! | `chrono` | Support for parsing [`Timestamp`] | //! //! [`Timestamp`]: responses::Timestamp #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod client; pub mod commands; pub mod filter; pub mod responses; pub mod tag; pub use mpd_protocol as protocol; pub use self::client::Client; mpd_client-1.4.1/src/responses/count.rs000064400000000000000000000110121046102023000162460ustar 00000000000000use std::time::Duration; use mpd_protocol::response::Frame; use crate::{ responses::{value, FromFieldValue, TypedResponseError}, tag::Tag, }; /// Response to the [`Count`][crate::commands::Count] command. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct Count { /// Number of songs pub songs: u64, /// Total playtime of the songs pub playtime: Duration, } impl Count { pub(crate) fn from_frame(mut frame: Frame) -> Result { Ok(Count { songs: value(&mut frame, "songs")?, playtime: value(&mut frame, "playtime")?, }) } pub(crate) fn from_frame_grouped( frame: Frame, group_by: &Tag, ) -> Result, TypedResponseError> { let mut out = Vec::with_capacity(frame.fields_len() / 3); build_grouped_values(&mut out, group_by, frame)?; Ok(out) } } fn build_grouped_values( out: &mut Vec<(String, Count)>, grouping_tag: &Tag, fields: I, ) -> Result<(), TypedResponseError> where I: IntoIterator, V: AsRef, { let mut fields = fields.into_iter(); while let Some((key, value)) = fields.next() { let mut songs: Option = None; let mut playtime: Option = None; if key.as_ref() != grouping_tag.as_str() { return Err(TypedResponseError::unexpected_field( grouping_tag.as_str(), key.as_ref(), )); } while songs.is_none() || playtime.is_none() { if let Some((key, value)) = fields.next() { match key.as_ref() { "songs" => { if songs.is_none() { songs = Some(u64::from_value(value, "songs")?); } else { return Err(TypedResponseError::unexpected_field("playtime", "songs")); } } "playtime" => { if playtime.is_none() { playtime = Some(Duration::from_value(value, "playtime")?); } else { return Err(TypedResponseError::unexpected_field("songs", "playtime")); } } other => { return Err(TypedResponseError::unexpected_field( if songs.is_some() { "playtime" } else { "songs" }, other, )); } } } else { return Err(TypedResponseError::missing(if songs.is_some() { "playtime" } else { "songs" })); } } out.push(( value, Count { songs: songs.unwrap(), playtime: playtime.unwrap(), }, )); } Ok(()) } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; #[test] fn grouped_values_parsing() { let mut out = Vec::new(); build_grouped_values::<_, &str>(&mut out, &Tag::Album, vec![]).unwrap(); assert_eq!(out, &[]); build_grouped_values( &mut out, &Tag::Album, vec![ ("Album", String::from("hello")), ("songs", String::from("1234")), ("playtime", String::from("1234")), ("Album", String::from("world")), ("playtime", String::from("1")), ("songs", String::from("1")), ], ) .unwrap(); assert_eq!( out, &[ ( String::from("hello"), Count { songs: 1234, playtime: Duration::from_secs(1234) } ), ( String::from("world"), Count { songs: 1, playtime: Duration::from_secs(1) } ) ] ); out.clear(); let res = build_grouped_values( &mut out, &Tag::Album, vec![ ("Album", String::from("hello")), ("songs", String::from("1234")), ], ); assert_matches!(res, Err(_)); } } mpd_client-1.4.1/src/responses/list.rs000064400000000000000000000137111046102023000161010ustar 00000000000000use std::{slice::Iter, vec::IntoIter}; use mpd_protocol::response::Frame; use crate::tag::Tag; /// Response to the [`list`] command. /// /// [`list`]: crate::commands::definitions::List #[derive(Clone, Debug, PartialEq, Eq)] pub struct List { primary_tag: Tag, groupings: [Tag; N], fields: Vec<(Tag, String)>, } impl List<0> { /// Returns an iterator over all distinct values returned. pub fn values(&self) -> ListValuesIter<'_> { ListValuesIter(self.fields.iter()) } } impl List { pub(crate) fn from_frame(primary_tag: Tag, groupings: [Tag; N], frame: Frame) -> List { let fields = frame .into_iter() // Unwrapping here is fine because the parser already validated the fields .map(|(tag, value)| (Tag::try_from(tag.as_ref()).unwrap(), value)) .collect(); List { primary_tag, groupings, fields, } } /// Returns an iterator over the grouped combinations returned by the command. /// /// The grouped values are returned in the same order they were passed to [`group_by`]. /// /// [`group_by`]: crate::commands::definitions::List::group_by pub fn grouped_values(&self) -> GroupedListValuesIter<'_, N> { GroupedListValuesIter { primary_tag: &self.primary_tag, grouping_tags: &self.groupings, grouping_values: [""; N], fields: self.fields.iter(), } } /// Returns the tags the response was grouped by. pub fn grouped_by(&self) -> &[Tag; N] { &self.groupings } /// Get the raw fields as they were returned by the server. pub fn into_raw_values(self) -> Vec<(Tag, String)> { self.fields } } impl<'a> IntoIterator for &'a List<0> { type Item = &'a str; type IntoIter = ListValuesIter<'a>; fn into_iter(self) -> Self::IntoIter { self.values() } } impl IntoIterator for List<0> { type Item = String; type IntoIter = ListValuesIntoIter; fn into_iter(self) -> Self::IntoIter { ListValuesIntoIter(self.fields.into_iter()) } } /// Iterator over references to grouped values. /// /// Returned by [`List::grouped_values`]. #[derive(Clone, Debug)] pub struct GroupedListValuesIter<'a, const N: usize> { primary_tag: &'a Tag, grouping_tags: &'a [Tag; N], grouping_values: [&'a str; N], fields: Iter<'a, (Tag, String)>, } impl<'a, const N: usize> Iterator for GroupedListValuesIter<'a, N> { type Item = (&'a str, [&'a str; N]); fn next(&mut self) -> Option { loop { let (tag, value) = self.fields.next()?; if tag == self.primary_tag { break Some((value, self.grouping_values)); } let idx = self.grouping_tags.iter().position(|t| t == tag).unwrap(); self.grouping_values[idx] = value; } } } /// Iterator over references to ungrouped [`List`] values. #[derive(Clone, Debug)] pub struct ListValuesIter<'a>(Iter<'a, (Tag, String)>); impl<'a> Iterator for ListValuesIter<'a> { type Item = &'a str; fn next(&mut self) -> Option { self.0.next().map(|(_, v)| &**v) } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } fn count(self) -> usize { self.0.count() } fn nth(&mut self, n: usize) -> Option { self.0.nth(n).map(|(_, v)| &**v) } fn last(self) -> Option where Self: Sized, { self.0.last().map(|(_, v)| &**v) } } impl<'a> DoubleEndedIterator for ListValuesIter<'a> { fn next_back(&mut self) -> Option { self.0.next_back().map(|(_, v)| &**v) } fn nth_back(&mut self, n: usize) -> Option { self.0.nth_back(n).map(|(_, v)| &**v) } } impl<'a> ExactSizeIterator for ListValuesIter<'a> {} /// Iterator over ungrouped [`List`] values. #[derive(Debug)] pub struct ListValuesIntoIter(IntoIter<(Tag, String)>); impl Iterator for ListValuesIntoIter { type Item = String; fn next(&mut self) -> Option { self.0.next().map(|(_, v)| v) } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } fn count(self) -> usize { self.0.count() } fn nth(&mut self, n: usize) -> Option { self.0.nth(n).map(|(_, v)| v) } fn last(self) -> Option where Self: Sized, { self.0.last().map(|(_, v)| v) } } impl DoubleEndedIterator for ListValuesIntoIter { fn next_back(&mut self) -> Option { self.0.next_back().map(|(_, v)| v) } fn nth_back(&mut self, n: usize) -> Option { self.0.nth_back(n).map(|(_, v)| v) } } impl ExactSizeIterator for ListValuesIntoIter {} #[cfg(test)] mod tests { use super::*; #[test] fn grouped_iterator() { let fields = vec![ (Tag::AlbumArtist, String::from("Foo")), (Tag::Album, String::from("Bar")), (Tag::Title, String::from("Title 1")), (Tag::Title, String::from("Title 2")), (Tag::Album, String::from("Quz")), (Tag::Title, String::from("Title 3")), (Tag::AlbumArtist, String::from("Asdf")), (Tag::Album, String::from("Qwert")), (Tag::Title, String::from("Title 4")), ]; let mut iter = GroupedListValuesIter { primary_tag: &Tag::Title, grouping_tags: &[Tag::Album, Tag::AlbumArtist], grouping_values: [""; 2], fields: fields.iter(), }; assert_eq!(iter.next(), Some(("Title 1", ["Bar", "Foo"]))); assert_eq!(iter.next(), Some(("Title 2", ["Bar", "Foo"]))); assert_eq!(iter.next(), Some(("Title 3", ["Quz", "Foo"]))); assert_eq!(iter.next(), Some(("Title 4", ["Qwert", "Asdf"]))); assert_eq!(iter.next(), None); } } mpd_client-1.4.1/src/responses/mod.rs000064400000000000000000000354051046102023000157110ustar 00000000000000//! Typed responses to individual commands. mod count; mod list; mod playlist; mod song; mod sticker; mod timestamp; use std::{error::Error, fmt, num::ParseIntError, str::FromStr, sync::Arc, time::Duration}; use bytes::BytesMut; use mpd_protocol::response::Frame; pub use self::{ count::Count, list::{GroupedListValuesIter, List, ListValuesIntoIter, ListValuesIter}, playlist::Playlist, song::{Song, SongInQueue, SongRange}, sticker::{StickerFind, StickerGet, StickerList}, timestamp::Timestamp, }; use crate::commands::{ReplayGainMode, SingleMode, SongId, SongPosition}; type KeyValuePair = (Arc, String); /// Error returned when failing to convert a raw [`Frame`] into the proper typed response. #[derive(Debug)] pub struct TypedResponseError { kind: ErrorKind, source: Option>, } impl TypedResponseError { /// Construct a "Missing field" error. pub fn missing(field: F) -> TypedResponseError where F: Into, { TypedResponseError { kind: ErrorKind::Missing { field: field.into(), }, source: None, } } /// Construct an "Unexpected field" error. pub fn unexpected_field(expected: E, found: F) -> TypedResponseError where E: Into, F: Into, { TypedResponseError { kind: ErrorKind::UnexpectedField { expected: expected.into(), found: found.into(), }, source: None, } } /// Construct an "Invalid value" error. pub fn invalid_value(field: F, value: String) -> TypedResponseError where F: Into, { TypedResponseError { kind: ErrorKind::InvalidValue { field: field.into(), value, }, source: None, } } /// Construct a nonspecific error. pub fn other() -> TypedResponseError { TypedResponseError { kind: ErrorKind::Other, source: None, } } /// Set a source error. /// /// This is most useful with [invalid value][TypedResponseError::invalid_value] and /// [unspecified][TypedResponseError::other] errors. pub fn source(self, source: E) -> TypedResponseError where E: Error + Send + Sync + 'static, { let source = Some(Box::from(source)); TypedResponseError { source, ..self } } } #[derive(Debug)] enum ErrorKind { Missing { field: String }, UnexpectedField { expected: String, found: String }, InvalidValue { field: String, value: String }, Other, } impl fmt::Display for TypedResponseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.kind { ErrorKind::Missing { field } => write!(f, "field {field:?} is required but missing"), ErrorKind::UnexpectedField { expected, found } => { write!(f, "expected field {expected:?} but found {found:?}") } ErrorKind::InvalidValue { field, value } => { write!(f, "invalid value {value:?} for field {field:?}") } ErrorKind::Other => write!(f, "invalid response"), } } } impl Error for TypedResponseError { fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_deref().map(|e| e as _) } } /// Types which can be converted from a field value. pub(crate) trait FromFieldValue: Sized { /// Convert the value. fn from_value(v: String, field: &str) -> Result; } impl FromFieldValue for bool { fn from_value(v: String, field: &str) -> Result { match &*v { "0" => Ok(false), "1" => Ok(true), _ => Err(TypedResponseError::invalid_value(field, v)), } } } impl FromFieldValue for Duration { fn from_value(v: String, field: &str) -> Result { parse_duration(field, v) } } impl FromFieldValue for PlayState { fn from_value(v: String, field: &str) -> Result { match &*v { "play" => Ok(PlayState::Playing), "pause" => Ok(PlayState::Paused), "stop" => Ok(PlayState::Stopped), _ => Err(TypedResponseError::invalid_value(field, v)), } } } impl FromFieldValue for ReplayGainMode { fn from_value(v: String, field: &str) -> Result { match &*v { "off" => Ok(ReplayGainMode::Off), "track" => Ok(ReplayGainMode::Track), "album" => Ok(ReplayGainMode::Album), "auto" => Ok(ReplayGainMode::Auto), _ => Err(TypedResponseError::invalid_value(field, v)), } } } fn parse_integer>( v: String, field: &str, ) -> Result { v.parse::() .map_err(|e| TypedResponseError::invalid_value(field, v).source(e)) } impl FromFieldValue for u8 { fn from_value(v: String, field: &str) -> Result { parse_integer(v, field) } } impl FromFieldValue for u32 { fn from_value(v: String, field: &str) -> Result { parse_integer(v, field) } } impl FromFieldValue for u64 { fn from_value(v: String, field: &str) -> Result { parse_integer(v, field) } } impl FromFieldValue for usize { fn from_value(v: String, field: &str) -> Result { parse_integer(v, field) } } /// Get a *required* value for the given field, as the given type. pub(crate) fn value( frame: &mut Frame, field: &'static str, ) -> Result { let value = frame .get(field) .ok_or_else(|| TypedResponseError::missing(field))?; V::from_value(value, field) } /// Get an *optional* value for the given field, as the given type. fn optional_value( frame: &mut Frame, field: &'static str, ) -> Result, TypedResponseError> { match frame.get(field) { None => Ok(None), Some(v) => { let v = V::from_value(v, field)?; Ok(Some(v)) } } } fn song_identifier( frame: &mut Frame, position_field: &'static str, id_field: &'static str, ) -> Result, TypedResponseError> { // The position field may or may not exist let position = match optional_value(frame, position_field)? { Some(p) => SongPosition(p), None => return Ok(None), }; // ... but if the position field existed, the ID field must exist too let id = value(frame, id_field).map(SongId)?; Ok(Some((position, id))) } fn parse_duration + Into>( field: &str, value: V, ) -> Result { let v = match value.as_ref().parse::() { Ok(v) => v, Err(e) => return Err(TypedResponseError::invalid_value(field, value.into()).source(e)), }; // Check if the parsed value is a reasonable duration, to avoid a panic from `from_secs_f64` if v >= 0.0 && v <= Duration::MAX.as_secs_f64() && v.is_finite() { Ok(Duration::from_secs_f64(v)) } else { Err(TypedResponseError::invalid_value(field, value.into())) } } /// Possible playback states. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(missing_docs)] pub enum PlayState { Stopped, Playing, Paused, } /// Response to the [`replay_gain_status`] command. /// /// See the [MPD documentation][replay-gain-status-command] for the specific meanings of the fields. /// /// [`replay_gain_status`]: crate::commands::definitions::ReplayGainStatus /// [replay-gain-status-command]: https://www.musicpd.org/doc/html/protocol.html#command-replay-gain-status #[derive(Clone, Debug, PartialEq, Eq)] #[allow(missing_docs)] #[non_exhaustive] pub struct ReplayGainStatus { pub mode: ReplayGainMode, } impl ReplayGainStatus { pub(crate) fn from_frame(mut raw: Frame) -> Result { let f = &mut raw; Ok(Self { mode: value(f, "replay_gain_mode")?, }) } } /// Response to the [`status`] command. /// /// See the [MPD documentation][status-command] for the specific meanings of the fields. /// /// [`status`]: crate::commands::definitions::Status /// [status-command]: https://www.musicpd.org/doc/html/protocol.html#command-status #[derive(Clone, Debug, PartialEq, Eq)] #[allow(missing_docs)] #[non_exhaustive] pub struct Status { pub volume: u8, pub state: PlayState, pub repeat: bool, pub random: bool, pub consume: bool, pub single: SingleMode, pub playlist_version: u32, pub playlist_length: usize, pub current_song: Option<(SongPosition, SongId)>, pub next_song: Option<(SongPosition, SongId)>, pub elapsed: Option, pub duration: Option, pub bitrate: Option, pub crossfade: Duration, pub update_job: Option, pub error: Option, pub partition: Option, } impl Status { pub(crate) fn from_frame(mut raw: Frame) -> Result { let single = match raw.get("single") { None => SingleMode::Disabled, Some(val) => match val.as_str() { "0" => SingleMode::Disabled, "1" => SingleMode::Enabled, "oneshot" => SingleMode::Oneshot, _ => return Err(TypedResponseError::invalid_value("single", val)), }, }; let duration = if let Some(val) = raw.get("duration") { Some(Duration::from_value(val, "duration")?) } else if let Some(time) = raw.get("Time") { // Backwards compatibility with protocol versions <0.20 if let Some((_, duration)) = time.split_once(':') { Some(Duration::from_value(duration.to_owned(), "Time")?) } else { // No separator return Err(TypedResponseError::invalid_value("Time", time)); } } else { None }; let f = &mut raw; Ok(Self { volume: optional_value(f, "volume")?.unwrap_or(0), state: value(f, "state")?, repeat: value(f, "repeat")?, random: value(f, "random")?, consume: value(f, "consume")?, single, playlist_length: optional_value(f, "playlistlength")?.unwrap_or(0), playlist_version: optional_value(f, "playlist")?.unwrap_or(0), current_song: song_identifier(f, "song", "songid")?, next_song: song_identifier(f, "nextsong", "nextsongid")?, elapsed: optional_value(f, "elapsed")?, duration, bitrate: optional_value(f, "bitrate")?, crossfade: optional_value(f, "xfade")?.unwrap_or(Duration::ZERO), update_job: optional_value(f, "update_job")?, error: f.get("error"), partition: f.get("partition"), }) } } /// Response to the [`stats`] command, containing general server statistics. /// /// [`stats`]: crate::commands::definitions::Stats #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(missing_docs)] #[non_exhaustive] pub struct Stats { pub artists: u64, pub albums: u64, pub songs: u64, pub uptime: Duration, pub playtime: Duration, pub db_playtime: Duration, /// Raw server UNIX timestamp of last database update. pub db_last_update: u64, } impl Stats { pub(crate) fn from_frame(mut f: Frame) -> Result { let f = &mut f; Ok(Self { artists: value(f, "artists")?, albums: value(f, "albums")?, songs: value(f, "songs")?, uptime: value(f, "uptime")?, playtime: value(f, "playtime")?, db_playtime: value(f, "db_playtime")?, db_last_update: value(f, "db_update")?, }) } } /// Response to the [`albumart`][crate::commands::AlbumArt] and /// [`readpicture`][crate::commands::AlbumArtEmbedded] commands. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct AlbumArt { /// The total size in bytes of the file. pub size: usize, /// The mime type, if known. pub mime: Option, /// The raw data. pub data: BytesMut, } impl AlbumArt { pub(crate) fn from_frame(mut frame: Frame) -> Result, TypedResponseError> { let Some(data) = frame.take_binary() else { return Ok(None); }; Ok(Some(AlbumArt { size: value(&mut frame, "size")?, mime: frame.get("type"), data, })) } } /// Parse response for the [`crate::commands::ReadChannelMessages`] command. pub(crate) fn parse_channel_messages( fields: F, ) -> Result, TypedResponseError> where F: IntoIterator, { let mut response = Vec::new(); let mut fields = fields.into_iter(); while let Some(channel) = fields.next() { if &*channel.0 != "channel" { return Err(TypedResponseError::unexpected_field("channel", &*channel.0)); } let Some(message) = fields.next() else { return Err(TypedResponseError::missing("message")); }; if &*message.0 != "message" { return Err(TypedResponseError::unexpected_field("message", &*message.0)); } response.push((channel.1, message.1)); } Ok(response) } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; #[test] fn duration_parsing() { assert_eq!( parse_duration("duration", "1.500").unwrap(), Duration::from_secs_f64(1.5) ); assert_eq!(parse_duration("Time", "3").unwrap(), Duration::from_secs(3)); // Error cases assert_matches!(parse_duration("duration", "asdf"), Err(_)); assert_matches!(parse_duration("duration", "-1"), Err(_)); assert_matches!(parse_duration("duration", "NaN"), Err(_)); assert_matches!(parse_duration("duration", "-1"), Err(_)); } #[test] fn channel_message_parsing() { assert_eq!(parse_channel_messages(Vec::new()).unwrap(), Vec::new()); let fields = vec![ (Arc::from("channel"), String::from("foo")), (Arc::from("message"), String::from("message 1")), (Arc::from("channel"), String::from("bar")), (Arc::from("message"), String::from("message 2")), ]; assert_eq!( parse_channel_messages(fields).unwrap(), vec![ (String::from("foo"), String::from("message 1")), (String::from("bar"), String::from("message 2")), ] ); } } mpd_client-1.4.1/src/responses/playlist.rs000064400000000000000000000030421046102023000167630ustar 00000000000000use mpd_protocol::response::Frame; use crate::responses::{FromFieldValue, Timestamp, TypedResponseError}; /// A stored playlist, as returned by [`listplaylists`]. /// /// [`listplaylists`]: crate::commands::definitions::GetPlaylists #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct Playlist { /// Name of the playlist. pub name: String, /// Server timestamp of last modification. pub last_modified: Timestamp, } impl Playlist { pub(crate) fn parse_frame(frame: Frame) -> Result, TypedResponseError> { let mut out = Vec::with_capacity(frame.fields_len() / 2); let mut current_name: Option = None; for (key, value) in frame { if let Some(name) = current_name.take() { if key.as_ref() == "Last-Modified" { let last_modified = Timestamp::from_value(value, "Last-Modified")?; out.push(Playlist { name, last_modified, }); } else { return Err(TypedResponseError::unexpected_field( "Last-Modified", key.as_ref(), )); } } else if key.as_ref() == "playlist" { current_name = Some(value); } else { return Err(TypedResponseError::unexpected_field( "playlist", key.as_ref(), )); } } Ok(out) } } mpd_client-1.4.1/src/responses/song.rs000064400000000000000000000344551046102023000161040ustar 00000000000000use std::{collections::HashMap, mem, path::Path, time::Duration}; use mpd_protocol::response::Frame; use crate::{ commands::{SongId, SongPosition}, responses::{parse_duration, FromFieldValue, Timestamp, TypedResponseError}, tag::Tag, }; /// A [`Song`] in the current queue, as returned by the [`playlistinfo`] command. /// /// [`playlistinfo`]: crate::commands::definitions::Queue #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct SongInQueue { /// Position in queue. pub position: SongPosition, /// ID in queue. pub id: SongId, /// The range of the song that will be played. pub range: Option, /// The priority. pub priority: u8, /// The song. pub song: Song, } impl SongInQueue { /// Convert the given frame into a single `SongInQueue`. pub(crate) fn from_frame_single( frame: Frame, ) -> Result, TypedResponseError> { let mut builder = SongBuilder::default(); for (key, value) in frame { builder.field(&key, value)?; } Ok(builder.finish()) } /// Convert the given frame into a list of `SongInQueue`s. pub(crate) fn from_frame_multi(frame: Frame) -> Result, TypedResponseError> { let mut out = Vec::new(); let mut builder = SongBuilder::default(); for (key, value) in frame { if let Some(song) = builder.field(&key, value)? { out.push(song); } } if let Some(song) = builder.finish() { out.push(song); } Ok(out) } } /// A single song, as returned by the [playlist] or [current song] commands. /// /// [playlist]: crate::commands::definitions::Queue /// [current song]: crate::commands::definitions::CurrentSong #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct Song { /// Unique identifier of the song. May be a file path relative to the library root, or an URL /// to a remote resource. /// /// This is the `file` key as returned by MPD. pub url: String, /// The `duration` as returned by MPD. pub duration: Option, /// Tags in this response. pub tags: HashMap>, /// The `format` as returned by MPD. pub format: Option, /// Last modification date of the underlying file. pub last_modified: Option, } impl Song { /// Get the file as a `Path`. Note that if the file is a remote URL, operations on the result /// will give unexpected results. pub fn file_path(&self) -> &Path { Path::new(&self.url) } /// Get all artists of the song. pub fn artists(&self) -> &[String] { self.tag_values(&Tag::Artist) } /// Get all album artists of the song. pub fn album_artists(&self) -> &[String] { self.tag_values(&Tag::AlbumArtist) } /// Get the album of the song. pub fn album(&self) -> Option<&str> { self.single_tag_value(&Tag::Album) } /// Get the title of the song. pub fn title(&self) -> Option<&str> { self.single_tag_value(&Tag::Title) } /// Get the disc and track number of the song. /// /// If either are not set on the song, 0 is returned. This is a utility for sorting. pub fn number(&self) -> (u64, u64) { let disc = self.single_tag_value(&Tag::Disc); let track = self.single_tag_value(&Tag::Track); ( disc.and_then(|v| v.parse().ok()).unwrap_or(0), track.and_then(|v| v.parse().ok()).unwrap_or(0), ) } /// Convert the given frame into a list of `Song`s. pub(crate) fn from_frame_multi(frame: Frame) -> Result, TypedResponseError> { let mut out = Vec::new(); let mut builder = SongBuilder::default(); for (key, value) in frame { if let Some(SongInQueue { song, .. }) = builder.field(&key, value)? { out.push(song); } } if let Some(SongInQueue { song, .. }) = builder.finish() { out.push(song); } Ok(out) } fn tag_values(&self, tag: &Tag) -> &[String] { match self.tags.get(tag) { Some(v) => v.as_slice(), None => &[], } } fn single_tag_value(&self, tag: &Tag) -> Option<&str> { match self.tag_values(tag) { [] => None, [v, ..] => Some(v), } } } /// Range used when playing only part of a [`Song`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SongRange { /// Start playback at this timestamp. pub from: Duration, /// End at this timestamp (if the end is known). pub to: Option, } impl FromFieldValue for SongRange { fn from_value(v: String, field: &str) -> Result { // The range follows the form "-" let Some((from, to)) = v.split_once('-') else { return Err(TypedResponseError::invalid_value(field, v)); }; let from = parse_duration(field, from)?; let to = if to.is_empty() { None } else { Some(parse_duration(field, to)?) }; Ok(SongRange { from, to }) } } #[derive(Debug, Default)] struct SongBuilder { url: String, position: usize, id: u64, range: Option, priority: u8, duration: Option, tags: HashMap>, format: Option, last_modified: Option, } impl SongBuilder { /// Handle a field from a song list. /// /// If this returns `Ok(Some(_))`, a song was completed and another one started. fn field( &mut self, key: &str, value: String, ) -> Result, TypedResponseError> { if self.url.is_empty() { // No song is currently in progress self.handle_start_field(key, value)?; Ok(None) } else { // Currently parsing a song self.handle_song_field(key, value) } } /// Handle a field that is expected to start a new song. fn handle_start_field(&mut self, key: &str, value: String) -> Result<(), TypedResponseError> { match key { // A `file` field starts a new song "file" => self.url = value, // Skip directory or playlist entries, encountered when using commands like // `listallinfo`, as well as the last modified date associated with these entries "directory" | "playlist" | "Last-Modified" => (), // Any other fields are invalid other => return Err(TypedResponseError::unexpected_field("file", other)), } Ok(()) } /// Handle a field that may be part of a song or may start a new one. fn handle_song_field( &mut self, key: &str, value: String, ) -> Result, TypedResponseError> { // If this field starts a new song, the current one is done if is_start_field(key) { // Reset the song builder and convert the existing data into a song let song = mem::take(self).into_song(); // Handle the current field self.handle_start_field(key, value)?; // Return the complete song return Ok(Some(song)); } // The field is a component of a song match key { "duration" => self.duration = Some(Duration::from_value(value, "duration")?), "Time" => { // Just a worse `duration` field, but retained for backwards compatibility with // protocol versions <0.20 if self.duration.is_none() { self.duration = Some(Duration::from_value(value, "Time")?); } } "Range" => self.range = Some(SongRange::from_value(value, "Range")?), "Format" => self.format = Some(value), "Last-Modified" => { let lm = Timestamp::from_value(value, "Last-Modified")?; self.last_modified = Some(lm); } "Prio" => self.priority = u8::from_value(value, "Prio")?, "Pos" => self.position = usize::from_value(value, "Pos")?, "Id" => self.id = u64::from_value(value, "Id")?, tag => { // Anything else is a tag. // It's fine to unwrap here because the protocol implementation already validated // the field name let tag = Tag::try_from(tag).unwrap(); self.tags.entry(tag).or_default().push(value); } } Ok(None) } /// Finish the building process. This returns the final song, if there is one. fn finish(self) -> Option { if self.url.is_empty() { None } else { Some(self.into_song()) } } fn into_song(self) -> SongInQueue { assert!(!self.url.is_empty()); SongInQueue { position: SongPosition(self.position), id: SongId(self.id), range: self.range, priority: self.priority, song: Song { url: self.url, duration: self.duration, tags: self.tags, format: self.format, last_modified: self.last_modified, }, } } } /// Returns `true` if the given field name starts a new song entry. fn is_start_field(f: &str) -> bool { matches!(f, "file" | "directory" | "playlist") } #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; const TEST_TIMESTAMP: &str = "2020-06-12T17:53:00Z"; #[test] fn song_builder() { let mut builder = SongBuilder::default(); assert_matches!(builder.field("file", String::from("test.flac")), Ok(None)); assert_matches!(builder.field("duration", String::from("123.456")), Ok(None)); assert_matches!( builder.field("Last-Modified", String::from(TEST_TIMESTAMP)), Ok(None) ); assert_matches!(builder.field("Title", String::from("Foo")), Ok(None)); assert_matches!(builder.field("Id", String::from("12")), Ok(None)); assert_matches!(builder.field("Pos", String::from("5")), Ok(None)); let song = builder .field("file", String::from("foo.flac")) .unwrap() .unwrap(); assert_eq!( song, SongInQueue { position: SongPosition(5), id: SongId(12), priority: 0, range: None, song: Song { url: String::from("test.flac"), duration: Some(Duration::from_secs_f64(123.456)), format: None, last_modified: Some(Timestamp::from_value(TEST_TIMESTAMP.into(), "").unwrap()), tags: [(Tag::Title, vec![String::from("Foo")])].into(), } } ); let song = builder.finish().unwrap(); assert_eq!( song, SongInQueue { position: SongPosition(0), id: SongId(0), priority: 0, range: None, song: Song { url: String::from("foo.flac"), duration: None, format: None, last_modified: None, tags: HashMap::new(), } } ); } #[test] fn song_builder_unrelated_entries() { let mut builder = SongBuilder::default(); assert_matches!(builder.field("playlist", String::from("foo.m3u")), Ok(None)); assert_matches!(builder.field("directory", String::from("foo")), Ok(None)); assert_matches!( builder.field("Last-Modified", String::from(TEST_TIMESTAMP)), Ok(None) ); assert_matches!(builder.field("file", String::from("foo.flac")), Ok(None)); let song = builder .field("directory", String::from("mep")) .unwrap() .unwrap(); assert_eq!( song, SongInQueue { position: SongPosition(0), id: SongId(0), priority: 0, range: None, song: Song { url: String::from("foo.flac"), duration: None, format: None, last_modified: None, tags: HashMap::new(), } } ); assert_matches!(builder.finish(), None); } #[test] fn song_builder_deprecated_time_field() { let mut builder = SongBuilder::default(); assert_matches!(builder.field("file", String::from("foo.flac")), Ok(None)); assert_matches!(builder.field("Time", String::from("123")), Ok(None)); assert_eq!(builder.duration, Some(Duration::from_secs(123))); assert_matches!(builder.field("duration", String::from("456.700")), Ok(None)); assert_eq!(builder.duration, Some(Duration::from_secs_f64(456.7))); assert_matches!(builder.field("Time", String::from("123")), Ok(None)); assert_eq!(builder.duration, Some(Duration::from_secs_f64(456.7))); let song = builder.finish().unwrap().song; assert_eq!( song, Song { url: String::from("foo.flac"), format: None, last_modified: None, duration: Some(Duration::from_secs_f64(456.7)), tags: HashMap::new(), } ); } #[test] fn parse_range() { assert_eq!( SongRange::from_value(String::from("1.500-5.642"), "Range").unwrap(), SongRange { from: Duration::from_secs_f64(1.5), to: Some(Duration::from_secs_f64(5.642)), } ); assert_eq!( SongRange::from_value(String::from("1.500-"), "Range").unwrap(), SongRange { from: Duration::from_secs_f64(1.5), to: None, } ); assert_matches!(SongRange::from_value(String::from("foo"), "Range"), Err(_)); assert_matches!( SongRange::from_value(String::from("1.000--5.000"), "Range"), Err(_) ); } } mpd_client-1.4.1/src/responses/sticker.rs000064400000000000000000000061461046102023000165760ustar 00000000000000use std::collections::HashMap; use mpd_protocol::response::Frame; use crate::responses::{KeyValuePair, TypedResponseError}; /// Response to the [`sticker get`] command. /// /// [`sticker get`]: crate::commands::definitions::StickerGet #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct StickerGet { /// The sticker value pub value: String, } impl StickerGet { pub(crate) fn from_frame(frame: Frame) -> Result { let Some((key, field_value)) = frame.into_iter().next() else { return Err(TypedResponseError::missing("sticker")); }; if &*key != "sticker" { return Err(TypedResponseError::unexpected_field( "sticker", key.as_ref(), )); } let (_, sticker_value) = parse_sticker_value(field_value)?; Ok(StickerGet { value: sticker_value, }) } } impl From for String { fn from(sticker_get: StickerGet) -> Self { sticker_get.value } } /// Response to the [`sticker list`] command. /// /// [`sticker list`]: crate::commands::definitions::StickerList #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct StickerList { /// A map of sticker names to their values pub value: HashMap, } impl StickerList { pub(crate) fn from_frame( raw: impl IntoIterator, ) -> Result { let value = raw .into_iter() .map(|(_, value)| parse_sticker_value(value)) .collect::>()?; Ok(Self { value }) } } impl From for HashMap { fn from(sticker_list: StickerList) -> Self { sticker_list.value } } /// Response to the [`sticker find`] command. /// /// [`sticker find`]: crate::commands::definitions::StickerFind #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct StickerFind { /// A map of songs to their sticker values pub value: HashMap, } impl StickerFind { pub(crate) fn from_frame( raw: impl IntoIterator, ) -> Result { let mut value = HashMap::new(); let mut file = String::new(); for (key, tag) in raw { match &*key { "file" => file = tag, "sticker" => { let (_, sticker_value) = parse_sticker_value(tag)?; value.insert(file.clone(), sticker_value); } other => return Err(TypedResponseError::unexpected_field("sticker", other)), } } Ok(Self { value }) } } /// Parses a `key=value` tag into its key and value strings fn parse_sticker_value(mut tag: String) -> Result<(String, String), TypedResponseError> { match tag.split_once('=') { Some((key, value)) => { let value = String::from(value); tag.truncate(key.len()); Ok((tag, value)) } None => Err(TypedResponseError::invalid_value("sticker", tag)), } } mpd_client-1.4.1/src/responses/timestamp.rs000064400000000000000000000037511046102023000171340ustar 00000000000000#[cfg(feature = "chrono")] use chrono::{DateTime, FixedOffset}; use crate::responses::{FromFieldValue, TypedResponseError}; /// A timestamp, used for modification times. /// /// This is a newtype wrapper to allow the optional use of the `chrono` library. #[derive(Clone, Debug, Eq)] pub struct Timestamp { raw: String, #[cfg(feature = "chrono")] chrono: DateTime, } impl Timestamp { /// Returns the timestamp string as it was returned by the server. pub fn raw(&self) -> &str { &self.raw } /// Returns the timestamp string as it was returned by the server. #[cfg(feature = "chrono")] pub fn chrono_datetime(&self) -> DateTime { self.chrono } } impl PartialEq for Timestamp { fn eq(&self, other: &Self) -> bool { self.raw == other.raw } } #[cfg(feature = "chrono")] impl PartialEq> for Timestamp { fn eq(&self, other: &DateTime) -> bool { &self.chrono == other } } impl PartialOrd for Timestamp { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Timestamp { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.raw.cmp(&other.raw) } } #[cfg(feature = "chrono")] impl PartialOrd> for Timestamp { fn partial_cmp(&self, other: &DateTime) -> Option { self.chrono.partial_cmp(other) } } impl FromFieldValue for Timestamp { #[cfg_attr(not(feature = "chrono"), allow(unused_variables))] fn from_value(v: String, field: &str) -> Result { #[cfg(feature = "chrono")] let chrono = match DateTime::parse_from_rfc3339(&v) { Ok(v) => v, Err(e) => return Err(TypedResponseError::invalid_value(field, v).source(e)), }; Ok(Self { raw: v, #[cfg(feature = "chrono")] chrono, }) } } mpd_client-1.4.1/src/tag.rs000064400000000000000000000203231046102023000136550ustar 00000000000000//! Metadata tags. use std::{ borrow::Cow, error::Error, fmt, hash::{Hash, Hasher}, }; use bytes::{BufMut, BytesMut}; use mpd_protocol::command::Argument; /// Tags which can be set on a [`Song`]. /// /// [MusicBrainz] tags are named differently from how they appear in the protocol to better /// reflect their actual purpose. /// /// # Tag validity /// /// **Manually** constructing a tag with the `Other` variant may result in protocols errors if the /// tag is invalid. Use the `TryFrom` implementation for checked conversion. /// /// # Unknown tags /// /// When parsing or constructing responses, tags not recognized by this type will be stored as they /// are encountered using the `Other` variant. Additionally the enum is marked as non-exhaustive, /// so additional tags may be added without breaking compatibility. /// /// The equality is checked using the string representation, so `Other` variants are /// forward-compatible with new variants being added. /// /// [`Song`]: crate::responses::Song /// [MusicBrainz]: https://musicbrainz.org #[derive(Clone, Debug)] #[allow(missing_docs)] #[non_exhaustive] pub enum Tag { Album, AlbumArtist, AlbumArtistSort, AlbumSort, Artist, ArtistSort, Comment, Composer, ComposerSort, Conductor, Date, Disc, Ensemble, Genre, Grouping, Label, Location, Movement, MovementNumber, MusicBrainzArtistId, MusicBrainzRecordingId, MusicBrainzReleaseArtistId, MusicBrainzReleaseId, MusicBrainzTrackId, MusicBrainzWorkId, Name, OriginalDate, Performer, Title, Track, Work, /// Catch-all variant that contains the raw tag string when it doesn't match any other /// variants, but is valid. Other(Box), } impl Tag { /// Creates a tag for [filtering] which will match *any* tag. /// /// [filtering]: crate::filter::Filter pub fn any() -> Self { Self::Other("any".into()) } pub(crate) fn as_str(&self) -> Cow<'static, str> { Cow::Borrowed(match self { Tag::Other(raw) => return Cow::Owned(raw.to_string()), Tag::Album => "Album", Tag::AlbumArtist => "AlbumArtist", Tag::AlbumArtistSort => "AlbumArtistSort", Tag::AlbumSort => "AlbumSort", Tag::Artist => "Artist", Tag::ArtistSort => "ArtistSort", Tag::Comment => "Comment", Tag::Composer => "Composer", Tag::ComposerSort => "ComposerSort", Tag::Conductor => "Conductor", Tag::Date => "Date", Tag::Disc => "Disc", Tag::Ensemble => "Ensemble", Tag::Genre => "Genre", Tag::Grouping => "Grouping", Tag::Label => "Label", Tag::Location => "Location", Tag::Movement => "Movement", Tag::MovementNumber => "MovementNumber", Tag::MusicBrainzArtistId => "MUSICBRAINZ_ARTISTID", Tag::MusicBrainzRecordingId => "MUSICBRAINZ_TRACKID", Tag::MusicBrainzReleaseArtistId => "MUSICBRAINZ_ALBUMARTISTID", Tag::MusicBrainzReleaseId => "MUSICBRAINZ_ALBUMID", Tag::MusicBrainzTrackId => "MUSICBRAINZ_RELEASETRACKID", Tag::MusicBrainzWorkId => "MUSICBRAINZ_WORKID", Tag::Name => "Name", Tag::OriginalDate => "OriginalDate", Tag::Performer => "Performer", Tag::Title => "Title", Tag::Track => "Track", Tag::Work => "Work", }) } } macro_rules! match_ignore_case { ($raw:ident, $($pattern:literal => $result:expr),+) => { $( if $raw.eq_ignore_ascii_case($pattern) { return Ok($result); } )+ }; } impl<'a> TryFrom<&'a str> for Tag { type Error = TagError; fn try_from(raw: &'a str) -> Result { if raw.is_empty() { return Err(TagError::Empty); } else if let Some((pos, chr)) = raw .char_indices() .find(|&(_, ch)| !(ch.is_ascii_alphabetic() || ch == '_' || ch == '-')) { return Err(TagError::InvalidCharacter { chr, pos }); } match_ignore_case! { raw, "Album" => Self::Album, "AlbumArtist" => Self::AlbumArtist, "AlbumArtistSort" => Self::AlbumArtistSort, "AlbumSort" => Self::AlbumSort, "Artist" => Self::Artist, "ArtistSort" => Self::ArtistSort, "Comment" => Self::Comment, "Composer" => Self::Composer, "ComposerSort" => Self::ComposerSort, "Conductor" => Self::Conductor, "Date" => Self::Date, "Disc" => Self::Disc, "Ensemble" => Self::Ensemble, "Genre" => Self::Genre, "Grouping" => Self::Grouping, "Label" => Self::Label, "Location" => Self::Location, "Movement" => Self::Movement, "MovementNumber" => Self::MovementNumber, "MUSICBRAINZ_ALBUMARTISTID" => Self::MusicBrainzReleaseArtistId, "MUSICBRAINZ_ALBUMID" => Self::MusicBrainzReleaseId, "MUSICBRAINZ_ARTISTID" => Self::MusicBrainzArtistId, "MUSICBRAINZ_RELEASETRACKID" => Self::MusicBrainzTrackId, "MUSICBRAINZ_TRACKID" => Self::MusicBrainzRecordingId, "MUSICBRAINZ_WORKID" => Self::MusicBrainzWorkId, "Name" => Self::Name, "OriginalDate" => Self::OriginalDate, "Performer" => Self::Performer, "Title" => Self::Title, "Track" => Self::Track, "Work" => Self::Work } Ok(Self::Other(raw.into())) } } impl PartialEq for Tag { fn eq(&self, other: &Tag) -> bool { self.as_str() == other.as_str() } } impl Eq for Tag {} impl<'a> PartialEq<&'a str> for Tag { fn eq(&self, other: &&'a str) -> bool { self.as_str() == *other } } impl PartialOrd for Tag { fn partial_cmp(&self, other: &Tag) -> Option { Some(self.cmp(other)) } } impl Ord for Tag { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.as_str().cmp(&other.as_str()) } } impl Hash for Tag { fn hash(&self, state: &mut H) { self.as_str().hash(state); } } impl Argument for Tag { fn render(&self, buf: &mut BytesMut) { buf.put_slice(self.as_str().as_bytes()); } } /// Errors that may occur when attempting to create a [`Tag`]. #[derive(Clone, Debug, PartialEq, Eq)] pub enum TagError { /// The raw tag was empty. Empty, /// The raw tag contained an invalid character. InvalidCharacter { /// The character. chr: char, /// Byte position of `chr`. pos: usize, }, } impl fmt::Display for TagError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Empty => write!(f, "empty tag"), Self::InvalidCharacter { chr, pos } => { write!(f, "invalid character {chr:?} at index {pos}") } } } } impl Error for TagError {} #[cfg(test)] mod tests { use super::*; #[test] fn try_from() { assert_eq!(Tag::try_from("Artist"), Ok(Tag::Artist)); // case-insensitive assert_eq!(Tag::try_from("artist"), Ok(Tag::Artist)); // unrecognized but valid tag assert_eq!(Tag::try_from("foo"), Ok(Tag::Other(Box::from("foo")))); } #[test] fn try_from_error() { assert_eq!(Tag::try_from(""), Err(TagError::Empty)); assert_eq!( Tag::try_from("foo bar"), Err(TagError::InvalidCharacter { chr: ' ', pos: 3 }) ); } #[test] fn as_arg() { assert_eq!(Tag::Album.as_str(), "Album"); assert_eq!(Tag::Other(Box::from("foo")).as_str(), "foo"); } #[test] fn equality() { assert_eq!(Tag::Album, Tag::Other(Box::from("Album"))); assert_eq!( Tag::Other(Box::from("Album")), Tag::Other(Box::from("Album")) ); assert_ne!(Tag::Other(Box::from("Foo")), Tag::Other(Box::from("Bar"))); assert_eq!(Tag::Artist, "Artist"); assert_eq!(Tag::Other(Box::from("Foo")), "Foo"); } }