kill signals
This commit is contained in:
parent
224a7ff3dc
commit
39982edc10
7 changed files with 11 additions and 1003 deletions
|
@ -16,12 +16,6 @@ pub use playbin_song::Song as PlaybinSong;
|
||||||
|
|
||||||
pub mod subsonic;
|
pub mod subsonic;
|
||||||
|
|
||||||
pub mod playbin;
|
|
||||||
pub use playbin::Playbin;
|
|
||||||
|
|
||||||
mod signal;
|
|
||||||
pub use signal::{Signal, SignalEmitter, SignalHandler};
|
|
||||||
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
mod player;
|
|
||||||
pub use player::Player;
|
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
|
@ -1,582 +0,0 @@
|
||||||
use crate::{Playbin, PlaybinSong};
|
|
||||||
use gtk::glib::spawn_future_local;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::{Rc, Weak};
|
|
||||||
use tracing::{event, Level};
|
|
||||||
use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value};
|
|
||||||
|
|
||||||
const MICROSECONDS: f64 = 1e6; // in a second
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct MetadataMap {
|
|
||||||
// mpris
|
|
||||||
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 {
|
|
||||||
fn from_playbin_song(song: Option<&PlaybinSong>) -> Self {
|
|
||||||
song.map(|song| MetadataMap {
|
|
||||||
// use a unique growing counter to identify tracks
|
|
||||||
track_id: Some({
|
|
||||||
format!("/eu/callcc/audrey/Track/{}", song.counter())
|
|
||||||
.try_into()
|
|
||||||
.unwrap()
|
|
||||||
}),
|
|
||||||
length: Some(song.duration() * MICROSECONDS as i64),
|
|
||||||
//art_url: Some(song.cover_art_url()), // FIXME: this would leak credentials
|
|
||||||
album: Some(song.album()),
|
|
||||||
artist: Some(vec![song.artist()]),
|
|
||||||
//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()),
|
|
||||||
//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_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_hash_map(&self) -> HashMap<&'static str, OwnedValue> {
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
|
|
||||||
if let Some(track_id) = &self.track_id {
|
|
||||||
map.insert(
|
|
||||||
"mpris:trackid",
|
|
||||||
Value::new(track_id.as_ref()).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(art_url) = &self.art_url {
|
|
||||||
map.insert(
|
|
||||||
"mpris:artUrl",
|
|
||||||
Value::new(art_url.to_string()).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(length) = &self.length {
|
|
||||||
map.insert("mpris:length", Value::new(length).try_into().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(album) = &self.album {
|
|
||||||
map.insert("xesam:album", Value::new(album).try_into().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(artist) = &self.artist {
|
|
||||||
map.insert("xesam:artist", Value::new(artist).try_into().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(content_created) = &self.content_created {
|
|
||||||
map.insert(
|
|
||||||
"xesam:contentCreated",
|
|
||||||
Value::new(content_created.format("%+").to_string())
|
|
||||||
.try_into()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(genre) = &self.genre {
|
|
||||||
map.insert("xesam:genre", Value::new(genre).try_into().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(track_number) = self.track_number {
|
|
||||||
map.insert(
|
|
||||||
"xesam:trackNumber",
|
|
||||||
Value::new(track_number).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(title) = &self.title {
|
|
||||||
map.insert("xesam:title", Value::new(title).try_into().unwrap());
|
|
||||||
}
|
|
||||||
if let Some(user_rating) = self.user_rating {
|
|
||||||
map.insert(
|
|
||||||
"xesam:userRating",
|
|
||||||
Value::new(user_rating).try_into().unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
map
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Player(async_channel::Sender<Box<dyn FnOnce(Rc<LocalPlayer>) + Send>>);
|
|
||||||
|
|
||||||
impl Player {
|
|
||||||
pub async fn setup(
|
|
||||||
object_server: &zbus::ObjectServer,
|
|
||||||
playbin: &Rc<Playbin>,
|
|
||||||
) -> Result<(), zbus::Error> {
|
|
||||||
let local = LocalPlayer {
|
|
||||||
metadata: MetadataMap::from_playbin_song(None),
|
|
||||||
playbin: Rc::downgrade(playbin),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (with_local_send, with_local_recv) = async_channel::unbounded();
|
|
||||||
|
|
||||||
let player = Self(with_local_send);
|
|
||||||
|
|
||||||
spawn_future_local(async move {
|
|
||||||
let local = Rc::new(local);
|
|
||||||
while let Ok(f) = with_local_recv.recv().await {
|
|
||||||
f(Rc::clone(&local));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
object_server.at("/org/mpris/MediaPlayer2", player).await?;
|
|
||||||
|
|
||||||
let _player_ref = object_server
|
|
||||||
.interface::<_, Self>("/org/mpris/MediaPlayer2")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
/*
|
|
||||||
playbin.connect_new_track(glib::clone!(
|
|
||||||
#[strong]
|
|
||||||
player_ref,
|
|
||||||
move |_, song| {
|
|
||||||
let metadata = MetadataMap::from_playbin_song(Some(song));
|
|
||||||
|
|
||||||
let player_ref = player_ref.clone();
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
let mut player = player_ref.get_mut().await;
|
|
||||||
player.metadata = metadata;
|
|
||||||
player
|
|
||||||
.metadata_changed(player_ref.signal_emitter())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
playbin.connect_seeked(glib::clone!(
|
|
||||||
#[strong]
|
|
||||||
player_ref,
|
|
||||||
move |_, position| {
|
|
||||||
let player_ref = player_ref.clone();
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
player_ref
|
|
||||||
.seeked((position * MICROSECONDS) as i64)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
playbin.connect_notify_future_local(
|
|
||||||
"play-queue-length",
|
|
||||||
glib::clone!(
|
|
||||||
#[strong]
|
|
||||||
player_ref,
|
|
||||||
move |_, _| {
|
|
||||||
let player_ref = player_ref.clone();
|
|
||||||
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_future_local(
|
|
||||||
"state",
|
|
||||||
glib::clone!(
|
|
||||||
#[strong]
|
|
||||||
player_ref,
|
|
||||||
move |_, _| {
|
|
||||||
let player_ref = player_ref.clone();
|
|
||||||
async move {
|
|
||||||
let player = player_ref.get_mut().await;
|
|
||||||
// properties that depend on the playbin state
|
|
||||||
player
|
|
||||||
.playback_status_changed(player_ref.signal_emitter())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
playbin.connect_notify_future_local(
|
|
||||||
"volume",
|
|
||||||
glib::clone!(
|
|
||||||
#[strong]
|
|
||||||
player_ref,
|
|
||||||
move |_, _| {
|
|
||||||
let player_ref = player_ref.clone();
|
|
||||||
async move {
|
|
||||||
let player = player_ref.get_mut().await;
|
|
||||||
player
|
|
||||||
.volume_changed(player_ref.signal_emitter())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn with_local<T: Send + 'static, F: std::future::Future<Output = T>>(
|
|
||||||
&self,
|
|
||||||
f: impl FnOnce(Rc<LocalPlayer>) -> F + Send + 'static,
|
|
||||||
) -> T {
|
|
||||||
let (send, recv) = async_channel::bounded(1);
|
|
||||||
|
|
||||||
self.0
|
|
||||||
.send(Box::new(move |local| {
|
|
||||||
gtk::glib::spawn_future_local(async move {
|
|
||||||
send.send(f(local).await).await.unwrap();
|
|
||||||
});
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
recv.recv().await.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// because zbus insists in being Send+Sync all proper
|
|
||||||
struct LocalPlayer {
|
|
||||||
metadata: MetadataMap,
|
|
||||||
playbin: Weak<Playbin>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus::interface(name = "org.mpris.MediaPlayer2.Player")]
|
|
||||||
impl Player {
|
|
||||||
async fn next(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.next() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn previous(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.previous() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pause(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.pause() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn play_pause(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.play_pause() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stop(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.stop() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn play(&self) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.play() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn seek(&self, offset: i64) -> zbus::fdo::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.seek(offset) })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn set_position(&self, track_id: ObjectPath<'_>, position: i64) -> zbus::fdo::Result<()> {
|
|
||||||
let track_id = track_id.to_owned();
|
|
||||||
self.with_local(move |local| async move { local.set_position(track_id, position) })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn open_uri(&self, _s: String) -> zbus::fdo::Result<()> {
|
|
||||||
Err(zbus::fdo::Error::NotSupported("OpenUri".into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn playback_status(&self) -> zbus::fdo::Result<String> {
|
|
||||||
self.with_local(|local| async move { local.playback_status() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
fn loop_status(&self) -> zbus::fdo::Result<String> {
|
|
||||||
Ok("None".into()) // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
fn set_loop_status(&self, _loop_status: &str) -> zbus::Result<()> {
|
|
||||||
Err(zbus::fdo::Error::NotSupported("setting LoopStatus".into()).into()) // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
fn rate(&self) -> zbus::fdo::Result<f64> {
|
|
||||||
Ok(1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn set_rate(&self, rate: f64) -> zbus::Result<()> {
|
|
||||||
// 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.with_local(|local| async move { local.pause() })
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// just ignore anything else
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
// FIXME: https://github.com/dbus2/zbus/issues/992
|
|
||||||
fn shuffle(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
// FIXME: see above
|
|
||||||
fn set_shuffle(&self, _shuffle: bool) -> zbus::Result<()> {
|
|
||||||
Err(zbus::fdo::Error::NotSupported("setting Shuffle".into()).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn metadata(&self) -> zbus::fdo::Result<HashMap<&'static str, OwnedValue>> {
|
|
||||||
self.with_local(move |local| async move { Ok(local.metadata.as_hash_map()) })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn volume(&self) -> zbus::fdo::Result<f64> {
|
|
||||||
self.with_local(|local| async move { local.volume() }).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn set_volume(&self, volume: f64) -> zbus::Result<()> {
|
|
||||||
self.with_local(move |local| async move { local.set_volume(volume) })
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property(emits_changed_signal = "false"))]
|
|
||||||
async fn position(&self) -> zbus::fdo::Result<i64> {
|
|
||||||
self.with_local(|local| async move { local.position() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
fn minimum_rate(&self) -> f64 {
|
|
||||||
1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
fn maximum_rate(&self) -> f64 {
|
|
||||||
1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn can_go_next(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
self.with_local(|local| async move { local.can_go_next() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn can_go_previous(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
self.with_local(|local| async move { local.can_go_previous() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn can_play(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
self.with_local(|local| async move { local.can_play() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn can_pause(&self) -> bool {
|
|
||||||
self.with_local(|local| async move { local.can_pause() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property)]
|
|
||||||
async fn can_seek(&self) -> bool {
|
|
||||||
self.with_local(|local| async move { local.can_seek() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[zbus(property(emits_changed_signal = "const"))]
|
|
||||||
async fn can_control(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalPlayer {
|
|
||||||
fn playbin(&self) -> zbus::fdo::Result<Rc<Playbin>> {
|
|
||||||
match self.playbin.upgrade() {
|
|
||||||
None => Err(zbus::fdo::Error::Failed("playbin was discarded".into())),
|
|
||||||
Some(playbin) => Ok(playbin),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next(&self) -> zbus::fdo::Result<()> {
|
|
||||||
// If CanGoNext is false, attempting to call this method should have no effect.
|
|
||||||
if !self.can_go_next()? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let playbin = self.playbin()?;
|
|
||||||
if playbin.current_entry().is_none()
|
|
||||||
|| (playbin.current_entry().unwrap() + 1 > playbin.entries().len())
|
|
||||||
{
|
|
||||||
// If there is no next track (and endless playback and track repeat are both off), stop playback.
|
|
||||||
// (interpret this as something else than what Stop does)
|
|
||||||
todo!();
|
|
||||||
} else {
|
|
||||||
playbin.next_entry();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn previous(&self) -> zbus::fdo::Result<()> {
|
|
||||||
let playbin = self.playbin()?;
|
|
||||||
|
|
||||||
// If CanGoPrevious is false, attempting to call this method should have no effect.
|
|
||||||
if !self.can_go_previous()? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
match playbin.current_entry() {
|
|
||||||
None | Some(0) => {
|
|
||||||
// If there is no previous track (and endless playback and track repeat are both off), stop playback.
|
|
||||||
// (interpret this as something else than what Stop does)
|
|
||||||
playbin.stop();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
playbin.prev_entry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pause(&self) -> zbus::fdo::Result<()> {
|
|
||||||
let playbin = self.playbin()?;
|
|
||||||
|
|
||||||
// If CanPause is false, attempting to call this method should have no effect.
|
|
||||||
if !self.can_pause() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If playback is already paused, this has no effect.
|
|
||||||
if playbin.paused() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
playbin.set_paused(true);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_pause(&self) -> zbus::fdo::Result<()> {
|
|
||||||
let playbin = self.playbin()?;
|
|
||||||
playbin.set_paused(!playbin.paused());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop(&self) -> zbus::fdo::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play(&self) -> zbus::fdo::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn seek(&self, _offset: i64) -> zbus::fdo::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_position(
|
|
||||||
&self,
|
|
||||||
_track_id: ObjectPath<'static>,
|
|
||||||
_position: i64,
|
|
||||||
) -> zbus::fdo::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn playback_status(&self) -> zbus::fdo::Result<String> {
|
|
||||||
match self.playbin()?.paused() {
|
|
||||||
//crate::playbin::State::Stopped => Ok("Stopped".into()),
|
|
||||||
false => Ok("Playing".into()),
|
|
||||||
true => Ok("Paused".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn volume(&self) -> zbus::fdo::Result<f64> {
|
|
||||||
Ok(self.playbin()?.volume() as f64 / 100.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_volume(&self, mut volume: f64) -> zbus::fdo::Result<()> {
|
|
||||||
// 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()?;
|
|
||||||
// FIXME: check if this is set by the notify callback: self.volume = volume;
|
|
||||||
playbin.set_volume((volume * 100.0) as i64);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position(&self) -> zbus::fdo::Result<i64> {
|
|
||||||
Ok(0) // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_go_next(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
// same as can_play
|
|
||||||
Ok(self.playbin()?.entries().len() > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_go_previous(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
// same as can_play
|
|
||||||
Ok(self.playbin()?.entries().len() > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_play(&self) -> zbus::fdo::Result<bool> {
|
|
||||||
// it only makes sense to disallow "play" when the play queue is empty
|
|
||||||
Ok(self.playbin()?.entries().len() > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_pause(&self) -> bool {
|
|
||||||
// we don't play anything that can't be paused
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_seek(&self) -> bool {
|
|
||||||
// we don't play anything that can't be seeked
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Player {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
event!(Level::DEBUG, "dropping MprisPlayer");
|
|
||||||
}
|
|
||||||
}
|
|
267
src/playbin.rs
267
src/playbin.rs
|
@ -1,267 +0,0 @@
|
||||||
use crate::mpv;
|
|
||||||
use crate::signal::{Signal, SignalEmitter};
|
|
||||||
use crate::Event;
|
|
||||||
use crate::PlaybinSong as Song;
|
|
||||||
use event_listener::EventListener;
|
|
||||||
use std::cell::{Ref, RefCell};
|
|
||||||
use tracing::{event, span, Level};
|
|
||||||
|
|
||||||
pub struct Playbin {
|
|
||||||
mpv: mpv::Handle,
|
|
||||||
entries: RefCell<Vec<Song>>,
|
|
||||||
|
|
||||||
sender: async_broadcast::Sender<Event>,
|
|
||||||
|
|
||||||
stopped: SignalEmitter<Self, ()>,
|
|
||||||
entry_removed: SignalEmitter<Self, usize>,
|
|
||||||
|
|
||||||
file_started: SignalEmitter<Self, ()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Playbin {
|
|
||||||
pub fn new(sender: async_broadcast::Sender<Event>) -> Self {
|
|
||||||
let mpv = mpv::Handle::new();
|
|
||||||
mpv.set_property("audio-client-name", "audrey").unwrap();
|
|
||||||
mpv.set_property("user-agent", crate::USER_AGENT).unwrap();
|
|
||||||
mpv.set_property("video", false).unwrap();
|
|
||||||
mpv.set_property("prefetch-playlist", true).unwrap();
|
|
||||||
mpv.set_property("gapless-audio", true).unwrap();
|
|
||||||
|
|
||||||
mpv.observe_property(0, "volume").unwrap();
|
|
||||||
mpv.observe_property(1, "mute").unwrap();
|
|
||||||
mpv.observe_property(2, "pause").unwrap();
|
|
||||||
mpv.observe_property(3, "playlist-pos").unwrap();
|
|
||||||
|
|
||||||
// "Useful to drain property changes before a new file is loaded."
|
|
||||||
mpv.add_hook(0, "on_before_start_file", 0).unwrap();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
mpv,
|
|
||||||
entries: RefCell::new(vec![]),
|
|
||||||
|
|
||||||
sender,
|
|
||||||
|
|
||||||
stopped: Default::default(),
|
|
||||||
entry_removed: Default::default(),
|
|
||||||
|
|
||||||
file_started: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn volume(&self) -> i64 {
|
|
||||||
self.mpv.get_property("volume").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_volume(&self, volume: i64) {
|
|
||||||
self.mpv.set_property("volume", volume).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn muted(&self) -> bool {
|
|
||||||
self.mpv.get_property("mute").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_muted(&self, muted: bool) {
|
|
||||||
self.mpv.set_property("mute", muted).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paused(&self) -> bool {
|
|
||||||
self.mpv.get_property("pause").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_paused(&self, paused: bool) {
|
|
||||||
self.mpv.set_property("pause", paused).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn position(&self) -> Option<f64> {
|
|
||||||
self.mpv.get_property("time-pos").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_entry(&self) -> Option<usize> {
|
|
||||||
self.mpv
|
|
||||||
.get_property::<i64>("playlist-pos")
|
|
||||||
.unwrap()
|
|
||||||
.try_into()
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seek(&self, _position: f64) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_entry(&self) {
|
|
||||||
self.mpv.command(["playlist-next"]).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_entry(&self) {
|
|
||||||
self.mpv.command(["playlist-prev"]).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play_entry(&self, index: usize) {
|
|
||||||
self.mpv
|
|
||||||
.command(["playlist-play-index", &index.to_string()])
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entries(&self) -> Ref<'_, [Song]> {
|
|
||||||
Ref::map(self.entries.borrow(), Vec::as_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push_entry(&self, entry: Song) {
|
|
||||||
let mut entries = self.entries.borrow_mut();
|
|
||||||
self.mpv
|
|
||||||
.command(["loadfile", &entry.stream_url(), "append-play"])
|
|
||||||
.unwrap();
|
|
||||||
let index = entries.len();
|
|
||||||
entries.push(entry);
|
|
||||||
|
|
||||||
drop(entries);
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinEntryInserted(index))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_entry(&self, index: usize, entry: Song) {
|
|
||||||
let mut entries = self.entries.borrow_mut();
|
|
||||||
self.mpv
|
|
||||||
.command([
|
|
||||||
"loadfile",
|
|
||||||
&entry.stream_url(),
|
|
||||||
"insert-at-play",
|
|
||||||
&index.to_string(),
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
entries.insert(index, entry);
|
|
||||||
|
|
||||||
drop(entries);
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinEntryInserted(index))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop playback and clear playlist
|
|
||||||
pub fn stop(&self) {
|
|
||||||
let mut entries = self.entries.borrow_mut();
|
|
||||||
self.mpv.command(["stop"]).unwrap();
|
|
||||||
entries.clear();
|
|
||||||
|
|
||||||
drop(entries);
|
|
||||||
self.sender.try_broadcast(Event::PlaybinStopped).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_entry(&self, index: usize) {
|
|
||||||
let mut entries = self.entries.borrow_mut();
|
|
||||||
self.mpv
|
|
||||||
.command(["playlist-remove", &index.to_string()])
|
|
||||||
.unwrap();
|
|
||||||
entries.remove(index);
|
|
||||||
|
|
||||||
drop(entries);
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinEntryRemoved(index))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn move_entry(&self, _from: usize, _to: usize) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&self) -> EventListener {
|
|
||||||
let listener = self.mpv.wakeup_listener();
|
|
||||||
while let Some(event) = self.mpv.wait_event(0.0) {
|
|
||||||
self.handle_event(event);
|
|
||||||
}
|
|
||||||
listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(&self, event: mpv::Event) {
|
|
||||||
let span = span!(Level::DEBUG, "mpv_handle_event");
|
|
||||||
let _guart = span.enter();
|
|
||||||
match event {
|
|
||||||
mpv::Event::PropertyChange(event) => match event.reply_userdata {
|
|
||||||
0 => {
|
|
||||||
assert_eq!(&event.name, "volume");
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinVolumeChanged)
|
|
||||||
.unwrap();
|
|
||||||
event!(Level::DEBUG, "volume change {}", self.volume());
|
|
||||||
}
|
|
||||||
|
|
||||||
1 => {
|
|
||||||
assert_eq!(&event.name, "mute");
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinMutedChanged)
|
|
||||||
.unwrap();
|
|
||||||
event!(Level::DEBUG, "mute state change to {}", self.muted());
|
|
||||||
}
|
|
||||||
|
|
||||||
2 => {
|
|
||||||
assert_eq!(&event.name, "pause");
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinPausedChanged)
|
|
||||||
.unwrap();
|
|
||||||
event!(Level::DEBUG, "pause state change to {}", self.paused());
|
|
||||||
}
|
|
||||||
|
|
||||||
3 => {
|
|
||||||
assert_eq!(&event.name, "playlist-pos");
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinCurrentEntryChanged)
|
|
||||||
.unwrap();
|
|
||||||
event!(
|
|
||||||
Level::DEBUG,
|
|
||||||
"playlist-pos change {:?}",
|
|
||||||
self.current_entry()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
|
|
||||||
mpv::Event::Hook(event) => match event.reply_userdata {
|
|
||||||
0 => {
|
|
||||||
assert_eq!(&event.name, "on_before_start_file");
|
|
||||||
event!(Level::DEBUG, "on_before_start_file triggered");
|
|
||||||
// just use this as a barrier
|
|
||||||
self.mpv.continue_hook(event.id).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
|
|
||||||
mpv::Event::StartFile(_) => {
|
|
||||||
// since we set up the hook before, the current song is guaranteed not to change
|
|
||||||
// under our feet
|
|
||||||
self.sender
|
|
||||||
.try_broadcast(Event::PlaybinFileStarted)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
assert_eq!(
|
|
||||||
self.entries()[self.current_entry().unwrap()].stream_url(),
|
|
||||||
self.mpv.get_property::<String>("path").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => event!(Level::DEBUG, "mpv event {:?}", event),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stopped(&self) -> Signal<'_, Self, ()> {
|
|
||||||
self.stopped.signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entry_removed(&self) -> Signal<'_, Self, usize> {
|
|
||||||
self.entry_removed.signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_started(&self) -> Signal<'_, Self, ()> {
|
|
||||||
self.file_started.signal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Playbin {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
event!(Level::DEBUG, "dropping Playbin2");
|
|
||||||
self.mpv.command(["quit"]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
123
src/signal.rs
123
src/signal.rs
|
@ -1,123 +0,0 @@
|
||||||
use gtk::{
|
|
||||||
glib::Object,
|
|
||||||
prelude::{IsA, ObjectExt},
|
|
||||||
};
|
|
||||||
use std::cell::{Cell, RefCell};
|
|
||||||
use std::rc::{Rc, Weak};
|
|
||||||
|
|
||||||
type SignalHandlerBox<E, T> = Box<dyn FnMut(&E, T) -> bool>;
|
|
||||||
|
|
||||||
pub struct SignalEmitter<E, T> {
|
|
||||||
handlers: RefCell<Vec<SignalHandlerBox<E, T>>>,
|
|
||||||
just_connected: RefCell<Vec<SignalHandlerBox<E, T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, T> Default for SignalEmitter<E, T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
handlers: RefCell::new(vec![]),
|
|
||||||
just_connected: RefCell::new(vec![]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Signal<'a, E, T> {
|
|
||||||
just_connected: &'a RefCell<Vec<SignalHandlerBox<E, T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct SignalHandler(Weak<Cell<bool>>);
|
|
||||||
|
|
||||||
impl SignalHandler {
|
|
||||||
pub fn disconnect(self) -> bool {
|
|
||||||
match self.0.upgrade() {
|
|
||||||
None => false,
|
|
||||||
Some(cell) => {
|
|
||||||
cell.set(true);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, T> Signal<'_, E, T> {
|
|
||||||
fn connect_impl(&self, f: impl FnMut(&E, T) -> bool + 'static) {
|
|
||||||
self.just_connected.borrow_mut().push(Box::new(f));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect(&self, mut f: impl FnMut(&E, T) -> bool + 'static) -> SignalHandler {
|
|
||||||
let disconnect = Rc::new(Cell::new(false));
|
|
||||||
let disconnect_weak = Rc::downgrade(&disconnect);
|
|
||||||
|
|
||||||
self.connect_impl(move |e, t| match disconnect.get() {
|
|
||||||
true => false,
|
|
||||||
false => {
|
|
||||||
f(e, t);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
SignalHandler(disconnect_weak)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect_rc<L: 'static>(
|
|
||||||
&self,
|
|
||||||
listener: &Rc<L>,
|
|
||||||
mut f: impl FnMut(&E, Rc<L>, T) -> bool + 'static,
|
|
||||||
) -> SignalHandler {
|
|
||||||
let listener = Rc::downgrade(listener);
|
|
||||||
|
|
||||||
self.connect(move |e, t| match listener.upgrade() {
|
|
||||||
None => false,
|
|
||||||
Some(listener) => f(e, listener, t),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect_object<L: IsA<Object>>(
|
|
||||||
&self,
|
|
||||||
listener: &L,
|
|
||||||
mut f: impl FnMut(&E, L, T) -> bool + 'static,
|
|
||||||
) -> SignalHandler {
|
|
||||||
let listener = listener.downgrade();
|
|
||||||
|
|
||||||
self.connect(move |e, t| match listener.upgrade() {
|
|
||||||
None => false,
|
|
||||||
Some(listener) => f(e, listener, t),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, T> SignalEmitter<E, T> {
|
|
||||||
pub fn signal(&self) -> Signal<'_, E, T> {
|
|
||||||
Signal {
|
|
||||||
just_connected: &self.just_connected,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_with(&self, emitter: &E, mut f: impl FnMut() -> T) {
|
|
||||||
let mut handlers = self
|
|
||||||
.handlers
|
|
||||||
.try_borrow_mut()
|
|
||||||
.expect("tried to re-emit signal during emission");
|
|
||||||
handlers.append(self.just_connected.borrow_mut().as_mut());
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
// FIXME: does not preserve ordering
|
|
||||||
while i < handlers.len() {
|
|
||||||
if handlers[i](emitter, f()) {
|
|
||||||
i += 1;
|
|
||||||
} else {
|
|
||||||
drop(handlers.swap_remove(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E, T> SignalEmitter<E, T>
|
|
||||||
where
|
|
||||||
T: Clone,
|
|
||||||
{
|
|
||||||
pub fn emit(&self, emitter: &E, t: T) {
|
|
||||||
self.emit_with(emitter, || t.clone());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,8 @@
|
||||||
mod imp {
|
mod imp {
|
||||||
use crate::signal::SignalEmitter;
|
|
||||||
use adw::{glib, prelude::*, subclass::prelude::*};
|
use adw::{glib, prelude::*, subclass::prelude::*};
|
||||||
use glib::subclass::InitializingObject;
|
use glib::subclass::InitializingObject;
|
||||||
|
use glib::WeakRef;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
||||||
|
@ -25,7 +24,8 @@ mod imp {
|
||||||
#[property(get, set)]
|
#[property(get, set)]
|
||||||
password: RefCell<String>,
|
password: RefCell<String>,
|
||||||
|
|
||||||
pub(super) connected: SignalEmitter<super::Setup, Rc<crate::subsonic::Client>>,
|
#[property(get, set)]
|
||||||
|
window: WeakRef<crate::ui::Window>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
@ -112,7 +112,7 @@ mod imp {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
self.connected.emit(self.obj().as_ref(), Rc::new(api));
|
self.window.upgrade().unwrap().setup_connected(api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,10 +138,6 @@ impl Default for Setup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::signal::Signal;
|
|
||||||
use crate::subsonic;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
impl Setup {
|
impl Setup {
|
||||||
pub fn load(&self) {
|
pub fn load(&self) {
|
||||||
glib::spawn_future_local(glib::clone!(
|
glib::spawn_future_local(glib::clone!(
|
||||||
|
@ -181,8 +177,4 @@ impl Setup {
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connected(&self) -> Signal<'_, super::Setup, Rc<subsonic::Client>> {
|
|
||||||
self.imp().connected.signal()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -325,6 +325,7 @@ mod imp {
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use gtk::{gio, glib};
|
use gtk::{gio, glib};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct Window(ObjectSubclass<imp::Window>)
|
pub struct Window(ObjectSubclass<imp::Window>)
|
||||||
|
@ -336,16 +337,7 @@ impl Window {
|
||||||
pub fn new(app: &impl IsA<gtk::Application>) -> Self {
|
pub fn new(app: &impl IsA<gtk::Application>) -> Self {
|
||||||
let window: Self = glib::Object::builder().property("application", app).build();
|
let window: Self = glib::Object::builder().property("application", app).build();
|
||||||
|
|
||||||
window
|
window.imp().setup.set_window(&window);
|
||||||
.imp()
|
|
||||||
.setup
|
|
||||||
.connected()
|
|
||||||
.connect_object(&window, |_setup, window, api| {
|
|
||||||
window.imp().api.replace(Some(api));
|
|
||||||
//window.imp().playbin.stop();
|
|
||||||
window.set_can_click_shuffle_all(true);
|
|
||||||
true
|
|
||||||
});
|
|
||||||
window.imp().setup.load();
|
window.imp().setup.load();
|
||||||
|
|
||||||
window
|
window
|
||||||
|
@ -369,4 +361,9 @@ impl Window {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.playlist_model().remove(index as u32);
|
self.playlist_model().remove(index as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setup_connected(&self, api: crate::subsonic::Client) {
|
||||||
|
self.imp().api.replace(Some(Rc::new(api)));
|
||||||
|
self.set_can_click_shuffle_all(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue