mpris-2.0.1/.cargo_vcs_info.json0000644000000001360000000000100121660ustar { "git": { "sha1": "d74e182d548cbf295c079659bfa78fbdc917305c" }, "path_in_vcs": "" }mpris-2.0.1/.gitignore000064400000000000000000000000371046102023000127460ustar 00000000000000/target/ **/*.rs.bk Cargo.lock mpris-2.0.1/.gitmodules000064400000000000000000000001471046102023000131350ustar 00000000000000[submodule "mpris-spec"] path = mpris-spec url = https://gitlab.freedesktop.org/mpris/mpris-spec.git mpris-2.0.1/.travis.yml000064400000000000000000000005241046102023000130700ustar 00000000000000language: rust rust: - stable - beta - nightly matrix: allow_failures: - rust: nightly fast_finish: true # Travis is starting to timeout all the time when using cache # cache: cargo addons: apt: packages: - libdbus-1-dev # To be able to compile dbus-rs - dbus # DBus server script: - xvfb-run ./script/ci.sh mpris-2.0.1/CHANGELOG.md000064400000000000000000000214401046102023000125700ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] Nothing yet. ## [v2.0.1] - 2023-06-15 ### Fixed * Add extra check for `Player::get_track_list()`. Without this, `Player::track_progress()` would fail for Spotify. - [Mihai Fufezan (fufexan)][fufexan] ## [v2.0.0] - 2022-11-15 ### Breaking changes * Minimum supported Rust version is now 1.54.0. ## [v2.0.0-rc3] - 2022-09-12 **Important:** Now using Rust 2018 edition. ### Fixed * Track change detection for some non-conforming players (e.g. Spotify). - [Stephan Henrichs (Kilobyte22)][Kilobyte22] * Error on progress tracker for players that do not support shuffling. - [Stephan Henrichs (Kilobyte22)][Kilobyte22] * Events not added for streams - [Kanjirito][Kanjirito] * Incorrect error messages when using the `Display` trait [Kanjirito][Kanjirito] ### Added * `Player::can_shuffle` and `Player::checked_get_shuffle`. - [Stephan Henrichs (Kilobyte22)][Kilobyte22] * Iterator methods to `Metadata`: * `impl IntoIterator` * `iter()` * `keys()` * `Player::bus_name_player_name_part` - [Koen Bolhuis (InputUsername)](https://github.com/InputUsername) * `Metadata::as_hashmap(&self)` which returns a simple borrowed hashmap. * More `Player::has_*`, `Player::can_*`, and `Player::checked_*` methods - [Harrison Thorne (harrisonthorne)][harrisonthorne] * `Player::has_volume`, `Player::checked_get_volume`, `Player::checked_set_volume` * `Player::has_position`, `Player::checked_get_position`, `Player::checked_set_position` * `Player::has_playback_rate`, `Player::checked_get_playback_rate`, `Player::checked_set_playback_rate` * `Player::can_loop`, `Player::checked_get_loop_status` * `PlayerIter` that iterates over all of the players [Kanjirito][Kanjirito] * `PlayerFinder.player_timeout_ms` field that changes the DBUS timeout value for all new `Player`s [Kanjirito][Kanjirito] ### Changed * Now using Rust 2018 edition. * `Player::checked_set_shuffle` also checks `::can_shuffle`. - [Stephan Henrichs (Kilobyte22)][Kilobyte22] * `Player::checked_set_loop_status` also checks `::can_loop` - [Harrison Thorne (harrisonthorne)][harrisonthorne] * `Progress` default values uses `checked_get_*` functions - [Harrison Thorne (harrisonthorne)][harrisonthorne] * Documentation was made easier to navigate - [Kanjirito][Kanjirito] * Use `thiserror` & `anyhow` instead of unmaintained `failure` - [fengalin][fengalin] * Removed `Player` lifetime [Kanjirito][Kanjirito] * All `PlayerFinder` find methods switched to using `PlayerIter` [Kanjirito][Kanjirito] ## [v2.0.0-rc2] - 2020-02-15 This is a RC for 2.0.0. If no major problems are discovered, this version will be re-labeled as 2.0.0. If issues are found, they will be fixed in subsequent versions. ### Changed - `PlayerEvents` is now properly exposed from the crate root. - Now using `dbus` 0.8.1. - This might require a bump of Rust version. ## [v2.0.0-rc1] - 2019-02-06 This is a RC for 2.0.0. If no major problems are discovered, this version will be re-labeled as 2.0.0. If issues are found, they will be fixed in subsequent versions. ### Changed - This library now only supports "latest stable" version of Rust. Hopefully this can be changed the day it is possible to mark minimum version in the crate manifest. - Some methods have a different error type to add more context to the errors that can happen. See `TrackListError` and `ProgressError`. - `ProgressTracker::tick` now returns a `ProgressTick` instead of a `bool`. - `ProgressTick` contains information about tracklist (if player supports it), and what parts have changed. ### Fixed - Emitted `Event::TrackChanged` events now contains full metadata. - Compilation warnings caused by newer Rust versions (up to 1.28) have been fixed. - `Player::set_volume` is fixed (always set to 0 previously) - Detection of volume and playback rate changes using `PlayerEvents` iterator now works. - Loading of length of a track now works in more clients. #40 ### Added - A new version of `Metadata` that should be much easier to use with extra metadata values, or to populate for tests. - A full implementation of all properties and methods on the `org.mpris.MediaPlayer2` interface. - Support for the `Seeked` signal in the blocking `PlayerEvents` iterator. - Support for TrackList signals in `PlayerEvents` iterator. - A new `TrackList` struct, which keeps track of `Metadata` for tracks. - `Progress` provides an up-to-date `TrackList` if the player supports it. - You can manually maintain this for your `PlayerEvents` iterator if you wish. - Support for loading `Metadata` for a specific `TrackID`. - `TrackListError` is an error type for problems with tracklists. - `ProgressError` is an error type for problems with progress tracking. - `Player::can_edit_tracks`. - `Player::checked_can_edit_tracks`. - `Player::supports_track_lists`. - A new example called "Capabilities" that shows capabilities of running players. ### Removed - All deprecated items in [v1.1.1] have been removed. ## [v1.1.1] - 2019-01-04 ### Fixed - Loading of length of a track now works in more clients. #40 ## [v1.1.0] - 2018-08-18 ### Added - `Player::events(&self)` returns a blocking iterator of player events. - Use this to block single-threaded apps until something happens and then react on this event. - This is not suitable if you want to render a progress bar as it will only return when something changes; if you want to render the information at a regular update interval then keep using `Player::track_progress(&self)` instead. - `MetadataValue` type, for dynamically types metadata values. This will replace the raw DBus values in `Metadata` in version 2.0. - `Player::get_metadata_hash` which returns a `Result, DBusError>`. - `Metadata::rest_hash` which converts values in the `rest` hash into `MetadataValue`s, where possible. This is closer to how `Metadata` will work in 2.0. - `Progress::playback_rate` returns the playback rate at the time of measurement. - `Player::is_running` checks if a player is still running. Use this to detect players shutting down. ### Changed - `Metadata` can now be constructed with empty metadata; `track_id` will then be the empty string. * Some players (like VLC) without any tracks on its play queue emits empty metadata, which would cause this library to return an error instead of an empty metadata. - `Metadata` now implements `Default`. ### Deprecated - `Metadata::rest` is deprecated; version 2.0 will have a method that returns `MetadataValue`s instead. - `Player::get_metadata_hash` is added as deprecated. It will likely be merged into `Metadata` in version 2.0, but presents a way to get all supported metadata values where `Metadata::rest` might not. ## [v1.0.0] - 2018-01-19 ### Added - `TrackID` struct added. - `Player` can now query and change `Shuffle` status. - `Player` can now query and change `LoopStatus`. - `Player` can now change playback rate. - `Player` can now query for valid playback rates and if it supports setting rates at all. - `Player` can now control volume. - `Player` can now query for current position as a `std::time::Duration` and not just a microsecond count. - `Player` can set position, if a valid `TrackID` is given. - Note: This library has no way of querying for valid `TrackID`s right now. ### Changed - `failure` replaces `error_chain` for error handling. - All errors now implements the `failure::Fail` trait, and methods return more fine-grained `Result`s. - All fields on `Progress` and `Metadata` are now methods instead. - Playback rate is now `f64` instead of `f32`. ### Removed - The `supports_progress` method is removed from `Progress`. - This is better left to clients to do themselves as this library cannot guarantee anything anyway. ## 0.1.0 - 2017-12-29 [Unreleased]: https://github.com/Mange/mpris-rs/compare/v2.0.1...HEAD [v2.0.1]: https://github.com/Mange/mpris-rs/compare/v2.0.0...v2.0.1 [v2.0.0]: https://github.com/Mange/mpris-rs/compare/v2.0.0-rc3...v2.0.0 [v2.0.0-rc3]: https://github.com/Mange/mpris-rs/compare/v2.0.0-rc2...v2.0.0-rc3 [v2.0.0-rc2]: https://github.com/Mange/mpris-rs/compare/v2.0.0-rc1...v2.0.0-rc2 [v2.0.0-rc1]: https://github.com/Mange/mpris-rs/compare/v1.1.0...v2.0.0-rc1 [v1.1.1]: https://github.com/Mange/mpris-rs/compare/v1.1.0...v1.1.1 [v1.1.0]: https://github.com/Mange/mpris-rs/compare/v1.0.0...v1.1.0 [v1.0.0]: https://github.com/Mange/mpris-rs/compare/v0.1.0...v1.0.0 [Kilobyte22]: https://github.com/Kilobyte22 [harrisonthorne]: https://github.com/harrisonthorne [Kanjirito]: https://github.com/Kanjirito [fengalin]: https://github.com/fengalin [fufexan]: https://github.com/fufexan mpris-2.0.1/Cargo.lock0000644000000201220000000000100101360ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "anyhow" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote 1.0.21", "strsim", "syn 1.0.103", ] [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote 1.0.21", "syn 1.0.103", ] [[package]] name = "dbus" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" dependencies = [ "libc", "libdbus-sys", "winapi", ] [[package]] name = "derive_is_enum_variant" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" dependencies = [ "heck", "quote 0.3.15", "syn 0.11.11", ] [[package]] name = "enum-kinds" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" dependencies = [ "proc-macro2", "quote 1.0.21", "syn 1.0.103", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "from_variants" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cf36180ca6f3c021e91b194e16b670ef5cbdd0cea48354ff6f5f83e3c2d1629" dependencies = [ "from_variants_impl", ] [[package]] name = "from_variants_impl" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13abfd95d43eabb051a8d4b408ef92dfe6d8d4aa17651e5786d5c761e5e6e7ad" dependencies = [ "darling", "proc-macro2", "quote 1.0.21", "syn 1.0.103", ] [[package]] name = "heck" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "libc" version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libdbus-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" dependencies = [ "pkg-config", ] [[package]] name = "mpris" version = "2.0.1" dependencies = [ "anyhow", "dbus", "derive_is_enum_variant", "enum-kinds", "from_variants", "termion", "thiserror", ] [[package]] name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "pkg-config" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "proc-macro2" version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "redox_termios" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ "redox_syscall", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" dependencies = [ "quote 0.3.15", "synom", "unicode-xid", ] [[package]] name = "syn" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote 1.0.21", "unicode-ident", ] [[package]] name = "synom" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" dependencies = [ "unicode-xid", ] [[package]] name = "termion" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90" dependencies = [ "libc", "numtoa", "redox_syscall", "redox_termios", ] [[package]] name = "thiserror" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote 1.0.21", "syn 1.0.103", ] [[package]] name = "unicode-ident" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-segmentation" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-xid" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" [[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" mpris-2.0.1/Cargo.toml0000644000000024500000000000100101650ustar # 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 = "2018" rust-version = "1.54.0" name = "mpris" version = "2.0.1" authors = ["Magnus Bergmark "] description = "Idiomatic MPRIS D-Bus interface library" readme = "README.md" keywords = [ "dbus", "mpris", ] categories = [ "api-bindings", "multimedia", "os::unix-apis", ] license = "Apache-2.0" repository = "https://github.com/Mange/mpris-rs" [dependencies.dbus] version = "0.9.6" [dependencies.derive_is_enum_variant] version = "0.1.1" [dependencies.enum-kinds] version = "0.5.1" [dependencies.from_variants] version = "1.0.0" [dependencies.thiserror] version = "1.0.37" [dev-dependencies.anyhow] version = "1.0.66" [dev-dependencies.termion] version = "2.0.1" [badges.maintenance] status = "actively-developed" [badges.travis-ci] branch = "master" repository = "Mange/mpris-rs" mpris-2.0.1/Cargo.toml.orig0000644000000013010000000000100111160ustar [package] name = "mpris" description = "Idiomatic MPRIS D-Bus interface library" version = "2.0.1" license = "Apache-2.0" edition = "2018" rust-version = "1.54.0" authors = ["Magnus Bergmark "] repository = "https://github.com/Mange/mpris-rs" readme = "README.md" categories = ["api-bindings", "multimedia", "os::unix-apis"] keywords = ["dbus", "mpris"] [badges] travis-ci = { repository = "Mange/mpris-rs", branch = "master" } maintenance = { status = "actively-developed" } [dependencies] dbus = "0.9.6" derive_is_enum_variant = "0.1.1" enum-kinds = "0.5.1" from_variants = "1.0.0" thiserror = "1.0.37" # For examples [dev-dependencies] anyhow = "1.0.66" termion = "2.0.1" mpris-2.0.1/Cargo.toml.orig000064400000000000000000000013011046102023000136400ustar 00000000000000[package] name = "mpris" description = "Idiomatic MPRIS D-Bus interface library" version = "2.0.1" license = "Apache-2.0" edition = "2018" rust-version = "1.54.0" authors = ["Magnus Bergmark "] repository = "https://github.com/Mange/mpris-rs" readme = "README.md" categories = ["api-bindings", "multimedia", "os::unix-apis"] keywords = ["dbus", "mpris"] [badges] travis-ci = { repository = "Mange/mpris-rs", branch = "master" } maintenance = { status = "actively-developed" } [dependencies] dbus = "0.9.6" derive_is_enum_variant = "0.1.1" enum-kinds = "0.5.1" from_variants = "1.0.0" thiserror = "1.0.37" # For examples [dev-dependencies] anyhow = "1.0.66" termion = "2.0.1" mpris-2.0.1/LICENSE000064400000000000000000000261351046102023000117720ustar 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. mpris-2.0.1/README.md000064400000000000000000000051331046102023000122370ustar 00000000000000# mpris > A Rust library for dealing with [MPRIS2][mpris2]-compatible players over > D-Bus. [![Crates.io][crate-badge]][crate] [![Documentation][docs-badge]][docs] [![Build Status][ci-badge]][ci] ![Actively developed][maintenance-badge] ## What is MPRIS2? > The Media Player Remote Interfacing Specification is a standard D-Bus > interface which aims to provide a common programmatic API for controlling > media players. > > It provides a mechanism for discovery, querying and basic playback control of > compliant media players, as well as a tracklist interface which is used to > add context to the active media item. From [*About*, in the MPRIS2 specification][mpris-about]. Basically, you can use it to control media players on your computer. This is most commonly used to build media player applets, UIs or to pause other players before your own software performs some action. You can also use it in order to query metadata about what is currently playing, or *if* something is playing. ## How to use ```rust use mpris::PlayerFinder; // Pauses currently playing media and prints metadata information about that // media. // If no player is running, exits with an error. fn main() { let player = PlayerFinder::new() .expect("Could not connect to D-Bus") .find_active() .expect("Could not find any player"); player.pause().expect("Could not pause"); let metadata = player.get_metadata().expect("Could not get metadata for player"); println!("{:#?}", metadata); } ``` See the `examples` directory for more examples. ## License Copyright 2017-2022 Magnus Bergmark 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. [mpris2]: https://specifications.freedesktop.org/mpris-spec/latest/ [mpris-about]: https://specifications.freedesktop.org/mpris-spec/latest/#About [docs]: https://docs.rs/mpris/ [docs-badge]: https://docs.rs/mpris/badge.svg [crate]: https://crates.io/crates/mpris [crate-badge]: https://img.shields.io/crates/v/mpris.svg [maintenance-badge]: https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg [ci-badge]: https://travis-ci.org/Mange/mpris-rs.svg?branch=master [ci]: https://travis-ci.org/Mange/mpris-rs mpris-2.0.1/examples/capabilities.rs000064400000000000000000000110501046102023000155700ustar 00000000000000use anyhow::{Context, Result}; use mpris::{DBusError, Player, PlayerFinder}; use std::borrow::Cow; const VALUE_INDENTATION: usize = 25; trait CustomDisplay { fn string_for_display(&self) -> Cow<'_, str>; } fn main() { match print_capabilities_for_all_players() { Ok(_) => {} Err(error) => { println!("Error: {}", error); for (i, cause) in error.chain().skip(1).enumerate() { print!("{}", " ".repeat(i + 1)); println!("Caused by: {}", cause); } std::process::exit(1); } } } fn print_capabilities_for_all_players() -> Result<()> { for player in PlayerFinder::new() .context("Failed to connect to D-Bus")? .find_all() .context("Could not fetch list of players")? { print_capabilities_for_player(player)?; println!(); } Ok(()) } fn print_capabilities_for_player(player: Player) -> Result<()> { println!( ">> Player: {} ({})", player.identity(), player.unique_name() ); println!(); println!("\t─── MediaPlayer2 ───"); print_value("CanQuit", player.can_quit()); print_value("CanRaise", player.can_raise()); print_value("CanSetFullscreen", player.can_set_fullscreen()); print_value("HasTrackList", player.get_has_track_list()); print_value("SupportedMimeTypes", player.get_supported_mime_types()); print_value("SupportedUriSchemes", player.get_supported_uri_schemes()); println!(); println!("\t─── MediaPlayer2.Player ───"); print_value("CanControl", player.can_control()); print_value("CanGoNext", player.can_go_next()); print_value("CanGoPrevious", player.can_go_previous()); print_value("CanLoop", player.can_loop()); print_value("CanPause", player.can_pause()); print_value("CanPlay", player.can_play()); print_value("CanSeek", player.can_seek()); print_value("CanSetPaybackRate", player.can_set_playback_rate()); print_value("CanShuffle", player.can_shuffle()); print_value("CanStop", player.can_stop()); print_value("HasPlaybackRate", player.has_playback_rate()); print_value("HasPosition", player.has_position()); print_value("HasVolume", player.has_volume()); print_value("Rate", player.get_playback_rate()); print_value("MaximumRate", player.get_maximum_playback_rate()); print_value("MinimumRate", player.get_minimum_playback_rate()); println!(); println!("\t─── MediaPlayer2.TrackList ───"); if player.supports_track_lists() { print_value("CanEditTracks", player.can_edit_tracks()); } else { println!("\tPlayer does not support TrackList interface!\n\tNote how they fail.\n"); print_value("CanEditTracks", player.can_edit_tracks()); println!( "\n\tYou can used the \"Checked\" variants to hide\n\terror handling for these cases:" ); print_value("CheckedCanEditTracks", player.checked_can_edit_tracks()); } Ok(()) } fn print_value(name: &str, value: T) { println!( "\t{1:>0$}:\t{2}", VALUE_INDENTATION, name, value.string_for_display() ); } impl CustomDisplay for bool { fn string_for_display(&self) -> Cow<'_, str> { match self { true => "✔ Yes".into(), false => "✖ No".into(), } } } impl CustomDisplay for f64 { fn string_for_display(&self) -> Cow<'_, str> { format!("{:.3}", self).into() } } impl CustomDisplay for String { fn string_for_display(&self) -> Cow<'_, str> { self.into() } } impl CustomDisplay for DBusError { fn string_for_display(&self) -> Cow<'_, str> { format!("Error: {}", self).into() } } impl CustomDisplay for Vec where T: CustomDisplay, { fn string_for_display(&self) -> Cow<'_, str> { let mut buf = String::new(); for val in self { if buf.is_empty() { buf.push_str(&val.string_for_display()); } else { buf.push_str(&format!( "\n\t{1:>0$} \t{2}", VALUE_INDENTATION, "", val.string_for_display() )); } } buf.into() } } impl CustomDisplay for Result where T: CustomDisplay, E: CustomDisplay, { fn string_for_display(&self) -> Cow<'_, str> { match self { Ok(val) => val.string_for_display(), Err(err) => err.string_for_display(), } } } mpris-2.0.1/examples/control.rs000064400000000000000000000345401046102023000146300ustar 00000000000000use std::borrow::Cow; use std::io::{stdout, Stdout, Write}; use std::time::Duration; use mpris::{ LoopStatus, Metadata, PlaybackStatus, Player, PlayerFinder, Progress, ProgressTick, ProgressTracker, TrackID, TrackList, }; use termion::color; use termion::input::TermRead; use termion::raw::{IntoRawMode, RawTerminal}; use termion::screen::{AlternateScreen, IntoAlternateScreen}; const REFRESH_INTERVAL: u32 = 100; // ms type Screen = AlternateScreen>; #[derive(Clone, Copy)] enum Action { Quit, PlayPause, Stop, Next, Previous, SeekForwards, SeekBackwards, ToggleShuffle, CycleLoopStatus, IncreaseVolume, DecreaseVolume, } const ACTIONS: &[Action] = &[ Action::PlayPause, Action::Stop, Action::Next, Action::Previous, Action::ToggleShuffle, Action::CycleLoopStatus, Action::SeekForwards, Action::SeekBackwards, Action::IncreaseVolume, Action::DecreaseVolume, Action::Quit, ]; impl Action { fn from_key(key: termion::event::Key) -> Option { use crate::Action::*; use termion::event::Key; match key { Key::Ctrl('c') | Key::Esc | Key::Char('q') => Some(Quit), Key::Char(' ') => Some(PlayPause), Key::Char('s') => Some(Stop), Key::Char('n') => Some(Next), Key::Char('p') => Some(Previous), Key::Char('z') => Some(ToggleShuffle), Key::Char('x') => Some(CycleLoopStatus), Key::Char('+') => Some(IncreaseVolume), Key::Char('-') => Some(DecreaseVolume), Key::Left => Some(SeekForwards), Key::Right => Some(SeekBackwards), _ => None, } } fn key_name(&self) -> &'static str { match *self { Action::Quit => "q", Action::PlayPause => "Space", Action::Stop => "s", Action::Next => "n", Action::Previous => "p", Action::ToggleShuffle => "z", Action::CycleLoopStatus => "x", Action::SeekForwards => "Left", Action::SeekBackwards => "Right", Action::IncreaseVolume => "+", Action::DecreaseVolume => "-", } } fn description(&self) -> &'static str { match *self { Action::Quit => "Quit example", Action::PlayPause => "Toggle play/pause", Action::Stop => "Stop", Action::Next => "Next media", Action::Previous => "Previous media", Action::ToggleShuffle => "Toggle shuffle", Action::CycleLoopStatus => "Cycle loop status", Action::SeekForwards => "Seek 5s forward", Action::SeekBackwards => "Seek 5s backward", Action::IncreaseVolume => "Increase volume", Action::DecreaseVolume => "Decrease volume", } } fn is_enabled(&self, player: &Player) -> bool { match *self { Action::Quit => true, Action::PlayPause => player.can_pause().unwrap_or(false), Action::Stop => player.can_stop().unwrap_or(false), Action::Next => player.can_go_next().unwrap_or(false), Action::Previous => player.can_go_previous().unwrap_or(false), Action::ToggleShuffle | Action::CycleLoopStatus => { player.can_control().unwrap_or(false) } Action::IncreaseVolume => player .can_control() .and_then(|_| player.get_volume()) .map(|vol| vol < 1.0) .unwrap_or(false), Action::DecreaseVolume => player .can_control() .and_then(|_| player.get_volume()) .map(|vol| vol > 0.0) .unwrap_or(false), Action::SeekForwards | Action::SeekBackwards => player.can_seek().unwrap_or(false), } } fn should_exit(&self) -> bool { matches!(self, Action::Quit) } } struct App<'a> { player: &'a Player, progress_tracker: ProgressTracker<'a>, stdin: termion::AsyncReader, screen: Screen, } impl<'a> App<'a> { fn main_loop(&mut self) { let mut should_continue = true; // Start on true so first iteration refreshes everything; after that it will be set to // false until something causes a change. let mut should_refresh = true; while should_continue { while let Some(action) = self.next_action() { self.perform_action(action); if action.should_exit() { should_continue = false; } should_refresh = true; } self.tick_progress_and_refresh(should_refresh); should_refresh = false; } } fn next_action(&mut self) -> Option { (&mut self.stdin) .keys() .next() .and_then(|result| result.ok()) .and_then(Action::from_key) } fn perform_action(&mut self, action: Action) { match action { Action::Quit => (), Action::PlayPause => control_player(self.player.play_pause()), Action::Stop => control_player(self.player.stop()), Action::Next => control_player(self.player.next()), Action::Previous => control_player(self.player.previous()), Action::ToggleShuffle => control_player(toggle_shuffle(self.player)), Action::CycleLoopStatus => control_player(cycle_loop_status(self.player)), Action::SeekBackwards => { control_player(self.player.seek_backwards(&Duration::new(5, 0))) } Action::SeekForwards => control_player(self.player.seek_forwards(&Duration::new(5, 0))), Action::IncreaseVolume => control_player(change_volume(self.player, 0.1)), Action::DecreaseVolume => control_player(change_volume(self.player, -0.1)), }; } fn tick_progress_and_refresh(&mut self, should_refresh: bool) { let supports_position = self.supports_position(); let ProgressTick { progress, progress_changed, track_list, track_list_changed, .. } = self.progress_tracker.tick(); // Dirty tracking to keep CPU usage lower. In case nothing happened since the last refresh, // only update the progress bar. // // If player doesn't support position handling, don't even try to refresh the progress bar // if no event took place. if progress_changed || track_list_changed || should_refresh { let current_track_id = progress.metadata().track_id(); clear_screen(&mut self.screen); print_instructions(&mut self.screen, self.player); print_playback_info(&mut self.screen, progress); if let Some(tracks) = track_list { let next_track = find_next_track(current_track_id, tracks, self.player); print_track_list(&mut self.screen, tracks, next_track); } print_progress_bar(&mut self.screen, progress, supports_position); } else if supports_position { clear_progress_bar(&mut self.screen); print_progress_bar(&mut self.screen, progress, supports_position); } self.screen.flush().unwrap(); } fn supports_position(&self) -> bool { self.player.identity() != "Spotify" } } fn print_instructions(screen: &mut Screen, player: &Player) { let bold = termion::style::Bold; // Note: The NoBold variant enables double-underscore in Kitty terminal let nobold = termion::style::Reset; write!( screen, "{}Instructions for controlling {}:{}\r\n", bold, player.identity(), nobold, ) .unwrap(); for action in ACTIONS { let is_enabled = action.is_enabled(player); if is_enabled { write!(screen, "{}", color::Fg(color::Reset)).unwrap(); } else { write!(screen, "{}", color::Fg(color::LightBlack)).unwrap(); }; write!( screen, " {bold}{key:>5}{nobold} - {description}", bold = bold, nobold = nobold, key = action.key_name(), description = action.description(), ) .unwrap(); if !is_enabled { write!(screen, " (not supported)").unwrap(); } write!( screen, "{nostatecolor}\r\n", nostatecolor = color::Fg(color::Reset), ) .unwrap(); } write!(screen, "\r\n").unwrap(); } fn control_player(result: Result<(), mpris::DBusError>) { result.expect("Could not control player"); } fn toggle_shuffle(player: &Player) -> Result<(), mpris::DBusError> { player.set_shuffle(!player.get_shuffle()?) } fn cycle_loop_status(player: &Player) -> Result<(), mpris::DBusError> { let current_status = player.get_loop_status()?; let next_status = match current_status { LoopStatus::None => LoopStatus::Playlist, LoopStatus::Playlist => LoopStatus::Track, LoopStatus::Track => LoopStatus::None, }; player.set_loop_status(next_status) } fn change_volume(player: &Player, diff: f64) -> Result<(), mpris::DBusError> { let current_volume = player.get_volume()?; let new_volume = (current_volume + diff).max(0.0).min(1.0); player.set_volume(new_volume) } fn print_playback_info(screen: &mut Screen, progress: &Progress) { let playback_string = match progress.playback_status() { PlaybackStatus::Playing => format!("{}▶", color::Fg(color::Green)), PlaybackStatus::Paused => format!("{}▮▮", color::Fg(color::LightBlack)), PlaybackStatus::Stopped => format!("{}◼", color::Fg(color::Red)), }; let shuffle_string = if progress.shuffle() { format!("{}⮭⮯", color::Fg(color::Green)) } else { format!("{}🠯🠯", color::Fg(color::LightBlack)) }; let loop_string = match progress.loop_status() { LoopStatus::None => format!("{}🠮", color::Fg(color::LightBlack)), LoopStatus::Playlist => format!("{}🔁", color::Fg(color::Green)), LoopStatus::Track => format!("{}🔂", color::Fg(color::Yellow)), }; let volume_string = format!("(vol: {:3.0}%)", progress.current_volume() * 100.0); write!( screen, "{playback} {shuffle} {loop} {color_reset} ", playback = playback_string, shuffle = shuffle_string, loop = loop_string, color_reset = color::Fg(color::Reset), ) .unwrap(); print_track_info(screen, progress.metadata()); write!(screen, " {volume}\r\n", volume = volume_string).unwrap(); } fn print_track_info(screen: &mut Screen, track: &Metadata) { let artist_string: Cow<'_, str> = track .artists() .map(|artists| Cow::Owned(artists.join(" + "))) .unwrap_or_else(|| Cow::Borrowed("Unknown artist")); let title_string = track.title().unwrap_or("Unkown title"); write!( screen, "{blue}{bold}{artist}{reset}{blue} - {title}{color_reset}", blue = color::Fg(color::Blue), color_reset = color::Fg(color::Reset), bold = termion::style::Bold, // Note: The NoBold variant enables double-underscore in Kitty terminal reset = termion::style::Reset, artist = artist_string, title = title_string, ) .unwrap(); } fn print_track_list(screen: &mut Screen, track_list: &TrackList, next_track: Option) { if let Some(track) = next_track { write!( screen, "{bold}Next:{reset} ", bold = termion::style::Bold, reset = termion::style::Reset, ) .unwrap(); print_track_info(screen, &track); write!(screen, ", ").unwrap(); } write!( screen, "{bold}{count}{reset} track(s) on list.\r\n", count = track_list.len(), bold = termion::style::Bold, reset = termion::style::Reset, ) .unwrap(); } fn find_next_track( current_track_id: Option, track_list: &TrackList, player: &Player, ) -> Option { if let Some(current_id) = current_track_id { track_list .metadata_iter(player) .ok()? .skip_while(|track| match track.track_id() { // Stops on current track Some(id) => id != current_id, None => false, }) .nth(1) // Skip one more to get the next one } else { None } } fn print_progress_bar(screen: &mut Screen, progress: &Progress, supports_position: bool) { let position_string: Cow<'_, str> = if supports_position { Cow::Owned(format_duration(progress.position())) } else { Cow::Borrowed("??:??:??") }; let length_string: Cow<'_, str> = progress .length() .map(|s| Cow::Owned(format_duration(s))) .unwrap_or_else(|| Cow::Borrowed("??:??:??")); write!( screen, "{position} / {length}\r\n", position = position_string, length = length_string, ) .unwrap(); } fn clear_progress_bar(screen: &mut Screen) { write!( screen, "{}\r{}", termion::cursor::Up(1), termion::clear::CurrentLine, ) .unwrap(); } fn clear_screen(screen: &mut Screen) { write!( screen, "{}{}", termion::clear::All, termion::cursor::Goto(1, 1) ) .unwrap(); } fn format_duration(duration: Duration) -> String { let secs = duration.as_secs(); let whole_hours = secs / (60 * 60); let secs = secs - whole_hours * 60 * 60; let whole_minutes = secs / 60; let secs = secs - whole_minutes * 60; format!("{:02}:{:02}:{:02}", whole_hours, whole_minutes, secs) } fn main() { let player = PlayerFinder::new() .unwrap() .find_active() .expect("Could not find a running player"); let progress_tracker = player .track_progress(REFRESH_INTERVAL) .expect("Could not determine progress of player"); let screen = stdout() .into_raw_mode() .expect("Failed to initialize terminal raw mode") .into_alternate_screen() .expect("Failed to enter alternative screen"); let mut app = App { player: &player, progress_tracker, screen, stdin: termion::async_stdin(), }; app.main_loop(); } mpris-2.0.1/examples/detecting_shutting_down.rs000064400000000000000000000035411046102023000200670ustar 00000000000000use std::thread::sleep; use std::time::Duration; use mpris::{Player, PlayerFinder}; fn move_cursor_up(n: usize) { print!("\x1b[{}A", n); } fn cursor_beginning_of_line() { print!("\r"); } fn clear_to_end_of_line() { print!("\x1b[K"); } fn main() { let finder = PlayerFinder::new().expect("Could not connect to D-Bus"); let mut lines_drawn = 0; let mut all_running = false; let mut players: Vec = Vec::new(); println!( " All found players will be listed below, but if any of those players shuts down the list will refresh. TIP: Start another player that is not on the list (list will stay the same) and then shut down one of the players on the list. You should see the running state of that player change, and then see the list reload and your new player taking the old one's place. Exit with Ctrl-C. " ); loop { if players.is_empty() { players = finder.find_all().expect("Could not find players"); all_running = true; } if lines_drawn > 0 { cursor_beginning_of_line(); for _ in 0..lines_drawn { clear_to_end_of_line(); move_cursor_up(1); } lines_drawn = 0; } println!("Current players: ({})", players.len()); lines_drawn += 1; for player in &players { let is_running = player.is_running(); println!( " * {} ({}) - running: {}", player.identity(), player.bus_name(), is_running ); all_running &= is_running; lines_drawn += 1; } if !all_running { players.clear(); } println!("\n(Refreshing running state every second)"); lines_drawn += 2; sleep(Duration::from_secs(1)); } } mpris-2.0.1/examples/events.rs000064400000000000000000000020211046102023000144410ustar 00000000000000use mpris::PlayerFinder; use std::time::{Duration, Instant}; fn main() { let player = PlayerFinder::new() .expect("Could not connect to D-Bus") .find_active() .expect("Could not find active player"); println!( "Showing event stream for player {}...\n(Exit with Ctrl-C)\n", player.identity() ); let events = player.events().expect("Could not start event stream"); let start = Instant::now(); for event in events { match event { Ok(event) => println!("{}: {:#?}", format_elapsed(start.elapsed()), event), Err(err) => { println!("D-Bus error: {}. Aborting.", err); break; } } } println!("Event stream ended."); } fn format_elapsed(duration: Duration) -> String { let seconds = duration.as_secs(); let minutes = seconds / 60; let seconds_left = seconds - (60 * minutes); let ms = duration.subsec_millis(); format!("{:02}:{:02}.{:3}", minutes, seconds_left, ms) } mpris-2.0.1/examples/get_metadata.rs000064400000000000000000000016731046102023000155700ustar 00000000000000use anyhow::{Context, Result}; use mpris::PlayerFinder; fn main() { match print_metadata() { Ok(_) => {} Err(error) => { println!("Error: {}", error); for (i, cause) in error.chain().skip(1).enumerate() { print!("{}", " ".repeat(i + 1)); println!("Caused by: {}", cause); } std::process::exit(1); } } } fn print_metadata() -> Result<()> { let player_finder = PlayerFinder::new().context("Could not connect to D-Bus")?; let player = player_finder .find_active() .context("Could not find any player")?; println!( "Found {identity} (on bus {bus_name})", bus_name = player.bus_name(), identity = player.identity(), ); let metadata = player .get_metadata() .context("Could not get metadata for player")?; println!("Metadata:\n{:#?}\n", metadata); Ok(()) } mpris-2.0.1/examples/play_pause.rs000064400000000000000000000031701046102023000153050ustar 00000000000000use mpris::{PlaybackStatus, PlayerFinder}; fn main() { match play_pause() { Ok(playback_status) => match playback_status { PlaybackStatus::Playing => println!("Player is now playing."), PlaybackStatus::Paused => println!("Player is now paused."), PlaybackStatus::Stopped => println!("Player is stopped."), }, Err(error) => { println!("ERROR: {}", error); std::process::exit(1); } } } fn play_pause() -> Result { let player_finder = PlayerFinder::new().map_err(|e| format!("Could not connect to D-Bus: {}", e))?; let player = player_finder .find_active() .map_err(|e| format!("Could not find any player: {}", e))?; let toggled = player .checked_play_pause() .map_err(|e| format!("Could not control player: {}", e))?; if toggled { // Give the player some time to respond to the message and update its properties. The // play_pause() call will wait for a reply, but the player might not update the properties // before replying. std::thread::sleep(std::time::Duration::from_millis(50)); player .get_playback_status() .map_err(|e| format!("Could not get playback status: {}", e)) } else { // Could not toggle play/pause status. This happens when the media cannot be paused, which // could be because of any number of reasons including: // - No media is playing // - Media is streaming and does not allow pause Err(String::from("Media cannot be paused")) } } mpris-2.0.1/examples/progress_tracker.rs000064400000000000000000000046351046102023000165310ustar 00000000000000use std::io::{stdout, Write}; use std::time::Duration; use mpris::{LoopStatus, Metadata, PlaybackStatus, PlayerFinder, Progress, ProgressTick}; fn reset_line() { print!("\r\x1b[K"); } fn print_duration(duration: Duration) { let secs = duration.as_secs(); let whole_hours = secs / (60 * 60); let secs = secs - whole_hours * 60 * 60; let whole_minutes = secs / 60; let secs = secs - whole_minutes * 60; print!("{:02}:{:02}:{:02}", whole_hours, whole_minutes, secs) } fn print_time(duration: Option) { match duration { Some(duration) => print_duration(duration), None => print!("??:??:??"), } } fn print_artist(metadata: &Metadata) { if let Some(artists) = metadata.artists() { if !artists.is_empty() { print!("{}", artists.join(" + ")); return; } } print!("Unknown artist"); } fn print_title(metadata: &Metadata) { print!("{}", metadata.title().unwrap_or("Unknown title")); } fn print_playback_status(progress: &Progress) { match progress.playback_status() { PlaybackStatus::Playing => print!("▶"), PlaybackStatus::Paused => print!("▮▮"), PlaybackStatus::Stopped => print!("◼"), } } fn print_shuffle_status(progress: &Progress) { if progress.shuffle() { print!("🔀"); } else { print!(" "); } } fn print_loop_status(progress: &Progress) { match progress.loop_status() { LoopStatus::None => print!(" "), LoopStatus::Track => print!("🔂"), LoopStatus::Playlist => print!("🔁"), } } fn main() { let player = PlayerFinder::new().unwrap().find_active().unwrap(); let identity = player.identity(); let mut progress_tracker = player.track_progress(100).unwrap(); loop { let ProgressTick { progress, .. } = progress_tracker.tick(); reset_line(); print_playback_status(progress); print_shuffle_status(progress); print_loop_status(progress); print!("\t"); print_artist(progress.metadata()); print!(" - "); print_title(progress.metadata()); print!(" ["); if identity != "Spotify" { print_time(Some(progress.position())); } else { print_time(None); } print!(" / "); print_time(progress.length()); print!("] ({})", identity); stdout().flush().unwrap(); } } mpris-2.0.1/examples/show_tracklist.rs000064400000000000000000000025111046102023000162010ustar 00000000000000use anyhow::{Context, Result}; use mpris::PlayerFinder; fn main() { match print_track_list() { Ok(_) => {} Err(error) => { println!("Error: {}", error); for (i, cause) in error.chain().skip(1).enumerate() { print!("{}", " ".repeat(i + 1)); println!("Caused by: {}", cause); } std::process::exit(1); } } } fn print_track_list() -> Result<()> { let player_finder = PlayerFinder::new().context("Could not connect to D-Bus")?; let player = player_finder .find_active() .context("Could not find any player")?; println!( "Found {identity} (on bus {bus_name})", bus_name = player.bus_name(), identity = player.identity(), ); let track_list = player .checked_get_track_list() .context("Could not get track list for player")?; let track_list = match track_list { Some(tracks) => tracks, None => { println!("Player does not support the TrackList interface."); return Ok(()); } }; println!("Track list:\n"); let iter = track_list .metadata_iter(&player) .context("Could not load metadata for tracks")?; for metadata in iter { println!("{:#?}", metadata); } Ok(()) } mpris-2.0.1/examples/tracklist_control.rs000064400000000000000000000103331046102023000167020ustar 00000000000000use anyhow::{anyhow, Context, Error, Result}; use mpris::{Player, PlayerFinder, TrackID}; fn main() { match run() { Ok(_) => {} Err(error) => { println!("Error: {}", error); for (i, cause) in error.chain().skip(1).enumerate() { print!("{}", " ".repeat(i + 1)); println!("Caused by: {}", cause); } std::process::exit(1); } } } fn prompt_string(message: &str) -> Result { use std::io::stdin; let mut answer = String::new(); println!("{}", message); stdin().read_line(&mut answer)?; Ok(String::from(answer.trim())) } fn run() -> Result<()> { let player_finder = PlayerFinder::new().context("Could not connect to D-Bus")?; let player = player_finder .find_active() .context("Could not find any player")?; println!( "Found {identity} (on bus {bus_name})", bus_name = player.bus_name(), identity = player.identity(), ); if !player.supports_track_lists() { println!("Player does not support TrackList"); return Ok(()); } loop { let answer = prompt_string("What to do? [q]uit, [g]oto, [l]ist, [a]dd, [r]emove >")?; match answer.as_str() { "q" | "Q" => break, "l" | "L" => print_track_list(&player).context("Failed to list tracks")?, "g" | "G" => goto_track(&player).context("Failed to change track")?, "a" | "A" => add_track(&player).context("Failed to add track")?, "r" | "R" => remove_track(&player).context("Failed to remove track")?, _ => println!("I don't understand \"{}\"", answer), } } Ok(()) } fn print_track_list(player: &Player) -> Result<()> { let track_list = player.get_track_list()?; println!("Track list:\n"); let iter = track_list .metadata_iter(player) .context("Could not load metadata for tracks")?; for (index, metadata) in iter.enumerate() { let title = metadata.title().unwrap_or("Unknown title"); let artist = metadata .artists() .map(|list| list.join(", ")) .unwrap_or_else(|| "Unknown artist".into()); println!("{}. {} - {}", index + 1, artist, title); } Ok(()) } fn select_track(player: &Player, lower_bound: usize) -> Result> { let track_list = player .get_track_list() .context("Could not get track list for player")?; let len = track_list.len(); let answer = prompt_string(&format!( "Select track index [{}-{}, q] > ", lower_bound, len ))?; if answer.is_empty() || answer == "q" { return Ok(None); } let number: usize = answer.parse::().context("Not a valid number")?; if number == 0 { return Ok(None); } let track_id = track_list .get(number - 1) .ok_or_else(|| anyhow!("Not a valid position"))?; Ok(Some(track_id.clone())) } fn goto_track(player: &Player) -> Result<()> { match select_track(player, 1) { Ok(Some(track_id)) => player.go_to(&track_id).map_err(Error::from), Ok(None) => Ok(()), Err(err) => Err(err.context("Failed to select track")), } } fn remove_track(player: &Player) -> Result<()> { match select_track(player, 1) { Ok(Some(track_id)) => player.remove_track(&track_id).map_err(Error::from), Ok(None) => Ok(()), Err(err) => Err(err.context("Failed to select track")), } } fn add_track(player: &Player) -> Result<()> { println!("NOTE: To add local media, start with the \"file://\" protocol. E.x. \"file:///path/to/file.mp3\""); let uri = prompt_string("Enter URI (or nothing to cancel) > ")?; if uri.is_empty() { return Ok(()); } println!( "Will be inserted after selected track. Select no track (0) to insert at the beginning." ); match select_track(player, 0) { Ok(Some(track_id)) => player .add_track(&uri, &track_id, false) .map_err(Error::from), Ok(None) => player.add_track_at_start(&uri, false).map_err(Error::from), Err(err) => Err(err) .context("Failed to select track") .map_err(Error::from), } } mpris-2.0.1/script/ci.sh000075500000000000000000000006101046102023000132110ustar 00000000000000#!/bin/sh # Run this on CI, inside a X11 instance. set -ex export RUST_BACKTRACE=1 if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then echo "Starting dbus" # dbus-launch will quote values itself, so quoting the string will actually # not set values correctly. # shellcheck disable=SC2046 export $(dbus-launch) fi cargo build --verbose && cargo test --verbose && cargo doc --no-deps mpris-2.0.1/script/generate-mpris-interface.sh000075500000000000000000000023641046102023000175060ustar 00000000000000#!/usr/bin/env bash # Regenerates the MPRIS interface code using `dbus-codegen-rust`. set -e root="$(readlink -f "$(dirname "$0")/..")" if [[ ! -d "$root" ]]; then echo "Could not find root $root" exit 1 fi if ! hash dbus-codegen-rust 2> /dev/null; then echo "Could not find dbus-codegen-rust binary. Do you want to install it using Cargo?" echo -n "[Yn] > " read -r c if [[ $c == "y" || $c == "Y" ]]; then cargo install dbus-codegen else exit 1 fi fi dest="$root/src/generated" for spec in "$root"/mpris-spec/spec/org.mpris.*.xml; do basename=$( basename "$spec" | \ sed -r 's/org\.mpris\.MediaPlayer2(\.(\w+))?\.xml/media_player_\2.rs/; s/_\.rs$/\.rs/' | \ tr '[:upper:]' '[:lower:]' ) dest_file="${dest}/${basename}" echo "Generating code from $(basename "${spec}") to ${basename}…" cat < "$dest_file" #![allow(unknown_lints)] #![allow(clippy::all)] #![allow( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_imports )] EOF dbus-codegen-rust -m None -c ffidisp < "$spec" >> "$dest_file" rustfmt ${dest_file} done echo "Done." mpris-2.0.1/src/event.rs000064400000000000000000000247051046102023000132440ustar 00000000000000use super::{ DBusError, LoopStatus, Metadata, PlaybackStatus, Player, Progress, TrackID, TrackList, TrackListError, }; use crate::pooled_connection::MprisEvent; use thiserror::Error; /// Represents a change in [`Player`] state. /// /// Note that this does not include position changes (seeking in a track or normal progress of time /// for playing media). #[derive(Debug)] pub enum Event { /// [`Player`] was shut down / quit. PlayerShutDown, /// [`Player`] was paused. Paused, /// [`Player`] started playing media. Playing, /// [`Player`] was stopped. Stopped, /// Loop status of [`Player`] was changed. New [`LoopStatus`] is provided. LoopingChanged(LoopStatus), /// Shuffle status of [`Player`] was changed. New shuffle status is provided. ShuffleToggled(bool), /// [`Player`]'s volume was changed. The new volume is provided. VolumeChanged(f64), /// [`Player`]'s playback rate was changed. New playback rate is provided. PlaybackRateChanged(f64), /// [`Player`]'s track changed. [`Metadata`] of the new track is provided. TrackChanged(Metadata), /// [`Player`] seeked (changed position in the current track). /// /// This will only be emitted when the player in question emits this signal. Some players do /// not support this signal. If you want to accurately detect seeking, you'll have to query /// the player's position yourself at some intervals. Seeked { /// The new position, in microseconds. position_in_us: u64, }, /// A new track was added to the [`TrackList`]. TrackAdded(TrackID), /// A track was removed from the [`TrackList`]. TrackRemoved(TrackID), /// A track on the [`TrackList`] had its metadata changed. /// /// This could also mean that a entry on the playlist completely changed; including the ID. TrackMetadataChanged { /// The id of the track *before* the change. /// /// Only use this ID if you are keeping track of track IDs somewhere. The ID might no /// longer be valid for the player, so loading metadata for it might fail. /// /// **Note:** This can be the same as the `new_id`. old_id: TrackID, /// The id of the track *after* the change. /// /// Use this ID if you intend to read metadata or anything else as the `old_id` may no /// longer be valid. /// /// **Note:** This can be the same as the `old_id`. new_id: TrackID, }, /// The track list was replaced. TrackListReplaced, } /// Errors that can occur while processing event streams. #[derive(Debug, Error)] pub enum EventError { /// Something went wrong with the D-Bus communication. See the [`DBusError`] type. #[error("D-Bus communication failed: {0}")] DBusError(#[from] DBusError), /// Something went wrong with the track list. See the [`TrackListError`] type. #[error("TrackList could not be refreshed: {0}")] TrackListError(#[from] TrackListError), } /// Iterator that blocks forever until the player has an [`Event`]. /// /// Iteration will stop if player stops running. If the player was running before this iterator /// blocks, one last [`Event::PlayerShutDown`] event will be emitted before stopping iteration. /// /// If multiple events are found between processing D-Bus events then all of them will be iterated /// in rapid succession before processing more events. #[derive(Debug)] pub struct PlayerEvents<'a> { /// [`Player`] to watch. player: &'a Player, /// Queued up events found after the last signal. buffer: Vec, /// Used to diff older state to find events. last_progress: Progress, /// Current tracklist of the player. Will be kept up to date. track_list: Option, } impl PlayerEvents<'_> { pub(crate) fn new(player: &Player) -> Result { let progress = Progress::from_player(player)?; Ok(PlayerEvents { player, buffer: Vec::new(), last_progress: progress, track_list: player.checked_get_track_list()?, }) } /// Current tracklist of the player. Will be kept up to date. pub fn track_list(&self) -> Option<&TrackList> { self.track_list.as_ref() } fn read_events(&mut self) -> Result<(), EventError> { self.player.process_events_blocking_until_received(); let mut new_progress: Option = None; let mut reload_track_list = false; for event in self.player.pending_events().into_iter() { match event { MprisEvent::PlayerQuit => { self.buffer.push(Event::PlayerShutDown); return Ok(()); } MprisEvent::PlayerPropertiesChanged => { if new_progress.is_none() { new_progress = Some(Progress::from_player(self.player)?); } } MprisEvent::Seeked { position_in_us } => { self.buffer.push(Event::Seeked { position_in_us }) } MprisEvent::TrackListPropertiesChanged => { reload_track_list = true; } MprisEvent::TrackListReplaced { ids } => { if let Some(ref mut list) = self.track_list { list.replace(ids.into_iter().map(TrackID::from).collect()); } self.buffer.push(Event::TrackListReplaced); } MprisEvent::TrackAdded { after_id, metadata } => { if let Some(id) = metadata.track_id() { if let Some(ref mut list) = self.track_list { list.insert(&after_id, metadata); } self.buffer.push(Event::TrackAdded(id)); } } MprisEvent::TrackRemoved { id } => { if let Some(ref mut list) = self.track_list { list.remove(&id); } self.buffer.push(Event::TrackRemoved(id)); } MprisEvent::TrackMetadataChanged { old_id, metadata } => { if let Some(ref mut list) = self.track_list { if let Some(new_id) = list.replace_track_metadata(&old_id, metadata) { self.buffer .push(Event::TrackMetadataChanged { old_id, new_id }); } } } } } if let Some(progress) = new_progress { self.detect_playback_status_events(&progress); self.detect_loop_status_events(&progress); reload_track_list |= self.detect_shuffle_events(&progress); self.detect_volume_events(&progress); self.detect_playback_rate_events(&progress); self.detect_metadata_events(&progress); self.last_progress = progress; } if reload_track_list && self.track_list.is_some() { if let Some(new_tracks) = self.player.checked_get_track_list()? { match self.track_list { Some(ref mut list) => list.replace(new_tracks), None => self.track_list = Some(new_tracks), } self.buffer.push(Event::TrackListReplaced); } } Ok(()) } fn detect_playback_status_events(&mut self, new_progress: &Progress) { match new_progress.playback_status() { status if self.last_progress.playback_status() == status => {} PlaybackStatus::Playing => self.buffer.push(Event::Playing), PlaybackStatus::Paused => self.buffer.push(Event::Paused), PlaybackStatus::Stopped => self.buffer.push(Event::Stopped), } } fn detect_loop_status_events(&mut self, new_progress: &Progress) { let loop_status = new_progress.loop_status(); if self.last_progress.loop_status() != loop_status { self.buffer.push(Event::LoopingChanged(loop_status)); } } fn detect_shuffle_events(&mut self, new_progress: &Progress) -> bool { let status = new_progress.shuffle(); if self.last_progress.shuffle() != status { self.buffer.push(Event::ShuffleToggled(status)); true } else { false } } fn detect_volume_events(&mut self, new_progress: &Progress) { let volume = new_progress.current_volume(); if is_different_float(self.last_progress.current_volume(), volume) { self.buffer.push(Event::VolumeChanged(volume)); } } fn detect_playback_rate_events(&mut self, new_progress: &Progress) { let rate = new_progress.playback_rate(); if is_different_float(self.last_progress.playback_rate(), rate) { self.buffer.push(Event::PlaybackRateChanged(rate)); } } fn detect_metadata_events(&mut self, new_progress: &Progress) { let new_metadata = new_progress.metadata(); let old_metadata = self.last_progress.metadata(); // As a workaround for Players not setting a valid track ID, we also check against the URL // Title and artists are checked to detect changes for streams (radios) because track ID and URL don't change. // Title is checked first because most radios set title to `Artist - Title` and have the station name in artists. if old_metadata.track_id() != new_metadata.track_id() || old_metadata.url() != new_metadata.url() || old_metadata.title() != new_metadata.title() || old_metadata.artists() != new_metadata.artists() { self.buffer.push(Event::TrackChanged(new_metadata.clone())); } } } fn is_different_float(a: f64, b: f64) -> bool { (a - b).abs() >= ::std::f64::EPSILON } impl<'a> Iterator for PlayerEvents<'a> { type Item = Result; fn next(&mut self) -> Option { while self.buffer.is_empty() { // Stop iteration when player is not running. Why beat a dead horse? if !self.player.is_running() { return None; } match self.read_events() { Ok(_) => {} Err(err) => return Some(Err(err)), }; } let event = self.buffer.remove(0); Some(Ok(event)) } } mpris-2.0.1/src/extensions.rs000064400000000000000000000025301046102023000143120ustar 00000000000000use std::time::Duration; pub(crate) trait DurationExtensions { // Rust beta has a from_micros function that is unstable. fn from_micros_ext(_: u64) -> Duration; fn as_millis(&self) -> u64; fn as_micros(&self) -> u64; } impl DurationExtensions for Duration { fn from_micros_ext(micros: u64) -> Duration { let whole_seconds = micros / 1_000_000; let rest = (micros - (whole_seconds * 1_000_000)) as u32; Duration::new(whole_seconds, rest * 1000) } fn as_millis(&self) -> u64 { self.as_secs() * 1000 + u64::from(self.subsec_millis()) } fn as_micros(&self) -> u64 { self.as_secs() * 1000 * 1000 + u64::from(self.subsec_micros()) } } #[cfg(test)] mod test { use super::*; #[test] fn it_constructs_durations_from_micros() { let expected = Duration::new(5, 543_210_000); let actual = Duration::from_micros_ext(5_543_210); assert_eq!(actual, expected); } #[test] fn it_calculates_whole_millis_from_durations() { let duration = Duration::new(5, 543_210_000); assert_eq!(DurationExtensions::as_millis(&duration), 5543); } #[test] fn it_calculates_whole_micros_from_durations() { let duration = Duration::new(5, 543_210_000); assert_eq!(DurationExtensions::as_micros(&duration), 5_543_210); } } mpris-2.0.1/src/find.rs000064400000000000000000000202541046102023000130360ustar 00000000000000use thiserror::Error; use std::iter::FusedIterator; use std::rc::Rc; use dbus::ffidisp::{BusType, Connection}; use dbus::{arg, Message}; use super::DBusError; use crate::player::{Player, DEFAULT_TIMEOUT_MS, MPRIS2_PREFIX}; use crate::pooled_connection::PooledConnection; use crate::PlaybackStatus; const LIST_NAMES_TIMEOUT_MS: i32 = 500; /// This enum encodes possible error cases that could happen when finding players. #[derive(Debug, Error)] pub enum FindingError { /// No player was found matching the requirements of the calling method. #[error("No player found")] NoPlayerFound, /// Finding failed due to an underlying [`DBusError`]. #[error("{0}")] DBusError(#[from] DBusError), } impl From for FindingError { fn from(error: dbus::Error) -> Self { FindingError::DBusError(error.into()) } } /// Used to find [`Player`]s running on a D-Bus connection. /// /// All find results are sorted in alphabetical order. #[derive(Debug)] pub struct PlayerFinder { connection: Rc, player_timeout_ms: i32, } impl PlayerFinder { /// Creates a new [`PlayerFinder`] with a new default D-Bus connection. /// /// Use [`for_connection`](Self::for_connection) if you want to provide the D-Bus connection yourself. pub fn new() -> Result { Ok(PlayerFinder::for_connection(Connection::get_private( BusType::Session, )?)) } /// Create a new [`PlayerFinder`] with the given connection. /// /// Use [`new`](Self::new) if you want a new default connection rather than manually managing the D-Bus /// connection. pub fn for_connection(connection: Connection) -> Self { PlayerFinder { connection: Rc::new(connection.into()), player_timeout_ms: DEFAULT_TIMEOUT_MS, } } /// Get the current timeout value that all [`Player`]s created through this finder will inherit /// /// Can be set with [`set_player_timeout_ms`][Self::set_player_timeout_ms] pub fn player_timeout_ms(&self) -> i32 { self.player_timeout_ms } /// Set the timeout value that all [`Player`]s created through this finder will inherit pub fn set_player_timeout_ms(&mut self, timeout_ms: i32) { self.player_timeout_ms = timeout_ms; } /// Find all available [`Player`]s in the connection. /// /// Will return an empty [`Vec`] and not [`NoPlayerFound`](FindingError::NoPlayerFound) if there are no players. pub fn find_all(&self) -> Result, FindingError> { self.iter_players()? .map(|x| x.map_err(FindingError::from)) .collect() } /// Return the first found [`Player`] regardless of state. pub fn find_first(&self) -> Result { if let Some(player) = self.iter_players()?.next() { player.map_err(FindingError::from) } else { Err(FindingError::NoPlayerFound) } } /// Try to find the "active" [`Player`] in the connection. /// /// This method will try to determine which player a user is most likely to use. First it will look for a player with /// the playback status [`Playing`](PlaybackStatus::Playing), then for a [`Paused`](PlaybackStatus::Paused), then one with /// track metadata, after that it will just return the first it finds. [`NoPlayerFound`](FindingError::NoPlayerFound) is returned /// only if there is no player on the DBus. pub fn find_active(&self) -> Result { let players: PlayerIter = self.iter_players()?; match self.find_active_player(players)? { Some(player) => Ok(player), None => Err(FindingError::NoPlayerFound), } } /// Finds the index of an "active" player. Follows the order mentioned in [`find_active`](Self::find_active). fn find_active_player(&self, players: PlayerIter) -> Result, DBusError> { if players.len() == 0 { return Ok(None); } let mut first_paused: Option = None; let mut first_with_track: Option = None; let mut first_found: Option = None; for player in players { let player = player?; let player_status = player.get_playback_status()?; if player_status == PlaybackStatus::Playing { return Ok(Some(player)); } if first_paused.is_none() && player_status == PlaybackStatus::Paused { first_paused.replace(player); } else if first_with_track.is_none() && !player.get_metadata()?.is_empty() { first_with_track.replace(player); } else if first_found.is_none() { first_found.replace(player); } } Ok(first_paused.or(first_with_track).or(first_found)) } /// Find a [`Player`] by it's MPRIS [`Identity`][identity]. Returns [`NoPlayerFound`](FindingError::NoPlayerFound) if no direct match found. /// /// [identity]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity pub fn find_by_name(&self, name: &str) -> Result { for player_result in self.iter_players()? { let player = player_result?; if player.identity().to_lowercase() == name.to_lowercase() { return Ok(player); } } Err(FindingError::NoPlayerFound) } /// Returns all of the MPRIS DBus paths fn all_player_buses(&self) -> Result, DBusError> { let list_names = Message::new_method_call( "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "ListNames", ) .unwrap(); let reply = self .connection .underlying() .send_with_reply_and_block(list_names, LIST_NAMES_TIMEOUT_MS)?; let names: arg::Array<'_, &str, _> = reply.read1().map_err(DBusError::from)?; let mut all_busses = names .filter(|name| name.starts_with(MPRIS2_PREFIX)) .map(|str_ref| str_ref.to_owned()) .collect::>(); all_busses.sort_by_key(|a| a.to_lowercase()); Ok(all_busses) } /// Returns a [`PlayerIter`] iterator, or an [`DBusError`] if there was a problem with the D-Bus /// /// For more details see [`PlayerIter`] documentation pub fn iter_players(&self) -> Result { let buses = self.all_player_buses()?; Ok(PlayerIter::new( buses, self.connection.clone(), self.player_timeout_ms, )) } } /// An iterator that lazily iterates over all of the found [`Player`]s. Useful for efficiently searching for a specific player. /// /// Created by calling [`PlayerFinder::iter_players`] /// /// Note that this iterator will not keep checking what players are connected after it's been created. A player might quit or /// a new one might connect at a later time, this will result in an error or the player not being present respectively. /// If you want to make sure the data is "fresh" you'll either have to make a new PlayerIter whenever you want to get new data or /// use [`PlayerFinder::find_all`] which will immediately return a [`Vec`] with all the [`Player`]s that were connected at that point. #[derive(Debug)] pub struct PlayerIter { buses: std::vec::IntoIter, connection: Rc, timeout_ms: i32, } impl PlayerIter { fn new(buses: Vec, connection: Rc, timeout_ms: i32) -> Self { Self { buses: buses.into_iter(), connection, timeout_ms, } } } impl Iterator for PlayerIter { type Item = Result; fn next(&mut self) -> Option { let bus = self.buses.next()?; Some(Player::for_pooled_connection( self.connection.clone(), bus, self.timeout_ms, )) } fn size_hint(&self) -> (usize, Option) { let size = self.buses.len(); (size, Some(size)) } } impl ExactSizeIterator for PlayerIter {} impl FusedIterator for PlayerIter {} mpris-2.0.1/src/generated/media_player.rs000064400000000000000000000076211046102023000165120ustar 00000000000000#![allow(unknown_lints)] #![allow(clippy::all)] #![allow( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_imports )] // This code was autogenerated with `dbus-codegen-rust -m None -c ffidisp`, see https://github.com/diwic/dbus-rs use dbus; #[allow(unused_imports)] use dbus::arg; use dbus::ffidisp; pub trait OrgMprisMediaPlayer2 { fn raise(&self) -> Result<(), dbus::Error>; fn quit(&self) -> Result<(), dbus::Error>; fn can_quit(&self) -> Result; fn fullscreen(&self) -> Result; fn set_fullscreen(&self, value: bool) -> Result<(), dbus::Error>; fn can_set_fullscreen(&self) -> Result; fn can_raise(&self) -> Result; fn has_track_list(&self) -> Result; fn identity(&self) -> Result; fn desktop_entry(&self) -> Result; fn supported_uri_schemes(&self) -> Result, dbus::Error>; fn supported_mime_types(&self) -> Result, dbus::Error>; } impl<'a, C: ::std::ops::Deref> OrgMprisMediaPlayer2 for ffidisp::ConnPath<'a, C> { fn raise(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2", "Raise", ()) } fn quit(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2", "Quit", ()) } fn can_quit(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "CanQuit", ) } fn fullscreen(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "Fullscreen", ) } fn can_set_fullscreen(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "CanSetFullscreen", ) } fn can_raise(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "CanRaise", ) } fn has_track_list(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "HasTrackList", ) } fn identity(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "Identity", ) } fn desktop_entry(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2", "DesktopEntry", ) } fn supported_uri_schemes(&self) -> Result, dbus::Error> { ::get( &self, "org.mpris.MediaPlayer2", "SupportedUriSchemes", ) } fn supported_mime_types(&self) -> Result, dbus::Error> { ::get( &self, "org.mpris.MediaPlayer2", "SupportedMimeTypes", ) } fn set_fullscreen(&self, value: bool) -> Result<(), dbus::Error> { ::set( &self, "org.mpris.MediaPlayer2", "Fullscreen", value, ) } } mpris-2.0.1/src/generated/media_player_player.rs000064400000000000000000000211731046102023000200640ustar 00000000000000#![allow(unknown_lints)] #![allow(clippy::all)] #![allow( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_imports )] // This code was autogenerated with `dbus-codegen-rust -m None -c ffidisp`, see https://github.com/diwic/dbus-rs use dbus; #[allow(unused_imports)] use dbus::arg; use dbus::ffidisp; pub trait OrgMprisMediaPlayer2Player { fn next(&self) -> Result<(), dbus::Error>; fn previous(&self) -> Result<(), dbus::Error>; fn pause(&self) -> Result<(), dbus::Error>; fn play_pause(&self) -> Result<(), dbus::Error>; fn stop(&self) -> Result<(), dbus::Error>; fn play(&self) -> Result<(), dbus::Error>; fn seek(&self, offset: i64) -> Result<(), dbus::Error>; fn set_position(&self, track_id: dbus::Path, position: i64) -> Result<(), dbus::Error>; fn open_uri(&self, uri: &str) -> Result<(), dbus::Error>; fn playback_status(&self) -> Result; fn loop_status(&self) -> Result; fn set_loop_status(&self, value: String) -> Result<(), dbus::Error>; fn rate(&self) -> Result; fn set_rate(&self, value: f64) -> Result<(), dbus::Error>; fn shuffle(&self) -> Result; fn set_shuffle(&self, value: bool) -> Result<(), dbus::Error>; fn metadata(&self) -> Result; fn volume(&self) -> Result; fn set_volume(&self, value: f64) -> Result<(), dbus::Error>; fn position(&self) -> Result; fn minimum_rate(&self) -> Result; fn maximum_rate(&self) -> Result; fn can_go_next(&self) -> Result; fn can_go_previous(&self) -> Result; fn can_play(&self) -> Result; fn can_pause(&self) -> Result; fn can_seek(&self) -> Result; fn can_control(&self) -> Result; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2PlayerSeeked { pub position: i64, } impl arg::AppendAll for OrgMprisMediaPlayer2PlayerSeeked { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.position, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2PlayerSeeked { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2PlayerSeeked { position: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2PlayerSeeked { const NAME: &'static str = "Seeked"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.Player"; } impl<'a, C: ::std::ops::Deref> OrgMprisMediaPlayer2Player for ffidisp::ConnPath<'a, C> { fn next(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Next", ()) } fn previous(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Previous", ()) } fn pause(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Pause", ()) } fn play_pause(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "PlayPause", ()) } fn stop(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Stop", ()) } fn play(&self) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Play", ()) } fn seek(&self, offset: i64) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "Seek", (offset,)) } fn set_position(&self, track_id: dbus::Path, position: i64) -> Result<(), dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.Player", "SetPosition", (track_id, position), ) } fn open_uri(&self, uri: &str) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.Player", "OpenUri", (uri,)) } fn playback_status(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "PlaybackStatus", ) } fn loop_status(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "LoopStatus", ) } fn rate(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "Rate", ) } fn shuffle(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "Shuffle", ) } fn metadata(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "Metadata", ) } fn volume(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "Volume", ) } fn position(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "Position", ) } fn minimum_rate(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "MinimumRate", ) } fn maximum_rate(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "MaximumRate", ) } fn can_go_next(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanGoNext", ) } fn can_go_previous(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanGoPrevious", ) } fn can_play(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanPlay", ) } fn can_pause(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanPause", ) } fn can_seek(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanSeek", ) } fn can_control(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Player", "CanControl", ) } fn set_loop_status(&self, value: String) -> Result<(), dbus::Error> { ::set( &self, "org.mpris.MediaPlayer2.Player", "LoopStatus", value, ) } fn set_rate(&self, value: f64) -> Result<(), dbus::Error> { ::set( &self, "org.mpris.MediaPlayer2.Player", "Rate", value, ) } fn set_shuffle(&self, value: bool) -> Result<(), dbus::Error> { ::set( &self, "org.mpris.MediaPlayer2.Player", "Shuffle", value, ) } fn set_volume(&self, value: f64) -> Result<(), dbus::Error> { ::set( &self, "org.mpris.MediaPlayer2.Player", "Volume", value, ) } } mpris-2.0.1/src/generated/media_player_playlists.rs000064400000000000000000000065411046102023000206160ustar 00000000000000#![allow(unknown_lints)] #![allow(clippy::all)] #![allow( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_imports )] // This code was autogenerated with `dbus-codegen-rust -m None -c ffidisp`, see https://github.com/diwic/dbus-rs use dbus; #[allow(unused_imports)] use dbus::arg; use dbus::ffidisp; pub trait OrgMprisMediaPlayer2Playlists { fn activate_playlist(&self, playlist_id: dbus::Path) -> Result<(), dbus::Error>; fn get_playlists( &self, index: u32, max_count: u32, order: &str, reverse_order: bool, ) -> Result, String, String)>, dbus::Error>; fn playlist_count(&self) -> Result; fn orderings(&self) -> Result, dbus::Error>; fn active_playlist(&self) -> Result<(bool, (dbus::Path<'static>, String, String)), dbus::Error>; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2PlaylistsPlaylistChanged { pub playlist: (dbus::Path<'static>, String, String), } impl arg::AppendAll for OrgMprisMediaPlayer2PlaylistsPlaylistChanged { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.playlist, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2PlaylistsPlaylistChanged { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2PlaylistsPlaylistChanged { playlist: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2PlaylistsPlaylistChanged { const NAME: &'static str = "PlaylistChanged"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.Playlists"; } impl<'a, C: ::std::ops::Deref> OrgMprisMediaPlayer2Playlists for ffidisp::ConnPath<'a, C> { fn activate_playlist(&self, playlist_id: dbus::Path) -> Result<(), dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.Playlists", "ActivatePlaylist", (playlist_id,), ) } fn get_playlists( &self, index: u32, max_count: u32, order: &str, reverse_order: bool, ) -> Result, String, String)>, dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.Playlists", "GetPlaylists", (index, max_count, order, reverse_order), ) .and_then(|r: (Vec<(dbus::Path<'static>, String, String)>,)| Ok(r.0)) } fn playlist_count(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.Playlists", "PlaylistCount", ) } fn orderings(&self) -> Result, dbus::Error> { ::get( &self, "org.mpris.MediaPlayer2.Playlists", "Orderings", ) } fn active_playlist( &self, ) -> Result<(bool, (dbus::Path<'static>, String, String)), dbus::Error> { ::get( &self, "org.mpris.MediaPlayer2.Playlists", "ActivePlaylist", ) } } mpris-2.0.1/src/generated/media_player_tracklist.rs000064400000000000000000000137251046102023000205740ustar 00000000000000#![allow(unknown_lints)] #![allow(clippy::all)] #![allow( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, unused_qualifications, unused_imports )] // This code was autogenerated with `dbus-codegen-rust -m None -c ffidisp`, see https://github.com/diwic/dbus-rs use dbus; #[allow(unused_imports)] use dbus::arg; use dbus::ffidisp; pub trait OrgMprisMediaPlayer2TrackList { fn get_tracks_metadata( &self, track_ids: Vec, ) -> Result, dbus::Error>; fn add_track( &self, uri: &str, after_track: dbus::Path, set_as_current: bool, ) -> Result<(), dbus::Error>; fn remove_track(&self, track_id: dbus::Path) -> Result<(), dbus::Error>; fn go_to(&self, track_id: dbus::Path) -> Result<(), dbus::Error>; fn tracks(&self) -> Result>, dbus::Error>; fn can_edit_tracks(&self) -> Result; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2TrackListTrackListReplaced { pub tracks: Vec>, pub current_track: dbus::Path<'static>, } impl arg::AppendAll for OrgMprisMediaPlayer2TrackListTrackListReplaced { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.tracks, i); arg::RefArg::append(&self.current_track, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2TrackListTrackListReplaced { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2TrackListTrackListReplaced { tracks: i.read()?, current_track: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2TrackListTrackListReplaced { const NAME: &'static str = "TrackListReplaced"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.TrackList"; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2TrackListTrackAdded { pub metadata: arg::PropMap, pub after_track: dbus::Path<'static>, } impl arg::AppendAll for OrgMprisMediaPlayer2TrackListTrackAdded { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.metadata, i); arg::RefArg::append(&self.after_track, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2TrackListTrackAdded { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2TrackListTrackAdded { metadata: i.read()?, after_track: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2TrackListTrackAdded { const NAME: &'static str = "TrackAdded"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.TrackList"; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2TrackListTrackRemoved { pub track_id: dbus::Path<'static>, } impl arg::AppendAll for OrgMprisMediaPlayer2TrackListTrackRemoved { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.track_id, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2TrackListTrackRemoved { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2TrackListTrackRemoved { track_id: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2TrackListTrackRemoved { const NAME: &'static str = "TrackRemoved"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.TrackList"; } #[derive(Debug)] pub struct OrgMprisMediaPlayer2TrackListTrackMetadataChanged { pub track_id: dbus::Path<'static>, pub metadata: arg::PropMap, } impl arg::AppendAll for OrgMprisMediaPlayer2TrackListTrackMetadataChanged { fn append(&self, i: &mut arg::IterAppend) { arg::RefArg::append(&self.track_id, i); arg::RefArg::append(&self.metadata, i); } } impl arg::ReadAll for OrgMprisMediaPlayer2TrackListTrackMetadataChanged { fn read(i: &mut arg::Iter) -> Result { Ok(OrgMprisMediaPlayer2TrackListTrackMetadataChanged { track_id: i.read()?, metadata: i.read()?, }) } } impl dbus::message::SignalArgs for OrgMprisMediaPlayer2TrackListTrackMetadataChanged { const NAME: &'static str = "TrackMetadataChanged"; const INTERFACE: &'static str = "org.mpris.MediaPlayer2.TrackList"; } impl<'a, C: ::std::ops::Deref> OrgMprisMediaPlayer2TrackList for ffidisp::ConnPath<'a, C> { fn get_tracks_metadata( &self, track_ids: Vec, ) -> Result, dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.TrackList", "GetTracksMetadata", (track_ids,), ) .and_then(|r: (Vec,)| Ok(r.0)) } fn add_track( &self, uri: &str, after_track: dbus::Path, set_as_current: bool, ) -> Result<(), dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.TrackList", "AddTrack", (uri, after_track, set_as_current), ) } fn remove_track(&self, track_id: dbus::Path) -> Result<(), dbus::Error> { self.method_call( "org.mpris.MediaPlayer2.TrackList", "RemoveTrack", (track_id,), ) } fn go_to(&self, track_id: dbus::Path) -> Result<(), dbus::Error> { self.method_call("org.mpris.MediaPlayer2.TrackList", "GoTo", (track_id,)) } fn tracks(&self) -> Result>, dbus::Error> { ::get( &self, "org.mpris.MediaPlayer2.TrackList", "Tracks", ) } fn can_edit_tracks(&self) -> Result { ::get( &self, "org.mpris.MediaPlayer2.TrackList", "CanEditTracks", ) } } mpris-2.0.1/src/generated.rs000064400000000000000000000010121046102023000140430ustar 00000000000000// The following modules have been automatically generated from the MPRIS standard. // You may regenerate them by running ./script/generate-mpris-interface.sh mod media_player; mod media_player_player; mod media_player_playlists; mod media_player_tracklist; // Re-export items used by the codebase here pub use self::media_player::OrgMprisMediaPlayer2; pub use self::media_player_player::{OrgMprisMediaPlayer2Player, OrgMprisMediaPlayer2PlayerSeeked}; pub use self::media_player_tracklist::OrgMprisMediaPlayer2TrackList; mpris-2.0.1/src/lib.rs000064400000000000000000000117031046102023000126630ustar 00000000000000#![warn(missing_docs)] #![deny( missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unreachable_pub, unstable_features, unused_import_braces, unused_qualifications )] //! //! # mpris //! //! `mpris` is an idiomatic library for dealing with [MPRIS2][spec]-compatible media players over D-Bus. //! //! This would mostly apply to the Linux-ecosystem which is a heavy user of D-Bus. //! //! ## Getting started //! //! Some hints on how to use this library: //! //! 1. Look at the examples under `examples/`. //! 2. Look at the [`PlayerFinder`] struct. //! //! [spec]: https://specifications.freedesktop.org/mpris-spec/latest/ use thiserror::Error; mod extensions; #[allow(unreachable_pub)] mod generated; mod event; mod find; mod metadata; mod player; mod pooled_connection; mod progress; mod track_list; pub use crate::event::{Event, EventError, PlayerEvents}; pub use crate::find::{FindingError, PlayerFinder, PlayerIter}; pub use crate::metadata::Metadata; pub use crate::metadata::Value as MetadataValue; pub use crate::metadata::ValueKind as MetadataValueKind; pub use crate::player::Player; pub use crate::progress::{Progress, ProgressError, ProgressTick, ProgressTracker}; pub use crate::track_list::{TrackID, TrackList, TrackListError}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[allow(missing_docs)] /// The [`Player`]'s playback status /// /// See: [MPRIS2 specification about `PlaybackStatus`][playback_status] /// /// [playback_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Playback_Status pub enum PlaybackStatus { /// A track is currently playing. Playing, /// A track is currently paused. Paused, /// There is no track currently playing. Stopped, } #[derive(Debug, PartialEq, Eq, Copy, Clone)] /// A [`Player`]'s looping status. /// /// See: [MPRIS2 specification about `Loop_Status`][loop_status] /// /// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Enum:Loop_Status pub enum LoopStatus { /// The playback will stop when there are no more tracks to play None, /// The current track will start again from the begining once it has finished playing Track, /// The playback loops through a list of tracks Playlist, } /// [`PlaybackStatus`] had an invalid string value. #[derive(Debug, Error)] #[error("PlaybackStatus must be one of Playing, Paused, Stopped, but was {0}")] pub struct InvalidPlaybackStatus(String); impl ::std::str::FromStr for PlaybackStatus { type Err = InvalidPlaybackStatus; fn from_str(string: &str) -> Result { use crate::PlaybackStatus::*; match string { "Playing" => Ok(Playing), "Paused" => Ok(Paused), "Stopped" => Ok(Stopped), other => Err(InvalidPlaybackStatus(other.to_string())), } } } /// [`LoopStatus`] had an invalid string value. #[derive(Debug, Error)] #[error("LoopStatus must be one of None, Track, Playlist, but was {0}")] pub struct InvalidLoopStatus(String); impl ::std::str::FromStr for LoopStatus { type Err = InvalidLoopStatus; fn from_str(string: &str) -> Result { match string { "None" => Ok(LoopStatus::None), "Track" => Ok(LoopStatus::Track), "Playlist" => Ok(LoopStatus::Playlist), other => Err(InvalidLoopStatus(other.to_string())), } } } impl LoopStatus { fn dbus_value(self) -> String { String::from(match self { LoopStatus::None => "None", LoopStatus::Track => "Track", LoopStatus::Playlist => "Playlist", }) } } /// Something went wrong when communicating with the D-Bus. This could either be an underlying /// D-Bus library problem, or that the other side did not conform to the expected protocols. #[derive(Debug, Error)] pub enum DBusError { /// An error occurred while talking to the D-Bus. #[error("D-Bus call failed: {0}")] TransportError(#[from] dbus::Error), /// Failed to parse an enum from a string value received from the [`Player`]. This means that the /// [`Player`] replied with unexpected data. #[error("Failed to parse enum value: {0}")] EnumParseError(String), /// A D-Bus method call did not pass arguments of the correct type. This means that the [`Player`] /// replied with unexpected data. #[error("D-Bus call failed: {0}")] TypeMismatchError(#[from] dbus::arg::TypeMismatchError), /// Some other unexpected error occurred. #[error("Unexpected error: {0}")] Miscellaneous(String), } impl From for DBusError { fn from(error: InvalidPlaybackStatus) -> Self { DBusError::EnumParseError(error.to_string()) } } impl From for DBusError { fn from(error: InvalidLoopStatus) -> Self { DBusError::EnumParseError(error.to_string()) } } mpris-2.0.1/src/metadata/value.rs000064400000000000000000000433661046102023000150230ustar 00000000000000use dbus::arg::ArgType; use derive_is_enum_variant::is_enum_variant; use enum_kinds::EnumKind; use from_variants::FromVariants; use std::collections::HashMap; /// Holds a dynamically-typed metadata value. /// /// You will need to type-check this at runtime in order to use the value. #[derive(Debug, PartialEq, Clone, EnumKind, is_enum_variant, FromVariants)] #[enum_kind(ValueKind)] pub enum Value { /// Value is a string. String(String), /// Value is a 16-bit integer. I16(i16), /// Value is a 32-bit integer. I32(i32), /// Value is a 64-bit integer. I64(i64), /// Value is an unsigned 8-bit integer. U8(u8), /// Value is an unsigned 16-bit integer. U16(u16), /// Value is an unsigned 32-bit integer. U32(u32), /// Value is an unsigned 64-bit integer. U64(u64), /// Value is a 64-bit float. F64(f64), /// Value is a boolean. Bool(bool), /// Value is an array of other values. Array(Vec), /// Value is a map of other values. Map(HashMap), /// Unsupported value type. #[from_variants(skip)] Unsupported, } impl Value { /// Returns a simple enum representing the type of value that this value holds. /// /// # Examples /// /// ```rust /// # use mpris::Metadata; /// # let metadata = Metadata::new("1234"); /// # let key_name = "foo"; /// use mpris::MetadataValueKind; /// if let Some(value) = metadata.get(key_name) { /// match value.kind() { /// MetadataValueKind::String => println!("{} is a string", key_name), /// MetadataValueKind::I16 | /// MetadataValueKind::I32 | /// MetadataValueKind::I64 | /// MetadataValueKind::U8 | /// MetadataValueKind::U16 | /// MetadataValueKind::U32 | /// MetadataValueKind::U64 => println!("{} is an integer", key_name), /// MetadataValueKind::F64 => println!("{} is a float", key_name), /// MetadataValueKind::Bool => println!("{} is a boolean", key_name), /// MetadataValueKind::Array => println!("{} is an array", key_name), /// MetadataValueKind::Map => println!("{} is a map", key_name), /// MetadataValueKind::Unsupported => println!("{} is not a supported type", key_name), /// } /// } else { /// println!("Metadata does not have a {} key", key_name); /// } /// ``` pub fn kind(&self) -> ValueKind { ValueKind::from(self) } /// Returns the value as a `Some(Vec<&str>)` if it is a `MetadataValue::Array`. Any elements /// that are not `MetadataValue::String` values will be ignored. pub fn as_str_array(&self) -> Option> { match *self { Value::Array(ref vec) => Some(vec.iter().flat_map(Value::as_str).collect()), Value::String(ref string) => Some(vec![string.as_ref()]), _ => None, } } /// Returns the value as a `Some(u8)` if it is a `MetadataValue::U8`, or `None` otherwise. pub fn as_u8(&self) -> Option { match *self { Value::U8(val) => Some(val), _ => None, } } /// Returns the value as a `Some(u16)` if it is an unsigned int smaller than or equal to u16, /// or `None` otherwise. pub fn as_u16(&self) -> Option { match *self { Value::U16(val) => Some(val), Value::U8(val) => Some(u16::from(val)), _ => None, } } /// Returns the value as a `Some(u32)` if it is an unsigned int smaller than or equal to u32, /// or `None` otherwise. pub fn as_u32(&self) -> Option { match *self { Value::U32(val) => Some(val), Value::U16(val) => Some(u32::from(val)), Value::U8(val) => Some(u32::from(val)), _ => None, } } /// Returns the value as a `Some(u64)` if it is an unsigned int smaller than or equal to u64, /// or `None` otherwise. pub fn as_u64(&self) -> Option { match *self { Value::U64(val) => Some(val), Value::U32(val) => Some(u64::from(val)), Value::U16(val) => Some(u64::from(val)), Value::U8(val) => Some(u64::from(val)), _ => None, } } /// Returns the value as a `Some(i16)` if it is a signed integer smaller than or equal to i16, /// or `None` otherwise. pub fn as_i16(&self) -> Option { match *self { Value::I16(val) => Some(val), _ => None, } } /// Returns the value as a `Some(i32)` if it is a signed integer smaller than or equal to i32, /// or `None` otherwise. pub fn as_i32(&self) -> Option { match *self { Value::I32(val) => Some(val), Value::I16(val) => Some(i32::from(val)), _ => None, } } /// Returns the value as a `Some(i64)` if it is a signed integer smaller than or equal to i64, /// or `None` otherwise. pub fn as_i64(&self) -> Option { match *self { Value::I64(val) => Some(val), Value::I32(val) => Some(i64::from(val)), Value::I16(val) => Some(i64::from(val)), _ => None, } } /// Returns the value as a `Some(f64)` if it is a `MetadataValue::F64`, or `None` otherwise. pub fn as_f64(&self) -> Option { match *self { Value::F64(val) => Some(val), _ => None, } } /// Returns the value as a `Some(bool)` if it is a `MetadataValue::Bool`, or `None` otherwise. pub fn as_bool(&self) -> Option { match *self { Value::Bool(val) => Some(val), _ => None, } } /// Returns the value as a `Some(&str)` if it is a `MetadataValue::String`, or `None` otherwise. pub fn as_str(&self) -> Option<&str> { match *self { Value::String(ref val) => Some(val), _ => None, } } /// Returns the value as a `Some(&String)` if it is a `MetadataValue::String`, or `None` otherwise. pub fn as_string(&self) -> Option<&String> { match *self { Value::String(ref val) => Some(val), _ => None, } } /// Returns the value as a `Some(&HashMap)` if it is a `MetadataValue::Map`, or `None` otherwise. pub fn as_map(&self) -> Option<&HashMap> { match *self { Value::Map(ref val) => Some(val), _ => None, } } /// Returns the value as a `Some(&Vec)` if it is a `MetadataValue::Array`, or `None` otherwise. pub fn as_array(&self) -> Option<&Vec> { match *self { Value::Array(ref val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(u8)` if it is a `MetadataValue::U8`, or `None` otherwise. pub fn into_u8(self) -> Option { match self { Value::U8(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(u16)` if it is an unsigned integer /// smaller than or equal to u16, or `None` otherwise. pub fn into_u16(self) -> Option { match self { Value::U16(val) => Some(val), Value::U8(val) => Some(u16::from(val)), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(u32)` if it is an unsigned integer /// smaller than or equal to u32, or `None` otherwise. pub fn into_u32(self) -> Option { match self { Value::U32(val) => Some(val), Value::U16(val) => Some(u32::from(val)), Value::U8(val) => Some(u32::from(val)), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(u64)` if it is an unsigned integer /// smaller than or equal to u64, or `None` otherwise. pub fn into_u64(self) -> Option { match self { Value::U64(val) => Some(val), Value::U32(val) => Some(u64::from(val)), Value::U16(val) => Some(u64::from(val)), Value::U8(val) => Some(u64::from(val)), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(i16)` if it is a signed integer /// smaller than or equal to i16, or `None` otherwise. pub fn into_i16(self) -> Option { match self { Value::I16(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(i32)` if it is a signed integer /// smaller than or equal to i32, or `None` otherwise. pub fn into_i32(self) -> Option { match self { Value::I32(val) => Some(val), Value::I16(val) => Some(i32::from(val)), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(i64)` if it is a signed integer /// smaller than or equal to i64, or `None` otherwise. pub fn into_i64(self) -> Option { match self { Value::I64(val) => Some(val), Value::I32(val) => Some(i64::from(val)), Value::I16(val) => Some(i64::from(val)), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(f64)` if it is a /// `MetadataValue::F64`, or `None` otherwise. pub fn into_f64(self) -> Option { match self { Value::F64(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(bool)` if it is a /// `MetadataValue::Bool`, or `None` otherwise. pub fn into_bool(self) -> Option { match self { Value::Bool(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(String)` if it is a /// `MetadataValue::String`, or `None` otherwise. pub fn into_string(self) -> Option { match self { Value::String(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(HashMap)` if it is a /// `MetadataValue::Map`, or `None` otherwise. pub fn into_map(self) -> Option> { match self { Value::Map(val) => Some(val), _ => None, } } /// Consumes `self` and returns the inner value as a `Some(Vec)` if it is a /// `MetadataValue::Array`, or `None` otherwise. pub fn into_array(self) -> Option> { match self { Value::Array(val) => Some(val), _ => None, } } } impl<'a> From<&'a str> for Value { fn from(string: &'a str) -> Value { Value::String(String::from(string)) } } impl dbus::arg::Arg for Value { const ARG_TYPE: ArgType = ArgType::Variant; fn signature() -> dbus::Signature<'static> { dbus::Signature::from_slice("v").unwrap() } } impl<'a> dbus::arg::Get<'a> for Value { fn get(i: &mut dbus::arg::Iter<'_>) -> Option { let arg_type = i.arg_type(); // Trying to calculate signature of an invalid arg will panic, so abort early. if let ArgType::Invalid = arg_type { return None; } let signature = i.signature(); match arg_type { // Hashes in DBus are arrays of Dict pairs ({string, variant}) ArgType::Array if *signature == *"a{sv}" => { i.get::>().map(Value::Map) } ArgType::Array => i.get::>().map(Value::Array), ArgType::Boolean => i.get::().map(Value::Bool), ArgType::Byte => i.get::().map(Value::U8), ArgType::Double => i.get::().map(Value::F64), ArgType::Int16 => i.get::().map(Value::I16), ArgType::Int32 => i.get::().map(Value::I32), ArgType::Int64 => i.get::().map(Value::I64), ArgType::String => i.get::().map(Value::String), ArgType::UInt16 => i.get::().map(Value::U16), ArgType::UInt32 => i.get::().map(Value::U32), ArgType::UInt64 => i.get::().map(Value::U64), ArgType::Variant => i.recurse(ArgType::Variant).and_then(|mut iter| iter.get()), ArgType::Invalid => unreachable!("Early return at the top of the method"), ArgType::ObjectPath => i .get::>() .map(|p| Value::String(p.to_string())), ArgType::DictEntry | ArgType::UnixFd | ArgType::Signature | ArgType::Struct => { Some(Value::Unsupported) } } } } #[cfg(test)] mod tests { use super::*; use dbus::arg::{Append, RefArg, Variant}; use dbus::ffidisp::{BusType, Connection, ConnectionItem}; use dbus::Message; fn send_values_over_dbus(appender: F) -> Message where F: FnOnce(Message) -> Message, { // // Open a connection, send a message to it and then read the message back again. // let connection = Connection::get_private(BusType::Session) .expect("Could not open a D-Bus session connection"); connection .register_object_path("/hello") .expect("Could not register object path"); let send_message = Message::new_method_call( &connection.unique_name(), "/hello", "com.example.hello", "Hello", ) .expect("Could not create message"); let send_message = appender(send_message); connection .send(send_message) .expect("Could not send message to myself"); for item in connection.iter(200) { if let ConnectionItem::MethodCall(received_message) = item { return received_message; } } panic!("Did not find a message on the bus"); } fn send_value_over_dbus(value: T) -> Message { send_values_over_dbus(|message| message.append1(value)) } #[test] fn it_supports_strings() { let message = send_value_over_dbus("Hello world!"); let string: Value = message.get1().unwrap(); assert!(string.is_string()); assert_eq!(string.as_str(), Some("Hello world!")); } #[test] fn it_supports_object_paths_as_strings() { let message = send_value_over_dbus(dbus::Path::from("/hello/world")); let string: Value = message.get1().unwrap(); assert!(string.is_string()); assert_eq!(string.as_str(), Some("/hello/world")); } #[test] fn it_supports_unsigned_integers() { let message = send_values_over_dbus(|input| input.append3(1u8, 2u16, 3u32).append1(4u64)); let mut values = message.iter_init(); let eight: Value = values.read().unwrap(); let sixteen: Value = values.read().unwrap(); let thirtytwo: Value = values.read().unwrap(); let sixtyfour: Value = values.read().unwrap(); assert_eq!(Value::U8(1), eight); assert_eq!(Value::U16(2), sixteen); assert_eq!(Value::U32(3), thirtytwo); assert_eq!(Value::U64(4), sixtyfour); } #[test] fn it_supports_signed_integers() { let message = send_values_over_dbus(|input| input.append3(1i16, 2i32, 3i64)); let mut values = message.iter_init(); let sixteen: Value = values.read().unwrap(); let thirtytwo: Value = values.read().unwrap(); let sixtyfour: Value = values.read().unwrap(); assert_eq!(Value::I16(1), sixteen); assert_eq!(Value::I32(2), thirtytwo); assert_eq!(Value::I64(3), sixtyfour); } #[test] fn it_supports_floats() { let message = send_value_over_dbus(42.0f64); let float: Value = message.get1().unwrap(); assert!(float.is_f64()); assert_eq!(float.as_f64(), Some(42.0)); } #[test] fn it_supports_booleans() { let message = send_value_over_dbus(true); let boolean: Value = message.get1().unwrap(); assert!(boolean.is_bool()); assert_eq!(boolean.as_bool(), Some(true)); } #[test] fn it_supports_arrays_of_variants() { let input: Vec>> = vec![ Variant(Box::new(String::from("World"))), Variant(Box::new(42u8)), ]; let expected = vec![Value::String("World".into()), Value::U8(42)]; let message = send_value_over_dbus(input); let array: Value = message.get1().unwrap(); assert!(array.is_array()); assert_eq!(array.into_array(), Some(expected)); } #[test] fn it_supports_arrays_of_strings() { let input: Vec = vec!["Hello".into(), "World".into()]; let expected: Vec = vec!["Hello".into(), "World".into()]; let message = send_value_over_dbus(input); let array: Value = message.get1().unwrap(); assert!(array.is_array()); assert_eq!(array.into_array(), Some(expected)); } #[test] fn it_supports_maps_of_variants() { let mut input: HashMap>> = HashMap::new(); input.insert( String::from("receiver"), Variant(Box::new(String::from("World"))), ); input.insert(String::from("times"), Variant(Box::new(42u8))); let mut expected = HashMap::new(); expected.insert( String::from("receiver"), Value::String(String::from("World")), ); expected.insert(String::from("times"), Value::U8(42)); let message = send_value_over_dbus(input); let hash: Value = message.get1().unwrap(); assert!(hash.is_map()); assert_eq!(hash.into_map(), Some(expected)); } } mpris-2.0.1/src/metadata.rs000064400000000000000000000212141046102023000136730ustar 00000000000000mod value; pub use self::value::{Value, ValueKind}; use super::TrackID; use std::collections::HashMap; use std::time::Duration; /// A structured representation of the [`Player`](crate::player::Player) metadata. /// /// * [Read more about the MPRIS2 `Metadata_Map` type.][metadata_map] /// * [Read MPRIS v2 metadata guidelines][metadata_guidelines] /// /// [metadata_map]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Mapping:Metadata_Map /// [metadata_guidelines]: https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ #[derive(Debug, Default, Clone)] pub struct Metadata { values: HashMap, } impl Metadata { /// Create a new [`Metadata`] struct with a given `track_id`. /// /// This is mostly useful for test fixtures and other places where you want to work with mock /// data. pub fn new(track_id: S) -> Self where S: Into, { let mut values = HashMap::with_capacity(1); values.insert( String::from("mpris:trackid"), Value::String(track_id.into()), ); Metadata { values } } /// Get a value from the metadata by key name. /// /// # Examples /// /// ```rust /// # use mpris::{Metadata, MetadataValue}; /// # let mut metadata = Metadata::new(String::from("1234")); /// # let key_name = "foo"; /// if let Some(MetadataValue::String(name)) = metadata.get("xesam:composer") { /// println!("Composed by: {}", name); /// } /// ``` pub fn get(&self, key: &str) -> Option<&Value> { self.values.get(key) } /// The track ID. /// /// If the [`TrackID`] could not be parsed as a proper [`TrackID`], [`None`] will be returned. /// /// Based on `mpris:trackid` /// > A unique identity for this track within the context of an MPRIS object. /// pub fn track_id(&self) -> Option { self.get("mpris:trackid") .and_then(Value::as_str) .and_then(|v| TrackID::new(v).ok()) } /// A list of artists of the album the track appears on. /// /// Based on `xesam:albumArtist` /// > The album artist(s). pub fn album_artists(&self) -> Option> { self.get("xesam:albumArtist").and_then(Value::as_str_array) } /// The name of the album the track appears on. /// /// Based on `xesam:album` /// > The album name. pub fn album_name(&self) -> Option<&str> { self.get("xesam:album").and_then(Value::as_str) } /// An URL to album art of the current track. /// /// Based on `mpris:artUrl` /// > The location of an image representing the track or album. Clients should not assume this /// > will continue to exist when the media player stops giving out the URL. pub fn art_url(&self) -> Option<&str> { self.get("mpris:artUrl").and_then(Value::as_str) } /// A list of artists of the track. /// /// Based on `xesam:artist` /// > The track artist(s). pub fn artists(&self) -> Option> { self.get("xesam:artist").and_then(Value::as_str_array) } /// Based on `xesam:autoRating` /// > An automatically-generated rating, based on things such as how often it has been played. /// > This should be in the range 0.0 to 1.0. pub fn auto_rating(&self) -> Option { self.get("xesam:autoRating").and_then(Value::as_f64) } /// Based on `xesam:discNumber` /// > The disc number on the album that this track is from. pub fn disc_number(&self) -> Option { self.get("xesam:discNumber").and_then(Value::as_i32) } /// The duration of the track, in microseconds /// /// Based on `mpris:length` /// > The duration of the track in microseconds. pub fn length_in_microseconds(&self) -> Option { match self.get("mpris:length") { Some(Value::I64(len)) => Some(*len as u64), Some(Value::U64(len)) => Some(*len), Some(_) => None, None => None, } } /// The duration of the track, as a [`Duration`] /// /// Based on `mpris:length`. pub fn length(&self) -> Option { use crate::extensions::DurationExtensions; self.length_in_microseconds().map(Duration::from_micros_ext) } /// The name of the track. /// /// Based on `xesam:title` /// > The track title. pub fn title(&self) -> Option<&str> { self.get("xesam:title").and_then(Value::as_str) } /// The track number on the disc of the album the track appears on. /// /// Based on `xesam:trackNumber` /// > The track number on the album disc. pub fn track_number(&self) -> Option { self.get("xesam:trackNumber").and_then(Value::as_i32) } /// A URL to the media being played. /// /// Based on `xesam:url` /// > The location of the media file. pub fn url(&self) -> Option<&str> { self.get("xesam:url").and_then(Value::as_str) } /// Returns an owned [`HashMap`] of borrowed values from this [`Metadata`]. Useful if you need a /// mutable hash but don't have ownership of [`Metadata`] or want to consume it. /// /// If you want to convert to a [`HashMap`], use [`Into::into`](std::convert::Into::into) instead. pub fn as_hashmap(&self) -> HashMap<&str, &Value> { self.iter().collect() } /// Iterate all metadata keys and values. pub fn iter(&self) -> impl Iterator { self.values.iter().map(|(k, v)| (k.as_str(), v)) } /// Iterate all metadata keys. pub fn keys(&self) -> impl Iterator { self.values.keys().map(String::as_str) } /// Returns [`true`] if there is no metadata pub fn is_empty(&self) -> bool { self.values.is_empty() } } impl IntoIterator for Metadata { type Item = (String, Value); type IntoIter = std::collections::hash_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.values.into_iter() } } // Disable implicit_hasher; suggested code fix does not compile. I think this might be a false // positive, but I'm not sure. #[cfg_attr(feature = "cargo-clippy", allow(clippy::implicit_hasher))] impl From for HashMap { fn from(metadata: Metadata) -> Self { metadata.values } } impl From> for Metadata { fn from(values: HashMap) -> Self { Metadata { values } } } #[cfg(test)] mod tests { use super::*; #[test] fn it_creates_new_metadata() { let metadata = Metadata::new("/foo"); assert_eq!(metadata.track_id(), Some(TrackID::new("/foo").unwrap())); } #[test] fn it_supports_blank_metadata() { let metadata = Metadata::from(HashMap::new()); assert_eq!(metadata.track_id(), None); } #[test] fn it_builds_values_hash() { let mut input_hash: HashMap = HashMap::new(); input_hash.insert(String::from("xesam:trackNumber"), Value::from(42)); let metadata = Metadata::from(input_hash.clone()); let output_hash = metadata.as_hashmap(); assert_eq!(input_hash.get("xesam:trackNumber"), Some(&Value::I32(42))); assert_eq!(output_hash.get("xesam:trackNumber"), Some(&&Value::I32(42))); } #[test] fn it_has_iterators() { let mut input_hash: HashMap = HashMap::new(); input_hash.insert(String::from("xesam:trackNumber"), Value::from(42)); let metadata = Metadata::from(input_hash); let keys: Vec<&str> = metadata.keys().collect(); assert_eq!(keys, vec!["xesam:trackNumber"]); let keyvals: Vec<(&str, &Value)> = metadata.iter().collect(); assert_eq!(keyvals, vec![("xesam:trackNumber", &Value::I32(42))]); for (key, val) in metadata { assert_eq!(key, String::from("xesam:trackNumber")); assert_eq!(val, Value::I32(42)); } } #[test] fn from_hashmap_artist_string() { use std::iter::FromIterator; let metadata = Metadata::from(HashMap::from_iter( vec![(String::from("xesam:artist"), Value::from("Agnes Obel"))].into_iter(), )); assert_eq!(metadata.artists(), Some(vec!["Agnes Obel"])); } #[test] fn from_hashmap_artists_list() { use std::iter::FromIterator; let metadata = Metadata::from(HashMap::from_iter( vec![( String::from("xesam:artist"), Value::from(vec![Value::from("Agnes Obel")]), )] .into_iter(), )); assert_eq!(metadata.artists(), Some(vec!["Agnes Obel"])); } } mpris-2.0.1/src/player.rs000064400000000000000000001464541046102023000134250ustar 00000000000000use std::collections::HashMap; use std::ops::Range; use std::rc::Rc; use std::time::Duration; use dbus::ffidisp::{ConnPath, Connection}; use dbus::strings::{BusName, Path}; use super::{DBusError, LoopStatus, MetadataValue, PlaybackStatus, TrackID, TrackList}; use crate::event::PlayerEvents; use crate::extensions::DurationExtensions; use crate::generated::OrgMprisMediaPlayer2; use crate::generated::OrgMprisMediaPlayer2Player; use crate::metadata::Metadata; use crate::pooled_connection::{MprisEvent, PooledConnection}; use crate::progress::ProgressTracker; pub(crate) const MPRIS2_PREFIX: &str = "org.mpris.MediaPlayer2."; pub(crate) const MPRIS2_PATH: &str = "/org/mpris/MediaPlayer2"; /// When D-Bus connection is managed for you, use this timeout while communicating with a Player. pub(crate) const DEFAULT_TIMEOUT_MS: i32 = 500; // ms /// A MPRIS-compatible player. /// /// You can query this player about the currently playing media, or control it. /// /// **See:** [MPRIS2 MediaPlayer2.Player Specification][spec]. /// /// [spec]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html #[derive(Debug)] pub struct Player { connection: Rc, bus_name: String, unique_name: String, identity: String, timeout_ms: i32, has_tracklist_interface: bool, } impl Player { /// Create a new [`Player`] using a D-Bus connection and address information. /// /// If no player is running on this bus name an [`Err`] will be returned. pub fn new( connection: Connection, bus_name: String, timeout_ms: i32, ) -> Result { Player::for_pooled_connection(Rc::new(connection.into()), bus_name, timeout_ms) } pub(crate) fn for_pooled_connection( pooled_connection: Rc, bus_name: String, timeout_ms: i32, ) -> Result { let path: Path = MPRIS2_PATH.into(); let bus: BusName = bus_name.as_str().into(); let identity = { let connection_path = pooled_connection.with_path(bus.clone(), path.clone(), timeout_ms); connection_path.identity()? }; let unique_name = pooled_connection .determine_unique_name(&bus_name) .ok_or_else(|| { DBusError::Miscellaneous(String::from( "Could not determine player's unique name. Did it exit during initialization?", )) })?; let has_tracklist_interface = { let connection_path = pooled_connection.with_path(bus, path, timeout_ms); has_tracklist_interface(connection_path).unwrap_or(false) }; Ok(Player { connection: pooled_connection, bus_name, unique_name, identity, timeout_ms, has_tracklist_interface, }) } /// Returns the current D-Bus communication timeout (in milliseconds). /// /// When querying D-Bus the call should not block longer than this, and will instead fail the /// query if no response has been received in this time. /// /// You can change this using [`set_dbus_timeout_ms`](Self::set_dbus_timeout_ms). pub fn dbus_timeout_ms(&self) -> i32 { self.timeout_ms } /// Change the D-Bus communication timeout. pub fn set_dbus_timeout_ms(&mut self, timeout_ms: i32) { self.timeout_ms = timeout_ms; } /// Returns the player's D-Bus bus name. pub fn bus_name(&self) -> &str { &self.bus_name } /// Returns the player name part of the player's D-Bus bus name. /// This is the part after "org.mpris.MediaPlayer2.", not including the instance part. /// /// See: [MPRIS2 specification about bus names][bus_names]. /// /// [bus_names]: https://specifications.freedesktop.org/mpris-spec/latest/#Bus-Name-Policy pub fn bus_name_player_name_part(&self) -> &str { self.bus_name() .trim_start_matches(MPRIS2_PREFIX) .split('.') // Remove the "instance" part .next() .unwrap() } /// Returns the player's unique D-Bus bus name (usually something like `:1.1337`). pub fn unique_name(&self) -> &str { &self.unique_name } /// Returns the player's MPRIS [`Identity`][identity]. /// /// This is usually the application's name, like `Spotify`. /// /// [identity]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Identity pub fn identity(&self) -> &str { &self.identity } /// Checks if the Player implements the `org.mpris.MediaPlayer2.TrackList` interface. pub fn supports_track_lists(&self) -> bool { self.has_tracklist_interface } /// Returns the player's `DesktopEntry` property, if supported. /// /// See: [MPRIS2 specification about `DesktopEntry`][desktop_entry]. /// /// [desktop_entry]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:DesktopEntry pub fn get_desktop_entry(&self) -> Result, DBusError> { handle_optional_property(self.connection_path().desktop_entry()) } /// Returns the player's `SupportedMimeTypes` property. /// /// See: [MPRIS2 specification about `SupportedMimeTypes`][mime_types]. /// /// [mime_types]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedMimeTypes pub fn get_supported_mime_types(&self) -> Result, DBusError> { self.connection_path() .supported_mime_types() .map_err(|e| e.into()) } /// Returns the player's `SupportedUriSchemes` property. /// /// See: [MPRIS2 specification about `SupportedUriSchemes`][schemes]. /// /// [schemes]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:SupportedUriSchemes pub fn get_supported_uri_schemes(&self) -> Result, DBusError> { self.connection_path() .supported_uri_schemes() .map_err(|e| e.into()) } /// Returns the player's `HasTrackList` property. /// /// See: [MPRIS2 specification about `HasTrackList`][track_list]. /// /// [track_list]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:HasTrackList pub fn get_has_track_list(&self) -> Result { self.connection_path() .has_track_list() .map_err(|e| e.into()) } /// Returns the player's MPRIS `position` as a [`Duration`] since the start of the media. pub fn get_position(&self) -> Result { self.get_position_in_microseconds() .map(Duration::from_micros_ext) } /// Gets the "Position" setting, if the player indicates that it supports it. /// /// Return [`Some`] containing the current value of the position. If the setting is not /// supported, return [`None`] pub fn checked_get_position(&self) -> Result, DBusError> { if self.has_position()? { Ok(Some(self.get_position()?)) } else { Ok(None) } } /// Returns the player's MPRIS `position` as a count of microseconds since the start of the /// media. pub fn get_position_in_microseconds(&self) -> Result { self.connection_path() .position() .map(|p| p as u64) .map_err(|e| e.into()) } /// Sets the position of the current track to the given position (as a [`Duration`]). /// /// Current [`TrackID`] must be provided to avoid race conditions with the player, in case it /// changes tracks while the signal is being sent. /// /// **Note:** There is currently no good way to retrieve the current [`TrackID`] through the /// `mpris` library. You will have to manually retrieve it through D-Bus until implemented. /// /// See: [MPRIS2 specification about `SetPosition`][set_position]. /// /// [set_position]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:SetPosition pub fn set_position(&self, track_id: TrackID, position: &Duration) -> Result<(), DBusError> { self.set_position_in_microseconds(track_id, DurationExtensions::as_micros(position)) } /// Set the "Position" setting of the player, if the player indicates that it supports the /// "Position" setting and can be controlled. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Position`][position]. /// /// [position]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Position pub fn checked_set_position( &self, track_id: TrackID, position: &Duration, ) -> Result { if self.can_control()? && self.has_position()? { self.set_position(track_id, position) .map(|_| true) .map_err(DBusError::from) } else { Ok(false) } } /// Sets the position of the current track to the given position (in microseconds). /// /// Current [`TrackID`] must be provided to avoid race conditions with the player, in case it /// changes tracks while the signal is being sent. /// /// **Note:** There is currently no good way to retrieve the current [`TrackID`] through the /// `mpris` library. You will have to manually retrieve it through D-Bus until implemented. /// /// See: [MPRIS2 specification about `SetPosition`][set_position]. /// /// [set_position]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:SetPosition pub fn set_position_in_microseconds( &self, track_id: TrackID, position_in_us: u64, ) -> Result<(), DBusError> { self.connection_path() .set_position(track_id.as_path(), position_in_us as i64) .map_err(|e| e.into()) } /// Returns the player's MPRIS (playback) `rate` as a factor. /// /// 1.0 would mean normal rate, while 2.0 would mean twice the playback speed. pub fn get_playback_rate(&self) -> Result { self.connection_path().rate().map_err(|e| e.into()) } /// Gets the "Rate" setting, if the player indicates that it supports it. /// /// Returns [`Some`] containing the current value of the rate setting. If the setting is not /// supported, returns [`None`] pub fn checked_get_playback_rate(&self) -> Result, DBusError> { if self.has_playback_rate()? { Ok(Some(self.get_playback_rate()?)) } else { Ok(None) } } /// Sets the player's MPRIS (playback) `rate` as a factor. /// /// 1.0 would mean normal rate, while 2.0 would mean twice the playback speed. /// /// It is not allowed to try to set playback rate to a value outside of the supported range. /// [`get_valid_playback_rate_range`](Self::get_valid_playback_rate_range) returns a [`Range`] that encodes the maximum and /// minimum values. /// /// You must not set rate to 0.0; instead call [`pause`](Self::pause). /// /// See: [MPRIS2 specification about `Rate`][rate]. /// /// [rate]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Rate pub fn set_playback_rate(&self, rate: f64) -> Result<(), DBusError> { self.connection_path().set_rate(rate).map_err(|e| e.into()) } /// Set the playback rate of the player, if the player indicates that supports it and that it /// can be controlled. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Rate`][rate]. /// /// [rate]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Rate pub fn checked_set_playback_rate(&self, rate: f64) -> Result { if self.can_control()? && self.has_playback_rate()? { self.set_playback_rate(rate) .map(|_| true) .map_err(DBusError::from) } else { Ok(false) } } /// Gets the minimum allowed value for playback rate. /// /// See: [MPRIS2 specification about `MinimumRate`][min_rate]. /// /// [min_rate]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MinimumRate pub fn get_minimum_playback_rate(&self) -> Result { self.connection_path().minimum_rate().map_err(|e| e.into()) } /// Gets the maximum allowed value for playback rate. /// /// See: [MPRIS2 specification about `MaximumRate`][max_rate]. /// /// [max_rate]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:MaximumRate pub fn get_maximum_playback_rate(&self) -> Result { self.connection_path().maximum_rate().map_err(|e| e.into()) } /// Gets the minimum-maximum allowed value range for playback rate. /// /// See: [`get_minimum_playback_rate`](Self::get_minimum_playback_rate) /// and [`get_maximum_playback_rate`](Self::get_maximum_playback_rate). pub fn get_valid_playback_rate_range(&self) -> Result, DBusError> { self.get_minimum_playback_rate() .and_then(|min| self.get_maximum_playback_rate().map(|max| min..max)) } /// Query the player for current metadata. /// /// See [`Metadata`] for more information about what is included here. pub fn get_metadata(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; let connection_path = self.connection_path(); Properties::get::>( &connection_path, "org.mpris.MediaPlayer2.Player", "Metadata", ) .map(Metadata::from) .map_err(DBusError::from) } /// Query the player for the current tracklist. /// /// **Note:** It's more expensive to rebuild this each time rather than trying to keep the same /// [`TrackList`] updated. See [`TrackList::reload`]. /// /// See [`checked_get_track_list`](Self::checked_get_track_list) to automatically detect players not supporting track lists. pub fn get_track_list(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; let connection_path = self.connection_path(); Properties::get::>>( &connection_path, "org.mpris.MediaPlayer2.TrackList", "Tracks", ) .map(TrackList::from) .map_err(DBusError::from) } /// Query the player for the current tracklist. /// /// **Note:** It's more expensive to rebuild this each time rather than trying to keep the same /// [`TrackList`] updated. See [`TrackList::reload`]. /// /// See [`get_track_list`](Self::get_track_list), [`supports_track_lists`](Self::supports_track_lists) and /// [`get_has_track_list`](Self::get_has_track_list) if you want to manually handle compatibility checks. pub fn checked_get_track_list(&self) -> Result, DBusError> { if self.supports_track_lists() && self.get_has_track_list()? { self.get_track_list().map(Some) } else { Ok(None) } } /// Query the player to see if it allows changes to its TrackList. /// /// Will return [`Err`] if Player isn't supporting the [`TrackList`] interface. /// /// See [`checked_can_edit_tracks`](Self::checked_can_edit_tracks) to automatically detect players not supporting track lists. /// /// See: [MPRIS2 specification about `CanEditTracks`][can_edit]. /// /// [can_edit]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:CanEditTracks pub fn can_edit_tracks(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; let connection_path = self.connection_path(); Properties::get::( &connection_path, "org.mpris.MediaPlayer2.TrackList", "CanEditTracks", ) .map_err(DBusError::from) } /// Query the player to see if it allows changes to its TrackList. /// /// Will return [`false`] if [`Player`] isn't supporting the `TrackList` interface. /// /// See [`can_edit_tracks`](Self::can_edit_tracks) and [`supports_track_lists`](Self::supports_track_lists) /// if you want to manually handle compatibility checks. /// /// See: [MPRIS2 specification about `CanEditTracks`][can_edit]. /// /// [can_edit]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Property:CanEditTracks pub fn checked_can_edit_tracks(&self) -> bool { if self.supports_track_lists() { self.can_edit_tracks().unwrap_or(false) } else { false } } /// Query the player for metadata for the given [`TrackID`]s. /// /// This is used by the [`TrackList`] type to iterator metadata for the tracks in the track list. /// /// See: [MediaPlayer2.TrackList.GetTracksMetadata][get_meta]. /// /// [get_meta]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:GetTracksMetadata pub fn get_tracks_metadata(&self, track_ids: &[TrackID]) -> Result, DBusError> { use dbus::arg::IterAppend; let connection_path = self.connection_path(); let mut method = connection_path.method_call_with_args( &"org.mpris.MediaPlayer2.TrackList".into(), &"GetTracksMetadata".into(), |msg| { let mut i = IterAppend::new(msg); i.append(track_ids.iter().map(|id| id.as_path()).collect::>()); }, )?; method.as_result()?; let mut i = method.iter_init(); let metadata: Vec<::std::collections::HashMap> = i.read()?; if metadata.len() == track_ids.len() { Ok(metadata.into_iter().map(Metadata::from).collect()) } else { Err(DBusError::Miscellaneous(format!( "Expected {} tracks, but got {} tracks returned.", track_ids.len(), metadata.len() ))) } } /// Query the player for metadata for a single [`TrackID`]. /// /// Note that [`get_tracks_metadata`](Self::get_tracks_metadata) with a list is more effective if you have more than a /// single [`TrackID`] to load. /// /// See: [MediaPlayer2.TrackList.GetTracksMetadata][get_meta]. /// /// [get_meta]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:GetTracksMetadata pub fn get_track_metadata(&self, track_id: &TrackID) -> Result { self.get_tracks_metadata(&[track_id.clone()]) .and_then(|mut result| { result.pop().map(Ok).unwrap_or_else(|| { Err(DBusError::Miscellaneous(format!( "Player gave no Metadata for {}", track_id ))) }) }) } /// Returns a new [`ProgressTracker`] for the player. /// /// Use this if you want to monitor a player in order to show close-to-realtime information /// about it. /// /// It is built like a blocking "frame limiter" where it returns at an approximately fixed /// interval with the most up-to-date information. It's mostly appropriate when trying to /// render something like a progress bar, or information about the current track. /// /// See: [`events`](Self::events) for an alternative approach. pub fn track_progress(&self, interval_ms: u32) -> Result, DBusError> { ProgressTracker::new(self, interval_ms) } /// Returns a [`PlayerEvents`] iterator, or an [`DBusError`] if there was a problem with the D-Bus /// connection to the player. /// /// This iterator will block until an event for the current player is emitted. This is a lot /// more bare-bones than [`track_progress`](Self::track_progress), but it's also something that makes it easier /// for you to translate events into your own application's domain events and only deal with /// actual changes. /// /// You could implement your own progress tracker on top of this, but it's probably not /// appropriate to render a live progress bar using this iterator as the progress bar will /// remain frozen until the next event is emitted and the iterator returns. /// /// See: [`track_progress`](Self::track_progress) for an alternative approach. pub fn events(&self) -> Result { PlayerEvents::new(self) } /// Returns true if the bus of this player is still occupied in the connection, or put in /// another way: If there's a process still listening on messages on this bus. /// /// If the player that you are controlling / querying has shut down, then this would return /// false. You can use this to do graceful restarts, begin looking for another player, etc. pub fn is_running(&self) -> bool { self.connection() .name_has_owner(self.bus_name.to_string()) .unwrap_or(false) } pub(crate) fn connection(&self) -> &PooledConnection { &self.connection } /// Send a `PlayPause` signal to the player. /// /// See: [MPRIS2 specification about `PlayPause`][play_pause] /// /// [play_pause]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:PlayPause pub fn play_pause(&self) -> Result<(), DBusError> { self.connection_path().play_pause().map_err(|e| e.into()) } /// Send a `Play` signal to the player. /// /// See: [MPRIS2 specification about `Play`][play]. /// /// [play]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Play pub fn play(&self) -> Result<(), DBusError> { self.connection_path().play().map_err(|e| e.into()) } /// Send a `Pause` signal to the player. /// /// See: [MPRIS2 specification about `Pause`][pause]. /// /// [pause]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Pause pub fn pause(&self) -> Result<(), DBusError> { self.connection_path().pause().map_err(|e| e.into()) } /// Send a `Stop` signal to the player. /// /// See: [MPRIS2 specification about `Stop`][stop]. /// /// [stop]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop pub fn stop(&self) -> Result<(), DBusError> { self.connection_path().stop().map_err(|e| e.into()) } /// Send a `Next` signal to the player. /// /// See: [MPRIS2 specification about `Next`][next]. /// /// [next]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Next pub fn next(&self) -> Result<(), DBusError> { self.connection_path().next().map_err(|e| e.into()) } /// Send a `Previous` signal to the player. /// /// See: [MPRIS2 specification about `Previous`][prev]. /// /// [prev]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Previous pub fn previous(&self) -> Result<(), DBusError> { self.connection_path().previous().map_err(|e| e.into()) } /// Send a `Seek` signal to the player. /// /// See: [MPRIS2 specification about `Seek`][seek]. /// /// [seek]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub fn seek(&self, offset_in_microseconds: i64) -> Result<(), DBusError> { self.connection_path() .seek(offset_in_microseconds) .map_err(|e| e.into()) } /// Tell the player to seek forwards. /// /// See: [`seek`](Self::seek) method. pub fn seek_forwards(&self, offset: &Duration) -> Result<(), DBusError> { self.seek(DurationExtensions::as_micros(offset) as i64) } /// Send a `Raise` signal to the player. /// /// > Brings the media player's user interface to the front using any appropriate mechanism /// > available. /// > /// > The media player may be unable to control how its user interface is displayed, or it may /// > not have a graphical user interface at all. In this case, the CanRaise property is false /// > and this method does nothing. /// /// See: [MPRIS2 specification about `Raise`][raise] and the [`can_raise`](Self::can_raise) method. /// /// [raise]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Raise pub fn raise(&self) -> Result<(), DBusError> { self.connection_path().raise().map_err(|e| e.into()) } /// Send a `Raise` signal to the player, if it supports it. /// /// See: [`can_raise`](Self::can_raise) and [`raise`](Self::raise) methods. pub fn checked_raise(&self) -> Result { if self.can_raise()? { self.raise().map(|_| true) } else { Ok(false) } } /// Send a `Quit` signal to the player. /// /// > Causes the media player to stop running. /// > /// > The media player may refuse to allow clients to shut it down. In this case, the CanQuit /// > property is false and this method does nothing. /// > /// > Note: Media players which can be D-Bus activated, or for which there is no sensibly easy /// > way to terminate a running instance (via the main interface or a notification area icon for /// > example) should allow clients to use this method. Otherwise, it should not be needed. /// > /// > If the media player does not have a UI, this should be implemented. /// /// See: [MPRIS2 specification about `Quit`][quit] and the [`can_quit`](Self::can_quit) method. /// /// [quit]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Method:Quit pub fn quit(&self) -> Result<(), DBusError> { self.connection_path().quit().map_err(|e| e.into()) } /// Send a `Quit` signal to the player, if it supports it. /// /// See: [`can_quit`](Self::can_quit) and [`quit`](Self::quit) methods. pub fn checked_quit(&self) -> Result { if self.can_quit()? { self.quit().map(|_| true) } else { Ok(false) } } /// Tell the player to seek backwards. /// /// See: [`seek`](Self::seek) method. pub fn seek_backwards(&self, offset: &Duration) -> Result<(), DBusError> { self.seek(-(DurationExtensions::as_micros(offset) as i64)) } /// Go to a specific track on the [`Player`]'s [`TrackList`]. /// /// If the given [`TrackID`] is not part of the player's [`TrackList`], it will have no effect. /// /// Requires the player to implement the `TrackList` interface. /// /// See: [MPRIS2 specification about `GoTo`][go_to] /// /// [go_to]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:GoTo pub fn go_to(&self, track_id: &TrackID) -> Result<(), DBusError> { use crate::generated::OrgMprisMediaPlayer2TrackList; self.connection_path() .go_to(track_id.into()) .map_err(DBusError::from) } /// Add a URI to the TrackList and optionally set it as current. /// /// It is placed after the specified [`TrackID`], if supported by the player. /// /// Requires the player to implement the `TrackList` interface. /// /// See: [MPRIS2 specification about `AddTrack`][add_track]. /// /// [add_track]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:AddTrack pub fn add_track( &self, uri: &str, after: &TrackID, set_as_current: bool, ) -> Result<(), DBusError> { use crate::generated::OrgMprisMediaPlayer2TrackList; self.connection_path() .add_track(uri, after.into(), set_as_current) .map_err(DBusError::from) } /// Add a URI to the start of the TrackList and optionally set it as current. /// /// Requires the player to implement the `TrackList` interface. /// /// See: [MPRIS2 specification about `AddTrack`][add_track]. /// /// [add_track]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:AddTrack pub fn add_track_at_start(&self, uri: &str, set_as_current: bool) -> Result<(), DBusError> { use crate::generated::OrgMprisMediaPlayer2TrackList; self.connection_path() .add_track(uri, crate::track_list::NO_TRACK.into(), set_as_current) .map_err(DBusError::from) } /// Remove an item from the TrackList. /// /// Requires the player to implement the `TrackList` interface. /// /// See: [MPRIS2 specification about `RemoveTrack`][remove]. /// /// [remove]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html#Method:RemoveTrack pub fn remove_track(&self, track_id: &TrackID) -> Result<(), DBusError> { use crate::generated::OrgMprisMediaPlayer2TrackList; self.connection_path() .remove_track(track_id.into()) .map_err(DBusError::from) } /// Sends a `PlayPause` signal to the player, if the player indicates that it can pause. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `PlayPause`][play_pause] /// /// [play_pause]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:PlayPause pub fn checked_play_pause(&self) -> Result { if self.can_pause()? { self.play_pause().map(|_| true) } else { Ok(false) } } /// Sends a `Play` signal to the player, if the player indicates that it can play. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Play`][play]. /// /// [play]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Play pub fn checked_play(&self) -> Result { if self.can_play()? { self.play().map(|_| true) } else { Ok(false) } } /// Sends a `Pause` signal to the player, if the player indicates that it can pause. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Pause`][pause]. /// /// [pause]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Pause pub fn checked_pause(&self) -> Result { if self.can_pause()? { self.pause().map(|_| true) } else { Ok(false) } } /// Sends a `Stop` signal to the player, if the player indicates that it can stop. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Stop`][stop]. /// /// [stop]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop pub fn checked_stop(&self) -> Result { if self.can_stop()? { self.stop().map(|_| true) } else { Ok(false) } } /// Sends a `Next` signal to the player, if the player indicates that it can go to the next /// media. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Next`][next]. /// /// [next]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Next pub fn checked_next(&self) -> Result { if self.can_go_next()? { self.next().map(|_| true) } else { Ok(false) } } /// Sends a `Previous` signal to the player, if the player indicates that it can go to a /// previous media. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Previous`][prev]. /// /// [prev]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Previous pub fn checked_previous(&self) -> Result { if self.can_go_previous()? { self.previous().map(|_| true) } else { Ok(false) } } /// Sends a `Seek` signal to the player, if the player indicates that it can seek. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Seek`][seek]. /// /// [seek]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub fn checked_seek(&self, offset_in_microseconds: i64) -> Result { if self.can_seek()? { self.seek(offset_in_microseconds).map(|_| true) } else { Ok(false) } } /// Seeks the player forwards, if the player indicates that it can seek. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Seek`][seek]. /// /// [seek]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub fn checked_seek_forwards(&self, offset: &Duration) -> Result { if self.can_seek()? { self.seek_forwards(offset).map(|_| true) } else { Ok(false) } } /// Seeks the player backwards, if the player indicates that it can seek. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Seek`][seek]. /// /// [seek]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Seek pub fn checked_seek_backwards(&self, offset: &Duration) -> Result { if self.can_seek()? { self.seek_backwards(offset).map(|_| true) } else { Ok(false) } } /// Queries the player to see if it can be raised or not. /// /// See: [MPRIS2 specification about `CanRaise`][can_raise] and the [`raise`](Self::raise) method. /// /// [can_raise]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanRaise pub fn can_raise(&self) -> Result { self.connection_path().can_raise().map_err(|e| e.into()) } /// Queries the player to see if it can be asked to quit. /// /// See: [MPRIS2 specification about `CanQuit`][can_quit] and the [`quit`](Self::quit) method. /// /// [can_quit]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanQuit pub fn can_quit(&self) -> Result { self.connection_path().can_quit().map_err(|e| e.into()) } /// Queries the player to see if it can be asked to entrer fullscreen. /// /// This property was added in MPRIS 2.2, and not all players will implement it. This method /// will try to detect this case and fall back to `Ok(false)`. /// /// It is up to you to decide if you want to ignore errors caused by this method or not. /// /// See: [MPRIS2 specification about `CanSetFullscreen`][can_full] and the [`set_fullscreen`](Self::set_fullscreen) method. /// /// [can_full]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:CanSetFullscreen pub fn can_set_fullscreen(&self) -> Result { handle_optional_property(self.connection_path().can_set_fullscreen()) .map(|o| o.unwrap_or(false)) } /// Queries the player to see if it can be controlled or not. /// /// See: [MPRIS2 specification about `CanControl`][can_control]. /// /// [can_control]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanControl pub fn can_control(&self) -> Result { self.connection_path().can_control().map_err(|e| e.into()) } /// Queries the player to see if it can go to next or not. /// /// See: [MPRIS2 specification about `CanGoNext`][can_next]. /// /// [can_next]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoNext pub fn can_go_next(&self) -> Result { self.connection_path().can_go_next().map_err(|e| e.into()) } /// Queries the player to see if it can go to previous or not. /// /// See: [MPRIS2 specification about `CanGoPrevious`][can_prev]. /// /// [can_prev]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanGoPrevious pub fn can_go_previous(&self) -> Result { self.connection_path() .can_go_previous() .map_err(|e| e.into()) } /// Queries the player to see if it can pause. /// /// See: [MPRIS2 specification about `CanPause`][can_pause] /// /// [can_pause]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPause pub fn can_pause(&self) -> Result { self.connection_path().can_pause().map_err(|e| e.into()) } /// Queries the player to see if it can play. /// /// See: [MPRIS2 specification about `CanPlay`][can_play]. /// /// [can_play]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanPlay pub fn can_play(&self) -> Result { self.connection_path().can_play().map_err(|e| e.into()) } /// Queries the player to see if it can seek within the media. /// /// See: [MPRIS2 specification about `CanSeek`][can_seek]. /// /// [can_seek]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanSeek pub fn can_seek(&self) -> Result { self.connection_path().can_seek().map_err(|e| e.into()) } /// Queries the player to see if it can stop. /// /// MPRIS2 defines [the `Stop` message to only work when the player can be controlled][can_stop], so that /// is the property used for this method. /// /// See: [MPRIS2 specification about `CanControl`][can_control]. /// /// [can_stop]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Method:Stop /// [can_control]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:CanControl pub fn can_stop(&self) -> Result { self.can_control() } /// Queries the player to see if it currently supports/allows changing playback rate. pub fn can_set_playback_rate(&self) -> Result { self.get_valid_playback_rate_range() .map(|range| range.start < 1.0 || range.end > 1.0) } /// Queries the player to see if it supports the "Shuffle" setting pub fn can_shuffle(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; self.connection_path() .get_all("org.mpris.MediaPlayer2.Player") .map(|props| props.contains_key("Shuffle")) .map_err(DBusError::from) } /// Queries the player to see if it supports the "LoopStatus" setting pub fn can_loop(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; self.connection_path() .get_all("org.mpris.MediaPlayer2.Player") .map(|props| props.contains_key("LoopStatus")) .map_err(DBusError::from) } /// Queries the player to see if it supports the "Rate" setting pub fn has_playback_rate(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; self.connection_path() .get_all("org.mpris.MediaPlayer2.Player") .map(|props| props.contains_key("Rate")) .map_err(DBusError::from) } /// Queries the player to see if it supports the "Position" setting pub fn has_position(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; self.connection_path() .get_all("org.mpris.MediaPlayer2.Player") .map(|props| props.contains_key("Position")) .map_err(DBusError::from) } /// Queries the player to see if it supports the "Volume" setting pub fn has_volume(&self) -> Result { use dbus::ffidisp::stdintf::org_freedesktop_dbus::Properties; self.connection_path() .get_all("org.mpris.MediaPlayer2.Player") .map(|props| props.contains_key("Volume")) .map_err(DBusError::from) } /// Query the player for current fullscreen state. /// /// This property was added in MPRIS 2.2, and not all players will implement it. This method /// will try to detect this case and fall back to `Ok(None)`. /// /// It is up to you to decide if you want to ignore errors caused by this method or not. /// /// See: [MPRIS2 specification about `Fullscreen`][full] and the [`can_set_fullscreen`](Self::can_set_fullscreen) method. /// /// [full]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub fn get_fullscreen(&self) -> Result, DBusError> { handle_optional_property(self.connection_path().fullscreen()) } /// Asks the player to change fullscreen state. /// /// If method call succeeded, `Ok(true)` will be returned. /// /// This property was added in MPRIS 2.2, and not all players will implement it. This method /// will try to detect this case and fall back to `Ok(false)`. /// /// Other errors will be returned as [`Err`]. /// /// See: [MPRIS2 specification about `Fullscreen`][full] and the [`can_set_fullscreen`](Self::can_set_fullscreen) method. /// /// [full]: https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html#Property:Fullscreen pub fn set_fullscreen(&self, new_state: bool) -> Result { handle_optional_property(self.connection_path().set_fullscreen(new_state)) .map(|o| o.is_some()) } /// Query the player for current playback status. pub fn get_playback_status(&self) -> Result { self.connection_path() .playback_status()? .parse() .map_err(DBusError::from) } /// Query player for the state of the "Shuffle" setting. /// /// See: [MPRIS2 specification about `Shuffle`][shuffle]. /// /// [shuffle]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub fn get_shuffle(&self) -> Result { self.connection_path().shuffle().map_err(DBusError::from) } /// Gets the "Shuffle" setting, if the player indicates that it supports it. /// /// Return [`Some`] containing the current value of the shuffle setting. If the setting is not /// supported, will return [`None`] pub fn checked_get_shuffle(&self) -> Result, DBusError> { if self.can_shuffle()? { Ok(Some(self.get_shuffle()?)) } else { Ok(None) } } /// Set the "Shuffle" setting of the player. /// /// See: [MPRIS2 specification about `Shuffle`][shuffle]. /// /// [shuffle]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub fn set_shuffle(&self, state: bool) -> Result<(), DBusError> { self.connection_path() .set_shuffle(state) .map_err(DBusError::from) } /// Set the "Shuffle" setting of the player, if the player indicates that it supports the /// "Shuffle" setting and can be controlled. /// /// Returns a [`bool`] to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Shuffle`][shuffle]. /// /// [shuffle]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Shuffle pub fn checked_set_shuffle(&self, state: bool) -> Result { if self.can_control()? && self.can_shuffle()? { self.set_shuffle(state) .map(|_| true) .map_err(DBusError::from) } else { Ok(false) } } /// Query the player for the current loop status. /// /// See: [MPRIS2 specification about `LoopStatus`][loop_status]. /// /// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub fn get_loop_status(&self) -> Result { self.connection_path() .loop_status()? .parse() .map_err(DBusError::from) } /// Gets the "LoopStatus" setting, if the player indicates that it supports it. /// /// Returns [`Some`] containing the current value of the loop setting. If the setting is not /// supported, returns [`None`] pub fn checked_get_loop_status(&self) -> Result, DBusError> { if self.can_loop()? { Ok(Some(self.get_loop_status()?)) } else { Ok(None) } } /// Set the loop status of the player. /// /// See: [MPRIS2 specification about `LoopStatus`][loop_status]. /// /// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub fn set_loop_status(&self, status: LoopStatus) -> Result<(), DBusError> { self.connection_path() .set_loop_status(status.dbus_value()) .map_err(DBusError::from) } /// Set the loop status of the player, if the player indicates that supports it and that it can /// be controlled. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `LoopStatus`][loop_status]. /// /// [loop_status]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:LoopStatus pub fn checked_set_loop_status(&self, status: LoopStatus) -> Result { if self.can_control()? && self.can_loop()? { self.set_loop_status(status) .map(|_| true) .map_err(DBusError::from) } else { Ok(false) } } /// Get the volume of the player. /// /// Volume should be between 0.0 and 1.0. Above 1.0 is possible, but not /// recommended. /// /// See: [MPRIS2 specification about `Volume`][vol]. /// /// [vol]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub fn get_volume(&self) -> Result { self.connection_path().volume().map_err(DBusError::from) } /// Gets the "Volume" setting, if the player indicates that it supports it. /// /// Returns [`Some`] containing the current value of the position. If the setting is not /// supported, returns [`None`] pub fn checked_get_volume(&self) -> Result, DBusError> { if self.has_volume()? { Ok(Some(self.get_volume()?)) } else { Ok(None) } } /// Set the volume of the player. /// /// Volume should be between 0.0 and 1.0. Above 1.0 is possible, but not /// recommended. /// /// See: [MPRIS2 specification about `Volume`][vol]. /// /// [vol]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub fn set_volume(&self, value: f64) -> Result<(), DBusError> { self.connection_path() .set_volume(value.max(0.0)) .map_err(DBusError::from) } /// Set the "Volume" setting of the player, if the player indicates that it supports the /// "Volume" setting and can be controlled. /// /// Returns a boolean to show if the signal was sent or not. /// /// See: [MPRIS2 specification about `Volume`][vol]. /// /// [vol]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub fn checked_set_volume(&self, volume: f64) -> Result { if self.can_control()? && self.has_volume()? { self.set_volume(volume) .map(|_| true) .map_err(DBusError::from) } else { Ok(false) } } /// Set the volume of the player, if the player indicates that it can be /// controlled. /// /// Volume should be between 0.0 and 1.0. Above 1.0 is possible, but not /// recommended. /// /// See: [MPRIS2 specification about `Volume`][vol]. /// /// [vol]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Volume pub fn set_volume_checked(&self, value: f64) -> Result { if self.can_control()? { self.set_volume(value).map(|_| true) } else { Ok(false) } } fn connection_path(&self) -> ConnPath<'_, &Connection> { self.connection.with_path( self.bus_name.as_str().into(), MPRIS2_PATH.into(), self.timeout_ms, ) } /// Blocks until player gets an event on the bus. /// /// Other player events will also be recorded, but will not cause this function to return. Note /// that this will block forever if player is not running. Make sure to check that the player /// is running before calling this method! pub(crate) fn process_events_blocking_until_received(&self) { while !self.connection.has_pending_events(&self.unique_name) { self.connection.process_events_blocking_until_received(); } } /// Return any events that are pending (for this player) on the connection. pub(crate) fn pending_events(&self) -> Vec { self.connection.pending_events(&self.unique_name) } } fn handle_optional_property(result: Result) -> Result, DBusError> { if let Err(ref error) = result { if let Some(error_name) = error.name() { if error_name == "org.freedesktop.DBus.Error.InvalidArgs" { // This property was likely just missing, which means that the player has not // implemented it. return Ok(None); } } } result.map(Some).map_err(|e| e.into()) } /// Checks if the Player implements the `org.mpris.MediaPlayer2.TrackList` interface. fn has_tracklist_interface(connection: ConnPath<'_, &Connection>) -> Result { // Get the introspection XML and look for the substring instead of parsing the XML. Yeah, // pretty dirty, but it's also a lot faster and doesn't require a huge XML library as a // dependency either. // // It's probably accurate enough. use dbus::ffidisp::stdintf::OrgFreedesktopDBusIntrospectable; let xml: String = connection.introspect()?; Ok(xml.contains("org.mpris.MediaPlayer2.TrackList")) } mpris-2.0.1/src/pooled_connection.rs000064400000000000000000000350671046102023000156270ustar 00000000000000use std::cell::RefCell; use std::collections::HashMap; use std::time::{Duration, Instant}; use dbus::ffidisp::{ConnPath, Connection}; use dbus::strings::{BusName, Path}; use dbus::Message; use crate::extensions::DurationExtensions; use crate::metadata::{Metadata, Value}; use crate::player::MPRIS2_PATH; use crate::track_list::TrackID; #[derive(Debug)] pub(crate) struct PooledConnection { connection: Connection, events: RefCell>>, } const GET_NAME_OWNER_TIMEOUT: i32 = 100; // ms const NAME_HAS_OWNER_TIMEOUT: i32 = 100; // ms impl PooledConnection { pub(crate) fn new(connection: Connection) -> Self { // Subscribe to events that relate to players. See [`MprisMessage`] below for details. let _ = connection.add_match( "interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='/org/mpris/MediaPlayer2'", ); let _ = connection.add_match( "interface='org.mpris.MediaPlayer2.Player',member='Seeked',path='/org/mpris/MediaPlayer2'", ); let _ = connection.add_match( "interface='org.mpris.MediaPlayer2.TrackList',path='/org/mpris/MediaPlayer2'", ); let _ = connection.add_match( "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged'", ); PooledConnection { connection, events: RefCell::new(HashMap::new()), } } pub(crate) fn with_path<'a>( &'a self, bus_name: BusName<'a>, path: Path<'a>, timeout_ms: i32, ) -> ConnPath<'_, &'a Connection> { self.connection.with_path(bus_name, path, timeout_ms) } pub(crate) fn underlying(&self) -> &Connection { &self.connection } pub(crate) fn determine_unique_name>(&self, bus_name: S) -> Option { let get_name_owner = Message::new_method_call( "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetNameOwner", ) .unwrap() .append1(bus_name.into()); self.connection .send_with_reply_and_block(get_name_owner, GET_NAME_OWNER_TIMEOUT) .ok() .and_then(|reply| reply.get1()) } pub(crate) fn name_has_owner>(&self, bus_name: S) -> Option { let name_has_owner = Message::new_method_call( "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "NameHasOwner", ) .unwrap() .append1(bus_name.into()); self.connection .send_with_reply_and_block(name_has_owner, NAME_HAS_OWNER_TIMEOUT) .ok() .and_then(|reply| reply.get1()) } /// Returns [`true`] if the given bus name has any pending events waiting to be processed. /// /// If you want to actually act on the messages, use [`pending_events`](Self::pending_events). pub(crate) fn has_pending_events(&self, bus_name: &str) -> bool { self.events .try_borrow() .ok() .map(|map| map.contains_key(bus_name)) .unwrap_or(false) } /// Removes all pending events from a bus' queue and returns them. /// /// If you want to non-destructively check if a bus has anything queued, use /// [`has_pending_events`](Self::has_pending_events). pub(crate) fn pending_events(&self, bus_name: &str) -> Vec { self.events .try_borrow_mut() .ok() .and_then(|mut events| events.remove(bus_name)) .unwrap_or_default() } /// Process events in a blocking fashion until the deadline/timebox [`Duration`] runs out. pub(crate) fn process_events_blocking_for(&self, duration: Duration) { let start = Instant::now(); while start.elapsed() < duration { let ms_left = duration .checked_sub(start.elapsed()) .map(|d| DurationExtensions::as_millis(&d)) .unwrap_or(0); // Don't bother if we have very little time left if ms_left < 2 { break; } if let Some(message) = self .connection .incoming(ms_left as u32) .flat_map(MprisMessage::try_parse) .next() { self.process_message(message); } } } /// Process events in a blocking fashion until any new event is found. pub(crate) fn process_events_blocking_until_received(&self) { // Loop will repeat every milliseconds, just waiting for new events to appear. let loop_interval = 5000; // ms loop { if let Some(message) = self .connection .incoming(loop_interval) .flat_map(MprisMessage::try_parse) .next() { self.process_message(message); return; } } } /// Takes a message and processes it appropriately. Returns the affected bus name, and a borrow /// to the generated [`MprisEvent`], if applicable. fn process_message(&self, message: MprisMessage) { let mut events = match self.events.try_borrow_mut() { Ok(val) => val, Err(_) => { // Drop the message. This is a better evil than triggering a panic inside a library // like this. return; } }; match message { MprisMessage::NameOwnerChanged { new_owner, old_owner, } => { // If `new_owner` is empty, then the client has quit. if new_owner.is_empty() { // Clear out existing events, if any. Then add a "PlayerQuit" event on the // queue. events.insert(old_owner, vec![MprisEvent::PlayerQuit]); } } MprisMessage::PlayerPropertiesChanged { unique_name } => { events .entry(unique_name) .or_default() .push(MprisEvent::PlayerPropertiesChanged); } MprisMessage::Seeked { unique_name, position_in_us, } => { events .entry(unique_name) .or_default() .push(MprisEvent::Seeked { position_in_us }); } MprisMessage::TrackListPropertiesChanged { unique_name } => { events .entry(unique_name) .or_default() .push(MprisEvent::TrackListPropertiesChanged); } MprisMessage::TrackListReplaced { unique_name, ids, .. } => { events .entry(unique_name) .or_default() .push(MprisEvent::TrackListReplaced { ids: ids.into_iter().map(TrackID::from).collect(), }); } MprisMessage::TrackAdded { unique_name, after_id, metadata, } => { events .entry(unique_name) .or_default() .push(MprisEvent::TrackAdded { after_id, metadata: Metadata::from(metadata), }); } MprisMessage::TrackRemoved { unique_name, id } => { events .entry(unique_name) .or_default() .push(MprisEvent::TrackRemoved { id }); } MprisMessage::TrackMetadataChanged { unique_name, old_id, metadata, } => { events .entry(unique_name) .or_default() .push(MprisEvent::TrackMetadataChanged { old_id, metadata: Metadata::from(metadata), }); } } } } impl From for PooledConnection { fn from(connection: Connection) -> Self { PooledConnection::new(connection) } } /// Event that a Player / ProgressTracker / Event iterator should react on. These are read via the /// bus and placed on queues for each player. When a component asks for pending events of a player /// they will be returned in the same order as they were emitted in. #[derive(Debug)] pub(crate) enum MprisEvent { PlayerQuit, PlayerPropertiesChanged, Seeked { position_in_us: u64, }, TrackListPropertiesChanged, TrackListReplaced { ids: Vec, }, TrackAdded { after_id: TrackID, metadata: Metadata, }, TrackRemoved { id: TrackID, }, TrackMetadataChanged { old_id: TrackID, metadata: Metadata, }, } /// Easier to use representation of supported [`D-Bus message`](Message). #[derive(Debug)] pub(crate) enum MprisMessage { NameOwnerChanged { new_owner: String, old_owner: String, }, PlayerPropertiesChanged { unique_name: String, }, Seeked { unique_name: String, position_in_us: u64, }, TrackListPropertiesChanged { unique_name: String, }, TrackListReplaced { unique_name: String, ids: Vec, _current_id: TrackID, }, TrackAdded { unique_name: String, after_id: TrackID, metadata: HashMap, }, TrackRemoved { unique_name: String, id: TrackID, }, TrackMetadataChanged { unique_name: String, old_id: TrackID, metadata: HashMap, }, } impl MprisMessage { /// Tries to convert the provided [`D-Bus message`](Message) into a MprisMessage; returns [`None`] if the /// message was not supported. fn try_parse(message: Message) -> Option { MprisMessage::try_parse_name_owner_changed(&message) .or_else(|| MprisMessage::try_parse_mpris_signal(&message)) } /// Return a [`MprisMessage::NameOwnerChanged`] if the provided D-Bus message is a /// org.freedesktop.DBus NameOwnerChanged message. fn try_parse_name_owner_changed(message: &Message) -> Option { match (message.sender(), message.member()) { (Some(ref sender), Some(ref member)) if &**sender == "org.freedesktop.DBus" && &**member == "NameOwnerChanged" => { let mut iter = message.iter_init(); let name: String = iter.read().ok()?; if !name.starts_with("org.mpris.") { return None; } let old_owner: String = iter.read().ok()?; let new_owner: String = iter.read().ok()?; Some(MprisMessage::NameOwnerChanged { new_owner, old_owner, }) } _ => None, } } fn try_parse_mpris_signal(message: &Message) -> Option { if let Some(ref path) = message.path() { if &**path == MPRIS2_PATH { let member = message .member() .map(|member| member.to_string()) .unwrap_or_else(String::default); return match member.as_ref() { "PropertiesChanged" => try_parse_properties_changed(message), "Seeked" => try_parse_seeked(message), "TrackListReplaced" => try_parse_tracklist_replaced(message), "TrackAdded" => try_parse_track_added(message), "TrackRemoved" => try_parse_track_removed(message), "TrackMetadataChanged" => try_parse_track_metadata_changed(message), _ => None, }; } } None } } fn try_parse_properties_changed(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let interface_name: String = iter.read().ok()?; match interface_name.as_ref() { "org.mpris.MediaPlayer2.Player" => { Some(MprisMessage::PlayerPropertiesChanged { unique_name }) } "org.mpris.MediaPlayer2.TrackList" => { Some(MprisMessage::TrackListPropertiesChanged { unique_name }) } _ => None, } } fn try_parse_seeked(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let position_in_us: u64 = iter.read().ok()?; Some(MprisMessage::Seeked { unique_name, position_in_us, }) } fn try_parse_tracklist_replaced(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let ids: Vec> = iter.read().ok()?; let current_id: Path<'_> = iter.read().ok()?; Some(MprisMessage::TrackListReplaced { unique_name, ids: ids.into_iter().map(TrackID::from).collect(), _current_id: TrackID::from(current_id), }) } fn try_parse_track_added(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let metadata: HashMap = iter.read().ok()?; let after_id: Path<'_> = iter.read().ok()?; Some(MprisMessage::TrackAdded { unique_name, metadata, after_id: TrackID::from(after_id), }) } fn try_parse_track_removed(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let id: Path<'_> = iter.read().ok()?; Some(MprisMessage::TrackRemoved { unique_name, id: TrackID::from(id), }) } fn try_parse_track_metadata_changed(message: &Message) -> Option { let unique_name = message.sender().map(|bus_name| bus_name.to_string())?; let mut iter = message.iter_init(); let old_id: Path<'_> = iter.read().ok()?; let metadata: HashMap = iter.read().ok()?; Some(MprisMessage::TrackMetadataChanged { unique_name, old_id: TrackID::from(old_id), metadata, }) } mpris-2.0.1/src/progress.rs000064400000000000000000000421571046102023000137700ustar 00000000000000use std::time::{Duration, Instant}; use thiserror::Error; use super::{DBusError, LoopStatus, PlaybackStatus, TrackList, TrackListError}; use crate::extensions::DurationExtensions; use crate::metadata::Metadata; use crate::player::Player; use crate::pooled_connection::MprisEvent; /// Struct containing information about current progress of a [`Player`]. /// /// It has access to the metadata of the current track, as well as information about the current /// position of the track. /// /// It is up to you to decide on how outdated information you want to rely on when implementing /// progress rendering. #[derive(Debug)] pub struct Progress { metadata: Metadata, playback_status: PlaybackStatus, shuffle: bool, loop_status: LoopStatus, /// When this Progress was constructed, in order to calculate how old it is. instant: Instant, position: Duration, rate: f64, current_volume: f64, } /// Controller for calculating [`Progress`] and maintaining a [`TrackList`] (if supported) for a given [`Player`]. /// /// Call the [`tick`](Self::tick) method to get the most current [`Progress`] data. #[derive(Debug)] pub struct ProgressTracker<'a> { player: &'a Player, track_list: Option, interval: Duration, last_tick: Instant, last_progress: Progress, } /// Return value of [`ProgressTracker::tick`](ProgressTracker::tick), which gives details about the latest refresh. #[derive(Debug)] pub struct ProgressTick<'a> { /// [`true`] if [`Player`] quit. This likely means that the player is no longer running. /// /// If the player is no longer running, then fetching new data will not be possible, /// so they will all be reused ([`progress_changed`](Self::progress_changed) and /// [`track_list_changed`](Self::track_list_changed) should all be [`false`]). pub player_quit: bool, /// [`true`] if [`Progress`] data changed (beyond the calculated `position`) /// /// **Examples:** /// /// * Playback status changed /// * Metadata changed for the track /// * Volume was decreased pub progress_changed: bool, /// [`true`] if [`TrackList`] data changed. This will always be [`false`] if player does not support /// track lists. /// /// **Examples:** /// /// * Track was added /// * Track was removed /// * Metadata changed for a track pub track_list_changed: bool, /// The current [`Progress`] from the [`ProgressTracker`]. [`progress_changed`](Self::progress_changed) /// tells you if this was reused from the last tick or if it's a new one. pub progress: &'a Progress, /// The current [`TrackList`] from the [`ProgressTracker`]. [`track_list_changed`](Self::track_list_changed) /// tells you if this was changed since the last tick. pub track_list: Option<&'a TrackList>, } /// Errors that can occur while refreshing progress. #[derive(Debug, Error)] pub enum ProgressError { /// Something went wrong with the D-Bus communication. See the [`DBusError`] type. #[error("D-Bus communication failed: {0}")] DBusError(#[from] DBusError), /// Something went wrong with the track list. See the [`TrackListError`] type. #[error("TrackList could not be refreshed: {0}")] TrackListError(#[from] TrackListError), } impl<'a> ProgressTracker<'a> { /// Construct a new [`ProgressTracker`] for the provided [`Player`]. /// /// The `interval_ms` value is the desired time between ticks when calling the [`tick`](Self::tick) method. /// See [`tick`](Self::tick) for more information about that. /// /// You probably want to use [`Player::track_progress`] instead of this method. /// /// # Errors /// /// Returns an error in case Player metadata or state retrieval over DBus fails. pub fn new(player: &'a Player, interval_ms: u32) -> Result { Ok(ProgressTracker { player, interval: Duration::from_millis(u64::from(interval_ms)), last_tick: Instant::now(), last_progress: Progress::from_player(player)?, track_list: player.checked_get_track_list()?, }) } /// Returns a [`ProgressTick`] at each interval, or as close to each interval as possible. /// /// The returned struct contains borrows of the current data along with booleans telling you if /// the underlying data changed or not. See [`ProgressTick`] for more information about that. /// /// If there is time left until the next interval window, then the tracker will process DBus /// events to determine if something changed (and potentially perform a full refresh of the /// data). If there is no time left, then the previous data will be reused. /// /// If refreshing failed for some reason the old data will be reused. /// /// It is recommended to call this inside a loop to maintain your progress display. /// /// ## On reusing data /// /// [`Progress`] can be reused until something about the player changes, like track or playback /// status. As long as nothing changes, [`Progress`] can accurately determine playback position /// from timing data. /// /// In addition, [`TrackList`] will maintain a cache of track metadata so as long as the list /// remains static if should be cheap to read from it. /// /// You can use the [`bool`]s in the [`ProgressTick`] to perform optimizations as they tell you if /// any data has changed. If all of them are [`false`] you don't have to treat any of the data as /// dirty. /// /// The calculated [`Progress::position`] might still change depending on the player state, so if /// you want to show the track position you might still want to refresh that part. /// /// # Examples /// /// Simple progress bar of track position: /// /// ```rust,no_run /// # use mpris::{PlayerFinder, Metadata, PlaybackStatus, Progress}; /// use mpris::ProgressTick; /// # use std::time::Duration; /// # fn update_progress_bar(_: Duration) { } /// # let player = PlayerFinder::new().unwrap().find_active().unwrap(); /// # /// // Re-render progress bar every 100ms /// let mut progress_tracker = player.track_progress(100).unwrap(); /// loop { /// let ProgressTick {progress, ..} = progress_tracker.tick(); /// update_progress_bar(progress.position()); /// } /// ``` /// /// Using the [`progress_changed`](ProgressTick::progress_changed) [`bool`]: /// /// ```rust,no_run /// # use mpris::PlayerFinder; /// use mpris::ProgressTick; /// # use std::time::Duration; /// # fn update_progress_bar(_: Duration) { } /// # fn reset_progress_bar(_: Duration, _: Option) { } /// # fn update_track_title(_: Option<&str>) { } /// # /// # let player = PlayerFinder::new().unwrap().find_active().unwrap(); /// # /// // Refresh every 100ms /// let mut progress_tracker = player.track_progress(100).unwrap(); /// loop { /// let ProgressTick {progress, progress_changed, ..} = progress_tracker.tick(); /// if progress_changed { /// update_track_title(progress.metadata().title()); /// reset_progress_bar(progress.position(), progress.length()); /// } else { /// update_progress_bar(progress.position()); /// } /// } /// ``` /// /// Showing only the track list until the player quits: /// /// ```rust,no_run /// # use mpris::{PlayerFinder, TrackList}; /// use mpris::ProgressTick; /// # fn render_track_list(_: &TrackList) { } /// # fn render_track_list_unavailable() { } /// # /// # let player = PlayerFinder::new().unwrap().find_active().unwrap(); /// # /// // Refresh every 10 seconds /// let mut progress_tracker = player.track_progress(10_000).unwrap(); /// loop { /// let ProgressTick {track_list, track_list_changed, player_quit, ..} = progress_tracker.tick(); /// if player_quit { /// break; /// } else if track_list_changed { /// if let Some(list) = track_list { /// render_track_list(list); /// } else { /// render_track_list_unavailable(); /// } /// } /// } /// ``` pub fn tick(&mut self) -> ProgressTick<'_> { let mut player_quit = false; let mut progress_changed = false; let mut track_list_changed = false; let old_shuffle = self.last_progress.shuffle; // Calculate time left until we're expected to return with new data. let time_left = self .interval .checked_sub(self.last_tick.elapsed()) .unwrap_or_else(|| Duration::from_millis(0)); // Refresh events if we're not late. if time_left > Duration::from_millis(0) { self.player .connection() .process_events_blocking_for(time_left); } // Process events that are queued up for us for event in self.player.pending_events().into_iter() { match event { MprisEvent::PlayerQuit => { player_quit = true; break; } MprisEvent::PlayerPropertiesChanged | MprisEvent::Seeked { .. } => { if !progress_changed { progress_changed |= self.refresh_player(); } } MprisEvent::TrackListPropertiesChanged => { track_list_changed |= self.refresh_track_list(); } MprisEvent::TrackListReplaced { ids } => { if let Some(ref mut list) = self.track_list { list.replace(ids.into_iter().collect()); } track_list_changed = true; } MprisEvent::TrackAdded { after_id, metadata } => { if let Some(ref mut list) = self.track_list { list.insert(&after_id, metadata); } track_list_changed = true; } MprisEvent::TrackRemoved { id } => { if let Some(ref mut list) = self.track_list { list.remove(&id); } track_list_changed = true; } MprisEvent::TrackMetadataChanged { old_id, metadata } => { if let Some(ref mut list) = self.track_list { list.replace_track_metadata(&old_id, metadata); } track_list_changed = true; } } } if old_shuffle != self.last_progress.shuffle { // Shuffle changed, which means that the tracklist is likely to have been changed too. // Do a reload, even if track_list_changed was true so the correct order is loaded even // if only a in-place change took place before. track_list_changed |= self.refresh_track_list(); } self.last_tick = Instant::now(); ProgressTick { progress: &self.last_progress, track_list: self.track_list.as_ref(), player_quit, progress_changed, track_list_changed, } } /// Force a refresh right now. /// /// This will ignore the interval and perform a refresh anyway. The new [`Progress`] will be /// saved, and the [`TrackList`] will be refreshed. /// /// # Errors /// /// Returns an error if the refresh failed. pub fn force_refresh(&mut self) -> Result<(), ProgressError> { self.last_progress = Progress::from_player(self.player)?; if let Some(ref mut list) = self.track_list { list.reload(self.player)?; } Ok(()) } fn refresh_player(&mut self) -> bool { if let Ok(progress) = Progress::from_player(self.player) { self.last_progress = progress; return true; } false } fn refresh_track_list(&mut self) -> bool { match self.track_list { Some(ref mut list) => list.reload(self.player).is_ok(), None => false, } } } impl Progress { pub(crate) fn from_player(player: &Player) -> Result { Ok(Progress { metadata: player.get_metadata()?, playback_status: player.get_playback_status()?, shuffle: player.checked_get_shuffle()?.unwrap_or(false), loop_status: player .checked_get_loop_status()? .unwrap_or(LoopStatus::None), rate: player.checked_get_playback_rate()?.unwrap_or(1.0), position: player .checked_get_position()? .unwrap_or_else(|| Duration::new(0, 0)), current_volume: player.checked_get_volume()?.unwrap_or(1.0), instant: Instant::now(), }) } /// The track metadata at the point in time that this Progress was constructed. pub fn metadata(&self) -> &Metadata { &self.metadata } /// The playback status at the point in time that this Progress was constructed. pub fn playback_status(&self) -> PlaybackStatus { self.playback_status } /// The shuffle status at the point in time that this Progress was constructed. pub fn shuffle(&self) -> bool { self.shuffle } /// The loop status at the point in time that this Progress was constructed. pub fn loop_status(&self) -> LoopStatus { self.loop_status } /// The playback rate at the point in time that this Progress was constructed. pub fn playback_rate(&self) -> f64 { self.rate } /// Returns the length of the current track as a [`Duration`]. pub fn length(&self) -> Option { self.metadata.length() } /// Returns the current position of the current track as a [`Duration`]. /// /// This method will calculate the expected position of the track at the instant of the /// invocation using the [`initial_position`](Self::initial_position) and knowledge of how long ago that position was /// determined. /// /// **Note:** Some players might not support this and will return a bad position. Spotify is /// one such example. There is no reliable way of detecting problematic players, so it will be /// up to your client to check for this. /// /// One way of doing this is to query the [`initial_position`](Self::initial_position) for two measures with the /// [`PlaybackStatus::Playing`] and if both are `0`, then it is likely that this client does not /// support positions. pub fn position(&self) -> Duration { self.position + self.elapsed() } /// Returns the position that the current track was at when the [`Progress`] was created. /// /// This is the number that was returned for the [`Position`][position] property in the MPRIS2 interface. /// /// [position]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Property:Position pub fn initial_position(&self) -> Duration { self.position } /// The instant where this [`Progress`] was recorded. /// /// See: [`age`](Self::age). pub fn created_at(&self) -> &Instant { &self.instant } /// Returns the age of the data as a [`Duration`]. /// /// If the [`Progress`] has a high age it is more likely to be out of date. pub fn age(&self) -> Duration { self.instant.elapsed() } /// Returns the player's volume as it was at the time of refresh. /// /// See: [`Player::get_volume`]. pub fn current_volume(&self) -> f64 { self.current_volume } fn elapsed(&self) -> Duration { let elapsed_ms = match self.playback_status { PlaybackStatus::Playing => { DurationExtensions::as_millis(&self.age()) as f64 * self.rate } _ => 0.0, }; Duration::from_millis(elapsed_ms as u64) } } #[cfg(test)] mod test { use super::*; #[test] fn it_progresses_position_when_playing_at_microseconds() { let progress = Progress { metadata: Metadata::new(String::from("id")), playback_status: PlaybackStatus::Playing, shuffle: false, loop_status: LoopStatus::None, rate: 1.0, position: Duration::from_micros_ext(1), current_volume: 0.0, instant: Instant::now(), }; assert_eq!(progress.initial_position(), Duration::from_micros_ext(1)); assert!(progress.position() >= progress.initial_position()); } #[test] fn it_does_not_progress_when_paused() { let progress = Progress { metadata: Metadata::new(String::from("id")), playback_status: PlaybackStatus::Paused, shuffle: false, loop_status: LoopStatus::None, rate: 1.0, position: Duration::from_micros_ext(1336), current_volume: 0.0, instant: Instant::now() - Duration::from_millis(500), }; assert_eq!(progress.position(), progress.initial_position()); } } mpris-2.0.1/src/track_list.rs000064400000000000000000000415211046102023000142550ustar 00000000000000use super::{DBusError, Metadata, Player}; use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::iter::{FromIterator, IntoIterator}; use thiserror::Error; pub(crate) const NO_TRACK: &str = "/org/mpris/MediaPlayer2/TrackList/NoTrack"; /// Represents [the MPRIS `Track_Id` type][track_id]. /// /// ```rust /// use mpris::TrackID; /// let no_track = TrackID::new("/org/mpris/MediaPlayer2/TrackList/NoTrack").unwrap(); /// ``` /// /// TrackIDs must be valid D-Bus object paths according to the spec. /// /// # Errors /// /// Trying to construct a [`TrackID`] from a string that is not a valid D-Bus Path will fail. /// /// ```rust /// # use mpris::TrackID; /// let result = TrackID::new("invalid track ID"); /// assert!(result.is_err()); /// ``` /// /// [track_id]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id #[derive(Debug, Clone, PartialEq, Hash, Eq)] pub struct TrackID(pub(crate) String); /// Represents a [`MediaPlayer2.TrackList`][track_list]. /// /// This type offers an iterator of the track's metadata, when provided a [`Player`] instance that /// matches the list. /// /// TrackLists cache metadata about tracks so multiple iterations should be fast. It also enables /// signals received from the Player to pre-populate metadata and to keep everything up to date. /// /// [track_list]: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html #[derive(Debug, Default)] pub struct TrackList { ids: Vec, metadata_cache: RefCell>, } /// TrackList-related errors. /// /// This is mostly [`DBusError`] with the extra possibility of borrow errors of the internal metadata /// cache. #[derive(Debug, Error)] pub enum TrackListError { /// Something went wrong with the D-Bus communication. See the [`DBusError`] type. #[error("D-Bus communication failed: {0}")] DBusError(#[from] DBusError), /// Something went wrong with the borrowing logic for the internal cache. Perhaps you have /// multiple borrowed references to the cache live at the same time, for example because of /// multiple iterations? #[error("Could not borrow cache: {0}")] BorrowError(String), } #[derive(Debug)] pub struct MetadataIter { order: Vec, metadata: HashMap, current: usize, } impl<'a> From> for TrackID { fn from(path: dbus::Path<'a>) -> TrackID { TrackID(path.to_string()) } } impl<'a> From<&'a TrackID> for TrackID { fn from(id: &'a TrackID) -> Self { TrackID(id.0.clone()) } } impl From for String { fn from(id: TrackID) -> String { id.0 } } impl<'a> From<&'a TrackID> for dbus::Path<'a> { fn from(id: &'a TrackID) -> dbus::Path<'a> { id.as_path() } } impl fmt::Display for TrackID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl TrackID { /// Create a new [`TrackID`] from a string-like entity. /// /// This is not something you should normally do as the IDs are temporary and will only work if /// the Player knows about it. /// /// However, creating [`TrackID`]s manually can help with test setup, comparisons, etc. /// /// # Example /// ```rust /// use mpris::TrackID; /// let id = TrackID::new("/dbus/path/id").expect("Parse error"); /// ``` pub fn new>(id: S) -> Result { let id = id.into(); // Validate the ID by constructing a dbus::Path. if let Err(error) = dbus::Path::new(id.as_str()) { Err(error) } else { Ok(TrackID(id)) } } /// Return a new [`TrackID`] that matches the MPRIS standard for the "No track" sentinel value. /// /// Some APIs takes this in order to signal a missing value for a track, for example by saying /// that no specific track is playing, or that a track should be added at the start of the /// list instead of after a specific track. /// /// The actual path is "/org/mpris/MediaPlayer2/TrackList/NoTrack". /// /// This value is only valid in some cases. Make sure to read the [MPRIS specification before /// you use this manually][track_id]. /// /// [track_id]: https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html#Simple-Type:Track_Id pub fn no_track() -> Self { TrackID(NO_TRACK.into()) } /// Returns a `&str` variant of the ID. pub fn as_str(&self) -> &str { &*self.0 } pub(crate) fn as_path(&self) -> dbus::Path<'_> { // All inputs to this class should be validated to work with [`dbus::Path`], so unwrapping // should be safe here. dbus::Path::new(self.as_str()).unwrap() } } impl AsRef for TrackID { fn as_ref(&self) -> &str { &self.0 } } impl From> for TrackList { fn from(ids: Vec) -> Self { TrackList::new(ids) } } impl<'a> From>> for TrackList { fn from(ids: Vec>) -> Self { ids.into_iter().map(TrackID::from).collect() } } impl FromIterator for TrackList { fn from_iter>(iter: I) -> Self { TrackList::new(iter.into_iter().collect()) } } impl TrackList { /// Construct a new [`TrackList`] without any existing cache. pub fn new(ids: Vec) -> TrackList { TrackList { metadata_cache: RefCell::new(HashMap::with_capacity(ids.len())), ids, } } /// Get a list of [`TrackID`]s that are part of this [`TrackList`]. The order matters. pub fn ids(&self) -> &[TrackID] { self.ids.as_ref() } /// Returns the number of tracks on the list. pub fn len(&self) -> usize { self.ids.len() } /// If the tracklist is empty or not. pub fn is_empty(&self) -> bool { self.ids.is_empty() } /// Return the [`TrackID`] of the index. Out-of-bounds will result in [`None`]. pub fn get(&self, index: usize) -> Option<&TrackID> { self.ids.get(index) } /// Insert a new track (via its metadata) after another one. If the provided ID cannot be found /// on the list, it will be inserted at the end. /// /// **NOTE:** This is *not* something that will affect a player's actual tracklist; this is /// strictly for client-side representation. Use this if you want to maintain your own instance /// of [`TrackList`] or to feed your code with test fixtures. pub fn insert(&mut self, after: &TrackID, metadata: Metadata) { let new_id = match metadata.track_id() { Some(val) => val, // Cannot insert ID if there is no ID in the metadata. None => return, }; let index = self.index_of_id(after).unwrap_or_else(|| self.ids.len()); // Vec::insert inserts BEFORE the given index, but we need to insert *after* the index. if index >= self.ids.len() { self.ids.push(new_id.clone()); } else { self.ids.insert(index + 1, new_id.clone()); } self.change_metadata(|cache| cache.insert(new_id, metadata)); } /// Removes a track from the list and metadata cache. /// /// **Note:** If the same id is present multiple times, all of them will be removed. pub fn remove(&mut self, id: &TrackID) { self.ids.retain(|existing_id| existing_id != id); self.change_metadata(|cache| cache.remove(id)); } /// Clears the entire list and cache. pub fn clear(&mut self) { self.ids.clear(); self.change_metadata(|cache| cache.clear()); } /// Replace the contents with the contents of the provided list. Cache will be reused when /// possible. pub fn replace(&mut self, other: TrackList) { self.ids = other.ids; let other_cache = other.metadata_cache.into_inner(); self.change_metadata(|self_cache| { // Will overwrite existing keys on conflicts; e.g. the newer cache wins. self_cache.extend(other_cache.into_iter()); }); } /// Adds/updates the metadata cache for a track (as identified by [`Metadata::track_id`]). /// /// The metadata will be added to the cache even if the [`TrackID`] isn't part of the list, but /// will be cleaned out again after the next cache cleanup unless the track in question have /// been added to the list before then. /// /// If provided metadata does not contain a [`TrackID`], the metadata will be discarded. pub fn add_metadata(&mut self, metadata: Metadata) { if let Some(id) = metadata.track_id() { self.change_metadata(|cache| cache.insert(id.to_owned(), metadata)); } } /// Replaces a track on the list with a new entry. The new metadata could contain a new track /// ID, and will in that case replace the old ID on the tracklist. /// /// The new ID (which *might* be identical to the old ID) will be returned by this method. /// /// If the old ID cannot be found, the metadata will be discarded and [`None`] will be returned. /// /// If provided metadata does not contain a [`TrackID`], the metadata will be discarded and /// [`None`] will be returned. pub fn replace_track_metadata( &mut self, old_id: &TrackID, new_metadata: Metadata, ) -> Option { if let Some(new_id) = new_metadata.track_id() { if let Some(index) = self.index_of_id(old_id) { self.ids[index] = new_id.to_owned(); self.change_metadata(|cache| cache.insert(new_id.to_owned(), new_metadata)); return Some(new_id); } } None } /// Iterates the tracks in the tracklist, returning a tuple of [`TrackID`] and [`Metadata`] for that /// track. /// /// [`Metadata`] will be loaded from the provided player when not present in the metadata cache. /// If metadata loading fails, then a [`DBusError`] will be returned instead of the iterator. pub fn metadata_iter(&self, player: &Player) -> Result { self.complete_cache(player)?; let metadata: HashMap<_, _> = self.metadata_cache.clone().into_inner(); let ids = self.ids.clone(); Ok(MetadataIter { current: 0, order: ids, metadata, }) } /// Reloads the tracklist from the given player. This can be compared with loading a new track /// list, but in this case the metadata cache can be maintained for tracks that remain on the /// list. /// /// Cache for tracks that are no longer part of the player's tracklist will be removed. pub fn reload(&mut self, player: &Player) -> Result<(), TrackListError> { self.ids = player.get_track_list()?.ids; self.clear_extra_cache(); Ok(()) } /// Clears all cache and reloads metadata for all tracks. /// /// Cache will be replaced *after* the new metadata has been loaded, so on load errors the /// cache will still be maintained. pub fn reload_cache(&self, player: &Player) -> Result<(), TrackListError> { let id_metadata = self .ids .iter() .cloned() .zip(player.get_tracks_metadata(&self.ids)?); // We only have a &self reference, so fail if we cannot borrow. let mut cache = self.metadata_cache.try_borrow_mut()?; *cache = id_metadata.collect(); Ok(()) } /// Fill in any holes in the cache so that each track on the list has a cached [`Metadata`] entry. /// /// If all tracks already have a cache entry, then this will do nothing. pub fn complete_cache(&self, player: &Player) -> Result<(), TrackListError> { let ids: Vec<_> = self .ids_without_cache() .into_iter() .map(Clone::clone) .collect(); if !ids.is_empty() { let metadata = player.get_tracks_metadata(&ids)?; // We only have a &self reference, so fail if we cannot borrow. let mut cache = self.metadata_cache.try_borrow_mut()?; for info in metadata.into_iter() { if let Some(id) = info.track_id() { cache.insert(id, info); } } } Ok(()) } /// Change metadata cache. As this requires a `&mut self`, the borrow is guaranteed to work. fn change_metadata(&mut self, f: F) -> T where F: FnOnce(&mut HashMap) -> T, { let mut cache = self.metadata_cache.borrow_mut(); // Safe. &mut self reference. f(&mut *cache) } fn ids_without_cache(&self) -> Vec<&TrackID> { let cache = &*self.metadata_cache.borrow(); self.ids .iter() .filter(|id| !cache.contains_key(id)) .collect() } fn clear_extra_cache(&mut self) { let ids: Vec = self.ids().iter().map(TrackID::from).collect(); self.change_metadata(|cache| { // For each id in the list, move the cache out into a new HashMap, then replace the old // one with the new. Only ids on the list will therefore be present on the new list. let new_cache: HashMap = ids .iter() .flat_map(|id| cache.remove(id).map(|value| (id.to_owned(), value))) .collect(); *cache = new_cache; }); } fn index_of_id(&self, id: &TrackID) -> Option { self.ids .iter() .enumerate() .find(|(_, item_id)| *item_id == id) .map(|(index, _)| index) } } impl PartialEq for TrackList { fn eq(&self, other: &TrackList) -> bool { self.ids.eq(&other.ids) } } impl Iterator for MetadataIter { type Item = Metadata; fn next(&mut self) -> Option { match self.order.get(self.current) { Some(next_id) => { self.current += 1; // In case of race conditions with cache population, emit a simple Metadata without // any interesting data in it. Some( self.metadata .remove(next_id) .unwrap_or_else(|| Metadata::new(next_id.clone())), ) } None => None, } } } impl From<::std::cell::BorrowMutError> for TrackListError { fn from(error: ::std::cell::BorrowMutError) -> TrackListError { TrackListError::BorrowError(format!("Could not borrow mutably: {}", error)) } } #[cfg(test)] mod tests { use super::*; fn track_id(s: &str) -> TrackID { TrackID::new(s).expect("Failed to parse a TrackID fixture") } mod track_list { use super::*; #[test] fn it_inserts_after_given_id() { let first = track_id("/path/1"); let third = track_id("/path/3"); let mut list = TrackList { ids: vec![first, third], metadata_cache: RefCell::new(HashMap::new()), }; let metadata = Metadata::new("/path/new"); list.insert(&track_id("/path/1"), metadata); assert_eq!(list.len(), 3); assert_eq!( &list.ids, &[ track_id("/path/1"), track_id("/path/new"), track_id("/path/3") ] ); assert_eq!( list.ids_without_cache(), vec![&track_id("/path/1"), &track_id("/path/3")], ); } #[test] fn it_inserts_at_end_on_missing_id() { let first = track_id("/path/1"); let third = track_id("/path/3"); let mut list = TrackList { ids: vec![first, third], metadata_cache: RefCell::new(HashMap::new()), }; let metadata = Metadata::new("/path/new"); list.insert(&track_id("/path/missing"), metadata); assert_eq!(list.len(), 3); assert_eq!( &list.ids, &[ track_id("/path/1"), track_id("/path/3"), track_id("/path/new"), ] ); assert_eq!( list.ids_without_cache(), vec![&track_id("/path/1"), &track_id("/path/3")], ); } #[test] fn it_inserts_at_end_on_empty() { let mut list = TrackList::default(); let metadata = Metadata::new("/path/new"); list.insert(&track_id("/path/missing"), metadata); assert_eq!(list.len(), 1); assert_eq!(&list.ids, &[track_id("/path/new")]); assert!(list.ids_without_cache().is_empty()); } } }