fix(discord): add Apple Music cover opt-in, show Paused state

- iTunes Search API calls are now gated behind enableAppleMusicCoversDiscord
  (default: false). No external requests to Apple are made unless the user
  explicitly enables the new opt-in toggle in Settings.
- When the toggle is flipped, Discord presence is immediately re-sent for
  the current track (coversSettingChanged bypasses the early-return guard).
- When paused, activity type switches to Playing (no auto-timer) and state
  text shows "Paused" instead of the artist name.
- New authStore field + setter; new Settings toggle shown only when Discord
  RPC is enabled; i18n keys added to all 7 languages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-07 20:33:16 +02:00
parent 4b8b6ae797
commit 9ef8566d64
11 changed files with 97 additions and 29 deletions
+2
View File
@@ -437,6 +437,8 @@ export const deTranslation = {
minimizeToTrayDesc: 'Beim Schließen des Fensters läuft Psysonic weiter im System-Tray statt zu beenden.',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Zeigt den aktuell gespielten Titel im Discord-Profil an. Discord muss dafür geöffnet sein.',
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.',
nowPlayingEnabled: 'Im Livefenster anzeigen',
nowPlayingEnabledDesc: 'Überträgt den aktuell gespielten Titel an die Livehörer-Ansicht des Servers. Deaktivieren, um keine Wiedergabedaten zu senden.',
lyricsServerFirst: 'Server-Lyrics bevorzugen',
+2
View File
@@ -438,6 +438,8 @@ export const enTranslation = {
minimizeToTrayDesc: 'When closing the window, keep Psysonic running in the system tray instead of quitting.',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Show the currently playing track on your Discord profile. Requires Discord to be running.',
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.',
nowPlayingEnabled: 'Show in Now Playing',
nowPlayingEnabledDesc: 'Broadcast your currently playing track to the server\'s live listener view. Disable to stop sending playback data.',
lyricsServerFirst: 'Prefer server lyrics',
+2
View File
@@ -437,6 +437,8 @@ export const frTranslation = {
minimizeToTrayDesc: 'Lors de la fermeture, Psysonic continue de fonctionner dans la barre système au lieu de se fermer.',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Affiche le titre en cours de lecture sur votre profil Discord. Discord doit être ouvert.',
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é.',
nowPlayingEnabled: 'Afficher dans la fenêtre live',
nowPlayingEnabledDesc: 'Diffuse le titre en cours de lecture vers la vue des auditeurs en direct du serveur. Désactiver pour ne pas envoyer de données de lecture.',
lyricsServerFirst: 'Préférer les paroles du serveur',
+2
View File
@@ -436,6 +436,8 @@ export const nbTranslation = {
minimizeToTrayDesc: 'Når vinduet lukkes, vil Psysonic bli kjørende i oppgavelinjen fremfor å bli avsluttet.',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Vis sporet som spilles i din Discord-profil. Krever at Discord kjører.',
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.',
nowPlayingEnabled: 'Vis i "Nå spiller"',
nowPlayingEnabledDesc: 'Send sporet som spilles av til tjenerens live-lyttervisning. Deaktiver for å stoppe sending av avspillingsdata.',
lyricsServerFirst: 'Foretrekk server-sangtekst',
+2
View File
@@ -437,6 +437,8 @@ export const nlTranslation = {
minimizeToTrayDesc: 'Bij het sluiten van het venster blijft Psysonic actief in het systeemvak in plaats van af te sluiten.',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: 'Toont het huidige nummer op je Discord-profiel. Discord moet daarvoor geopend zijn.',
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.',
nowPlayingEnabled: 'Weergeven in live-venster',
nowPlayingEnabledDesc: 'Stuurt het huidige nummer naar de live-luisteraarsweergave van de server. Uitschakelen om geen afspeelgegevens te verzenden.',
lyricsServerFirst: 'Server-songtekst voorrang geven',
+2
View File
@@ -456,6 +456,8 @@ export const ruTranslation = {
discordRichPresence: 'Статус в Discord',
discordRichPresenceDesc:
'Показывать текущий трек в профиле и статусе Discord. Нужен запущенный клиент Discord.',
discordAppleCovers: 'Загружать обложки через Apple Music для Discord',
discordAppleCoversDesc: 'Отправляет имя исполнителя и альбома в API поиска Apple для поиска обложки в профиле Discord. По умолчанию отключено из соображений конфиденциальности.',
nowPlayingEnabled: 'Показывать в «Сейчас играет»',
nowPlayingEnabledDesc:
'Отправлять на сервер, что вы сейчас слушаете. Отключите, чтобы не делиться этим.',
+2
View File
@@ -433,6 +433,8 @@ export const zhTranslation = {
minimizeToTrayDesc: '关闭窗口时,Psysonic 将继续在系统托盘中运行,而不是退出。',
discordRichPresence: 'Discord Rich Presence',
discordRichPresenceDesc: '在 Discord 个人资料上显示当前播放的曲目。需要 Discord 处于运行状态。',
discordAppleCovers: '通过 Apple Music 为 Discord 获取封面',
discordAppleCoversDesc: '将艺术家和专辑名称发送至 Apple 搜索 API,以为 Discord 个人资料查找封面图片。出于隐私考虑,默认禁用。',
nowPlayingEnabled: '在实时窗口中显示',
nowPlayingEnabledDesc: '将当前播放的曲目广播到服务器的实时听众视图。禁用以停止发送播放数据。',
lyricsServerFirst: '优先使用服务器歌词',
+15
View File
@@ -637,6 +637,21 @@ export default function Settings() {
<span className="toggle-track" />
</label>
</div>
{auth.discordRichPresence && (
<>
<div className="settings-section-divider" />
<div className="settings-toggle-row" style={{ paddingLeft: 16 }}>
<div>
<div style={{ fontWeight: 500 }}>{t('settings.discordAppleCovers')}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.discordAppleCoversDesc')}</div>
</div>
<label className="toggle-switch" aria-label={t('settings.discordAppleCovers')}>
<input type="checkbox" checked={auth.enableAppleMusicCoversDiscord} onChange={e => auth.setEnableAppleMusicCoversDiscord(e.target.checked)} />
<span className="toggle-track" />
</label>
</div>
</>
)}
<div className="settings-section-divider" />
<div className="settings-toggle-row">
<div>
+4
View File
@@ -41,6 +41,7 @@ interface AuthState {
showTrayIcon: boolean;
minimizeToTray: boolean;
discordRichPresence: boolean;
enableAppleMusicCoversDiscord: boolean;
nowPlayingEnabled: boolean;
lyricsServerFirst: boolean;
showFullscreenLyrics: boolean;
@@ -103,6 +104,7 @@ interface AuthState {
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setDiscordRichPresence: (v: boolean) => void;
setEnableAppleMusicCoversDiscord: (v: boolean) => void;
setNowPlayingEnabled: (v: boolean) => void;
setLyricsServerFirst: (v: boolean) => void;
setShowFullscreenLyrics: (v: boolean) => void;
@@ -153,6 +155,7 @@ export const useAuthStore = create<AuthState>()(
showTrayIcon: true,
minimizeToTray: false,
discordRichPresence: false,
enableAppleMusicCoversDiscord: false,
nowPlayingEnabled: false,
lyricsServerFirst: true,
showFullscreenLyrics: true,
@@ -236,6 +239,7 @@ export const useAuthStore = create<AuthState>()(
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setDiscordRichPresence: (v) => set({ discordRichPresence: v }),
setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }),
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
setShowFullscreenLyrics: (v: boolean) => set({ showFullscreenLyrics: v }),
+10 -4
View File
@@ -505,15 +505,17 @@ export function initAudioListeners(): () => void {
// Discord auto-counts up the elapsed timer from the start_timestamp we set.
let discordPrevTrackId: string | null = null;
let discordPrevIsPlaying: boolean | null = null;
let discordPrevFetchCovers: boolean | null = null;
function syncDiscord() {
const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState();
const { discordRichPresence } = useAuthStore.getState();
const { discordRichPresence, enableAppleMusicCoversDiscord } = useAuthStore.getState();
if (!discordRichPresence || !currentTrack) {
if (discordPrevTrackId !== null) {
discordPrevTrackId = null;
discordPrevIsPlaying = null;
discordPrevFetchCovers = null;
invoke('discord_clear_presence').catch(() => {});
}
return;
@@ -521,21 +523,25 @@ export function initAudioListeners(): () => void {
const trackChanged = currentTrack.id !== discordPrevTrackId;
const playingChanged = isPlaying !== discordPrevIsPlaying;
if (!trackChanged && !playingChanged) return;
const coversSettingChanged = enableAppleMusicCoversDiscord !== discordPrevFetchCovers;
if (!trackChanged && !playingChanged && !coversSettingChanged) return;
discordPrevTrackId = currentTrack.id;
discordPrevIsPlaying = isPlaying;
discordPrevFetchCovers = enableAppleMusicCoversDiscord;
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 shows the song/artist without a timer.
// Pass null when paused — Discord clears the timer.
elapsedSecs: isPlaying ? currentTime : null,
// coverArtUrl is intentionally not passed — Subsonic URLs require auth.
// Backend will fetch artwork from iTunes Search API instead.
// iTunes cover fetching is only done when explicitly opted in.
coverArtUrl: null,
fetchItunesCovers: enableAppleMusicCoversDiscord,
}).catch(() => {});
}