mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(radio): forward ICY StreamTitle to MPRIS metadata
When a radio stream is active, push the station name to MPRIS on start and forward ICY StreamTitle updates (radio:metadata events) so the OS now-playing overlay stays in sync. Skips ad metadata (is_ad flag) and suppresses position ticks for radio streams (no meaningful duration). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -597,15 +597,17 @@ export function initAudioListeners(): () => void {
|
||||
// Whenever the current track or playback state changes, push updates to the
|
||||
// Rust souvlaki MediaControls so the OS media overlay stays accurate.
|
||||
let prevTrackId: string | null = null;
|
||||
let prevRadioId: string | null = null;
|
||||
let prevIsPlaying: boolean | null = null;
|
||||
let lastMprisPositionUpdate = 0;
|
||||
|
||||
const unsubMpris = usePlayerStore.subscribe((state) => {
|
||||
const { currentTrack, isPlaying, currentTime } = state;
|
||||
const { currentTrack, currentRadio, isPlaying, currentTime } = state;
|
||||
|
||||
// Update metadata when track changes
|
||||
if (currentTrack && currentTrack.id !== prevTrackId) {
|
||||
prevTrackId = currentTrack.id;
|
||||
prevRadioId = null;
|
||||
const coverUrl = currentTrack.coverArt
|
||||
? buildCoverArtUrl(currentTrack.coverArt, 512)
|
||||
: undefined;
|
||||
@@ -618,6 +620,20 @@ export function initAudioListeners(): () => void {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Update metadata when a radio station starts (initial push — station name as title).
|
||||
// ICY StreamTitle updates are forwarded by the radio:metadata listener below.
|
||||
if (currentRadio && currentRadio.id !== prevRadioId) {
|
||||
prevRadioId = currentRadio.id;
|
||||
prevTrackId = null;
|
||||
invoke('mpris_set_metadata', {
|
||||
title: currentRadio.name,
|
||||
artist: null,
|
||||
album: null,
|
||||
coverUrl: null,
|
||||
durationSecs: null,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Update playback state on play/pause change
|
||||
const playbackChanged = isPlaying !== prevIsPlaying;
|
||||
if (playbackChanged) {
|
||||
@@ -633,7 +649,8 @@ export function initAudioListeners(): () => void {
|
||||
|
||||
// Keep position in sync while playing — update every ~500 ms so Plasma
|
||||
// always shows the correct time without interpolation gaps.
|
||||
if (isPlaying && Date.now() - lastMprisPositionUpdate >= 500) {
|
||||
// Radio streams have no meaningful position, so skip for radio.
|
||||
if (!currentRadio && isPlaying && Date.now() - lastMprisPositionUpdate >= 500) {
|
||||
lastMprisPositionUpdate = Date.now();
|
||||
invoke('mpris_set_playback', {
|
||||
playing: true,
|
||||
@@ -642,6 +659,30 @@ export function initAudioListeners(): () => void {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Radio ICY StreamTitle → MPRIS ─────────────────────────────────────────
|
||||
// The Rust download task emits "radio:metadata" with { title, is_ad } every
|
||||
// time an ICY metadata block changes (typically every 8–32 KB of audio).
|
||||
// Forward each update to mpris_set_metadata so the OS now-playing overlay
|
||||
// stays in sync while the stream is live.
|
||||
const radioMetaUnlisten = listen<{ title: string; is_ad: boolean }>('radio:metadata', ({ payload }) => {
|
||||
const { currentRadio } = usePlayerStore.getState();
|
||||
if (!currentRadio) return; // guard: only forward during active radio session
|
||||
if (payload.is_ad) return; // skip CDN-injected ad metadata
|
||||
|
||||
// Parse "Artist - Title" convention used by most ICY streams.
|
||||
const sep = payload.title.indexOf(' - ');
|
||||
const artist = sep !== -1 ? payload.title.slice(0, sep).trim() : null;
|
||||
const title = sep !== -1 ? payload.title.slice(sep + 3).trim() : payload.title;
|
||||
|
||||
invoke('mpris_set_metadata', {
|
||||
title: title || currentRadio.name,
|
||||
artist: artist || currentRadio.name,
|
||||
album: null,
|
||||
coverUrl: null,
|
||||
durationSecs: null,
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
// ── Discord Rich Presence sync ────────────────────────────────────────────
|
||||
// Updates on track change or play/pause toggle. No per-tick updates needed —
|
||||
// Discord auto-counts up the elapsed timer from the start_timestamp we set.
|
||||
@@ -696,6 +737,7 @@ export function initAudioListeners(): () => void {
|
||||
unsubDiscordPlayer();
|
||||
unsubDiscordAuth();
|
||||
pending.forEach(p => p.then(unlisten => unlisten()));
|
||||
radioMetaUnlisten.then(unlisten => unlisten());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user