Compare commits

..

6 commits

Author SHA1 Message Date
7da4c6cea3 uh huh 2024-11-03 19:08:58 +01:00
99d31fb28f better devbugprinting 2024-11-03 18:51:58 +01:00
d4a2996eac sanity check 2024-11-03 18:50:49 +01:00
ec0992ce54 play queue now works again (sort of) 2024-11-03 18:45:52 +01:00
0a5f8a9162 todo mpris rethink 2024-11-03 18:01:29 +01:00
fc9eae2eda aaalmost theeeereee 2024-11-03 16:27:47 +01:00
13 changed files with 464 additions and 334 deletions

View file

@ -5,7 +5,7 @@ template $AudreyUiPlayQueue: Adw.Bin {
name: "play-queue"; name: "play-queue";
child: Stack { child: Stack {
visible-child-name: bind $visible_child_name (template.playbin as <$AudreyPlaybin>.play-queue-length) as <string>; visible-child-name: bind $visible_child_name (template.model as <$GListStore>.n-items) as <string>;
StackPage { StackPage {
name: "empty"; name: "empty";
@ -31,7 +31,7 @@ template $AudreyUiPlayQueue: Adw.Bin {
activate => $on_row_activated () swapped; activate => $on_row_activated () swapped;
model: NoSelection { model: NoSelection {
model: bind template.playbin as <$AudreyPlaybin>.play_queue; model: bind template.model;
}; };
factory: SignalListItemFactory { factory: SignalListItemFactory {

View file

@ -35,7 +35,7 @@ template $AudreyUiPlayQueueSong: Box {
margin-top: 1; margin-top: 1;
margin-bottom: 1; margin-bottom: 1;
pixel-size: 50; pixel-size: 50;
paintable: bind template.song as <$AudreyPlaybinSong>.thumbnail; //paintable: bind template.song as <$AudreyPlaybinSong>.thumbnail;
} }
Box title_box { Box title_box {

View file

@ -119,7 +119,7 @@ template $AudreyUiWindow: Adw.ApplicationWindow {
margin-end: 24; margin-end: 24;
styles [ "frame" ] styles [ "frame" ]
playbin: bind template.playbin; //playbin: bind template.playbin;
} }
}; };
} }

View file

@ -1,8 +1,10 @@
use glib::SendWeakRef; use gtk::glib::spawn_future_local;
use gtk::glib;
use std::collections::HashMap; use std::collections::HashMap;
use zbus::object_server::SignalEmitter; use std::rc::{Rc, Weak};
use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value}; use zbus::zvariant::{ObjectPath, OwnedObjectPath, OwnedValue, Value};
use crate::playbin::Song as PlaybinSong;
type Playbin = crate::playbin2::Playbin<PlaybinSong>;
const MICROSECONDS: f64 = 1e6; // in a second const MICROSECONDS: f64 = 1e6; // in a second
@ -57,70 +59,91 @@ impl MetadataMap {
.unwrap_or_default() .unwrap_or_default()
} }
fn as_hash_map(&self) -> HashMap<&'static str, Value> { fn as_hash_map(&self) -> HashMap<&'static str, OwnedValue> {
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()).try_into().unwrap(),
);
} }
if let Some(art_url) = &self.art_url { if let Some(art_url) = &self.art_url {
map.insert("mpris:artUrl", Value::new(art_url.to_string())); map.insert(
"mpris:artUrl",
Value::new(art_url.to_string()).try_into().unwrap(),
);
} }
if let Some(length) = &self.length { if let Some(length) = &self.length {
map.insert("mpris:length", Value::new(length)); map.insert("mpris:length", Value::new(length).try_into().unwrap());
} }
if let Some(album) = &self.album { if let Some(album) = &self.album {
map.insert("xesam:album", Value::new(album)); map.insert("xesam:album", Value::new(album).try_into().unwrap());
} }
if let Some(artist) = &self.artist { if let Some(artist) = &self.artist {
map.insert("xesam:artist", Value::new(artist)); map.insert("xesam:artist", Value::new(artist).try_into().unwrap());
} }
if let Some(content_created) = &self.content_created { if let Some(content_created) = &self.content_created {
map.insert( map.insert(
"xesam:contentCreated", "xesam:contentCreated",
Value::new(content_created.format("%+").to_string()), Value::new(content_created.format("%+").to_string())
.try_into()
.unwrap(),
); );
} }
if let Some(genre) = &self.genre { if let Some(genre) = &self.genre {
map.insert("xesam:genre", Value::new(genre)); map.insert("xesam:genre", Value::new(genre).try_into().unwrap());
} }
if let Some(track_number) = self.track_number { if let Some(track_number) = self.track_number {
map.insert("xesam:trackNumber", Value::new(track_number)); map.insert(
"xesam:trackNumber",
Value::new(track_number).try_into().unwrap(),
);
} }
if let Some(title) = &self.title { if let Some(title) = &self.title {
map.insert("xesam:title", Value::new(title)); map.insert("xesam:title", Value::new(title).try_into().unwrap());
} }
if let Some(user_rating) = self.user_rating { if let Some(user_rating) = self.user_rating {
map.insert("xesam:userRating", Value::new(user_rating)); map.insert(
"xesam:userRating",
Value::new(user_rating).try_into().unwrap(),
);
} }
map map
} }
} }
pub struct Player { pub struct Player(async_channel::Sender<Box<dyn FnOnce(Rc<LocalPlayer>) + Send>>);
playbin: SendWeakRef<crate::Playbin>,
metadata: MetadataMap,
}
impl Player { impl Player {
pub async fn setup( pub async fn setup(
object_server: &zbus::ObjectServer, object_server: &zbus::ObjectServer,
playbin: &crate::Playbin, playbin: &Rc<Playbin>,
) -> Result<(), zbus::Error> { ) -> Result<(), zbus::Error> {
use adw::prelude::*; let local = LocalPlayer {
let player = Self {
playbin: playbin.downgrade().into(),
metadata: MetadataMap::from_playbin_song(None), 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?; object_server.at("/org/mpris/MediaPlayer2", player).await?;
let player_ref = object_server let _player_ref = object_server
.interface::<_, Self>("/org/mpris/MediaPlayer2") .interface::<_, Self>("/org/mpris/MediaPlayer2")
.await?; .await?;
/*
playbin.connect_new_track(glib::clone!( playbin.connect_new_track(glib::clone!(
#[strong] #[strong]
player_ref, player_ref,
@ -216,182 +239,46 @@ impl Player {
} }
), ),
); );
*/
Ok(()) Ok(())
} }
fn playbin(&self) -> zbus::fdo::Result<crate::Playbin> { async fn with_local<T: Send + 'static, F: std::future::Future<Output = T>>(
match self.playbin.upgrade() { &self,
None => Err(zbus::fdo::Error::Failed("playbin was discarded".into())), f: impl FnOnce(Rc<LocalPlayer>) -> F + Send + 'static,
Some(playbin) => Ok(playbin), ) -> 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")] #[zbus::interface(name = "org.mpris.MediaPlayer2.Player")]
impl Player { impl Player {
fn next(&self) -> zbus::fdo::Result<()> { async fn next(&self) -> zbus::fdo::Result<()> {
// If CanGoNext is false, attempting to call this method should have no effect. self.with_local(move |local| async move { local.next() })
if !self.can_go_next()? { .await
return Ok(());
}
let playbin = self.playbin()?;
if playbin.play_queue_position() + 1 > playbin.play_queue_length() {
// 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)
playbin.stop();
} else {
playbin.go_to_next_track();
}
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(());
}
if playbin.play_queue_position() == 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();
} else {
playbin.go_to_prev_track();
}
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.state() != crate::playbin::State::Playing {
return Ok(());
}
playbin.pause();
Ok(())
}
fn play_pause(&self) -> zbus::fdo::Result<()> {
// don't think this is exactly according to spec but it looks more reasonable to me
if self.playbin()?.state() == crate::playbin::State::Paused {
self.play()
} else {
self.pause()
}
}
fn stop(&self) -> zbus::fdo::Result<()> {
let playbin = self.playbin()?;
// If playback is already stopped, this has no effect.
if playbin.state() != crate::playbin::State::Playing {
return Ok(());
}
// Calling Play after this should cause playback to start again from the beginning of the track.
playbin.pause();
playbin.seek(0.0);
Ok(())
}
fn play(&self) -> zbus::fdo::Result<()> {
let playbin = self.playbin()?;
// If CanPlay is false, attempting to call this method should have no effect.
if !self.can_play()? {
return Ok(());
}
// If already playing, this has no effect.
if playbin.state() == crate::playbin::State::Playing {
return Ok(());
}
// If there is no track to play, this has no effect.
if playbin.play_queue_length() == 0 {
return Ok(());
}
playbin.play();
Ok(())
}
fn seek(&self, offset: i64) -> zbus::fdo::Result<()> {
// If the CanSeek property is false, this has no effect.
if !self.can_seek() {
return Ok(());
}
let playbin = self.playbin()?;
// Seeks forward in the current track by the specified number of microseconds.
let mut new_position = (playbin.position() * MICROSECONDS) as i64 + offset;
// A negative value seeks back. If this would mean seeking back further than the start of the track, the position is set to 0.
if new_position < 0 {
new_position = 0;
}
// If the value passed in would mean seeking beyond the end of the track, acts like a call to Next.
if new_position >= (playbin.duration() * MICROSECONDS) as i64 {
return self.next();
}
playbin.seek(new_position as f64 / MICROSECONDS);
Ok(())
}
fn set_position(&self, track_id: ObjectPath<'_>, position: i64) -> zbus::fdo::Result<()> {
let playbin = self.playbin()?;
// If the Position argument is less than 0, do nothing.
if position < 0 {
return Ok(());
}
// If the Position argument is greater than the track length, do nothing.
if position > (playbin.duration() * MICROSECONDS) as i64 {
return Ok(());
}
// If the CanSeek property is false, this has no effect.
if !self.can_seek() {
return Ok(());
}
// check if it's stale
if self.metadata.track_id.as_deref() != Some(&track_id) {
// TODO: warn of stale seek
return Ok(());
}
playbin.seek(position as f64 / MICROSECONDS);
Ok(())
}
fn open_uri(&self, _s: &str) -> zbus::fdo::Result<()> {
Err(zbus::fdo::Error::NotSupported("OpenUri".into()))
}
#[zbus(signal)]
async fn seeked(signal_emitter: &SignalEmitter<'_>, position: i64) -> zbus::Result<()>;
#[zbus(property)] #[zbus(property)]
fn playback_status(&self) -> zbus::fdo::Result<&str> { async fn playback_status(&self) -> zbus::fdo::Result<String> {
match self.playbin()?.state() { self.with_local(|local| async move { local.playback_status() })
crate::playbin::State::Stopped => Ok("Stopped"), .await
crate::playbin::State::Playing => Ok("Playing"),
crate::playbin::State::Paused => Ok("Paused"),
}
} }
#[zbus(property)] #[zbus(property)]
@ -410,10 +297,11 @@ impl Player {
} }
#[zbus(property)] #[zbus(property)]
fn set_rate(&self, rate: f64) -> zbus::Result<()> { 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. // 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 { if rate == 0.0 {
self.pause()?; self.with_local(|local| async move { local.pause() })
.await?;
} }
// just ignore anything else // just ignore anything else
@ -421,42 +309,39 @@ impl Player {
} }
#[zbus(property)] #[zbus(property)]
// FIXME: zbus bug (?): this getter can't be infallible // FIXME: https://github.com/dbus2/zbus/issues/992
fn shuffle(&self) -> zbus::fdo::Result<bool> { fn shuffle(&self) -> zbus::fdo::Result<bool> {
Ok(false) Ok(false)
} }
#[zbus(property)] #[zbus(property)]
// FIXME: zbus bug (?): this setter can't return zbus::fdo::Result // FIXME: see above
fn set_shuffle(&self, _shuffle: bool) -> zbus::Result<()> { fn set_shuffle(&self, _shuffle: bool) -> zbus::Result<()> {
Err(zbus::fdo::Error::NotSupported("setting Shuffle".into()).into()) Err(zbus::fdo::Error::NotSupported("setting Shuffle".into()).into())
} }
#[zbus(property)] #[zbus(property)]
fn metadata(&self) -> HashMap<&'static str, Value> { async fn metadata(&self) -> zbus::fdo::Result<HashMap<&'static str, OwnedValue>> {
self.metadata.as_hash_map() self.with_local(move |local| async move { Ok(local.metadata.as_hash_map()) })
.await
} }
#[zbus(property)] #[zbus(property)]
fn volume(&self) -> zbus::fdo::Result<f64> { async fn volume(&self) -> zbus::fdo::Result<f64> {
Ok(self.playbin()?.volume() as f64 / 100.0) self.with_local(|local| async move { local.volume() }).await
} }
#[zbus(property)] #[zbus(property)]
fn set_volume(&mut self, mut volume: f64) -> zbus::fdo::Result<()> { async fn set_volume(&self, volume: f64) -> zbus::Result<()> {
// When setting, if a negative value is passed, the volume should be set to 0.0. self.with_local(move |local| async move { local.set_volume(volume) })
if volume < 0.0 { .await?;
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 i32);
Ok(()) Ok(())
} }
#[zbus(property(emits_changed_signal = "false"))] #[zbus(property(emits_changed_signal = "false"))]
fn position(&self) -> zbus::fdo::Result<i64> { async fn position(&self) -> zbus::fdo::Result<i64> {
Ok((self.playbin()?.position() * MICROSECONDS) as i64) self.with_local(|local| async move { local.position() })
.await
} }
#[zbus(property)] #[zbus(property)]
@ -470,36 +355,186 @@ impl Player {
} }
#[zbus(property)] #[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<'_>, _position: i64) -> zbus::fdo::Result<()> {
todo!()
}
fn open_uri(&self, _s: &str) -> zbus::fdo::Result<()> {
Err(zbus::fdo::Error::NotSupported("OpenUri".into()))
}
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> { fn can_go_next(&self) -> zbus::fdo::Result<bool> {
// same as can_play // same as can_play
Ok(self.playbin()?.play_queue_length() > 0) Ok(self.playbin()?.entries().len() > 0)
} }
#[zbus(property)]
fn can_go_previous(&self) -> zbus::fdo::Result<bool> { fn can_go_previous(&self) -> zbus::fdo::Result<bool> {
// same as can_play // same as can_play
Ok(self.playbin()?.play_queue_length() > 0) Ok(self.playbin()?.entries().len() > 0)
} }
#[zbus(property)]
fn can_play(&self) -> zbus::fdo::Result<bool> { fn can_play(&self) -> zbus::fdo::Result<bool> {
// it only makes sense to disallow "play" when the play queue is empty // it only makes sense to disallow "play" when the play queue is empty
Ok(self.playbin()?.play_queue_length() > 0) Ok(self.playbin()?.entries().len() > 0)
} }
#[zbus(property)]
fn can_pause(&self) -> bool { fn can_pause(&self) -> bool {
// we don't play anything that can't be paused // we don't play anything that can't be paused
true true
} }
#[zbus(property)]
fn can_seek(&self) -> bool { fn can_seek(&self) -> bool {
// we don't play anything that can't be seeked // we don't play anything that can't be seeked
true true
} }
#[zbus(property(emits_changed_signal = "const"))]
fn can_control(&self) -> bool { fn can_control(&self) -> bool {
true true
} }

View file

@ -1,5 +1,5 @@
use super::{ffi, Error}; use super::{ffi, Error};
use std::ffi::{c_char, c_int, c_void, CString}; use std::ffi::{c_char, c_int, c_void, CStr, CString};
pub trait SetProperty { pub trait SetProperty {
/// # Safety /// # Safety
@ -121,3 +121,18 @@ impl GetProperty for i64 {
Ok(value) Ok(value)
} }
} }
impl GetProperty for String {
unsafe fn get_property(ctx: *mut ffi::mpv_handle, name: *const c_char) -> Result<Self, Error> {
let mut value: *mut c_char = std::ptr::null_mut();
Error::from_return_code(ffi::mpv_get_property(
ctx,
name,
ffi::mpv_format_MPV_FORMAT_STRING,
std::ptr::from_mut::<*mut c_char>(&mut value) as *mut c_void,
))?;
let result = CStr::from_ptr(value).to_string_lossy().into_owned();
ffi::mpv_free(value as *mut c_void);
Ok(result)
}
}

