feat: add mpris2-zbus for org.mpris.MediaPlayer2
This commit is contained in:
parent
4d8815361d
commit
945f80f036
21 changed files with 2203 additions and 0 deletions
2
mpris2/.gitignore
vendored
Normal file
2
mpris2/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
17
mpris2/Cargo.toml
Normal file
17
mpris2/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "mpris2-zbus"
|
||||
description = "zbus-based bindings for MPRIS2 (Media Player Remote Interfacing Specification) on Linux"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
time = { version = "0.3", features = ["parsing"] }
|
||||
zbus = "3.8.0"
|
||||
zvariant = "3.10"
|
||||
|
||||
[dev-dependencies]
|
||||
miette = { version = "4.3", features = ["fancy"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
346
mpris2/LICENSE.md
Normal file
346
mpris2/LICENSE.md
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
# Mozilla Public License Version 2.0
|
||||
|
||||
### 1. Definitions
|
||||
|
||||
**1.1. “Contributor”**
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
**1.2. “Contributor Version”**
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
**1.3. “Contribution”**
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
**1.4. “Covered Software”**
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
**1.5. “Incompatible With Secondary Licenses”**
|
||||
means
|
||||
|
||||
- **(a)** that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
- **(b)** that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
**1.6. “Executable Form”**
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
**1.7. “Larger Work”**
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
**1.8. “License”**
|
||||
means this document.
|
||||
|
||||
**1.9. “Licensable”**
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
**1.10. “Modifications”**
|
||||
means any of the following:
|
||||
|
||||
- **(a)** any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
- **(b)** any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
**1.11. “Patent Claims” of a Contributor**
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
**1.12. “Secondary License”**
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
**1.13. “Source Code Form”**
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
**1.14. “You” (or “Your”)**
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, “You” includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, “control” means **(a)** the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or **(b)** ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
### 2. License Grants and Conditions
|
||||
|
||||
#### 2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
- **(a)** under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
- **(b)** under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
#### 2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
#### 2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
- **(a)** for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
- **(b)** for infringements caused by: **(i)** Your and any other third party's
|
||||
modifications of Covered Software, or **(ii)** the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
- **(c)** under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
#### 2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
#### 2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
#### 2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
#### 2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
### 3. Responsibilities
|
||||
|
||||
#### 3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
#### 3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
- **(a)** such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
- **(b)** You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
#### 3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
#### 3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
#### 3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
### 4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: **(a)** comply with
|
||||
the terms of this License to the maximum extent possible; and **(b)**
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
### 5. Termination
|
||||
|
||||
**5.1.** The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated **(a)** provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and **(b)** on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
**5.2.** If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
### 6. Disclaimer of Warranty
|
||||
|
||||
> Covered Software is provided under this License on an “as is”
|
||||
> basis, without warranty of any kind, either expressed, implied, or
|
||||
> statutory, including, without limitation, warranties that the
|
||||
> Covered Software is free of defects, merchantable, fit for a
|
||||
> particular purpose or non-infringing. The entire risk as to the
|
||||
> quality and performance of the Covered Software is with You.
|
||||
> Should any Covered Software prove defective in any respect, You
|
||||
> (not any Contributor) assume the cost of any necessary servicing,
|
||||
> repair, or correction. This disclaimer of warranty constitutes an
|
||||
> essential part of this License. No use of any Covered Software is
|
||||
> authorized under this License except under this disclaimer.
|
||||
|
||||
### 7. Limitation of Liability
|
||||
|
||||
> Under no circumstances and under no legal theory, whether tort
|
||||
> (including negligence), contract, or otherwise, shall any
|
||||
> Contributor, or anyone who distributes Covered Software as
|
||||
> permitted above, be liable to You for any direct, indirect,
|
||||
> special, incidental, or consequential damages of any character
|
||||
> including, without limitation, damages for lost profits, loss of
|
||||
> goodwill, work stoppage, computer failure or malfunction, or any
|
||||
> and all other commercial damages or losses, even if such party
|
||||
> shall have been informed of the possibility of such damages. This
|
||||
> limitation of liability shall not apply to liability for death or
|
||||
> personal injury resulting from such party's negligence to the
|
||||
> extent applicable law prohibits such limitation. Some
|
||||
> jurisdictions do not allow the exclusion or limitation of
|
||||
> incidental or consequential damages, so this exclusion and
|
||||
> limitation may not apply to You.
|
||||
|
||||
### 8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
### 9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
### 10. Versions of the License
|
||||
|
||||
#### 10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
#### 10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
#### 10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
3
mpris2/README.md
Normal file
3
mpris2/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# mpris2-zbus
|
||||
|
||||
A zbus client proxy for [org.mpris.MediaPlayer2](https://mpris2.readthedocs.io/en/latest/).
|
||||
156
mpris2/examples/list.rs
Normal file
156
mpris2/examples/list.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use mpris2_zbus::{media_player::MediaPlayer, playlists::ordering::PlaylistOrdering};
|
||||
use zbus::Connection;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let connection = Connection::session()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to establish session D-Bus connection")?;
|
||||
let media_players = MediaPlayer::new_all(&connection)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed get available players")?;
|
||||
for media_player in media_players {
|
||||
let name = media_player
|
||||
.identity()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to get identity for media player")?;
|
||||
let desktop_entry = media_player
|
||||
.desktop_entry()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get desktop entry for media player '{}'", name))?;
|
||||
println!("{} ({})", name, desktop_entry);
|
||||
let player = media_player
|
||||
.player()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get player for media player '{}'", name))?;
|
||||
let playback_status = player
|
||||
.playback_status()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to get playback status for media player '{}'", name)
|
||||
})?;
|
||||
println!("\tPlayback Status: {}", playback_status);
|
||||
let position = player
|
||||
.position()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get position for media player '{}'", name))?
|
||||
.map(|s| format!("{} seconds", s.as_seconds_f32()))
|
||||
.unwrap_or_else(|| "N/A".to_owned());
|
||||
println!("\tPosition: {}", position);
|
||||
if !player
|
||||
.can_seek()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get can_seek for media player '{}'", name))?
|
||||
{
|
||||
println!("\tDoesn't support seeking");
|
||||
} else {
|
||||
println!("\tSupports seeking");
|
||||
}
|
||||
let supported_rates = player
|
||||
.available_rates()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get supported rates for media player '{}'", name))?
|
||||
.map(|s| format!("{}x through {}x", s.start(), s.end()))
|
||||
.unwrap_or_else(|| "N/A".to_owned());
|
||||
println!("\tSupported Rates: {}", supported_rates);
|
||||
let current_rate = player
|
||||
.rate()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get current rate for media player '{}'", name))?
|
||||
.map(|s| format!("{}x", s))
|
||||
.unwrap_or_else(|| "N/A".to_owned());
|
||||
println!("\tCurrent Rate: {}", current_rate);
|
||||
let metadata = player
|
||||
.metadata()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get metadata for media player '{}'", name))?;
|
||||
println!("\tMetadata: {}", metadata);
|
||||
let track_list = media_player
|
||||
.track_list()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to get track list for media player '{}'", name))?;
|
||||
if let Some(track_list) = track_list {
|
||||
let tracks = track_list
|
||||
.detailed_tracks()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to get detailed tracks for media player '{}'", name)
|
||||
})?;
|
||||
println!("\tTracks:");
|
||||
for (track, metadata) in tracks {
|
||||
println!("\t\t{}", track);
|
||||
if let Some(title) = metadata.title() {
|
||||
println!("\t\t\tTitle: {}", title);
|
||||
}
|
||||
if let Some(album) = metadata.album() {
|
||||
println!("\t\t\tAlbum: {}", album);
|
||||
}
|
||||
if let Some(artists) = metadata.artists() {
|
||||
println!("\t\t\tArtists: {}", artists.join(", "));
|
||||
}
|
||||
if let Some(composers) = metadata.composer() {
|
||||
println!("\t\t\tComposers: {}", composers.join(", "));
|
||||
}
|
||||
if let Some(bpm) = metadata.bpm() {
|
||||
println!("\t\t\tBPM: {}", bpm);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("\tTracks: N/A");
|
||||
}
|
||||
let playlists = media_player
|
||||
.playlists()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to get playlists interface for media player '{}'",
|
||||
name
|
||||
)
|
||||
})?;
|
||||
if let Some(playlists) = playlists {
|
||||
println!("\tPlaylists:");
|
||||
let playlist_count = playlists
|
||||
.playlist_count()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to get playlist count for media player '{}'", name)
|
||||
})?;
|
||||
if playlist_count > 0 {
|
||||
let playlists = playlists
|
||||
.get_playlists(0, playlist_count, PlaylistOrdering::Alphabetical, false)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to get playlists for media player '{}'", name)
|
||||
})?;
|
||||
for playlist in playlists {
|
||||
println!("\t{}", playlist.id());
|
||||
println!("\t\tName: {}", playlist.name());
|
||||
println!("\t\tIcon: {}", playlist.icon());
|
||||
}
|
||||
} else {
|
||||
println!("\t\t None :(");
|
||||
}
|
||||
} else {
|
||||
println!("\tPlaylists: N/A");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
5
mpris2/src/bindings.rs
Normal file
5
mpris2/src/bindings.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
pub mod media_player;
|
||||
pub mod player;
|
||||
pub mod playlist;
|
||||
pub mod track_list;
|
||||
63
mpris2/src/bindings/media_player.rs
Normal file
63
mpris2/src/bindings/media_player.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
//! # DBus interface proxies for: `org.mpris.MediaPlayer2`
|
||||
//!
|
||||
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
|
||||
//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.firefox.instance103520' on session bus`.
|
||||
//!
|
||||
//! You may prefer to adapt it, instead of using it verbatim.
|
||||
//!
|
||||
//! More information can be found in the
|
||||
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
|
||||
//! section of the zbus documentation.
|
||||
//!
|
||||
//! This DBus object implements
|
||||
//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html),
|
||||
//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used:
|
||||
//!
|
||||
//! * [`zbus::fdo::PropertiesProxy`]
|
||||
//! * [`zbus::fdo::IntrospectableProxy`]
|
||||
//! * [`zbus::fdo::PeerProxy`]
|
||||
//!
|
||||
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
|
||||
use zbus::dbus_proxy;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait MediaPlayer2 {
|
||||
/// Quit method
|
||||
fn quit(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Raise method
|
||||
fn raise(&self) -> zbus::Result<()>;
|
||||
|
||||
/// CanQuit property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_quit(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanRaise property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_raise(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// DesktopEntry property
|
||||
#[dbus_proxy(property)]
|
||||
fn desktop_entry(&self) -> zbus::Result<String>;
|
||||
|
||||
/// HasTrackList property
|
||||
#[dbus_proxy(property)]
|
||||
fn has_track_list(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// Identity property
|
||||
#[dbus_proxy(property)]
|
||||
fn identity(&self) -> zbus::Result<String>;
|
||||
|
||||
/// SupportedMimeTypes property
|
||||
#[dbus_proxy(property)]
|
||||
fn supported_mime_types(&self) -> zbus::Result<Vec<String>>;
|
||||
|
||||
/// SupportedUriSchemes property
|
||||
#[dbus_proxy(property)]
|
||||
fn supported_uri_schemes(&self) -> zbus::Result<Vec<String>>;
|
||||
}
|
||||
131
mpris2/src/bindings/player.rs
Normal file
131
mpris2/src/bindings/player.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
//! # DBus interface proxies for: `org.mpris.MediaPlayer2.Player`
|
||||
//!
|
||||
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
|
||||
//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.firefox.instance103520' on session bus`.
|
||||
//!
|
||||
//! You may prefer to adapt it, instead of using it verbatim.
|
||||
//!
|
||||
//! More information can be found in the
|
||||
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
|
||||
//! section of the zbus documentation.
|
||||
//!
|
||||
//! This DBus object implements
|
||||
//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html),
|
||||
//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used:
|
||||
//!
|
||||
//! * [`zbus::fdo::PropertiesProxy`]
|
||||
//! * [`zbus::fdo::IntrospectableProxy`]
|
||||
//! * [`zbus::fdo::PeerProxy`]
|
||||
//!
|
||||
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
|
||||
use crate::track::TrackId;
|
||||
use zbus::dbus_proxy;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2.Player",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait Player {
|
||||
/// Next method
|
||||
fn next(&self) -> zbus::Result<()>;
|
||||
|
||||
/// OpenUri method
|
||||
fn open_uri(&self, uri: &str) -> zbus::Result<()>;
|
||||
|
||||
/// Pause method
|
||||
fn pause(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Play method
|
||||
fn play(&self) -> zbus::Result<()>;
|
||||
|
||||
/// PlayPause method
|
||||
fn play_pause(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Previous method
|
||||
fn previous(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Seek method
|
||||
fn seek(&self, offset: i64) -> zbus::Result<()>;
|
||||
|
||||
/// SetPosition method
|
||||
fn set_position(&self, track_id: &TrackId, position: i64) -> zbus::Result<()>;
|
||||
|
||||
/// Stop method
|
||||
fn stop(&self) -> zbus::Result<()>;
|
||||
|
||||
/// Seeked signal
|
||||
#[dbus_proxy(signal)]
|
||||
fn seeked(&self, position: i64) -> zbus::Result<()>;
|
||||
|
||||
/// CanControl property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_control(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanGoNext property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_go_next(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanGoPrevious property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_go_previous(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanPause property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_pause(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanPlay property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_play(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// CanSeek property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_seek(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// MaximumRate property
|
||||
#[dbus_proxy(property)]
|
||||
fn maximum_rate(&self) -> zbus::Result<f64>;
|
||||
|
||||
/// Metadata property
|
||||
#[dbus_proxy(property)]
|
||||
fn metadata(
|
||||
&self,
|
||||
) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>;
|
||||
|
||||
/// MinimumRate property
|
||||
#[dbus_proxy(property)]
|
||||
fn minimum_rate(&self) -> zbus::Result<f64>;
|
||||
|
||||
/// PlaybackStatus property
|
||||
#[dbus_proxy(property)]
|
||||
fn playback_status(&self) -> zbus::Result<String>;
|
||||
|
||||
/// Position property
|
||||
#[dbus_proxy(property)]
|
||||
fn position(&self) -> zbus::Result<i64>;
|
||||
|
||||
/// Rate property
|
||||
#[dbus_proxy(property)]
|
||||
fn rate(&self) -> zbus::Result<f64>;
|
||||
#[dbus_proxy(property)]
|
||||
fn set_rate(&self, value: f64) -> zbus::Result<()>;
|
||||
|
||||
/// Shuffle property (optional)
|
||||
#[dbus_proxy(property)]
|
||||
fn shuffle(&self) -> zbus::Result<bool>;
|
||||
#[dbus_proxy(property)]
|
||||
fn set_shuffle(&self, value: bool) -> zbus::Result<()>;
|
||||
|
||||
/// LoopStatus property (optional)
|
||||
#[dbus_proxy(property)]
|
||||
fn loop_status(&self) -> zbus::Result<String>;
|
||||
#[dbus_proxy(property)]
|
||||
fn set_loop_status(&self, value: String) -> zbus::Result<()>;
|
||||
|
||||
/// Volume property
|
||||
#[dbus_proxy(property)]
|
||||
fn volume(&self) -> zbus::Result<f64>;
|
||||
#[dbus_proxy(property)]
|
||||
fn set_volume(&self, value: f64) -> zbus::Result<()>;
|
||||
}
|
||||
57
mpris2/src/bindings/playlist.rs
Normal file
57
mpris2/src/bindings/playlist.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
//! # DBus interface proxies for: `org.mpris.MediaPlayer2`, `org.mpris.MediaPlayer2.Player`, `org.mpris.MediaPlayer2.TrackList`, `org.mpris.MediaPlayer2.Playlists`
|
||||
//!
|
||||
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
|
||||
//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.org.gnome.Music' on session bus`.
|
||||
//!
|
||||
//! You may prefer to adapt it, instead of using it verbatim.
|
||||
//!
|
||||
//! More information can be found in the
|
||||
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
|
||||
//! section of the zbus documentation.
|
||||
//!
|
||||
//! This DBus object implements
|
||||
//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html),
|
||||
//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used:
|
||||
//!
|
||||
//! * [`zbus::fdo::IntrospectableProxy`]
|
||||
//! * [`zbus::fdo::PropertiesProxy`]
|
||||
//!
|
||||
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
|
||||
use crate::playlists::{id::PlaylistId, ordering::PlaylistOrdering, playlist::Playlist};
|
||||
use zbus::dbus_proxy;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2.Playlists",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait Playlists {
|
||||
/// ActivatePlaylist method
|
||||
fn activate_playlist(&self, playlist_id: &PlaylistId) -> zbus::Result<()>;
|
||||
|
||||
/// GetPlaylists method
|
||||
fn get_playlists(
|
||||
&self,
|
||||
index: u32,
|
||||
max_count: u32,
|
||||
order: PlaylistOrdering,
|
||||
reverse_order: bool,
|
||||
) -> zbus::Result<Vec<Playlist>>;
|
||||
|
||||
/// PlaylistChanged signal
|
||||
#[dbus_proxy(signal)]
|
||||
fn playlist_changed(&self, playlist: Playlist) -> zbus::Result<()>;
|
||||
|
||||
/// ActivePlaylist property
|
||||
#[dbus_proxy(property)]
|
||||
fn active_playlist(&self) -> zbus::Result<(bool, Playlist)>;
|
||||
|
||||
/// Orderings property
|
||||
#[dbus_proxy(property)]
|
||||
fn orderings(&self) -> zbus::Result<Vec<String>>;
|
||||
|
||||
/// PlaylistCount property
|
||||
#[dbus_proxy(property)]
|
||||
fn playlist_count(&self) -> zbus::Result<u32>;
|
||||
}
|
||||
58
mpris2/src/bindings/track_list.rs
Normal file
58
mpris2/src/bindings/track_list.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
//! # DBus interface proxies for: `org.mpris.MediaPlayer2`, `org.mpris.MediaPlayer2.Player`, `org.mpris.MediaPlayer2.TrackList`, `org.mpris.MediaPlayer2.Playlists`
|
||||
//!
|
||||
//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data.
|
||||
//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.org.gnome.Music' on session bus`.
|
||||
//!
|
||||
//! You may prefer to adapt it, instead of using it verbatim.
|
||||
//!
|
||||
//! More information can be found in the
|
||||
//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html)
|
||||
//! section of the zbus documentation.
|
||||
//!
|
||||
//! This DBus object implements
|
||||
//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html),
|
||||
//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used:
|
||||
//!
|
||||
//! * [`zbus::fdo::IntrospectableProxy`]
|
||||
//! * [`zbus::fdo::PropertiesProxy`]
|
||||
//!
|
||||
//! …consequently `zbus-xmlgen` did not generate code for the above interfaces.
|
||||
|
||||
use crate::track::TrackId;
|
||||
use zbus::dbus_proxy;
|
||||
|
||||
#[dbus_proxy(
|
||||
interface = "org.mpris.MediaPlayer2.TrackList",
|
||||
default_path = "/org/mpris/MediaPlayer2"
|
||||
)]
|
||||
trait TrackList {
|
||||
/// AddTrack method
|
||||
fn add_track(&self, uri: &str, after_track: &TrackId, set_as_current: bool)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
/// GetTracksMetadata method
|
||||
fn get_tracks_metadata(
|
||||
&self,
|
||||
track_ids: Vec<TrackId>,
|
||||
) -> zbus::Result<Vec<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>>;
|
||||
|
||||
/// GoTo method
|
||||
fn go_to(&self, track_id: &TrackId) -> zbus::Result<()>;
|
||||
|
||||
/// RemoveTrack method
|
||||
fn remove_track(&self, track_id: &TrackId) -> zbus::Result<()>;
|
||||
|
||||
/// TrackListReplaced signal
|
||||
#[dbus_proxy(signal)]
|
||||
fn track_list_replaced(&self, tracks: Vec<TrackId>, current_track: TrackId)
|
||||
-> zbus::Result<()>;
|
||||
|
||||
/// CanEditTracks property
|
||||
#[dbus_proxy(property)]
|
||||
fn can_edit_tracks(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// Tracks property
|
||||
#[dbus_proxy(property)]
|
||||
fn tracks(&self) -> zbus::Result<Vec<TrackId>>;
|
||||
}
|
||||
51
mpris2/src/error.rs
Normal file
51
mpris2/src/error.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Invalid enum variant when converting from String.
|
||||
#[error("Invalid enum variant: {got}, expected something in {expected:?}")]
|
||||
InvalidEnum {
|
||||
got: String,
|
||||
expected: &'static [&'static str],
|
||||
},
|
||||
|
||||
#[error("Tried to extract a {wanted}, but it was actually {actual}")]
|
||||
IncorrectVariant {
|
||||
wanted: &'static str,
|
||||
actual: &'static str,
|
||||
},
|
||||
|
||||
#[error("Tried to convert Value::{wanted}, but it was got {actual:?}")]
|
||||
IncorrectValue {
|
||||
wanted: &'static str,
|
||||
actual: zvariant::OwnedValue,
|
||||
},
|
||||
|
||||
/// A zbus error.
|
||||
#[error("zbus error: {0}")]
|
||||
Zbus(zbus::Error),
|
||||
|
||||
/// A zbus::fdo error.
|
||||
#[error("zbus fdo error: {0}")]
|
||||
Fdo(zbus::fdo::Error),
|
||||
}
|
||||
|
||||
impl From<zbus::fdo::Error> for Error {
|
||||
fn from(err: zbus::fdo::Error) -> Self {
|
||||
match err {
|
||||
zbus::fdo::Error::ZBus(err) => Self::Zbus(err),
|
||||
_ => Self::Fdo(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<zbus::Error> for Error {
|
||||
fn from(err: zbus::Error) -> Self {
|
||||
match err {
|
||||
zbus::Error::FDO(err) => Self::Fdo(*err),
|
||||
_ => Self::Zbus(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
21
mpris2/src/lib.rs
Normal file
21
mpris2/src/lib.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
pub mod bindings;
|
||||
pub mod error;
|
||||
pub mod media_player;
|
||||
pub mod metadata;
|
||||
pub mod player;
|
||||
pub mod playlists;
|
||||
pub mod track;
|
||||
pub mod track_list;
|
||||
|
||||
pub(crate) fn handle_optional<T>(input: zbus::Result<T>) -> error::Result<Option<T>> {
|
||||
match input {
|
||||
Ok(input) => Ok(Some(input)),
|
||||
Err(zbus::Error::FDO(fdo_error))
|
||||
if matches!(*fdo_error, zbus::fdo::Error::NotSupported(_)) =>
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => Err(error::Error::from(err)),
|
||||
}
|
||||
}
|
||||
111
mpris2/src/media_player.rs
Normal file
111
mpris2/src/media_player.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use crate::{
|
||||
bindings::{
|
||||
media_player::MediaPlayer2Proxy, player::PlayerProxy, playlist::PlaylistsProxy,
|
||||
track_list::TrackListProxy,
|
||||
},
|
||||
error::{Error, Result},
|
||||
player::Player,
|
||||
playlists::Playlists,
|
||||
track_list::TrackList,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use zbus::{fdo::DBusProxy, names::OwnedBusName, Connection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaPlayer {
|
||||
proxy: MediaPlayer2Proxy<'static>,
|
||||
}
|
||||
|
||||
impl MediaPlayer {
|
||||
/// Creates a new instance of the `org.mpris.MediaPlayer2` interface.
|
||||
pub async fn new(connection: &Connection, name: OwnedBusName) -> Result<Self> {
|
||||
MediaPlayer2Proxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await
|
||||
.map(Self::from)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Gets the names of all the MPRIS players that are available on the current session.
|
||||
pub async fn available_players(connection: &Connection) -> Result<Vec<OwnedBusName>> {
|
||||
let dbus = DBusProxy::builder(connection)
|
||||
.path("/org/freedesktop/DBus")?
|
||||
.build()
|
||||
.await?;
|
||||
let mut players = Vec::new();
|
||||
for name in dbus.list_names().await? {
|
||||
if name.starts_with("org.mpris.MediaPlayer2.") {
|
||||
players.push(name);
|
||||
}
|
||||
}
|
||||
Ok(players)
|
||||
}
|
||||
|
||||
/// Gets a new instance of all the MPRIS players that are available on the current session.
|
||||
pub async fn new_all(connection: &Connection) -> Result<Vec<Self>> {
|
||||
let players = Self::available_players(connection).await?;
|
||||
let mut instances = Vec::with_capacity(players.len());
|
||||
for player in players {
|
||||
instances.push(Self::new(connection, player).await?);
|
||||
}
|
||||
Ok(instances)
|
||||
}
|
||||
|
||||
/// Returns an instance to the `org.mpris.MediaPlayer2.Player` interface of this object.
|
||||
pub async fn player(&self) -> Result<Player> {
|
||||
PlayerProxy::builder(self.proxy.connection())
|
||||
.destination(self.proxy.destination().to_owned())?
|
||||
.build()
|
||||
.await
|
||||
.map(Player::from)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Returns an instance to the `org.mpris.MediaPlayer2.TrackList` interface of this object,
|
||||
/// if a track list is available.
|
||||
pub async fn track_list(&self) -> Result<Option<TrackList>> {
|
||||
if self.proxy.has_track_list().await? {
|
||||
TrackListProxy::builder(self.proxy.connection())
|
||||
.destination(self.proxy.destination().to_owned())?
|
||||
.build()
|
||||
.await
|
||||
.map(TrackList::from)
|
||||
.map(Some)
|
||||
.map_err(Error::from)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an instance to the `org.mpris.MediaPlayer2.Playlists` interface of this object,
|
||||
/// if a track list is available.
|
||||
pub async fn playlists(&self) -> Result<Option<Playlists>> {
|
||||
if self.proxy.has_track_list().await? {
|
||||
PlaylistsProxy::builder(self.proxy.connection())
|
||||
.destination(self.proxy.destination().to_owned())?
|
||||
.build()
|
||||
.await
|
||||
.map(Playlists::from)
|
||||
.map(Some)
|
||||
.map_err(Error::from)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MediaPlayer {
|
||||
type Target = MediaPlayer2Proxy<'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.proxy
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaPlayer2Proxy<'static>> for MediaPlayer {
|
||||
fn from(proxy: MediaPlayer2Proxy<'static>) -> Self {
|
||||
Self { proxy }
|
||||
}
|
||||
}
|
||||
533
mpris2/src/metadata.rs
Normal file
533
mpris2/src/metadata.rs
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use crate::error::{Error, Result};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use zbus::zvariant::{OwnedObjectPath, Value as ZValue};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Metadata {
|
||||
inner: HashMap<String, MetadataValue>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// `xesam:album`: The track artist(s).
|
||||
pub fn album(&self) -> Option<String> {
|
||||
self.inner
|
||||
.get("xesam:album")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
}
|
||||
|
||||
/// `xesam:artist`: The track artist(s).
|
||||
pub fn artists(&self) -> Option<Vec<String>> {
|
||||
self.inner
|
||||
.get("xesam:artist")
|
||||
.cloned()
|
||||
.and_then(|artists| artists.try_into_array().ok())
|
||||
.map(|artists| {
|
||||
artists
|
||||
.into_iter()
|
||||
.filter_map(|v| v.try_into_string().ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `xesam:asText`: The track lyrics.
|
||||
pub fn lyrics(&self) -> Option<String> {
|
||||
self.inner
|
||||
.get("xesam:asText")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
}
|
||||
|
||||
/// `xesam:albumArtist`: The album artist(s).
|
||||
pub fn album_artists(&self) -> Option<Vec<String>> {
|
||||
self.inner
|
||||
.get("xesam:albumArtist")
|
||||
.cloned()
|
||||
.and_then(|artists| artists.try_into_array().ok())
|
||||
.map(|artists| {
|
||||
artists
|
||||
.into_iter()
|
||||
.filter_map(|v| v.try_into_string().ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `xesam:audioBPM`: The speed of the music, in beats per minute.
|
||||
pub fn bpm(&self) -> Option<u64> {
|
||||
self.inner
|
||||
.get("xesam:audioBPM")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_uint().ok())
|
||||
}
|
||||
|
||||
/// `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<f64> {
|
||||
self.inner
|
||||
.get("xesam:autoRating")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_double().ok())
|
||||
}
|
||||
|
||||
/// `xesam:composer`: The composer(s) of the track.
|
||||
pub fn composer(&self) -> Option<Vec<String>> {
|
||||
self.inner
|
||||
.get("xesam:composer")
|
||||
.cloned()
|
||||
.and_then(|artists| artists.try_into_array().ok())
|
||||
.map(|artists| {
|
||||
artists
|
||||
.into_iter()
|
||||
.filter_map(|v| v.try_into_string().ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `xesam:contentCreated`: When the track was created. Usually only the year component will be useful.
|
||||
pub fn created(&self) -> Option<OffsetDateTime> {
|
||||
self.inner
|
||||
.get("xesam:contentCreated")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_date().ok())
|
||||
}
|
||||
|
||||
/// `xesam:discNumber`: The disc number on the album that this track is from.
|
||||
pub fn disc_number(&self) -> Option<u64> {
|
||||
self.inner
|
||||
.get("xesam:discNumber")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_uint().ok())
|
||||
}
|
||||
|
||||
/// `xesam:firstUsed`: When the track was first played.
|
||||
pub fn first_played(&self) -> Option<OffsetDateTime> {
|
||||
self.inner
|
||||
.get("xesam:firstUsed")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_date().ok())
|
||||
}
|
||||
|
||||
/// `xesam:genre`: The genre(s) of the track.
|
||||
pub fn genre(&self) -> Option<Vec<String>> {
|
||||
self.inner
|
||||
.get("xesam:genre")
|
||||
.cloned()
|
||||
.and_then(|artists| artists.try_into_array().ok())
|
||||
.map(|artists| {
|
||||
artists
|
||||
.into_iter()
|
||||
.filter_map(|v| v.try_into_string().ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `xesam:lastUsed`: When the track was last played.
|
||||
pub fn last_played(&self) -> Option<OffsetDateTime> {
|
||||
self.inner
|
||||
.get("xesam:lastUsed")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_date().ok())
|
||||
}
|
||||
|
||||
/// `xesam:lyricist`: The lyricist(s) of the track.
|
||||
pub fn lyricist(&self) -> Option<Vec<String>> {
|
||||
self.inner
|
||||
.get("xesam:lyricist")
|
||||
.cloned()
|
||||
.and_then(|artists| artists.try_into_array().ok())
|
||||
.map(|artists| {
|
||||
artists
|
||||
.into_iter()
|
||||
.filter_map(|v| v.try_into_string().ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// `xesam:title`: The track title.
|
||||
pub fn title(&self) -> Option<String> {
|
||||
self.inner
|
||||
.get("xesam:title")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
}
|
||||
|
||||
/// `xesam:trackNumber`: The track number on the album that this track is from.
|
||||
pub fn track_number(&self) -> Option<u64> {
|
||||
self.inner
|
||||
.get("xesam:trackNumber")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_uint().ok())
|
||||
}
|
||||
|
||||
/// `xesam:url`: The location of the media file.
|
||||
pub fn url(&self) -> Option<String> {
|
||||
self.inner
|
||||
.get("xesam:url")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
}
|
||||
|
||||
/// `xesam:useCount`: The number of times the track has been played.
|
||||
pub fn use_count(&self) -> Option<u64> {
|
||||
self.inner
|
||||
.get("xesam:useCount")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_uint().ok())
|
||||
}
|
||||
|
||||
/// `xesam:userRating`: The user's rating of the track.
|
||||
pub fn user_rating(&self) -> Option<f64> {
|
||||
self.inner
|
||||
.get("xesam:userRating")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_double().ok())
|
||||
}
|
||||
|
||||
/// `mpris:trackid`: D-Bus path: A unique identity for this track within the context of an MPRIS object (eg: tracklist).
|
||||
pub fn track_id(&self) -> Option<OwnedObjectPath> {
|
||||
self.inner
|
||||
.get("mpris:trackid")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
.and_then(|path| OwnedObjectPath::try_from(path).ok())
|
||||
}
|
||||
|
||||
/// `mpris:length`: The length of the track in microseconds.
|
||||
pub fn length(&self) -> Option<Duration> {
|
||||
self.inner
|
||||
.get("mpris:length")
|
||||
.cloned()
|
||||
.and_then(|v| match &v {
|
||||
MetadataValue::Int(i) => Some(*i),
|
||||
MetadataValue::UInt(u) => Some(*u as i64),
|
||||
MetadataValue::Str(s) => s.parse().ok(),
|
||||
_ => None,
|
||||
})
|
||||
.map(Duration::microseconds)
|
||||
}
|
||||
|
||||
/// `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<String> {
|
||||
self.inner
|
||||
.get("mpris:artUrl")
|
||||
.cloned()
|
||||
.and_then(|v| v.try_into_string().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Metadata {
|
||||
type Target = HashMap<String, MetadataValue>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Metadata {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Metadata {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{{")?;
|
||||
let mut iter = self.inner.iter().peekable();
|
||||
while let Some((k, v)) = iter.next() {
|
||||
if iter.peek().is_some() {
|
||||
write!(f, "{}: {}, ", k, v)?;
|
||||
} else {
|
||||
write!(f, "{}: {}", k, v)?;
|
||||
}
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, V: Into<ZValue<'a>>> From<HashMap<String, V>> for Metadata {
|
||||
fn from(map: HashMap<String, V>) -> Self {
|
||||
Self {
|
||||
inner: map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, MetadataValue::from(&v.into())))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum MetadataValue {
|
||||
Str(String),
|
||||
Double(f64),
|
||||
Int(i64),
|
||||
UInt(u64),
|
||||
Bool(bool),
|
||||
Array(Vec<MetadataValue>),
|
||||
Dict(HashMap<String, MetadataValue>),
|
||||
__Unsupported,
|
||||
}
|
||||
|
||||
impl MetadataValue {
|
||||
fn variant(&self) -> &'static str {
|
||||
match self {
|
||||
MetadataValue::Str(_) => "Str",
|
||||
MetadataValue::Double(_) => "Double",
|
||||
MetadataValue::Int(_) => "Int",
|
||||
MetadataValue::UInt(_) => "UInt",
|
||||
MetadataValue::Bool(_) => "Bool",
|
||||
MetadataValue::Array(_) => "Array",
|
||||
MetadataValue::Dict(_) => "Dict",
|
||||
MetadataValue::__Unsupported => "Unsupported",
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a string from the variant,
|
||||
/// returning an error if the variant is not a string.
|
||||
pub fn try_into_string(self) -> Result<String> {
|
||||
match self {
|
||||
MetadataValue::Str(s) => Ok(s),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Str",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a string from the variant,
|
||||
/// panicking if the variant is not a string.
|
||||
pub fn into_string(self) -> String {
|
||||
self.try_into_string()
|
||||
.unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract a date/time from the variant,
|
||||
/// returning an error if the variant is not a date/time.
|
||||
pub fn try_into_date(self) -> Result<OffsetDateTime> {
|
||||
let variant = self.variant();
|
||||
match self {
|
||||
MetadataValue::Str(s) => {
|
||||
OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err(
|
||||
|_| Error::IncorrectVariant {
|
||||
wanted: "String (DateTime)",
|
||||
actual: variant,
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "String (DateTime)",
|
||||
actual: variant,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a date/time from the variant,
|
||||
/// panicking if the variant is not a date/time.
|
||||
pub fn into_date(self) -> OffsetDateTime {
|
||||
self.try_into_date().unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract a double from the variant,
|
||||
/// returning an error if the variant is not a double.
|
||||
pub fn try_into_double(self) -> Result<f64> {
|
||||
match self {
|
||||
MetadataValue::Double(d) => Ok(d),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Double",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a double from the variant,
|
||||
/// panicking if the variant is not a double.
|
||||
pub fn into_double(self) -> f64 {
|
||||
self.try_into_double()
|
||||
.unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract an integer from the variant,
|
||||
/// returning an error if the variant is not an integer.
|
||||
pub fn try_into_int(self) -> Result<i64> {
|
||||
match self {
|
||||
MetadataValue::Int(i) => Ok(i),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Int",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract an integer from the variant,
|
||||
/// panicking if the variant is not an integer.
|
||||
pub fn into_int(self) -> i64 {
|
||||
self.try_into_int().unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract an unsigned integer from the variant,
|
||||
/// returning an error if the variant is not an unsigned integer.
|
||||
pub fn try_into_uint(self) -> Result<u64> {
|
||||
match self {
|
||||
MetadataValue::UInt(u) => Ok(u),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "UInt",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract an unsigned integer from the variant,
|
||||
/// panicking if the variant is not an unsigned integer.
|
||||
pub fn into_uint(self) -> u64 {
|
||||
self.try_into_uint().unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract a boolean from the variant,
|
||||
/// returning an error if the variant is not a boolean.
|
||||
pub fn try_into_bool(self) -> Result<bool> {
|
||||
match self {
|
||||
MetadataValue::Bool(b) => Ok(b),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Bool",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a boolean from the variant,
|
||||
/// panicking if the variant is not a boolean.
|
||||
pub fn into_bool(self) -> bool {
|
||||
self.try_into_bool().unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract an array from the variant,
|
||||
/// returning an error if the variant is not an array.
|
||||
pub fn try_into_array(self) -> Result<Vec<MetadataValue>> {
|
||||
match self {
|
||||
MetadataValue::Array(a) => Ok(a),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Array",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract an array from the variant,
|
||||
/// panicking if the variant is not an array.
|
||||
pub fn into_array(self) -> Vec<MetadataValue> {
|
||||
self.try_into_array()
|
||||
.unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
|
||||
/// Tries to extract a dictionary from the variant,
|
||||
/// returning an error if the variant is not a dictionary.
|
||||
/// The dictionary is returned as a map from string keys to values.
|
||||
pub fn try_into_dict(self) -> Result<HashMap<String, MetadataValue>> {
|
||||
match self {
|
||||
MetadataValue::Dict(d) => Ok(d),
|
||||
_ => Err(Error::IncorrectVariant {
|
||||
wanted: "Dict",
|
||||
actual: self.variant(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to extract a dictionary from the variant,
|
||||
/// panicking if the variant is not a dictionary.
|
||||
/// The dictionary is returned as a map from string keys to values.
|
||||
pub fn into_dict(self) -> HashMap<String, MetadataValue> {
|
||||
self.try_into_dict().unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&ZValue<'a>> for MetadataValue {
|
||||
fn from(value: &ZValue) -> Self {
|
||||
match value {
|
||||
ZValue::U8(u) => Self::UInt(*u as u64),
|
||||
ZValue::Bool(b) => Self::Bool(*b),
|
||||
ZValue::I16(i) => Self::Int(*i as i64),
|
||||
ZValue::U16(u) => Self::UInt(*u as u64),
|
||||
ZValue::I32(i) => Self::Int(*i as i64),
|
||||
ZValue::U32(u) => Self::UInt(*u as u64),
|
||||
ZValue::I64(i) => Self::Int(*i),
|
||||
ZValue::U64(u) => Self::UInt(*u),
|
||||
ZValue::F64(f) => Self::Double(*f),
|
||||
ZValue::Str(s) => Self::Str(s.to_string()),
|
||||
ZValue::ObjectPath(path) => Self::Str(path.to_string()),
|
||||
ZValue::Array(a) => Self::Array(a.iter().map(|v| v.into()).collect()),
|
||||
ZValue::Dict(d) => Self::Dict(
|
||||
HashMap::<String, ZValue>::try_from(d.to_owned())
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, (&v).into()))
|
||||
.collect(),
|
||||
),
|
||||
ZValue::Value(value) => Self::from(&**value),
|
||||
_ => Self::__Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MetadataValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::__Unsupported => write!(f, "__Unsupported"),
|
||||
Self::Int(i) => write!(f, "{}", i),
|
||||
Self::UInt(u) => write!(f, "{}", u),
|
||||
Self::Double(d) => write!(f, "{}", d),
|
||||
Self::Str(s) => write!(f, "{}", s),
|
||||
Self::Bool(b) => write!(f, "{}", b),
|
||||
Self::Array(a) => write!(f, "{:?}", a),
|
||||
Self::Dict(d) => {
|
||||
let mut debug_struct = f.debug_struct("Dict");
|
||||
for (k, v) in d {
|
||||
debug_struct.field(k, &v);
|
||||
}
|
||||
debug_struct.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MetadataValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::__Unsupported => write!(f, "__Unsupported"),
|
||||
Self::Int(i) => write!(f, "{}", i),
|
||||
Self::UInt(u) => write!(f, "{}", u),
|
||||
Self::Double(d) => write!(f, "{}", d),
|
||||
Self::Str(s) => write!(f, "\"{}\"", s),
|
||||
Self::Bool(b) => write!(f, "{}", b),
|
||||
Self::Array(a) => {
|
||||
write!(f, "[")?;
|
||||
let mut iter = a.iter().peekable();
|
||||
while let Some(value) = iter.next() {
|
||||
if iter.peek().is_some() {
|
||||
write!(f, "{}, ", value)?;
|
||||
} else {
|
||||
write!(f, "{}", value)?;
|
||||
}
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
Self::Dict(d) => {
|
||||
write!(f, "{{")?;
|
||||
let mut iter = d.iter().peekable();
|
||||
while let Some((k, v)) = iter.next() {
|
||||
if iter.peek().is_some() {
|
||||
write!(f, "{}: {}, ", k, v)?;
|
||||
} else {
|
||||
write!(f, "{}: {}", k, v)?;
|
||||
}
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
267
mpris2/src/player.rs
Normal file
267
mpris2/src/player.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use crate::{
|
||||
bindings::{media_player::MediaPlayer2Proxy, player::PlayerProxy},
|
||||
error::{Error, Result},
|
||||
handle_optional,
|
||||
media_player::MediaPlayer,
|
||||
metadata::Metadata,
|
||||
track::TrackId,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
};
|
||||
use time::Duration;
|
||||
use zbus::{names::OwnedBusName, Connection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Player {
|
||||
proxy: PlayerProxy<'static>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
/// Creates a new instance of the `org.mpris.MediaPlayer2.Player` interface.
|
||||
pub async fn new(connection: &Connection, name: OwnedBusName) -> Result<Self> {
|
||||
PlayerProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await
|
||||
.map(Self::from)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Returns this player's `org.mpris.MediaPlayer2` instance
|
||||
pub async fn media_player(&self) -> Result<MediaPlayer> {
|
||||
let proxy = MediaPlayer2Proxy::builder(self.proxy.connection())
|
||||
.destination(self.proxy.destination().to_owned())?
|
||||
.build()
|
||||
.await?;
|
||||
Ok(proxy.into())
|
||||
}
|
||||
|
||||
/// Seeks the specified duration.
|
||||
pub async fn seek(&self, duration: Duration) -> Result<bool> {
|
||||
if self.proxy.can_seek().await? {
|
||||
self.proxy
|
||||
.seek(duration.whole_microseconds() as i64)
|
||||
.await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current track position.
|
||||
///
|
||||
/// If `track` does not match the id of the currently-playing track, the call is ignored as "stale".
|
||||
pub async fn set_position(&self, track: &TrackId, position: Duration) -> Result<()> {
|
||||
self.proxy
|
||||
.set_position(track, position.whole_microseconds() as i64)
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// How far into the current track the player is.
|
||||
///
|
||||
/// Not all players support this, and it will return None if this is the case.
|
||||
pub async fn position(&self) -> Result<Option<Duration>> {
|
||||
handle_optional(self.proxy.position().await.map(Duration::microseconds))
|
||||
}
|
||||
|
||||
/// Gets the current playback status of the player.
|
||||
pub async fn playback_status(&self) -> Result<PlaybackStatus> {
|
||||
self.proxy
|
||||
.playback_status()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.and_then(|status| PlaybackStatus::from_str(&status))
|
||||
}
|
||||
|
||||
/// Returns the current rate of playback.
|
||||
///
|
||||
/// Not all players support this, and it will return None if this is the case.
|
||||
pub async fn rate(&self) -> Result<Option<f64>> {
|
||||
handle_optional(self.proxy.rate().await)
|
||||
}
|
||||
|
||||
/// Sets the current rate of playback.
|
||||
pub async fn set_rate(&self, value: f64) -> Result<()> {
|
||||
handle_optional(self.proxy.set_rate(value).await).map(|_| ())
|
||||
}
|
||||
|
||||
/// Returns the minimum supported rate for the player.
|
||||
///
|
||||
/// Not all players support this, and it will return None if this is the case.
|
||||
pub async fn minimum_rate(&self) -> Result<Option<f64>> {
|
||||
handle_optional(self.proxy.minimum_rate().await)
|
||||
}
|
||||
|
||||
/// Returns the minimum supported rate for the player.
|
||||
///
|
||||
/// Not all players support this, and it will return None if this is the case.
|
||||
pub async fn maximum_rate(&self) -> Result<Option<f64>> {
|
||||
handle_optional(self.proxy.maximum_rate().await)
|
||||
}
|
||||
|
||||
/// Returns the range of playback rates available for the player.
|
||||
///
|
||||
/// Not all players support this, and it will return None if this is the case.
|
||||
pub async fn available_rates(&self) -> Result<Option<std::ops::RangeInclusive<f64>>> {
|
||||
let minimum = match self.minimum_rate().await? {
|
||||
Some(min) => min,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let maximum = match self.maximum_rate().await? {
|
||||
Some(max) => max,
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(Some(minimum..=maximum))
|
||||
}
|
||||
|
||||
/// Returns the metadata for the player.
|
||||
pub async fn metadata(&self) -> Result<Metadata> {
|
||||
self.proxy
|
||||
.metadata()
|
||||
.await
|
||||
.map(|metadata| metadata.into())
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Whether the current playlist is shuffled or not.
|
||||
///
|
||||
/// A value of false indicates that playback is progressing linearly through a playlist,
|
||||
/// while true means playback is progressing through a playlist in some other order.
|
||||
pub async fn shuffle(&self) -> Result<Option<bool>> {
|
||||
if self.can_control().await? {
|
||||
handle_optional(self.proxy.shuffle().await)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether the current playlist is shuffled or not.
|
||||
///
|
||||
/// A value of false indicates that playback is progressing linearly through a playlist,
|
||||
/// while true means playback is progressing through a playlist in some other order.
|
||||
pub async fn set_shuffle(&self, value: bool) -> Result<()> {
|
||||
if self.proxy.can_control().await? {
|
||||
self.proxy.set_shuffle(value).await.map_err(Error::from)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The current loop / repeat status.
|
||||
pub async fn loop_status(&self) -> Result<Option<LoopStatus>> {
|
||||
if self.proxy.can_control().await? {
|
||||
handle_optional(self.proxy.loop_status().await)
|
||||
.map(|status| status.and_then(|status| LoopStatus::from_str(&status).ok()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current loop / repeat status.
|
||||
pub async fn set_loop_status(&self, value: LoopStatus) -> Result<()> {
|
||||
if self.proxy.can_control().await? {
|
||||
handle_optional(self.proxy.set_loop_status(value.to_string()).await).map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Player {
|
||||
type Target = PlayerProxy<'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.proxy
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlayerProxy<'static>> for Player {
|
||||
fn from(proxy: PlayerProxy<'static>) -> Self {
|
||||
Self { proxy }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PlaybackStatus {
|
||||
/// A track is currently playing.
|
||||
Playing,
|
||||
/// A track is currently paused.
|
||||
Paused,
|
||||
/// There is no track currently playing.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl FromStr for PlaybackStatus {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_lowercase().trim() {
|
||||
"playing" => Ok(Self::Playing),
|
||||
"paused" => Ok(Self::Paused),
|
||||
"stopped" => Ok(Self::Stopped),
|
||||
_ => Err(Error::InvalidEnum {
|
||||
got: s.to_string(),
|
||||
expected: &["Playing", "Paused", "Stopped"],
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PlaybackStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Playing => "Playing",
|
||||
Self::Paused => "Paused",
|
||||
Self::Stopped => "Stopped",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl FromStr for LoopStatus {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_lowercase().trim() {
|
||||
"none" => Ok(Self::None),
|
||||
"track" => Ok(Self::Track),
|
||||
"playlist" => Ok(Self::Playlist),
|
||||
_ => Err(Error::InvalidEnum {
|
||||
got: s.to_string(),
|
||||
expected: &["Playing", "Paused", "Stopped"],
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LoopStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::Track => "Track",
|
||||
Self::Playlist => "Playlist",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
41
mpris2/src/playlists.rs
Normal file
41
mpris2/src/playlists.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
pub mod id;
|
||||
pub mod ordering;
|
||||
pub mod playlist;
|
||||
|
||||
use crate::{
|
||||
bindings::playlist::PlaylistsProxy,
|
||||
error::{Error, Result},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use zbus::{names::OwnedBusName, Connection};
|
||||
|
||||
pub struct Playlists {
|
||||
proxy: PlaylistsProxy<'static>,
|
||||
}
|
||||
|
||||
impl Playlists {
|
||||
/// Creates a new instance of the `org.mpris.MediaPlayer2.Playlists` interface.
|
||||
pub async fn new(connection: &Connection, name: OwnedBusName) -> Result<Self> {
|
||||
PlaylistsProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await
|
||||
.map(Self::from)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Playlists {
|
||||
type Target = PlaylistsProxy<'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.proxy
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PlaylistsProxy<'static>> for Playlists {
|
||||
fn from(proxy: PlaylistsProxy<'static>) -> Self {
|
||||
Self { proxy }
|
||||
}
|
||||
}
|
||||
53
mpris2/src/playlists/id.rs
Normal file
53
mpris2/src/playlists/id.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::{Ord, Ordering, PartialOrd},
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
use zvariant::{ObjectPath, OwnedObjectPath, Type, Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Serialize, Deserialize, Value)]
|
||||
pub struct PlaylistId(OwnedObjectPath);
|
||||
|
||||
impl PlaylistId {
|
||||
pub fn into_inner(self) -> OwnedObjectPath {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn into_static_path(self) -> ObjectPath<'static> {
|
||||
self.0.into_inner().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PlaylistId {
|
||||
type Target = OwnedObjectPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<ObjectPath<'a>> for PlaylistId {
|
||||
fn as_ref(&self) -> &ObjectPath<'a> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PlaylistId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.as_str().partial_cmp(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for PlaylistId {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.as_str().cmp(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PlaylistId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0.as_str())
|
||||
}
|
||||
}
|
||||
120
mpris2/src/playlists/ordering.rs
Normal file
120
mpris2/src/playlists/ordering.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use crate::error::{Error, Result};
|
||||
use serde::{
|
||||
de::{self, Deserialize, Visitor},
|
||||
ser::{Serialize, Serializer},
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
use zvariant::{OwnedValue, Signature, Type, Value};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PlaylistOrdering {
|
||||
/// Alphabetical ordering by name, ascending.
|
||||
Alphabetical,
|
||||
/// Ordering by creation date, oldest first.
|
||||
CreationDate,
|
||||
/// Ordering by last modified date, oldest first.
|
||||
ModifiedDate,
|
||||
/// Ordering by date of last playback, oldest first.
|
||||
LastPlayDate,
|
||||
/// A user-defined ordering.
|
||||
UserDefined,
|
||||
}
|
||||
|
||||
impl Type for PlaylistOrdering {
|
||||
fn signature() -> Signature<'static> {
|
||||
String::signature()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Value<'a>> for PlaylistOrdering {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: Value<'a>) -> Result<Self> {
|
||||
match value {
|
||||
Value::Str(value) => Self::from_str(&value),
|
||||
_ => Err(Error::IncorrectValue {
|
||||
wanted: "Str",
|
||||
actual: OwnedValue::from(value),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<PlaylistOrdering> for Value<'a> {
|
||||
fn from(ordering: PlaylistOrdering) -> Self {
|
||||
Value::Str(ordering.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PlaylistOrdering {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_lowercase().trim() {
|
||||
"alphabetical" => Ok(Self::Alphabetical),
|
||||
"created" => Ok(Self::CreationDate),
|
||||
"modified" => Ok(Self::ModifiedDate),
|
||||
"played" => Ok(Self::LastPlayDate),
|
||||
"user" => Ok(Self::UserDefined),
|
||||
_ => Err(Error::InvalidEnum {
|
||||
got: s.to_string(),
|
||||
expected: &["Alphabetical", "Created", "Modified", "Played", "User"],
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PlaylistOrdering {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Alphabetical => "Alphabetical",
|
||||
Self::CreationDate => "Created",
|
||||
Self::ModifiedDate => "Modified",
|
||||
Self::LastPlayDate => "Played",
|
||||
Self::UserDefined => "User",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PlaylistOrdering {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PlaylistOrdering {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(PlaylistOrderingVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaylistOrderingVisitor;
|
||||
|
||||
impl Visitor<'_> for PlaylistOrderingVisitor {
|
||||
type Value = PlaylistOrdering;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
PlaylistOrdering::from_str(s).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
21
mpris2/src/playlists/playlist.rs
Normal file
21
mpris2/src/playlists/playlist.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use super::id::PlaylistId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zbus::zvariant::{Type, Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Value, Serialize, Deserialize)]
|
||||
pub struct Playlist((PlaylistId, String, String));
|
||||
|
||||
impl Playlist {
|
||||
pub fn id(&self) -> &PlaylistId {
|
||||
&self.0 .0
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0 .1
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> &str {
|
||||
&self.0 .2
|
||||
}
|
||||
}
|
||||
54
mpris2/src/track.rs
Normal file
54
mpris2/src/track.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
use zbus::zvariant::{ObjectPath, OwnedObjectPath, Type, Value};
|
||||
|
||||
/// A reference to an MPRIS track.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Serialize, Deserialize, Value)]
|
||||
pub struct TrackId(OwnedObjectPath);
|
||||
|
||||
impl TrackId {
|
||||
pub fn into_inner(self) -> OwnedObjectPath {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn into_static_path(self) -> ObjectPath<'static> {
|
||||
self.0.into_inner().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TrackId {
|
||||
type Target = OwnedObjectPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<ObjectPath<'a>> for TrackId {
|
||||
fn as_ref(&self) -> &ObjectPath<'a> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for TrackId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.as_str().partial_cmp(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TrackId {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.as_str().cmp(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TrackId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0.as_str())
|
||||
}
|
||||
}
|
||||
93
mpris2/src/track_list.rs
Normal file
93
mpris2/src/track_list.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
use crate::{
|
||||
bindings::track_list::TrackListProxy,
|
||||
error::{Error, Result},
|
||||
metadata::Metadata,
|
||||
track::TrackId,
|
||||
};
|
||||
use std::{collections::BTreeMap, ops::Deref};
|
||||
use zbus::{names::OwnedBusName, Connection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackList {
|
||||
proxy: TrackListProxy<'static>,
|
||||
}
|
||||
|
||||
impl TrackList {
|
||||
/// Creates a new instance of the `org.mpris.MediaPlayer2.TrackList` interface.
|
||||
pub async fn new(connection: &Connection, name: OwnedBusName) -> Result<Self> {
|
||||
TrackListProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.build()
|
||||
.await
|
||||
.map(Self::from)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Adds a new track to this track list.
|
||||
pub async fn add_track<S: ToString>(
|
||||
&self,
|
||||
uri: S,
|
||||
after: &TrackId,
|
||||
set_as_current: bool,
|
||||
) -> Result<()> {
|
||||
let uri = uri.to_string();
|
||||
self.proxy
|
||||
.add_track(&uri, after, set_as_current)
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Gets the metadata of the given tracks.
|
||||
pub async fn get_tracks_metadata<T: AsRef<[TrackId]>>(
|
||||
&self,
|
||||
tracks: T,
|
||||
) -> Result<Vec<Metadata>> {
|
||||
self.proxy
|
||||
.get_tracks_metadata(tracks.as_ref().to_vec())
|
||||
.await
|
||||
.map(|x| x.into_iter().map(Metadata::from).collect())
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Goes to the specified track.
|
||||
pub async fn go_to(&self, track: &TrackId) -> Result<()> {
|
||||
self.proxy.go_to(track).await.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Removes the specified track.
|
||||
pub async fn remove(&self, track: &TrackId) -> Result<()> {
|
||||
self.proxy.remove_track(track).await.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Returns a list of all available [Track]s.
|
||||
pub async fn tracks(&self) -> Result<Vec<TrackId>> {
|
||||
self.proxy
|
||||
.tracks()
|
||||
.await
|
||||
.map(|x| x.into_iter().map(TrackId::from).collect())
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Returns a list of all available [Track]s and their associated metadata,
|
||||
/// in order.
|
||||
pub async fn detailed_tracks(&self) -> Result<BTreeMap<TrackId, Metadata>> {
|
||||
let tracks = self.tracks().await?;
|
||||
let metadata = self.get_tracks_metadata(&tracks).await?;
|
||||
Ok(tracks.into_iter().zip(metadata.into_iter()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TrackList {
|
||||
type Target = TrackListProxy<'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.proxy
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TrackListProxy<'static>> for TrackList {
|
||||
fn from(proxy: TrackListProxy<'static>) -> Self {
|
||||
Self { proxy }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue