From 2bd65662a0fdf3c8e6db0e42fc64f03bfacdf8e3 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:29:19 +0100 Subject: [PATCH 01/11] Add discord rpc --- Cargo.lock | 102 +++++++++++++++++++++++++++- psst-core/Cargo.toml | 9 +-- psst-core/src/discord_rpc.rs | 84 +++++++++++++++++++++++ psst-core/src/error.rs | 2 + psst-core/src/lib.rs | 1 + psst-gui/Cargo.toml | 16 +++-- psst-gui/src/controller/playback.rs | 94 +++++++++++++++++++++---- psst-gui/src/data/config.rs | 4 ++ 8 files changed, 287 insertions(+), 25 deletions(-) create mode 100644 psst-core/src/discord_rpc.rs diff --git a/Cargo.lock b/Cargo.lock index 378e66d7..772702a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,6 +645,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.40" @@ -1094,6 +1100,29 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "discord-presence" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806b031712b2d07bb83d077c1d66d1d7c4209e2b812cb5d35a6aadfc2670f91f" +dependencies = [ + "byteorder", + "bytes", + "cfg-if", + "crossbeam-channel", + "log", + "num-derive", + "num-traits", + "parking_lot", + "paste", + "quork", + "serde", + "serde_json", + "serde_repr", + "thiserror 2.0.12", + "uuid", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -3041,6 +3070,18 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3656,6 +3697,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -3716,6 +3779,7 @@ dependencies = [ "crossbeam-channel", "ctr", "cubeb", + "discord-presence", "git-version", "gix-config", "hmac", @@ -3751,6 +3815,7 @@ version = "0.1.0" dependencies = [ "crossbeam-channel", "directories", + "discord-presence", "druid", "druid-enums", "druid-shell", @@ -3830,6 +3895,32 @@ dependencies = [ "byteorder", ] +[[package]] +name = "quork" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4282adb2f46ab23e5e5c7540c5e91f71fb5cb00101a29280b1b77a6e9b2be33" +dependencies = [ + "cfg-if", + "nix 0.29.0", + "quork-proc", + "thiserror 2.0.12", + "windows 0.58.0", +] + +[[package]] +name = "quork-proc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0386bc87f6a1770b849cd7b1127499d9fe6526ea24cc267e0c89874bc7ae5e0c" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "quote" version = "1.0.40" @@ -5445,6 +5536,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "v_frame" version = "0.3.8" @@ -6197,7 +6297,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.4", "once_cell", "ordered-stream", "rand", diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index 0e09e5f7..0ef140e1 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -12,6 +12,7 @@ time = { version = "0.3.36", features = ["local-offset"] } [dependencies] psst-protocol = { path = "../psst-protocol" } rustfm-scrobble = "1.1.1" +discord-presence = { version = "1.5", features = ["activity_type"] } # Common byteorder = { version = "1.5.0" } @@ -47,12 +48,12 @@ cubeb = { git = "https://github.com/mozilla/cubeb-rs", optional = true } libsamplerate = { version = "0.1.0" } rb = { version = "0.4.1" } symphonia = { version = "0.5.4", default-features = false, features = [ - "ogg", - "vorbis", - "mp3", + "ogg", + "vorbis", + "mp3", ] } [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58.0", features = [ - "Win32_System_Com", + "Win32_System_Com", ], default-features = false } diff --git a/psst-core/src/discord_rpc.rs b/psst-core/src/discord_rpc.rs new file mode 100644 index 00000000..1360e30b --- /dev/null +++ b/psst-core/src/discord_rpc.rs @@ -0,0 +1,84 @@ +use crate::error::Error; +use discord_presence::{models::ActivityType, Client as DiscordClient, DiscordError}; + +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +pub struct DiscordRPCClient; + +impl DiscordRPCClient { + /// Creates a Discord Rich Presence client for Spotify with the provided application ID. + pub fn create_client(app_id: u64) -> Result { + let mut client = DiscordClient::new(app_id); + client.start(); + log::info!("Discord RPC client created and started"); + Ok(client) + } + + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs() + } + + /// Update the Discord Rich Presence with currently playing Spotify track information. + pub fn now_playing_song( + client: &mut DiscordClient, + track_name: &str, + artist: &str, + album: Option<&str>, + album_cover_url: Option<&str>, + track_duration: Option, + playback_position: Option, + ) -> Result<(), Error> { + client + .set_activity(|act| { + let mut act = act + .details(track_name) + .state(artist) + ._type(ActivityType::Listening); + + if let Some(cover_url) = album_cover_url { + act = act.assets(|assets| { + let mut assets = assets.large_image(cover_url); + if let Some(album_name) = album { + assets = assets.large_text(album_name); + } + assets + }); + } + + if let Some(duration) = track_duration { + let now = Self::get_current_timestamp(); + let position_secs = playback_position + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + + let start_time = now.saturating_sub(position_secs); + let end_time = start_time + duration.as_secs(); + log::info!( + "Discord RPC StartTime: {} - EndTime: {}", + start_time, + end_time + ); + act = act.timestamps(|timestamps| timestamps.start(start_time).end(end_time)); + } + + act + }) + .map(|_| ()) // Discard the Payload and return () + .map_err(Error::from) + } + + /// Stop displaying Rich Presence by clearing the activity. + pub fn clear_presence(client: &mut DiscordClient) -> Result<(), Error> { + // Map the payload result to () to match our return type + client.clear_activity().map(|_| ()).map_err(Error::from) + } +} + +impl From for Error { + fn from(value: DiscordError) -> Self { + Self::DiscordRPCError(Box::new(value)) + } +} diff --git a/psst-core/src/error.rs b/psst-core/src/error.rs index d22664dc..85119101 100644 --- a/psst-core/src/error.rs +++ b/psst-core/src/error.rs @@ -15,6 +15,7 @@ pub enum Error { AudioOutputError(Box), AudioProbeError(Box), ScrobblerError(Box), + DiscordRPCError(Box), ResamplingError(i32), ConfigError(String), IoError(io::Error), @@ -57,6 +58,7 @@ impl fmt::Display for Error { | Self::AudioDecodingError(err) | Self::AudioOutputError(err) | Self::ScrobblerError(err) + | Self::DiscordRPCError(err) | Self::AudioProbeError(err) => err.fmt(f), Self::IoError(err) => err.fmt(f), Self::SendError => write!(f, "Failed to send into a channel"), diff --git a/psst-core/src/lib.rs b/psst-core/src/lib.rs index d2e0dad2..f7e1ee3b 100644 --- a/psst-core/src/lib.rs +++ b/psst-core/src/lib.rs @@ -11,6 +11,7 @@ pub mod audio; pub mod cache; pub mod cdn; pub mod connection; +pub mod discord_rpc; pub mod error; pub mod item_id; pub mod lastfm; diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index c82169f5..cb3612e8 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -37,23 +37,25 @@ url = { version = "2.5.2" } # GUI druid = { git = "https://github.com/jpochyla/druid", branch = "psst", features = [ - "im", - "image", - "jpeg", - "png", - "serde", + "im", + "image", + "jpeg", + "png", + "serde", ] } druid-enums = { git = "https://github.com/jpochyla/druid-enums" } druid-shell = { git = "https://github.com/jpochyla/druid", branch = "psst", features = [ - "raw-win-handle", + "raw-win-handle", ] } open = { version = "5.3.0" } raw-window-handle = { version = "0.5.2" } # Must stay compatible with Druid souvlaki = { version = "0.7.3", default-features = false, features = [ - "use_zbus", + "use_zbus", ] } sanitize_html = "0.8.1" rustfm-scrobble = "1.1.1" +discord-presence = "1.5" + [target.'cfg(windows)'.build-dependencies] winres = { version = "0.1.12" } image = { version = "0.25.4" } diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 4631221a..2657dbb1 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -4,6 +4,8 @@ use std::{ }; use crossbeam_channel::Sender; +use discord_presence::Client as DiscordClient; + use druid::{ im::Vector, widget::{prelude::*, Controller}, @@ -13,11 +15,13 @@ use psst_core::{ audio::{normalize::NormalizationLevel, output::DefaultAudioOutput}, cache::Cache, cdn::Cdn, + discord_rpc::DiscordRPCClient, lastfm::LastFmClient, player::{item::PlaybackItem, PlaybackConfig, Player, PlayerCommand, PlayerEvent}, session::SessionService, }; use rustfm_scrobble::Scrobbler; + use souvlaki::{ MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, }; @@ -39,22 +43,18 @@ pub struct PlaybackController { media_controls: Option, has_scrobbled: bool, scrobbler: Option, + discord_rpc: Option, startup: bool, } -fn init_scrobbler_instance( - data: &AppState -) -> Option { +fn init_scrobbler_instance(data: &AppState) -> Option { if data.config.lastfm_enable { if let (Some(api_key), Some(api_secret), Some(session_key)) = ( data.config.lastfm_api_key.as_deref(), data.config.lastfm_api_secret.as_deref(), data.config.lastfm_session_key.as_deref(), ) { - match LastFmClient::create_scrobbler( - Some(api_key), - Some(api_secret), - Some(session_key), - ) { + match LastFmClient::create_scrobbler(Some(api_key), Some(api_secret), Some(session_key)) + { Ok(scr) => { log::info!("Last.fm Scrobbler instance created/updated."); return Some(scr); @@ -64,9 +64,7 @@ fn init_scrobbler_instance( } } } else { - log::info!( - "Last.fm credentials incomplete or removed, clearing Scrobbler instance." - ); + log::info!("Last.fm credentials incomplete or removed, clearing Scrobbler instance."); } } else { log::info!("Last.fm scrobbling is disabled, clearing Scrobbler instance."); @@ -74,6 +72,25 @@ fn init_scrobbler_instance( None } +fn init_discord_rpc_instance(data: &AppState) -> Option { + if data.config.discord_rcp_enable { + if let Some(client_id) = data.config.discord_rpc_client_id { + match DiscordRPCClient::create_client(client_id) { + Ok(rpc) => Some(rpc), + Err(e) => { + log::warn!("Failed to create Discord RPC: {}", e); + None + } + } + } else { + log::info!("Discord RPC client ID not provided"); + None + } + } else { + log::info!("Discord RPC is disabled"); + None + } +} impl PlaybackController { pub fn new() -> Self { @@ -84,6 +101,7 @@ impl PlaybackController { media_controls: None, has_scrobbled: false, scrobbler: None, + discord_rpc: None, startup: true, } } @@ -269,6 +287,51 @@ impl PlaybackController { } } + fn clear_discord_rpc(&mut self) { + if let Some(discord_rpc) = &mut self.discord_rpc { + if let Err(e) = DiscordRPCClient::clear_presence(discord_rpc) { + log::warn!("error clearing Discord RPC presence: {:?}", e); + } else { + log::info!("Discord RPC presence cleared"); + } + } + } + + fn update_discord_rpc(&mut self, playback: &Playback) { + if let Some(now_playing) = playback.now_playing.as_ref() { + if let Playable::Track(track) = &now_playing.item { + if let Some(discord_rpc) = &mut self.discord_rpc { + let artist = track.artist_name(); + let title = track.name.clone(); + let album = track.album.clone(); + let track_duration = track.duration; + let progress = now_playing.progress; + + let album_cover_url = album.as_ref().and_then(|a| { + a.images + .iter() + .find(|img| img.width == Some(64)) + .map(|img| img.url.as_ref()) + }); + + if let Err(e) = DiscordRPCClient::now_playing_song( + discord_rpc, + title.as_ref(), + artist.as_ref(), + album.as_ref().map(|a| a.name.as_ref()), + album_cover_url, + Some(track_duration), + Some(progress), + ) { + log::warn!("failed to update discord RPC: {}", e); + } else { + log::info!("updated discord RPC: {} - {}", artist, title); + } + } + } + } + } + fn report_now_playing(&mut self, playback: &Playback) { if let Some(now_playing) = playback.now_playing.as_ref() { if let Playable::Track(track) = &now_playing.item { @@ -451,6 +514,7 @@ where // Song has changed, so we reset the has_scrobbled value self.has_scrobbled = false; self.report_now_playing(&data.playback); + self.update_discord_rpc(&data.playback); if let Some(queued) = data.queued_entry(*item) { data.start_playback(queued.item, queued.origin, progress.to_owned()); @@ -475,11 +539,13 @@ where Event::Command(cmd) if cmd.is(cmd::PLAYBACK_PAUSING) => { data.pause_playback(); self.update_media_control_playback(&data.playback); + self.clear_discord_rpc(); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::PLAYBACK_RESUMING) => { data.resume_playback(); self.update_media_control_playback(&data.playback); + self.update_discord_rpc(&data.playback); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::PLAYBACK_BLOCKED) => { @@ -489,6 +555,7 @@ where Event::Command(cmd) if cmd.is(cmd::PLAYBACK_STOPPED) => { data.stop_playback(); self.update_media_control_playback(&data.playback); + self.clear_discord_rpc(); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::PLAY_TRACKS) => { @@ -547,6 +614,7 @@ where ); self.seek(position); } + self.update_discord_rpc(&data.playback); ctx.set_handled(); } Event::Command(cmd) if cmd.is(cmd::SKIP_TO_POSITION) => { @@ -622,8 +690,8 @@ where if self.startup { self.startup = false; self.scrobbler = init_scrobbler_instance(data); - - } + self.discord_rpc = init_discord_rpc_instance(data); + } child.lifecycle(ctx, event, data, env); } diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index 99010ce0..7bf407c5 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -124,6 +124,8 @@ pub struct Config { pub lastfm_api_key: Option, pub lastfm_api_secret: Option, pub lastfm_enable: bool, + pub discord_rpc_client_id: Option, + pub discord_rcp_enable: bool, } impl Default for Config { @@ -146,6 +148,8 @@ impl Default for Config { lastfm_api_key: None, lastfm_api_secret: None, lastfm_enable: false, + discord_rpc_client_id: Some(1365312248914116639), + discord_rcp_enable: true, } } } From b817a951618a2f1d5c0601e8a11c0017b63ebdc3 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:06:31 +0100 Subject: [PATCH 02/11] Prevent discord rpc from blocking main thread --- psst-core/src/discord_rpc.rs | 62 ++++++++++++++++++++++++++--- psst-gui/src/controller/playback.rs | 53 +++++++++++------------- 2 files changed, 80 insertions(+), 35 deletions(-) diff --git a/psst-core/src/discord_rpc.rs b/psst-core/src/discord_rpc.rs index 1360e30b..ea3cb439 100644 --- a/psst-core/src/discord_rpc.rs +++ b/psst-core/src/discord_rpc.rs @@ -1,10 +1,28 @@ use crate::error::Error; use discord_presence::{models::ActivityType, Client as DiscordClient, DiscordError}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{ + sync::Arc, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use crossbeam_channel::{unbounded, Receiver, Sender}; pub struct DiscordRPCClient; +pub enum DiscordRpcCmd { + Update { + track: Arc, + artist: Arc, + album: Option, + cover_url: Option, + duration: Option, + position: Option, + }, + Clear, +} + impl DiscordRPCClient { /// Creates a Discord Rich Presence client for Spotify with the provided application ID. pub fn create_client(app_id: u64) -> Result { @@ -14,6 +32,43 @@ impl DiscordRPCClient { Ok(client) } + pub fn spawn_rpc_worker(app_id: u64) -> Result, Error> { + let mut rpc = DiscordRPCClient::create_client(app_id)?; + + let (tx, rx): (Sender, Receiver) = unbounded(); + + thread::spawn(move || { + for cmd in rx { + match cmd { + DiscordRpcCmd::Update { + track, + artist, + album, + cover_url, + duration, + position, + } => { + let _ = DiscordRPCClient::now_playing_song( + &mut rpc, + &track, + &artist, + album.as_deref(), + cover_url.as_deref(), + duration, + position, + ); + } + DiscordRpcCmd::Clear => { + let _ = DiscordRPCClient::clear_presence(&mut rpc); + } + } + } + // when tx is dropped everywhere, rx returns Err -> loop ends, rpc is dropped + }); + + Ok(tx) + } + fn get_current_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -56,11 +111,6 @@ impl DiscordRPCClient { let start_time = now.saturating_sub(position_secs); let end_time = start_time + duration.as_secs(); - log::info!( - "Discord RPC StartTime: {} - EndTime: {}", - start_time, - end_time - ); act = act.timestamps(|timestamps| timestamps.start(start_time).end(end_time)); } diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 2657dbb1..1807531c 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -4,7 +4,6 @@ use std::{ }; use crossbeam_channel::Sender; -use discord_presence::Client as DiscordClient; use druid::{ im::Vector, @@ -15,7 +14,7 @@ use psst_core::{ audio::{normalize::NormalizationLevel, output::DefaultAudioOutput}, cache::Cache, cdn::Cdn, - discord_rpc::DiscordRPCClient, + discord_rpc::{DiscordRPCClient, DiscordRpcCmd}, lastfm::LastFmClient, player::{item::PlaybackItem, PlaybackConfig, Player, PlayerCommand, PlayerEvent}, session::SessionService, @@ -43,7 +42,7 @@ pub struct PlaybackController { media_controls: Option, has_scrobbled: bool, scrobbler: Option, - discord_rpc: Option, + discord_rpc_sender: Option>, startup: bool, } fn init_scrobbler_instance(data: &AppState) -> Option { @@ -72,11 +71,11 @@ fn init_scrobbler_instance(data: &AppState) -> Option { None } -fn init_discord_rpc_instance(data: &AppState) -> Option { +fn init_discord_rpc_instance(data: &AppState) -> Option> { if data.config.discord_rcp_enable { if let Some(client_id) = data.config.discord_rpc_client_id { - match DiscordRPCClient::create_client(client_id) { - Ok(rpc) => Some(rpc), + match DiscordRPCClient::spawn_rpc_worker(client_id) { + Ok(discord_rpc_sender) => Some(discord_rpc_sender), Err(e) => { log::warn!("Failed to create Discord RPC: {}", e); None @@ -101,7 +100,7 @@ impl PlaybackController { media_controls: None, has_scrobbled: false, scrobbler: None, - discord_rpc: None, + discord_rpc_sender: None, startup: true, } } @@ -288,19 +287,18 @@ impl PlaybackController { } fn clear_discord_rpc(&mut self) { - if let Some(discord_rpc) = &mut self.discord_rpc { - if let Err(e) = DiscordRPCClient::clear_presence(discord_rpc) { - log::warn!("error clearing Discord RPC presence: {:?}", e); - } else { - log::info!("Discord RPC presence cleared"); - } + if let Some(sender) = &self.discord_rpc_sender { + // `sender` is &Sender + let _ = sender + .send(DiscordRpcCmd::Clear) + .map_err(|e| log::error!("error clearing Discord RPC: {:?}", e)); } } fn update_discord_rpc(&mut self, playback: &Playback) { if let Some(now_playing) = playback.now_playing.as_ref() { if let Playable::Track(track) = &now_playing.item { - if let Some(discord_rpc) = &mut self.discord_rpc { + if let Some(discord_rpc_sender) = &mut self.discord_rpc_sender { let artist = track.artist_name(); let title = track.name.clone(); let album = track.album.clone(); @@ -313,20 +311,17 @@ impl PlaybackController { .find(|img| img.width == Some(64)) .map(|img| img.url.as_ref()) }); - - if let Err(e) = DiscordRPCClient::now_playing_song( - discord_rpc, - title.as_ref(), - artist.as_ref(), - album.as_ref().map(|a| a.name.as_ref()), - album_cover_url, - Some(track_duration), - Some(progress), - ) { - log::warn!("failed to update discord RPC: {}", e); - } else { - log::info!("updated discord RPC: {} - {}", artist, title); - } + let album_name = album.as_ref().map(|a| a.name.as_ref()); + let _ = discord_rpc_sender + .send(DiscordRpcCmd::Update { + track: title.clone(), + artist: artist.clone(), + album: album_name.map(str::to_owned), + cover_url: album_cover_url.map(str::to_owned), + duration: Some(track_duration), + position: Some(progress), + }) + .map_err(|e| log::error!("error updating Discord RPC: {:?}", e)); } } } @@ -690,7 +685,7 @@ where if self.startup { self.startup = false; self.scrobbler = init_scrobbler_instance(data); - self.discord_rpc = init_discord_rpc_instance(data); + self.discord_rpc_sender = init_discord_rpc_instance(data); } child.lifecycle(ctx, event, data, env); } From 8c7658987f11c992fa94ffc9777d32784ca6b419 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:02:23 +0100 Subject: [PATCH 03/11] discord rpc ui/config stuff --- psst-core/src/discord_rpc.rs | 75 ++++++++++++++++++++------ psst-gui/src/controller/playback.rs | 84 +++++++++++++++++++++++++++-- psst-gui/src/data/config.rs | 8 +-- psst-gui/src/ui/preferences.rs | 25 +++++++++ 4 files changed, 168 insertions(+), 24 deletions(-) diff --git a/psst-core/src/discord_rpc.rs b/psst-core/src/discord_rpc.rs index ea3cb439..1147470e 100644 --- a/psst-core/src/discord_rpc.rs +++ b/psst-core/src/discord_rpc.rs @@ -9,8 +9,6 @@ use std::{ use crossbeam_channel::{unbounded, Receiver, Sender}; -pub struct DiscordRPCClient; - pub enum DiscordRpcCmd { Update { track: Arc, @@ -20,10 +18,25 @@ pub enum DiscordRpcCmd { duration: Option, position: Option, }, + Shutdown, Clear, + UpdateClientId(u64), +} + +pub struct DiscordRPCClient { + client: Option, } impl DiscordRPCClient { + #[inline] + fn with_client(&mut self, f: F) + where + F: FnOnce(&mut DiscordClient), + { + if let Some(c) = self.client.as_mut() { + f(c); + } + } /// Creates a Discord Rich Presence client for Spotify with the provided application ID. pub fn create_client(app_id: u64) -> Result { let mut client = DiscordClient::new(app_id); @@ -32,9 +45,11 @@ impl DiscordRPCClient { Ok(client) } + /// Spawns a worker thread to handle Discord RPC commands. pub fn spawn_rpc_worker(app_id: u64) -> Result, Error> { - let mut rpc = DiscordRPCClient::create_client(app_id)?; - + let mut rpc = DiscordRPCClient { + client: Some(Self::create_client(app_id)?), + }; let (tx, rx): (Sender, Receiver) = unbounded(); thread::spawn(move || { @@ -48,18 +63,47 @@ impl DiscordRPCClient { duration, position, } => { - let _ = DiscordRPCClient::now_playing_song( - &mut rpc, - &track, - &artist, - album.as_deref(), - cover_url.as_deref(), - duration, - position, - ); + while !discord_presence::Client::is_ready() { + std::thread::sleep(Duration::from_millis(10)); + } + rpc.with_client(|c| { + let _ = Self::now_playing_song( + c, // <- &mut DiscordClient + &track, + &artist, + album.as_deref(), + cover_url.as_deref(), + duration, + position, + ); + }); } DiscordRpcCmd::Clear => { - let _ = DiscordRPCClient::clear_presence(&mut rpc); + rpc.with_client(|c| { + let _ = DiscordRPCClient::clear_presence(c); + }); + } + DiscordRpcCmd::Shutdown => { + if let Some(client) = rpc.client.take() { + if let Err(e) = client.shutdown() { + log::warn!("Shutdown failed: {}", e); + } + } + // Exit the loop + break; + } + DiscordRpcCmd::UpdateClientId(new_id) => { + // take the old client out + if let Some(old) = rpc.client.take() { + if let Err(e) = old.shutdown() { + log::warn!("shutdown failed: {}", e); + } + } + // create replacement + match Self::create_client(new_id) { + Ok(new_cli) => rpc.client = Some(new_cli), + Err(e) => log::warn!("failed to create new client: {}", e), + } } } } @@ -116,13 +160,12 @@ impl DiscordRPCClient { act }) - .map(|_| ()) // Discard the Payload and return () + .map(|_| ()) .map_err(Error::from) } /// Stop displaying Rich Presence by clearing the activity. pub fn clear_presence(client: &mut DiscordClient) -> Result<(), Error> { - // Map the payload result to () to match our return type client.clear_activity().map(|_| ()).map_err(Error::from) } } diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 1807531c..4db105ee 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -71,18 +71,47 @@ fn init_scrobbler_instance(data: &AppState) -> Option { None } +fn parse_valid_client_id(id_str: &str) -> Option { + let trimmed = id_str.trim(); + + if trimmed.is_empty() { + log::info!("Discord RPC client ID not provided"); + return None; + } + + if !trimmed.chars().all(|c| c.is_ascii_digit()) { + log::warn!("Discord RPC client ID contains non-digit characters"); + return None; + } + // Check if the client ID has a valid length for a snowflake 17-19 + if !(17..=19).contains(&trimmed.len()) { + log::warn!( + "Discord RPC client ID has invalid length ({} characters)", + trimmed.len() + ); + return None; + } + + match trimmed.parse::() { + Ok(id) => Some(id), + Err(e) => { + log::warn!("Failed to parse Discord RPC client ID '{}': {}", trimmed, e); + None + } + } +} + fn init_discord_rpc_instance(data: &AppState) -> Option> { - if data.config.discord_rcp_enable { - if let Some(client_id) = data.config.discord_rpc_client_id { + if data.config.discord_rpc_enable { + if let Some(client_id) = parse_valid_client_id(&data.config.discord_rpc_client_id) { match DiscordRPCClient::spawn_rpc_worker(client_id) { - Ok(discord_rpc_sender) => Some(discord_rpc_sender), + Ok(sender) => Some(sender), Err(e) => { log::warn!("Failed to create Discord RPC: {}", e); None } } } else { - log::info!("Discord RPC client ID not provided"); None } } else { @@ -305,6 +334,8 @@ impl PlaybackController { let track_duration = track.duration; let progress = now_playing.progress; + log::info!("Updating Discord RPC with track: {}", title); + let album_cover_url = album.as_ref().and_then(|a| { a.images .iter() @@ -327,6 +358,49 @@ impl PlaybackController { } } + fn reconcile_discord_rpc(&mut self, old: &AppState, new: &AppState, playback: &Playback) { + let was_enabled = old.config.discord_rpc_enable; + let is_enabled = new.config.discord_rpc_enable; + let id_changed = old.config.discord_rpc_client_id != new.config.discord_rpc_client_id; + + match (was_enabled, is_enabled, self.discord_rpc_sender.is_some()) { + // turned OFF + (true, false, true) => { + // shutdown the RPC + log::info!("Shutting down Discord RPC"); + if let Some(ref tx) = self.discord_rpc_sender { + let _ = tx.send(DiscordRpcCmd::Shutdown); + } + // then drop the sender + self.discord_rpc_sender = None; + } + + // turned ON first time create a new sender + (false, true, false) => { + log::info!("Starting Discord RPC"); + self.discord_rpc_sender = init_discord_rpc_instance(new); + self.update_discord_rpc(playback) + } + + // still ON and ID changed update the app id + (_, true, true) if id_changed => { + log::info!("Updating Discord RPC client ID"); + if let Some(ref tx) = self.discord_rpc_sender { + if let Some(client_id) = + parse_valid_client_id(&new.config.discord_rpc_client_id) + { + let _ = tx.send(DiscordRpcCmd::UpdateClientId(client_id)); + self.update_discord_rpc(playback); + } else { + log::warn!("Client ID changed but new value was invalid; not updating"); + } + } + } + + _ => {} // nothing else to do + } + } + fn report_now_playing(&mut self, playback: &Playback) { if let Some(now_playing) = playback.now_playing.as_ref() { if let Playable::Track(track) = &now_playing.item { @@ -707,6 +781,8 @@ where || old_data.config.lastfm_session_key != data.config.lastfm_session_key || old_data.config.lastfm_enable != data.config.lastfm_enable; + self.reconcile_discord_rpc(old_data, data, &data.playback); + if lastfm_changed { self.scrobbler = init_scrobbler_instance(data); } diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index 7bf407c5..eb8bdbd8 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -124,8 +124,8 @@ pub struct Config { pub lastfm_api_key: Option, pub lastfm_api_secret: Option, pub lastfm_enable: bool, - pub discord_rpc_client_id: Option, - pub discord_rcp_enable: bool, + pub discord_rpc_client_id: String, + pub discord_rpc_enable: bool, } impl Default for Config { @@ -148,8 +148,8 @@ impl Default for Config { lastfm_api_key: None, lastfm_api_secret: None, lastfm_enable: false, - discord_rpc_client_id: Some(1365312248914116639), - discord_rcp_enable: true, + discord_rpc_client_id: String::new(), + discord_rpc_enable: true, } } } diff --git a/psst-gui/src/ui/preferences.rs b/psst-gui/src/ui/preferences.rs index f3c69dd4..f1ac15ca 100644 --- a/psst-gui/src/ui/preferences.rs +++ b/psst-gui/src/ui/preferences.rs @@ -356,6 +356,31 @@ fn account_tab_widget(tab: AccountTab) -> impl Widget { }, )); } + if matches!(tab, AccountTab::InPreferences) { + col = col + .with_spacer(theme::grid(2.0)) + .with_child(Label::new("Discord RPC")) + .with_spacer(theme::grid(1.0)) + .with_child( + Label::new("Uses the Discord RPC socket to display currently playing music") + .with_text_color(theme::PLACEHOLDER_COLOR) + .with_line_break_mode(LineBreaking::WordWrap), + ) + .with_spacer(theme::grid(2.0)) + .with_child( + Flex::row().with_child( + Checkbox::new("Enable Discord RPC") + .lens(AppState::config.then(Config::discord_rpc_enable)) + .padding((0.0, 0.0, theme::grid(1.0), 0.0)), + ), + ) + .with_spacer(theme::grid(1.0)) + .with_child(make_input_row( + "Client ID", + "Enter your Discord Client ID", + AppState::config.then(Config::discord_rpc_client_id), + )) + } col.controller(Authenticate::new(tab)) } From b4a0fa56046a214ff4c3a882fc056202b799bba5 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:18:01 +0100 Subject: [PATCH 04/11] change to discord app id instead of client id --- psst-core/src/discord_rpc.rs | 4 ++-- psst-gui/src/controller/playback.rs | 20 +++++++++----------- psst-gui/src/data/config.rs | 4 ++-- psst-gui/src/ui/preferences.rs | 21 +++++++++++++++------ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/psst-core/src/discord_rpc.rs b/psst-core/src/discord_rpc.rs index 1147470e..a4d32efa 100644 --- a/psst-core/src/discord_rpc.rs +++ b/psst-core/src/discord_rpc.rs @@ -20,7 +20,7 @@ pub enum DiscordRpcCmd { }, Shutdown, Clear, - UpdateClientId(u64), + UpdateAppId(u64), } pub struct DiscordRPCClient { @@ -92,7 +92,7 @@ impl DiscordRPCClient { // Exit the loop break; } - DiscordRpcCmd::UpdateClientId(new_id) => { + DiscordRpcCmd::UpdateAppId(new_id) => { // take the old client out if let Some(old) = rpc.client.take() { if let Err(e) = old.shutdown() { diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 4db105ee..f7b4d4e3 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -71,22 +71,22 @@ fn init_scrobbler_instance(data: &AppState) -> Option { None } -fn parse_valid_client_id(id_str: &str) -> Option { +fn parse_valid_app_id(id_str: &str) -> Option { let trimmed = id_str.trim(); if trimmed.is_empty() { - log::info!("Discord RPC client ID not provided"); + log::info!("Discord RPC app ID not provided"); return None; } if !trimmed.chars().all(|c| c.is_ascii_digit()) { - log::warn!("Discord RPC client ID contains non-digit characters"); + log::warn!("Discord RPC app ID contains non-digit characters"); return None; } // Check if the client ID has a valid length for a snowflake 17-19 if !(17..=19).contains(&trimmed.len()) { log::warn!( - "Discord RPC client ID has invalid length ({} characters)", + "Discord RPC app ID has invalid length ({} characters)", trimmed.len() ); return None; @@ -95,7 +95,7 @@ fn parse_valid_client_id(id_str: &str) -> Option { match trimmed.parse::() { Ok(id) => Some(id), Err(e) => { - log::warn!("Failed to parse Discord RPC client ID '{}': {}", trimmed, e); + log::warn!("Failed to parse Discord RPC app ID '{}': {}", trimmed, e); None } } @@ -103,7 +103,7 @@ fn parse_valid_client_id(id_str: &str) -> Option { fn init_discord_rpc_instance(data: &AppState) -> Option> { if data.config.discord_rpc_enable { - if let Some(client_id) = parse_valid_client_id(&data.config.discord_rpc_client_id) { + if let Some(client_id) = parse_valid_app_id(&data.config.discord_rpc_app_id) { match DiscordRPCClient::spawn_rpc_worker(client_id) { Ok(sender) => Some(sender), Err(e) => { @@ -361,7 +361,7 @@ impl PlaybackController { fn reconcile_discord_rpc(&mut self, old: &AppState, new: &AppState, playback: &Playback) { let was_enabled = old.config.discord_rpc_enable; let is_enabled = new.config.discord_rpc_enable; - let id_changed = old.config.discord_rpc_client_id != new.config.discord_rpc_client_id; + let id_changed = old.config.discord_rpc_app_id != new.config.discord_rpc_app_id; match (was_enabled, is_enabled, self.discord_rpc_sender.is_some()) { // turned OFF @@ -386,10 +386,8 @@ impl PlaybackController { (_, true, true) if id_changed => { log::info!("Updating Discord RPC client ID"); if let Some(ref tx) = self.discord_rpc_sender { - if let Some(client_id) = - parse_valid_client_id(&new.config.discord_rpc_client_id) - { - let _ = tx.send(DiscordRpcCmd::UpdateClientId(client_id)); + if let Some(client_id) = parse_valid_app_id(&new.config.discord_rpc_app_id) { + let _ = tx.send(DiscordRpcCmd::UpdateAppId(client_id)); self.update_discord_rpc(playback); } else { log::warn!("Client ID changed but new value was invalid; not updating"); diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index eb8bdbd8..ebed01b3 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -124,7 +124,7 @@ pub struct Config { pub lastfm_api_key: Option, pub lastfm_api_secret: Option, pub lastfm_enable: bool, - pub discord_rpc_client_id: String, + pub discord_rpc_app_id: String, pub discord_rpc_enable: bool, } @@ -148,7 +148,7 @@ impl Default for Config { lastfm_api_key: None, lastfm_api_secret: None, lastfm_enable: false, - discord_rpc_client_id: String::new(), + discord_rpc_app_id: String::new(), discord_rpc_enable: true, } } diff --git a/psst-gui/src/ui/preferences.rs b/psst-gui/src/ui/preferences.rs index f1ac15ca..dd313878 100644 --- a/psst-gui/src/ui/preferences.rs +++ b/psst-gui/src/ui/preferences.rs @@ -362,9 +362,11 @@ fn account_tab_widget(tab: AccountTab) -> impl Widget { .with_child(Label::new("Discord RPC")) .with_spacer(theme::grid(1.0)) .with_child( - Label::new("Uses the Discord RPC socket to display currently playing music") - .with_text_color(theme::PLACEHOLDER_COLOR) - .with_line_break_mode(LineBreaking::WordWrap), + Label::new( + "Connects to a running Discord client to display currently playing music", + ) + .with_text_color(theme::PLACEHOLDER_COLOR) + .with_line_break_mode(LineBreaking::WordWrap), ) .with_spacer(theme::grid(2.0)) .with_child( @@ -376,10 +378,17 @@ fn account_tab_widget(tab: AccountTab) -> impl Widget { ) .with_spacer(theme::grid(1.0)) .with_child(make_input_row( - "Client ID", - "Enter your Discord Client ID", - AppState::config.then(Config::discord_rpc_client_id), + "App ID", + "Enter your Discord App ID", + AppState::config.then(Config::discord_rpc_app_id), )) + .with_child( + Button::new("Create a Discord App →") + .on_click(|_, _, _| { + open::that("https://discord.com/developers/applications").ok(); + }) + .padding((0.0, theme::grid(0.5))), + ) } col.controller(Authenticate::new(tab)) } From df9df9aa77274fed918c4d8cb5f8826542db8c13 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:46:42 +0100 Subject: [PATCH 05/11] fix edge case with app id state --- psst-gui/src/controller/playback.rs | 51 +++++++++++++---------------- psst-gui/src/data/config.rs | 2 +- psst-gui/src/ui/preferences.rs | 1 + 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index f7b4d4e3..fc236055 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -361,41 +361,36 @@ impl PlaybackController { fn reconcile_discord_rpc(&mut self, old: &AppState, new: &AppState, playback: &Playback) { let was_enabled = old.config.discord_rpc_enable; let is_enabled = new.config.discord_rpc_enable; - let id_changed = old.config.discord_rpc_app_id != new.config.discord_rpc_app_id; + let rpc_running = self.discord_rpc_sender.is_some(); + let app_id_changed = old.config.discord_rpc_app_id != new.config.discord_rpc_app_id; - match (was_enabled, is_enabled, self.discord_rpc_sender.is_some()) { - // turned OFF - (true, false, true) => { - // shutdown the RPC - log::info!("Shutting down Discord RPC"); - if let Some(ref tx) = self.discord_rpc_sender { - let _ = tx.send(DiscordRpcCmd::Shutdown); - } - // then drop the sender - self.discord_rpc_sender = None; + // Shut down if RPC was disabled + if was_enabled && !is_enabled && rpc_running { + log::info!("Shutting down Discord RPC"); + if let Some(ref tx) = self.discord_rpc_sender { + let _ = tx.send(DiscordRpcCmd::Shutdown); } + self.discord_rpc_sender = None; + } - // turned ON first time create a new sender - (false, true, false) => { - log::info!("Starting Discord RPC"); - self.discord_rpc_sender = init_discord_rpc_instance(new); - self.update_discord_rpc(playback) - } + // Start if RPC is enabled and no worker running + if is_enabled && !rpc_running { + log::info!("Starting Discord RPC"); + self.discord_rpc_sender = init_discord_rpc_instance(new); + self.update_discord_rpc(playback); + } - // still ON and ID changed update the app id - (_, true, true) if id_changed => { - log::info!("Updating Discord RPC client ID"); + // Update App ID if RPC is running and App ID changed + if is_enabled && rpc_running && app_id_changed { + if let Some(app_id) = parse_valid_app_id(&new.config.discord_rpc_app_id) { + log::info!("Updating Discord RPC app ID to {}", app_id); if let Some(ref tx) = self.discord_rpc_sender { - if let Some(client_id) = parse_valid_app_id(&new.config.discord_rpc_app_id) { - let _ = tx.send(DiscordRpcCmd::UpdateAppId(client_id)); - self.update_discord_rpc(playback); - } else { - log::warn!("Client ID changed but new value was invalid; not updating"); - } + let _ = tx.send(DiscordRpcCmd::UpdateAppId(app_id)); + self.update_discord_rpc(playback); } + } else { + log::warn!("App ID changed but new ID is invalid; not updating"); } - - _ => {} // nothing else to do } } diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index ebed01b3..91f43e8a 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -149,7 +149,7 @@ impl Default for Config { lastfm_api_secret: None, lastfm_enable: false, discord_rpc_app_id: String::new(), - discord_rpc_enable: true, + discord_rpc_enable: false, } } } diff --git a/psst-gui/src/ui/preferences.rs b/psst-gui/src/ui/preferences.rs index dd313878..2230d973 100644 --- a/psst-gui/src/ui/preferences.rs +++ b/psst-gui/src/ui/preferences.rs @@ -382,6 +382,7 @@ fn account_tab_widget(tab: AccountTab) -> impl Widget { "Enter your Discord App ID", AppState::config.then(Config::discord_rpc_app_id), )) + .with_spacer(theme::grid(1.0)) .with_child( Button::new("Create a Discord App →") .on_click(|_, _, _| { From d709f7bf39442427693c5d35428f31e8d941a5e5 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:23:24 +0100 Subject: [PATCH 06/11] add support for podcasts --- psst-gui/src/controller/playback.rs | 73 +++++++++++++++++------------ 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index fc236055..f96bf513 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -323,37 +323,52 @@ impl PlaybackController { .map_err(|e| log::error!("error clearing Discord RPC: {:?}", e)); } } - fn update_discord_rpc(&mut self, playback: &Playback) { if let Some(now_playing) = playback.now_playing.as_ref() { - if let Playable::Track(track) = &now_playing.item { - if let Some(discord_rpc_sender) = &mut self.discord_rpc_sender { - let artist = track.artist_name(); - let title = track.name.clone(); - let album = track.album.clone(); - let track_duration = track.duration; - let progress = now_playing.progress; - - log::info!("Updating Discord RPC with track: {}", title); - - let album_cover_url = album.as_ref().and_then(|a| { - a.images - .iter() - .find(|img| img.width == Some(64)) - .map(|img| img.url.as_ref()) - }); - let album_name = album.as_ref().map(|a| a.name.as_ref()); - let _ = discord_rpc_sender - .send(DiscordRpcCmd::Update { - track: title.clone(), - artist: artist.clone(), - album: album_name.map(str::to_owned), - cover_url: album_cover_url.map(str::to_owned), - duration: Some(track_duration), - position: Some(progress), - }) - .map_err(|e| log::error!("error updating Discord RPC: {:?}", e)); - } + if let Some(discord_rpc_sender) = &mut self.discord_rpc_sender { + let (title, artist, album_name, images, duration, progress) = + match &now_playing.item { + Playable::Track(track) => ( + track.name.clone(), + track.artist_name(), + track.album.as_ref().map(|a| &a.name), + track.album.as_ref().map(|a| &a.images), + track.duration, + now_playing.progress, + ), + Playable::Episode(episode) => ( + episode.name.clone(), + episode.show.name.clone(), + None, + Some(&episode.images), + episode.duration, + now_playing.progress, + ), + }; + + let album_cover_url = images.and_then(|imgs| { + imgs.iter() + .find(|img| img.width == Some(64)) + .or_else(|| imgs.get(0)) + .map(|img| img.url.as_ref()) + }); + + log::info!( + "Updating Discord RPC with track/episode: {} by {}", + title, + artist, + ); + + let _ = discord_rpc_sender + .send(DiscordRpcCmd::Update { + track: title.to_owned(), + artist: artist.to_owned(), + album: album_name.map(|a| a.as_ref().to_owned()), + cover_url: album_cover_url.map(str::to_owned), + duration: Some(duration), + position: Some(progress), + }) + .map_err(|e| log::error!("error updating Discord RPC: {:?}", e)); } } } From 4b414c8b818d5e3de2b1119b72a1ebeafad2e2b2 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:05:57 +0100 Subject: [PATCH 07/11] added taplo.toml The lsp defaults to 4 spaces for some reason without it --- .taplo.toml | 2 ++ psst-core/Cargo.toml | 8 ++++---- psst-gui/Cargo.toml | 14 +++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 .taplo.toml diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 00000000..d433d7fe --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,2 @@ +[formatting] +indent_string = " " diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index 0ef140e1..37876c70 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -48,12 +48,12 @@ cubeb = { git = "https://github.com/mozilla/cubeb-rs", optional = true } libsamplerate = { version = "0.1.0" } rb = { version = "0.4.1" } symphonia = { version = "0.5.4", default-features = false, features = [ - "ogg", - "vorbis", - "mp3", + "ogg", + "vorbis", + "mp3", ] } [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.58.0", features = [ - "Win32_System_Com", + "Win32_System_Com", ], default-features = false } diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index cb3612e8..b079f6ec 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -37,20 +37,20 @@ url = { version = "2.5.2" } # GUI druid = { git = "https://github.com/jpochyla/druid", branch = "psst", features = [ - "im", - "image", - "jpeg", - "png", - "serde", + "im", + "image", + "jpeg", + "png", + "serde", ] } druid-enums = { git = "https://github.com/jpochyla/druid-enums" } druid-shell = { git = "https://github.com/jpochyla/druid", branch = "psst", features = [ - "raw-win-handle", + "raw-win-handle", ] } open = { version = "5.3.0" } raw-window-handle = { version = "0.5.2" } # Must stay compatible with Druid souvlaki = { version = "0.7.3", default-features = false, features = [ - "use_zbus", + "use_zbus", ] } sanitize_html = "0.8.1" rustfm-scrobble = "1.1.1" From d7ef8b3dd7905ac62563990e2cc9504404af2099 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:12:17 +0100 Subject: [PATCH 08/11] Remove new lines --- psst-gui/src/controller/playback.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index f96bf513..c92c4cec 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -4,7 +4,6 @@ use std::{ }; use crossbeam_channel::Sender; - use druid::{ im::Vector, widget::{prelude::*, Controller}, @@ -20,7 +19,6 @@ use psst_core::{ session::SessionService, }; use rustfm_scrobble::Scrobbler; - use souvlaki::{ MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, }; From 2a096de8189f2a3e051100956da6bea5a3e1b20d Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:26:06 +0100 Subject: [PATCH 09/11] change all logs to lowercase --- psst-core/src/discord_rpc.rs | 4 ++-- psst-gui/src/controller/playback.rs | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/psst-core/src/discord_rpc.rs b/psst-core/src/discord_rpc.rs index a4d32efa..b975339c 100644 --- a/psst-core/src/discord_rpc.rs +++ b/psst-core/src/discord_rpc.rs @@ -41,7 +41,7 @@ impl DiscordRPCClient { pub fn create_client(app_id: u64) -> Result { let mut client = DiscordClient::new(app_id); client.start(); - log::info!("Discord RPC client created and started"); + log::info!("discord rpc client created and started"); Ok(client) } @@ -86,7 +86,7 @@ impl DiscordRPCClient { DiscordRpcCmd::Shutdown => { if let Some(client) = rpc.client.take() { if let Err(e) = client.shutdown() { - log::warn!("Shutdown failed: {}", e); + log::warn!("shutdown failed: {}", e); } } // Exit the loop diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index c92c4cec..dd7fbc0c 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -73,18 +73,18 @@ fn parse_valid_app_id(id_str: &str) -> Option { let trimmed = id_str.trim(); if trimmed.is_empty() { - log::info!("Discord RPC app ID not provided"); + log::info!("discord rpc app id not provided"); return None; } if !trimmed.chars().all(|c| c.is_ascii_digit()) { - log::warn!("Discord RPC app ID contains non-digit characters"); + log::warn!("discord rpc app id contains non-digit characters"); return None; } // Check if the client ID has a valid length for a snowflake 17-19 if !(17..=19).contains(&trimmed.len()) { log::warn!( - "Discord RPC app ID has invalid length ({} characters)", + "discord rpc app id has invalid length ({} characters)", trimmed.len() ); return None; @@ -93,7 +93,7 @@ fn parse_valid_app_id(id_str: &str) -> Option { match trimmed.parse::() { Ok(id) => Some(id), Err(e) => { - log::warn!("Failed to parse Discord RPC app ID '{}': {}", trimmed, e); + log::warn!("failed to parse discord rpc app id '{}': {}", trimmed, e); None } } @@ -105,7 +105,7 @@ fn init_discord_rpc_instance(data: &AppState) -> Option> { match DiscordRPCClient::spawn_rpc_worker(client_id) { Ok(sender) => Some(sender), Err(e) => { - log::warn!("Failed to create Discord RPC: {}", e); + log::warn!("failed to create discord rpc: {}", e); None } } @@ -113,7 +113,7 @@ fn init_discord_rpc_instance(data: &AppState) -> Option> { None } } else { - log::info!("Discord RPC is disabled"); + log::info!("discord rpc is disabled"); None } } @@ -315,10 +315,9 @@ impl PlaybackController { fn clear_discord_rpc(&mut self) { if let Some(sender) = &self.discord_rpc_sender { - // `sender` is &Sender let _ = sender .send(DiscordRpcCmd::Clear) - .map_err(|e| log::error!("error clearing Discord RPC: {:?}", e)); + .map_err(|e| log::error!("error clearing discord rpc: {:?}", e)); } } fn update_discord_rpc(&mut self, playback: &Playback) { @@ -352,7 +351,7 @@ impl PlaybackController { }); log::info!( - "Updating Discord RPC with track/episode: {} by {}", + "updating discord rpc with track/episode: {} by {}", title, artist, ); @@ -366,7 +365,7 @@ impl PlaybackController { duration: Some(duration), position: Some(progress), }) - .map_err(|e| log::error!("error updating Discord RPC: {:?}", e)); + .map_err(|e| log::error!("error updating discord rpc: {:?}", e)); } } } @@ -379,7 +378,7 @@ impl PlaybackController { // Shut down if RPC was disabled if was_enabled && !is_enabled && rpc_running { - log::info!("Shutting down Discord RPC"); + log::info!("shutting down discord rpc"); if let Some(ref tx) = self.discord_rpc_sender { let _ = tx.send(DiscordRpcCmd::Shutdown); } @@ -388,7 +387,7 @@ impl PlaybackController { // Start if RPC is enabled and no worker running if is_enabled && !rpc_running { - log::info!("Starting Discord RPC"); + log::info!("starting discord rpc"); self.discord_rpc_sender = init_discord_rpc_instance(new); self.update_discord_rpc(playback); } @@ -396,13 +395,13 @@ impl PlaybackController { // Update App ID if RPC is running and App ID changed if is_enabled && rpc_running && app_id_changed { if let Some(app_id) = parse_valid_app_id(&new.config.discord_rpc_app_id) { - log::info!("Updating Discord RPC app ID to {}", app_id); + log::info!("updating discord rpc app id to {}", app_id); if let Some(ref tx) = self.discord_rpc_sender { let _ = tx.send(DiscordRpcCmd::UpdateAppId(app_id)); self.update_discord_rpc(playback); } } else { - log::warn!("App ID changed but new ID is invalid; not updating"); + log::warn!("app id changed but new id is invalid; not updating"); } } } From e3fa1645077e7b69350e040ea10d769317f81772 Mon Sep 17 00:00:00 2001 From: ThePotato97 <10260415+ThePotato97@users.noreply.github.com> Date: Sun, 27 Apr 2025 14:01:32 +0100 Subject: [PATCH 10/11] removed unused crate --- psst-gui/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index b079f6ec..a28a92d3 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -54,7 +54,6 @@ souvlaki = { version = "0.7.3", default-features = false, features = [ ] } sanitize_html = "0.8.1" rustfm-scrobble = "1.1.1" -discord-presence = "1.5" [target.'cfg(windows)'.build-dependencies] winres = { version = "0.1.12" } From b864b70588d82bde6bc80c72afb8e6ec42b8cb9d Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 17 Jun 2025 23:24:22 +0100 Subject: [PATCH 11/11] Fix #610 --- psst-gui/src/webapi/client.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index bdf36b46..fce2ad7a 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -942,9 +942,12 @@ impl WebApi { .query("market", "from_token"); let mut results = Vector::new(); - self.for_all_pages(request, |page: Page| { + self.for_all_pages(request, |page: Page>| { if !page.items.is_empty() { - let ids = page.items.into_iter().map(|link| link.id); + let ids = page + .items + .into_iter() + .filter_map(|link| link.map(|link| link.id)); let episodes = self.get_episodes(ids)?; results.append(episodes); }