View file

@ -13,58 +13,35 @@ public class Audrey.PlaybinSong : Object {
private static int64 next_counter = 0; private static int64 next_counter = 0;
public int64 counter { get; private set; } public int64 counter { get; private set; }
private Subsonic.Song inner; public string id { get; set; }
public string id { get { return inner.id; } } public string title { get; set; }
public string title { get { return inner.title; } } public string artist { get; set; }
public string artist { get { return inner.artist; } } public string album { get; set; }
public string album { get { return inner.album; } } public string? genre { get; set; }
public string? genre { get { return inner.genre; } } public int64 duration { get; set; }
public int64 duration { get { return inner.duration; } } public int64 track { get; set; }
public int64 track { get { return inner.track; } } public int64 play_count { get; set; }
public int64 play_count { get { return inner.play_count; } }
public string cover_art_url { owned get { return this.api.cover_art_uri (this.id); } } public string cover_art_url { get; set; }
public string stream_url { owned get { return this.api.stream_uri (this.id); } } public string stream_url { get; set; }
public Gdk.Paintable? thumbnail { get; private set; }
private Cancellable cancel_loading_thumbnail;
public PlaybinSong (Subsonic.Client api, Subsonic.Song song) {
this.api = api;
this.inner = song;
construct {
this.counter = next_counter; this.counter = next_counter;
next_counter += 1; next_counter += 1;
} }
private Subsonic.Client api; public PlaybinSong (Subsonic.Client api, Subsonic.Song song) {
this.id = song.id;
this.title = song.title;
this.artist = song.artist;
this.album = song.album;
this.genre = song.genre;
this.duration = song.duration;
this.track = song.track;
this.play_count = song.play_count;
public void need_cover_art () { this.cover_art_url = api.cover_art_uri(this.id);
/* TODO this.stream_url = api.stream_uri(this.id);
if (this.cancel_loading_thumbnail != null) return;
if (this.thumbnail != null) return;
this.cancel_loading_thumbnail = new Cancellable ();
// TODO: dpi scaling maybe?? probably
api.cover_art.begin (this.id, 50, Priority.LOW, this.cancel_loading_thumbnail, (obj, res) => {
try {
var pixbuf = api.cover_art.end (res);
this.thumbnail = Gdk.Texture.for_pixbuf (pixbuf);
} catch (Error e) {
if (!(e is IOError.CANCELLED)) {
warning ("could not fetch cover art for song %s: %s", this.id, e.message);
}
}
this.cancel_loading_thumbnail = null;
});
*/
}
~PlaybinSong () {
if (this.cancel_loading_thumbnail != null) {
this.cancel_loading_thumbnail.cancel ();
}
} }
} }

