From 81af324260a2d3a247d52ae121b8b200d7d8d4a6 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 22 Jan 2026 01:30:45 -0800 Subject: [PATCH] handle radio metadata for mpris / mediasession (#1586) --- .../player/hooks/use-media-session.ts | 86 +++++++++++++- .../features/player/hooks/use-mpris.ts | 105 +++++++++++++++++- 2 files changed, 181 insertions(+), 10 deletions(-) diff --git a/src/renderer/features/player/hooks/use-media-session.ts b/src/renderer/features/player/hooks/use-media-session.ts index e43ac4fdd..9af2b8ddf 100644 --- a/src/renderer/features/player/hooks/use-media-session.ts +++ b/src/renderer/features/player/hooks/use-media-session.ts @@ -4,6 +4,10 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { getItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { usePlayer } from '/@/renderer/features/player/context/player-context'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { usePlaybackSettings, usePlaybackType, @@ -22,6 +26,8 @@ export const useMediaSession = () => { const player = usePlayer(); const skip = useSkipButtons(); const playbackType = useSettingsStore((state) => state.playback.type); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer(); const isMediaSessionEnabled = useMemo(() => { // Always enable media session on web @@ -38,6 +44,10 @@ export const useMediaSession = () => { } mediaSession.setActionHandler('nexttrack', () => { + if (isRadioActive && isRadioPlaying) { + return; + } + player.mediaNext(); }); @@ -50,10 +60,18 @@ export const useMediaSession = () => { }); mediaSession.setActionHandler('previoustrack', () => { + if (isRadioActive && isRadioPlaying) { + return; + } + player.mediaPrevious(); }); mediaSession.setActionHandler('seekto', (e) => { + if (isRadioActive && isRadioPlaying) { + return; + } + if (e.seekTime) { player.mediaSeekToTimestamp(e.seekTime); } else if (e.seekOffset) { @@ -67,6 +85,10 @@ export const useMediaSession = () => { }); mediaSession.setActionHandler('seekbackward', (e) => { + if (isRadioActive && isRadioPlaying) { + return; + } + const currentTimestamp = useTimestampStoreBase.getState().timestamp; player.mediaSeekToTimestamp( currentTimestamp - (e.seekOffset || skip?.skipBackwardSeconds || 5), @@ -74,6 +96,10 @@ export const useMediaSession = () => { }); mediaSession.setActionHandler('seekforward', (e) => { + if (isRadioActive && isRadioPlaying) { + return; + } + const currentTimestamp = useTimestampStoreBase.getState().timestamp; player.mediaSeekToTimestamp( currentTimestamp + (e.seekOffset || skip?.skipForwardSeconds || 5), @@ -90,11 +116,37 @@ export const useMediaSession = () => { mediaSession.setActionHandler('seekbackward', null); mediaSession.setActionHandler('seekforward', null); }; - }, [player, skip?.skipBackwardSeconds, skip?.skipForwardSeconds, isMediaSessionEnabled]); + }, [ + player, + skip?.skipBackwardSeconds, + skip?.skipForwardSeconds, + isMediaSessionEnabled, + isRadioActive, + isRadioPlaying, + ]); const updateMediaSessionMetadata = useCallback( (song: QueueSong | undefined) => { - if (!isMediaSessionEnabled || !song) { + if (!isMediaSessionEnabled) { + return; + } + + // Handle radio metadata when radio is active and playing + if (isRadioActive && isRadioPlaying) { + const title = radioMetadata?.title || stationName || 'Radio'; + const artist = radioMetadata?.artist || stationName || ''; + + mediaSession.metadata = new MediaMetadata({ + album: stationName || '', + artist: artist, + artwork: [], + title: title, + }); + return; + } + + // Handle regular song metadata + if (!song) { return; } @@ -112,9 +164,27 @@ export const useMediaSession = () => { title: song?.name ?? '', }); }, - [isMediaSessionEnabled], + [isMediaSessionEnabled, isRadioActive, isRadioPlaying, radioMetadata, stationName], ); + // Update metadata when radio metadata changes + useEffect(() => { + if (!isMediaSessionEnabled) { + return; + } + + if (isRadioActive && isRadioPlaying) { + updateMediaSessionMetadata(undefined); + } + }, [ + isMediaSessionEnabled, + isRadioActive, + isRadioPlaying, + radioMetadata, + stationName, + updateMediaSessionMetadata, + ]); + usePlayerEvents( { onCurrentSongChange: (properties) => { @@ -122,6 +192,10 @@ export const useMediaSession = () => { return; } + if (isRadioActive && isRadioPlaying) { + return; + } + updateMediaSessionMetadata(properties.song); }, onPlayerRepeated: () => { @@ -129,6 +203,10 @@ export const useMediaSession = () => { return; } + if (isRadioActive && isRadioPlaying) { + return; + } + const currentSong = usePlayerStore.getState().getCurrentSong(); updateMediaSessionMetadata(currentSong); }, @@ -141,7 +219,7 @@ export const useMediaSession = () => { mediaSession.playbackState = status === PlayerStatus.PLAYING ? 'playing' : 'paused'; }, }, - [isMediaSessionEnabled, mediaSession], + [isMediaSessionEnabled, isRadioActive, isRadioPlaying, updateMediaSessionMetadata], ); }; diff --git a/src/renderer/features/player/hooks/use-mpris.ts b/src/renderer/features/player/hooks/use-mpris.ts index 86d40bbd9..9d9958f64 100644 --- a/src/renderer/features/player/hooks/use-mpris.ts +++ b/src/renderer/features/player/hooks/use-mpris.ts @@ -1,11 +1,15 @@ import isElectron from 'is-electron'; -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; +import { + useIsRadioActive, + useRadioPlayer, +} from '/@/renderer/features/radio/hooks/use-radio-player'; import { usePlayerSong, usePlayerStore } from '/@/renderer/store'; -import { LibraryItem } from '/@/shared/types/domain-types'; -import { PlayerShuffle } from '/@/shared/types/types'; +import { LibraryItem, QueueSong } from '/@/shared/types/domain-types'; +import { PlayerShuffle, ServerType } from '/@/shared/types/types'; const ipc = isElectron() ? window.api.ipc : null; const utils = isElectron() ? window.api.utils : null; @@ -14,6 +18,8 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null; export const useMPRIS = () => { const player = usePlayerStore(); const currentSong = usePlayerSong(); + const isRadioActive = useIsRadioActive(); + const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer(); const imageUrl = useItemImageUrl({ id: currentSong?.imageId || undefined, @@ -22,6 +28,89 @@ export const useMPRIS = () => { type: 'itemCard', }); + const radioSong = useMemo((): QueueSong | undefined => { + if (!isRadioActive || !isRadioPlaying) { + return undefined; + } + + const title = radioMetadata?.title || stationName || 'Radio'; + const artist = radioMetadata?.artist || stationName || null; + const album = stationName || null; + + const radioId = `radio-${stationName || 'unknown'}`; + + return { + _itemType: LibraryItem.SONG, + _serverId: '', + _serverType: ServerType.NAVIDROME, + _uniqueId: radioId, + album: album || null, + albumArtistName: artist || '', + albumArtists: artist + ? [ + { + id: '', + imageId: null, + imageUrl: null, + name: artist, + userFavorite: false, + userRating: null, + }, + ] + : [], + albumId: '', + artistName: artist || '', + artists: artist + ? [ + { + id: '', + imageId: null, + imageUrl: null, + name: artist, + userFavorite: false, + userRating: null, + }, + ] + : [], + bitDepth: null, + bitRate: 0, + bpm: null, + channels: null, + comment: null, + compilation: null, + container: null, + createdAt: '', + discNumber: 0, + discSubtitle: null, + duration: 0, + explicitStatus: null, + gain: null, + genres: [], + id: radioId, + imageId: null, + imageUrl: null, + lastPlayedAt: null, + lyrics: null, + mbzRecordingId: null, + mbzTrackId: null, + name: title, + participants: null, + path: null, + peak: null, + playCount: 0, + releaseDate: null, + releaseYear: null, + sampleRate: null, + size: 0, + tags: null, + trackNumber: 0, + trackSubtitle: null, + updatedAt: new Date().toISOString(), + userFavorite: false, + userRating: null, + }; + }, [isRadioActive, isRadioPlaying, radioMetadata, stationName]); + useEffect(() => { if (!mpris) { return; @@ -56,14 +145,18 @@ export const useMPRIS = () => { }; }, [player]); - // Update MPRIS when song or imageUrl changes + // Update MPRIS when song, imageUrl, or radio metadata changes useEffect(() => { if (!mpris) { return; } - mpris?.updateSong(currentSong, imageUrl); - }, [currentSong, imageUrl]); + // Use radio song if radio is active and playing, otherwise use current song + const songToUpdate = isRadioActive && isRadioPlaying ? radioSong : currentSong; + const imageUrlToUpdate = isRadioActive && isRadioPlaying ? null : imageUrl; + + mpris?.updateSong(songToUpdate, imageUrlToUpdate); + }, [currentSong, imageUrl, isRadioActive, isRadioPlaying, radioSong]); usePlayerEvents( {