This commit is contained in:
Erica Z 2024-11-03 16:11:25 +01:00
parent e4083288e6
commit 0c2a614375
6 changed files with 173 additions and 108 deletions

View file

@ -17,7 +17,7 @@ pub use playbin::Playbin;
pub mod subsonic; pub mod subsonic;
pub mod subsonic_vala; pub mod subsonic_vala;
mod playbin2; pub mod playbin2;
mod signal; mod signal;
pub use signal::{Signal, SignalEmitter, SignalHandler}; pub use signal::{Signal, SignalEmitter, SignalHandler};

View file

@ -1,16 +1,16 @@
use crate::mpv; use crate::mpv;
use crate::signal::SignalEmitter; use crate::signal::{Signal, SignalEmitter};
use event_listener::EventListener; use event_listener::EventListener;
use std::cell::{Ref, RefCell}; use std::cell::{Ref, RefCell};
use url::Url; use url::Url;
pub trait PlaybinEntry { pub trait PlaybinEntry {
fn url(&self) -> &Url; fn url(&self) -> Url;
} }
impl PlaybinEntry for Url { impl PlaybinEntry for Url {
fn url(&self) -> &Url { fn url(&self) -> Url {
self self.clone()
} }
} }
@ -19,12 +19,16 @@ pub struct Playbin<E> {
mpv: mpv::Handle, mpv: mpv::Handle,
entries: RefCell<Vec<E>>, entries: RefCell<Vec<E>>,
paused_changed: SignalEmitter<()>, volume_changed: SignalEmitter<Self, ()>,
current_entry_changed: SignalEmitter<()>, muted_changed: SignalEmitter<Self, ()>,
paused_changed: SignalEmitter<Self, ()>,
current_entry_changed: SignalEmitter<Self, ()>,
entry_inserted: SignalEmitter<u32>, entry_inserted: SignalEmitter<Self, u32>,
stopped: SignalEmitter<()>, stopped: SignalEmitter<Self, ()>,
entry_removed: SignalEmitter<u32>, entry_removed: SignalEmitter<Self, u32>,
file_started: SignalEmitter<Self, ()>,
} }
impl<E> Default for Playbin<E> { impl<E> Default for Playbin<E> {
@ -36,8 +40,10 @@ impl<E> Default for Playbin<E> {
mpv.set_property("prefetch-playlist", true).unwrap(); mpv.set_property("prefetch-playlist", true).unwrap();
mpv.set_property("gapless-audio", true).unwrap(); mpv.set_property("gapless-audio", true).unwrap();
mpv.observe_property(0, "pause").unwrap(); mpv.observe_property(0, "volume").unwrap();
mpv.observe_property(1, "playlist-pos").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." // "Useful to drain property changes before a new file is loaded."
mpv.add_hook(0, "on_before_start_file", 0).unwrap(); mpv.add_hook(0, "on_before_start_file", 0).unwrap();
@ -46,12 +52,16 @@ impl<E> Default for Playbin<E> {
mpv, mpv,
entries: RefCell::new(vec![]), entries: RefCell::new(vec![]),
volume_changed: Default::default(),
muted_changed: Default::default(),
paused_changed: Default::default(), paused_changed: Default::default(),
current_entry_changed: Default::default(), current_entry_changed: Default::default(),
entry_inserted: Default::default(), entry_inserted: Default::default(),
stopped: Default::default(), stopped: Default::default(),
entry_removed: Default::default(), entry_removed: Default::default(),
file_started: Default::default(),
} }
} }
} }
@ -60,6 +70,22 @@ impl<E> Playbin<E>
where where
E: PlaybinEntry, E: PlaybinEntry,
{ {
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 { pub fn paused(&self) -> bool {
self.mpv.get_property("pause").unwrap() self.mpv.get_property("pause").unwrap()
} }
@ -111,7 +137,7 @@ where
entries.push(entry); entries.push(entry);
drop(entries); drop(entries);
self.entry_inserted.emit(index as u32); self.entry_inserted.emit(self, index as u32);
} }
pub fn insert_entry(&self, index: u32, entry: E) { pub fn insert_entry(&self, index: u32, entry: E) {
@ -122,7 +148,7 @@ where
entries.insert(index as usize, entry); entries.insert(index as usize, entry);
drop(entries); drop(entries);
self.entry_inserted.emit(index); self.entry_inserted.emit(self, index);
} }
// stop playback and clear playlist // stop playback and clear playlist
@ -132,7 +158,7 @@ where
entries.clear(); entries.clear();
drop(entries); drop(entries);
self.stopped.emit(()); self.stopped.emit(self, ());
} }
pub fn remove_entry(&self, index: u32) { pub fn remove_entry(&self, index: u32) {
@ -141,7 +167,7 @@ where
entries.remove(index as usize); entries.remove(index as usize);
drop(entries); drop(entries);
self.entry_removed.emit(index); self.entry_removed.emit(self, index);
} }
pub fn move_entry(&self, _from: u32, _to: u32) { pub fn move_entry(&self, _from: u32, _to: u32) {
@ -160,14 +186,26 @@ where
match event { match event {
mpv::Event::PropertyChange(event) => match event.reply_userdata { mpv::Event::PropertyChange(event) => match event.reply_userdata {
0 => { 0 => {
assert_eq!(&event.name, "pause"); assert_eq!(&event.name, "volume");
self.paused_changed.emit(()); self.volume_changed.emit(self, ());
println!("new paused! {:?}", self.paused()); println!("new volume! {:?}", self.volume());
} }
1 => { 1 => {
assert_eq!(&event.name, "mute");
self.muted_changed.emit(self, ());
println!("new muted! {:?}", self.muted());
}
2 => {
assert_eq!(&event.name, "pause");
self.paused_changed.emit(self, ());
println!("new paused! {:?}", self.paused());
}
3 => {
assert_eq!(&event.name, "playlist-pos"); assert_eq!(&event.name, "playlist-pos");
self.current_entry_changed.emit(()); self.current_entry_changed.emit(self, ());
println!("new current_entry! {:?}", self.current_entry()); println!("new current_entry! {:?}", self.current_entry());
} }
@ -185,9 +223,23 @@ where
_ => unreachable!(), _ => unreachable!(),
}, },
mpv::Event::StartFile(_) => {
// since we set up the hook before, the current song is guaranteed not to change
// under our feet
self.file_started.emit(self, ());
}
_ => println!("mpv event {:?}", event), _ => println!("mpv event {:?}", event),
} }
} }
pub fn volume_changed(&self) -> Signal<'_, Self, ()> {
self.volume_changed.signal()
}
pub fn file_started(&self) -> Signal<'_, Self, ()> {
self.file_started.signal()
}
} }
impl<E> Drop for Playbin<E> { impl<E> Drop for Playbin<E> {

View file

@ -5,14 +5,14 @@ use gtk::{
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
type SignalHandlerBox<T> = Box<dyn FnMut(T) -> bool>; type SignalHandlerBox<E, T> = Box<dyn FnMut(&E, T) -> bool>;
pub struct SignalEmitter<T> { pub struct SignalEmitter<E, T> {
handlers: RefCell<Vec<SignalHandlerBox<T>>>, handlers: RefCell<Vec<SignalHandlerBox<E, T>>>,
just_connected: RefCell<Vec<SignalHandlerBox<T>>>, just_connected: RefCell<Vec<SignalHandlerBox<E, T>>>,
} }
impl<T> Default for SignalEmitter<T> { impl<E, T> Default for SignalEmitter<E, T> {
fn default() -> Self { fn default() -> Self {
Self { Self {
handlers: RefCell::new(vec![]), handlers: RefCell::new(vec![]),
@ -21,8 +21,8 @@ impl<T> Default for SignalEmitter<T> {
} }
} }
pub struct Signal<'a, T> { pub struct Signal<'a, E, T> {
just_connected: &'a RefCell<Vec<SignalHandlerBox<T>>>, just_connected: &'a RefCell<Vec<SignalHandlerBox<E, T>>>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -40,19 +40,19 @@ impl SignalHandler {
} }
} }
impl<T> Signal<'_, T> { impl<E, T> Signal<'_, E, T> {
fn connect_impl(&self, f: impl FnMut(T) -> bool + 'static) { fn connect_impl(&self, f: impl FnMut(&E, T) -> bool + 'static) {
self.just_connected.borrow_mut().push(Box::new(f)); self.just_connected.borrow_mut().push(Box::new(f));
} }
pub fn connect(&self, mut f: impl FnMut(T) -> bool + 'static) -> SignalHandler { pub fn connect(&self, mut f: impl FnMut(&E, T) -> bool + 'static) -> SignalHandler {
let disconnect = Rc::new(Cell::new(false)); let disconnect = Rc::new(Cell::new(false));
let disconnect_weak = Rc::downgrade(&disconnect); let disconnect_weak = Rc::downgrade(&disconnect);
self.connect_impl(move |t| match disconnect.get() { self.connect_impl(move |e, t| match disconnect.get() {
false => false, true => false,
true => { false => {
f(t); f(e, t);
true true
} }
}); });
@ -63,38 +63,38 @@ impl<T> Signal<'_, T> {
pub fn connect_rc<L: 'static>( pub fn connect_rc<L: 'static>(
&self, &self,
listener: &Rc<L>, listener: &Rc<L>,
mut f: impl FnMut(Rc<L>, T) -> bool + 'static, mut f: impl FnMut(&E, Rc<L>, T) -> bool + 'static,
) -> SignalHandler { ) -> SignalHandler {
let listener = Rc::downgrade(listener); let listener = Rc::downgrade(listener);
self.connect(move |t| match listener.upgrade() { self.connect(move |e, t| match listener.upgrade() {
None => false, None => false,
Some(listener) => f(listener, t), Some(listener) => f(e, listener, t),
}) })
} }
pub fn connect_object<L: IsA<Object>>( pub fn connect_object<L: IsA<Object>>(
&self, &self,
listener: &L, listener: &L,
mut f: impl FnMut(L, T) -> bool + 'static, mut f: impl FnMut(&E, L, T) -> bool + 'static,
) -> SignalHandler { ) -> SignalHandler {
let listener = listener.downgrade(); let listener = listener.downgrade();
self.connect(move |t| match listener.upgrade() { self.connect(move |e, t| match listener.upgrade() {
None => false, None => false,
Some(listener) => f(listener, t), Some(listener) => f(e, listener, t),
}) })
} }
} }
impl<T> SignalEmitter<T> { impl<E, T> SignalEmitter<E, T> {
pub fn signal(&self) -> Signal<'_, T> { pub fn signal(&self) -> Signal<'_, E, T> {
Signal { Signal {
just_connected: &self.just_connected, just_connected: &self.just_connected,
} }
} }
pub fn emit_with(&self, mut f: impl FnMut() -> T) { pub fn emit_with(&self, emitter: &E, mut f: impl FnMut() -> T) {
let mut handlers = self let mut handlers = self
.handlers .handlers
.try_borrow_mut() .try_borrow_mut()
@ -108,7 +108,7 @@ impl<T> SignalEmitter<T> {
let mut i = 0; let mut i = 0;
let mut skip = 0; let mut skip = 0;
loop { loop {
if handlers[i + skip](f()) { if handlers[i + skip](emitter, f()) {
i += 1; i += 1;
} else { } else {
skip += 1; skip += 1;
@ -121,15 +121,16 @@ impl<T> SignalEmitter<T> {
handlers.swap(i, i + skip); handlers.swap(i, i + skip);
} }
println!("emitted to {i} listeners");
handlers.truncate(i); handlers.truncate(i);
} }
} }
impl<T> SignalEmitter<T> impl<E, T> SignalEmitter<E, T>
where where
T: Clone, T: Clone,
{ {
pub fn emit(&self, t: T) { pub fn emit(&self, emitter: &E, t: T) {
self.emit_with(|| t.clone()); self.emit_with(emitter, || t.clone());
} }
} }

View file

@ -1,4 +1,4 @@
mod schema; pub mod schema;
use md5::Digest; use md5::Digest;
use rand::Rng; use rand::Rng;
@ -145,17 +145,22 @@ impl Client {
} }
} }
async fn get<T: serde::de::DeserializeOwned + Send + 'static>( fn url(&self, path: &[&str], query: &[(&str, &str)]) -> url::Url {
&self,
path: &[&str],
query: &[(&str, &str)],
) -> Result<T, Error> {
let mut url = self.base_url.clone(); let mut url = self.base_url.clone();
url.path_segments_mut() url.path_segments_mut()
// literally can't fail // literally can't fail
.unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() }) .unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() })
.extend(path); .extend(path);
self.send(self.client.get(url).query(query)).await url.query_pairs_mut().extend_pairs(query);
url
}
async fn get<T: serde::de::DeserializeOwned + Send + 'static>(
&self,
path: &[&str],
query: &[(&str, &str)],
) -> Result<T, Error> {
self.send(self.client.get(self.url(path, query))).await
} }
pub async fn ping(&self) -> Result<(), Error> { pub async fn ping(&self) -> Result<(), Error> {
@ -170,6 +175,10 @@ impl Client {
.await .await
.map(|response| response.random_songs.song) .map(|response| response.random_songs.song)
} }
pub fn stream_url(&self, id: &str) -> url::Url {
self.url(&["rest", "stream"], &[("id", id)])
}
} }
impl Drop for Client { impl Drop for Client {

View file

@ -1,7 +1,9 @@
mod imp { mod imp {
use crate::signal::SignalEmitter;
use adw::{glib, prelude::*, subclass::prelude::*}; use adw::{glib, prelude::*, subclass::prelude::*};
use glib::subclass::{InitializingObject, Signal}; use glib::subclass::{InitializingObject, Signal};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::OnceLock; use std::sync::OnceLock;
#[derive(gtk::CompositeTemplate, glib::Properties, Default)] #[derive(gtk::CompositeTemplate, glib::Properties, Default)]
@ -22,6 +24,8 @@ mod imp {
username: RefCell<String>, username: RefCell<String>,
#[property(get, set)] #[property(get, set)]
password: RefCell<String>, password: RefCell<String>,
pub(super) connected: SignalEmitter<super::Setup, Rc<crate::subsonic::Client>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -145,6 +149,7 @@ mod imp {
self.obj().set_authn_can_edit(true); self.obj().set_authn_can_edit(true);
self.obj().emit_by_name::<()>("connected", &[&vala_api]); self.obj().emit_by_name::<()>("connected", &[&vala_api]);
self.connected.emit(self.obj().as_ref(), Rc::new(api));
} }
} }
@ -170,6 +175,10 @@ 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!(
@ -209,6 +218,10 @@ impl Setup {
} }
)); ));
} }
pub fn connected(&self) -> Signal<'_, super::Setup, Rc<subsonic::Client>> {
self.imp().connected.signal()
}
} }
mod ffi { mod ffi {

View file

@ -26,9 +26,9 @@ mod imp {
song: RefCell<Option<crate::playbin::Song>>, song: RefCell<Option<crate::playbin::Song>>,
pub(super) setup: crate::ui::Setup, pub(super) setup: crate::ui::Setup,
pub(super) api: RefCell<Option<crate::subsonic_vala::Client>>,
playbin2: Rc<crate::playbin2::Playbin<url::Url>>, pub(super) playbin2: Rc<crate::playbin2::Playbin<url::Url>>,
pub(super) api2: RefCell<Option<Rc<crate::subsonic::Client>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -52,13 +52,6 @@ mod imp {
fn constructed(&self) { fn constructed(&self) {
self.parent_constructed(); self.parent_constructed();
self.playbin2.tick();
self.playbin2.push_entry(
"https://www.youtube.com/watch?v=19y8YTbvri8"
.try_into()
.unwrap(),
);
let playbin = Rc::downgrade(&self.playbin2); let playbin = Rc::downgrade(&self.playbin2);
glib::spawn_future_local(glib::clone!(async move { glib::spawn_future_local(glib::clone!(async move {
loop { loop {
@ -128,22 +121,15 @@ mod imp {
#[template_callback] #[template_callback]
async fn shuffle_all(&self) { async fn shuffle_all(&self) {
/* self.obj().set_can_click_shuffle_all(false);
this.can_click_shuffle_all = false; self.playbin2.stop();
this.playbin.clear (); let api = self.api2.borrow();
api.get_random_songs.begin (null, (song) => { let api = api.as_ref().unwrap();
this.playbin.append_track (song); for song in api.get_random_songs(10).await.unwrap().into_iter() {
}, (obj, res) => { println!("{song:?}");
try { self.playbin2.push_entry(api.stream_url(&song.id));
api.get_random_songs.end (res);
} catch (Error e) {
error ("could not get random songs: %s", e.message);
} }
this.can_click_shuffle_all = true; self.obj().set_can_click_shuffle_all(true);
this.playbin.select_track (0);
});*/
todo!()
} }
#[template_callback] #[template_callback]
@ -202,39 +188,43 @@ 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 // manual bidirectional sync
.playbin() window.imp().playbin2.volume_changed().connect_object(
.bind_property("volume", &*window.imp().playbar, "volume") &*window.imp().playbar,
.bidirectional() |playbin, playbar, ()| {
.sync_create() playbar.set_volume(playbin.volume() as i32);
.build(); true
},
window.imp().setup.connect_closure( );
"connected", window.imp().playbar.connect_notify_local(
false, Some("volume"),
glib::closure_local!( glib::clone!(
#[weak] #[weak(rename_to = playbin)]
window, window.imp().playbin2,
move |_setup: crate::ui::Setup, api: crate::subsonic_vala::Client| { move |playbar, _| playbin.set_volume(playbar.volume() as i64)
window.imp().api.replace(Some(api.clone()));
window.playbin().set_api(&api);
window.set_can_click_shuffle_all(true);
}
), ),
); );
window
.imp()
.setup
.connected()
.connect_object(&window, |_setup, window, api| {
window.imp().api2.replace(Some(api));
window.imp().playbin2.stop();
window.set_can_click_shuffle_all(true);
true
});
window.imp().setup.load(); window.imp().setup.load();
window.playbin().connect_closure( window
"new-track", .imp()
false, .playbin2
glib::closure_local!( .file_started()
#[weak] .connect_object(&window, |_playbin, _window, ()| {
window, // TODO window.imp().now_playing(song);
move |_playbin: crate::Playbin, song: crate::playbin::Song| { true
window.imp().now_playing(&song); });
}
),
);
window.playbin().connect_closure( window.playbin().connect_closure(
"stopped", "stopped",