mpris.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GLib from 'gi://GLib';
  6. import GObject from 'gi://GObject';
  7. import * as Components from '../components/index.js';
  8. import Config from '../../config.js';
  9. import * as Core from '../core.js';
  10. import * as DBus from '../utils/dbus.js';
  11. import {Player} from '../components/mpris.js';
  12. import Plugin from '../plugin.js';
  13. export const Metadata = {
  14. label: _('MPRIS'),
  15. description: _('Bidirectional remote media playback control'),
  16. id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.MPRIS',
  17. incomingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
  18. outgoingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'],
  19. actions: {},
  20. };
  21. /**
  22. * MPRIS Plugin
  23. * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mpriscontrol
  24. *
  25. * See also:
  26. * https://specifications.freedesktop.org/mpris-spec/latest/
  27. * https://github.com/GNOME/gnome-shell/blob/master/js/ui/mpris.js
  28. */
  29. const MPRISPlugin = GObject.registerClass({
  30. GTypeName: 'GSConnectMPRISPlugin',
  31. }, class MPRISPlugin extends Plugin {
  32. _init(device) {
  33. super._init(device, 'mpris');
  34. this._players = new Map();
  35. this._transferring = new WeakSet();
  36. this._updating = new WeakSet();
  37. this._queueTimers = new Map();
  38. this._mpris = Components.acquire('mpris');
  39. this._playerAddedId = this._mpris.connect(
  40. 'player-added',
  41. this._sendPlayerList.bind(this)
  42. );
  43. this._playerRemovedId = this._mpris.connect(
  44. 'player-removed',
  45. this._sendPlayerList.bind(this)
  46. );
  47. this._playerChangedId = this._mpris.connect(
  48. 'player-changed',
  49. this._onPlayerChanged.bind(this)
  50. );
  51. this._playerSeekedId = this._mpris.connect(
  52. 'player-seeked',
  53. this._onPlayerSeeked.bind(this)
  54. );
  55. }
  56. connected() {
  57. super.connected();
  58. this._requestPlayerList();
  59. this._sendPlayerList();
  60. }
  61. disconnected() {
  62. super.disconnected();
  63. for (const [identity, timer] of this._queueTimers) {
  64. if (timer)
  65. GLib.source_remove(timer);
  66. this._queueTimers.delete(identity);
  67. }
  68. for (const [identity, player] of this._players) {
  69. this._players.delete(identity);
  70. player.destroy();
  71. }
  72. }
  73. handlePacket(packet) {
  74. switch (packet.type) {
  75. case 'kdeconnect.mpris':
  76. this._handleUpdate(packet);
  77. break;
  78. case 'kdeconnect.mpris.request':
  79. this._handleRequest(packet);
  80. break;
  81. }
  82. }
  83. /**
  84. * Handle a remote player update.
  85. *
  86. * @param {Core.Packet} packet - A `kdeconnect.mpris`
  87. */
  88. _handleUpdate(packet) {
  89. try {
  90. if (packet.body.hasOwnProperty('playerList'))
  91. this._handlePlayerList(packet.body.playerList);
  92. else if (packet.body.hasOwnProperty('player'))
  93. this._handlePlayerUpdate(packet);
  94. } catch (e) {
  95. debug(e, this.device.name);
  96. }
  97. }
  98. /**
  99. * Handle an updated list of remote players.
  100. *
  101. * @param {string[]} playerList - A list of remote player names
  102. */
  103. _handlePlayerList(playerList) {
  104. // Destroy removed players before adding new ones
  105. for (const player of this._players.values()) {
  106. if (!playerList.includes(player.Identity)) {
  107. this._players.delete(player.Identity);
  108. player.destroy();
  109. }
  110. }
  111. for (const identity of playerList) {
  112. if (!this._players.has(identity)) {
  113. const player = new PlayerRemote(this.device, identity);
  114. this._players.set(identity, player);
  115. }
  116. // Always request player updates; packets are cheap
  117. this.device.sendPacket({
  118. type: 'kdeconnect.mpris.request',
  119. body: {
  120. player: identity,
  121. requestNowPlaying: true,
  122. requestVolume: true,
  123. },
  124. });
  125. }
  126. }
  127. /**
  128. * Handle an update for a remote player.
  129. *
  130. * @param {object} packet - A `kdeconnect.mpris` packet
  131. */
  132. _handlePlayerUpdate(packet) {
  133. const player = this._players.get(packet.body.player);
  134. if (player === undefined)
  135. return;
  136. if (packet.body.hasOwnProperty('transferringAlbumArt'))
  137. player.handleAlbumArt(packet);
  138. else
  139. player.update(packet.body);
  140. }
  141. /**
  142. * Request a list of remote players.
  143. */
  144. _requestPlayerList() {
  145. this.device.sendPacket({
  146. type: 'kdeconnect.mpris.request',
  147. body: {
  148. requestPlayerList: true,
  149. },
  150. });
  151. }
  152. /**
  153. * Handle a request for player information or action.
  154. *
  155. * @param {Core.Packet} packet - a `kdeconnect.mpris.request`
  156. * @returns {undefined} no return value
  157. */
  158. _handleRequest(packet) {
  159. // A request for the list of players
  160. if (packet.body.hasOwnProperty('requestPlayerList'))
  161. return this._sendPlayerList();
  162. // A request for an unknown player; send the list of players
  163. if (!this._mpris.hasPlayer(packet.body.player))
  164. return this._sendPlayerList();
  165. // An album art request
  166. if (packet.body.hasOwnProperty('albumArtUrl'))
  167. return this._sendAlbumArt(packet);
  168. // A player command
  169. this._handleCommand(packet);
  170. }
  171. /**
  172. * Handle an incoming player command or information request
  173. *
  174. * @param {Core.Packet} packet - A `kdeconnect.mpris.request`
  175. */
  176. async _handleCommand(packet) {
  177. if (!this.settings.get_boolean('share-players'))
  178. return;
  179. let player;
  180. try {
  181. player = this._mpris.getPlayer(packet.body.player);
  182. if (player === undefined || this._updating.has(player))
  183. return;
  184. this._updating.add(player);
  185. // Player Actions
  186. if (packet.body.hasOwnProperty('action')) {
  187. switch (packet.body.action) {
  188. case 'PlayPause':
  189. case 'Play':
  190. case 'Pause':
  191. case 'Next':
  192. case 'Previous':
  193. case 'Stop':
  194. player[packet.body.action]();
  195. break;
  196. default:
  197. debug(`unknown action: ${packet.body.action}`);
  198. }
  199. }
  200. // Player Properties
  201. if (packet.body.hasOwnProperty('setLoopStatus'))
  202. player.LoopStatus = packet.body.setLoopStatus;
  203. if (packet.body.hasOwnProperty('setShuffle'))
  204. player.Shuffle = packet.body.setShuffle;
  205. if (packet.body.hasOwnProperty('setVolume'))
  206. player.Volume = packet.body.setVolume / 100;
  207. if (packet.body.hasOwnProperty('Seek'))
  208. await player.Seek(packet.body.Seek);
  209. if (packet.body.hasOwnProperty('SetPosition')) {
  210. // We want to avoid implementing this as a seek operation,
  211. // because some players seek a fixed amount for every
  212. // seek request, only respecting the sign of the parameter.
  213. // (Chrome, for example, will only seek ±5 seconds, regardless
  214. // what value is passed to Seek().)
  215. const position = packet.body.SetPosition;
  216. const metadata = player.Metadata;
  217. if (metadata.hasOwnProperty('mpris:trackid')) {
  218. const trackId = metadata['mpris:trackid'];
  219. await player.SetPosition(trackId, position * 1000);
  220. } else {
  221. await player.Seek(position * 1000 - player.Position);
  222. }
  223. }
  224. if (packet.body.hasOwnProperty('requestNowPlaying') ||
  225. packet.body.hasOwnProperty('requestVolume')) {
  226. const response = this._getUpdate(player.Identity, packet);
  227. this._sendUpdate(player, response);
  228. }
  229. } catch (e) {
  230. debug(e, this.device.name);
  231. } finally {
  232. this._updating.delete(player);
  233. }
  234. }
  235. // Respond to information request (or push updated information)
  236. _getUpdate(identity, packet) {
  237. const player = this._mpris?.getPlayer(identity);
  238. if (!player)
  239. return;
  240. const response = {
  241. type: 'kdeconnect.mpris',
  242. body: {
  243. player: player.Identity,
  244. },
  245. };
  246. try {
  247. if (packet.body.hasOwnProperty('requestNowPlaying')) {
  248. Object.assign(response.body, {
  249. pos: Math.floor(player.Position / 1000),
  250. isPlaying: (player.PlaybackStatus === 'Playing'),
  251. canPause: player.CanPause,
  252. canPlay: player.CanPlay,
  253. canGoNext: player.CanGoNext,
  254. canGoPrevious: player.CanGoPrevious,
  255. canSeek: player.CanSeek,
  256. loopStatus: player.LoopStatus,
  257. shuffle: player.Shuffle,
  258. // default values for members that will be filled conditionally
  259. albumArtUrl: '',
  260. length: 0,
  261. artist: '',
  262. title: '',
  263. album: '',
  264. nowPlaying: '',
  265. volume: 0,
  266. });
  267. const metadata = player.Metadata;
  268. if (metadata.hasOwnProperty('mpris:artUrl')) {
  269. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  270. response.body.albumArtUrl = file.get_uri();
  271. }
  272. if (metadata.hasOwnProperty('mpris:length')) {
  273. const trackLen = Math.floor(metadata['mpris:length'] / 1000);
  274. response.body.length = trackLen;
  275. }
  276. if (metadata.hasOwnProperty('xesam:artist')) {
  277. const artists = metadata['xesam:artist'];
  278. response.body.artist = artists.join(', ');
  279. }
  280. if (metadata.hasOwnProperty('xesam:title'))
  281. response.body.title = metadata['xesam:title'];
  282. if (metadata.hasOwnProperty('xesam:album'))
  283. response.body.album = metadata['xesam:album'];
  284. // Now Playing
  285. if (response.body.artist && response.body.title) {
  286. response.body.nowPlaying = [
  287. response.body.artist,
  288. response.body.title,
  289. ].join(' - ');
  290. } else if (response.body.artist) {
  291. response.body.nowPlaying = response.body.artist;
  292. } else if (response.body.title) {
  293. response.body.nowPlaying = response.body.title;
  294. } else {
  295. response.body.nowPlaying = _('Unknown');
  296. }
  297. }
  298. if (packet.body.hasOwnProperty('requestVolume'))
  299. response.body.volume = Math.floor(player.Volume * 100);
  300. return response;
  301. } catch (e) {
  302. debug(e, this.device.name);
  303. }
  304. }
  305. _sendUpdate(player, packet = null) {
  306. if (!player || (!packet && this._updating.has(player)))
  307. return GLib.SOURCE_REMOVE;
  308. debug(`Sending update for ${player.Identity}`);
  309. this._updating.add(player);
  310. if (this._queueTimers.has(player.Identity)) {
  311. const timer_id = this._queueTimers.get(player.Identity);
  312. if (timer_id) {
  313. debug(`Stopping timer id ${timer_id}`);
  314. GLib.source_remove(timer_id);
  315. }
  316. this._queueTimers.delete(player.Identity);
  317. }
  318. if (!packet) {
  319. packet = this._getUpdate(player.Identity, {
  320. body: {
  321. requestNowPlaying: true,
  322. requestVolume: true,
  323. },
  324. }, false);
  325. }
  326. this.device.sendPacket(packet);
  327. this._updating.delete(player);
  328. return GLib.SOURCE_REMOVE;
  329. }
  330. _onPlayerChanged(mpris, player) {
  331. if (!this.settings.get_boolean('share-players'))
  332. return;
  333. // Set a timer to send the updated state after a short delay.
  334. // Allows further state changes to be bundled into a single packet.
  335. if (this._queueTimers.has(player.Identity))
  336. return;
  337. this._queueTimers.set(player.Identity, 0);
  338. const timer_id = GLib.timeout_add(
  339. GLib.PRIORITY_DEFAULT,
  340. 250, // ms (0.25 seconds)
  341. this._sendUpdate.bind(this, player)
  342. );
  343. this._queueTimers.set(player.Identity, timer_id);
  344. debug(`Set update timer id ${timer_id} for ${player.Identity}`);
  345. }
  346. _onPlayerSeeked(mpris, player, offset) {
  347. // TODO: although we can handle full seeked signals, kdeconnect-android
  348. // does not, and expects a position update instead
  349. this.device.sendPacket({
  350. type: 'kdeconnect.mpris',
  351. body: {
  352. player: player.Identity,
  353. pos: Math.floor(player.Position / 1000),
  354. // Seek: Math.floor(offset / 1000),
  355. },
  356. });
  357. }
  358. async _sendAlbumArt(packet) {
  359. let player;
  360. try {
  361. // Reject concurrent requests for album art
  362. player = this._mpris.getPlayer(packet.body.player);
  363. if (player === undefined || this._transferring.has(player))
  364. return;
  365. // Ensure the requested albumArtUrl matches the current mpris:artUrl
  366. const metadata = player.Metadata;
  367. if (!metadata.hasOwnProperty('mpris:artUrl'))
  368. return;
  369. const file = Gio.File.new_for_uri(metadata['mpris:artUrl']);
  370. const request = Gio.File.new_for_uri(packet.body.albumArtUrl);
  371. if (file.get_uri() !== request.get_uri())
  372. throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`);
  373. // Transfer the album art
  374. this._transferring.add(player);
  375. const transfer = this.device.createTransfer();
  376. transfer.addFile({
  377. type: 'kdeconnect.mpris',
  378. body: {
  379. transferringAlbumArt: true,
  380. player: packet.body.player,
  381. albumArtUrl: packet.body.albumArtUrl,
  382. },
  383. }, file);
  384. await transfer.start();
  385. } catch (e) {
  386. debug(e, this.device.name);
  387. } finally {
  388. this._transferring.delete(player);
  389. }
  390. }
  391. /**
  392. * Send the list of player identities and indicate whether we support
  393. * transferring album art
  394. */
  395. _sendPlayerList() {
  396. let playerList = [];
  397. if (this.settings.get_boolean('share-players'))
  398. playerList = this._mpris.getIdentities();
  399. this.device.sendPacket({
  400. type: 'kdeconnect.mpris',
  401. body: {
  402. playerList: playerList,
  403. supportAlbumArtPayload: true,
  404. },
  405. });
  406. }
  407. destroy() {
  408. for (const [identity, timer] of this._queueTimers) {
  409. this._queueTimers.delete(identity);
  410. GLib.source_remove(timer);
  411. }
  412. if (this._mpris !== undefined) {
  413. this._mpris.disconnect(this._playerAddedId);
  414. this._mpris.disconnect(this._playerRemovedId);
  415. this._mpris.disconnect(this._playerChangedId);
  416. this._mpris.disconnect(this._playerSeekedId);
  417. this._mpris = Components.release('mpris');
  418. }
  419. for (const [identity, player] of this._players) {
  420. this._players.delete(identity);
  421. player.destroy();
  422. }
  423. super.destroy();
  424. }
  425. });
  426. /*
  427. * A class for mirroring a remote Media Player on DBus
  428. */
  429. const PlayerRemote = GObject.registerClass({
  430. GTypeName: 'GSConnectMPRISPlayerRemote',
  431. }, class PlayerRemote extends Player {
  432. _init(device, identity) {
  433. super._init();
  434. this._device = device;
  435. this._Identity = identity;
  436. this._isPlaying = false;
  437. this._artist = null;
  438. this._title = null;
  439. this._album = null;
  440. this._length = 0;
  441. this._artUrl = null;
  442. this._ownerId = 0;
  443. this._connection = null;
  444. this._applicationIface = null;
  445. this._playerIface = null;
  446. }
  447. _getFile(albumArtUrl) {
  448. const hash = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5,
  449. albumArtUrl, -1);
  450. const path = GLib.build_filenamev([Config.CACHEDIR, hash]);
  451. return Gio.File.new_for_uri(`file://${path}`);
  452. }
  453. _requestAlbumArt(state) {
  454. if (this._artUrl === state.albumArtUrl)
  455. return;
  456. const file = this._getFile(state.albumArtUrl);
  457. if (file.query_exists(null)) {
  458. this._artUrl = file.get_uri();
  459. this._Metadata = undefined;
  460. this.notify('Metadata');
  461. } else {
  462. this.device.sendPacket({
  463. type: 'kdeconnect.mpris.request',
  464. body: {
  465. player: this.Identity,
  466. albumArtUrl: state.albumArtUrl,
  467. },
  468. });
  469. }
  470. }
  471. _updateMetadata(state) {
  472. let metadataChanged = false;
  473. if (state.hasOwnProperty('artist')) {
  474. if (this._artist !== state.artist) {
  475. this._artist = state.artist;
  476. metadataChanged = true;
  477. }
  478. } else if (this._artist) {
  479. this._artist = null;
  480. metadataChanged = true;
  481. }
  482. if (state.hasOwnProperty('title')) {
  483. if (this._title !== state.title) {
  484. this._title = state.title;
  485. metadataChanged = true;
  486. }
  487. } else if (this._title) {
  488. this._title = null;
  489. metadataChanged = true;
  490. }
  491. if (state.hasOwnProperty('album')) {
  492. if (this._album !== state.album) {
  493. this._album = state.album;
  494. metadataChanged = true;
  495. }
  496. } else if (this._album) {
  497. this._album = null;
  498. metadataChanged = true;
  499. }
  500. if (state.hasOwnProperty('length')) {
  501. if (this._length !== state.length * 1000) {
  502. this._length = state.length * 1000;
  503. metadataChanged = true;
  504. }
  505. } else if (this._length) {
  506. this._length = 0;
  507. metadataChanged = true;
  508. }
  509. if (state.hasOwnProperty('albumArtUrl')) {
  510. this._requestAlbumArt(state);
  511. } else if (this._artUrl) {
  512. this._artUrl = null;
  513. metadataChanged = true;
  514. }
  515. if (metadataChanged) {
  516. this._Metadata = undefined;
  517. this.notify('Metadata');
  518. }
  519. }
  520. async export() {
  521. try {
  522. if (this._connection === null) {
  523. this._connection = await DBus.newConnection();
  524. const MPRISIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2');
  525. const MPRISPlayerIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2.Player');
  526. if (this._applicationIface === null) {
  527. this._applicationIface = new DBus.Interface({
  528. g_instance: this,
  529. g_connection: this._connection,
  530. g_object_path: '/org/mpris/MediaPlayer2',
  531. g_interface_info: MPRISIface,
  532. });
  533. }
  534. if (this._playerIface === null) {
  535. this._playerIface = new DBus.Interface({
  536. g_instance: this,
  537. g_connection: this._connection,
  538. g_object_path: '/org/mpris/MediaPlayer2',
  539. g_interface_info: MPRISPlayerIface,
  540. });
  541. }
  542. }
  543. if (this._ownerId !== 0)
  544. return;
  545. const name = [
  546. this.device.name,
  547. this.Identity,
  548. ].join('').replace(/[\W]*/g, '');
  549. this._ownerId = Gio.bus_own_name_on_connection(
  550. this._connection,
  551. `org.mpris.MediaPlayer2.GSConnect.${name}`,
  552. Gio.BusNameOwnerFlags.NONE,
  553. null,
  554. null
  555. );
  556. } catch (e) {
  557. debug(e, this.Identity);
  558. }
  559. }
  560. unexport() {
  561. if (this._ownerId === 0)
  562. return;
  563. Gio.bus_unown_name(this._ownerId);
  564. this._ownerId = 0;
  565. }
  566. /**
  567. * Download album art for the current track of the remote player.
  568. *
  569. * @param {Core.Packet} packet - A `kdeconnect.mpris` packet
  570. */
  571. async handleAlbumArt(packet) {
  572. let file;
  573. try {
  574. file = this._getFile(packet.body.albumArtUrl);
  575. // Transfer the album art
  576. const transfer = this.device.createTransfer();
  577. transfer.addFile(packet, file);
  578. await transfer.start();
  579. this._artUrl = file.get_uri();
  580. this._Metadata = undefined;
  581. this.notify('Metadata');
  582. } catch (e) {
  583. debug(e, this.device.name);
  584. if (file)
  585. file.delete_async(GLib.PRIORITY_DEFAULT, null, null);
  586. }
  587. }
  588. /**
  589. * Update the internal state of the media player.
  590. *
  591. * @param {Core.Packet} state - The body of a `kdeconnect.mpris` packet
  592. */
  593. update(state) {
  594. this.freeze_notify();
  595. // Metadata
  596. if (state.hasOwnProperty('nowPlaying') ||
  597. state.hasOwnProperty('artist') ||
  598. state.hasOwnProperty('title'))
  599. this._updateMetadata(state);
  600. // Playback Status
  601. if (state.hasOwnProperty('isPlaying')) {
  602. if (this._isPlaying !== state.isPlaying) {
  603. this._isPlaying = state.isPlaying;
  604. this.notify('PlaybackStatus');
  605. }
  606. }
  607. if (state.hasOwnProperty('canPlay')) {
  608. if (this.CanPlay !== state.canPlay) {
  609. this._CanPlay = state.canPlay;
  610. this.notify('CanPlay');
  611. }
  612. }
  613. if (state.hasOwnProperty('canPause')) {
  614. if (this.CanPause !== state.canPause) {
  615. this._CanPause = state.canPause;
  616. this.notify('CanPause');
  617. }
  618. }
  619. if (state.hasOwnProperty('canGoNext')) {
  620. if (this.CanGoNext !== state.canGoNext) {
  621. this._CanGoNext = state.canGoNext;
  622. this.notify('CanGoNext');
  623. }
  624. }
  625. if (state.hasOwnProperty('canGoPrevious')) {
  626. if (this.CanGoPrevious !== state.canGoPrevious) {
  627. this._CanGoPrevious = state.canGoPrevious;
  628. this.notify('CanGoPrevious');
  629. }
  630. }
  631. if (state.hasOwnProperty('pos'))
  632. this._Position = state.pos * 1000;
  633. if (state.hasOwnProperty('volume')) {
  634. if (this.Volume !== state.volume / 100) {
  635. this._Volume = state.volume / 100;
  636. this.notify('Volume');
  637. }
  638. }
  639. this.thaw_notify();
  640. if (!this._isPlaying && !this.CanControl)
  641. this.unexport();
  642. else
  643. this.export();
  644. }
  645. /*
  646. * Native properties
  647. */
  648. get device() {
  649. return this._device;
  650. }
  651. /*
  652. * The org.mpris.MediaPlayer2.Player Interface
  653. */
  654. get CanControl() {
  655. return (this.CanPlay || this.CanPause);
  656. }
  657. get Metadata() {
  658. if (this._Metadata === undefined) {
  659. this._Metadata = {};
  660. if (this._artist) {
  661. this._Metadata['xesam:artist'] = new GLib.Variant('as',
  662. [this._artist]);
  663. }
  664. if (this._title) {
  665. this._Metadata['xesam:title'] = new GLib.Variant('s',
  666. this._title);
  667. }
  668. if (this._album) {
  669. this._Metadata['xesam:album'] = new GLib.Variant('s',
  670. this._album);
  671. }
  672. if (this._artUrl) {
  673. this._Metadata['mpris:artUrl'] = new GLib.Variant('s',
  674. this._artUrl);
  675. }
  676. this._Metadata['mpris:length'] = new GLib.Variant('x',
  677. this._length);
  678. }
  679. return this._Metadata;
  680. }
  681. get PlaybackStatus() {
  682. if (this._isPlaying)
  683. return 'Playing';
  684. return 'Stopped';
  685. }
  686. get Volume() {
  687. if (this._Volume === undefined)
  688. this._Volume = 0.3;
  689. return this._Volume;
  690. }
  691. set Volume(level) {
  692. if (this._Volume === level)
  693. return;
  694. this._Volume = level;
  695. this.notify('Volume');
  696. this.device.sendPacket({
  697. type: 'kdeconnect.mpris.request',
  698. body: {
  699. player: this.Identity,
  700. setVolume: Math.floor(this._Volume * 100),
  701. },
  702. });
  703. }
  704. Next() {
  705. if (!this.CanGoNext)
  706. return;
  707. this.device.sendPacket({
  708. type: 'kdeconnect.mpris.request',
  709. body: {
  710. player: this.Identity,
  711. action: 'Next',
  712. },
  713. });
  714. }
  715. Pause() {
  716. if (!this.CanPause)
  717. return;
  718. this.device.sendPacket({
  719. type: 'kdeconnect.mpris.request',
  720. body: {
  721. player: this.Identity,
  722. action: 'Pause',
  723. },
  724. });
  725. }
  726. Play() {
  727. if (!this.CanPlay)
  728. return;
  729. this.device.sendPacket({
  730. type: 'kdeconnect.mpris.request',
  731. body: {
  732. player: this.Identity,
  733. action: 'Play',
  734. },
  735. });
  736. }
  737. PlayPause() {
  738. if (!this.CanPlay && !this.CanPause)
  739. return;
  740. this.device.sendPacket({
  741. type: 'kdeconnect.mpris.request',
  742. body: {
  743. player: this.Identity,
  744. action: 'PlayPause',
  745. },
  746. });
  747. }
  748. Previous() {
  749. if (!this.CanGoPrevious)
  750. return;
  751. this.device.sendPacket({
  752. type: 'kdeconnect.mpris.request',
  753. body: {
  754. player: this.Identity,
  755. action: 'Previous',
  756. },
  757. });
  758. }
  759. Seek(offset) {
  760. if (!this.CanSeek)
  761. return;
  762. this.device.sendPacket({
  763. type: 'kdeconnect.mpris.request',
  764. body: {
  765. player: this.Identity,
  766. Seek: offset,
  767. },
  768. });
  769. }
  770. SetPosition(trackId, position) {
  771. debug(`${this._Identity}: SetPosition(${trackId}, ${position})`);
  772. if (!this.CanControl || !this.CanSeek)
  773. return;
  774. this.device.sendPacket({
  775. type: 'kdeconnect.mpris.request',
  776. body: {
  777. player: this.Identity,
  778. SetPosition: position / 1000,
  779. },
  780. });
  781. }
  782. Stop() {
  783. if (!this.CanControl)
  784. return;
  785. this.device.sendPacket({
  786. type: 'kdeconnect.mpris.request',
  787. body: {
  788. player: this.Identity,
  789. action: 'Stop',
  790. },
  791. });
  792. }
  793. destroy() {
  794. this.unexport();
  795. if (this._connection) {
  796. this._connection.close(null, null);
  797. this._connection = null;
  798. if (this._applicationIface) {
  799. this._applicationIface.destroy();
  800. this._applicationIface = null;
  801. }
  802. if (this._playerIface) {
  803. this._playerIface.destroy();
  804. this._playerIface = null;
  805. }
  806. }
  807. }
  808. });
  809. export default MPRISPlugin;