View file

@ -82,4 +82,19 @@ impl Song {
}; };
url::Url::parse(&url).expect("invalid url from vala side") url::Url::parse(&url).expect("invalid url from vala side")
} }
pub fn from_child(
api: &crate::subsonic::Client,
child: &crate::subsonic::schema::Child,
) -> Self {
glib::Object::builder()
.property("id", &child.id)
.property("title", &child.title)
.property("artist", &child.artist)
.property("album", &child.album)
.property("duration", child.duration as i64)
.property("cover-art-url", api.cover_art_url(&child.id).as_str())
.property("stream-url", api.stream_url(&child.id).as_str())
.build()
}
} }

View file

@ -24,9 +24,9 @@ pub struct Playbin<E> {
paused_changed: SignalEmitter<Self, ()>, paused_changed: SignalEmitter<Self, ()>,
current_entry_changed: SignalEmitter<Self, ()>, current_entry_changed: SignalEmitter<Self, ()>,
entry_inserted: SignalEmitter<Self, u32>, entry_inserted: SignalEmitter<Self, usize>,
stopped: SignalEmitter<Self, ()>, stopped: SignalEmitter<Self, ()>,
entry_removed: SignalEmitter<Self, u32>, entry_removed: SignalEmitter<Self, usize>,
file_started: SignalEmitter<Self, ()>, file_started: SignalEmitter<Self, ()>,
} }
@ -98,7 +98,7 @@ where
todo!() todo!()
} }
pub fn current_entry(&self) -> Option<u32> { pub fn current_entry(&self) -> Option<usize> {
self.mpv self.mpv
.get_property::<i64>("playlist-pos") .get_property::<i64>("playlist-pos")
.unwrap() .unwrap()
@ -118,7 +118,7 @@ where
self.mpv.command(["playlist-prev"]).unwrap(); self.mpv.command(["playlist-prev"]).unwrap();
} }
pub fn play_entry(&self, index: u32) { pub fn play_entry(&self, index: usize) {
self.mpv self.mpv
.command(["playlist-play-index", &index.to_string()]) .command(["playlist-play-index", &index.to_string()])
.unwrap(); .unwrap();
@ -137,10 +137,10 @@ where
entries.push(entry); entries.push(entry);
drop(entries); drop(entries);
self.entry_inserted.emit(self, index as u32); self.entry_inserted.emit(self, index as usize);
} }
pub fn insert_entry(&self, index: u32, entry: E) { pub fn insert_entry(&self, index: usize, entry: E) {
let mut entries = self.entries.borrow_mut(); let mut entries = self.entries.borrow_mut();
self.mpv self.mpv
.command(["loadfile", entry.url().as_str(), "insert-at-play"]) .command(["loadfile", entry.url().as_str(), "insert-at-play"])
@ -161,16 +161,18 @@ where
self.stopped.emit(self, ()); self.stopped.emit(self, ());
} }
pub fn remove_entry(&self, index: u32) { pub fn remove_entry(&self, index: usize) {
let mut entries = self.entries.borrow_mut(); let mut entries = self.entries.borrow_mut();
self.mpv.command(["remove", &index.to_string()]).unwrap(); self.mpv
.command(["playlist-remove", &index.to_string()])
.unwrap();
entries.remove(index as usize); entries.remove(index as usize);
drop(entries); drop(entries);
self.entry_removed.emit(self, index); self.entry_removed.emit(self, index);
} }
pub fn move_entry(&self, _from: u32, _to: u32) { pub fn move_entry(&self, _from: usize, _to: usize) {
todo!() todo!()
} }
@ -188,25 +190,25 @@ where
0 => { 0 => {
assert_eq!(&event.name, "volume"); assert_eq!(&event.name, "volume");
self.volume_changed.emit(self, ()); self.volume_changed.emit(self, ());
println!("new volume! {:?}", self.volume()); dbg!(self.volume());
} }
1 => { 1 => {
assert_eq!(&event.name, "mute"); assert_eq!(&event.name, "mute");
self.muted_changed.emit(self, ()); self.muted_changed.emit(self, ());
println!("new muted! {:?}", self.muted()); dbg!(self.muted());
} }
2 => { 2 => {
assert_eq!(&event.name, "pause"); assert_eq!(&event.name, "pause");
self.paused_changed.emit(self, ()); self.paused_changed.emit(self, ());
println!("new paused! {:?}", self.paused()); dbg!(self.paused());
} }
3 => { 3 => {
assert_eq!(&event.name, "playlist-pos"); assert_eq!(&event.name, "playlist-pos");
self.current_entry_changed.emit(self, ()); self.current_entry_changed.emit(self, ());
println!("new current_entry! {:?}", self.current_entry()); dbg!(self.current_entry());
} }
_ => unreachable!(), _ => unreachable!(),
@ -227,6 +229,12 @@ where
// since we set up the hook before, the current song is guaranteed not to change // since we set up the hook before, the current song is guaranteed not to change
// under our feet // under our feet
self.file_started.emit(self, ()); self.file_started.emit(self, ());
// sanity check
assert_eq!(
self.entries()[self.current_entry().unwrap()].url().as_str(),
&self.mpv.get_property::<String>("path").unwrap()
);
} }
_ => println!("mpv event {:?}", event), _ => println!("mpv event {:?}", event),
@ -241,6 +249,22 @@ where
self.muted_changed.signal() self.muted_changed.signal()
} }
pub fn current_entry_changed(&self) -> Signal<'_, Self, ()> {
self.current_entry_changed.signal()
}
pub fn entry_inserted(&self) -> Signal<'_, Self, usize> {
self.entry_inserted.signal()
}
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, ()> { pub fn file_started(&self) -> Signal<'_, Self, ()> {
self.file_started.signal() self.file_started.signal()
} }

