Event handling refactor and Rich Presence implementation

This commit is contained in:
Ryze 2023-02-06 03:22:32 +03:00
parent b04ce61aba
commit d349e2ac16
9 changed files with 513 additions and 149 deletions

View file

@ -9,6 +9,7 @@ crate-type = ["cdylib"]
[dependencies]
mpv-client = "0.4.1"
discord-rich-presence="0.2.3"
[profile.release-full]
inherits = "release"

View file

@ -1,66 +0,0 @@
use std::rc::Rc;
use crate::mpv_event_handler::events::{MpvEvent, Listener, FileInfo};
use crate::logging::{self, Logger};
pub struct MpvListener {
logger: Rc<Logger>
}
impl MpvListener {
pub fn new(logger: Rc<Logger>) -> Self {
Self {
logger
}
}
fn print_file_info(&self, file_info: FileInfo) -> Result<(), &'static str> {
let FileInfo {filename, metadata} = file_info;
logging::info!(self.logger, "FILENAME {}", filename);
if let Some(artist) = metadata.artist {
logging::info!(self.logger, "ARTIST: {artist}");
}
if let Some(album) = metadata.album {
logging::info!(self.logger, "ALBUM: {album}");
}
if let Some(title) = metadata.title {
logging::info!(self.logger, "TITLE: {title}");
}
if let Some(track) = metadata.track {
logging::info!(self.logger, "TRACK: {track}");
}
Ok(())
}
fn print_seek_time(&self, time: i64) -> Result<(), &'static str>{
logging::info!(self.logger, "SEEKING: {time}");
Ok(())
}
fn print_play(&self) -> Result<(), &'static str> {
logging::info!(self.logger, "PLAY");
Ok(())
}
fn print_pause(&self) -> Result<(), &'static str> {
logging::info!(self.logger, "PAUSE");
Ok(())
}
}
impl Listener for MpvListener {
fn handle_event(&self, event: MpvEvent) -> Result<(), &'static str>{
match event {
MpvEvent::FileLoaded(file_info) => self.print_file_info(file_info),
MpvEvent::Seek(time) => self.print_seek_time(time),
MpvEvent::Pause => self.print_pause(),
MpvEvent::Play => self.print_play(),
}
}
}

View file

