From 5576fb0d13c064a484945295a3360d8737c4be36 Mon Sep 17 00:00:00 2001 From: kveld9 Date: Thu, 16 Apr 2026 21:41:22 -0300 Subject: [PATCH] fix DRP timer --- src-tauri/src/discord.rs | 51 ++++++++++++++++++++++------------------ src/store/playerStore.ts | 12 ++++++---- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src-tauri/src/discord.rs b/src-tauri/src/discord.rs index b4bb1c57..fc4a8aee 100644 --- a/src-tauri/src/discord.rs +++ b/src-tauri/src/discord.rs @@ -34,6 +34,10 @@ pub struct DiscordState { pub artwork_cache: Arc>>, /// HTTP client for iTunes API requests. blocking::Client is Clone (Arc-internally). pub http_client: Client, + /// Last calculated start timestamp for the current track (for freezing timer when paused). + pub last_start_timestamp: Mutex>, + /// Track whether we were playing in the last update to detect transitions. + pub was_playing: Mutex, } impl DiscordState { @@ -45,6 +49,8 @@ impl DiscordState { .timeout(std::time::Duration::from_secs(5)) .build() .unwrap_or_else(|_| Client::new()), + last_start_timestamp: Mutex::new(None), + was_playing: Mutex::new(false), } } } @@ -219,7 +225,7 @@ fn try_connect() -> Option { /// Apply a template string, replacing placeholders with actual values. /// Supported placeholders: {title}, {artist}, {album}, {paused} fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>, is_playing: bool) -> String { - let paused_text = if is_playing { "" } else { "Paused — " }; + let paused_text = if is_playing { "" } else { "(Paused) " }; let album_text = album.unwrap_or(""); template @@ -252,6 +258,7 @@ pub async fn discord_update_presence( album: Option, is_playing: bool, elapsed_secs: Option, + duration_secs: Option, cover_art_url: Option, fetch_itunes_covers: bool, details_template: Option, @@ -316,38 +323,33 @@ pub async fn discord_update_presence( .large_text(&large_text) }; - // ActivityType::Listening causes the Discord client to auto-start a running - // timer from "now" even when no timestamps are provided. Switch to Playing - // when paused — Playing only shows a timer when timestamps are explicitly set. - let activity_type = if is_playing { - ActivityType::Listening - } else { - ActivityType::Playing - }; + // When paused: clear activity completely to avoid any timer issues + // When playing: show full activity with timer + if !is_playing { + if client.clear_activity().is_err() { + *guard = None; + } + return Ok(()); + } - let mut activity = Activity::new() - .activity_type(activity_type) + // Only reach here when playing + let activity = Activity::new() + .activity_type(ActivityType::Listening) .details(&details_text) .state(&state_text) - .assets(assets); - - // Start timestamp: Discord auto-counts up from this point. We back-calculate - // it so the displayed elapsed time matches the actual playback position. - // Only set when playing — Playing type without timestamps shows no timer. - if is_playing { - if let Some(elapsed) = elapsed_secs { + .assets(assets) + .timestamps(if let Some(elapsed) = elapsed_secs { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs() as i64; let start = now - elapsed.floor() as i64; - activity = activity.timestamps(Timestamps::new().start(start)); - } - } + Timestamps::new().start(start) + } else { + Timestamps::new() + }); if client.set_activity(activity).is_err() { - // IPC pipe broke (Discord restarted etc.) — drop the client so the next - // call re-connects. *guard = None; } @@ -363,5 +365,8 @@ pub fn discord_clear_presence(state: tauri::State) -> Result<(), S *guard = None; } } + // Clear saved timestamp so next track starts fresh + let mut start_guard = state.last_start_timestamp.lock().unwrap(); + *start_guard = None; Ok(()) } diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 82429307..06545f4c 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -692,6 +692,7 @@ export function initAudioListeners(): () => void { let discordPrevTemplateDetails: string | null = null; let discordPrevTemplateState: string | null = null; let discordPrevTemplateLargeText: string | null = null; + let discordPrevCurrentTime: number | null = null; function syncDiscord() { const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState(); @@ -722,7 +723,8 @@ export function initAudioListeners(): () => void { const detailsTemplateChanged = discordTemplateDetails !== discordPrevTemplateDetails; const stateTemplateChanged = discordTemplateState !== discordPrevTemplateState; const largeTextTemplateChanged = discordTemplateLargeText !== discordPrevTemplateLargeText; - if (!trackChanged && !playingChanged && !coversSettingChanged && !detailsTemplateChanged && !stateTemplateChanged && !largeTextTemplateChanged) return; + const timeChanged = Math.abs(currentTime - (discordPrevCurrentTime ?? -1)) > 1; // Only update if time changed by more than 1 second + if (!trackChanged && !playingChanged && !coversSettingChanged && !detailsTemplateChanged && !stateTemplateChanged && !largeTextTemplateChanged && !timeChanged) return; discordPrevTrackId = currentTrack.id; discordPrevIsPlaying = isPlaying; @@ -730,15 +732,17 @@ export function initAudioListeners(): () => void { discordPrevTemplateDetails = discordTemplateDetails; discordPrevTemplateState = discordTemplateState; discordPrevTemplateLargeText = discordTemplateLargeText; + discordPrevCurrentTime = currentTime; invoke('discord_update_presence', { title: currentTrack.title, artist: currentTrack.artist ?? 'Unknown Artist', album: currentTrack.album ?? null, isPlaying, - // Pass elapsed when playing so Discord shows a live running timer. - // Pass null when paused — Discord clears the timer. - elapsedSecs: isPlaying ? currentTime : null, + // Always pass elapsedSecs so backend can handle both playing and paused states. + // Backend will freeze timer when paused using end timestamp. + elapsedSecs: currentTime, + durationSecs: currentTrack.duration, // coverArtUrl is intentionally not passed — Subsonic URLs require auth. // iTunes cover fetching is only done when explicitly opted in. coverArtUrl: null,