@jellybrick/mpris-service 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,477 @@
1
+ require('source-map-support').install();
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ const dbus = require('dbus-next');
6
+ const PlayerInterface = require('./interfaces/player');
7
+ const RootInterface = require('./interfaces/root');
8
+ const PlaylistsInterface = require('./interfaces/playlists');
9
+ const TracklistInterface = require('./interfaces/tracklist');
10
+ const types = require('./interfaces/types');
11
+ const constants = require('./constants');
12
+
13
+ const MPRIS_PATH = '/org/mpris/MediaPlayer2';
14
+
15
+ function lcfirst(str) {
16
+ return str[0].toLowerCase() + str.substring(1);
17
+ }
18
+
19
+ class Player extends EventEmitter {
20
+ /**
21
+ * Construct a new Player and export it on the DBus session bus.
22
+ *
23
+ * For more information about the properties of this class, see [the MPRIS DBus Interface Specification](https://specifications.freedesktop.org/mpris-spec/latest/).
24
+ *
25
+ * Method Call Events
26
+ * ------------------
27
+ *
28
+ * The Player is an `EventEmitter` that emits events when the corresponding
29
+ * methods are called on the DBus interface over the wire.
30
+ *
31
+ * The Player emits events whenever the corresponding methods on the DBus
32
+ * interface are called.
33
+ *
34
+ * * `raise` - Brings the media player's user interface to the front using any appropriate mechanism available.
35
+ * * `quit` - Causes the media player to stop running.
36
+ * * `next` - Skips to the next track in the tracklist.
37
+ * * `previous` - Skips to the previous track in the tracklist.
38
+ * * `pause` - Pauses playback.
39
+ * * `playPause` - Pauses playback. If playback is already paused, resumes playback. If playback is stopped, starts playback.
40
+ * * `stop` - Stops playback.
41
+ * * `play` - Starts or resumes playback.
42
+ * * `seek` - Seeks forward in the current track by the specified number of microseconds. With event data `offset`.
43
+ * * `position` - Sets the current track position in microseconds. With event data `{ trackId, position }`.
44
+ * * `open` - Opens the Uri given as an argument. With event data `{ uri }`.
45
+ * * `volume` - Sets the volume of the player. With event data `volume` (between 0.0 and 1.0).
46
+ * * `shuffle` - Sets whether shuffle is enabled on the player. With event data `shuffleStatus` (boolean).
47
+ * * `rate` - Sets the playback rate of the player. A value of 1.0 is the normal rate. With event data `rate`.
48
+ * * `loopStatus` - Sets the loop status of the player to either 'None', 'Track', or 'Playlist'. With event data `loopStatus`.
49
+ * * `activatePlaylist` - Starts playing the given playlist. With event data `playlistId`.
50
+ *
51
+ * The Player may also emit an `error` event with the underlying Node `Error`
52
+ * as the event data. After receiving this event, the Player may be
53
+ * disconnected.
54
+ *
55
+ * ```
56
+ * player.on('play', () => {
57
+ * realPlayer.play();
58
+ * });
59
+ *
60
+ * player.on('shuffle', (enableShuffle) => {
61
+ * realPlayer.setShuffle(enableShuffle);
62
+ * player.shuffle = enableShuffle;
63
+ * });
64
+ * ```
65
+ *
66
+ * Player Properties
67
+ * -----------------
68
+ *
69
+ * Player properties (documented below) should be kept up to date to reflect
70
+ * the state of your real player. These properties can be gotten by the client
71
+ * through the `org.freedesktop.DBus.Properties` interface which will return
72
+ * the value currently set on the player. Setting these properties on the
73
+ * player to a different value will emit the `PropertiesChanged` signal on the
74
+ * properties interface to notify clients that properties of the player have
75
+ * changed.
76
+ *
77
+ * ```
78
+ * realPlayer.on('shuffle:changed', (shuffleEnabled) => {
79
+ * player.shuffle = shuffleEnabled;
80
+ * });
81
+ *
82
+ * realPlayer.on('play', () => {
83
+ * player.playbackStatus = 'Playing';
84
+ * });
85
+ * ```
86
+ *
87
+ * Player Position
88
+ * ---------------
89
+ *
90
+ * Clients can get the position of your player by getting the `Position`
91
+ * property of the `org.mpris.MediaPlayer2.Player` interface. Since position
92
+ * updates continuously, {@link Player#getPosition} is implemented as a getter
93
+ * you can override on your Player. This getter will be called when a client
94
+ * requests the position and should return the position of your player for the
95
+ * client in microseconds.
96
+ *
97
+ * ```
98
+ * player.getPosition() {
99
+ * return realPlayer.getPositionInMicroseconds();
100
+ * }
101
+ * ```
102
+ *
103
+ * When your real player seeks to a new location, such as when someone clicks
104
+ * on the time bar, you can notify clients of the new position by calling the
105
+ * {@link Player#seeked} method. This will raise the `Seeked` signal on the
106
+ * `org.mpris.MediaPlayer2.Player` interface with the given current time of the
107
+ * player in microseconds.
108
+ *
109
+ * ```
110
+ * realPlayer.on('seeked', (positionInMicroseconds) => {
111
+ * player.seeked(positionInMicroseconds);
112
+ * });
113
+ * ```
114
+ *
115
+ * Clients can request to set position using the `Seek` and `SetPosition`
116
+ * methods of the `org.mpris.MediaPlayer2.Player` interface. These requests are
117
+ * implemented as events on the Player similar to the other requests.
118
+ *
119
+ * ```
120
+ * player.on('seek', (offset) => {
121
+ * // note that offset may be negative
122
+ * let currentPosition = realPlayer.getPositionInMicroseconds();
123
+ * let newPosition = currentPosition + offset;
124
+ * realPlayer.setPosition(newPosition);
125
+ * });
126
+ *
127
+ * player.on('position', (event) => {
128
+ * // check that event.trackId is the current track before continuing.
129
+ * realPlayer.setPosition(event.position);
130
+ * });
131
+ * ```
132
+ *
133
+ * @class Player
134
+ * @param {
135
+ * name: String,
136
+ * identity: String,
137
+ * supportedMimeTypes: string[],
138
+ * supportedInterfaces: string[]
139
+ * } options - Options for the player.
140
+ * @param {String} options.name - Name on the bus to export to as `org.mpris.MediaPlayer2.{name}`.
141
+ * @param {String} options.identity - Identity for the player to display on the root media player interface.
142
+ * @param {Array} options.supportedMimeTypes - Mime types this player can open with the `org.mpris.MediaPlayer2.Open` method.
143
+ * @param {Array} options.supportedInterfaces - The interfaces this player supports. Can include `'player'`, `'playlists'`, and `'trackList'`.
144
+ * @property {String} identity - A friendly name to identify the media player to users.
145
+ * @property {Boolean} fullscreen - Whether the media player is occupying the fullscreen.
146
+ * @property {Array} supportedUriSchemes - The URI schemes supported by the media player.
147
+ * @property {Array} supportedMimeTypes - The mime-types supported by the media player.
148
+ * @property {Boolean} canQuit - Whether the player can quit.
149
+ * @property {Boolean} canRaise - Whether the player can raise.
150
+ * @property {Boolean} canSetFullscreen - Whether the player can be set to fullscreen.
151
+ * @property {Boolean} hasTrackList - Indicates whether the /org/mpris/MediaPlayer2 object implements the org.mpris.MediaPlayer2.TrackList interface.
152
+ * @property {String} desktopEntry - The basename of an installed .desktop file which complies with the Desktop entry specification, with the ".desktop" extension stripped.
153
+ * @property {String} playbackStatus - The current playback status. May be "Playing", "Paused" or "Stopped".
154
+ * @property {String} loopStatus - The current loop/repeat status. May be "None", "Track", or "Playlist".
155
+ * @property {Boolean} shuffle - Whether the player is shuffling.
156
+ * @property {Object} metadata - The metadata of the current element. If there is a current track, this must have a "mpris:trackid" entry (of D-Bus type "o") at the very least, which contains a D-Bus path that uniquely identifies this track.
157
+ * @property {Double} volume - The volume level.
158
+ * @property {Boolean} canControl - Whether the media player may be controlled over this interface.
159
+ * @property {Boolean} canPause - Whether playback can be paused using Pause or PlayPause.
160
+ * @property {Boolean} canPlay - Whether playback can be started using Play or PlayPause.
161
+ * @property {Boolean} canSeek - Whether the client can control the playback position using Seek and SetPosition.
162
+ * @property {Boolean} canGoNext - Whether the client can call the Next method on this interface and expect the current track to change.
163
+ * @property {Boolean} canGoPrevious - Whether the client can call the Previous method on this interface and expect the current track to change.
164
+ * @property {Double} rate - The current playback rate.
165
+ * @property {Double} minimumRate - The minimum value which the Rate property can take.
166
+ * @property {Double} maximumRate - The maximum value which the Rate property can take.
167
+ * @property {Array} playlists - The current playlists set by {@link Player#setPlaylists}. (Not a DBus property).
168
+ * @property {String} activePlaylist - The id of the currently-active playlist.
169
+ */
170
+ constructor(options) {
171
+ super();
172
+
173
+ this.name = options.name;
174
+ this.supportedInterfaces = options.supportedInterfaces || ['player'];
175
+ this._tracks = [];
176
+ this.init(options);
177
+ }
178
+
179
+ init(opts) {
180
+ this.serviceName = `org.mpris.MediaPlayer2.${this.name}`;
181
+ dbus.validators.assertBusNameValid(this.serviceName);
182
+
183
+ this._bus = dbus.sessionBus();
184
+
185
+ this._bus.on('error', (err) => {
186
+ this.emit('error', err);
187
+ });
188
+
189
+ this.interfaces = {};
190
+
191
+ this.#addRootInterface(this._bus, opts);
192
+
193
+ if (this.supportedInterfaces.indexOf('player') >= 0) {
194
+ this.#addPlayerInterface(this._bus);
195
+ }
196
+ if (this.supportedInterfaces.indexOf('trackList') >= 0) {
197
+ this.#addTracklistInterface(this._bus);
198
+ }
199
+ if (this.supportedInterfaces.indexOf('playlists') >= 0) {
200
+ this.#addPlaylistsInterface(this._bus);
201
+ }
202
+
203
+ for (let k of Object.keys(this.interfaces)) {
204
+ let iface = this.interfaces[k];
205
+ this._bus.export(MPRIS_PATH, iface);
206
+ }
207
+
208
+ this._bus.requestName(this.serviceName, dbus.NameFlag.DO_NOT_QUEUE)
209
+ .then((reply) => {
210
+ if (reply === dbus.RequestNameReply.EXISTS) {
211
+ this.serviceName = `${this.serviceName}.instance${process.pid}`;
212
+ return this._bus.requestName(this.serviceName);
213
+ }
214
+ })
215
+ .catch((err) => {
216
+ this.emit('error', err);
217
+ });
218
+ }
219
+
220
+ #addRootInterface(bus, opts) {
221
+ this.interfaces.root = new RootInterface(this, opts);
222
+ this.#addEventedPropertiesList(this.interfaces.root,
223
+ ['Identity', 'Fullscreen', 'SupportedUriSchemes', 'SupportedMimeTypes',
224
+ 'CanQuit', 'CanRaise', 'CanSetFullscreen', 'HasTrackList',
225
+ 'DesktopEntry']);
226
+ }
227
+
228
+ #addPlayerInterface(bus) {
229
+ this.interfaces.player = new PlayerInterface(this);
230
+ let eventedProps = ['PlaybackStatus', 'LoopStatus', 'Rate', 'Shuffle',
231
+ 'Metadata', 'Volume', 'CanControl', 'CanPause', 'CanPlay', 'CanSeek',
232
+ 'CanGoNext', 'CanGoPrevious', 'MinimumRate', 'MaximumRate'];
233
+ this.#addEventedPropertiesList(this.interfaces.player, eventedProps);
234
+ }
235
+
236
+ #addTracklistInterface(bus) {
237
+ this.interfaces.tracklist = new TracklistInterface(this);
238
+ this.#addEventedPropertiesList(this.interfaces.tracklist, ['CanEditTracks']);
239
+
240
+ Object.defineProperty(this, 'tracks', {
241
+ get: function() {
242
+ return this._tracks;
243
+ },
244
+ set: function(value) {
245
+ this._tracks = value;
246
+ this.interfaces.tracklist.TrackListReplaced(value);
247
+ },
248
+ enumerable: true,
249
+ configurable: true
250
+ });
251
+ }
252
+
253
+ #addPlaylistsInterface(bus) {
254
+ this.interfaces.playlists = new PlaylistsInterface(this);
255
+ this.#addEventedPropertiesList(this.interfaces.playlists,
256
+ ['PlaylistCount', 'ActivePlaylist']);
257
+ }
258
+
259
+ /**
260
+ * Get a valid object path with the `subpath` as the basename which is suitable
261
+ * for use as an id.
262
+ *
263
+ * @name Player#objectPath
264
+ * @function
265
+ * @param {String} subpath - The basename of this path
266
+ * @returns {String} - A valid object path that can be used as an id.
267
+ */
268
+ objectPath(subpath) {
269
+ let path = `/org/node/mediaplayer/${this.name}`;
270
+ if (subpath) {
271
+ path += `/${subpath}`;
272
+ }
273
+ return path;
274
+ }
275
+
276
+ #addEventedProperty(iface, name) {
277
+ let localName = lcfirst(name);
278
+
279
+ Object.defineProperty(this, localName, {
280
+ get: function() {
281
+ let value = iface[name];
282
+ if (name === 'ActivePlaylist') {
283
+ return types.playlistToPlain(value);
284
+ } else if (name === 'Metadata') {
285
+ return types.metadataToPlain(value);
286
+ }
287
+ return value;
288
+ },
289
+ set: function(value) {
290
+ iface.setProperty(name, value);
291
+ },
292
+ enumerable: true,
293
+ configurable: true
294
+ });
295
+ }
296
+
297
+ #addEventedPropertiesList(iface, props) {
298
+ for (let i = 0; i < props.length; i++) {
299
+ this.#addEventedProperty(iface, props[i]);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Gets the position of this player. This method is intended to be overridden
305
+ * by the user to return the position of the player in microseconds.
306
+ *
307
+ * @name Player#getPosition
308
+ * @function
309
+ * @returns {Integer} - The current position of the player in microseconds.
310
+ */
311
+ getPosition() {
312
+ return 0;
313
+ }
314
+
315
+ /**
316
+ * Emits the `Seeked` DBus signal to listening clients with the given position.
317
+ *
318
+ * @name Player#seeked
319
+ * @function
320
+ * @param {Integer} position - The position in microseconds.
321
+ */
322
+ seeked(position) {
323
+ let seekTo = Math.floor(position || 0);
324
+ if (isNaN(seekTo)) {
325
+ throw new Error(`seeked expected a number (got ${position})`);
326
+ }
327
+ this.interfaces.player.Seeked(seekTo);
328
+ }
329
+
330
+ getTrackIndex(trackId) {
331
+ for (let i = 0; i < this.tracks.length; i++) {
332
+ let track = this.tracks[i];
333
+
334
+ if (track['mpris:trackid'] === trackId) {
335
+ return i;
336
+ }
337
+ }
338
+
339
+ return -1;
340
+ }
341
+
342
+ getTrack(trackId) {
343
+ return this.tracks[this.getTrackIndex(trackId)];
344
+ }
345
+
346
+ addTrack(track) {
347
+ this.tracks.push(track);
348
+ this.interfaces.tracklist.setTracks(this.tracks);
349
+
350
+ let afterTrack = '/org/mpris/MediaPlayer2/TrackList/NoTrack';
351
+ if (this.tracks.length > 2) {
352
+ afterTrack = this.tracks[this.tracks.length - 2]['mpris:trackid'];
353
+ }
354
+ this.interfaces.tracklist.TrackAdded(afterTrack);
355
+ }
356
+
357
+ removeTrack(trackId) {
358
+ let i = this.getTrackIndex(trackId);
359
+ this.tracks.splice(i, 1);
360
+ this.interfaces.tracklist.setTracks(this.tracks);
361
+
362
+ this.interfaces.tracklist.TrackRemoved(trackId);
363
+ }
364
+
365
+ /**
366
+ * Get the index of a playlist entry in the `playlists` list property of the
367
+ * player from the given id.
368
+ *
369
+ * @name Player#getPlaylistIndex
370
+ * @function
371
+ * @param {String} playlistId - The id for the playlist entry.
372
+ */
373
+ getPlaylistIndex(playlistId) {
374
+ for (let i = 0; i < this.playlists.length; i++) {
375
+ let playlist = this.playlists[i];
376
+
377
+ if (playlist.Id === playlistId) {
378
+ return i;
379
+ }
380
+ }
381
+
382
+ return -1;
383
+ }
384
+
385
+ /**
386
+ * Set the list of playlists advertised to listeners on the bus. Each playlist
387
+ * must have string members `Id`, `Name`, and `Icon`.
388
+ *
389
+ * @name Player#setPlaylists
390
+ * @function
391
+ * @param {Array} playlists - A list of playlists.
392
+ */
393
+ setPlaylists(playlists) {
394
+ this.playlists = playlists;
395
+ this.playlistCount = playlists.length;
396
+
397
+ this.playlists.forEach((playlist) => {
398
+ if (playlist) {
399
+ this.interfaces.playlists.PlaylistChanged(playlist);
400
+ }
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Set the playlist identified by `playlistId` to be the currently active
406
+ * playlist.
407
+ *
408
+ * @name Player#setActivePlaylist
409
+ * @function
410
+ * @param {String} playlistId - The id of the playlist to activate.
411
+ */
412
+ setActivePlaylist(playlistId) {
413
+ this.interfaces.playlists.setActivePlaylistId(playlistId);
414
+ }
415
+
416
+ /**
417
+ * Enumerated value for the `playbackStatus` property of the player to indicate
418
+ * a track is currently playing.
419
+ *
420
+ * @name Player#PLAYBACK_STATUS_PLAYING
421
+ * @static
422
+ * @constant
423
+ */
424
+ static PLAYBACK_STATUS_PLAYING = constants.PLAYBACK_STATUS_PLAYING;
425
+ /**
426
+ * Enumerated value for the `playbackStatus` property of the player to indicate
427
+ * a track is currently paused.
428
+ *
429
+ * @name Player#PLAYBACK_STATUS_PAUSED
430
+ * @static
431
+ * @constant
432
+ */
433
+ static PLAYBACK_STATUS_PAUSED = constants.PLAYBACK_STATUS_PAUSED;
434
+
435
+ /**
436
+ * Enumerated value for the `playbackStatus` property of the player to indicate
437
+ * there is no track currently playing.
438
+ *
439
+ * @name Player#PLAYBACK_STATUS_STOPPED
440
+ * @static
441
+ * @constant
442
+ */
443
+ static PLAYBACK_STATUS_STOPPED = constants.PLAYBACK_STATUS_STOPPED;
444
+
445
+ /**
446
+ * Enumerated value for the `loopStatus` property of the player to indicate
447
+ * playback will stop when there are no more tracks to play.
448
+ *
449
+ * @name Player#LOOP_STATUS_NONE
450
+ * @static
451
+ * @constant
452
+ */
453
+ static LOOP_STATUS_NONE = constants.LOOP_STATUS_NONE;
454
+
455
+ /**
456
+ * Enumerated value for the `loopStatus` property of the player to indicate the
457
+ * current track will start again from the beginning once it has finished
458
+ * playing.
459
+ *
460
+ * @name Player#LOOP_STATUS_TRACK
461
+ * @static
462
+ * @constant
463
+ */
464
+ static LOOP_STATUS_TRACK = constants.LOOP_STATUS_TRACK;
465
+
466
+ /**
467
+ * Enumerated value for the `loopStatus` property of the player to indicate the
468
+ * playback loops through a list of tracks.
469
+ *
470
+ * @name Player#LOOP_STATUS_PLAYLIST
471
+ * @static
472
+ * @constant
473
+ */
474
+ static LOOP_STATUS_PLAYLIST = constants.LOOP_STATUS_PLAYLIST;
475
+ }
476
+
477
+ module.exports = Player;
@@ -0,0 +1,57 @@
1
+ const dbus = require('dbus-next');
2
+ const Variant = dbus.Variant;
3
+ const types = require('./types');
4
+ const deepEqual = require('deep-equal');
5
+ const constants = require('../constants');
6
+ const logging = require('../logging');
7
+
8
+ let {
9
+ Interface, property, method, signal, DBusError,
10
+ ACCESS_READ, ACCESS_WRITE, ACCESS_READWRITE
11
+ } = dbus.interface;
12
+
13
+ class MprisInterface extends Interface {
14
+ constructor(name, player) {
15
+ super(name);
16
+ this.player = player;
17
+ }
18
+
19
+ _setPropertyInternal(property, valueDbus) {
20
+ // nothing is currently settable internally that needs conversion to plain
21
+ this.player.emit(property[0].toLowerCase() + property.substr(1), valueDbus);
22
+ }
23
+
24
+ setProperty(property, valuePlain) {
25
+ // convert the plain value to a dbus value (default to the plain value)
26
+ let valueDbus = valuePlain;
27
+
28
+ if (property === 'Metadata') {
29
+ valueDbus = types.metadataToDbus(valuePlain);
30
+ } else if (property === 'ActivePlaylist') {
31
+ if (valuePlain) {
32
+ valueDbus = [ true, types.playlistToDbus(valuePlain) ];
33
+ } else {
34
+ valueDbus = [ false, types.emptyPlaylist ];
35
+ }
36
+ } else if (property === 'Tracks') {
37
+ valueDbus =
38
+ valuePlain.filter((t) => t['mpris:trackid']).map((t) => t['mpris:trackid']);
39
+ }
40
+
41
+ if (!deepEqual(this[`_${property}`], valueDbus)) {
42
+ this[`_${property}`] = valueDbus;
43
+
44
+ if (property == 'LoopStatus' && !constants.isLoopStatusValid(valuePlain)) {
45
+ logging.warn(`setting player loop status to an invalid value: ${valuePlain}`);
46
+ } else if (property == 'PlaybackStatus' && !constants.isPlaybackStatusValid(valuePlain)) {
47
+ logging.warn(`setting player playback status to an invalid value: ${valuePlain}`);
48
+ } else {
49
+ let changedProperties = {};
50
+ changedProperties[property] = valueDbus;
51
+ Interface.emitPropertiesChanged(this, changedProperties);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = MprisInterface;