@ -0,0 +1,181 @@
use std::rc::Rc;
use std::time::SystemTime;
use crate::logging::{self, Logger};
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent, FileInfo};
use discord_rich_presence::{DiscordIpcClient, DiscordIpc};
use discord_rich_presence::activity::{Activity, Assets, Timestamps};
struct ActivityInfo {
details: String,
state: String,
timestamps: Timestamps
}
impl ActivityInfo {
pub fn new(details: String, state: String, timestamps: Timestamps) -> Self {
Self {
details,
state,
timestamps
}
}
pub fn empty() -> Self {
Self {
details: String::new(),
state: String::new(),
timestamps: Timestamps::new()
}
}
pub fn get_activity(&self) -> Activity {
let assets = Assets::new()
.large_image("logo")
.large_text("mpv");
Activity::new()
.assets(assets)
.details(&self.details)
.state(&self.state)
.timestamps(self.timestamps.clone())
}
}
pub struct DiscordClient {
logger: Rc<Logger>,
discord: DiscordIpcClient,
activity_info: ActivityInfo,
active: bool
}
impl DiscordClient {
pub fn new(client_id: &str, logger: Rc<Logger>) -> Result<Self, &'static str> {
let discord = match DiscordIpcClient::new(client_id) {
Ok(discord) => discord,
Err(_) => return Err("cannot init discord client")
};
Ok(Self {
logger,
discord,
activity_info: ActivityInfo::empty(),
active: false
})
}
fn get_state(file_info: &FileInfo) -> String {
let metadata = &file_info.metadata;
let mut state = String::new();
if let Some(artist) = &metadata.artist {
state += &format!("by {artist}");
}
if let Some(album) = &metadata.album {
state += &format!(" on {album}");
}
state
}
fn get_details(file_info: &FileInfo) -> String {
let metadata = &file_info.metadata;
let title = match &metadata.title {
Some(title) => title,
None => return file_info.filename.clone()
};
if let Some(track) = &metadata.track {
format!("{title} [T{track}] ")
}
else {
title.clone()
}
}
fn update_presence(&mut self) -> Result<(), &'static str> {
if !self.active {
return Ok(())
}
logging::info!(self.logger, "Updating rich presence");
match self.discord.set_activity(self.activity_info.get_activity()) {
Ok(()) => {
Ok(())
}
Err(_) => Err("cannot set presence")
}
}
fn set_presence(&mut self, file_info: FileInfo) -> Result<(), &'static str> {
let details = DiscordClient::get_details(&file_info);
let state = DiscordClient::get_state(&file_info);
self.activity_info = ActivityInfo::new(details, state, Timestamps::new());
self.update_presence()
}
fn update_timestamps(&mut self, remaining_time: i64) -> Result<(), &'static str> {
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH);
let current_time = match current_time {
Ok(time) => time.as_secs() as i64,
Err(_) => return Err("cannot get current system time")
};
let predicted_time = current_time + remaining_time;
self.activity_info.timestamps = Timestamps::new().end(predicted_time);
self.update_presence()
}
fn open(&mut self) -> Result<(), &'static str> {
if self.active {
return Ok(());
}
logging::info!(self.logger, "Opening discord client");
match self.discord.connect() {
Ok(()) => {
self.active = true;
self.update_presence()
}
Err(_) => Err("cannot connect to Discord")
}
}
fn close(&mut self) -> Result<(), &'static str> {
if !self.active {
return Ok(());
}
logging::info!(self.logger, "Closing discord client");
match self.discord.close() {
Ok(()) => {
self.active = false;
Ok(())
}
Err(_) => Err("cannot disconnect from Discord")
}
}
fn toggle_activity(&mut self) -> Result<(), &'static str> {
match self.active {
false => self.open(),
true => self.close()
}
}
}
impl MpvEventHandler for DiscordClient {
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str> {
match event {
MpvEvent::FileLoaded(file_info) => self.set_presence(file_info),
MpvEvent::Seek(remaining_time) => self.update_timestamps(remaining_time),
MpvEvent::Toggle => self.toggle_activity(),
MpvEvent::Exit => self.close(),
_ => Ok(())
}
}
}

View file

