This commit is contained in:
Timothy Z. 2025-12-29 13:28:05 +00:00 committed by GitHub
commit 47677cabc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 226 additions and 0 deletions

36
Cargo.lock generated
View file

@ -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"

View file

@ -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",

View file

@ -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<Mutex<Option<DiscordRpc>>> = 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,62 @@ 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"),

122
src/stremio_app/discord.rs Normal file
View file

@ -0,0 +1,122 @@
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<Mutex<Option<DiscordIpcClient>>>,
}
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);
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 set_activity(
&self,
state: &str,
details: &str,
large_image: Option<&str>,
start_timestamp: Option<i64>,
) -> 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();
}
}

View file

@ -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,
}])))
}
}

View file

@ -1,5 +1,6 @@
pub mod app;
pub use app::MainWindow;
pub mod discord;
pub mod ipc;
pub mod stremio_player;
pub mod stremio_server;