summaryrefslogtreecommitdiffstats
path: root/js/ui/mpris.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/mpris.js')
-rw-r--r--js/ui/mpris.js300
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);
+ }
+});