diff options
Diffstat (limited to 'js/ui/mpris.js')
-rw-r--r-- | js/ui/mpris.js | 300 |
1 files changed, 300 insertions, 0 deletions
diff --git a/js/ui/mpris.js b/js/ui/mpris.js new file mode 100644 index 0000000..3650c57 --- /dev/null +++ b/js/ui/mpris.js @@ -0,0 +1,300 @@ +/* exported MediaSection */ +const { Gio, GObject, Shell, St } = imports.gi; +const Signals = imports.signals; + +const Main = imports.ui.main; +const MessageList = imports.ui.messageList; + +const { loadInterfaceXML } = imports.misc.fileUtils; + +const DBusIface = loadInterfaceXML('org.freedesktop.DBus'); +const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface); + +const MprisIface = loadInterfaceXML('org.mpris.MediaPlayer2'); +const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface); + +const MprisPlayerIface = loadInterfaceXML('org.mpris.MediaPlayer2.Player'); +const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface); + +const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.'; + +var MediaMessage = GObject.registerClass( +class MediaMessage extends MessageList.Message { + _init(player) { + super._init('', ''); + + this._player = player; + + this._icon = new St.Icon({ style_class: 'media-message-cover-icon' }); + this.setIcon(this._icon); + + this._prevButton = this.addMediaControl('media-skip-backward-symbolic', + () => { + this._player.previous(); + }); + + this._playPauseButton = this.addMediaControl(null, + () => { + this._player.playPause(); + }); + + this._nextButton = this.addMediaControl('media-skip-forward-symbolic', + () => { + this._player.next(); + }); + + this._updateHandlerId = + this._player.connect('changed', this._update.bind(this)); + this._closedHandlerId = + this._player.connect('closed', this.close.bind(this)); + this._update(); + } + + _onDestroy() { + super._onDestroy(); + this._player.disconnect(this._updateHandlerId); + this._player.disconnect(this._closedHandlerId); + } + + vfunc_clicked() { + this._player.raise(); + Main.panel.closeCalendar(); + } + + _updateNavButton(button, sensitive) { + button.reactive = sensitive; + } + + _update() { + this.setTitle(this._player.trackArtists.join(', ')); + this.setBody(this._player.trackTitle); + + if (this._player.trackCoverUrl) { + let file = Gio.File.new_for_uri(this._player.trackCoverUrl); + this._icon.gicon = new Gio.FileIcon({ file }); + this._icon.remove_style_class_name('fallback'); + } else { + this._icon.icon_name = 'audio-x-generic-symbolic'; + this._icon.add_style_class_name('fallback'); + } + + let isPlaying = this._player.status == 'Playing'; + let iconName = isPlaying + ? 'media-playback-pause-symbolic' + : 'media-playback-start-symbolic'; + this._playPauseButton.child.icon_name = iconName; + + this._updateNavButton(this._prevButton, this._player.canGoPrevious); + this._updateNavButton(this._nextButton, this._player.canGoNext); + } +}); + +var MprisPlayer = class MprisPlayer { + constructor(busName) { + this._mprisProxy = new MprisProxy(Gio.DBus.session, busName, + '/org/mpris/MediaPlayer2', + this._onMprisProxyReady.bind(this)); + this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName, + '/org/mpris/MediaPlayer2', + this._onPlayerProxyReady.bind(this)); + + this._visible = false; + this._trackArtists = []; + this._trackTitle = ''; + this._trackCoverUrl = ''; + this._busName = busName; + } + + get status() { + return this._playerProxy.PlaybackStatus; + } + + get trackArtists() { + return this._trackArtists; + } + + get trackTitle() { + return this._trackTitle; + } + + get trackCoverUrl() { + return this._trackCoverUrl; + } + + playPause() { + this._playerProxy.PlayPauseRemote(); + } + + get canGoNext() { + return this._playerProxy.CanGoNext; + } + + next() { + this._playerProxy.NextRemote(); + } + + get canGoPrevious() { + return this._playerProxy.CanGoPrevious; + } + + previous() { + this._playerProxy.PreviousRemote(); + } + + raise() { + // The remote Raise() method may run into focus stealing prevention, + // so prefer activating the app via .desktop file if possible + let app = null; + if (this._mprisProxy.DesktopEntry) { + let desktopId = '%s.desktop'.format(this._mprisProxy.DesktopEntry); + app = Shell.AppSystem.get_default().lookup_app(desktopId); + } + + if (app) + app.activate(); + else if (this._mprisProxy.CanRaise) + this._mprisProxy.RaiseRemote(); + } + + _close() { + this._mprisProxy.disconnect(this._ownerNotifyId); + this._mprisProxy = null; + + this._playerProxy.disconnect(this._propsChangedId); + this._playerProxy = null; + + this.emit('closed'); + } + + _onMprisProxyReady() { + this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner', + () => { + if (!this._mprisProxy.g_name_owner) + this._close(); + }); + // It is possible for the bus to disappear before the previous signal + // is connected, so we must ensure that the bus still exists at this + // point. + if (!this._mprisProxy.g_name_owner) + this._close(); + } + + _onPlayerProxyReady() { + this._propsChangedId = this._playerProxy.connect('g-properties-changed', + this._updateState.bind(this)); + this._updateState(); + } + + _updateState() { + let metadata = {}; + for (let prop in this._playerProxy.Metadata) + metadata[prop] = this._playerProxy.Metadata[prop].deep_unpack(); + + // Validate according to the spec; some clients send buggy metadata: + // https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata + this._trackArtists = metadata['xesam:artist']; + if (!Array.isArray(this._trackArtists) || + !this._trackArtists.every(artist => typeof artist === 'string')) { + if (typeof this._trackArtists !== 'undefined') { + log(('Received faulty track artist metadata from %s; ' + + 'expected an array of strings, got %s (%s)').format( + this._busName, this._trackArtists, typeof this._trackArtists)); + } + this._trackArtists = [_("Unknown artist")]; + } + + this._trackTitle = metadata['xesam:title']; + if (typeof this._trackTitle !== 'string') { + if (typeof this._trackTitle !== 'undefined') { + log(('Received faulty track title metadata from %s; ' + + 'expected a string, got %s (%s)').format( + this._busName, this._trackTitle, typeof this._trackTitle)); + } + this._trackTitle = _("Unknown title"); + } + + this._trackCoverUrl = metadata['mpris:artUrl']; + if (typeof this._trackCoverUrl !== 'string') { + if (typeof this._trackCoverUrl !== 'undefined') { + log(('Received faulty track cover art metadata from %s; ' + + 'expected a string, got %s (%s)').format( + this._busName, this._trackCoverUrl, typeof this._trackCoverUrl)); + } + this._trackCoverUrl = ''; + } + + this.emit('changed'); + + let visible = this._playerProxy.CanPlay; + + if (this._visible != visible) { + this._visible = visible; + if (visible) + this.emit('show'); + else + this.emit('hide'); + } + } +}; +Signals.addSignalMethods(MprisPlayer.prototype); + +var MediaSection = GObject.registerClass( +class MediaSection extends MessageList.MessageListSection { + _init() { + super._init(); + + this._players = new Map(); + + this._proxy = new DBusProxy(Gio.DBus.session, + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + this._onProxyReady.bind(this)); + } + + get allowed() { + return !Main.sessionMode.isGreeter; + } + + _addPlayer(busName) { + if (this._players.get(busName)) + return; + + let player = new MprisPlayer(busName); + let message = null; + player.connect('closed', + () => { + this._players.delete(busName); + }); + player.connect('show', () => { + message = new MediaMessage(player); + this.addMessage(message, true); + }); + player.connect('hide', () => { + this.removeMessage(message, true); + message = null; + }); + + this._players.set(busName, player); + } + + _onProxyReady() { + this._proxy.ListNamesRemote(([names]) => { + names.forEach(name => { + if (!name.startsWith(MPRIS_PLAYER_PREFIX)) + return; + + this._addPlayer(name); + }); + }); + this._proxy.connectSignal('NameOwnerChanged', + this._onNameOwnerChanged.bind(this)); + } + + _onNameOwnerChanged(proxy, sender, [name, oldOwner, newOwner]) { + if (!name.startsWith(MPRIS_PLAYER_PREFIX)) + return; + + if (newOwner && !oldOwner) + this._addPlayer(name); + } +}); |