dot/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js

782 lines
21 KiB
JavaScript
Raw Normal View History

2020-05-11 09:16:27 +00:00
'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
var Player = GObject.registerClass({
GTypeName: 'GSConnectMPRISPlayer',
Implements: [Gio.DBusInterface],
Properties: {
// Application Properties
'CanQuit': GObject.ParamSpec.boolean(
'CanQuit',
'Can Quit',
'Whether the client can call the Quit method.',
GObject.ParamFlags.READABLE,
false
),
'Fullscreen': GObject.ParamSpec.boolean(
'Fullscreen',
'Fullscreen',
'Whether the player is in fullscreen mode.',
GObject.ParamFlags.READWRITE,
false
),
'CanSetFullscreen': GObject.ParamSpec.boolean(
'CanSetFullscreen',
'Can Set Fullscreen',
'Whether the client can set the Fullscreen property.',
GObject.ParamFlags.READABLE,
false
),
'CanRaise': GObject.ParamSpec.boolean(
'CanRaise',
'Can Raise',
'Whether the client can call the Raise method.',
GObject.ParamFlags.READABLE,
false
),
'HasTrackList': GObject.ParamSpec.boolean(
'HasTrackList',
'Has Track List',
'Whether the player has a track list.',
GObject.ParamFlags.READABLE,
false
),
'Identity': GObject.ParamSpec.string(
'Identity',
'Identity',
'The application name.',
GObject.ParamFlags.READABLE,
null
),
'DesktopEntry': GObject.ParamSpec.string(
'DesktopEntry',
'DesktopEntry',
'The basename of an installed .desktop file.',
GObject.ParamFlags.READABLE,
null
),
'SupportedUriSchemes': GObject.param_spec_variant(
'SupportedUriSchemes',
'Supported URI Schemes',
'The URI schemes supported by the media player.',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
'SupportedMimeTypes': GObject.param_spec_variant(
'SupportedMimeTypes',
'Supported MIME Types',
'The mime-types supported by the media player.',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
// Player Properties
'PlaybackStatus': GObject.ParamSpec.string(
'PlaybackStatus',
'Playback Status',
'The current playback status.',
GObject.ParamFlags.READABLE,
null
),
'LoopStatus': GObject.ParamSpec.string(
'LoopStatus',
'Loop Status',
'The current loop status.',
GObject.ParamFlags.READWRITE,
null
),
'Rate': GObject.ParamSpec.double(
'Rate',
'Rate',
'The current playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'MinimumRate': GObject.ParamSpec.double(
'MinimumRate',
'Minimum Rate',
'The minimum playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'MaximimRate': GObject.ParamSpec.double(
'MaximumRate',
'Maximum Rate',
'The maximum playback rate.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'Shuffle': GObject.ParamSpec.boolean(
'Shuffle',
'Shuffle',
'Whether track changes are linear.',
GObject.ParamFlags.READWRITE,
null
),
'Metadata': GObject.param_spec_variant(
'Metadata',
'Metadata',
'The metadata of the current element.',
new GLib.VariantType('a{sv}'),
null,
GObject.ParamFlags.READABLE
),
'Volume': GObject.ParamSpec.double(
'Volume',
'Volume',
'The volume level.',
GObject.ParamFlags.READWRITE,
0.0, 1.0,
1.0
),
'Position': GObject.ParamSpec.int64(
'Position',
'Position',
'The current track position in microseconds.',
GObject.ParamFlags.READABLE,
0, Number.MAX_SAFE_INTEGER,
0
),
'CanGoNext': GObject.ParamSpec.boolean(
'CanGoNext',
'Can Go Next',
'Whether the client can call the Next method.',
GObject.ParamFlags.READABLE,
false
),
'CanGoPrevious': GObject.ParamSpec.boolean(
'CanGoPrevious',
'Can Go Previous',
'Whether the client can call the Previous method.',
GObject.ParamFlags.READABLE,
false
),
'CanPlay': GObject.ParamSpec.boolean(
'CanPlay',
'Can Play',
'Whether playback can be started using Play or PlayPause.',
GObject.ParamFlags.READABLE,
false
),
'CanPause': GObject.ParamSpec.boolean(
'CanPause',
'Can Pause',
'Whether playback can be paused using Play or PlayPause.',
GObject.ParamFlags.READABLE,
false
),
'CanSeek': GObject.ParamSpec.boolean(
'CanSeek',
'Can Seek',
'Whether the client can control the playback position using Seek and SetPosition.',
GObject.ParamFlags.READABLE,
false
),
'CanControl': GObject.ParamSpec.boolean(
'CanControl',
'Can Control',
'Whether the media player may be controlled over this interface.',
GObject.ParamFlags.READABLE,
false
)
},
Signals: {
'Seeked': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_INT64]
}
}
}, class Player extends Gio.DBusProxy {
_init(name) {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: name,
g_object_path: '/org/mpris/MediaPlayer2',
g_interface_name: 'org.mpris.MediaPlayer2.Player'
});
this._application = new Gio.DBusProxy({
g_bus_type: Gio.BusType.SESSION,
g_name: name,
g_object_path: '/org/mpris/MediaPlayer2',
g_interface_name: 'org.mpris.MediaPlayer2'
});
this._propertiesChangedId = this._application.connect(
'g-properties-changed',
this._onPropertiesChanged.bind(this)
);
this._cancellable = new Gio.Cancellable();
}
vfunc_g_properties_changed(changed, invalidated) {
try {
if (this.__disposed !== undefined)
return;
for (let name in changed.deepUnpack()) {
this.notify(name);
}
} catch (e) {
debug(e, this.g_name);
}
}
vfunc_g_signal(sender_name, signal_name, parameters) {
try {
if (signal_name === 'Seeked') {
this.emit('Seeked', parameters.deepUnpack()[0]);
}
} catch (e) {
debug(e, this.g_name);
}
}
_call(name, parameters = null) {
this.call(
name,
parameters,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, result) => {
try {
proxy.call_finish(result);
} catch (e) {
Gio.DBusError.strip_remote_error(e);
logError(e, this.g_name);
}
}
);
}
_get(name, fallback = null) {
try {
return this.get_cached_property(name).recursiveUnpack();
} catch (e) {
return fallback;
}
}
_set(name, value) {
try {
this.set_cached_property(name, value);
this.call(
'org.freedesktop.DBus.Properties.Set',
new GLib.Variant('(ssv)', [this.g_interface_name, name, value]),
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, result) => {
try {
proxy.call_finish(result);
} catch (e) {
logError(e);
}
}
);
} catch (e) {
logError(e, this.g_name);
}
}
_onPropertiesChanged(proxy, changed, invalidated) {
try {
if (this.__disposed !== undefined)
return;
for (let name in changed.deepUnpack()) {
this.notify(name);
}
} catch (e) {
logError(e, this.g_name);
}
}
initPromise() {
let player = new Promise((resolve, reject) => {
this.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
let application = new Promise((resolve, reject) => {
this._application.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
return Promise.all([player, application]);
}
/*
* The org.mpris.MediaPlayer2 Interface
*/
get CanQuit() {
return this._get.call(this._application, 'CanQuit', false);
}
get Fullscreen() {
return this._get.call(this._application, 'Fullscreen', false);
}
set Fullscreen(mode) {
this._set.call(this._application, 'Fullscreen', new GLib.Variant('b', mode));
}
get CanSetFullscreen() {
return this._get.call(this._application, 'CanSetFullscreen', false);
}
get CanRaise() {
return this._get.call(this._application, 'CanRaise', false);
}
get HasTrackList() {
return this._get.call(this._application, 'HasTrackList', false);
}
get Identity() {
return this._get.call(this._application, 'Identity', _('Unknown'));
}
get DesktopEntry() {
return this._get.call(this._application, 'DesktopEntry', null);
}
get SupportedUriSchemes() {
return this._get.call(this._application, 'SupportedUriSchemes', []);
}
get SupportedMimeTypes() {
return this._get.call(this._application, 'SupportedMimeTypes', []);
}
Raise() {
this._call.call(this._application, 'Raise');
}
Quit() {
this._call.call(this._application, 'Quit');
}
/*
* The org.mpris.MediaPlayer2.Player Interface
*/
get PlaybackStatus() {
return this._get('PlaybackStatus', 'Stopped');
}
// 'None', 'Track', 'Playlist'
get LoopStatus() {
return this._get('LoopStatus', 'None');
}
set LoopStatus(status) {
this._set('LoopStatus', new GLib.Variant('s', status));
}
get Rate() {
return this._get('Rate', 1.0);
}
set Rate(rate) {
this._set('Rate', new GLib.Variant('d', rate));
}
get Shuffle() {
return this._get('Shuffle', false);
}
set Shuffle(mode) {
this._set('Shuffle', new GLib.Variant('b', mode));
}
get Metadata() {
if (this._metadata == undefined) {
this._metadata = {
'xesam:artist': [_('Unknown')],
'xesam:album': _('Unknown'),
'xesam:title': _('Unknown'),
'mpris:length': 0
};
}
return this._get('Metadata', this._metadata);
}
get Volume() {
return this._get('Volume', 1.0);
}
set Volume(level) {
this._set('Volume', new GLib.Variant('d', level));
}
// g-properties-changed is not emitted for this property
get Position() {
try {
let reply = this.call_sync(
'org.freedesktop.DBus.Properties.Get',
new GLib.Variant('(ss)', [this.g_interface_name, 'Position']),
Gio.DBusCallFlags.NONE,
-1,
null
);
return reply.recursiveUnpack()[0];
} catch (e) {
return 0;
}
}
get MinimumRate() {
return this._get('MinimumRate', 1.0);
}
get MaximumRate() {
return this._get('MaximumRate', 1.0);
}
get CanGoNext() {
return this._get('CanGoNext', false);
}
get CanGoPrevious() {
return this._get('CanGoPrevious', false);
}
get CanPlay() {
return this._get('CanPlay', false);
}
get CanPause() {
return this._get('CanPause', false);
}
get CanSeek() {
return this._get('CanSeek', false);
}
get CanControl() {
return this._get('CanControl', false);
}
Next() {
this._call('Next');
}
Previous() {
this._call('Previous');
}
Pause() {
this._call('Pause');
}
PlayPause() {
this._call('PlayPause');
}
Stop() {
this._call('Stop');
}
Play() {
this._call('Play');
}
Seek(offset) {
this._call('Seek', new GLib.Variant('(x)', [offset]));
}
SetPosition(trackId, position) {
this._call('SetPosition', new GLib.Variant('(ox)', [trackId, position]));
}
OpenUri(uri) {
this._call('OpenUri', new GLib.Variant('(s)', [uri]));
}
destroy() {
if (this.__disposed === undefined) {
this.__disposed = true;
this._cancellable.cancel();
this._application.disconnect(this._propertiesChangedId);
this._application.run_dispose();
this.run_dispose();
}
}
});
var Manager = GObject.registerClass({
GTypeName: 'GSConnectMPRISManager',
Implements: [Gio.DBusInterface],
Properties: {
'identities': GObject.param_spec_variant(
'identities',
'IdentityList',
'A list of MediaPlayer2.Identity for each player',
new GLib.VariantType('as'),
null,
GObject.ParamFlags.READABLE
),
// Actually returns an Object of MediaPlayer2Proxy objects,
// Player.Identity as key
'players': GObject.param_spec_variant(
'players',
'PlayerList',
'A list of known devices',
new GLib.VariantType('a{sv}'),
null,
GObject.ParamFlags.READABLE
)
},
Signals: {
'player-changed': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
},
'player-seeked': {
flags: GObject.SignalFlags.RUN_FIRST,
param_types: [GObject.TYPE_OBJECT]
}
}
}, class Manager extends Gio.DBusProxy {
_init() {
super._init({
g_bus_type: Gio.BusType.SESSION,
g_name: 'org.freedesktop.DBus',
g_object_path: '/org/freedesktop/DBus'
});
// Asynchronous setup
this._cancellable = new Gio.Cancellable();
this._init_async();
}
async _init_async() {
try {
await new Promise((resolve, reject) => {
this.init_async(0, this._cancellable, (proxy, res) => {
try {
resolve(proxy.init_finish(res));
} catch (e) {
reject(e);
}
});
});
let names = await new Promise((resolve, reject) => {
this.call(
'org.freedesktop.DBus.ListNames',
null,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
this._cancellable,
(proxy, res) => {
try {
resolve(proxy.call_finish(res).deepUnpack()[0]);
} catch (e) {
reject(e);
}
}
);
});
for (let i = 0, len = names.length; i < len; i++) {
let name = names[i];
if (name.startsWith('org.mpris.MediaPlayer2') &&
!name.includes('GSConnect')) {
this._addPlayer(name);
}
}
} catch (e) {
// FIXME: if something goes wrong the component will appear active
logError(e);
this.destroy();
}
}
get identities() {
let identities = [];
for (let player of this.players.values()) {
let identity = player.Identity;
if (identity)
identities.push(identity);
}
return identities;
}
get players() {
if (this._players === undefined) {
this._players = new Map();
}
return this._players;
}
get paused() {
if (this._paused === undefined) {
this._paused = new Map();
}
return this._paused;
}
vfunc_g_signal(sender_name, signal_name, parameters) {
try {
if (signal_name === 'NameOwnerChanged') {
let [name, old_owner, new_owner] = parameters.deepUnpack();
if (name.startsWith('org.mpris.MediaPlayer2') &&
!name.includes('GSConnect')) {
if (new_owner.length) {
this._addPlayer(name);
} else if (old_owner.length) {
this._removePlayer(name);
}
}
}
} catch (e) {
debug(e);
}
}
async _addPlayer(name) {
try {
if (!this.players.has(name)) {
let player = new Player(name);
await player.initPromise();
player.__propertiesId = player.connect(
'g-properties-changed',
(player) => this.emit('player-changed', player)
);
player.__seekedId = player.connect(
'Seeked',
(player) => this.emit('player-seeked', player)
);
this.players.set(name, player);
this.notify('players');
}
} catch (e) {
debug(e);
}
}
async _removePlayer(name) {
try {
let player = this.players.get(name);
if (player !== undefined) {
debug(`Removing MPRIS Player ${name}`);
player.disconnect(player.__propertiesId);
player.disconnect(player.__seekedId);
player.destroy();
this.paused.delete(name);
this.players.delete(name);
this.notify('players');
}
} catch (e) {
debug(e);
}
}
/**
* Get a player by its Identity.
*/
getPlayer(identity) {
for (let player of this.players.values()) {
if (player.Identity === identity) {
return player;
}
}
return null;
}
/**
* A convenience function for pausing all players currently playing.
*/
pauseAll() {
for (let [name, player] of this.players.entries()) {
if (player.PlaybackStatus === 'Playing' && player.CanPause) {
player.Pause();
this.paused.set(name, player);
}
}
}
/**
* A convenience function for restarting all players paused with pauseAll().
*/
unpauseAll() {
for (let player of this.paused.values()) {
if (player.PlaybackStatus === 'Paused' && player.CanPlay) {
player.Play();
}
}
this.paused.clear();
}
destroy() {
if (this.__disposed == undefined) {
this.__disposed = true;
this._cancellable.cancel();
for (let player of this.players.values()) {
player.disconnect(player.__propertiesId);
player.disconnect(player.__seekedId);
player.destroy();
}
this.players.clear();
this.run_dispose();
}
}
});
/**
* The service class for this component
*/
var Component = Manager;