diff --git a/Cargo.lock b/Cargo.lock index 4991ec3..af7b310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "discord-rich-presence" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c55d69cab17c19677ce3a5f8face993a9e6eaf847fecac3547f3a3ff4a2494" +dependencies = [ + "log", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "thiserror", + "uuid 0.8.2", +] + [[package]] name = "displaydoc" version = "0.2.6" @@ -2594,6 +2609,7 @@ dependencies = [ "chrono", "clap", "dirs", + "discord-rich-presence", "epoxy", "flume", "gdk4-wayland", @@ -2924,6 +2940,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "uuid" version = "1.23.4" @@ -3305,7 +3330,7 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "uuid", + "uuid 1.23.4", "windows-sys", "winnow", "zbus_macros", diff --git a/Cargo.toml b/Cargo.toml index 3564e72..1cbf9e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ url = "2.5.8" dirs = "6.0.0" base64 = "0.22.1" libc = "0.2.189" +discord-rich-presence = "1.1.0" [build-dependencies] anyhow = "1.0.103" diff --git a/flatpak/com.stremio.Stremio.Devel.json b/flatpak/com.stremio.Stremio.Devel.json index 697ee08..229a4eb 100644 --- a/flatpak/com.stremio.Stremio.Devel.json +++ b/flatpak/com.stremio.Stremio.Devel.json @@ -31,6 +31,8 @@ "--persist=.stremio-server", "--filesystem=xdg-download", "--filesystem=xdg-run/pipewire-0:ro", + "--filesystem=xdg-run/discord-ipc-0", + "--filesystem=xdg-run/app/com.discordapp.Discord:create", "--talk-name=org.kde.StatusNotifierWatcher", "--own-name=com.stremio.Stremio.Devel", "--own-name=org.mpris.MediaPlayer2.Stremio", diff --git a/src/app/discord/config.rs b/src/app/discord/config.rs new file mode 100644 index 0000000..7f8e421 --- /dev/null +++ b/src/app/discord/config.rs @@ -0,0 +1 @@ +pub const CLIENT_ID: &str = "1452620752263319665"; diff --git a/src/app/discord/mod.rs b/src/app/discord/mod.rs new file mode 100644 index 0000000..eb3787d --- /dev/null +++ b/src/app/discord/mod.rs @@ -0,0 +1,62 @@ +mod config; + +use std::cell::RefCell; + +use discord_rich_presence::{ + DiscordIpc, DiscordIpcClient, + activity::{Activity, ActivityType, Assets}, +}; +use tracing::error; + +use config::CLIENT_ID; + +pub struct Discord { + client: RefCell, +} + +impl Discord { + pub fn new() -> Self { + let client = RefCell::new(DiscordIpcClient::new(CLIENT_ID)); + + Self { client } + } + + pub fn connect(&self) -> bool { + if let Err(e) = self.client.borrow_mut().connect() { + error!("Failed to connect: {e}"); + return false; + } + + true + } + + pub fn disconnect(&self) { + if let Err(e) = self.client.borrow_mut().close() { + error!("Failed to disconnect: {e}"); + } + } + + pub fn set_activity(&self, details: String, state: String, image: Option) { + let mut assets = Assets::new().large_text("Stremio"); + + if let Some(image) = image { + assets = assets.large_image(image); + } + + let activity = Activity::default() + .activity_type(ActivityType::Watching) + .details(details) + .state(state) + .assets(assets); + + if let Err(e) = self.client.borrow_mut().set_activity(activity) { + error!("Failed to set activity: {e}"); + } + } + + pub fn clear_activity(&self) { + if let Err(e) = self.client.borrow_mut().clear_activity() { + error!("Failed to clear activity: {e}"); + } + } +} diff --git a/src/app/imp.rs b/src/app/imp.rs index cc841a8..89cd019 100644 --- a/src/app/imp.rs +++ b/src/app/imp.rs @@ -7,9 +7,10 @@ use tracing::error; use crate::{ app::{ config::{APP_ID, APP_NAME, URI_SCHEME}, + discord::Discord, ipc::{ self, - event::{IpcEvent, IpcEventMpv}, + event::{IpcEvent, IpcEventDiscord, IpcEventMpv}, }, mpris::Mpris, tray::Tray, @@ -71,6 +72,7 @@ impl ApplicationImpl for Application { let tray = Tray::default(); let video = Video::default(); let mpris = Mpris::default(); + let discord = Discord::new(); let startup_url = self.startup_url.borrow(); let dev_mode = self.dev_mode.get(); @@ -159,6 +161,21 @@ impl ApplicationImpl for Application { IpcEvent::Quit => { app.quit(); } + IpcEvent::Discord(event) => match event { + IpcEventDiscord::Connect => { + let connected = discord.connect(); + let message = ipc::create_response(IpcEvent::Discord( + IpcEventDiscord::Status(connected), + )); + webview.send(&message); + } + IpcEventDiscord::Disconnect => discord.disconnect(), + IpcEventDiscord::SetActivity((details, state, image)) => { + discord.set_activity(details, state, image) + } + IpcEventDiscord::ClearActivity => discord.clear_activity(), + _ => {} + }, IpcEvent::Mpv(event) => match event { IpcEventMpv::Observe(name) => video.observe_mpv_property(name), IpcEventMpv::Command((name, args)) => { diff --git a/src/app/ipc/event.rs b/src/app/ipc/event.rs index 3cc5967..4f07337 100644 --- a/src/app/ipc/event.rs +++ b/src/app/ipc/event.rs @@ -12,6 +12,15 @@ pub enum IpcEventMpv { Ended((String, Option)), } +#[derive(Deserialize, Debug)] +pub enum IpcEventDiscord { + Status(bool), + Connect, + Disconnect, + SetActivity((String, String, Option)), + ClearActivity, +} + #[derive(Deserialize, Debug)] pub enum IpcEvent { Init, @@ -23,6 +32,7 @@ pub enum IpcEvent { Mpv(IpcEventMpv), MediaMetadata((String, Option, Option)), MediaStatus(bool), + Discord(IpcEventDiscord), } impl TryFrom<&str> for IpcEvent { diff --git a/src/app/ipc/request.rs b/src/app/ipc/request.rs index 2b85ed8..41230fb 100644 --- a/src/app/ipc/request.rs +++ b/src/app/ipc/request.rs @@ -1,6 +1,8 @@ use serde::Deserialize; use serde_json::Value; +use crate::app::ipc::event::IpcEventDiscord; + use super::event::{IpcEvent, IpcEventMpv}; #[derive(Deserialize, Debug)] @@ -28,6 +30,14 @@ pub struct IpcMessageRequestMediaStatus { paused: bool, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IpcMessageRequestDiscordSetActivity { + details: String, + state: String, + image: Option, +} + impl TryFrom for IpcEvent { type Error = String; @@ -100,6 +110,24 @@ impl TryFrom for IpcEvent { Ok(IpcEvent::MediaStatus(data.paused)) } + "discord-connect" => Ok(IpcEvent::Discord(IpcEventDiscord::Connect)), + "discord-disconnect" => { + Ok(IpcEvent::Discord(IpcEventDiscord::Disconnect)) + } + "discord-set-activity" => { + let data: IpcMessageRequestDiscordSetActivity = + serde_json::from_value(data) + .map_err(|_| "Invalid discord-set-activity object")?; + + Ok(IpcEvent::Discord(IpcEventDiscord::SetActivity(( + data.details, + data.state, + data.image, + )))) + } + "discord-clear-activity" => { + Ok(IpcEvent::Discord(IpcEventDiscord::ClearActivity)) + } method => Err(format!("Invalid method: {method}")), }, None => match name { diff --git a/src/app/ipc/response.rs b/src/app/ipc/response.rs index 76803be..5dcb34c 100644 --- a/src/app/ipc/response.rs +++ b/src/app/ipc/response.rs @@ -1,6 +1,8 @@ use serde::Serialize; use serde_json::json; +use crate::app::ipc::event::IpcEventDiscord; + use super::{ TRANSPORT_NAME, VERSION, event::{IpcEvent, IpcEventMpv}, @@ -102,6 +104,15 @@ impl TryFrom for IpcMessageResponse { "paused": paused }])), }), + IpcEvent::Discord(IpcEventDiscord::Status(connected)) => Ok(IpcMessageResponse { + id: 1, + r#type: 1, + object: TRANSPORT_NAME.to_owned(), + data: None, + args: Some(json!(["discord-status", { + "connected": connected + }])), + }), _ => Err("Failed to convert IpcEvent to IpcMessageResponse"), } } diff --git a/src/app/mod.rs b/src/app/mod.rs index a923107..6b21333 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,5 +1,6 @@ mod config; mod dialogs; +mod discord; mod imp; mod ipc; mod mpris;