Compare commits

...

2 commits

Author SHA1 Message Date
acd1d7d803 mute 2024-11-03 16:17:54 +01:00
0c2a614375 volume 2024-11-03 16:11:25 +01:00
8 changed files with 199 additions and 111 deletions

View file

@ -150,7 +150,7 @@ template $AudreyUiPlaybar: Adw.Bin {
}
Button {
icon-name: bind $mute_button_icon_name (template.playbin as <$AudreyPlaybin>.mute) as <string>;
icon-name: bind $mute_button_icon_name (template.mute) as <string>;
valign: center;
clicked => $on_mute_toggle () swapped;

View file

@ -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};

View file

@ -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<E> {
mpv: mpv::Handle,
entries: RefCell<Vec<E>>,
paused_changed: SignalEmitter<()>,
current_entry_changed: SignalEmitter<()>,
volume_changed: SignalEmitter<Self, ()>,
muted_changed: SignalEmitter<Self, ()>,
paused_changed: SignalEmitter<Self, ()>,
current_entry_changed: SignalEmitter<Self, ()>,
entry_inserted: SignalEmitter<u32>,
stopped: SignalEmitter<()>,
entry_removed: SignalEmitter<u32>,
entry_inserted: SignalEmitter<Self, u32>,
stopped: SignalEmitter<Self, ()>,
entry_removed: SignalEmitter<Self, u32>,
file_started: SignalEmitter<Self, ()>,
}
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("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<E> Default for Playbin<E> {
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<E> Playbin<E>
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,27 @@ 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 muted_changed(&self) -> Signal<'_, Self, ()> {
self.muted_changed.signal()
}
pub fn file_started(&self) -> Signal<'_, Self, ()> {
self.file_started.signal()
}
}
impl<E> Drop for Playbin<E> {

View file

@ -5,14 +5,14 @@ use gtk::{
use std::cell::{Cell, RefCell};
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> {
handlers: RefCell<Vec<SignalHandlerBox<T>>>,
just_connected: RefCell<Vec<SignalHandlerBox<T>>>,
pub struct SignalEmitter<E, T> {
handlers: RefCell<Vec<SignalHandlerBox<E, 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 {
Self {
handlers: RefCell::new(vec![]),
@ -21,8 +21,8 @@ impl<T> Default for SignalEmitter<T> {
}
}
pub struct Signal<'a, T> {
just_connected: &'a RefCell<Vec<SignalHandlerBox<T>>>,
pub struct Signal<'a, E, T> {
just_connected: &'a RefCell<Vec<SignalHandlerBox<E, T>>>,
}
#[derive(Clone)]
@ -40,19 +40,19 @@ impl SignalHandler {
}
}
impl<T> Signal<'_, T> {
fn connect_impl(&self, f: impl FnMut(T) -> bool + 'static) {
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(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<T> Signal<'_, T> {
pub fn connect_rc<L: 'static>(
&self,
listener: &Rc<L>,
mut f: impl FnMut(Rc<L>, T) -> bool + 'static,
mut f: impl FnMut(&E, Rc<L>, 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<L: IsA<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<T> SignalEmitter<T> {
pub fn signal(&self) -> Signal<'_, 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, 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<T> SignalEmitter<T> {
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<T> SignalEmitter<T> {
handlers.swap(i, i + skip);
}
println!("emitted to {i} listeners");
handlers.truncate(i);
}
}
impl<T> SignalEmitter<T>
impl<E, T> SignalEmitter<E, T>
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());
}
}

View file

@ -1,4 +1,4 @@
mod schema;
pub mod schema;
use md5::Digest;
use rand::Rng;
@ -145,17 +145,22 @@ impl Client {
}
}
async fn get<T: serde::de::DeserializeOwned + Send + 'static>(
&self,
path: &[&str],
query: &[(&str, &str)],
) -> Result<T, Error> {
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<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> {
@ -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 {

View file

@ -21,6 +21,8 @@ mod imp {
#[property(get, set)]
volume: Cell<i32>,
#[property(get, set)]
mute: Cell<bool>,
}
#[glib::object_subclass]
@ -156,8 +158,7 @@ mod imp {
#[template_callback]
fn on_mute_toggle(&self) {
let playbin = self.playbin.upgrade().unwrap();
playbin.set_mute(!playbin.mute());
self.obj().set_mute(!self.obj().mute());
}
}

View file

@ -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<String>,
#[property(get, set)]
password: RefCell<String>,
pub(super) connected: SignalEmitter<super::Setup, Rc<crate::subsonic::Client>>,
}
#[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<subsonic::Client>> {
self.imp().connected.signal()
}
}
mod ffi {

View file

@ -26,9 +26,9 @@ mod imp {
song: RefCell<Option<crate::playbin::Song>>,
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]
@ -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,61 @@ impl Window {
pub fn new(app: &impl IsA<gtk::Application>) -> 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().playbar.set_volume(window.imp().playbin2.volume() as i32);
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().playbar.set_mute(window.imp().playbin2.muted());
window.imp().playbin2.muted_changed().connect_object(
&*window.imp().playbar,
|playbin, playbar, ()| {
playbar.set_mute(playbin.muted());
true
},
);
window.imp().playbar.connect_notify_local(
Some("mute"),
glib::clone!(
#[weak(rename_to = playbin)]
window.imp().playbin2,
move |playbar, _| playbin.set_muted(playbar.mute())
),
);
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",