View file

@ -25,7 +25,7 @@ pub struct Signal<'a, E, T> {
just_connected: &'a RefCell<Vec<SignalHandlerBox<E, T>>>, just_connected: &'a RefCell<Vec<SignalHandlerBox<E, T>>>,
} }
#[derive(Clone)] #[derive(Clone, Default)]
pub struct SignalHandler(Weak<Cell<bool>>); pub struct SignalHandler(Weak<Cell<bool>>);
impl SignalHandler { impl SignalHandler {
@ -101,28 +101,16 @@ impl<E, T> SignalEmitter<E, T> {
.expect("tried to re-emit signal during emission"); .expect("tried to re-emit signal during emission");
handlers.append(self.just_connected.borrow_mut().as_mut()); handlers.append(self.just_connected.borrow_mut().as_mut());
if handlers.is_empty() {
return;
}
let mut i = 0; let mut i = 0;
let mut skip = 0; let mut skip = 0;
loop { // FIXME: does not preserve ordering
if handlers[i + skip](emitter, f()) { while i < handlers.len() {
if handlers[i](emitter, f()) {
i += 1; i += 1;
} else { } else {
skip += 1; drop(handlers.swap_remove(i));
} }
if i + skip == handlers.len() {
break;
}
handlers.swap(i, i + skip);
} }
println!("emitted to {i} listeners");
handlers.truncate(i);
} }
} }