@ -1,25 +1,21 @@
use std::rc::Rc;
use mpv_client::mpv_handle;
mod mpv_event_handler;
mod logging;
mod basic_listener; // For testing purposes
mod mpv_event_queue;
mod discord_client;
mod plugin;
use plugin::RPCPlugin;
const DISCORD_APPID: &str = "1071519995588264016";
#[no_mangle]
fn mpv_open_cplugin(handle: *mut mpv_handle) -> std::os::raw::c_int {
let logger = Rc::new(logging::Logger::from_env());
let listener = Box::new(basic_listener::MpvListener::new(Rc::clone(&logger)));
let result = mpv_event_handler::MpvEventHandler::from_ptr(handle, listener, Rc::clone(&logger));
let client = match result {
Ok(v) => v,
Err(e) => {
logging::error!(logger, "Error initializing event_handling: {e}");
return -1;
}
let plugin = match RPCPlugin::new(handle, DISCORD_APPID) {
Ok(plugin) => plugin,
Err(_) => return -1
};
client.run();
plugin.run();
return 0;
}

View file

@ -1,7 +1,6 @@
use std::rc::Rc;
use mpv_client::{Handle, Event, Property, Format, mpv_handle};
use crate::logging::{self, Logger};
use std::{rc::Rc, cell::{RefCell, Cell}, borrow::Borrow};
use crate::{logging::{self, Logger}, info};
use mpv_client::{Handle, Event, Property, Format, mpv_handle, ClientMessage};
pub mod events;
use events::{MpvEvent, Listener, FileInfo, FileMetadata};
@ -10,6 +9,7 @@ const NAME_PAUSE_PROP: &str = "pause";
const REPL_PAUSE_PROP: u64 = 1;
pub struct MpvEventHandler {
listening: bool,
mpv: Handle,
listener: Box<dyn Listener>,
logger: Rc<Logger>
@ -17,7 +17,11 @@ pub struct MpvEventHandler {
impl MpvEventHandler {
pub fn new(mpv: Handle, listener: Box<dyn Listener>, logger: Rc<Logger>) -> Result<Self, String> {
let mpv = Cell::new(mpv);
let listener = Cell::new(listener);
let new_self = Self {
listening: false,
mpv,
listener,
logger
@ -31,8 +35,21 @@ impl MpvEventHandler {
MpvEventHandler::new(Handle::from_ptr(handle), listener, logger)
}
pub fn run(self) {
logging::info!(self.logger, "Starting mpv-rpc");
logging::info!(self.logger, "Client name: {}", self.mpv.borrow().as_ptr().cl);
while self.poll_events() {
// Wait for shutdown
}
if self.listening {
self.listener.get_mut().close();
}
}
fn initialize(&self) -> Result<(), String> {
self.observe_property(REPL_PAUSE_PROP, NAME_PAUSE_PROP, bool::MPV_FORMAT)
self.observe_property(REPL_PAUSE_PROP, NAME_PAUSE_PROP, bool::MPV_FORMAT)
}
fn observe_property(&self, id: u64, name: &str, format: i32) -> Result<(), String>{
@ -41,22 +58,58 @@ impl MpvEventHandler {
Err(_) => Err(format!("Couldn't observe property: {name} (id: {id})"))
}
}
fn get_event(&self) -> Event {
self.mpv.wait_event(0.0)
}
fn handle_event(&self, event: Event) -> Result<(), &'static str>{
fn poll_events(&self) -> bool {
let event = self.mpv.wait_event(0.0);
let result = match event {
Event::None => Ok(()),
Event::Shutdown => return false,
event => self.handle_event(event)
};
if let Err(e) = result {
logging::error!(self.logger, "Error handling event: {e}");
}
true
}
fn handle_event(&self, event: Event) -> Result<(), &'static str> {
logging::info!(self.logger, "Event: {event}");
if let Event::ClientMessage(message) = event {
return self.handle_client_message(message);
}
if self.listening {
match event {
Event::PropertyChange(prop_id, prop) => self.send_property_to_listener(prop_id, prop),
e => self.send_event_to_listener(e)
}
}
else {
Ok(())
}
}
fn send_event_to_listener(&self, event: Event) -> Result<(), &'static str>{
match event {
Event::FileLoaded => self.handle_file_loaded(),
Event::PlaybackRestart => self.handle_playback_restart(),
Event::FileLoaded => self.send_file_info(),
Event::PlaybackRestart => self.send_seek(),
_ => Ok(())
}
}
fn on_property_change(&self, prop_id: u64, prop: Property) -> Result<(), &'static str> {
fn send_property_to_listener(&self, prop_id: u64, prop: Property) -> Result<(), &'static str> {
logging::info!(self.logger, "Property changed: {prop_id}");
match prop_id {
REPL_PAUSE_PROP => {
match prop.data() {
Some(pause) => self.handle_pause_change(pause),
Some(pause) => self.send_pause(pause),
None => Err("property pause doesn't exist")
}
}
@ -64,12 +117,12 @@ impl MpvEventHandler {
}
}
fn handle_file_loaded(&self) -> Result<(), &'static str> {
fn send_file_info(&self) -> Result<(), &'static str> {
let filename_res = self.mpv.get_property("filename");
let artist = self.mpv.get_property("metadata/by-key/artist").ok();
let album = self.mpv.get_property("metadata/by-key/album").ok();
let title = self.mpv.get_property("metadata/by-key/title").ok();
let track = self.mpv.get_property("metadata/by-key/Artist").ok();
let track = self.mpv.get_property("metadata/by-key/track").ok();
let filename = match filename_res {
Ok(name) => name,
@ -89,53 +142,55 @@ impl MpvEventHandler {
};
let event = MpvEvent::FileLoaded(file_info);
self.listener.handle_event(event)
self.listener.get_mut().handle_event(event)
}
fn handle_playback_restart(&self) -> Result<(), &'static str>{
let res = self.mpv.get_property("playback-time").ok();
match res {
Some(time) => {
let event = MpvEvent::Seek(time);
self.listener.handle_event(event)
}
None => {
logging::warning!(self.logger, "Failed retrieving playback-time.");
logging::warning!(self.logger, "This usually happens seeking into file end. Possibly mpv bug?");
Ok(())
},
}
fn send_seek(&self) -> Result<(), &'static str>{
let remaining_time = self.mpv.get_property("time-remaining").unwrap_or_else(|_| {
logging::warning!(self.logger, "Failed retrieving remaing-time.");
logging::warning!(self.logger, "This usually happens seeking into file end. Possibly mpv bug?");
logging::warning!(self.logger, "Defaulting to 0.");
0
});
let event = MpvEvent::Seek(remaining_time);
self.listener.get_mut().handle_event(event)
}
fn handle_pause_change(&self, pause: bool) -> Result<(), &'static str> {
fn send_pause(&self, pause: bool) -> Result<(), &'static str> {
let event = match pause {
false => MpvEvent::Play,
true => MpvEvent::Pause
};
self.listener.handle_event(event)
self.listener.get_mut().handle_event(event)
}
fn toggle_listening(&self) -> Result<(), &'static str> {
let new_state = !self.listening;
let mut listener_ref = self.listener.get_mut();
pub fn run(&self) {
while self.poll_events() {
// Wait for shutdown
}
}
// Refactor to accept warnings as well
pub fn poll_events(&self) -> bool {
let event = self.mpv.wait_event(0.0);
let event_name = event.to_string();
let result = match event {
Event::None => Ok(()),
Event::Shutdown => return false,
Event::PropertyChange(prop_id, prop) => self.on_property_change(prop_id, prop),
e => self.handle_event(e)
let res = match new_state {
true => listener_ref.open(),
false => listener_ref.close()
};
if let Err(e) = result {
logging::error!(self.logger, "Error handling event {event_name}: {e}");
match res {
Ok(()) => self.listening = true,
Err(e) => return Err(e)
}
Ok(())
}
fn handle_client_message(&self, message: ClientMessage) -> Result<(), &'static str> {
let command = message.args().join(" ");
logging::info!(self.logger, "Client message: {command}");
if command.starts_with("key-binding toggle-rpc d-") {
self.toggle_listening()
}
else {
Ok(())
}
true
}
}

View file

@ -1,22 +0,0 @@
pub struct FileInfo {
pub filename: String,
pub metadata: FileMetadata
}
pub struct FileMetadata {
pub artist: Option<String>,
pub album: Option<String>,
pub title: Option<String>,
pub track: Option<String>
}
pub enum MpvEvent {
FileLoaded(FileInfo),
Seek(i64),
Pause,
Play
}
pub trait Listener {
fn handle_event(&self, event: MpvEvent) -> Result<(), &'static str>;
}

124
src/mpv_event_queue.rs Normal file
View file

@ -0,0 +1,124 @@
use std::rc::Rc;
use mpv_client::{Handle, Event, Property, Format, mpv_handle, ClientMessage};
use crate::logging::{self, Logger};
pub mod events;
use events::{MpvEvent, FileInfo, FileMetadata};
const NAME_PAUSE_PROP: &str = "pause";
const REPL_PAUSE_PROP: u64 = 1;
pub struct MpvEventQueue {
mpv: Handle,
logger: Rc<Logger>,
}
impl MpvEventQueue {
pub fn new(mpv: Handle,logger: Rc<Logger>) -> Result<Self, &'static str> {
let new_self = Self {
mpv,
logger,
};
new_self.initialize()?;
Ok(new_self)
}
pub fn from_ptr<'a>(handle: *mut mpv_handle, logger: Rc<Logger>) -> Result<Self, &'static str> {
MpvEventQueue::new(Handle::from_ptr(handle), logger)
}
fn initialize(&self) -> Result<(), &'static str> {
self.observe_property(REPL_PAUSE_PROP, NAME_PAUSE_PROP, bool::MPV_FORMAT)
}
fn observe_property(&self, id: u64, name: &str, format: i32) -> Result<(), &'static str> {
match self.mpv.observe_property(id, name, format) {
Ok(_) => Ok(()),
Err(_) => Err("cannot observe property")
}
}
pub fn next_event(&self) -> Option<MpvEvent> {
let event = self.mpv.wait_event(0.0);
let mpv_event = self.convert_event(event);
if let Some(ref event) = mpv_event {
logging::info!(self.logger, "Event: {event}");
}
mpv_event
}
fn convert_event(&self, event: Event) -> Option<MpvEvent> {
match event {
Event::FileLoaded => self.get_file_info_event(),
Event::PlaybackRestart => self.get_seek_event(),
Event::ClientMessage(message) => self.get_toggle_event(message),
Event::PropertyChange(prop_id, prop) => self.get_property_event(prop_id, prop),
Event::Shutdown => Some(MpvEvent::Exit),
_ => None
}
}
fn get_file_info_event(&self) -> Option<MpvEvent> {
let filename = self.mpv.get_property("filename").unwrap();
let artist = self.mpv.get_property("metadata/by-key/artist").ok();
let album = self.mpv.get_property("metadata/by-key/album").ok();
let title = self.mpv.get_property("metadata/by-key/title").ok();
let track = self.mpv.get_property("metadata/by-key/track").ok();
let metadata = FileMetadata {
artist,
album,
title,
track
};
let file_info = FileInfo {
filename,
metadata
};
Some(MpvEvent::FileLoaded(file_info))
}
fn get_property_event(&self, prop_id: u64, prop: Property) -> Option<MpvEvent> {
logging::info!(self.logger, "Property changed: {prop_id}");
match prop_id {
1 => Some(self.convert_pause_prop(prop.data().unwrap())),
_ => None
}
}
fn convert_pause_prop(&self, pause: bool) -> MpvEvent {
match pause {
false => MpvEvent::Play,
true => MpvEvent::Pause
}
}
fn get_seek_event(&self) -> Option<MpvEvent> {
let remaining_time = self.mpv.get_property("time-remaining").unwrap_or_else(|_| {
logging::warning!(self.logger, "Failed retrieving remaing-time.");
logging::warning!(self.logger, "This usually happens seeking into file end. Possibly mpv bug?");
logging::warning!(self.logger, "Defaulting to 0.");
0
});
Some(MpvEvent::Seek(remaining_time))
}
fn get_toggle_event(&self, message: ClientMessage) -> Option<MpvEvent> {
let command = message.args().join(" ");
logging::info!(self.logger, "Client message: {command}");
if command.starts_with("key-binding toggle-rpc d-") {
return Some(MpvEvent::Toggle)
}
else {
None
}
}
}

