mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +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
|
// Whenever the current track or playback state changes, push updates to the
|
||||||
// Rust souvlaki MediaControls so the OS media overlay stays accurate.
|
// Rust souvlaki MediaControls so the OS media overlay stays accurate.
|
||||||
let prevTrackId: string | null = null;
|
let prevTrackId: string | null = null;
|
||||||
|
let prevRadioId: string | null = null;
|
||||||
let prevIsPlaying: boolean | null = null;
|
let prevIsPlaying: boolean | null = null;
|
||||||
let lastMprisPositionUpdate = 0;
|
let lastMprisPositionUpdate = 0;
|
||||||
|
|
||||||
const unsubMpris = usePlayerStore.subscribe((state) => {
|
const unsubMpris = usePlayerStore.subscribe((state) => {
|
||||||
const { currentTrack, isPlaying, currentTime } = state;
|
const { currentTrack, currentRadio, isPlaying, currentTime } = state;
|
||||||
|
|
||||||
// Update metadata when track changes
|
// Update metadata when track changes
|
||||||
if (currentTrack && currentTrack.id !== prevTrackId) {
|
if (currentTrack && currentTrack.id !== prevTrackId) {
|
||||||
prevTrackId = currentTrack.id;
|
prevTrackId = currentTrack.id;
|
||||||
|
prevRadioId = null;
|
||||||
const coverUrl = currentTrack.coverArt
|
const coverUrl = currentTrack.coverArt
|
||||||
? buildCoverArtUrl(currentTrack.coverArt, 512)
|
? buildCoverArtUrl(currentTrack.coverArt, 512)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -618,6 +620,20 @@ export function initAudioListeners(): () => void {
|
|||||||
}).catch(() => {});
|
}).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
|
// Update playback state on play/pause change
|
||||||
const playbackChanged = isPlaying !== prevIsPlaying;
|
const playbackChanged = isPlaying !== prevIsPlaying;
|
||||||
if (playbackChanged) {
|
if (playbackChanged) {
|
||||||
@@ -633,7 +649,8 @@ export function initAudioListeners(): () => void {
|
|||||||
|
|
||||||
// Keep position in sync while playing — update every ~500 ms so Plasma
|
// Keep position in sync while playing — update every ~500 ms so Plasma
|
||||||
// always shows the correct time without interpolation gaps.
|
// 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();
|
lastMprisPositionUpdate = Date.now();
|
||||||
invoke('mpris_set_playback', {
|
invoke('mpris_set_playback', {
|
||||||
playing: true,
|
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 ────────────────────────────────────────────
|
// ── Discord Rich Presence sync ────────────────────────────────────────────
|
||||||
// Updates on track change or play/pause toggle. No per-tick updates needed —
|
// 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.
|
// Discord auto-counts up the elapsed timer from the start_timestamp we set.
|
||||||
@@ -696,6 +737,7 @@ export function initAudioListeners(): () => void {
|
|||||||
unsubDiscordPlayer();
|
unsubDiscordPlayer();
|
||||||
unsubDiscordAuth();
|
unsubDiscordAuth();
|
||||||
pending.forEach(p => p.then(unlisten => unlisten()));
|
pending.forEach(p => p.then(unlisten => unlisten()));
|
||||||
|
radioMetaUnlisten.then(unlisten => unlisten());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user