more mpris etc etc

This commit is contained in:
Erica Z 2024-11-01 19:33:10 +01:00
parent 7279532745
commit a8776faa1f
6 changed files with 200 additions and 16 deletions

64
Cargo.lock generated
View file

@ -38,6 +38,21 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.1" version = "0.7.1"
@ -204,6 +219,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"bindgen", "bindgen",
"chrono",
"gettext-rs", "gettext-rs",
"glib-build-tools", "glib-build-tools",
"gtk4", "gtk4",
@ -397,6 +413,21 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -1180,6 +1211,29 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -2577,6 +2631,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.2.0" version = "0.2.0"
@ -2889,6 +2952,7 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c690a1da8858fd4377b8cc3134a753b0bea1d8ebd78ad6e5897fab821c5e184e" checksum = "c690a1da8858fd4377b8cc3134a753b0bea1d8ebd78ad6e5897fab821c5e184e"
dependencies = [ dependencies = [
"chrono",
"endi", "endi",
"enumflags2", "enumflags2",
"serde", "serde",

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] } adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] }
async-channel = "2.3.1" async-channel = "2.3.1"
chrono = { version = "0.4.38", features = ["serde"] }
gettext-rs = { version = "0.7.2", features = ["gettext-system"] } gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] } gtk = { version = "0.9.2", package = "gtk4", features = ["v4_16"] }
oo7 = "0.3.3" oo7 = "0.3.3"
@ -14,7 +15,7 @@ reqwest = { version = "0.12.9", features = ["json"] }
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1.0.214", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
url = "2.5.2" url = "2.5.2"
zbus = "5.0.1" zbus = { version = "5.0.1", features = ["chrono"] }
[build-dependencies] [build-dependencies]
bindgen = "0.70.1" bindgen = "0.70.1"

View file

