diff --git a/Cargo.lock b/Cargo.lock index fb027dc..424c88a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,6 +406,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "discord-rich-presence" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ead3c5edc7e048c317c6fc4a7e24aff0c7e4c136918e2ba38106a385b2cc53a5" +dependencies = [ + "log", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1513,6 +1528,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "serde_test" version = "1.0.176" @@ -1604,6 +1630,7 @@ dependencies = [ "bitflags 2.4.2", "chrono", "clap", + "discord-rich-presence", "flume", "libmpv2", "libmpv2-sys", @@ -1964,6 +1991,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.12", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index cd4c949..fac6884 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "5.0.15" edition = "2018" [dependencies] +discord-rich-presence = "1" once_cell = "1.19" native-windows-gui = { git = "https://github.com/Stremio/native-windows-gui", features = [ "high-dpi", diff --git a/src/stremio_app/app.rs b/src/stremio_app/app.rs index 024272e..bb0e8a0 100644 --- a/src/stremio_app/app.rs +++ b/src/stremio_app/app.rs @@ -27,6 +27,7 @@ use crate::stremio_app::{ PipeServer, }; +use super::discord::DiscordRpc; use super::stremio_server::StremioServer; #[derive(Default, NwgUi)] @@ -247,6 +248,10 @@ impl MainWindow { let hide_splash_sender = self.hide_splash_notice.sender(); let focus_sender = self.focus_notice.sender(); let autoupdater_setup_mutex = self.autoupdater_setup_file.clone(); + + let discord_rpc: Arc>> = Arc::new(Mutex::new(None)); + let discord_rpc_clone = discord_rpc.clone(); + thread::spawn(move || loop { if let Some(msg) = web_rx .recv() @@ -336,6 +341,58 @@ impl MainWindow { } } } + Some("discord-connect") => { + let mut discord_guard = discord_rpc_clone.lock().unwrap(); + if discord_guard.is_none() { + let discord = DiscordRpc::new(); + match discord.connect() { + Ok(()) => { + *discord_guard = Some(discord); + web_tx_web.send(RPCResponse::discord_status(true)).ok(); + } + Err(e) => { + eprintln!("Discord connect error: {}", e); + web_tx_web.send(RPCResponse::discord_status(false)).ok(); + } + } + } else { + // Already connected + web_tx_web.send(RPCResponse::discord_status(true)).ok(); + } + } + Some("discord-disconnect") => { + let mut discord_guard = discord_rpc_clone.lock().unwrap(); + if let Some(ref discord) = *discord_guard { + if let Err(e) = discord.disconnect() { + eprintln!("Discord disconnect error: {}", e); + } + } + *discord_guard = None; + web_tx_web.send(RPCResponse::discord_status(false)).ok(); + } + Some("discord-set-activity") => { + if let Some(params) = msg.get_params() { + let state = params.get("state").and_then(|v| v.as_str()).unwrap_or(""); + let details = params.get("details").and_then(|v| v.as_str()).unwrap_or(""); + let image = params.get("image").and_then(|v| v.as_str()); + let start_timestamp = params.get("startTimestamp").and_then(|v| v.as_i64()); + + let discord_guard = discord_rpc_clone.lock().unwrap(); + if let Some(ref discord) = *discord_guard { + if let Err(e) = discord.set_activity(state, details, image, start_timestamp) { + eprintln!("Discord set activity error: {}", e); + } + } + } + } + Some("discord-clear-activity") => { + let discord_guard = discord_rpc_clone.lock().unwrap(); + if let Some(ref discord) = *discord_guard { + if let Err(e) = discord.clear_activity() { + eprintln!("Discord clear activity error: {}", e); + } + } + } Some(player_command) if player_command.starts_with("mpv-") => { let resp_json = serde_json::to_string( &msg.args.expect("Cannot have method without args"), diff --git a/src/stremio_app/discord.rs b/src/stremio_app/discord.rs new file mode 100644 index 0000000..8039bf7 --- /dev/null +++ b/src/stremio_app/discord.rs @@ -0,0 +1,130 @@ +use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient}; +use std::sync::{Arc, Mutex}; + +const DISCORD_APP_ID: &str = "1452620752263319665"; + +/// Discord Rich Presence for Stremio +/// Handles connection to Discord and activity updates +pub struct DiscordRpc { + client: Arc>>, +} + +impl Default for DiscordRpc { + fn default() -> Self { + Self::new() + } +} + +impl DiscordRpc { + pub fn new() -> Self { + Self { + client: Arc::new(Mutex::new(None)), + } + } + + pub fn connect(&self) -> Result<(), String> { + let mut client_guard = self.client.lock().map_err(|e| e.to_string())?; + + if client_guard.is_some() { + return Ok(()); + } + + let mut client = DiscordIpcClient::new(DISCORD_APP_ID) + .map_err(|e| format!("Failed to create Discord client: {}", e))?; + + client + .connect() + .map_err(|e| format!("Failed to connect to Discord: {}", e))?; + + *client_guard = Some(client); + + println!("Discord RPC connected"); + Ok(()) + } + + pub fn disconnect(&self) -> Result<(), String> { + let mut client_guard = self.client.lock().map_err(|e| e.to_string())?; + + if let Some(mut client) = client_guard.take() { + client + .close() + .map_err(|e| format!("Failed to close Discord connection: {}", e))?; + } + + println!("Discord RPC disconnected"); + Ok(()) + } + + pub fn is_connected(&self) -> bool { + self.client + .lock() + .map(|guard| guard.is_some()) + .unwrap_or(false) + } + + pub fn set_activity( + &self, + state: &str, + details: &str, + large_image: Option<&str>, + start_timestamp: Option, + ) -> Result<(), String> { + let mut client_guard = self.client.lock().map_err(|e| e.to_string())?; + + if let Some(ref mut client) = *client_guard { + let mut payload = activity::Activity::new().state(state).details(details); + + // add assets + if let Some(image) = large_image { + payload = payload.assets( + activity::Assets::new() + .large_image(image) + .large_text("Stremio"), + ); + } else { + // Default Stremio logo + payload = payload.assets( + activity::Assets::new() + .large_image("stremio_logo") + .large_text("Stremio"), + ); + } + + // add timestamps + if let Some(start) = start_timestamp { + payload = payload.timestamps(activity::Timestamps::new().start(start)); + } + + client + .set_activity(payload) + .map_err(|e| format!("Failed to set activity: {}", e))?; + + println!("Discord activity set: {} - {}", state, details); + } else { + return Err("Discord not connected".to_string()); + } + + Ok(()) + } + + /// Clear activity + pub fn clear_activity(&self) -> Result<(), String> { + let mut client_guard = self.client.lock().map_err(|e| e.to_string())?; + + if let Some(ref mut client) = *client_guard { + client + .clear_activity() + .map_err(|e| format!("Failed to clear activity: {}", e))?; + + println!("Discord activity cleared"); + } + + Ok(()) + } +} + +impl Drop for DiscordRpc { + fn drop(&mut self) { + let _ = self.disconnect(); + } +} diff --git a/src/stremio_app/ipc.rs b/src/stremio_app/ipc.rs index 22221f6..35e97a2 100644 --- a/src/stremio_app/ipc.rs +++ b/src/stremio_app/ipc.rs @@ -105,4 +105,9 @@ impl RPCResponse { pub fn update_available() -> String { Self::response_message(Some(json!(["autoupdater-show-notif"]))) } + pub fn discord_status(connected: bool) -> String { + Self::response_message(Some(json!(["discord-status", { + "connected": connected, + }]))) + } } diff --git a/src/stremio_app/mod.rs b/src/stremio_app/mod.rs index 0301d8b..80f3997 100644 --- a/src/stremio_app/mod.rs +++ b/src/stremio_app/mod.rs @@ -1,6 +1,7 @@ pub mod app; pub use app::MainWindow; pub mod ipc; +pub mod discord; pub mod stremio_player; pub mod stremio_server; pub mod stremio_wevbiew;