@rocksky/sdk 0.10.0 → 0.10.2

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/dist/index.js CHANGED
@@ -1494,7 +1494,12 @@ function trackFromJson(d) {
1494
1494
  albumArt: d.album_art,
1495
1495
  durationMs: d.duration_ms ?? d.length,
1496
1496
  elapsedMs: d.elapsed,
1497
- isPlaying: d.is_playing
1497
+ isPlaying: d.is_playing,
1498
+ songUri: d.song_uri,
1499
+ albumUri: d.album_uri,
1500
+ artistUri: d.artist_uri,
1501
+ sha256: d.sha256,
1502
+ liked: d.liked
1498
1503
  };
1499
1504
  }
1500
1505
  function queueItemFromJson(d) {
@@ -34,6 +34,16 @@ export interface RemoteNowPlaying {
34
34
  /** Current position, ms. */
35
35
  elapsedMs?: number;
36
36
  isPlaying?: boolean;
37
+ /** The song's `at://` URI. */
38
+ songUri?: string;
39
+ /** The album's `at://` URI. */
40
+ albumUri?: string;
41
+ /** The artist's `at://` URI. */
42
+ artistUri?: string;
43
+ /** Content hash (matches the server's `sha256`). */
44
+ sha256?: string;
45
+ /** Whether the current user has liked this track. */
46
+ liked?: boolean;
37
47
  }
38
48
  /** One queue entry. `uploadId` (Rocksky uploads) or `trackId` (Navidrome id). */
39
49
  export interface RemoteQueueItem {
@@ -1 +1 @@
1
- {"version":3,"file":"remote-player.d.ts","sourceRoot":"","sources":["../src/remote-player.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-player.d.ts","sourceRoot":"","sources":["../src/remote-player.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { RemotePlayer, DEFAULT_REMOTE_WS, type RemotePlayerOptions, type RemotePlayerHandlers, type RemoteNowPlaying, type RemoteQueueItem, type EnqueueCommand, } from "./remote-player.js";
2
+ export { RemoteController, type RemoteControllerOptions, type RemoteControllerEvents, type RemoteDevice, type RemoteStatus, } from "./remote-controller.js";
3
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC"}
package/dist/remote.js ADDED
@@ -0,0 +1,529 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __returnValue = (v) => v;
3
+ function __exportSetter(name, newValue) {
4
+ this[name] = __returnValue.bind(null, newValue);
5
+ }
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true,
11
+ configurable: true,
12
+ set: __exportSetter.bind(all, name)
13
+ });
14
+ };
15
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
+
17
+ // src/remote-player.ts
18
+ var DEFAULT_REMOTE_WS = "wss://api.rocksky.app/ws";
19
+
20
+ class RemotePlayer {
21
+ opts;
22
+ ws = null;
23
+ deviceId = "";
24
+ stopped = false;
25
+ heartbeat;
26
+ reconnectTimer;
27
+ url;
28
+ heartbeatMs;
29
+ reconnectMs;
30
+ getToken;
31
+ debug;
32
+ handlers = {};
33
+ lastTrack = null;
34
+ lastStatus = null;
35
+ lastQueue = null;
36
+ constructor(opts) {
37
+ this.opts = opts;
38
+ this.url = opts.url ?? DEFAULT_REMOTE_WS;
39
+ this.heartbeatMs = opts.heartbeatMs ?? 1e4;
40
+ this.reconnectMs = opts.reconnectMs ?? 3000;
41
+ this.getToken = typeof opts.token === "function" ? opts.token : () => opts.token;
42
+ this.debug = opts.debug ?? (() => {});
43
+ }
44
+ on(event, handler) {
45
+ this.handlers[event] = handler;
46
+ return this;
47
+ }
48
+ get id() {
49
+ return this.deviceId;
50
+ }
51
+ connect() {
52
+ this.stopped = false;
53
+ this.open();
54
+ }
55
+ disconnect() {
56
+ this.stopped = true;
57
+ if (this.reconnectTimer)
58
+ clearTimeout(this.reconnectTimer);
59
+ if (this.heartbeat)
60
+ clearInterval(this.heartbeat);
61
+ try {
62
+ this.ws?.close();
63
+ } catch {}
64
+ this.ws = null;
65
+ }
66
+ setNowPlaying(track) {
67
+ this.lastTrack = track;
68
+ this.send({
69
+ type: "message",
70
+ device_id: this.deviceId,
71
+ token: this.getToken(),
72
+ data: {
73
+ type: "track",
74
+ title: track.title,
75
+ artist: track.artist,
76
+ album: track.album,
77
+ album_artist: track.albumArtist ?? track.artist,
78
+ length: track.durationMs ?? 0,
79
+ elapsed: track.elapsedMs ?? 0,
80
+ duration_ms: track.durationMs ?? 0,
81
+ album_art: track.albumArt,
82
+ is_playing: track.isPlaying ?? true,
83
+ device_name: this.opts.name
84
+ }
85
+ });
86
+ }
87
+ setStatus(status) {
88
+ const code = status === "playing" ? 1 : status === "paused" ? 2 : 0;
89
+ this.lastStatus = code;
90
+ this.send({
91
+ type: "message",
92
+ device_id: this.deviceId,
93
+ token: this.getToken(),
94
+ data: { type: "status", status: code }
95
+ });
96
+ }
97
+ setQueue(items, index) {
98
+ this.lastQueue = { items, index };
99
+ this.send({
100
+ type: "message",
101
+ device_id: this.deviceId,
102
+ token: this.getToken(),
103
+ data: {
104
+ type: "queue",
105
+ index,
106
+ queue: items.map((t) => ({
107
+ uploadId: t.uploadId,
108
+ trackId: t.trackId,
109
+ title: t.title,
110
+ artist: t.artist,
111
+ album: t.album,
112
+ album_artist: t.albumArtist,
113
+ album_art: t.albumArt,
114
+ duration: t.durationMs,
115
+ song_uri: t.songUri,
116
+ album_uri: t.albumUri,
117
+ track_number: t.trackNumber
118
+ }))
119
+ }
120
+ });
121
+ }
122
+ send(payload) {
123
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
124
+ try {
125
+ this.ws.send(JSON.stringify(payload));
126
+ } catch (e) {
127
+ this.debug("send error", e);
128
+ }
129
+ }
130
+ }
131
+ open() {
132
+ if (this.stopped)
133
+ return;
134
+ const token = this.getToken();
135
+ if (!token) {
136
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
137
+ return;
138
+ }
139
+ let ws;
140
+ try {
141
+ ws = new WebSocket(this.url);
142
+ } catch (e) {
143
+ this.debug("connect failed", e);
144
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
145
+ return;
146
+ }
147
+ this.ws = ws;
148
+ ws.onopen = () => {
149
+ this.debug("connected");
150
+ this.send({ type: "register", clientName: this.opts.name, token: this.getToken() });
151
+ if (this.heartbeat)
152
+ clearInterval(this.heartbeat);
153
+ this.heartbeat = setInterval(() => {
154
+ if (ws.readyState === WebSocket.OPEN)
155
+ ws.send("ping");
156
+ }, this.heartbeatMs);
157
+ };
158
+ ws.onmessage = (ev) => {
159
+ if (ev.data === "pong")
160
+ return;
161
+ let msg;
162
+ try {
163
+ msg = JSON.parse(ev.data);
164
+ } catch {
165
+ return;
166
+ }
167
+ this.handle(msg);
168
+ };
169
+ ws.onerror = () => {
170
+ try {
171
+ ws.close();
172
+ } catch {}
173
+ };
174
+ ws.onclose = () => {
175
+ this.debug("disconnected");
176
+ if (this.heartbeat)
177
+ clearInterval(this.heartbeat);
178
+ if (this.ws === ws)
179
+ this.ws = null;
180
+ this.deviceId = "";
181
+ if (!this.stopped) {
182
+ if (this.reconnectTimer)
183
+ clearTimeout(this.reconnectTimer);
184
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
185
+ }
186
+ };
187
+ }
188
+ handle(msg) {
189
+ if (msg.status === "registered" && typeof msg.deviceId === "string") {
190
+ this.deviceId = msg.deviceId;
191
+ this.debug("registered", this.deviceId);
192
+ this.resync();
193
+ return;
194
+ }
195
+ if (msg.type === "command") {
196
+ this.dispatch(msg);
197
+ return;
198
+ }
199
+ }
200
+ dispatch(msg) {
201
+ const h = this.handlers;
202
+ switch (msg.action) {
203
+ case "play":
204
+ h.play?.();
205
+ break;
206
+ case "pause":
207
+ h.pause?.();
208
+ break;
209
+ case "next":
210
+ h.next?.();
211
+ break;
212
+ case "previous":
213
+ h.previous?.();
214
+ break;
215
+ case "seek": {
216
+ const a = msg.args;
217
+ const pos = typeof a === "number" ? a : a?.position ?? 0;
218
+ h.seek?.(pos);
219
+ break;
220
+ }
221
+ case "queue_jump":
222
+ h.queueJump?.(msg.args?.index ?? 0);
223
+ break;
224
+ case "queue_remove":
225
+ h.queueRemove?.(msg.args?.index ?? 0);
226
+ break;
227
+ case "enqueue": {
228
+ const a = msg.args ?? {};
229
+ h.enqueue?.({
230
+ tracks: (a.tracks ?? []).map(descriptorToItem),
231
+ mode: a.mode ?? "now",
232
+ shuffle: !!a.shuffle,
233
+ startIndex: a.startIndex ?? 0
234
+ });
235
+ break;
236
+ }
237
+ default:
238
+ this.debug("unknown command", msg.action);
239
+ }
240
+ }
241
+ resync() {
242
+ if (this.lastTrack)
243
+ this.setNowPlaying(this.lastTrack);
244
+ if (this.lastStatus !== null) {
245
+ this.send({
246
+ type: "message",
247
+ device_id: this.deviceId,
248
+ token: this.getToken(),
249
+ data: { type: "status", status: this.lastStatus }
250
+ });
251
+ }
252
+ if (this.lastQueue)
253
+ this.setQueue(this.lastQueue.items, this.lastQueue.index);
254
+ }
255
+ }
256
+ function descriptorToItem(d) {
257
+ return {
258
+ uploadId: d.uploadId,
259
+ trackId: d.trackId,
260
+ title: d.title ?? "",
261
+ artist: d.artist ?? "",
262
+ album: d.album,
263
+ albumArtist: d.album_artist,
264
+ albumArt: d.album_art,
265
+ durationMs: d.duration,
266
+ songUri: d.song_uri,
267
+ albumUri: d.album_uri,
268
+ trackNumber: d.track_number
269
+ };
270
+ }
271
+ // src/remote-controller.ts
272
+ class RemoteController {
273
+ opts;
274
+ ws = null;
275
+ stopped = false;
276
+ heartbeat;
277
+ reconnectTimer;
278
+ url;
279
+ heartbeatMs;
280
+ reconnectMs;
281
+ getToken;
282
+ debug;
283
+ handlers = {};
284
+ constructor(opts) {
285
+ this.opts = opts;
286
+ this.url = opts.url ?? DEFAULT_REMOTE_WS;
287
+ this.heartbeatMs = opts.heartbeatMs ?? 1e4;
288
+ this.reconnectMs = opts.reconnectMs ?? 3000;
289
+ this.getToken = typeof opts.token === "function" ? opts.token : () => opts.token;
290
+ this.debug = opts.debug ?? (() => {});
291
+ }
292
+ on(event, handler) {
293
+ this.handlers[event] = handler;
294
+ return this;
295
+ }
296
+ connect() {
297
+ this.stopped = false;
298
+ this.open();
299
+ }
300
+ disconnect() {
301
+ this.stopped = true;
302
+ if (this.reconnectTimer)
303
+ clearTimeout(this.reconnectTimer);
304
+ if (this.heartbeat)
305
+ clearInterval(this.heartbeat);
306
+ try {
307
+ this.ws?.close();
308
+ } catch {}
309
+ this.ws = null;
310
+ }
311
+ setPrimary(deviceId) {
312
+ this.send({ type: "set_primary", device_id: deviceId, token: this.getToken() });
313
+ }
314
+ play(target) {
315
+ this.command("play", target);
316
+ }
317
+ pause(target) {
318
+ this.command("pause", target);
319
+ }
320
+ next(target) {
321
+ this.command("next", target);
322
+ }
323
+ previous(target) {
324
+ this.command("previous", target);
325
+ }
326
+ seek(target, positionMs) {
327
+ this.command("seek", target, { position: positionMs });
328
+ }
329
+ queueJump(target, index) {
330
+ this.command("queue_jump", target, { index });
331
+ }
332
+ queueRemove(target, index) {
333
+ this.command("queue_remove", target, { index });
334
+ }
335
+ enqueue(target, tracks, mode = "now", shuffle = false, startIndex = 0) {
336
+ this.command("enqueue", target, {
337
+ tracks: tracks.map((t) => ({
338
+ uploadId: t.uploadId,
339
+ trackId: t.trackId,
340
+ title: t.title,
341
+ artist: t.artist,
342
+ album: t.album,
343
+ album_artist: t.albumArtist,
344
+ album_art: t.albumArt,
345
+ duration: t.durationMs,
346
+ song_uri: t.songUri,
347
+ album_uri: t.albumUri,
348
+ track_number: t.trackNumber
349
+ })),
350
+ mode,
351
+ shuffle,
352
+ startIndex
353
+ });
354
+ }
355
+ command(action, target, args) {
356
+ const payload = { type: "command", action, token: this.getToken() };
357
+ if (target)
358
+ payload.target = target;
359
+ if (args !== undefined)
360
+ payload.args = args;
361
+ this.send(payload);
362
+ }
363
+ send(payload) {
364
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
365
+ try {
366
+ this.ws.send(JSON.stringify(payload));
367
+ } catch (e) {
368
+ this.debug("send error", e);
369
+ }
370
+ }
371
+ }
372
+ open() {
373
+ if (this.stopped)
374
+ return;
375
+ const token = this.getToken();
376
+ if (!token) {
377
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
378
+ return;
379
+ }
380
+ let ws;
381
+ try {
382
+ ws = new WebSocket(this.url);
383
+ } catch (e) {
384
+ this.debug("connect failed", e);
385
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
386
+ return;
387
+ }
388
+ this.ws = ws;
389
+ ws.onopen = () => {
390
+ this.debug("connected");
391
+ this.send({ type: "register", clientName: this.opts.name, token: this.getToken() });
392
+ if (this.heartbeat)
393
+ clearInterval(this.heartbeat);
394
+ this.heartbeat = setInterval(() => {
395
+ if (ws.readyState === WebSocket.OPEN)
396
+ ws.send("ping");
397
+ }, this.heartbeatMs);
398
+ };
399
+ ws.onmessage = (ev) => {
400
+ if (ev.data === "pong")
401
+ return;
402
+ let msg;
403
+ try {
404
+ msg = JSON.parse(ev.data);
405
+ } catch {
406
+ return;
407
+ }
408
+ this.handle(msg);
409
+ };
410
+ ws.onerror = () => {
411
+ try {
412
+ ws.close();
413
+ } catch {}
414
+ };
415
+ ws.onclose = () => {
416
+ this.debug("disconnected");
417
+ if (this.heartbeat)
418
+ clearInterval(this.heartbeat);
419
+ if (this.ws === ws)
420
+ this.ws = null;
421
+ if (!this.stopped) {
422
+ if (this.reconnectTimer)
423
+ clearTimeout(this.reconnectTimer);
424
+ this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
425
+ }
426
+ };
427
+ }
428
+ handle(msg) {
429
+ if (msg.status === "registered")
430
+ return;
431
+ switch (msg.type) {
432
+ case "devices":
433
+ this.handlers.devices?.({
434
+ primaryDevice: msg.primary_device ?? null,
435
+ devices: (msg.devices ?? []).map(deviceFromJson)
436
+ });
437
+ break;
438
+ case "device_registered":
439
+ this.handlers.deviceRegistered?.({
440
+ deviceId: msg.deviceId ?? "",
441
+ name: msg.clientName ?? ""
442
+ });
443
+ break;
444
+ case "device_unregistered":
445
+ this.handlers.deviceUnregistered?.({ deviceId: msg.device_id ?? "" });
446
+ break;
447
+ case "primary_changed":
448
+ this.handlers.primaryChanged?.({ deviceId: msg.device_id ?? "" });
449
+ break;
450
+ case "message":
451
+ this.handleMessage(msg);
452
+ break;
453
+ default:
454
+ this.debug("unknown frame", msg.type);
455
+ }
456
+ }
457
+ handleMessage(msg) {
458
+ const deviceId = msg.device_id ?? "";
459
+ const deviceName = msg.device_name ?? "";
460
+ const data = msg.data;
461
+ if (!data)
462
+ return;
463
+ switch (data.type) {
464
+ case "track":
465
+ this.handlers.nowPlaying?.({ deviceId, deviceName, track: trackFromJson(data) });
466
+ break;
467
+ case "status":
468
+ this.handlers.status?.({ deviceId, deviceName, status: statusFromCode(data.status) });
469
+ break;
470
+ case "queue":
471
+ this.handlers.queue?.({
472
+ deviceId,
473
+ deviceName,
474
+ index: data.index ?? 0,
475
+ queue: (data.queue ?? []).map(queueItemFromJson)
476
+ });
477
+ break;
478
+ }
479
+ }
480
+ }
481
+ function statusFromCode(code) {
482
+ return code === 1 ? "playing" : code === 0 ? "stopped" : "paused";
483
+ }
484
+ function trackFromJson(d) {
485
+ return {
486
+ title: d.title ?? "",
487
+ artist: d.artist ?? "",
488
+ album: d.album,
489
+ albumArtist: d.album_artist,
490
+ albumArt: d.album_art,
491
+ durationMs: d.duration_ms ?? d.length,
492
+ elapsedMs: d.elapsed,
493
+ isPlaying: d.is_playing,
494
+ songUri: d.song_uri,
495
+ albumUri: d.album_uri,
496
+ artistUri: d.artist_uri,
497
+ sha256: d.sha256,
498
+ liked: d.liked
499
+ };
500
+ }
501
+ function queueItemFromJson(d) {
502
+ return {
503
+ uploadId: d.uploadId,
504
+ trackId: d.trackId,
505
+ title: d.title ?? "",
506
+ artist: d.artist ?? "",
507
+ album: d.album,
508
+ albumArtist: d.album_artist,
509
+ albumArt: d.album_art,
510
+ durationMs: d.duration,
511
+ songUri: d.song_uri,
512
+ albumUri: d.album_uri,
513
+ trackNumber: d.track_number
514
+ };
515
+ }
516
+ function deviceFromJson(d) {
517
+ return {
518
+ deviceId: d.device_id ?? "",
519
+ name: d.name ?? "",
520
+ nowPlaying: d.now_playing ? trackFromJson(d.now_playing) : undefined,
521
+ queueIndex: d.queue?.index ?? 0,
522
+ queue: (d.queue?.queue ?? []).map(queueItemFromJson)
523
+ };
524
+ }
525
+ export {
526
+ RemotePlayer,
527
+ RemoteController,
528
+ DEFAULT_REMOTE_WS
529
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rocksky/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "TypeScript SDK for Rocksky — built on atcute: AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), a local dedup index, and Jetstream real-time sync.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,6 +21,11 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "import": "./dist/index.js",
23
23
  "default": "./dist/index.js"
24
+ },
25
+ "./remote": {
26
+ "types": "./dist/remote.d.ts",
27
+ "import": "./dist/remote.js",
28
+ "default": "./dist/remote.js"
24
29
  }
25
30
  },
26
31
  "files": [
@@ -29,7 +34,7 @@
29
34
  "README.md"
30
35
  ],
31
36
  "scripts": {
32
- "build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external && bun run build:types",
37
+ "build": "bun build ./src/index.ts ./src/remote.ts --outdir ./dist --target node --format esm --packages external && bun run build:types",
33
38
  "build:types": "tsc -p tsconfig.build.json",
34
39
  "typecheck": "tsc --noEmit",
35
40
  "test": "bun test",
@@ -332,6 +332,12 @@ function trackFromJson(d: Json): RemoteNowPlaying {
332
332
  durationMs: d.duration_ms ?? d.length,
333
333
  elapsedMs: d.elapsed,
334
334
  isPlaying: d.is_playing,
335
+ // Server-enriched fields (present on the broadcast a controller receives).
336
+ songUri: d.song_uri,
337
+ albumUri: d.album_uri,
338
+ artistUri: d.artist_uri,
339
+ sha256: d.sha256,
340
+ liked: d.liked,
335
341
  };
336
342
  }
337
343
 
@@ -36,6 +36,19 @@ export interface RemoteNowPlaying {
36
36
  /** Current position, ms. */
37
37
  elapsedMs?: number;
38
38
  isPlaying?: boolean;
39
+ // The following are filled in by the server on the broadcast a controller
40
+ // receives (a player leaves them unset — the server resolves them from the
41
+ // library). They let a controller UI deep-link and show like state.
42
+ /** The song's `at://` URI. */
43
+ songUri?: string;
44
+ /** The album's `at://` URI. */
45
+ albumUri?: string;
46
+ /** The artist's `at://` URI. */
47
+ artistUri?: string;
48
+ /** Content hash (matches the server's `sha256`). */
49
+ sha256?: string;
50
+ /** Whether the current user has liked this track. */
51
+ liked?: boolean;
39
52
  }
40
53
 
41
54
  /** One queue entry. `uploadId` (Rocksky uploads) or `trackId` (Navidrome id). */
package/src/remote.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Browser-safe entry: the remote-control player + controller only.
2
+ //
3
+ // The main entry (`@rocksky/sdk`) bundles the dedup index (classic-level) and
4
+ // the identity hashes (node:crypto), which are Node-only. The remote player /
5
+ // controller are pure WebSocket + JSON with zero Node dependencies, so this
6
+ // subpath (`@rocksky/sdk/remote`) is safe to import from a browser bundle.
7
+
8
+ export {
9
+ RemotePlayer,
10
+ DEFAULT_REMOTE_WS,
11
+ type RemotePlayerOptions,
12
+ type RemotePlayerHandlers,
13
+ type RemoteNowPlaying,
14
+ type RemoteQueueItem,
15
+ type EnqueueCommand,
16
+ } from "./remote-player.js";
17
+ export {
18
+ RemoteController,
19
+ type RemoteControllerOptions,
20
+ type RemoteControllerEvents,
21
+ type RemoteDevice,
22
+ type RemoteStatus,
23
+ } from "./remote-controller.js";