From d349e2ac16ec92f2f125217f69f9a335b13ff723 Mon Sep 17 00:00:00 2001 From: Ryze Date: Mon, 6 Feb 2023 03:22:32 +0300 Subject: [PATCH] Event handling refactor and Rich Presence implementation --- Cargo.toml | 1 + src/basic_listener.rs | 66 ------------ src/discord_client.rs | 181 ++++++++++++++++++++++++++++++++ src/lib.rs | 26 ++--- src/mpv_event_handler.rs | 147 ++++++++++++++++++-------- src/mpv_event_handler/events.rs | 22 ---- src/mpv_event_queue.rs | 124 ++++++++++++++++++++++ src/mpv_event_queue/events.rs | 41 ++++++++ src/plugin.rs | 54 ++++++++++ 9 files changed, 513 insertions(+), 149 deletions(-) delete mode 100644 src/basic_listener.rs delete mode 100644 src/mpv_event_handler/events.rs create mode 100644 src/mpv_event_queue.rs create mode 100644 src/mpv_event_queue/events.rs create mode 100644 src/plugin.rs diff --git a/Cargo.toml b/Cargo.toml index db230f1..a221056 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib"] [dependencies] mpv-client = "0.4.1" +discord-rich-presence="0.2.3" [profile.release-full] inherits = "release" diff --git a/src/basic_listener.rs b/src/basic_listener.rs deleted file mode 100644 index 34404a1..0000000 --- a/src/basic_listener.rs +++ /dev/null @@ -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 -} - -impl MpvListener { - pub fn new(logger: Rc) -> 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(), - } - } -} \ No newline at end of file diff --git a/src/discord_client.rs b/src/discord_client.rs index e69de29..6db7d2a 100644 --- a/src/discord_client.rs +++ b/src/discord_client.rs @@ -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, + discord: DiscordIpcClient, + activity_info: ActivityInfo, + active: bool +} + +impl DiscordClient { + pub fn new(client_id: &str, logger: Rc) -> Result { + 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(()) + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7e0f2f3..6e38196 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; } \ No newline at end of file diff --git a/src/mpv_event_handler.rs b/src/mpv_event_handler.rs index 97eb132..b9818b0 100644 --- a/src/mpv_event_handler.rs +++ b/src/mpv_event_handler.rs @@ -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, logger: Rc @@ -17,7 +17,11 @@ pub struct MpvEventHandler { impl MpvEventHandler { pub fn new(mpv: Handle, listener: Box, logger: Rc) -> Result { + 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 } } diff --git a/src/mpv_event_handler/events.rs b/src/mpv_event_handler/events.rs deleted file mode 100644 index c09e175..0000000 --- a/src/mpv_event_handler/events.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub struct FileInfo { - pub filename: String, - pub metadata: FileMetadata -} - -pub struct FileMetadata { - pub artist: Option, - pub album: Option, - pub title: Option, - pub track: Option -} - -pub enum MpvEvent { - FileLoaded(FileInfo), - Seek(i64), - Pause, - Play -} - -pub trait Listener { - fn handle_event(&self, event: MpvEvent) -> Result<(), &'static str>; -} \ No newline at end of file diff --git a/src/mpv_event_queue.rs b/src/mpv_event_queue.rs new file mode 100644 index 0000000..b602017 --- /dev/null +++ b/src/mpv_event_queue.rs @@ -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, +} + +impl MpvEventQueue { + pub fn new(mpv: Handle,logger: Rc) -> Result { + let new_self = Self { + mpv, + logger, + }; + + new_self.initialize()?; + Ok(new_self) + } + + pub fn from_ptr<'a>(handle: *mut mpv_handle, logger: Rc) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + } + } +} \ No newline at end of file diff --git a/src/mpv_event_queue/events.rs b/src/mpv_event_queue/events.rs new file mode 100644 index 0000000..ef33b6c --- /dev/null +++ b/src/mpv_event_queue/events.rs @@ -0,0 +1,41 @@ +use std::fmt::Display; + +pub struct FileInfo { + pub filename: String, + pub metadata: FileMetadata +} + +pub struct FileMetadata { + pub artist: Option, + pub album: Option, + pub title: Option, + pub track: Option +} + +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>; +} \ No newline at end of file diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 0000000..65da59a --- /dev/null +++ b/src/plugin.rs @@ -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, + mpv: MpvEventQueue, + discord: DiscordClient, +} + +impl RPCPlugin { + pub fn new(handle: *mut mpv_handle, client_id: &str) -> Result { + 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 + } +} \ No newline at end of file