Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to version 3.0.0 #14

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
1,205 changes: 446 additions & 759 deletions Cargo.lock

Large diffs are not rendered by default.

40 changes: 20 additions & 20 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "empress"
version = "2.0.0"
authors = ["raykast <me@ryan-s.net>"]
version = "3.0.0-alpha.1"
authors = ["raykast <me@june-s.net>"]
edition = "2021"
description = "A D-Bus MPRIS daemon for controlling media players."
documentation = "https://docs.rs/empress"
Expand All @@ -16,28 +16,28 @@ build = "build.rs"
[profile.release]
opt-level = 3
lto = "thin"
strip = true

[dependencies]
anyhow = "1.0.72"
async-trait = "0.1.72"
atty = "0.2.14"
clap = { version = "4.3.19", features = ["derive"] }
dispose = "0.5.0"
env_logger = "0.10.0"
futures-util = "0.3.28"
lalrpop-util = { version = "0.20.0", features = ["lexer"] }
lazy_static = "1.4.0"
log = "0.4.19"
anyhow = "1.0.91"
async-trait = "0.1.83"
clap = { version = "4.5.20", features = ["cargo", "derive", "wrap_help"] }
dispose = "0.5.1"
env_logger = "0.11.5"
futures-util = "0.3.31"
jiff = { version = "0.1.14", features = ["logging", "serde"] }
lalrpop-util = { version = "0.22.0", features = ["lexer"] }
log = "0.4.22"
nom = "7.1.3"
regex = "1.9.1"
serde = { version = "1.0.179", features = ["derive"] }
serde_json = "1.0.104"
strum = { version = "0.25.0", features = ["derive"] }
thiserror = "1.0.44"
zbus = { version = "3.14.1", default-features = false, features = ["tokio"] }
regex = "1.11.1"
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.132"
strum = { version = "0.26.3", features = ["derive"] }
thiserror = "2.0.3"
zbus = { version = "5.0.1", default-features = false, features = ["tokio"] }

[dependencies.tokio]
version = "1.29.1"
version = "1.41.0"
features = [
"macros",
"net",
Expand All @@ -49,4 +49,4 @@ features = [
]

[build-dependencies]
lalrpop = "0.19.8"
lalrpop = "0.22.0"
4 changes: 1 addition & 3 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
fn main() {
lalrpop::process_root().unwrap();
}
fn main() { lalrpop::process_root().unwrap(); }
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# D-Bus service file for empress

[D-BUS Service]
Name=net.ryan_s.Empress2
Name=club.bnuy.Empress
Exec=/path/to/empress server
SystemdService=empress.service

Expand Down
2 changes: 1 addition & 1 deletion etc/empress.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ PartOf=graphical-session.target

[Service]
Type=dbus
BusName=net.ryan_s.Empress2
BusName=club.bnuy.Empress
ExecStart=/path/to/empress server

# vim:set ft=systemd:
36 changes: 30 additions & 6 deletions etc/now_playing_long.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,53 @@ Function calls take the form {{ ident(arg1, arg2, ...) }}. See below for a list

Any expression can be wrapped in parentheses for clarity - {{ (12) }} is equivalent to {{ 12 }}, etc.

Values in an array or an object can be accessed using the index operator. For example {{ artist[0] }} retrieves the first artist and {{ player["id"] }} retrieves the player name. Indexing into a value with an invalid index will throw an error.
Values in an array or an object can be accessed using the index operator. For example {{ artists[0] }} retrieves the first artist and {{ player["id"] }} retrieves the player name. Indexing into a value with an invalid index will throw an error.

Expressions can be piped through functions using the | (pipe) operator. When calling a pipe function with no arguments, parentheses are optional, so {{ "hi" | lower() }} is equivalent to {{ "hi" | lower }}. You can chain as many functions together using pipes as you like.

Additionally, the null-propagating pipe |! can be used for values that may be null passed to functions that don't accept null. If a null value is piped with |!, the right side of the pipeline is ignored and the entire pipeline returns null. For example, {{ position | time }} will fail on players that don't report playback position, but {{ position |! time }} will succeed and print nothing.

Function arguments can be suffixed with the ! (null-propagating) operator. If a function argument with ! is null, evaluation of the function will be skipped and the entire result will be null instead. This is useful to avoid errors with functions that cannot accept null arguments, for example {{ position |! eta(length!) }} will simply print nothing rather than throwing an error if length is null.

Pipes and indexes/member accesses can be chained with {{ ... | .member }} or {{ ... | .[index] }}. This is particularly useful with the null-propagating pipe, as something like {{ artists |! .[0] }} will not throw an error if artists is null.

To provide a fallback value in case an expression is blank the ?? (null-coalescing) operator can be used. For instance, to handle players that can return null for the album field, one might use {{ album ?? "Unknown album" }}.

VALUE REFERENCE

To see the values available to format strings, simply run the now-playing command without any format string and the raw JSON will be printed. Currently, the values provided are:

{
"status": string, // one of 'Playing', 'Paused', or 'Stopped'
"status": string, // one of 'Playing', 'Paused', or 'Stopped'
"player": {
"bus": string,
"id": string,
},
"title": string?,
"artist": string[]?,
"album": string?,
"length": int64?,
"position": int64?,

"trackId": string?,
"length": int64?,
"artUrl": string?,

"album": string?,
"albumArtists": string[]?,
"artists": string[]?,
"lyrics": string?,
"bpm": integer?,
"autoRating": double?,
"comments": string[]?,
"composers": string[]?,
"dateCreated": string?, // Formatted as a local DateTime
"discNum": integer?,
"dateFirstPlayed": string?, // Formatted as a local DateTime
"genres": string[]?,
"dateLastPlayed": string?, // Formatted as a local DateTime
"lyricists": string[]?,
"title": string?,
"trackNum": integer?,
"url": string?,
"playCount": integer?,
"userRating": double?,
}

FUNCTION REFERENCE
Expand Down
10 changes: 4 additions & 6 deletions scripts/install-services.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,12 @@ fi

cd "$(dirname "$0")/.."

version="$(cargo read-manifest --manifest-path Cargo.toml | jq -r \
'.version | capture("^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<rev>\\d+)(?<extra>.*)$")')"
service="$(awk '/cfg\(not\(debug_assertions\)\)/,EOF { print }' src/main.rs \
| sed -nre 's/^.*const.*SERVER_NAME_STR.*=.*"(.*)".*$/\1/p')"