View file

@ -0,0 +1,41 @@
use std::fmt::Display;
pub struct FileInfo {
pub filename: String,
pub metadata: FileMetadata
}
pub struct FileMetadata {
pub artist: Option<String>,
pub album: Option<String>,
pub title: Option<String>,
pub track: Option<String>
}
pub enum MpvEvent {
Toggle,
Play,
Pause,
Exit,
FileLoaded(FileInfo),
Seek(i64)
}
impl Display for MpvEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let event_name = match self {
MpvEvent::Toggle => "Toggle",
MpvEvent::Play => "Play",
MpvEvent::Pause => "Pause",
MpvEvent::Exit => "Exit",
MpvEvent::FileLoaded(_) => "FileLoaded",
MpvEvent::Seek(_) => "Seek"
};
write!(f, "{}", event_name)
}
}
pub trait MpvEventHandler {
fn handle_event(&mut self, event: MpvEvent) -> Result<(), &'static str>;
}

54
src/plugin.rs Normal file
View file

@ -0,0 +1,54 @@
use std::rc::Rc;
use mpv_client::mpv_handle;
use crate::logging::{self, Logger};
use crate::discord_client::DiscordClient;
use crate::mpv_event_queue::MpvEventQueue;
use crate::mpv_event_queue::events::{MpvEventHandler, MpvEvent};
pub struct RPCPlugin {
logger: Rc<Logger>,
mpv: MpvEventQueue,
discord: DiscordClient,
}
impl RPCPlugin {
pub fn new(handle: *mut mpv_handle, client_id: &str) -> Result<Self, &'static str> {
let logger = Rc::new(Logger::from_env());
let mpv = MpvEventQueue::from_ptr(handle, Rc::clone(&logger))?;
let discord = DiscordClient::new(client_id, Rc::clone(&logger))?;
Ok(Self {
logger,
mpv,
discord
})
}
pub fn run(mut self) {
loop {
let event = self.mpv.next_event();
match event {
None => continue,
Some(event) => {
if self.handle_event(event)
{
break;
}
}
}
}
}
fn handle_event(&mut self, event: MpvEvent) -> bool {
let exit = match event {
MpvEvent::Exit => true,
_ => false
};
if let Err(e) = self.discord.handle_event(event) {
logging::error!(self.logger, "Failed to handle event: {e}");
}
exit
}
}