From 0a9c5cd7c237c4067772f401b731b6eba765f195 Mon Sep 17 00:00:00 2001 From: ewired Date: Mon, 23 Feb 2026 14:40:09 -0500 Subject: [PATCH 1/8] Remove direct media key handling --- .../extension.js | 31 +-------- .../modules/mediaKeys.js | 67 ------------------- .../prefs.js | 16 ----- ...ell.extensions.yetanotherradio.gschema.xml | 7 -- 4 files changed, 1 insertion(+), 120 deletions(-) delete mode 100644 yetanotherradio@io.github.buddysirjava/modules/mediaKeys.js diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index 8bcee53..6aa2ac6 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -12,7 +12,6 @@ import { createMetadataItem, updateMetadataDisplay, updatePlaybackStateIcon } fr import { createVolumeItem, onVolumeChanged } from './modules/volumeControl.js'; import { createScrollableSection, createStationMenuItem, refreshStationsMenu } from './modules/stationMenu.js'; import PlaybackManager from './modules/playbackManager.js'; -import { setupMediaKeys, cleanupMediaKeys } from './modules/mediaKeys.js'; const Indicator = GObject.registerClass( class Indicator extends PanelMenu.Button { @@ -168,14 +167,6 @@ const Indicator = GObject.registerClass( this._playbackManager.stop(); } - handleMediaPlayPause() { - this._togglePlayback(); - } - - handleMediaStop() { - this._stopPlayback(); - } - destroy() { this._playbackManager.destroy(); @@ -202,6 +193,7 @@ export default class YetAnotherRadioExtension extends Extension { ensureStorageFile(); this._settings = this.getSettings(); this._indicator = new Indicator([], () => this.openPreferences(), this.path, this._settings); + Main.panel.addToStatusArea(this.uuid, this._indicator); loadStations().then(stations => { @@ -213,7 +205,6 @@ export default class YetAnotherRadioExtension extends Extension { }); this._monitor = this._watchStationsFile(); - this._setupMediaKeys(); } _watchStationsFile() { @@ -229,24 +220,6 @@ export default class YetAnotherRadioExtension extends Extension { return monitor; } - _setupMediaKeys() { - const { mediaKeyAccelerators, acceleratorHandlerId, mediaKeysSettingsHandlerId } = setupMediaKeys(this._settings, this._indicator); - this._mediaKeyAccelerators = mediaKeyAccelerators; - this._acceleratorHandlerId = acceleratorHandlerId; - this._mediaKeysSettingsHandlerId = mediaKeysSettingsHandlerId; - this._mediaKeysSettingsHandlerId = this._settings?.connect('changed::enable-media-keys', () => { - this._cleanupMediaKeys(); - this._setupMediaKeys(); - }); - } - - _cleanupMediaKeys() { - const { mediaKeyAccelerators, acceleratorHandlerId, mediaKeysSettingsHandlerId } = cleanupMediaKeys(this._mediaKeyAccelerators, this._acceleratorHandlerId, this._mediaKeysSettingsHandlerId, this._settings); - this._mediaKeyAccelerators = mediaKeyAccelerators; - this._acceleratorHandlerId = acceleratorHandlerId; - this._mediaKeysSettingsHandlerId = mediaKeysSettingsHandlerId; - } - disable() { if (this._monitor) { if (this._monitorHandlerId) { @@ -257,8 +230,6 @@ export default class YetAnotherRadioExtension extends Extension { this._monitor = null; } - this._cleanupMediaKeys(); - this._indicator?.destroy(); this._indicator = null; diff --git a/yetanotherradio@io.github.buddysirjava/modules/mediaKeys.js b/yetanotherradio@io.github.buddysirjava/modules/mediaKeys.js deleted file mode 100644 index a022f51..0000000 --- a/yetanotherradio@io.github.buddysirjava/modules/mediaKeys.js +++ /dev/null @@ -1,67 +0,0 @@ -import Meta from 'gi://Meta'; - -export function setupMediaKeys(settings, indicator) { - const enableMediaKeys = settings?.get_boolean('enable-media-keys') ?? true; - if (!enableMediaKeys) { - return { mediaKeyAccelerators: [], acceleratorHandlerId: null, mediaKeysSettingsHandlerId: null }; - } - - const display = global.display; - const mediaKeyAccelerators = []; - - const playPauseId = display.grab_accelerator('XF86AudioPlay', Meta.KeyBindingFlags.NONE); - if (playPauseId > 0) { - mediaKeyAccelerators.push({ - id: playPauseId, - action: 'play-pause' - }); - } - - const stopId = display.grab_accelerator('XF86AudioStop', Meta.KeyBindingFlags.NONE); - if (stopId > 0) { - mediaKeyAccelerators.push({ - id: stopId, - action: 'stop' - }); - } - - const acceleratorHandlerId = global.display.connect('accelerator-activated', (display, action, deviceId, timestamp) => { - const accelerator = mediaKeyAccelerators.find(acc => acc.id === action); - if (!accelerator || !indicator) { - return; - } - - if (accelerator.action === 'play-pause') { - indicator.handleMediaPlayPause(); - } else if (accelerator.action === 'stop') { - indicator.handleMediaStop(); - } - }); - - return { mediaKeyAccelerators, acceleratorHandlerId, mediaKeysSettingsHandlerId: null }; -} - -export function cleanupMediaKeys(mediaKeyAccelerators, acceleratorHandlerId, mediaKeysSettingsHandlerId, settings) { - if (acceleratorHandlerId) { - global.display.disconnect(acceleratorHandlerId); - acceleratorHandlerId = null; - } - - if (mediaKeyAccelerators) { - const display = global.display; - mediaKeyAccelerators.forEach(acc => { - try { - display.ungrab_accelerator(acc.id); - } catch (e) { - console.debug(e); - } - }); - } - - if (mediaKeysSettingsHandlerId) { - settings?.disconnect(mediaKeysSettingsHandlerId); - mediaKeysSettingsHandlerId = null; - } - - return { mediaKeyAccelerators: [], acceleratorHandlerId, mediaKeysSettingsHandlerId }; -} diff --git a/yetanotherradio@io.github.buddysirjava/prefs.js b/yetanotherradio@io.github.buddysirjava/prefs.js index aaf8cca..92c379d 100644 --- a/yetanotherradio@io.github.buddysirjava/prefs.js +++ b/yetanotherradio@io.github.buddysirjava/prefs.js @@ -712,22 +712,6 @@ const GeneralSettingsPage = GObject.registerClass( }); this.add(generalGroup); - const mediaKeysRow = new Adw.ActionRow({ - title: _('Enable Media Keys'), - subtitle: _('Use keyboard media keys (Play/Pause, Stop) to control playback'), - }); - mediaKeysRow.set_activatable(false); - - const mediaKeysSwitch = new Gtk.Switch({ - active: this._settings.get_boolean('enable-media-keys'), - valign: 3, - }); - mediaKeysSwitch.connect('notify::active', (sw) => { - this._settings.set_boolean('enable-media-keys', sw.active); - }); - mediaKeysRow.add_suffix(mediaKeysSwitch); - generalGroup.add(mediaKeysRow); - const playingNotificationRow = new Adw.ActionRow({ title: _('Show Playing Notification'), subtitle: _('Show an on-screen notification when starting playback'), diff --git a/yetanotherradio@io.github.buddysirjava/schemas/org.gnome.shell.extensions.yetanotherradio.gschema.xml b/yetanotherradio@io.github.buddysirjava/schemas/org.gnome.shell.extensions.yetanotherradio.gschema.xml index de5fce7..2348a4d 100644 --- a/yetanotherradio@io.github.buddysirjava/schemas/org.gnome.shell.extensions.yetanotherradio.gschema.xml +++ b/yetanotherradio@io.github.buddysirjava/schemas/org.gnome.shell.extensions.yetanotherradio.gschema.xml @@ -27,13 +27,6 @@ Auto-play last station Automatically play the last played station when the extension is enabled - - - true - Enable media keys - Enable global keyboard media keys (Play/Pause, Stop) to control playback - - true Show playing notification From 1d8b8d8e1918f42dc48fa558092193470ddeb011 Mon Sep 17 00:00:00 2001 From: ewired Date: Mon, 23 Feb 2026 21:44:14 -0500 Subject: [PATCH 2/8] Refactor event emitter for playback manager --- .../modules/playbackManager.js | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js index 911da2d..5072864 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js +++ b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js @@ -16,6 +16,7 @@ export default class PlaybackManager { constructor(settings, callbacks, osdIcon = null) { this._settings = settings; this._callbacks = callbacks || {}; + this._listeners = {}; this._osdIcon = osdIcon; this._player = null; @@ -39,6 +40,23 @@ export default class PlaybackManager { }; } + addListener(event, fn) { + (this._listeners[event] ||= []).push(fn); + } + + removeListener(event, fn) { + const list = this._listeners[event]; + if (list) { + const idx = list.indexOf(fn); + if (idx !== -1) list.splice(idx, 1); + } + } + + _emit(event, ...args) { + this._callbacks[event]?.(...args); + (this._listeners[event] || []).forEach(fn => fn(...args)); + } + _initGst() { if (!Gst.is_initialized()) { Gst.init(null); @@ -88,9 +106,7 @@ export default class PlaybackManager { if (metadata.albumArt) this._currentMetadata.albumArt = metadata.albumArt; if (metadata.bitrate) this._currentMetadata.bitrate = metadata.bitrate; - if (this._callbacks.onMetadataUpdate) { - this._callbacks.onMetadataUpdate(); - } + this._emit('onMetadataUpdate'); } } else if (message.type === Gst.MessageType.ERROR) { const [error, debug] = message.parse_error(); @@ -159,9 +175,9 @@ export default class PlaybackManager { this._currentMetadata.nowPlaying = station; this._currentMetadata.playbackState = 'playing'; - if (this._callbacks.onStateChanged) this._callbacks.onStateChanged('playing'); - if (this._callbacks.onStationChanged) this._callbacks.onStationChanged(station); - if (this._callbacks.onVisibilityChanged) this._callbacks.onVisibilityChanged(true); + this._emit('onStateChanged', 'playing'); + this._emit('onStationChanged', station); + this._emit('onVisibilityChanged', true); this._startMetadataUpdate(); @@ -209,7 +225,7 @@ export default class PlaybackManager { this._pausedAt = Date.now(); this._currentMetadata.playbackState = 'paused'; - if (this._callbacks.onStateChanged) this._callbacks.onStateChanged('paused'); + this._emit('onStateChanged', 'paused'); } else if (this._playbackState === 'paused') { const pauseDuration = this._pausedAt ? Date.now() - this._pausedAt : 0; @@ -222,7 +238,7 @@ export default class PlaybackManager { this._playbackState = 'playing'; this._currentMetadata.playbackState = 'playing'; - if (this._callbacks.onStateChanged) this._callbacks.onStateChanged('playing'); + this._emit('onStateChanged', 'playing'); } this._pausedAt = null; } @@ -246,9 +262,9 @@ export default class PlaybackManager { this._stopMetadataUpdate(); - if (this._callbacks.onStateChanged) this._callbacks.onStateChanged('stopped'); - if (this._callbacks.onStationChanged) this._callbacks.onStationChanged(null); - if (this._callbacks.onVisibilityChanged) this._callbacks.onVisibilityChanged(false); + this._emit('onStateChanged', 'stopped'); + this._emit('onStationChanged', null); + this._emit('onVisibilityChanged', false); } setVolume(volume) { @@ -265,9 +281,7 @@ export default class PlaybackManager { interval, () => { queryPlayerTags(this._player, this._currentMetadata); - if (this._callbacks.onMetadataUpdate) { - this._callbacks.onMetadataUpdate(); - } + this._emit('onMetadataUpdate'); return true; } ); From 5a7c4a40298c3a7e3fd823a3212fdd4a4aa6ed56 Mon Sep 17 00:00:00 2001 From: ewired Date: Mon, 23 Feb 2026 22:52:05 -0500 Subject: [PATCH 3/8] Add minimal MPRIS integration --- .../extension.js | 12 + .../modules/mprisInterface.js | 235 ++++++++++++++++++ .../modules/playbackManager.js | 13 + 3 files changed, 260 insertions(+) create mode 100644 yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index 6aa2ac6..eebd1f2 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -12,6 +12,7 @@ import { createMetadataItem, updateMetadataDisplay, updatePlaybackStateIcon } fr import { createVolumeItem, onVolumeChanged } from './modules/volumeControl.js'; import { createScrollableSection, createStationMenuItem, refreshStationsMenu } from './modules/stationMenu.js'; import PlaybackManager from './modules/playbackManager.js'; +import MprisInterface from './modules/mprisInterface.js'; const Indicator = GObject.registerClass( class Indicator extends PanelMenu.Button { @@ -194,6 +195,12 @@ export default class YetAnotherRadioExtension extends Extension { this._settings = this.getSettings(); this._indicator = new Indicator([], () => this.openPreferences(), this.path, this._settings); + try { + this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); + } catch (error) { + console.warn('Failed to initialize MPRIS interface:', error); + } + Main.panel.addToStatusArea(this.uuid, this._indicator); loadStations().then(stations => { @@ -230,6 +237,11 @@ export default class YetAnotherRadioExtension extends Extension { this._monitor = null; } + if (this._mpris) { + this._mpris.destroy(); + this._mpris = null; + } + this._indicator?.destroy(); this._indicator = null; diff --git a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js new file mode 100644 index 0000000..867a1f6 --- /dev/null +++ b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js @@ -0,0 +1,235 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +const MPRIS_SERVICE_NAME = 'org.mpris.MediaPlayer2.yetanotherradio'; +const MPRIS_OBJECT_PATH = '/org/mpris/MediaPlayer2'; + +const MPRIS_ROOT_XML = ` + + + + + + +`; + +const MPRIS_PLAYER_XML = ` + + + + + + + + + + + + +`; + +export default class MprisInterface { + constructor(playbackManager, settings) { + this._playbackManager = playbackManager; + this._settings = settings; + this._dbusConnection = null; + this._rootExported = null; + this._playerExported = null; + this._ownerId = 0; + this._settingsChangedId = 0; + + this._setupCallbacks(); + this._setupSettingsMonitoring(); + this._register(); + } + + _setupCallbacks() { + if (!this._playbackManager) return; + + this._onState = () => this._emitPlayerPropertiesChanged(['PlaybackStatus', 'CanPlay', 'CanPause']); + this._onMeta = () => this._emitPlayerPropertiesChanged(['Metadata']); + this._onStation = () => this._emitPlayerPropertiesChanged(['Metadata', 'CanPlay', 'CanPause']); + + this._playbackManager.addListener('onStateChanged', this._onState); + this._playbackManager.addListener('onMetadataUpdate', this._onMeta); + this._playbackManager.addListener('onStationChanged', this._onStation); + } + + _setupSettingsMonitoring() { + if (this._settings) { + this._settingsChangedId = this._settings.connect('changed::volume', () => { + this._emitPlayerPropertiesChanged(['Volume']); + }); + } + } + + _register() { + try { + this._dbusConnection = Gio.bus_get_sync(Gio.BusType.SESSION, null); + + const self = this; + + this._rootExported = Gio.DBusExportedObject.wrapJSObject( + MPRIS_ROOT_XML, + { + get Identity() { return 'Yet Another Radio'; }, + get CanQuit() { return false; }, + get CanRaise() { return false; }, + get HasTrackList() { return false; }, + } + ); + + this._playerExported = Gio.DBusExportedObject.wrapJSObject( + MPRIS_PLAYER_XML, + { + Play() { const m = self._playbackManager; if (m?.nowPlaying) m.play(m.nowPlaying); }, + Pause() { const m = self._playbackManager; if (m?.playbackState === 'playing') m.toggle(); }, + PlayPause() { self._playbackManager?.toggle(); }, + Stop() { self._playbackManager?.stop(); }, + get PlaybackStatus() { return self._getPlaybackStatus(); }, + get Metadata() { return self._getMetadata(); }, + get Volume() { return self._getVolume(); }, + set Volume(v) { + const vol = Math.max(0.0, Math.min(1.0, v)); + self._playbackManager?.setVolume(vol); + self._settings?.set_int('volume', Math.round(vol * 100)); + }, + get CanPlay() { return !!self._playbackManager?.nowPlaying; }, + get CanPause() { return !!self._playbackManager?.nowPlaying && self._getPlaybackStatus() !== 'Stopped'; }, + get CanControl() { return true; }, + } + ); + + this._rootExported.export(this._dbusConnection, MPRIS_OBJECT_PATH); + this._playerExported.export(this._dbusConnection, MPRIS_OBJECT_PATH); + + this._ownerId = Gio.bus_own_name_on_connection( + this._dbusConnection, + MPRIS_SERVICE_NAME, + Gio.BusNameOwnerFlags.NONE, + null, + null + ); + + if (this._ownerId === 0) { + console.error('MPRIS: Failed to acquire bus name'); + this._cleanup(); + return; + } + + console.log('MPRIS: Service registered successfully'); + this._emitPlayerPropertiesChanged(['PlaybackStatus', 'Metadata', 'CanPlay', 'CanPause', 'Volume']); + } catch (error) { + console.error('MPRIS: Registration failed:', error); + this._cleanup(); + } + } + + _getPlaybackStatus() { + if (!this._playbackManager) return 'Stopped'; + switch (this._playbackManager.playbackState) { + case 'playing': return 'Playing'; + case 'paused': return 'Paused'; + default: return 'Stopped'; + } + } + + _getMetadata() { + if (this._playbackManager) { + const metadata = this._playbackManager.getMPRISMetadata(); + if (!metadata['mpris:trackid']) { + metadata['mpris:trackid'] = new GLib.Variant('o', '/org/mpris/MediaPlayer2/NoTrack'); + } + return metadata; + } + + return { + 'xesam:title': new GLib.Variant('s', 'Yet Another Radio'), + }; + } + + _getVolume() { + if (!this._settings) return 1.0; + return Math.max(0.0, Math.min(1.0, this._settings.get_int('volume') / 100.0)); + } + + _emitPropertiesChanged(interfaceName, changedProps) { + if (!this._dbusConnection) return; + + try { + this._dbusConnection.emit_signal( + null, + MPRIS_OBJECT_PATH, + 'org.freedesktop.DBus.Properties', + 'PropertiesChanged', + GLib.Variant.new_tuple([ + new GLib.Variant('s', interfaceName), + new GLib.Variant('a{sv}', changedProps), + new GLib.Variant('as', []) + ]) + ); + } catch (error) { + console.error('MPRIS: Failed to emit PropertiesChanged:', error); + } + } + + _emitPlayerPropertiesChanged(propertyNames) { + const getters = { + PlaybackStatus: () => new GLib.Variant('s', this._getPlaybackStatus()), + Metadata: () => new GLib.Variant('a{sv}', this._getMetadata()), + Volume: () => new GLib.Variant('d', this._getVolume()), + CanPlay: () => new GLib.Variant('b', !!this._playbackManager?.nowPlaying), + CanPause: () => new GLib.Variant('b', !!this._playbackManager?.nowPlaying && this._getPlaybackStatus() !== 'Stopped'), + }; + + const changed = {}; + for (const name of propertyNames) { + if (getters[name]) changed[name] = getters[name](); + } + + if (Object.keys(changed).length > 0) { + this._emitPropertiesChanged('org.mpris.MediaPlayer2.Player', changed); + } + } + + _cleanup() { + if (this._playbackManager) { + this._playbackManager.removeListener('onStateChanged', this._onState); + this._playbackManager.removeListener('onMetadataUpdate', this._onMeta); + this._playbackManager.removeListener('onStationChanged', this._onStation); + } + this._onState = null; + this._onMeta = null; + this._onStation = null; + + if (this._settingsChangedId !== 0) { + if (this._settings) { + this._settings.disconnect(this._settingsChangedId); + } + this._settingsChangedId = 0; + } + + if (this._ownerId !== 0) { + Gio.bus_unown_name(this._ownerId); + this._ownerId = 0; + } + + if (this._rootExported) { + this._rootExported.unexport(); + this._rootExported = null; + } + + if (this._playerExported) { + this._playerExported.unexport(); + this._playerExported = null; + } + + this._dbusConnection = null; + } + + destroy() { + this._cleanup(); + this._playbackManager = null; + this._settings = null; + } +} diff --git a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js index 5072864..0359de0 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js +++ b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js @@ -75,6 +75,19 @@ export default class PlaybackManager { return this._nowPlaying; } + getMPRISMetadata() { + const metadata = {}; + + if (this._currentMetadata.title) { + metadata['xesam:title'] = new GLib.Variant('s', this._currentMetadata.title); + } + if (this._currentMetadata.artist) { + metadata['xesam:artist'] = new GLib.Variant('as', [this._currentMetadata.artist]); + } + + return metadata; + } + _ensurePlayer() { if (this._player) return; From 16e6b94d3064e85e7ac7b1847a31913f3b647a85 Mon Sep 17 00:00:00 2001 From: ewired Date: Mon, 23 Feb 2026 22:55:59 -0500 Subject: [PATCH 4/8] Add toggle for MPRIS integration --- .../extension.js | 33 +++++++++++++++--- .../prefs.js | 16 +++++++++ .../schemas/gschemas.compiled | Bin 768 -> 760 bytes ...ell.extensions.yetanotherradio.gschema.xml | 6 ++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index eebd1f2..5fbd759 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -195,12 +195,32 @@ export default class YetAnotherRadioExtension extends Extension { this._settings = this.getSettings(); this._indicator = new Indicator([], () => this.openPreferences(), this.path, this._settings); - try { - this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); - } catch (error) { - console.warn('Failed to initialize MPRIS interface:', error); + if (this._settings.get_boolean('enable-mpris')) { + try { + this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); + } catch (error) { + console.warn('Failed to initialize MPRIS interface:', error); + } } + this._mprisSettingId = 0; + this._mprisSettingId = this._settings.connect('changed::enable-mpris', () => { + if (this._settings.get_boolean('enable-mpris')) { + if (!this._mpris) { + try { + this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); + } catch (error) { + console.warn('Failed to initialize MPRIS interface:', error); + } + } + } else { + if (this._mpris) { + this._mpris.destroy(); + this._mpris = null; + } + } + }); + Main.panel.addToStatusArea(this.uuid, this._indicator); loadStations().then(stations => { @@ -237,6 +257,11 @@ export default class YetAnotherRadioExtension extends Extension { this._monitor = null; } + if (this._mprisSettingId) { + this._settings.disconnect(this._mprisSettingId); + this._mprisSettingId = 0; + } + if (this._mpris) { this._mpris.destroy(); this._mpris = null; diff --git a/yetanotherradio@io.github.buddysirjava/prefs.js b/yetanotherradio@io.github.buddysirjava/prefs.js index 92c379d..68df683 100644 --- a/yetanotherradio@io.github.buddysirjava/prefs.js +++ b/yetanotherradio@io.github.buddysirjava/prefs.js @@ -744,6 +744,22 @@ const GeneralSettingsPage = GObject.registerClass( autoPlayRow.add_suffix(autoPlaySwitch); generalGroup.add(autoPlayRow); + const mprisRow = new Adw.ActionRow({ + title: _('MPRIS Integration'), + subtitle: _('Expose the player over D-Bus so media keys and system controls work'), + }); + mprisRow.set_activatable(false); + + const mprisSwitch = new Gtk.Switch({ + active: this._settings.get_boolean('enable-mpris'), + valign: 3, + }); + mprisSwitch.connect('notify::active', (sw) => { + this._settings.set_boolean('enable-mpris', sw.active); + }); + mprisRow.add_suffix(mprisSwitch); + generalGroup.add(mprisRow); + const importExportGroup = new Adw.PreferencesGroup({ title: _('Import / Export'), description: _('Backup or restore your station list.'), diff --git a/yetanotherradio@io.github.buddysirjava/schemas/gschemas.compiled b/yetanotherradio@io.github.buddysirjava/schemas/gschemas.compiled index 5ca108df776cdc2094ae535b36aa2888dfe1d420..0036f5d15b7f0c4ae721e5ef4f9733602aaaeaf5 100644 GIT binary patch delta 100 zcmZo*`@uS41uHuP0|WcSwbR{PN_!@906DXn7#MgM${1Dv>9s&CTr=MTBo1Z+#SZ|* nZUZr^CShow playing notification Show an on-screen notification when starting radio playback + + + true + Enable MPRIS integration + Expose the player over D-Bus via the MPRIS2 protocol + 100 From 776655b9666ec03d7e1941b8cfa51974dcae4ac3 Mon Sep 17 00:00:00 2001 From: ewired Date: Mon, 23 Feb 2026 23:47:39 -0500 Subject: [PATCH 5/8] Display album/station art in MPRIS controls --- .../modules/playbackManager.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js index 0359de0..d7931a8 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js +++ b/yetanotherradio@io.github.buddysirjava/modules/playbackManager.js @@ -85,6 +85,14 @@ export default class PlaybackManager { metadata['xesam:artist'] = new GLib.Variant('as', [this._currentMetadata.artist]); } + let artUrl = this._currentMetadata.albumArt || this._nowPlaying?.favicon || null; + if (artUrl) { + if (artUrl.startsWith('/')) { + artUrl = 'file://' + artUrl; + } + metadata['mpris:artUrl'] = new GLib.Variant('s', artUrl); + } + return metadata; } From eecf1069a2d5e44d2bf9df1fd7bb87aa28a8c760 Mon Sep 17 00:00:00 2001 From: ewired Date: Tue, 24 Feb 2026 00:03:24 -0500 Subject: [PATCH 6/8] Switch stations with next/previous MPRIS controls --- .../extension.js | 46 +++++++++++++++++-- .../modules/mprisInterface.js | 26 ++++++++++- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index 5fbd759..e1d9a09 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -16,12 +16,13 @@ import MprisInterface from './modules/mprisInterface.js'; const Indicator = GObject.registerClass( class Indicator extends PanelMenu.Button { - _init(stations, openPrefs, extensionPath, settings) { + _init(stations, openPrefs, extensionPath, settings, onStationsChanged) { super._init(0.0, _('Yet Another Radio')); this._stations = stations ?? []; this._openPrefs = openPrefs; this._settings = settings; + this._onStationsChanged = onStationsChanged; this._refreshIdleId = 0; const iconPath = `${extensionPath}/icons/yetanotherradio.svg`; @@ -125,6 +126,7 @@ const Indicator = GObject.registerClass( setStations(stations) { this._stations = stations ?? []; this._refreshStationsMenu(); + this._onStationsChanged?.(this._stations.length); } _refreshStationsMenu() { @@ -168,6 +170,26 @@ const Indicator = GObject.registerClass( this._playbackManager.stop(); } + _orderedStations() { + const favorites = this._stations + .filter(s => s.favorite) + .sort((a, b) => a.name.localeCompare(b.name)); + const regulars = this._stations.filter(s => !s.favorite); + return [...favorites, ...regulars]; + } + + navigateStation(delta) { + if (!this._playbackManager.nowPlaying) return; + const ordered = this._orderedStations(); + if (ordered.length <= 1) return; + const currentIdx = ordered.findIndex( + s => s.uuid === this._playbackManager.nowPlaying.uuid + ); + if (currentIdx === -1) return; + const nextIdx = (currentIdx + delta + ordered.length) % ordered.length; + this._playStation(ordered[nextIdx]); + } + destroy() { this._playbackManager.destroy(); @@ -193,11 +215,22 @@ export default class YetAnotherRadioExtension extends Extension { initTranslations(_); ensureStorageFile(); this._settings = this.getSettings(); - this._indicator = new Indicator([], () => this.openPreferences(), this.path, this._settings); + this._indicator = new Indicator( + [], + () => this.openPreferences(), + this.path, + this._settings, + (count) => this._mpris?.setStationCount(count) + ); if (this._settings.get_boolean('enable-mpris')) { try { - this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); + this._mpris = new MprisInterface( + this._indicator._playbackManager, + this._settings, + (delta) => this._indicator.navigateStation(delta) + ); + this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { console.warn('Failed to initialize MPRIS interface:', error); } @@ -208,7 +241,12 @@ export default class YetAnotherRadioExtension extends Extension { if (this._settings.get_boolean('enable-mpris')) { if (!this._mpris) { try { - this._mpris = new MprisInterface(this._indicator._playbackManager, this._settings); + this._mpris = new MprisInterface( + this._indicator._playbackManager, + this._settings, + (delta) => this._indicator.navigateStation(delta) + ); + this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { console.warn('Failed to initialize MPRIS interface:', error); } diff --git a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js index 867a1f6..38e925c 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js +++ b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js @@ -19,19 +19,25 @@ const MPRIS_PLAYER_XML = ` + + + + `; export default class MprisInterface { - constructor(playbackManager, settings) { + constructor(playbackManager, settings, navigateCallback) { this._playbackManager = playbackManager; this._settings = settings; + this._navigateCallback = navigateCallback ?? null; + this._stationCount = 0; this._dbusConnection = null; this._rootExported = null; this._playerExported = null; @@ -48,7 +54,7 @@ export default class MprisInterface { this._onState = () => this._emitPlayerPropertiesChanged(['PlaybackStatus', 'CanPlay', 'CanPause']); this._onMeta = () => this._emitPlayerPropertiesChanged(['Metadata']); - this._onStation = () => this._emitPlayerPropertiesChanged(['Metadata', 'CanPlay', 'CanPause']); + this._onStation = () => this._emitPlayerPropertiesChanged(['Metadata', 'CanPlay', 'CanPause', 'CanGoNext', 'CanGoPrevious']); this._playbackManager.addListener('onStateChanged', this._onState); this._playbackManager.addListener('onMetadataUpdate', this._onMeta); @@ -86,6 +92,8 @@ export default class MprisInterface { Pause() { const m = self._playbackManager; if (m?.playbackState === 'playing') m.toggle(); }, PlayPause() { self._playbackManager?.toggle(); }, Stop() { self._playbackManager?.stop(); }, + Next() { self._navigateCallback?.(+1); }, + Previous() { self._navigateCallback?.(-1); }, get PlaybackStatus() { return self._getPlaybackStatus(); }, get Metadata() { return self._getMetadata(); }, get Volume() { return self._getVolume(); }, @@ -97,6 +105,8 @@ export default class MprisInterface { get CanPlay() { return !!self._playbackManager?.nowPlaying; }, get CanPause() { return !!self._playbackManager?.nowPlaying && self._getPlaybackStatus() !== 'Stopped'; }, get CanControl() { return true; }, + get CanGoNext() { return self._canNavigate(); }, + get CanGoPrevious() { return self._canNavigate(); }, } ); @@ -153,6 +163,16 @@ export default class MprisInterface { return Math.max(0.0, Math.min(1.0, this._settings.get_int('volume') / 100.0)); } + _canNavigate() { + if (!this._playbackManager?.nowPlaying) return false; + return this._stationCount > 1; + } + + setStationCount(count) { + this._stationCount = count; + this._emitPlayerPropertiesChanged(['CanGoNext', 'CanGoPrevious']); + } + _emitPropertiesChanged(interfaceName, changedProps) { if (!this._dbusConnection) return; @@ -180,6 +200,8 @@ export default class MprisInterface { Volume: () => new GLib.Variant('d', this._getVolume()), CanPlay: () => new GLib.Variant('b', !!this._playbackManager?.nowPlaying), CanPause: () => new GLib.Variant('b', !!this._playbackManager?.nowPlaying && this._getPlaybackStatus() !== 'Stopped'), + CanGoNext: () => new GLib.Variant('b', this._canNavigate()), + CanGoPrevious: () => new GLib.Variant('b', this._canNavigate()), }; const changed = {}; From e4095b81843ddcc641a267b4d6c667f53452ce59 Mon Sep 17 00:00:00 2001 From: ewired Date: Tue, 24 Feb 2026 00:45:59 -0500 Subject: [PATCH 7/8] Play last station for MPRIS play request while stopped --- .../extension.js | 6 ++++-- .../modules/mprisInterface.js | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index e1d9a09..2ba45de 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -228,7 +228,8 @@ export default class YetAnotherRadioExtension extends Extension { this._mpris = new MprisInterface( this._indicator._playbackManager, this._settings, - (delta) => this._indicator.navigateStation(delta) + (delta) => this._indicator.navigateStation(delta), + () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null ); this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { @@ -244,7 +245,8 @@ export default class YetAnotherRadioExtension extends Extension { this._mpris = new MprisInterface( this._indicator._playbackManager, this._settings, - (delta) => this._indicator.navigateStation(delta) + (delta) => this._indicator.navigateStation(delta), + () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null ); this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { diff --git a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js index 38e925c..8295cb1 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js +++ b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js @@ -33,10 +33,11 @@ const MPRIS_PLAYER_XML = ` `; export default class MprisInterface { - constructor(playbackManager, settings, navigateCallback) { + constructor(playbackManager, settings, navigateCallback, lastStationCallback) { this._playbackManager = playbackManager; this._settings = settings; this._navigateCallback = navigateCallback ?? null; + this._lastStationCallback = lastStationCallback ?? null; this._stationCount = 0; this._dbusConnection = null; this._rootExported = null; @@ -88,9 +89,23 @@ export default class MprisInterface { this._playerExported = Gio.DBusExportedObject.wrapJSObject( MPRIS_PLAYER_XML, { - Play() { const m = self._playbackManager; if (m?.nowPlaying) m.play(m.nowPlaying); }, + Play() { + const m = self._playbackManager; + if (!m) return; + const target = m.nowPlaying ?? self._lastStationCallback?.(); + if (target) m.play(target); + }, Pause() { const m = self._playbackManager; if (m?.playbackState === 'playing') m.toggle(); }, - PlayPause() { self._playbackManager?.toggle(); }, + PlayPause() { + const m = self._playbackManager; + if (!m) return; + if (m.playbackState === 'stopped') { + const target = m.nowPlaying ?? self._lastStationCallback?.(); + if (target) m.play(target); + } else { + m.toggle(); + } + }, Stop() { self._playbackManager?.stop(); }, Next() { self._navigateCallback?.(+1); }, Previous() { self._navigateCallback?.(-1); }, From ec004715ebb22d6f224e90bb7a4e2f284657b1c0 Mon Sep 17 00:00:00 2001 From: ewired Date: Tue, 24 Feb 2026 00:58:40 -0500 Subject: [PATCH 8/8] Raise popup from MPRIS controls --- yetanotherradio@io.github.buddysirjava/extension.js | 6 ++++-- .../modules/mprisInterface.js | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/yetanotherradio@io.github.buddysirjava/extension.js b/yetanotherradio@io.github.buddysirjava/extension.js index 2ba45de..5abb2fe 100644 --- a/yetanotherradio@io.github.buddysirjava/extension.js +++ b/yetanotherradio@io.github.buddysirjava/extension.js @@ -229,7 +229,8 @@ export default class YetAnotherRadioExtension extends Extension { this._indicator._playbackManager, this._settings, (delta) => this._indicator.navigateStation(delta), - () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null + () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null, + () => this._indicator.menu.open(true) ); this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { @@ -246,7 +247,8 @@ export default class YetAnotherRadioExtension extends Extension { this._indicator._playbackManager, this._settings, (delta) => this._indicator.navigateStation(delta), - () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null + () => this._indicator._stations.slice().sort((a, b) => (b.lastPlayed ?? 0) - (a.lastPlayed ?? 0))[0] ?? null, + () => this._indicator.menu.open(true) ); this._mpris.setStationCount(this._indicator._stations.length); } catch (error) { diff --git a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js index 8295cb1..5641b02 100644 --- a/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js +++ b/yetanotherradio@io.github.buddysirjava/modules/mprisInterface.js @@ -6,6 +6,7 @@ const MPRIS_OBJECT_PATH = '/org/mpris/MediaPlayer2'; const MPRIS_ROOT_XML = ` + @@ -33,11 +34,12 @@ const MPRIS_PLAYER_XML = ` `; export default class MprisInterface { - constructor(playbackManager, settings, navigateCallback, lastStationCallback) { + constructor(playbackManager, settings, navigateCallback, lastStationCallback, raiseCallback) { this._playbackManager = playbackManager; this._settings = settings; this._navigateCallback = navigateCallback ?? null; this._lastStationCallback = lastStationCallback ?? null; + this._raiseCallback = raiseCallback ?? null; this._stationCount = 0; this._dbusConnection = null; this._rootExported = null; @@ -79,9 +81,10 @@ export default class MprisInterface { this._rootExported = Gio.DBusExportedObject.wrapJSObject( MPRIS_ROOT_XML, { + Raise() { self._raiseCallback?.(); }, get Identity() { return 'Yet Another Radio'; }, get CanQuit() { return false; }, - get CanRaise() { return false; }, + get CanRaise() { return true; }, get HasTrackList() { return false; }, } );