@rocksky/sdk 0.10.1 → 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.
@@ -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.1",
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",
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";