@ -8,30 +8,88 @@ const MICROSECONDS: f64 = 1e6; // in a second
#[derive(Default)] #[derive(Default)]
struct MetadataMap { struct MetadataMap {
pub track_id: Option<OwnedObjectPath>, // mpris
// rest: TODO track_id: Option<OwnedObjectPath>,
length: Option<i64>,
//art_url: Option<url::Url>,
// xesam
album: Option<String>,
//album_artist: Option<Vec<String>>,
artist: Option<Vec<String>>,
//as_text: Option<String>,
//audio_bpm: Option<i32>,
//auto_rating: Option<f32>,
//comment: Option<Vec<String>>,
//composer: Option<Vec<String>>,
content_created: Option<chrono::NaiveDateTime>,
//disc_number: Option<i32>,
//first_used: Option<chrono::DateTime>,
genre: Option<Vec<String>>,
//last_used: Option<chrono::DateTime>,
//lyricist: Option<Vec<String>>,
title: Option<String>,
track_number: Option<i32>,
//url: Option<String>,
//use_count: Option<String>,
user_rating: Option<f32>,
} }
impl MetadataMap { impl MetadataMap {
fn from_playbin_song(song: Option<&crate::playbin::Song>) -> Self { fn from_playbin_song(song: Option<&crate::playbin::Song>) -> Self {
song.map(|song| MetadataMap { song.map(|song| MetadataMap {
// FIXME: see if there's a better way to do this lol // use a unique growing counter to identify tracks
track_id: Some({ track_id: Some({
let song_object_intptr = format!("/eu/callcc/audrey/Track/{}", song.counter())
glib::translate::ToGlibPtr::<*const _>::to_glib_none(song).0 as usize;
format!("/eu/callcc/audrey/TrackId/{song_object_intptr:x}")
.try_into() .try_into()
.unwrap() .unwrap()
}), }),
//length: Some((song.duration() * MICROSECONDS) as i64),
album: Some(song.album().into()),
artist: Some(vec![song.artist().into()]),
//content_created: song.year().map(|year| chrono::NaiveDate::from_yo_opt(year, 1).unwrap()), // FIXME: replace this unwrap with Some(Err) -> None
//genre: Some(song.genre.iter().collect()),
title: Some(song.title().into()),
//track_number: song.track().map(|u| u as i32),
//user_rating: Some(if song.starred().is_none() { 0.0 } else { 1.0 }),
..Default::default()
}) })
.unwrap_or_else(Default::default) .unwrap_or_else(Default::default)
} }
fn as_hash_map(&self) -> HashMap<&'static str, Value> { fn as_hash_map(&self) -> HashMap<&'static str, Value> {
let mut map = HashMap::new(); let mut map = HashMap::new();
if let Some(track_id) = &self.track_id { if let Some(track_id) = &self.track_id {
map.insert("mpris:trackid", Value::new(track_id.as_ref())); map.insert("mpris:trackid", Value::new(track_id.as_ref()));
} }
if let Some(length) = &self.length {
map.insert("mpris:length", Value::new(length));
}
if let Some(album) = &self.album {
map.insert("xesam:album", Value::new(album));
}
if let Some(artist) = &self.artist {
map.insert("xesam:artist", Value::new(artist));
}
if let Some(content_created) = &self.content_created {
map.insert(
"xesam:contentCreated",
Value::new(content_created.format("%+").to_string()),
);
}
if let Some(genre) = &self.genre {
map.insert("xesam:genre", Value::new(genre));
}
if let Some(track_number) = self.track_number {
map.insert("xesam:trackNumber", Value::new(track_number));
}
if let Some(title) = &self.title {
map.insert("xesam:title", Value::new(title));
}
if let Some(user_rating) = self.user_rating {
map.insert("xesam:userRating", Value::new(user_rating));
}
map map
} }
} }
@ -83,6 +141,33 @@ impl Player {
), ),
); );
playbin.connect_notify_local(
Some("play-queue-length"),
glib::clone!(
#[strong]
player_ref,
move |_playbin: &crate::Playbin, _| {
let player_ref = player_ref.clone();
glib::spawn_future_local(async move {
let player = player_ref.get_mut().await;
// properties that depend on the play queue length
player
.can_go_next_changed(player_ref.signal_emitter())
.await
.unwrap();
player
.can_go_previous_changed(player_ref.signal_emitter())
.await
.unwrap();
player
.can_play_changed(player_ref.signal_emitter())
.await
.unwrap();
});
}
),
);
playbin.connect_notify_local( playbin.connect_notify_local(
Some("state"), Some("state"),
glib::clone!( glib::clone!(
@ -91,7 +176,9 @@ impl Player {
move |_playbin: &crate::Playbin, _| { move |_playbin: &crate::Playbin, _| {
let player_ref = player_ref.clone(); let player_ref = player_ref.clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
player_ref.get_mut().await let player = player_ref.get_mut().await;
// properties that depend on the playbin state
player
.playback_status_changed(player_ref.signal_emitter()) .playback_status_changed(player_ref.signal_emitter())
.await .await
.unwrap(); .unwrap();
@ -269,13 +356,18 @@ impl Player {
} }
#[zbus(property)] #[zbus(property)]
fn playback_rate(&self) -> f64 { fn rate(&self) -> f64 {
1.0 1.0
} }
#[zbus(property)] #[zbus(property)]
fn set_playback_rate(&self, _playback_rate: f64) -> zbus::Result<()> { fn set_rate(&self, rate: f64) {
Err(zbus::Error::Unsupported) // A value of 0.0 should not be set by the client. If it is, the media player should act as though Pause was called.
if rate == 0.0 {
self.pause();
}
// just ignore anything else
} }
#[zbus(property)] #[zbus(property)]
@ -299,7 +391,11 @@ impl Player {
} }
#[zbus(property)] #[zbus(property)]
fn set_volume(&mut self, volume: f64) { fn set_volume(&mut self, mut volume: f64) {
// When setting, if a negative value is passed, the volume should be set to 0.0.
if volume < 0.0 {
volume = 0.0;
}
let playbin = self.playbin.upgrade().unwrap(); let playbin = self.playbin.upgrade().unwrap();
// FIXME: check if this is set by the notify callback: self.volume = volume; // FIXME: check if this is set by the notify callback: self.volume = volume;
playbin.set_volume((volume * 100.0) as i32); playbin.set_volume((volume * 100.0) as i32);
@ -322,26 +418,31 @@ impl Player {
#[zbus(property)] #[zbus(property)]
fn can_go_next(&self) -> bool { fn can_go_next(&self) -> bool {
true // same as can_play
self.playbin.upgrade().unwrap().play_queue_length() > 0
} }
#[zbus(property)] #[zbus(property)]
fn can_go_previous(&self) -> bool { fn can_go_previous(&self) -> bool {
true // same as can_play
self.playbin.upgrade().unwrap().play_queue_length() > 0
} }
#[zbus(property)] #[zbus(property)]
fn can_play(&self) -> bool { fn can_play(&self) -> bool {
true // it only makes sense to disallow "play" when the play queue is empty
self.playbin.upgrade().unwrap().play_queue_length() > 0
} }
#[zbus(property)] #[zbus(property)]
fn can_pause(&self) -> bool { fn can_pause(&self) -> bool {
// we don't play anything that can't be paused
true true
} }
#[zbus(property)] #[zbus(property)]
fn can_seek(&self) -> bool { fn can_seek(&self) -> bool {
// we don't play anything that can't be seeked
true true
} }

View file

@ -10,6 +10,9 @@ private struct Audrey.CommandCallback {
} }
public class Audrey.PlaybinSong : Object { public class Audrey.PlaybinSong : Object {
private static int64 next_counter = 0;
public int64 counter { get; private set; }
private Subsonic.Song inner; private Subsonic.Song inner;
public string id { get { return inner.id; } } public string id { get { return inner.id; } }
public string title { get { return inner.title; } } public string title { get { return inner.title; } }
@ -27,6 +30,9 @@ public class Audrey.PlaybinSong : Object {
public PlaybinSong (Subsonic.Client api, Subsonic.Song song) { public PlaybinSong (Subsonic.Client api, Subsonic.Song song) {
this.api = api; this.api = api;
this.inner = song; this.inner = song;
this.counter = next_counter;
next_counter += 1;
} }
private Subsonic.Client api; private Subsonic.Client api;

View file

@ -13,6 +13,9 @@ pub mod ffi {
extern "C" { extern "C" {
pub fn audrey_playbin_song_get_type() -> glib::ffi::GType; pub fn audrey_playbin_song_get_type() -> glib::ffi::GType;
pub fn audrey_playbin_song_get_counter(self_: *mut AudreyPlaybinSong) -> i64;
pub fn audrey_playbin_song_get_id(self_: *mut AudreyPlaybinSong)
-> *const std::ffi::c_char;
pub fn audrey_playbin_song_get_title( pub fn audrey_playbin_song_get_title(
self_: *mut AudreyPlaybinSong, self_: *mut AudreyPlaybinSong,
) -> *const std::ffi::c_char; ) -> *const std::ffi::c_char;
@ -38,6 +41,14 @@ glib::wrapper! {
} }
impl Song { impl Song {
pub fn counter(&self) -> i64 {
unsafe { ffi::audrey_playbin_song_get_counter(self.to_glib_none().0) }
}
pub fn id(&self) -> GString {
unsafe { from_glib_none(ffi::audrey_playbin_song_get_id(self.to_glib_none().0)) }
}
pub fn title(&self) -> GString { pub fn title(&self) -> GString {
unsafe { from_glib_none(ffi::audrey_playbin_song_get_title(self.to_glib_none().0)) } unsafe { from_glib_none(ffi::audrey_playbin_song_get_title(self.to_glib_none().0)) }
} }

View file

@ -44,7 +44,8 @@ pub struct Child {
pub artist: String, pub artist: String,
pub track: Option<u32>, pub track: Option<u32>,
pub year: Option<u32>, pub year: Option<u32>,
pub starred: Option<()>, pub starred: Option<chrono::DateTime<chrono::offset::Utc>>, // TODO: check which is best
// applicable
pub duration: u64, pub duration: u64,
pub play_count: Option<u32>, pub play_count: Option<u32>,
pub genre: Option<String>, pub genre: Option<String>,