for part in major minor rev extra; do
typeset ver_$part="$(jq -r .$part <<<"$version")"
done
# Assert the real service name matches the expected output of this script
[[ "$service" == 'club.bnuy.Empress' ]]

service=net.ryan_s.Empress$ver_major
systemd_file=empress.service
dbus_file=$service.service

Expand Down
165 changes: 141 additions & 24 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::{future::Future, io, time::Duration};
use std::{future::Future, io, io::IsTerminal, time::Duration};

use anyhow::{Context, Error};
use futures_util::StreamExt;
use jiff::civil::DateTime;
use log::{info, trace, warn};
use serde::Serialize;
use zbus::ConnectionBuilder;
use zbus::{
connection,
zvariant::{ObjectPath, OwnedValue},
};

use self::proxy::EmpressProxy;
use crate::{
Expand All @@ -30,11 +34,86 @@ struct NowPlayingPlayer {
struct NowPlayingResult {
status: PlaybackStatus,
player: NowPlayingPlayer,
title: Option<String>,
artist: Option<Vec<String>>,
album: Option<String>,
length: Option<i64>,
position: Option<i64>,

track_id: Option<String>,
length: Option<i64>,
art_url: Option<String>,

album: Option<String>,
album_artists: Option<Vec<String>>,
artists: Option<Vec<String>>,
lyrics: Option<String>,
bpm: Option<i64>,
auto_rating: Option<f64>,
comments: Option<Vec<String>>,
composers: Option<Vec<String>>,
date_created: Option<DateTime>,
disc_num: Option<i64>,
date_first_played: Option<DateTime>,
genres: Option<Vec<String>>,
date_last_played: Option<DateTime>,
lyricists: Option<Vec<String>>,
title: Option<String>,
track_num: Option<i64>,
url: Option<String>,
play_count: Option<i64>,
user_rating: Option<f64>,
}

macro_rules! extract {
($meta:ident { $($key:ident => $var:ident ($conv:expr)),* $(,)? }) => {
$(
let $var = $meta
.remove(mpris::track_list::$key)
.and_then(|v| {
$conv(v)
.context(concat!(
"Error parsing field \"",
stringify!($var),
"\" of player status",
))
.map_err(|e| warn!("{:?}", e))
.ok()
});
)*
};
}

#[allow(clippy::needless_pass_by_value)]
fn parse_path(val: OwnedValue) -> Result<String, zbus::zvariant::Error> {
val.downcast_ref::<ObjectPath>()
.map(|p| p.to_string())
.or_else(|_| String::try_from(val))
}

#[allow(clippy::needless_pass_by_value)]
fn parse_i64(val: OwnedValue) -> Result<i64> {
i64::try_from(&val)
.or_else(|_| i32::try_from(&val).map(Into::into))
.context("Error downcasting i64/i32")
.or_else(|_| {
u64::try_from(&val)
.context("Error downcasting u64")
.and_then(|u| u.try_into().context("Error converting u64 to i64"))
})
}

#[allow(clippy::needless_pass_by_value)]
fn parse_datetime(val: OwnedValue) -> Result<DateTime> {
let s = val
.downcast_ref::<&str>()
.context("Unable to downcast value to string")?;
let tz = jiff::tz::TimeZone::system();

s.parse::<jiff::Timestamp>()
.map(|t| t.to_zoned(tz.clone()).datetime())
.or_else(|_| {
s.parse::<jiff::Zoned>()
.map(|z| z.with_time_zone(tz).datetime())
})
.or_else(|_| s.parse::<DateTime>())
.context("Error parsing date string")
}

impl TryFrom<PlayerStatus> for NowPlayingResult {
Expand All @@ -56,27 +135,60 @@ impl TryFrom<PlayerStatus> for NowPlayingResult {
PlayerStatusKind::Default => (Some(bus), Some(ident), Some(position)),
};

let title = metadata
.remove(mpris::track_list::ATTR_TITLE)
.and_then(|v| v.try_into().ok());
let artist = metadata
.remove(mpris::track_list::ATTR_ARTIST)
.and_then(|v| v.try_into().ok());
let album = metadata
.remove(mpris::track_list::ATTR_ALBUM)
.and_then(|v| v.try_into().ok());
let length = metadata
.remove(mpris::track_list::ATTR_LENGTH)
.and_then(|v| v.try_into().ok());
extract!(metadata {
ATTR_TRACK_ID => track_id(parse_path),
ATTR_LENGTH => length(parse_i64),
ATTR_ART_URL => art_url(String::try_from),

ATTR_ALBUM => album(String::try_from),
ATTR_ALBUM_ARTISTS => album_artists(Vec::try_from),
ATTR_ARTISTS => artists(Vec::try_from),
ATTR_LYRICS => lyrics(String::try_from),
ATTR_BPM => bpm(parse_i64),
ATTR_AUTO_RATING => auto_rating(f64::try_from),
ATTR_COMMENTS => comments(Vec::try_from),
ATTR_COMPOSERS => composers(Vec::try_from),
ATTR_DATE_CREATED => date_created(parse_datetime),
ATTR_DISC_NUM => disc_num(parse_i64),
ATTR_DATE_FIRST_PLAYED => date_first_played(parse_datetime),
ATTR_GENRES => genres(Vec::try_from),
ATTR_DATE_LAST_PLAYED => date_last_played(parse_datetime),
ATTR_LYRICISTS => lyricists(Vec::try_from),
ATTR_TITLE => title(String::try_from),
ATTR_TRACK_NUM => track_num(parse_i64),
ATTR_URL => url(String::try_from),
ATTR_PLAY_COUNT => play_count(parse_i64),
ATTR_USER_RATING => user_rating(f64::try_from),
});

trace!("Unused metadata for status: {metadata:?}");

Ok(Self {
status,
player: NowPlayingPlayer { bus, id },
title,
artist,
album,
length,
position,
track_id,
length,
art_url,
album,
album_artists,
artists,
lyrics,
bpm,
auto_rating,
comments,
composers,
date_created,
disc_num,
date_first_played,
genres,
date_last_played,
lyricists,
title,
track_num,
url,
play_count,
user_rating,
})
}
}
Expand All @@ -93,14 +205,15 @@ impl MatchPlayer for NowPlayingResult {

macro_rules! courtesy_line {
() => {
if ::atty::is(::atty::Stream::Stdout) {
if std::io::stdout().is_terminal() {
println!();
}
};
}

#[allow(clippy::too_many_lines)]
pub(super) async fn run(cmd: ClientCommand) -> Result {
let conn = ConnectionBuilder::session()
let conn = connection::Builder::session()
.context("Error creatihng session connection builder")?
.build()
.await
Expand All @@ -127,6 +240,10 @@ pub(super) async fn run(cmd: ClientCommand) -> Result {
}
}
},
ClientCommand::Raise(opts) => {
let opts = opts.into();
try_send(&proxy, |p| p.raise(&opts)).await?;
},
ClientCommand::Next(opts) => {
let opts = opts.into();
try_send(&proxy, |p| p.next(&opts)).await?;
Expand Down
Loading