fix(discord): remove dead fields, timeChanged invoke loop, and unsupported {paused} placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-17 15:33:58 +02:00
parent 5576fb0d13
commit c50addaacf
10 changed files with 18 additions and 37 deletions
+8 -21
View File
@@ -34,10 +34,6 @@ pub struct DiscordState {
pub artwork_cache: Arc<Mutex<HashMap<String, ArtworkCacheEntry>>>, pub artwork_cache: Arc<Mutex<HashMap<String, ArtworkCacheEntry>>>,
/// HTTP client for iTunes API requests. blocking::Client is Clone (Arc-internally). /// HTTP client for iTunes API requests. blocking::Client is Clone (Arc-internally).
pub http_client: Client, pub http_client: Client,
/// Last calculated start timestamp for the current track (for freezing timer when paused).
pub last_start_timestamp: Mutex<Option<i64>>,
/// Track whether we were playing in the last update to detect transitions.
pub was_playing: Mutex<bool>,
} }
impl DiscordState { impl DiscordState {
@@ -49,8 +45,6 @@ impl DiscordState {
.timeout(std::time::Duration::from_secs(5)) .timeout(std::time::Duration::from_secs(5))
.build() .build()
.unwrap_or_else(|_| Client::new()), .unwrap_or_else(|_| Client::new()),
last_start_timestamp: Mutex::new(None),
was_playing: Mutex::new(false),
} }
} }
} }
@@ -223,16 +217,13 @@ fn try_connect() -> Option<DiscordIpcClient> {
} }
/// Apply a template string, replacing placeholders with actual values. /// Apply a template string, replacing placeholders with actual values.
/// Supported placeholders: {title}, {artist}, {album}, {paused} /// Supported placeholders: {title}, {artist}, {album}
fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>, is_playing: bool) -> String { fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>) -> String {
let paused_text = if is_playing { "" } else { "(Paused) " };
let album_text = album.unwrap_or(""); let album_text = album.unwrap_or("");
template template
.replace("{title}", title) .replace("{title}", title)
.replace("{artist}", artist) .replace("{artist}", artist)
.replace("{album}", album_text) .replace("{album}", album_text)
.replace("{paused}", paused_text)
} }
/// Update the Discord Rich Presence activity. /// Update the Discord Rich Presence activity.
@@ -245,11 +236,11 @@ fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>
/// `cover_art_url` is provided. If false (default), fall back to the Psysonic app icon /// `cover_art_url` is provided. If false (default), fall back to the Psysonic app icon
/// without making any external request — required for privacy opt-in. /// without making any external request — required for privacy opt-in.
/// - `details_template`: template string for the "details" field. Default: "{artist} - {title}". /// - `details_template`: template string for the "details" field. Default: "{artist} - {title}".
/// Supported placeholders: {title}, {artist}, {album}, {paused} /// Supported placeholders: {title}, {artist}, {album}
/// - `state_template`: template string for the "state" field. Default: "{album}". /// - `state_template`: template string for the "state" field. Default: "{album}".
/// Supported placeholders: {title}, {artist}, {album}, {paused} /// Supported placeholders: {title}, {artist}, {album}
/// - `large_text_template`: template string for the large image tooltip. Default: "{album}". /// - `large_text_template`: template string for the large image tooltip. Default: "{album}".
/// Supported placeholders: {title}, {artist}, {album}, {paused} /// Supported placeholders: {title}, {artist}, {album}
#[tauri::command] #[tauri::command]
pub async fn discord_update_presence( pub async fn discord_update_presence(
state: tauri::State<'_, DiscordState>, state: tauri::State<'_, DiscordState>,
@@ -258,7 +249,6 @@ pub async fn discord_update_presence(
album: Option<String>, album: Option<String>,
is_playing: bool, is_playing: bool,
elapsed_secs: Option<f64>, elapsed_secs: Option<f64>,
duration_secs: Option<f64>,
cover_art_url: Option<String>, cover_art_url: Option<String>,
fetch_itunes_covers: bool, fetch_itunes_covers: bool,
details_template: Option<String>, details_template: Option<String>,
@@ -304,13 +294,13 @@ pub async fn discord_update_presence(
// Apply templates for the three configurable text fields. // Apply templates for the three configurable text fields.
let details_str = details_template.as_deref().unwrap_or("{artist} - {title}"); let details_str = details_template.as_deref().unwrap_or("{artist} - {title}");
let details_text = apply_template(details_str, &title, &artist, album.as_deref(), is_playing); let details_text = apply_template(details_str, &title, &artist, album.as_deref());
let state_str = state_template.as_deref().unwrap_or("{album}"); let state_str = state_template.as_deref().unwrap_or("{album}");
let state_text = apply_template(state_str, &title, &artist, album.as_deref(), is_playing); let state_text = apply_template(state_str, &title, &artist, album.as_deref());
let large_text_str = large_text_template.as_deref().unwrap_or("{album}"); let large_text_str = large_text_template.as_deref().unwrap_or("{album}");
let large_text = apply_template(large_text_str, &title, &artist, album.as_deref(), is_playing); let large_text = apply_template(large_text_str, &title, &artist, album.as_deref());
let assets = if let Some(ref url) = artwork_url { let assets = if let Some(ref url) = artwork_url {
Assets::new() Assets::new()
@@ -365,8 +355,5 @@ pub fn discord_clear_presence(state: tauri::State<DiscordState>) -> Result<(), S
*guard = None; *guard = None;
} }
} }
// Clear saved timestamp so next track starts fresh
let mut start_guard = state.last_start_timestamp.lock().unwrap();
*start_guard = None;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -540,7 +540,7 @@ export const deTranslation = {
discordAppleCovers: 'Cover über Apple Music für Discord laden', discordAppleCovers: 'Cover über Apple Music für Discord laden',
discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.', discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.',
discordTemplates: 'Benutzerdefinierte Textvorlagen', discordTemplates: 'Benutzerdefinierte Textvorlagen',
discordTemplatesDesc: 'Passen Sie an, welche Informationen in Ihrem Discord-Profil angezeigt werden. Variablen: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Passen Sie an, welche Informationen in Ihrem Discord-Profil angezeigt werden. Variablen: {title}, {artist}, {album}',
discordTemplateDetails: 'Primäre Zeile (details)', discordTemplateDetails: 'Primäre Zeile (details)',
discordTemplateState: 'Sekundäre Zeile (state)', discordTemplateState: 'Sekundäre Zeile (state)',
discordTemplateLargeText: 'Album-Tooltip (largeText)', discordTemplateLargeText: 'Album-Tooltip (largeText)',
+1 -1
View File
@@ -542,7 +542,7 @@ export const enTranslation = {
discordAppleCovers: 'Fetch covers from Apple Music for Discord', discordAppleCovers: 'Fetch covers from Apple Music for Discord',
discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.', discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.',
discordTemplates: 'Custom text templates', discordTemplates: 'Custom text templates',
discordTemplatesDesc: 'Customize what information is shown on your Discord profile. Variables: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Customize what information is shown on your Discord profile. Variables: {title}, {artist}, {album}',
discordTemplateDetails: 'Primary line (details)', discordTemplateDetails: 'Primary line (details)',
discordTemplateState: 'Secondary line (state)', discordTemplateState: 'Secondary line (state)',
discordTemplateLargeText: 'Album tooltip (largeText)', discordTemplateLargeText: 'Album tooltip (largeText)',
+1 -1
View File
@@ -543,7 +543,7 @@ export const esTranslation = {
discordAppleCovers: 'Obtener portadas de Apple Music para Discord', discordAppleCovers: 'Obtener portadas de Apple Music para Discord',
discordAppleCoversDesc: 'Envía el artista y nombre del álbum a la API de búsqueda de Apple para encontrar portadas para tu perfil de Discord. Desactivado por defecto por privacidad.', discordAppleCoversDesc: 'Envía el artista y nombre del álbum a la API de búsqueda de Apple para encontrar portadas para tu perfil de Discord. Desactivado por defecto por privacidad.',
discordTemplates: 'Plantillas de texto personalizadas', discordTemplates: 'Plantillas de texto personalizadas',
discordTemplatesDesc: 'Personaliza qué información se muestra en tu perfil de Discord. Variables: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Personaliza qué información se muestra en tu perfil de Discord. Variables: {title}, {artist}, {album}',
discordTemplateDetails: 'Línea principal (details)', discordTemplateDetails: 'Línea principal (details)',
discordTemplateState: 'Línea secundaria (state)', discordTemplateState: 'Línea secundaria (state)',
discordTemplateLargeText: 'Tooltip del álbum (largeText)', discordTemplateLargeText: 'Tooltip del álbum (largeText)',
+1 -1
View File
@@ -538,7 +538,7 @@ export const frTranslation = {
discordAppleCovers: 'Récupérer les pochettes via Apple Music pour Discord', discordAppleCovers: 'Récupérer les pochettes via Apple Music pour Discord',
discordAppleCoversDesc: 'Envoie le nom de l\'artiste et de l\'album à l\'API Apple pour trouver une pochette pour votre profil Discord. Désactivé par défaut pour des raisons de confidentialité.', discordAppleCoversDesc: 'Envoie le nom de l\'artiste et de l\'album à l\'API Apple pour trouver une pochette pour votre profil Discord. Désactivé par défaut pour des raisons de confidentialité.',
discordTemplates: 'Modèles de texte personnalisés', discordTemplates: 'Modèles de texte personnalisés',
discordTemplatesDesc: 'Personnalisez les informations affichées sur votre profil Discord. Variables : {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Personnalisez les informations affichées sur votre profil Discord. Variables : {title}, {artist}, {album}',
discordTemplateDetails: 'Ligne principale (details)', discordTemplateDetails: 'Ligne principale (details)',
discordTemplateState: 'Ligne secondaire (state)', discordTemplateState: 'Ligne secondaire (state)',
discordTemplateLargeText: 'Info-bulle album (largeText)', discordTemplateLargeText: 'Info-bulle album (largeText)',
+1 -1
View File
@@ -537,7 +537,7 @@ export const nbTranslation = {
discordAppleCovers: 'Hent covere fra Apple Music til Discord', discordAppleCovers: 'Hent covere fra Apple Music til Discord',
discordAppleCoversDesc: 'Sender artist- og albumnavn til Apples søke-API for å finne cover til Discord-profilen din. Deaktivert som standard av personvernhensyn.', discordAppleCoversDesc: 'Sender artist- og albumnavn til Apples søke-API for å finne cover til Discord-profilen din. Deaktivert som standard av personvernhensyn.',
discordTemplates: 'Egendefinerte tekstmaler', discordTemplates: 'Egendefinerte tekstmaler',
discordTemplatesDesc: 'Tilpass hvilken informasjon som vises på Discord-profilen din. Variabler: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Tilpass hvilken informasjon som vises på Discord-profilen din. Variabler: {title}, {artist}, {album}',
discordTemplateDetails: 'Primær linje (details)', discordTemplateDetails: 'Primær linje (details)',
discordTemplateState: 'Sekundær linje (state)', discordTemplateState: 'Sekundær linje (state)',
discordTemplateLargeText: 'Album-verktøytips (largeText)', discordTemplateLargeText: 'Album-verktøytips (largeText)',
+1 -1
View File
@@ -537,7 +537,7 @@ export const nlTranslation = {
discordAppleCovers: 'Hoezen ophalen via Apple Music voor Discord', discordAppleCovers: 'Hoezen ophalen via Apple Music voor Discord',
discordAppleCoversDesc: 'Stuurt artiest- en albumnaam naar de Apple-zoek-API om een hoes te vinden voor je Discord-profiel. Standaard uitgeschakeld vanwege privacy.', discordAppleCoversDesc: 'Stuurt artiest- en albumnaam naar de Apple-zoek-API om een hoes te vinden voor je Discord-profiel. Standaard uitgeschakeld vanwege privacy.',
discordTemplates: 'Aangepaste tekstsjablonen', discordTemplates: 'Aangepaste tekstsjablonen',
discordTemplatesDesc: 'Pas aan welke informatie wordt weergegeven op je Discord-profiel. Variabelen: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Pas aan welke informatie wordt weergegeven op je Discord-profiel. Variabelen: {title}, {artist}, {album}',
discordTemplateDetails: 'Primaire regel (details)', discordTemplateDetails: 'Primaire regel (details)',
discordTemplateState: 'Secundaire regel (state)', discordTemplateState: 'Secundaire regel (state)',
discordTemplateLargeText: 'Album-tooltip (largeText)', discordTemplateLargeText: 'Album-tooltip (largeText)',
+1 -1
View File
@@ -560,7 +560,7 @@ export const ruTranslation = {
discordAppleCovers: 'Загружать обложки через Apple Music для Discord', discordAppleCovers: 'Загружать обложки через Apple Music для Discord',
discordAppleCoversDesc: 'Отправляет имя исполнителя и альбома в Apple Music API для поиска обложки для профиля Discord. По умолчанию отключено из соображений конфиденциальности.', discordAppleCoversDesc: 'Отправляет имя исполнителя и альбома в Apple Music API для поиска обложки для профиля Discord. По умолчанию отключено из соображений конфиденциальности.',
discordTemplates: 'Пользовательские шаблоны текста', discordTemplates: 'Пользовательские шаблоны текста',
discordTemplatesDesc: 'Настройте, какая информация отображается в профиле Discord. Переменные: {title}, {artist}, {album}, {paused}', discordTemplatesDesc: 'Настройте, какая информация отображается в профиле Discord. Переменные: {title}, {artist}, {album}',
discordTemplateDetails: 'Основная строка (details)', discordTemplateDetails: 'Основная строка (details)',
discordTemplateState: 'Вторичная строка (state)', discordTemplateState: 'Вторичная строка (state)',
discordTemplateLargeText: 'Подсказка альбома (largeText)', discordTemplateLargeText: 'Подсказка альбома (largeText)',
+1 -1
View File
@@ -533,7 +533,7 @@ export const zhTranslation = {
discordAppleCovers: '通过 Apple Music 为 Discord 获取封面', discordAppleCovers: '通过 Apple Music 为 Discord 获取封面',
discordAppleCoversDesc: '将艺术家和专辑名称发送至 Apple 搜索 API,以为 Discord 个人资料查找封面图片。出于隐私考虑,默认禁用。', discordAppleCoversDesc: '将艺术家和专辑名称发送至 Apple 搜索 API,以为 Discord 个人资料查找封面图片。出于隐私考虑,默认禁用。',
discordTemplates: '自定义文本模板', discordTemplates: '自定义文本模板',
discordTemplatesDesc: '自定义在 Discord 个人资料上显示的信息。变量:{title}, {artist}, {album}, {paused}', discordTemplatesDesc: '自定义在 Discord 个人资料上显示的信息。变量:{title}, {artist}, {album}',
discordTemplateDetails: '主行 (details)', discordTemplateDetails: '主行 (details)',
discordTemplateState: '副行 (state)', discordTemplateState: '副行 (state)',
discordTemplateLargeText: '专辑提示 (largeText)', discordTemplateLargeText: '专辑提示 (largeText)',
+2 -8
View File
@@ -692,7 +692,6 @@ export function initAudioListeners(): () => void {
let discordPrevTemplateDetails: string | null = null; let discordPrevTemplateDetails: string | null = null;
let discordPrevTemplateState: string | null = null; let discordPrevTemplateState: string | null = null;
let discordPrevTemplateLargeText: string | null = null; let discordPrevTemplateLargeText: string | null = null;
let discordPrevCurrentTime: number | null = null;
function syncDiscord() { function syncDiscord() {
const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState(); const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState();
@@ -723,8 +722,7 @@ export function initAudioListeners(): () => void {
const detailsTemplateChanged = discordTemplateDetails !== discordPrevTemplateDetails; const detailsTemplateChanged = discordTemplateDetails !== discordPrevTemplateDetails;
const stateTemplateChanged = discordTemplateState !== discordPrevTemplateState; const stateTemplateChanged = discordTemplateState !== discordPrevTemplateState;
const largeTextTemplateChanged = discordTemplateLargeText !== discordPrevTemplateLargeText; const largeTextTemplateChanged = discordTemplateLargeText !== discordPrevTemplateLargeText;
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) return;
if (!trackChanged && !playingChanged && !coversSettingChanged && !detailsTemplateChanged && !stateTemplateChanged && !largeTextTemplateChanged && !timeChanged) return;
discordPrevTrackId = currentTrack.id; discordPrevTrackId = currentTrack.id;
discordPrevIsPlaying = isPlaying; discordPrevIsPlaying = isPlaying;
@@ -732,17 +730,13 @@ export function initAudioListeners(): () => void {
discordPrevTemplateDetails = discordTemplateDetails; discordPrevTemplateDetails = discordTemplateDetails;
discordPrevTemplateState = discordTemplateState; discordPrevTemplateState = discordTemplateState;
discordPrevTemplateLargeText = discordTemplateLargeText; discordPrevTemplateLargeText = discordTemplateLargeText;
discordPrevCurrentTime = currentTime;
invoke('discord_update_presence', { invoke('discord_update_presence', {
title: currentTrack.title, title: currentTrack.title,
artist: currentTrack.artist ?? 'Unknown Artist', artist: currentTrack.artist ?? 'Unknown Artist',
album: currentTrack.album ?? null, album: currentTrack.album ?? null,
isPlaying, isPlaying,
// Always pass elapsedSecs so backend can handle both playing and paused states. elapsedSecs: isPlaying ? currentTime : null,
// Backend will freeze timer when paused using end timestamp.
elapsedSecs: currentTime,
durationSecs: currentTrack.duration,
// coverArtUrl is intentionally not passed — Subsonic URLs require auth. // coverArtUrl is intentionally not passed — Subsonic URLs require auth.
// iTunes cover fetching is only done when explicitly opted in. // iTunes cover fetching is only done when explicitly opted in.
coverArtUrl: null, coverArtUrl: null,