diff --git a/src/main.rs b/src/main.rs index b8e44df..bff1568 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ pub use playbin::Playbin; pub mod subsonic; pub mod subsonic_vala; -mod playbin2; +pub mod playbin2; mod signal; pub use signal::{Signal, SignalEmitter, SignalHandler}; diff --git a/src/playbin2.rs b/src/playbin2.rs index ede297c..f0252f5 100644 --- a/src/playbin2.rs +++ b/src/playbin2.rs @@ -1,16 +1,16 @@ use crate::mpv; -use crate::signal::SignalEmitter; +use crate::signal::{Signal, SignalEmitter}; use event_listener::EventListener; use std::cell::{Ref, RefCell}; use url::Url; pub trait PlaybinEntry { - fn url(&self) -> &Url; + fn url(&self) -> Url; } impl PlaybinEntry for Url { - fn url(&self) -> &Url { - self + fn url(&self) -> Url { + self.clone() } } @@ -19,12 +19,16 @@ pub struct Playbin { mpv: mpv::Handle, entries: RefCell>, - paused_changed: SignalEmitter<()>, - current_entry_changed: SignalEmitter<()>, + volume_changed: SignalEmitter, + muted_changed: SignalEmitter, + paused_changed: SignalEmitter, + current_entry_changed: SignalEmitter, - entry_inserted: SignalEmitter, - stopped: SignalEmitter<()>, - entry_removed: SignalEmitter, + entry_inserted: SignalEmitter, + stopped: SignalEmitter, + entry_removed: SignalEmitter, + + file_started: SignalEmitter, } impl Default for Playbin { @@ -36,8 +40,10 @@ impl Default for Playbin { mpv.set_property("prefetch-playlist", true).unwrap(); mpv.set_property("gapless-audio", true).unwrap(); - mpv.observe_property(0, "pause").unwrap(); - mpv.observe_property(1, "playlist-pos").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(); @@ -46,12 +52,16 @@ impl Default for Playbin { mpv, entries: RefCell::new(vec![]), + volume_changed: Default::default(), + muted_changed: Default::default(), paused_changed: Default::default(), current_entry_changed: Default::default(), entry_inserted: Default::default(), stopped: Default::default(), entry_removed: Default::default(), + + file_started: Default::default(), } } } @@ -60,6 +70,22 @@ impl Playbin where 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 { self.mpv.get_property("pause").unwrap() } @@ -111,7 +137,7 @@ where entries.push(entry); 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) { @@ -122,7 +148,7 @@ where entries.insert(index as usize, entry); drop(entries); - self.entry_inserted.emit(index); + self.entry_inserted.emit(self, index); } // stop playback and clear playlist @@ -132,7 +158,7 @@ where entries.clear(); drop(entries); - self.stopped.emit(()); + self.stopped.emit(self, ()); } pub fn remove_entry(&self, index: u32) { @@ -141,7 +167,7 @@ where entries.remove(index as usize); drop(entries); - self.entry_removed.emit(index); + self.entry_removed.emit(self, index); } pub fn move_entry(&self, _from: u32, _to: u32) { @@ -160,14 +186,26 @@ where match event { mpv::Event::PropertyChange(event) => match event.reply_userdata { 0 => { - assert_eq!(&event.name, "pause"); - self.paused_changed.emit(()); - println!("new paused! {:?}", self.paused()); + assert_eq!(&event.name, "volume"); + self.volume_changed.emit(self, ()); + println!("new volume! {:?}", self.volume()); } 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"); - self.current_entry_changed.emit(()); + self.current_entry_changed.emit(self, ()); println!("new current_entry! {:?}", self.current_entry()); } @@ -185,9 +223,23 @@ where _ => 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), } } + + pub fn volume_changed(&self) -> Signal<'_, Self, ()> { + self.volume_changed.signal() + } + + pub fn file_started(&self) -> Signal<'_, Self, ()> { + self.file_started.signal() + } } impl Drop for Playbin { diff --git a/src/signal.rs b/src/signal.rs index 3443339..cb1780e 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -5,14 +5,14 @@ use gtk::{ use std::cell::{Cell, RefCell}; use std::rc::{Rc, Weak}; -type SignalHandlerBox = Box bool>; +type SignalHandlerBox = Box bool>; -pub struct SignalEmitter { - handlers: RefCell>>, - just_connected: RefCell>>, +pub struct SignalEmitter { + handlers: RefCell>>, + just_connected: RefCell>>, } -impl Default for SignalEmitter { +impl Default for SignalEmitter { fn default() -> Self { Self { handlers: RefCell::new(vec![]), @@ -21,8 +21,8 @@ impl Default for SignalEmitter { } } -pub struct Signal<'a, T> { - just_connected: &'a RefCell>>, +pub struct Signal<'a, E, T> { + just_connected: &'a RefCell>>, } #[derive(Clone)] @@ -40,19 +40,19 @@ impl SignalHandler { } } -impl Signal<'_, T> { - fn connect_impl(&self, f: impl FnMut(T) -> bool + 'static) { +impl 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(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_weak = Rc::downgrade(&disconnect); - self.connect_impl(move |t| match disconnect.get() { - false => false, - true => { - f(t); + self.connect_impl(move |e, t| match disconnect.get() { + true => false, + false => { + f(e, t); true } }); @@ -63,38 +63,38 @@ impl Signal<'_, T> { pub fn connect_rc( &self, listener: &Rc, - mut f: impl FnMut(Rc, T) -> bool + 'static, + mut f: impl FnMut(&E, Rc, T) -> bool + 'static, ) -> SignalHandler { let listener = Rc::downgrade(listener); - self.connect(move |t| match listener.upgrade() { + self.connect(move |e, t| match listener.upgrade() { None => false, - Some(listener) => f(listener, t), + Some(listener) => f(e, listener, t), }) } pub fn connect_object>( &self, listener: &L, - mut f: impl FnMut(L, T) -> bool + 'static, + mut f: impl FnMut(&E, L, T) -> bool + 'static, ) -> SignalHandler { let listener = listener.downgrade(); - self.connect(move |t| match listener.upgrade() { + self.connect(move |e, t| match listener.upgrade() { None => false, - Some(listener) => f(listener, t), + Some(listener) => f(e, listener, t), }) } } -impl SignalEmitter { - pub fn signal(&self) -> Signal<'_, T> { +impl SignalEmitter { + pub fn signal(&self) -> Signal<'_, E, T> { Signal { 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 .handlers .try_borrow_mut() @@ -108,7 +108,7 @@ impl SignalEmitter { let mut i = 0; let mut skip = 0; loop { - if handlers[i + skip](f()) { + if handlers[i + skip](emitter, f()) { i += 1; } else { skip += 1; @@ -121,15 +121,16 @@ impl SignalEmitter { handlers.swap(i, i + skip); } + println!("emitted to {i} listeners"); handlers.truncate(i); } } -impl SignalEmitter +impl SignalEmitter where T: Clone, { - pub fn emit(&self, t: T) { - self.emit_with(|| t.clone()); + pub fn emit(&self, emitter: &E, t: T) { + self.emit_with(emitter, || t.clone()); } } diff --git a/src/subsonic.rs b/src/subsonic.rs index 82613de..8dba947 100644 --- a/src/subsonic.rs +++ b/src/subsonic.rs @@ -1,4 +1,4 @@ -mod schema; +pub mod schema; use md5::Digest; use rand::Rng; @@ -145,17 +145,22 @@ impl Client { } } - async fn get( - &self, - path: &[&str], - query: &[(&str, &str)], - ) -> Result { + fn url(&self, path: &[&str], query: &[(&str, &str)]) -> url::Url { let mut url = self.base_url.clone(); url.path_segments_mut() // literally can't fail .unwrap_or_else(|_| unsafe { std::hint::unreachable_unchecked() }) .extend(path); - self.send(self.client.get(url).query(query)).await + url.query_pairs_mut().extend_pairs(query); + url + } + + async fn get( + &self, + path: &[&str], + query: &[(&str, &str)], + ) -> Result { + self.send(self.client.get(self.url(path, query))).await } pub async fn ping(&self) -> Result<(), Error> { @@ -170,6 +175,10 @@ impl Client { .await .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 { diff --git a/src/ui/setup.rs b/src/ui/setup.rs index 4b2b73d..ee207ec 100644 --- a/src/ui/setup.rs +++ b/src/ui/setup.rs @@ -1,7 +1,9 @@ mod imp { + use crate::signal::SignalEmitter; use adw::{glib, prelude::*, subclass::prelude::*}; use glib::subclass::{InitializingObject, Signal}; use std::cell::{Cell, RefCell}; + use std::rc::Rc; use std::sync::OnceLock; #[derive(gtk::CompositeTemplate, glib::Properties, Default)] @@ -22,6 +24,8 @@ mod imp { username: RefCell, #[property(get, set)] password: RefCell, + + pub(super) connected: SignalEmitter>, } #[glib::object_subclass] @@ -145,6 +149,7 @@ mod imp { self.obj().set_authn_can_edit(true); 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 { pub fn load(&self) { glib::spawn_future_local(glib::clone!( @@ -209,6 +218,10 @@ impl Setup { } )); } + + pub fn connected(&self) -> Signal<'_, super::Setup, Rc> { + self.imp().connected.signal() + } } mod ffi { diff --git a/src/ui/window.rs b/src/ui/window.rs index 6d5aaff..60defd5 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -26,9 +26,9 @@ mod imp { song: RefCell>, pub(super) setup: crate::ui::Setup, - pub(super) api: RefCell>, - playbin2: Rc>, + pub(super) playbin2: Rc>, + pub(super) api2: RefCell>>, } #[glib::object_subclass] @@ -52,13 +52,6 @@ mod imp { fn constructed(&self) { 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); glib::spawn_future_local(glib::clone!(async move { loop { @@ -128,22 +121,15 @@ mod imp { #[template_callback] async fn shuffle_all(&self) { - /* - this.can_click_shuffle_all = false; - this.playbin.clear (); - api.get_random_songs.begin (null, (song) => { - this.playbin.append_track (song); - }, (obj, res) => { - try { - api.get_random_songs.end (res); - } catch (Error e) { - error ("could not get random songs: %s", e.message); - } - this.can_click_shuffle_all = true; - - this.playbin.select_track (0); - });*/ - todo!() + self.obj().set_can_click_shuffle_all(false); + self.playbin2.stop(); + let api = self.api2.borrow(); + let api = api.as_ref().unwrap(); + for song in api.get_random_songs(10).await.unwrap().into_iter() { + println!("{song:?}"); + self.playbin2.push_entry(api.stream_url(&song.id)); + } + self.obj().set_can_click_shuffle_all(true); } #[template_callback] @@ -202,39 +188,43 @@ impl Window { pub fn new(app: &impl IsA) -> Self { let window: Self = glib::Object::builder().property("application", app).build(); - window - .playbin() - .bind_property("volume", &*window.imp().playbar, "volume") - .bidirectional() - .sync_create() - .build(); - - window.imp().setup.connect_closure( - "connected", - false, - glib::closure_local!( - #[weak] - window, - move |_setup: crate::ui::Setup, api: crate::subsonic_vala::Client| { - window.imp().api.replace(Some(api.clone())); - window.playbin().set_api(&api); - window.set_can_click_shuffle_all(true); - } + // manual bidirectional sync + window.imp().playbin2.volume_changed().connect_object( + &*window.imp().playbar, + |playbin, playbar, ()| { + playbar.set_volume(playbin.volume() as i32); + true + }, + ); + window.imp().playbar.connect_notify_local( + Some("volume"), + glib::clone!( + #[weak(rename_to = playbin)] + window.imp().playbin2, + move |playbar, _| playbin.set_volume(playbar.volume() as i64) ), ); + + 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.playbin().connect_closure( - "new-track", - false, - glib::closure_local!( - #[weak] - window, - move |_playbin: crate::Playbin, song: crate::playbin::Song| { - window.imp().now_playing(&song); - } - ), - ); + window + .imp() + .playbin2 + .file_started() + .connect_object(&window, |_playbin, _window, ()| { + // TODO window.imp().now_playing(song); + true + }); window.playbin().connect_closure( "stopped",