Event handling refactor and Rich Presence implementation
This commit is contained in:
parent
b04ce61aba
commit
d349e2ac16
9 changed files with 513 additions and 149 deletions
|
@ -9,6 +9,7 @@ crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mpv-client = "0.4.1"
|
mpv-client = "0.4.1"
|
||||||
|
discord-rich-presence="0.2.3"
|
||||||
|
|
||||||
[profile.release-full]
|
[profile.release-full]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|
|
@ -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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/lib.rs
26
src/lib.rs
|
@ -1,25 +1,21 @@
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use mpv_client::mpv_handle;
|
use mpv_client::mpv_handle;
|
||||||
|
|
||||||
mod mpv_event_handler;
|
|
||||||
mod logging;
|
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]
|
#[no_mangle]
|
||||||
fn mpv_open_cplugin(handle: *mut mpv_handle) -> std::os::raw::c_int {
|
fn mpv_open_cplugin(handle: *mut mpv_handle) -> std::os::raw::c_int {
|
||||||
let logger = Rc::new(logging::Logger::from_env());
|
let plugin = match RPCPlugin::new(handle, DISCORD_APPID) {
|
||||||
|
Ok(plugin) => plugin,
|
||||||
let listener = Box::new(basic_listener::MpvListener::new(Rc::clone(&logger)));
|
Err(_) => return -1
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
client.run();
|
plugin.run();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
use std::rc::Rc;
|
use std::{rc::Rc, cell::{RefCell, Cell}, borrow::Borrow};
|
||||||
|
use crate::{logging::{self, Logger}, info};
|
||||||
use mpv_client::{Handle, Event, Property, Format, mpv_handle};
|
use mpv_client::{Handle, Event, Property, Format, mpv_handle, ClientMessage};
|
||||||
use crate::logging::{self, Logger};
|
|
||||||
|
|
||||||
pub mod events;
|
pub mod events;
|
||||||
use events::{MpvEvent, Listener, FileInfo, FileMetadata};
|
use events::{MpvEvent, Listener, FileInfo, FileMetadata};
|
||||||
|
@ -10,6 +9,7 @@ const NAME_PAUSE_PROP: &str = "pause";
|
||||||
const REPL_PAUSE_PROP: u64 = 1;
|
const REPL_PAUSE_PROP: u64 = 1;
|
||||||
|
|
||||||
pub struct MpvEventHandler {
|
pub struct MpvEventHandler {
|
||||||
|
listening: bool,
|
||||||
mpv: Handle,
|
mpv: Handle,
|
||||||
listener: Box<dyn Listener>,
|
listener: Box<dyn Listener>,
|
||||||
logger: Rc<Logger>
|
logger: Rc<Logger>
|
||||||
|
@ -17,7 +17,11 @@ pub struct MpvEventHandler {
|
||||||
|
|
||||||
impl MpvEventHandler {
|
impl MpvEventHandler {
|
||||||
pub fn new(mpv: Handle, listener: Box<dyn Listener>, logger: Rc<Logger>) -> Result<Self, String> {
|
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 {
|
let new_self = Self {
|
||||||
|
listening: false,
|
||||||
mpv,
|
mpv,
|
||||||
listener,
|
listener,
|
||||||
logger
|
logger
|
||||||
|
@ -31,6 +35,19 @@ impl MpvEventHandler {
|
||||||
MpvEventHandler::new(Handle::from_ptr(handle), listener, logger)
|
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> {
|
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)
|
||||||
}
|
}
|
||||||
|
@ -42,21 +59,57 @@ impl MpvEventHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_event(&self) -> Event {
|
||||||
|
self.mpv.wait_event(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
fn handle_event(&self, event: Event) -> Result<(), &'static str> {
|
||||||
logging::info!(self.logger, "Event: {event}");
|
logging::info!(self.logger, "Event: {event}");
|
||||||
|
|
||||||
|
if let Event::ClientMessage(message) = event {
|
||||||
|
return self.handle_client_message(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.listening {
|
||||||
match event {
|
match event {
|
||||||
Event::FileLoaded => self.handle_file_loaded(),
|
Event::PropertyChange(prop_id, prop) => self.send_property_to_listener(prop_id, prop),
|
||||||
Event::PlaybackRestart => self.handle_playback_restart(),
|
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.send_file_info(),
|
||||||
|
Event::PlaybackRestart => self.send_seek(),
|
||||||
_ => Ok(())
|
_ => 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}");
|
logging::info!(self.logger, "Property changed: {prop_id}");
|
||||||
match prop_id {
|
match prop_id {
|
||||||
REPL_PAUSE_PROP => {
|
REPL_PAUSE_PROP => {
|
||||||
match prop.data() {
|
match prop.data() {
|
||||||
Some(pause) => self.handle_pause_change(pause),
|
Some(pause) => self.send_pause(pause),
|
||||||
None => Err("property pause doesn't exist")
|
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 filename_res = self.mpv.get_property("filename");
|
||||||
let artist = self.mpv.get_property("metadata/by-key/artist").ok();
|
let artist = self.mpv.get_property("metadata/by-key/artist").ok();
|
||||||
let album = self.mpv.get_property("metadata/by-key/album").ok();
|
let album = self.mpv.get_property("metadata/by-key/album").ok();
|
||||||
let title = self.mpv.get_property("metadata/by-key/title").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 {
|
let filename = match filename_res {
|
||||||
Ok(name) => name,
|
Ok(name) => name,
|
||||||
|
@ -89,53 +142,55 @@ impl MpvEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = MpvEvent::FileLoaded(file_info);
|
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();
|
fn send_seek(&self) -> Result<(), &'static str>{
|
||||||
match res {
|
let remaining_time = self.mpv.get_property("time-remaining").unwrap_or_else(|_| {
|
||||||
Some(time) => {
|
logging::warning!(self.logger, "Failed retrieving remaing-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?");
|
logging::warning!(self.logger, "This usually happens seeking into file end. Possibly mpv bug?");
|
||||||
Ok(())
|
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 {
|
let event = match pause {
|
||||||
false => MpvEvent::Play,
|
false => MpvEvent::Play,
|
||||||
true => MpvEvent::Pause
|
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) {
|
let res = match new_state {
|
||||||
while self.poll_events() {
|
true => listener_ref.open(),
|
||||||
// Wait for shutdown
|
false => listener_ref.close()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = result {
|
match res {
|
||||||
logging::error!(self.logger, "Error handling event {event_name}: {e}");
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
124
src/mpv_event_queue.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/mpv_event_queue/events.rs
Normal file
41
src/mpv_event_queue/events.rs
Normal 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
54
src/plugin.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue