978 lines
25 KiB
JavaScript
978 lines
25 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const Gio = imports.gi.Gio;
|
||
|
const GLib = imports.gi.GLib;
|
||
|
const GObject = imports.gi.GObject;
|
||
|
|
||
|
const PluginsBase = imports.service.plugins.base;
|
||
|
const DBus = imports.utils.dbus;
|
||
|
|
||
|
|
||
|
var Metadata = {
|
||
|
label: _('MPRIS'),
|
||
|
id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.MPRIS',
|
||
|
incomingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
|
||
|
outgoingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
|
||
|
actions: {}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* MPRIS Plugin
|
||
|
* https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mpriscontrol
|
||
|
*
|
||
|
* See also:
|
||
|
* https://specifications.freedesktop.org/mpris-spec/latest/
|
||
|
* https://github.com/GNOME/gnome-shell/blob/master/js/ui/mpris.js
|
||
|
* https://github.com/JasonLG1979/gnome-shell-extensions-mediaplayer/wiki/Known-Player-Bugs
|
||
|
*/
|
||
|
var Plugin = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectMPRISPlugin',
|
||
|
}, class Plugin extends PluginsBase.Plugin {
|
||
|
|
||
|
_init(device) {
|
||
|
super._init(device, 'mpris');
|
||
|
|
||
|
try {
|
||
|
this._mpris = this.service.components.get('mpris');
|
||
|
|
||
|
this._notifyPlayersId = this._mpris.connect(
|
||
|
'notify::players',
|
||
|
this._sendPlayerList.bind(this)
|
||
|
);
|
||
|
|
||
|
this._playerChangedId = this._mpris.connect(
|
||
|
'player-changed',
|
||
|
this._onPlayerChanged.bind(this)
|
||
|
);
|
||
|
|
||
|
this._playerSeekedId = this._mpris.connect(
|
||
|
'player-seeked',
|
||
|
this._onPlayerSeeked.bind(this)
|
||
|
);
|
||
|
|
||
|
this._players = new Map();
|
||
|
this._transferring = new WeakSet();
|
||
|
this._updating = new WeakSet();
|
||
|
} catch (e) {
|
||
|
this.destroy();
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
handlePacket(packet) {
|
||
|
if (packet.type === 'kdeconnect.mpris.request') {
|
||
|
this._handleRequest(packet);
|
||
|
} else if (packet.type === 'kdeconnect.mpris') {
|
||
|
this._handleStatus(packet);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
connected() {
|
||
|
super.connected();
|
||
|
|
||
|
this._requestPlayerList();
|
||
|
this._sendPlayerList();
|
||
|
}
|
||
|
|
||
|
disconnected() {
|
||
|
super.disconnected();
|
||
|
|
||
|
for (let [identity, player] of this._players.entries()) {
|
||
|
player.destroy();
|
||
|
this._players.delete(identity);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_handleStatus(packet) {
|
||
|
try {
|
||
|
if (packet.body.hasOwnProperty('playerList')) {
|
||
|
this._handlePlayerList(packet.body.playerList);
|
||
|
} else if (packet.body.hasOwnProperty('player')) {
|
||
|
this._handlePlayerState(packet.body);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
debug(e, `${this.device.name}: MPRIS`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a player list update
|
||
|
*
|
||
|
* @param {array} playerList - A list of remote player names
|
||
|
*/
|
||
|
_handlePlayerList(playerList) {
|
||
|
for (let player of this._players.values()) {
|
||
|
if (!playerList.includes(player.Identity)) {
|
||
|
this._players.delete(player.Identity);
|
||
|
player.destroy();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (let identity of playerList) {
|
||
|
this._device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: identity,
|
||
|
requestNowPlaying: true,
|
||
|
requestVolume: true
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle a player state update
|
||
|
*
|
||
|
* @param {object} state - The body of a kdeconnect.mpris packet
|
||
|
*/
|
||
|
_handlePlayerState(state) {
|
||
|
let player = this._players.get(state.player);
|
||
|
|
||
|
if (player === undefined) {
|
||
|
player = new RemotePlayer(this.device, state);
|
||
|
this._players.set(state.player, player);
|
||
|
} else {
|
||
|
player.parseState(state);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Request the list of player identities
|
||
|
*/
|
||
|
_requestPlayerList() {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
requestPlayerList: true
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_handleRequest(packet) {
|
||
|
// A request for the list of players
|
||
|
if (packet.body.requestPlayerList) {
|
||
|
this._sendPlayerList();
|
||
|
|
||
|
// A request for an unknown player; send the list of players
|
||
|
} else if (!this._mpris.identities.includes(packet.body.player)) {
|
||
|
this._sendPlayerList();
|
||
|
|
||
|
// An album art request
|
||
|
} else if (packet.body.hasOwnProperty('albumArtUrl')) {
|
||
|
this._sendAlbumArt(packet);
|
||
|
|
||
|
// A player command
|
||
|
} else {
|
||
|
this._handleCommand(packet);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle an incoming player command or information request
|
||
|
*
|
||
|
* @param {kdeconnect.mpris.request} - A command for a specific player
|
||
|
*/
|
||
|
async _handleCommand(packet) {
|
||
|
if (!this.settings.get_boolean('share-players')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let player;
|
||
|
|
||
|
try {
|
||
|
player = this._mpris.getPlayer(packet.body.player);
|
||
|
|
||
|
if (player === undefined || this._updating.has(player))
|
||
|
return;
|
||
|
|
||
|
// Player Actions
|
||
|
if (packet.body.hasOwnProperty('action')) {
|
||
|
switch (packet.body.action) {
|
||
|
case 'PlayPause':
|
||
|
case 'Play':
|
||
|
case 'Pause':
|
||
|
case 'Next':
|
||
|
case 'Previous':
|
||
|
case 'Stop':
|
||
|
player[packet.body.action]();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
logError(new Error(`unknown action: ${packet.body.action}`));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Player Properties
|
||
|
if (packet.body.hasOwnProperty('setVolume')) {
|
||
|
player.Volume = packet.body.setVolume / 100;
|
||
|
}
|
||
|
|
||
|
if (packet.body.hasOwnProperty('Seek')) {
|
||
|
await player.Seek(packet.body.Seek);
|
||
|
}
|
||
|
|
||
|
if (packet.body.hasOwnProperty('SetPosition')) {
|
||
|
let offset = (packet.body.SetPosition * 1000) - player.Position;
|
||
|
await player.Seek(offset);
|
||
|
}
|
||
|
|
||
|
// Information Request
|
||
|
let hasResponse = false;
|
||
|
|
||
|
let response = {
|
||
|
type: 'kdeconnect.mpris',
|
||
|
body: {
|
||
|
player: packet.body.player
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (packet.body.hasOwnProperty('requestNowPlaying')) {
|
||
|
hasResponse = true;
|
||
|
|
||
|
Object.assign(response.body, {
|
||
|
pos: Math.floor(player.Position / 1000),
|
||
|
isPlaying: (player.PlaybackStatus === 'Playing'),
|
||
|
canPause: player.CanPause,
|
||
|
canPlay: player.CanPlay,
|
||
|
canGoNext: player.CanGoNext,
|
||
|
canGoPrevious: player.CanGoPrevious,
|
||
|
canSeek: player.CanSeek,
|
||
|
artist: _('Unknown'),
|
||
|
title: _('Unknown')
|
||
|
});
|
||
|
|
||
|
let metadata = player.Metadata;
|
||
|
|
||
|
if (metadata.hasOwnProperty('mpris:artUrl')) {
|
||
|
let file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
|
||
|
response.body.albumArtUrl = file.get_uri();
|
||
|
}
|
||
|
|
||
|
if (metadata.hasOwnProperty('mpris:length')) {
|
||
|
let trackLen = Math.floor(metadata['mpris:length'] / 1000);
|
||
|
response.body.length = trackLen;
|
||
|
}
|
||
|
|
||
|
if (metadata.hasOwnProperty('xesam:artist')) {
|
||
|
let artists = metadata['xesam:artist'];
|
||
|
response.body.artist = artists.join(', ');
|
||
|
}
|
||
|
|
||
|
if (metadata.hasOwnProperty('xesam:title')) {
|
||
|
response.body.title = metadata['xesam:title'];
|
||
|
}
|
||
|
|
||
|
if (metadata.hasOwnProperty('xesam:album')) {
|
||
|
response.body.album = metadata['xesam:album'];
|
||
|
}
|
||
|
|
||
|
response.body.nowPlaying = [
|
||
|
response.body.artist,
|
||
|
response.body.title
|
||
|
].join(' - ');
|
||
|
}
|
||
|
|
||
|
if (packet.body.hasOwnProperty('requestVolume')) {
|
||
|
hasResponse = true;
|
||
|
response.body.volume = player.Volume * 100;
|
||
|
}
|
||
|
|
||
|
if (hasResponse) {
|
||
|
this.device.sendPacket(response);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
} finally {
|
||
|
this._updating.delete(player);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_onPlayerChanged(mpris, player) {
|
||
|
if (!this.settings.get_boolean('share-players')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._handleCommand({
|
||
|
body: {
|
||
|
player: player.Identity,
|
||
|
requestNowPlaying: true,
|
||
|
requestVolume: true
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_onPlayerSeeked(mpris, player) {
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris',
|
||
|
body: {
|
||
|
player: player.Identity,
|
||
|
pos: Math.floor(player.Position / 1000)
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async _sendAlbumArt(packet) {
|
||
|
let player;
|
||
|
|
||
|
try {
|
||
|
// Reject concurrent requests for album art
|
||
|
player = this._mpris.getPlayer(packet.body.player);
|
||
|
|
||
|
if (player === undefined || this._transferring.has(player)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Ensure the requested albumArtUrl matches the current mpris:artUrl
|
||
|
let metadata = player.Metadata;
|
||
|
|
||
|
if (!metadata.hasOwnProperty('mpris:artUrl')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
|
||
|
let request = Gio.File.new_for_uri(packet.body.albumArtUrl);
|
||
|
|
||
|
if (file.get_uri() !== request.get_uri()) {
|
||
|
throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`);
|
||
|
}
|
||
|
|
||
|
// Start the transfer process
|
||
|
this._transferring.add(player);
|
||
|
|
||
|
let transfer = this.device.createTransfer({
|
||
|
input_stream: file.read(null),
|
||
|
size: file.query_info('standard::size', 0, null).get_size()
|
||
|
});
|
||
|
|
||
|
await transfer.upload({
|
||
|
type: 'kdeconnect.mpris',
|
||
|
body: {
|
||
|
transferringAlbumArt: true,
|
||
|
player: packet.body.player,
|
||
|
albumArtUrl: packet.body.albumArtUrl
|
||
|
}
|
||
|
});
|
||
|
} catch (e) {
|
||
|
debug(e, 'transferring album art');
|
||
|
} finally {
|
||
|
this._transferring.delete(player);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send the list of player identities and indicate whether we support
|
||
|
* transferring album art
|
||
|
*/
|
||
|
_sendPlayerList() {
|
||
|
let playerList = [];
|
||
|
|
||
|
if (this.settings.get_boolean('share-players')) {
|
||
|
playerList = this._mpris.identities;
|
||
|
}
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris',
|
||
|
body: {
|
||
|
playerList: playerList,
|
||
|
supportAlbumArtPayload: true
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
try {
|
||
|
this._mpris.disconnect(this._notifyPlayersId);
|
||
|
this._mpris.disconnect(this._playerChangedId);
|
||
|
this._mpris.disconnect(this._playerSeekedId);
|
||
|
} catch (e) {
|
||
|
// Silence errors
|
||
|
}
|
||
|
|
||
|
for (let [identity, player] of this._players.entries()) {
|
||
|
player.destroy();
|
||
|
this._players.delete(identity);
|
||
|
}
|
||
|
|
||
|
super.destroy();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
|
||
|
/*
|
||
|
* A class for mirroring a remote Media Player on DBus
|
||
|
*/
|
||
|
const MPRISIface = gsconnect.dbusinfo.lookup_interface('org.mpris.MediaPlayer2');
|
||
|
const MPRISPlayerIface = gsconnect.dbusinfo.lookup_interface('org.mpris.MediaPlayer2.Player');
|
||
|
|
||
|
|
||
|
var RemotePlayer = GObject.registerClass({
|
||
|
GTypeName: 'GSConnectMPRISRemotePlayer',
|
||
|
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
|
||
|
),
|
||
|
'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 RemotePlayer extends GObject.Object {
|
||
|
|
||
|
_init(device, initialState) {
|
||
|
super._init();
|
||
|
|
||
|
this._device = device;
|
||
|
this._isPlaying = false;
|
||
|
|
||
|
this._ownerId = 0;
|
||
|
this._connection = null;
|
||
|
this._applicationIface = null;
|
||
|
this._playerIface = null;
|
||
|
|
||
|
this.parseState(initialState);
|
||
|
}
|
||
|
|
||
|
async export() {
|
||
|
try {
|
||
|
if (this._ownerId === 0) {
|
||
|
if (!this._connection) {
|
||
|
this._connection = await DBus.newConnection();
|
||
|
}
|
||
|
|
||
|
if (!this._applicationIface) {
|
||
|
this._applicationIface = new DBus.Interface({
|
||
|
g_instance: this,
|
||
|
g_connection: this._connection,
|
||
|
g_object_path: '/org/mpris/MediaPlayer2',
|
||
|
g_interface_info: MPRISIface
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (!this._playerIface) {
|
||
|
this._playerIface = new DBus.Interface({
|
||
|
g_instance: this,
|
||
|
g_connection: this._connection,
|
||
|
g_object_path: '/org/mpris/MediaPlayer2',
|
||
|
g_interface_info: MPRISPlayerIface
|
||
|
});
|
||
|
}
|
||
|
|
||
|
let name = [
|
||
|
this.device.name,
|
||
|
this.Identity
|
||
|
].join('').replace(/[\W]*/g, '');
|
||
|
|
||
|
this._ownerId = Gio.bus_own_name_on_connection(
|
||
|
this._connection,
|
||
|
`org.mpris.MediaPlayer2.GSConnect.${name}`,
|
||
|
Gio.BusNameOwnerFlags.NONE,
|
||
|
null,
|
||
|
null
|
||
|
);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
logError(e);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
unexport() {
|
||
|
if (this._ownerId !== 0) {
|
||
|
Gio.bus_unown_name(this._ownerId);
|
||
|
this._ownerId = 0;
|
||
|
}
|
||
|
|
||
|
if (this._applicationIface) {
|
||
|
this._applicationIface.destroy();
|
||
|
this._applicationIface = null;
|
||
|
}
|
||
|
|
||
|
if (this._playerIface) {
|
||
|
this._playerIface.destroy();
|
||
|
this._playerIface = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parseState(state) {
|
||
|
this._Identity = state.player;
|
||
|
|
||
|
// Metadata
|
||
|
let metadataChanged = false;
|
||
|
|
||
|
if (state.hasOwnProperty('title')) {
|
||
|
metadataChanged = true;
|
||
|
this._title = state.title;
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('artist')) {
|
||
|
metadataChanged = true;
|
||
|
this._artist = state.artist;
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('album')) {
|
||
|
metadataChanged = true;
|
||
|
this._album = state.album;
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('length')) {
|
||
|
metadataChanged = true;
|
||
|
this._length = state.length * 1000;
|
||
|
}
|
||
|
|
||
|
// Probably a good idea to update this before emitting the length change
|
||
|
if (state.hasOwnProperty('pos')) {
|
||
|
this._Position = state.pos * 1000;
|
||
|
}
|
||
|
|
||
|
if (metadataChanged) this.notify('Metadata');
|
||
|
|
||
|
// Playback Status
|
||
|
if (state.hasOwnProperty('isPlaying')) {
|
||
|
if (this._isPlaying !== state.isPlaying) {
|
||
|
this._isPlaying = state.isPlaying;
|
||
|
this.notify('PlaybackStatus');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('canPlay')) {
|
||
|
if (this.CanPlay !== state.canPlay) {
|
||
|
this._CanPlay = state.canPlay;
|
||
|
this.notify('CanPlay');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('canPause')) {
|
||
|
if (this.CanPause !== state.canPause) {
|
||
|
this._CanPause = state.canPause;
|
||
|
this.notify('CanPause');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('canGoNext')) {
|
||
|
if (this.CanGoNext !== state.canGoNext) {
|
||
|
this._CanGoNext = state.canGoNext;
|
||
|
this.notify('CanGoNext');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('canGoPrevious')) {
|
||
|
if (this.CanGoPrevious !== state.canGoPrevious) {
|
||
|
this._CanGoPrevious = state.canGoPrevious;
|
||
|
this.notify('CanGoPrevious');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (state.hasOwnProperty('volume')) {
|
||
|
this.volume = state.volume / 100;
|
||
|
}
|
||
|
|
||
|
if (!this._isPlaying && !this.CanControl) {
|
||
|
this.unexport();
|
||
|
} else {
|
||
|
this.export();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Native properties
|
||
|
*/
|
||
|
get device() {
|
||
|
return this._device;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* The org.mpris.MediaPlayer2 Interface
|
||
|
*/
|
||
|
get CanQuit() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
get Fullscreen() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
get CanSetFullscreen() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
get CanRaise() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
get HasTrackList() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
get Identity() {
|
||
|
return this._Identity;
|
||
|
}
|
||
|
|
||
|
get DesktopEntry() {
|
||
|
return 'org.gnome.Shell.Extensions.GSConnect';
|
||
|
}
|
||
|
|
||
|
get SupportedUriSchemes() {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
get SupportedMimeTypes() {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
Raise() {
|
||
|
}
|
||
|
|
||
|
Quit() {
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* The org.mpris.MediaPlayer2.Player Interface
|
||
|
*/
|
||
|
|
||
|
// 'Playing', 'Paused', 'Stopped'
|
||
|
get PlaybackStatus() {
|
||
|
if (this._isPlaying) {
|
||
|
return 'Playing';
|
||
|
} else {
|
||
|
return 'Stopped';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 'None', 'Track', 'Playlist'
|
||
|
get LoopStatus() {
|
||
|
return 'None';
|
||
|
}
|
||
|
|
||
|
set LoopStatus(status) {
|
||
|
this.notify('LoopStatus');
|
||
|
}
|
||
|
|
||
|
get Rate() {
|
||
|
return 1.0;
|
||
|
}
|
||
|
|
||
|
set Rate(rate) {
|
||
|
this.notify('Rate');
|
||
|
}
|
||
|
|
||
|
get Shuffle() {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
set Shuffle(mode) {
|
||
|
this.notify('Shuffle');
|
||
|
}
|
||
|
|
||
|
get Metadata() {
|
||
|
if (this._metadata === undefined) {
|
||
|
this._metadata = {};
|
||
|
}
|
||
|
|
||
|
Object.assign(this._metadata, {
|
||
|
'xesam:artist': new GLib.Variant('as', [this._artist || '']),
|
||
|
'xesam:album': new GLib.Variant('s', this._album || ''),
|
||
|
'xesam:title': new GLib.Variant('s', this._title || ''),
|
||
|
'mpris:length': new GLib.Variant('x', this._length || 0)
|
||
|
});
|
||
|
|
||
|
return this._metadata;
|
||
|
}
|
||
|
|
||
|
get Volume() {
|
||
|
if (this._Volume === undefined) {
|
||
|
this._Volume = 1.0;
|
||
|
}
|
||
|
|
||
|
return this._Volume;
|
||
|
}
|
||
|
|
||
|
set Volume(level) {
|
||
|
if (this._Volume !== level) {
|
||
|
this._Volume = level;
|
||
|
this.notify('Volume');
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
setVolume: this.Volume * 100
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get Position() {
|
||
|
if (this._Position === undefined) {
|
||
|
this._Position = 0;
|
||
|
}
|
||
|
|
||
|
return this._Position;
|
||
|
}
|
||
|
|
||
|
get MinimumRate() {
|
||
|
return 1.0;
|
||
|
}
|
||
|
|
||
|
get MaximumRate() {
|
||
|
return 1.0;
|
||
|
}
|
||
|
|
||
|
get CanGoNext() {
|
||
|
if (this._CanGoNext === undefined) {
|
||
|
this._CanGoNext = false;
|
||
|
}
|
||
|
|
||
|
return this._CanGoNext;
|
||
|
}
|
||
|
|
||
|
get CanGoPrevious() {
|
||
|
if (this._CanGoPrevious === undefined) {
|
||
|
this._CanGoPrevious = false;
|
||
|
}
|
||
|
|
||
|
return this._CanGoPrevious;
|
||
|
}
|
||
|
|
||
|
get CanPlay() {
|
||
|
if (this._CanPlay === undefined) {
|
||
|
this._CanPlay = false;
|
||
|
}
|
||
|
|
||
|
return this._CanPlay;
|
||
|
}
|
||
|
|
||
|
get CanPause() {
|
||
|
if (this._CanPause === undefined) {
|
||
|
this._CanPause = false;
|
||
|
}
|
||
|
|
||
|
return this._CanPause;
|
||
|
}
|
||
|
|
||
|
get CanSeek() {
|
||
|
if (this._CanSeek === undefined) {
|
||
|
this._CanSeek = false;
|
||
|
}
|
||
|
|
||
|
return this._CanSeek;
|
||
|
}
|
||
|
|
||
|
get CanControl() {
|
||
|
if (this._CanControl === undefined) {
|
||
|
this._CanControl = false;
|
||
|
}
|
||
|
|
||
|
return (this.CanPlay || this.CanPause);
|
||
|
}
|
||
|
|
||
|
Next() {
|
||
|
if (!this.CanControl || !this.CanGoNext) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'Next'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Previous() {
|
||
|
if (!this.CanControl || !this.CanGoPrevious) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'Previous'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Pause() {
|
||
|
if (!this.CanControl || !this.CanGoPause) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'Pause'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
PlayPause() {
|
||
|
if (!this.CanControl || !this.CanPause) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'PlayPause'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Stop() {
|
||
|
if (!this.CanControl) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'Stop'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Play() {
|
||
|
if (!this.CanControl || !this.CanPlay) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
action: 'Next'
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
Seek(offset) {
|
||
|
if (!this.CanControl || !this.CanSeek) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
Seek: offset
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
SetPosition(trackId, position) {
|
||
|
debug(`${this._Identity}: SetPosition(${trackId}, ${position})`);
|
||
|
|
||
|
if (!this.CanControl || !this.CanSeek) return;
|
||
|
|
||
|
this.device.sendPacket({
|
||
|
type: 'kdeconnect.mpris.request',
|
||
|
body: {
|
||
|
player: this.Identity,
|
||
|
SetPosition: position / 1000
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
OpenUri(uri) {
|
||
|
debug(`OpenUri(${uri}): Not Supported`);
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
if (this.__disposed === undefined) {
|
||
|
this.__disposed = true;
|
||
|
|
||
|
this.unexport();
|
||
|
|
||
|
if (this._connection) {
|
||
|
this._connection.close(null, null);
|
||
|
this._connection = null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|