@rocksky/sdk 0.8.0 → 0.10.0
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/README.md +27 -0
- package/dist/generated/types.d.ts +4 -0
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -0
- package/dist/remote-controller.d.ts +123 -0
- package/dist/remote-controller.d.ts.map +1 -0
- package/dist/remote-player.d.ts +124 -0
- package/dist/remote-player.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/generated/types.ts +4 -0
- package/src/index.ts +16 -0
- package/src/remote-controller.ts +362 -0
- package/src/remote-player.ts +379 -0
package/dist/index.js
CHANGED
|
@@ -1018,6 +1018,509 @@ function* walkMst(blocks, cid) {
|
|
|
1018
1018
|
yield* walkMst(blocks, e.t.$link);
|
|
1019
1019
|
}
|
|
1020
1020
|
}
|
|
1021
|
+
// src/remote-player.ts
|
|
1022
|
+
var DEFAULT_REMOTE_WS = "wss://api.rocksky.app/ws";
|
|
1023
|
+
|
|
1024
|
+
class RemotePlayer {
|
|
1025
|
+
opts;
|
|
1026
|
+
ws = null;
|
|
1027
|
+
deviceId = "";
|
|
1028
|
+
stopped = false;
|
|
1029
|
+
heartbeat;
|
|
1030
|
+
reconnectTimer;
|
|
1031
|
+
url;
|
|
1032
|
+
heartbeatMs;
|
|
1033
|
+
reconnectMs;
|
|
1034
|
+
getToken;
|
|
1035
|
+
debug;
|
|
1036
|
+
handlers = {};
|
|
1037
|
+
lastTrack = null;
|
|
1038
|
+
lastStatus = null;
|
|
1039
|
+
lastQueue = null;
|
|
1040
|
+
constructor(opts) {
|
|
1041
|
+
this.opts = opts;
|
|
1042
|
+
this.url = opts.url ?? DEFAULT_REMOTE_WS;
|
|
1043
|
+
this.heartbeatMs = opts.heartbeatMs ?? 1e4;
|
|
1044
|
+
this.reconnectMs = opts.reconnectMs ?? 3000;
|
|
1045
|
+
this.getToken = typeof opts.token === "function" ? opts.token : () => opts.token;
|
|
1046
|
+
this.debug = opts.debug ?? (() => {});
|
|
1047
|
+
}
|
|
1048
|
+
on(event, handler) {
|
|
1049
|
+
this.handlers[event] = handler;
|
|
1050
|
+
return this;
|
|
1051
|
+
}
|
|
1052
|
+
get id() {
|
|
1053
|
+
return this.deviceId;
|
|
1054
|
+
}
|
|
1055
|
+
connect() {
|
|
1056
|
+
this.stopped = false;
|
|
1057
|
+
this.open();
|
|
1058
|
+
}
|
|
1059
|
+
disconnect() {
|
|
1060
|
+
this.stopped = true;
|
|
1061
|
+
if (this.reconnectTimer)
|
|
1062
|
+
clearTimeout(this.reconnectTimer);
|
|
1063
|
+
if (this.heartbeat)
|
|
1064
|
+
clearInterval(this.heartbeat);
|
|
1065
|
+
try {
|
|
1066
|
+
this.ws?.close();
|
|
1067
|
+
} catch {}
|
|
1068
|
+
this.ws = null;
|
|
1069
|
+
}
|
|
1070
|
+
setNowPlaying(track) {
|
|
1071
|
+
this.lastTrack = track;
|
|
1072
|
+
this.send({
|
|
1073
|
+
type: "message",
|
|
1074
|
+
device_id: this.deviceId,
|
|
1075
|
+
token: this.getToken(),
|
|
1076
|
+
data: {
|
|
1077
|
+
type: "track",
|
|
1078
|
+
title: track.title,
|
|
1079
|
+
artist: track.artist,
|
|
1080
|
+
album: track.album,
|
|
1081
|
+
album_artist: track.albumArtist ?? track.artist,
|
|
1082
|
+
length: track.durationMs ?? 0,
|
|
1083
|
+
elapsed: track.elapsedMs ?? 0,
|
|
1084
|
+
duration_ms: track.durationMs ?? 0,
|
|
1085
|
+
album_art: track.albumArt,
|
|
1086
|
+
is_playing: track.isPlaying ?? true,
|
|
1087
|
+
device_name: this.opts.name
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
setStatus(status) {
|
|
1092
|
+
const code = status === "playing" ? 1 : status === "paused" ? 2 : 0;
|
|
1093
|
+
this.lastStatus = code;
|
|
1094
|
+
this.send({
|
|
1095
|
+
type: "message",
|
|
1096
|
+
device_id: this.deviceId,
|
|
1097
|
+
token: this.getToken(),
|
|
1098
|
+
data: { type: "status", status: code }
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
setQueue(items, index) {
|
|
1102
|
+
this.lastQueue = { items, index };
|
|
1103
|
+
this.send({
|
|
1104
|
+
type: "message",
|
|
1105
|
+
device_id: this.deviceId,
|
|
1106
|
+
token: this.getToken(),
|
|
1107
|
+
data: {
|
|
1108
|
+
type: "queue",
|
|
1109
|
+
index,
|
|
1110
|
+
queue: items.map((t) => ({
|
|
1111
|
+
uploadId: t.uploadId,
|
|
1112
|
+
trackId: t.trackId,
|
|
1113
|
+
title: t.title,
|
|
1114
|
+
artist: t.artist,
|
|
1115
|
+
album: t.album,
|
|
1116
|
+
album_artist: t.albumArtist,
|
|
1117
|
+
album_art: t.albumArt,
|
|
1118
|
+
duration: t.durationMs,
|
|
1119
|
+
song_uri: t.songUri,
|
|
1120
|
+
album_uri: t.albumUri,
|
|
1121
|
+
track_number: t.trackNumber
|
|
1122
|
+
}))
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
send(payload) {
|
|
1127
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1128
|
+
try {
|
|
1129
|
+
this.ws.send(JSON.stringify(payload));
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
this.debug("send error", e);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
open() {
|
|
1136
|
+
if (this.stopped)
|
|
1137
|
+
return;
|
|
1138
|
+
const token = this.getToken();
|
|
1139
|
+
if (!token) {
|
|
1140
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
let ws;
|
|
1144
|
+
try {
|
|
1145
|
+
ws = new WebSocket(this.url);
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
this.debug("connect failed", e);
|
|
1148
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
this.ws = ws;
|
|
1152
|
+
ws.onopen = () => {
|
|
1153
|
+
this.debug("connected");
|
|
1154
|
+
this.send({ type: "register", clientName: this.opts.name, token: this.getToken() });
|
|
1155
|
+
if (this.heartbeat)
|
|
1156
|
+
clearInterval(this.heartbeat);
|
|
1157
|
+
this.heartbeat = setInterval(() => {
|
|
1158
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
1159
|
+
ws.send("ping");
|
|
1160
|
+
}, this.heartbeatMs);
|
|
1161
|
+
};
|
|
1162
|
+
ws.onmessage = (ev) => {
|
|
1163
|
+
if (ev.data === "pong")
|
|
1164
|
+
return;
|
|
1165
|
+
let msg;
|
|
1166
|
+
try {
|
|
1167
|
+
msg = JSON.parse(ev.data);
|
|
1168
|
+
} catch {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
this.handle(msg);
|
|
1172
|
+
};
|
|
1173
|
+
ws.onerror = () => {
|
|
1174
|
+
try {
|
|
1175
|
+
ws.close();
|
|
1176
|
+
} catch {}
|
|
1177
|
+
};
|
|
1178
|
+
ws.onclose = () => {
|
|
1179
|
+
this.debug("disconnected");
|
|
1180
|
+
if (this.heartbeat)
|
|
1181
|
+
clearInterval(this.heartbeat);
|
|
1182
|
+
if (this.ws === ws)
|
|
1183
|
+
this.ws = null;
|
|
1184
|
+
this.deviceId = "";
|
|
1185
|
+
if (!this.stopped) {
|
|
1186
|
+
if (this.reconnectTimer)
|
|
1187
|
+
clearTimeout(this.reconnectTimer);
|
|
1188
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
handle(msg) {
|
|
1193
|
+
if (msg.status === "registered" && typeof msg.deviceId === "string") {
|
|
1194
|
+
this.deviceId = msg.deviceId;
|
|
1195
|
+
this.debug("registered", this.deviceId);
|
|
1196
|
+
this.resync();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (msg.type === "command") {
|
|
1200
|
+
this.dispatch(msg);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
dispatch(msg) {
|
|
1205
|
+
const h = this.handlers;
|
|
1206
|
+
switch (msg.action) {
|
|
1207
|
+
case "play":
|
|
1208
|
+
h.play?.();
|
|
1209
|
+
break;
|
|
1210
|
+
case "pause":
|
|
1211
|
+
h.pause?.();
|
|
1212
|
+
break;
|
|
1213
|
+
case "next":
|
|
1214
|
+
h.next?.();
|
|
1215
|
+
break;
|
|
1216
|
+
case "previous":
|
|
1217
|
+
h.previous?.();
|
|
1218
|
+
break;
|
|
1219
|
+
case "seek": {
|
|
1220
|
+
const a = msg.args;
|
|
1221
|
+
const pos = typeof a === "number" ? a : a?.position ?? 0;
|
|
1222
|
+
h.seek?.(pos);
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
case "queue_jump":
|
|
1226
|
+
h.queueJump?.(msg.args?.index ?? 0);
|
|
1227
|
+
break;
|
|
1228
|
+
case "queue_remove":
|
|
1229
|
+
h.queueRemove?.(msg.args?.index ?? 0);
|
|
1230
|
+
break;
|
|
1231
|
+
case "enqueue": {
|
|
1232
|
+
const a = msg.args ?? {};
|
|
1233
|
+
h.enqueue?.({
|
|
1234
|
+
tracks: (a.tracks ?? []).map(descriptorToItem),
|
|
1235
|
+
mode: a.mode ?? "now",
|
|
1236
|
+
shuffle: !!a.shuffle,
|
|
1237
|
+
startIndex: a.startIndex ?? 0
|
|
1238
|
+
});
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
default:
|
|
1242
|
+
this.debug("unknown command", msg.action);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
resync() {
|
|
1246
|
+
if (this.lastTrack)
|
|
1247
|
+
this.setNowPlaying(this.lastTrack);
|
|
1248
|
+
if (this.lastStatus !== null) {
|
|
1249
|
+
this.send({
|
|
1250
|
+
type: "message",
|
|
1251
|
+
device_id: this.deviceId,
|
|
1252
|
+
token: this.getToken(),
|
|
1253
|
+
data: { type: "status", status: this.lastStatus }
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
if (this.lastQueue)
|
|
1257
|
+
this.setQueue(this.lastQueue.items, this.lastQueue.index);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function descriptorToItem(d) {
|
|
1261
|
+
return {
|
|
1262
|
+
uploadId: d.uploadId,
|
|
1263
|
+
trackId: d.trackId,
|
|
1264
|
+
title: d.title ?? "",
|
|
1265
|
+
artist: d.artist ?? "",
|
|
1266
|
+
album: d.album,
|
|
1267
|
+
albumArtist: d.album_artist,
|
|
1268
|
+
albumArt: d.album_art,
|
|
1269
|
+
durationMs: d.duration,
|
|
1270
|
+
songUri: d.song_uri,
|
|
1271
|
+
albumUri: d.album_uri,
|
|
1272
|
+
trackNumber: d.track_number
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
// src/remote-controller.ts
|
|
1276
|
+
class RemoteController {
|
|
1277
|
+
opts;
|
|
1278
|
+
ws = null;
|
|
1279
|
+
stopped = false;
|
|
1280
|
+
heartbeat;
|
|
1281
|
+
reconnectTimer;
|
|
1282
|
+
url;
|
|
1283
|
+
heartbeatMs;
|
|
1284
|
+
reconnectMs;
|
|
1285
|
+
getToken;
|
|
1286
|
+
debug;
|
|
1287
|
+
handlers = {};
|
|
1288
|
+
constructor(opts) {
|
|
1289
|
+
this.opts = opts;
|
|
1290
|
+
this.url = opts.url ?? DEFAULT_REMOTE_WS;
|
|
1291
|
+
this.heartbeatMs = opts.heartbeatMs ?? 1e4;
|
|
1292
|
+
this.reconnectMs = opts.reconnectMs ?? 3000;
|
|
1293
|
+
this.getToken = typeof opts.token === "function" ? opts.token : () => opts.token;
|
|
1294
|
+
this.debug = opts.debug ?? (() => {});
|
|
1295
|
+
}
|
|
1296
|
+
on(event, handler) {
|
|
1297
|
+
this.handlers[event] = handler;
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
connect() {
|
|
1301
|
+
this.stopped = false;
|
|
1302
|
+
this.open();
|
|
1303
|
+
}
|
|
1304
|
+
disconnect() {
|
|
1305
|
+
this.stopped = true;
|
|
1306
|
+
if (this.reconnectTimer)
|
|
1307
|
+
clearTimeout(this.reconnectTimer);
|
|
1308
|
+
if (this.heartbeat)
|
|
1309
|
+
clearInterval(this.heartbeat);
|
|
1310
|
+
try {
|
|
1311
|
+
this.ws?.close();
|
|
1312
|
+
} catch {}
|
|
1313
|
+
this.ws = null;
|
|
1314
|
+
}
|
|
1315
|
+
setPrimary(deviceId) {
|
|
1316
|
+
this.send({ type: "set_primary", device_id: deviceId, token: this.getToken() });
|
|
1317
|
+
}
|
|
1318
|
+
play(target) {
|
|
1319
|
+
this.command("play", target);
|
|
1320
|
+
}
|
|
1321
|
+
pause(target) {
|
|
1322
|
+
this.command("pause", target);
|
|
1323
|
+
}
|
|
1324
|
+
next(target) {
|
|
1325
|
+
this.command("next", target);
|
|
1326
|
+
}
|
|
1327
|
+
previous(target) {
|
|
1328
|
+
this.command("previous", target);
|
|
1329
|
+
}
|
|
1330
|
+
seek(target, positionMs) {
|
|
1331
|
+
this.command("seek", target, { position: positionMs });
|
|
1332
|
+
}
|
|
1333
|
+
queueJump(target, index) {
|
|
1334
|
+
this.command("queue_jump", target, { index });
|
|
1335
|
+
}
|
|
1336
|
+
queueRemove(target, index) {
|
|
1337
|
+
this.command("queue_remove", target, { index });
|
|
1338
|
+
}
|
|
1339
|
+
enqueue(target, tracks, mode = "now", shuffle = false, startIndex = 0) {
|
|
1340
|
+
this.command("enqueue", target, {
|
|
1341
|
+
tracks: tracks.map((t) => ({
|
|
1342
|
+
uploadId: t.uploadId,
|
|
1343
|
+
trackId: t.trackId,
|
|
1344
|
+
title: t.title,
|
|
1345
|
+
artist: t.artist,
|
|
1346
|
+
album: t.album,
|
|
1347
|
+
album_artist: t.albumArtist,
|
|
1348
|
+
album_art: t.albumArt,
|
|
1349
|
+
duration: t.durationMs,
|
|
1350
|
+
song_uri: t.songUri,
|
|
1351
|
+
album_uri: t.albumUri,
|
|
1352
|
+
track_number: t.trackNumber
|
|
1353
|
+
})),
|
|
1354
|
+
mode,
|
|
1355
|
+
shuffle,
|
|
1356
|
+
startIndex
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
command(action, target, args) {
|
|
1360
|
+
const payload = { type: "command", action, token: this.getToken() };
|
|
1361
|
+
if (target)
|
|
1362
|
+
payload.target = target;
|
|
1363
|
+
if (args !== undefined)
|
|
1364
|
+
payload.args = args;
|
|
1365
|
+
this.send(payload);
|
|
1366
|
+
}
|
|
1367
|
+
send(payload) {
|
|
1368
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1369
|
+
try {
|
|
1370
|
+
this.ws.send(JSON.stringify(payload));
|
|
1371
|
+
} catch (e) {
|
|
1372
|
+
this.debug("send error", e);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
open() {
|
|
1377
|
+
if (this.stopped)
|
|
1378
|
+
return;
|
|
1379
|
+
const token = this.getToken();
|
|
1380
|
+
if (!token) {
|
|
1381
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
let ws;
|
|
1385
|
+
try {
|
|
1386
|
+
ws = new WebSocket(this.url);
|
|
1387
|
+
} catch (e) {
|
|
1388
|
+
this.debug("connect failed", e);
|
|
1389
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this.ws = ws;
|
|
1393
|
+
ws.onopen = () => {
|
|
1394
|
+
this.debug("connected");
|
|
1395
|
+
this.send({ type: "register", clientName: this.opts.name, token: this.getToken() });
|
|
1396
|
+
if (this.heartbeat)
|
|
1397
|
+
clearInterval(this.heartbeat);
|
|
1398
|
+
this.heartbeat = setInterval(() => {
|
|
1399
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
1400
|
+
ws.send("ping");
|
|
1401
|
+
}, this.heartbeatMs);
|
|
1402
|
+
};
|
|
1403
|
+
ws.onmessage = (ev) => {
|
|
1404
|
+
if (ev.data === "pong")
|
|
1405
|
+
return;
|
|
1406
|
+
let msg;
|
|
1407
|
+
try {
|
|
1408
|
+
msg = JSON.parse(ev.data);
|
|
1409
|
+
} catch {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
this.handle(msg);
|
|
1413
|
+
};
|
|
1414
|
+
ws.onerror = () => {
|
|
1415
|
+
try {
|
|
1416
|
+
ws.close();
|
|
1417
|
+
} catch {}
|
|
1418
|
+
};
|
|
1419
|
+
ws.onclose = () => {
|
|
1420
|
+
this.debug("disconnected");
|
|
1421
|
+
if (this.heartbeat)
|
|
1422
|
+
clearInterval(this.heartbeat);
|
|
1423
|
+
if (this.ws === ws)
|
|
1424
|
+
this.ws = null;
|
|
1425
|
+
if (!this.stopped) {
|
|
1426
|
+
if (this.reconnectTimer)
|
|
1427
|
+
clearTimeout(this.reconnectTimer);
|
|
1428
|
+
this.reconnectTimer = setTimeout(() => this.open(), this.reconnectMs);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
handle(msg) {
|
|
1433
|
+
if (msg.status === "registered")
|
|
1434
|
+
return;
|
|
1435
|
+
switch (msg.type) {
|
|
1436
|
+
case "devices":
|
|
1437
|
+
this.handlers.devices?.({
|
|
1438
|
+
primaryDevice: msg.primary_device ?? null,
|
|
1439
|
+
devices: (msg.devices ?? []).map(deviceFromJson)
|
|
1440
|
+
});
|
|
1441
|
+
break;
|
|
1442
|
+
case "device_registered":
|
|
1443
|
+
this.handlers.deviceRegistered?.({
|
|
1444
|
+
deviceId: msg.deviceId ?? "",
|
|
1445
|
+
name: msg.clientName ?? ""
|
|
1446
|
+
});
|
|
1447
|
+
break;
|
|
1448
|
+
case "device_unregistered":
|
|
1449
|
+
this.handlers.deviceUnregistered?.({ deviceId: msg.device_id ?? "" });
|
|
1450
|
+
break;
|
|
1451
|
+
case "primary_changed":
|
|
1452
|
+
this.handlers.primaryChanged?.({ deviceId: msg.device_id ?? "" });
|
|
1453
|
+
break;
|
|
1454
|
+
case "message":
|
|
1455
|
+
this.handleMessage(msg);
|
|
1456
|
+
break;
|
|
1457
|
+
default:
|
|
1458
|
+
this.debug("unknown frame", msg.type);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
handleMessage(msg) {
|
|
1462
|
+
const deviceId = msg.device_id ?? "";
|
|
1463
|
+
const deviceName = msg.device_name ?? "";
|
|
1464
|
+
const data = msg.data;
|
|
1465
|
+
if (!data)
|
|
1466
|
+
return;
|
|
1467
|
+
switch (data.type) {
|
|
1468
|
+
case "track":
|
|
1469
|
+
this.handlers.nowPlaying?.({ deviceId, deviceName, track: trackFromJson(data) });
|
|
1470
|
+
break;
|
|
1471
|
+
case "status":
|
|
1472
|
+
this.handlers.status?.({ deviceId, deviceName, status: statusFromCode(data.status) });
|
|
1473
|
+
break;
|
|
1474
|
+
case "queue":
|
|
1475
|
+
this.handlers.queue?.({
|
|
1476
|
+
deviceId,
|
|
1477
|
+
deviceName,
|
|
1478
|
+
index: data.index ?? 0,
|
|
1479
|
+
queue: (data.queue ?? []).map(queueItemFromJson)
|
|
1480
|
+
});
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
function statusFromCode(code) {
|
|
1486
|
+
return code === 1 ? "playing" : code === 0 ? "stopped" : "paused";
|
|
1487
|
+
}
|
|
1488
|
+
function trackFromJson(d) {
|
|
1489
|
+
return {
|
|
1490
|
+
title: d.title ?? "",
|
|
1491
|
+
artist: d.artist ?? "",
|
|
1492
|
+
album: d.album,
|
|
1493
|
+
albumArtist: d.album_artist,
|
|
1494
|
+
albumArt: d.album_art,
|
|
1495
|
+
durationMs: d.duration_ms ?? d.length,
|
|
1496
|
+
elapsedMs: d.elapsed,
|
|
1497
|
+
isPlaying: d.is_playing
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function queueItemFromJson(d) {
|
|
1501
|
+
return {
|
|
1502
|
+
uploadId: d.uploadId,
|
|
1503
|
+
trackId: d.trackId,
|
|
1504
|
+
title: d.title ?? "",
|
|
1505
|
+
artist: d.artist ?? "",
|
|
1506
|
+
album: d.album,
|
|
1507
|
+
albumArtist: d.album_artist,
|
|
1508
|
+
albumArt: d.album_art,
|
|
1509
|
+
durationMs: d.duration,
|
|
1510
|
+
songUri: d.song_uri,
|
|
1511
|
+
albumUri: d.album_uri,
|
|
1512
|
+
trackNumber: d.track_number
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function deviceFromJson(d) {
|
|
1516
|
+
return {
|
|
1517
|
+
deviceId: d.device_id ?? "",
|
|
1518
|
+
name: d.name ?? "",
|
|
1519
|
+
nowPlaying: d.now_playing ? trackFromJson(d.now_playing) : undefined,
|
|
1520
|
+
queueIndex: d.queue?.index ?? 0,
|
|
1521
|
+
queue: (d.queue?.queue ?? []).map(queueItemFromJson)
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1021
1524
|
|
|
1022
1525
|
// src/index.ts
|
|
1023
1526
|
init_errors();
|
|
@@ -1031,7 +1534,10 @@ export {
|
|
|
1031
1534
|
RockskyIndex,
|
|
1032
1535
|
RockskyError,
|
|
1033
1536
|
RockskyClient,
|
|
1537
|
+
RemotePlayer,
|
|
1538
|
+
RemoteController,
|
|
1034
1539
|
Interval,
|
|
1540
|
+
DEFAULT_REMOTE_WS,
|
|
1035
1541
|
DEFAULT_JETSTREAM_SERVERS,
|
|
1036
1542
|
DEFAULT_APPVIEW,
|
|
1037
1543
|
Agent
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteController — build a Rocksky remote UI in a few lines.
|
|
3
|
+
*
|
|
4
|
+
* The other half of the remote-control WebSocket protocol (see
|
|
5
|
+
* remote-ws/PROTOCOL.md): where {@link RemotePlayer} is a controllable player, a
|
|
6
|
+
* controller lists the user's players, observes what each is playing
|
|
7
|
+
* (now-playing / status / queue), picks the primary device, and sends commands
|
|
8
|
+
* (play/pause/next/previous/seek/queue actions/enqueue). Heartbeat, reconnect,
|
|
9
|
+
* and the register handshake are handled for you.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const controller = new RemoteController({ token, name: "My Controller" });
|
|
13
|
+
* controller.on("devices", (d) => render(d.devices));
|
|
14
|
+
* controller.on("nowPlaying", (e) => showTrack(e.deviceId, e.track));
|
|
15
|
+
* controller.connect();
|
|
16
|
+
* // …then drive a device:
|
|
17
|
+
* controller.setPrimary(deviceId);
|
|
18
|
+
* controller.pause(deviceId);
|
|
19
|
+
* controller.seek(deviceId, 42000);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { DEFAULT_REMOTE_WS, type RemoteNowPlaying, type RemoteQueueItem } from "./remote-player.js";
|
|
23
|
+
export { DEFAULT_REMOTE_WS, type RemoteNowPlaying, type RemoteQueueItem };
|
|
24
|
+
/** A player device visible to the controller. */
|
|
25
|
+
export interface RemoteDevice {
|
|
26
|
+
deviceId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
nowPlaying?: RemoteNowPlaying;
|
|
29
|
+
queueIndex: number;
|
|
30
|
+
queue: RemoteQueueItem[];
|
|
31
|
+
}
|
|
32
|
+
/** Transport state advertised by a device. */
|
|
33
|
+
export type RemoteStatus = "playing" | "paused" | "stopped";
|
|
34
|
+
/** Events the controller emits (from the server). */
|
|
35
|
+
export interface RemoteControllerEvents {
|
|
36
|
+
/** The full device snapshot (on connect + reconnect). */
|
|
37
|
+
devices(payload: {
|
|
38
|
+
primaryDevice: string | null;
|
|
39
|
+
devices: RemoteDevice[];
|
|
40
|
+
}): void;
|
|
41
|
+
deviceRegistered(payload: {
|
|
42
|
+
deviceId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}): void;
|
|
45
|
+
deviceUnregistered(payload: {
|
|
46
|
+
deviceId: string;
|
|
47
|
+
}): void;
|
|
48
|
+
primaryChanged(payload: {
|
|
49
|
+
deviceId: string;
|
|
50
|
+
}): void;
|
|
51
|
+
nowPlaying(payload: {
|
|
52
|
+
deviceId: string;
|
|
53
|
+
deviceName: string;
|
|
54
|
+
track: RemoteNowPlaying;
|
|
55
|
+
}): void;
|
|
56
|
+
status(payload: {
|
|
57
|
+
deviceId: string;
|
|
58
|
+
deviceName: string;
|
|
59
|
+
status: RemoteStatus;
|
|
60
|
+
}): void;
|
|
61
|
+
queue(payload: {
|
|
62
|
+
deviceId: string;
|
|
63
|
+
deviceName: string;
|
|
64
|
+
index: number;
|
|
65
|
+
queue: RemoteQueueItem[];
|
|
66
|
+
}): void;
|
|
67
|
+
}
|
|
68
|
+
type EventName = keyof RemoteControllerEvents;
|
|
69
|
+
type EventHandler<K extends EventName> = RemoteControllerEvents[K];
|
|
70
|
+
export interface RemoteControllerOptions {
|
|
71
|
+
/** Rocksky access token, or a getter (read fresh on each (re)connect/send). */
|
|
72
|
+
token: string | (() => string | undefined);
|
|
73
|
+
/** Registration label (controllers are hidden from device lists). */
|
|
74
|
+
name: string;
|
|
75
|
+
/** WebSocket endpoint. Defaults to {@link DEFAULT_REMOTE_WS}. */
|
|
76
|
+
url?: string;
|
|
77
|
+
/** Heartbeat interval, ms (default 10000). */
|
|
78
|
+
heartbeatMs?: number;
|
|
79
|
+
/** Reconnect delay after a drop, ms (default 3000). */
|
|
80
|
+
reconnectMs?: number;
|
|
81
|
+
/** Optional debug logger. */
|
|
82
|
+
debug?: (...args: unknown[]) => void;
|
|
83
|
+
}
|
|
84
|
+
type Json = any;
|
|
85
|
+
export declare class RemoteController {
|
|
86
|
+
private readonly opts;
|
|
87
|
+
private ws;
|
|
88
|
+
private stopped;
|
|
89
|
+
private heartbeat?;
|
|
90
|
+
private reconnectTimer?;
|
|
91
|
+
private readonly url;
|
|
92
|
+
private readonly heartbeatMs;
|
|
93
|
+
private readonly reconnectMs;
|
|
94
|
+
private readonly getToken;
|
|
95
|
+
private readonly debug;
|
|
96
|
+
private handlers;
|
|
97
|
+
constructor(opts: RemoteControllerOptions);
|
|
98
|
+
/** Subscribe to a server event. Returns `this` for chaining. */
|
|
99
|
+
on<K extends EventName>(event: K, handler: EventHandler<K>): this;
|
|
100
|
+
/** Connect, register, and start the heartbeat + auto-reconnect loop. */
|
|
101
|
+
connect(): void;
|
|
102
|
+
/** Tear the connection down and stop reconnecting. */
|
|
103
|
+
disconnect(): void;
|
|
104
|
+
/** Choose the primary (scrobble/profile) device. */
|
|
105
|
+
setPrimary(deviceId: string): void;
|
|
106
|
+
/** `play`. `target` is a device id, or omit to broadcast to all devices. */
|
|
107
|
+
play(target?: string): void;
|
|
108
|
+
pause(target?: string): void;
|
|
109
|
+
next(target?: string): void;
|
|
110
|
+
previous(target?: string): void;
|
|
111
|
+
seek(target: string | undefined, positionMs: number): void;
|
|
112
|
+
queueJump(target: string | undefined, index: number): void;
|
|
113
|
+
queueRemove(target: string | undefined, index: number): void;
|
|
114
|
+
/** Enqueue tracks on the target device. */
|
|
115
|
+
enqueue(target: string | undefined, tracks: RemoteQueueItem[], mode?: "now" | "next" | "last", shuffle?: boolean, startIndex?: number): void;
|
|
116
|
+
/** Send an arbitrary command (escape hatch). */
|
|
117
|
+
command(action: string, target?: string, args?: Json): void;
|
|
118
|
+
private send;
|
|
119
|
+
private open;
|
|
120
|
+
private handle;
|
|
121
|
+
private handleMessage;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=remote-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-controller.d.ts","sourceRoot":"","sources":["../src/remote-controller.ts"],"names":[],"mappings":"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"}
|