View file

@ -176,6 +176,10 @@ impl Client {
.map(|response| response.random_songs.song) .map(|response| response.random_songs.song)
} }
pub fn cover_art_url(&self, id: &str) -> url::Url {
self.url(&["rest", "coverArt"], &[("id", id)])
}
pub fn stream_url(&self, id: &str) -> url::Url { pub fn stream_url(&self, id: &str) -> url::Url {
self.url(&["rest", "stream"], &[("id", id)]) self.url(&["rest", "stream"], &[("id", id)])
} }

View file

@ -2,15 +2,22 @@ pub mod song;
pub use song::Song; pub use song::Song;
mod imp { mod imp {
use adw::{glib, prelude::*, subclass::prelude::*}; use crate::playbin::Song as PlaybinSong;
use adw::{gio, glib, prelude::*, subclass::prelude::*};
use glib::{subclass::InitializingObject, WeakRef}; use glib::{subclass::InitializingObject, WeakRef};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
type Playbin = crate::playbin2::Playbin<PlaybinSong>;
#[derive(gtk::CompositeTemplate, glib::Properties, Default)] #[derive(gtk::CompositeTemplate, glib::Properties, Default)]
#[template(resource = "/eu/callcc/audrey/play_queue.ui")] #[template(resource = "/eu/callcc/audrey/play_queue.ui")]
#[properties(wrapper_type = super::PlayQueue)] #[properties(wrapper_type = super::PlayQueue)]
pub struct PlayQueue { pub struct PlayQueue {
#[property(get, set)] #[property(get, set)]
playbin: WeakRef<crate::Playbin>, pub(super) model: RefCell<Option<gio::ListStore>>,
pub(super) playbin: RefCell<Option<Rc<Playbin>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -30,7 +37,13 @@ mod imp {
} }
#[glib::derived_properties] #[glib::derived_properties]
impl ObjectImpl for PlayQueue {} impl ObjectImpl for PlayQueue {
fn constructed(&self) {
self.parent_constructed();
self.obj().set_model(gio::ListStore::new::<PlaybinSong>());
}
}
impl WidgetImpl for PlayQueue {} impl WidgetImpl for PlayQueue {}
@ -49,7 +62,7 @@ mod imp {
#[template_callback] #[template_callback]
fn on_song_list_setup(&self, item: &gtk::ListItem, _factory: &gtk::SignalListItemFactory) { fn on_song_list_setup(&self, item: &gtk::ListItem, _factory: &gtk::SignalListItemFactory) {
let child = super::Song::new(&self.playbin.upgrade().unwrap()); let child = super::Song::new(self.playbin.borrow().as_ref().unwrap());
child.set_draggable(true); child.set_draggable(true);
child.set_show_position(true); child.set_show_position(true);
@ -81,7 +94,11 @@ mod imp {
#[template_callback] #[template_callback]
fn on_row_activated(&self, position: u32) { fn on_row_activated(&self, position: u32) {
self.obj().playbin().unwrap().select_track(position); self.playbin
.borrow()
.as_ref()
.unwrap()
.play_entry(position as usize);
} }
} }
@ -92,10 +109,47 @@ mod imp {
} }
} }
use crate::playbin::Song as PlaybinSong;
use adw::subclass::prelude::*;
use gtk::glib; use gtk::glib;
use std::rc::Rc;
type Playbin = crate::playbin2::Playbin<PlaybinSong>;
glib::wrapper! { glib::wrapper! {
pub struct PlayQueue(ObjectSubclass<imp::PlayQueue>) pub struct PlayQueue(ObjectSubclass<imp::PlayQueue>)
@extends adw::Bin, gtk::Widget, @extends adw::Bin, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
} }
impl PlayQueue {
pub fn set_playbin(&self, playbin: &Rc<Playbin>) {
assert!(self
.imp()
.playbin
.replace(Some(Rc::clone(playbin)))
.is_none()); // only set once
playbin
.entry_inserted()
.connect_object(self, |playbin, play_queue, index| {
play_queue
.model()
.unwrap()
.insert(index as u32, &playbin.entries()[index]);
true
});
playbin
.stopped()
.connect_object(self, |playbin, play_queue, ()| {
play_queue.model().unwrap().remove_all();
true
});
playbin
.entry_removed()
.connect_object(self, |playbin, play_queue, index| {
play_queue.model().unwrap().remove(index as u32);
true
});
}
}

View file

@ -1,14 +1,17 @@
mod imp { mod imp {
use crate::playbin::Song as PlaybinSong;
use crate::signal::SignalHandler;
use glib::{subclass::InitializingObject, WeakRef}; use glib::{subclass::InitializingObject, WeakRef};
use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*}; use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*};
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::rc::Rc;
type Playbin = crate::playbin2::Playbin<PlaybinSong>;
#[derive(gtk::CompositeTemplate, glib::Properties, Default)] #[derive(gtk::CompositeTemplate, glib::Properties, Default)]
#[template(resource = "/eu/callcc/audrey/play_queue_song.ui")] #[template(resource = "/eu/callcc/audrey/play_queue_song.ui")]
#[properties(wrapper_type = super::Song)] #[properties(wrapper_type = super::Song)]
pub struct Song { pub struct Song {
pub(super) playbin: WeakRef<crate::Playbin>,
#[property(set, get)] #[property(set, get)]
draggable: Cell<bool>, draggable: Cell<bool>,
#[property(set, get)] #[property(set, get)]
@ -24,12 +27,13 @@ mod imp {
#[property(set, get)] #[property(set, get)]
displayed_position: Cell<u32>, displayed_position: Cell<u32>,
#[property(get, set)] #[property(get, set)]
song: RefCell<Option<crate::playbin::Song>>, song: RefCell<Option<PlaybinSong>>,
pub(super) connection: Cell<Option<glib::SignalHandlerId>>,
drag_pos: Cell<(i32, i32)>, drag_pos: Cell<(i32, i32)>,
drag_widget: Cell<Option<gtk::ListBox>>, drag_widget: Cell<Option<gtk::ListBox>>,
pub(super) playbin: RefCell<Option<Rc<Playbin>>>,
pub(super) connection: Cell<SignalHandler>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -61,7 +65,7 @@ mod imp {
self_ self_
.obj() .obj()
.playbin() .playbin()
.remove_track(self_.obj().displayed_position() - 1) .remove_entry(self_.obj().displayed_position() as usize - 1)
} }
)) ))
.build(); .build();
@ -161,18 +165,16 @@ mod imp {
false false
} }
} }
impl Drop for Song {
fn drop(&mut self) {
println!("dropping AudreyUiPlayQueueSong");
}
}
} }
use crate::playbin::Song as PlaybinSong;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::Object; use glib::Object;
use gtk::glib; use gtk::glib;
use std::rc::Rc;
type Playbin = crate::playbin2::Playbin<PlaybinSong>;
glib::wrapper! { glib::wrapper! {
pub struct Song(ObjectSubclass<imp::Song>) pub struct Song(ObjectSubclass<imp::Song>)
@ -181,36 +183,38 @@ glib::wrapper! {
} }
impl Song { impl Song {
pub fn new(playbin: &crate::Playbin) -> Self { pub fn new(playbin: &Rc<Playbin>) -> Self {
let song: Self = Object::new(); let song: Self = Object::new();
song.imp().playbin.set(Some(playbin));
assert!(song
.imp()
.playbin
.replace(Some(Rc::clone(playbin)))
.is_none()); // only set once
song song
} }
fn playbin(&self) -> crate::Playbin { fn playbin(&self) -> Rc<Playbin> {
self.imp().playbin.upgrade().unwrap() Rc::clone(self.imp().playbin.borrow().as_ref().unwrap())
} }
pub fn bind(&self, position: u32, song: &crate::playbin::Song) { pub fn bind(&self, position: u32, song: &crate::playbin::Song) {
self.set_displayed_position(position + 1); self.set_displayed_position(position + 1);
self.set_song(song); self.set_song(song);
self.set_current(self.playbin().play_queue_position() == position); self.set_current(self.playbin().current_entry() == Some(position as usize));
self.imp() self.imp()
.connection .connection
.replace(Some(self.playbin().connect_notify_local( .replace(self.playbin().current_entry_changed().connect_object(
Some("play-queue-position"), self,
glib::clone!( move |playbin, song, ()| {
#[weak(rename_to = self_)] song.set_current(playbin.current_entry() == Some(position as usize));
self, true
move |playbin: &crate::Playbin, _| { },
self_.set_current(playbin.play_queue_position() == position) ));
}
),
)));
} }
pub fn unbind(&self) { pub fn unbind(&self) {
self.playbin() self.imp().connection.take().disconnect();
.disconnect(self.imp().connection.take().unwrap());
} }
} }

View file

@ -6,6 +6,12 @@ mod imp {
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::rc::Rc; use std::rc::Rc;
impl crate::playbin2::PlaybinEntry for crate::playbin::Song {
fn url(&self) -> url::Url {
self.stream_url()
}
}
#[derive(gtk::CompositeTemplate, glib::Properties, Default)] #[derive(gtk::CompositeTemplate, glib::Properties, Default)]
#[template(resource = "/eu/callcc/audrey/window.ui")] #[template(resource = "/eu/callcc/audrey/window.ui")]
#[properties(wrapper_type = super::Window)] #[properties(wrapper_type = super::Window)]
@ -13,6 +19,9 @@ mod imp {
#[template_child] #[template_child]
pub(super) playbar: TemplateChild<crate::ui::Playbar>, pub(super) playbar: TemplateChild<crate::ui::Playbar>,
#[template_child]
pub(super) play_queue: TemplateChild<crate::ui::PlayQueue>,
#[property(get, set)] #[property(get, set)]
playbin: RefCell<crate::Playbin>, playbin: RefCell<crate::Playbin>,
@ -27,7 +36,7 @@ mod imp {
pub(super) setup: crate::ui::Setup, pub(super) setup: crate::ui::Setup,
pub(super) playbin2: Rc<crate::playbin2::Playbin<url::Url>>, pub(super) playbin2: Rc<crate::playbin2::Playbin<crate::playbin::Song>>,
pub(super) api2: RefCell<Option<Rc<crate::subsonic::Client>>>, pub(super) api2: RefCell<Option<Rc<crate::subsonic::Client>>>,
} }
@ -66,6 +75,8 @@ mod imp {
} }
})); }));
self.play_queue.set_playbin(&self.playbin2);
// set up mpris // set up mpris
let window = self.obj().clone(); let window = self.obj().clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
@ -90,7 +101,7 @@ mod imp {
crate::Mpris::setup(conn.object_server(), &window) crate::Mpris::setup(conn.object_server(), &window)
.await .await
.expect("could not serve mpris"); .expect("could not serve mpris");
crate::mpris::Player::setup(conn.object_server(), &window.playbin()) crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin2)
.await .await
.expect("could not serve mpris player"); .expect("could not serve mpris player");
@ -126,8 +137,8 @@ mod imp {
let api = self.api2.borrow(); let api = self.api2.borrow();
let api = api.as_ref().unwrap(); let api = api.as_ref().unwrap();
for song in api.get_random_songs(10).await.unwrap().into_iter() { for song in api.get_random_songs(10).await.unwrap().into_iter() {
println!("{song:?}"); self.playbin2
self.playbin2.push_entry(api.stream_url(&song.id)); .push_entry(crate::playbin::Song::from_child(api, &song));
} }
self.obj().set_can_click_shuffle_all(true); self.obj().set_can_click_shuffle_all(true);
} }
@ -189,7 +200,10 @@ impl Window {
let window: Self = glib::Object::builder().property("application", app).build(); let window: Self = glib::Object::builder().property("application", app).build();
// manual bidirectional sync // manual bidirectional sync
window.imp().playbar.set_volume(window.imp().playbin2.volume() as i32); window
.imp()
.playbar
.set_volume(window.imp().playbin2.volume() as i32);
window.imp().playbin2.volume_changed().connect_object( window.imp().playbin2.volume_changed().connect_object(
&*window.imp().playbar, &*window.imp().playbar,
|playbin, playbar, ()| { |playbin, playbar, ()| {