alphatheta-connect 0.26.1 → 0.27.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/lib/index.js CHANGED
@@ -700,14 +700,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
700
700
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
701
701
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
702
702
  };
703
- var _Control_hostDevice, _Control_beatSocket, _Control_correlationByte;
703
+ var _Control_hostDevice, _Control_beatSocket, _Control_statusSocket, _Control_correlationByte;
704
704
  Object.defineProperty(exports, "__esModule", ({ value: true }));
705
- exports.makePlaystatePacket = void 0;
705
+ exports.makePlaystatePacket = exports.makeLoadTrackPacket = void 0;
706
706
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
707
707
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
708
708
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
709
709
  const udp_1 = __webpack_require__(/*! src/utils/udp */ "./src/utils/udp.ts");
710
+ const load_1 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
710
711
  const stagehand_1 = __webpack_require__(/*! ./stagehand */ "./src/control/stagehand.ts");
712
+ var load_2 = __webpack_require__(/*! ./load */ "./src/control/load.ts");
713
+ Object.defineProperty(exports, "makeLoadTrackPacket", ({ enumerable: true, get: function () { return load_2.makeLoadTrackPacket; } }));
711
714
  const STATE_MAP = {
712
715
  [types_1.CDJStatus.PlayState.Cued]: 0x01,
713
716
  [types_1.CDJStatus.PlayState.Playing]: 0x00,
@@ -728,20 +731,51 @@ const makePlaystatePacket = ({ hostDevice, device, playState }) => Uint8Array.fr
728
731
  ]);
729
732
  exports.makePlaystatePacket = makePlaystatePacket;
730
733
  class Control {
731
- constructor(beatSocket, hostDevice) {
734
+ constructor(beatSocket, hostDevice, statusSocket = null) {
732
735
  _Control_hostDevice.set(this, void 0);
733
736
  /**
734
737
  * The socket used to send control packets
735
738
  */
736
739
  _Control_beatSocket.set(this, void 0);
740
+ /**
741
+ * The socket bound to the status port, which the Load Track command must
742
+ * leave from: a CDJ-3000 ignores it from any other port. Absent when the
743
+ * control service was built without one.
744
+ */
745
+ _Control_statusSocket.set(this, void 0);
737
746
  /**
738
747
  * Randomized correlation byte per session for Stagehand commands
739
748
  */
740
749
  _Control_correlationByte.set(this, void 0);
741
750
  __classPrivateFieldSet(this, _Control_beatSocket, beatSocket, "f");
751
+ __classPrivateFieldSet(this, _Control_statusSocket, statusSocket, "f");
742
752
  __classPrivateFieldSet(this, _Control_hostDevice, hostDevice, "f");
743
753
  __classPrivateFieldSet(this, _Control_correlationByte, Math.floor(Math.random() * 256), "f");
744
754
  }
755
+ /**
756
+ * Send one Stagehand transport packet (0x07) carrying `op` with the press /
757
+ * release flag, for opcodes the named methods do not cover.
758
+ */
759
+ async transport(device, op, press) {
760
+ const p = (0, stagehand_1.makeStagehandTransportPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), op, press, __classPrivateFieldGet(this, _Control_correlationByte, "f"));
761
+ await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.BEAT_PORT, device.ip.address);
762
+ }
763
+ /**
764
+ * Tell a player to load a track, as rekordbox does when a row is dragged
765
+ * onto a deck: the Load Track command (0x19) from our status port to the
766
+ * player's. The player answers 0x1a and the load shows in its next status.
767
+ *
768
+ * `source` names where the track lives: a device number and its slot (a
769
+ * player's USB or SD, or the rekordbox slot of a library served over the
770
+ * link).
771
+ */
772
+ async loadTrack(device, trackId, source) {
773
+ if (__classPrivateFieldGet(this, _Control_statusSocket, "f") === null) {
774
+ throw new Error('loadTrack needs the status socket; connect through ProlinkNetwork');
775
+ }
776
+ const p = (0, load_1.makeLoadTrackPacket)(__classPrivateFieldGet(this, _Control_hostDevice, "f"), device, trackId, source);
777
+ await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_statusSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
778
+ }
745
779
  /**
746
780
  * Start or stop a CDJ on the network.
747
781
  * Delegates automatically to Stagehand-specific transport control if connected in Stagehand mode.
@@ -807,10 +841,79 @@ class Control {
807
841
  await (0, udp_1.udpSend)(__classPrivateFieldGet(this, _Control_beatSocket, "f"), p, constants_1.STATUS_PORT, device.ip.address);
808
842
  }
809
843
  }
810
- _Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
844
+ _Control_hostDevice = new WeakMap(), _Control_beatSocket = new WeakMap(), _Control_statusSocket = new WeakMap(), _Control_correlationByte = new WeakMap();
811
845
  exports["default"] = Control;
812
846
 
813
847
 
848
+ /***/ },
849
+
850
+ /***/ "./src/control/load.ts"
851
+ /*!*****************************!*\
852
+ !*** ./src/control/load.ts ***!
853
+ \*****************************/
854
+ (__unused_webpack_module, exports, __webpack_require__) {
855
+
856
+ "use strict";
857
+
858
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
859
+ exports.LOAD_TRACK_LENGTH = void 0;
860
+ exports.makeLoadTrackPacket = makeLoadTrackPacket;
861
+ const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
862
+ const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
863
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
864
+ /**
865
+ * Byte length of a Load Track command.
866
+ */
867
+ exports.LOAD_TRACK_LENGTH = 0x58;
868
+ /**
869
+ * Generates the Load Track command (0x19, 88 bytes), unicast to a player's
870
+ * status port.
871
+ *
872
+ * Byte for byte what rekordbox 7.2 sends a CDJ-3000 (captured 2026-09-13 and
873
+ * verified live on two CDJ-3000s): the status-style header with a 20-byte
874
+ * name, `01 01`, our device number, the length `0034` of what follows, our
875
+ * number again, then the source device, slot and track type, the track ID
876
+ * big-endian, `32` at 0x33, the destination player counted from zero at
877
+ * 0x40, and `32` at 0x4b.
878
+ *
879
+ * A player only honours the command from the address its keep-alives
880
+ * announced and from port 50002.
881
+ */
882
+ function makeLoadTrackPacket(hostDevice, device, trackId, source) {
883
+ var _a;
884
+ const packet = new Uint8Array(exports.LOAD_TRACK_LENGTH);
885
+ // 0-9: magic header
886
+ packet.set(constants_1.PROLINK_HEADER, 0);
887
+ // 10: kind 0x19
888
+ packet[10] = 0x19;
889
+ // 11-30: device name (20 bytes)
890
+ packet.set((0, utils_1.buildName)(hostDevice), 11);
891
+ // 31-35: 01, subtype 01, our number, then the length 0x0034 of what follows
892
+ packet[31] = 0x01;
893
+ packet[32] = 0x01;
894
+ packet[33] = hostDevice.id;
895
+ packet[34] = 0x00;
896
+ packet[35] = 0x34;
897
+ // 36: our number again
898
+ packet[36] = hostDevice.id;
899
+ // 40-42: the source device, its slot and the track type
900
+ packet[40] = source.deviceId;
901
+ packet[41] = source.slot;
902
+ packet[42] = (_a = source.trackType) !== null && _a !== void 0 ? _a : types_1.TrackType.RB;
903
+ // 44-47: the track ID, big-endian
904
+ packet[44] = (trackId >>> 24) & 0xff;
905
+ packet[45] = (trackId >>> 16) & 0xff;
906
+ packet[46] = (trackId >>> 8) & 0xff;
907
+ packet[47] = trackId & 0xff;
908
+ packet[0x33] = 0x32;
909
+ // 0x40: the player to load onto, counted from zero; a player accepts the
910
+ // command by address regardless
911
+ packet[0x40] = Math.max(device.id - 1, 0);
912
+ packet[0x4b] = 0x32;
913
+ return packet;
914
+ }
915
+
916
+
814
917
  /***/ },
815
918
 
816
919
  /***/ "./src/control/stagehand.ts"
@@ -827,59 +930,75 @@ exports.makeStagehandPrefWritePacket = makeStagehandPrefWritePacket;
827
930
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
828
931
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
829
932
  /**
830
- * Generates a Stagehand transport control packet (0x07, 56 bytes)
933
+ * The name field of a Stagehand unicast frame: 19 bytes, one shorter than the
934
+ * broadcast frames' 20, so the bytes that follow it sit at offset 30.
935
+ */
936
+ function buildUnicastName(device) {
937
+ return (0, utils_1.buildName)(device).subarray(0, 19);
938
+ }
939
+ /**
940
+ * Generates a Stagehand transport control packet (0x07, 48 bytes).
941
+ *
942
+ * Byte for byte what the iPad app sends a CDJ-3000 (captured 2026-05-23,
943
+ * `stagehand-allproto-20260523T142615.pcap`, pinned in the tests): the
944
+ * unicast header `03 01 00`, a byte that varies per command, the length
945
+ * `0030`, then `3a`, `01`, the opcode and the press flag each in their own
946
+ * 16-bit slot.
831
947
  *
832
948
  * @param hostDevice - The Stagehand device posing as sender
833
- * @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x1a, 0x1b)
949
+ * @param op - The command opcode (e.g. 0x0f, 0x14, 0x18, 0x19, 0x1a, 0x1b)
834
950
  * @param press - Whether the action is press (true) or release (false)
835
- * @param correlationByte - The randomized per-session correlation byte
951
+ * @param correlationByte - The per-session correlation byte
836
952
  */
837
953
  function makeStagehandTransportPacket(hostDevice, op, press, correlationByte) {
838
- const packet = new Uint8Array(56);
954
+ const packet = new Uint8Array(48);
839
955
  // 0-9: magic header
840
956
  packet.set(constants_1.PROLINK_HEADER, 0);
841
957
  // 10: opcode 0x07
842
958
  packet[10] = 0x07;
843
- // 11-30: device name
844
- packet.set((0, utils_1.buildName)(hostDevice), 11);
845
- // 31: 0x01
959
+ // 11-29: device name
960
+ packet.set(buildUnicastName(hostDevice), 11);
961
+ // 30-32: unicast header 03 01 00
962
+ packet[30] = 0x03;
846
963
  packet[31] = 0x01;
847
- // 32: 0x03
848
- packet[32] = 0x03;
964
+ packet[32] = 0x00;
849
965
  // 33: per-session correlation byte
850
966
  packet[33] = correlationByte;
851
- // 34-35: remaining length 0x0030 (48 bytes)
967
+ // 34-35: packet length 0x0030 (48 bytes)
852
968
  packet[34] = 0x00;
853
969
  packet[35] = 0x30;
854
- // 40: Stagehand sub-id 0x3a
855
- packet[40] = 0x3a;
856
- // 44: command opcode
857
- packet[44] = op;
858
- // 46: press/release flag
859
- packet[46] = press ? 0x01 : 0x00;
970
+ // 39: Stagehand sub-id 0x3a
971
+ packet[39] = 0x3a;
972
+ // 41: 0x01
973
+ packet[41] = 0x01;
974
+ // 43: command opcode
975
+ packet[43] = op;
976
+ // 45: press/release flag
977
+ packet[45] = press ? 0x01 : 0x00;
860
978
  return packet;
861
979
  }
862
980
  /**
863
- * Generates a Stagehand preference write packet (0x6b, 124 bytes)
981
+ * Generates a Stagehand preference write packet (0x6b, 116 bytes).
982
+ *
983
+ * As captured from the iPad app (same capture as the transport packet): the
984
+ * unicast header `03 01 00 3a`, the length `0050`, the write flag, then the
985
+ * preference slots.
864
986
  *
865
987
  * @param hostDevice - The Stagehand device posing as sender
866
988
  * @param options - The preferences to write (onAir, quantize)
867
989
  */
868
990
  function makeStagehandPrefWritePacket(hostDevice, options) {
869
- const packet = new Uint8Array(124);
991
+ const packet = new Uint8Array(116);
870
992
  // 0-9: magic header
871
993
  packet.set(constants_1.PROLINK_HEADER, 0);
872
994
  // 10: opcode 0x6b
873
995
  packet[10] = 0x6b;
874
- // 11-30: device name
875
- const name = (0, utils_1.buildName)(hostDevice);
876
- // Trailing byte 30 (which is offset 30, meaning index 19 of name) is set to 0x03
877
- name[19] = 0x03;
878
- packet.set(name, 11);
879
- // 31: 0x01 (subscription-id-a constant)
996
+ // 11-29: device name
997
+ packet.set(buildUnicastName(hostDevice), 11);
998
+ // 30-32: unicast header 03 01 00
999
+ packet[30] = 0x03;
880
1000
  packet[31] = 0x01;
881
- // 32: 0x03 (constant)
882
- packet[32] = 0x03;
1001
+ packet[32] = 0x00;
883
1002
  // 33: Stagehand sub-id constant 0x3a
884
1003
  packet[33] = 0x3a;
885
1004
  // 34-35: body length 0x0050 (80 bytes)
@@ -2137,10 +2256,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
2137
2256
  return (mod && mod.__esModule) ? mod : { "default": mod };
2138
2257
  };
2139
2258
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2140
- exports.noopLogger = exports.REMOTEDB_MAX_DEVICE_ID = exports.playerNumberCeiling = exports.pickRemoteDbQueryId = exports.pickAvailableDeviceId = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.DEFAULT_MIXER_PLAYER_CEILING = exports.OneLibraryAdapter = exports.fetchFile = exports.loadAnlz = exports.isMetadataExtractionSupported = exports.extractMetadataFromDevice = exports.extractFullMetadata = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = void 0;
2259
+ exports.noopLogger = exports.REMOTEDB_MAX_DEVICE_ID = exports.playerNumberCeiling = exports.pickRemoteDbQueryId = exports.pickAvailableDeviceId = exports.MIXER_PLAYER_CEILING = exports.MAX_DEVICE_ID = exports.DEFAULT_MIXER_PLAYER_CEILING = exports.OneLibraryAdapter = exports.fetchFile = exports.loadAnlz = exports.isMetadataExtractionSupported = exports.extractMetadataFromDevice = exports.extractFullMetadata = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = exports.createRemoteServer = exports.Control = void 0;
2260
+ var control_1 = __webpack_require__(/*! ./control */ "./src/control/index.ts");
2261
+ Object.defineProperty(exports, "Control", ({ enumerable: true, get: function () { return __importDefault(control_1).default; } }));
2141
2262
  __exportStar(__webpack_require__(/*! ./entities */ "./src/entities.ts"), exports);
2142
2263
  __exportStar(__webpack_require__(/*! ./mixstatus */ "./src/mixstatus/index.ts"), exports);
2143
2264
  __exportStar(__webpack_require__(/*! ./network */ "./src/network.ts"), exports);
2265
+ __exportStar(__webpack_require__(/*! ./remote */ "./src/remote/index.ts"), exports);
2266
+ __exportStar(__webpack_require__(/*! ./remote/hue */ "./src/remote/hue.ts"), exports);
2267
+ var server_1 = __webpack_require__(/*! ./remote/server */ "./src/remote/server.ts");
2268
+ Object.defineProperty(exports, "createRemoteServer", ({ enumerable: true, get: function () { return server_1.createServer; } }));
2144
2269
  var position_1 = __webpack_require__(/*! ./status/position */ "./src/status/position.ts");
2145
2270
  Object.defineProperty(exports, "PositionEmitter", ({ enumerable: true, get: function () { return __importDefault(position_1).default; } }));
2146
2271
  // Passive mode (pcap-based monitoring without announcing a VCDJ)
@@ -7252,9 +7377,9 @@ class ProlinkNetwork {
7252
7377
  // Create unified database
7253
7378
  const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
7254
7379
  // Create controller service
7255
- const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
7380
+ const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_statusSocket, "f"));
7256
7381
  __classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
7257
- __classPrivateFieldSet(this, _ProlinkNetwork_connection, { announcer, heartbeat, control, remotedb, localdb, database }, "f");
7382
+ __classPrivateFieldSet(this, _ProlinkNetwork_connection, { vcdj, announcer, heartbeat, control, remotedb, localdb, database }, "f");
7258
7383
  tx.finish();
7259
7384
  }
7260
7385
  /**
@@ -7341,6 +7466,15 @@ class ProlinkNetwork {
7341
7466
  get positionEmitter() {
7342
7467
  return __classPrivateFieldGet(this, _ProlinkNetwork_state, "f") === types_1.NetworkState.Connected ? __classPrivateFieldGet(this, _ProlinkNetwork_positionEmitter, "f") : null;
7343
7468
  }
7469
+ /**
7470
+ * The device this network announces itself as, once connected: the
7471
+ * `hostDevice` the status emitter's media query and the control service
7472
+ * send from.
7473
+ */
7474
+ get virtualDevice() {
7475
+ var _a, _b;
7476
+ return (_b = (_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.vcdj) !== null && _b !== void 0 ? _b : null;
7477
+ }
7344
7478
  /**
7345
7479
  * Get the {@link Control} service. This service can be used to control the
7346
7480
  * Playstate of CDJs on the network.
@@ -10551,6 +10685,773 @@ _PassiveStatusEmitter_emitter = new WeakMap(), _PassiveStatusEmitter_adapter = n
10551
10685
  exports["default"] = PassiveStatusEmitter;
10552
10686
 
10553
10687
 
10688
+ /***/ },
10689
+
10690
+ /***/ "./src/remote/hue.ts"
10691
+ /*!***************************!*\
10692
+ !*** ./src/remote/hue.ts ***!
10693
+ \***************************/
10694
+ (__unused_webpack_module, exports, __webpack_require__) {
10695
+
10696
+ "use strict";
10697
+
10698
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
10699
+ if (kind === "m") throw new TypeError("Private method is not writable");
10700
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
10701
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10702
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
10703
+ };
10704
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
10705
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10706
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10707
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
10708
+ };
10709
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10710
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10711
+ };
10712
+ var _Hue_instances, _Hue_config, _Hue_url;
10713
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10714
+ exports.Hue = exports.HueNoGroupError = exports.HueUnconfiguredError = exports.HueError = void 0;
10715
+ exports.hueConfigPath = hueConfigPath;
10716
+ exports.loadHueConfig = loadHueConfig;
10717
+ exports.saveHueConfig = saveHueConfig;
10718
+ exports.pairHue = pairHue;
10719
+ exports.discoverHueBridge = discoverHueBridge;
10720
+ /**
10721
+ * The Hue bridge: the room or zone the booth's gear is plugged into, switched
10722
+ * as one group over the bridge's v1 API (plain HTTP, no certificate dance).
10723
+ *
10724
+ * The application key comes from `hue pair`, which asks the bridge for one
10725
+ * while its link button is pressed, and is kept in a config file.
10726
+ */
10727
+ const fs_1 = __webpack_require__(/*! fs */ "fs");
10728
+ const os_1 = __webpack_require__(/*! os */ "os");
10729
+ const path_1 = __importDefault(__webpack_require__(/*! path */ "path"));
10730
+ class HueError extends Error {
10731
+ }
10732
+ exports.HueError = HueError;
10733
+ /**
10734
+ * Raised when nothing has been paired and the environment names no bridge.
10735
+ */
10736
+ class HueUnconfiguredError extends HueError {
10737
+ constructor() {
10738
+ super('Hue is not configured: run `alphatheta-remote hue pair` (or set HUE_BRIDGE and HUE_APP_KEY)');
10739
+ }
10740
+ }
10741
+ exports.HueUnconfiguredError = HueUnconfiguredError;
10742
+ /**
10743
+ * Raised when the bridge has no group by the name asked for.
10744
+ */
10745
+ class HueNoGroupError extends HueError {
10746
+ constructor(name, groups) {
10747
+ const have = groups.map(g => JSON.stringify(g.name)).join(', ');
10748
+ super(`no Hue group named ${JSON.stringify(name)}; the bridge has: ${have}`);
10749
+ }
10750
+ }
10751
+ exports.HueNoGroupError = HueNoGroupError;
10752
+ const REQUEST_TIMEOUT = 10000;
10753
+ function hueConfigPath() {
10754
+ var _a, _b;
10755
+ const base = (_a = process.env.XDG_CONFIG_HOME) !== null && _a !== void 0 ? _a : path_1.default.join((_b = process.env.HOME) !== null && _b !== void 0 ? _b : '.', '.config');
10756
+ return path_1.default.join(base, 'alphatheta-remote', 'hue.json');
10757
+ }
10758
+ /**
10759
+ * The environment first (`HUE_BRIDGE`, `HUE_APP_KEY`), then the file `hue
10760
+ * pair` wrote; `null` when neither is there.
10761
+ */
10762
+ async function loadHueConfig() {
10763
+ const { HUE_BRIDGE, HUE_APP_KEY } = process.env;
10764
+ if (HUE_BRIDGE && HUE_APP_KEY) {
10765
+ return { bridge: HUE_BRIDGE, appKey: HUE_APP_KEY };
10766
+ }
10767
+ try {
10768
+ const text = await fs_1.promises.readFile(hueConfigPath(), 'utf8');
10769
+ const parsed = JSON.parse(text);
10770
+ if (typeof parsed.bridge !== 'string' || typeof parsed.appKey !== 'string') {
10771
+ throw new HueError(`${hueConfigPath()}: expected {bridge, appKey}`);
10772
+ }
10773
+ return { bridge: parsed.bridge, appKey: parsed.appKey };
10774
+ }
10775
+ catch (e) {
10776
+ if (e.code === 'ENOENT') {
10777
+ return null;
10778
+ }
10779
+ throw e;
10780
+ }
10781
+ }
10782
+ async function saveHueConfig(config) {
10783
+ const file = hueConfigPath();
10784
+ await fs_1.promises.mkdir(path_1.default.dirname(file), { recursive: true });
10785
+ await fs_1.promises.writeFile(file, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
10786
+ return file;
10787
+ }
10788
+ async function request(url, init) {
10789
+ const response = await fetch(url, {
10790
+ ...init,
10791
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT),
10792
+ });
10793
+ if (!response.ok) {
10794
+ throw new HueError(`hue bridge: HTTP ${response.status} ${await response.text()}`);
10795
+ }
10796
+ return (await response.json());
10797
+ }
10798
+ /**
10799
+ * A v1 reply is a list of `{success: …}` / `{error: …}` items; any error
10800
+ * item fails the call.
10801
+ */
10802
+ function bridgeErrors(reply) {
10803
+ if (!Array.isArray(reply)) {
10804
+ return;
10805
+ }
10806
+ const errors = reply
10807
+ .map(item => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.error) === null || _a === void 0 ? void 0 : _a.description; })
10808
+ .filter((d) => typeof d === 'string');
10809
+ if (errors.length > 0) {
10810
+ throw new HueError(`hue bridge answered: ${errors.join('; ')}`);
10811
+ }
10812
+ }
10813
+ class Hue {
10814
+ constructor(config) {
10815
+ _Hue_instances.add(this);
10816
+ _Hue_config.set(this, void 0);
10817
+ __classPrivateFieldSet(this, _Hue_config, config, "f");
10818
+ }
10819
+ get bridge() {
10820
+ return __classPrivateFieldGet(this, _Hue_config, "f").bridge;
10821
+ }
10822
+ /**
10823
+ * Every room and zone on the bridge, by name.
10824
+ */
10825
+ async groups() {
10826
+ const raw = await request(__classPrivateFieldGet(this, _Hue_instances, "m", _Hue_url).call(this, 'groups'));
10827
+ bridgeErrors(raw);
10828
+ return Object.entries(raw)
10829
+ .map(([id, g]) => {
10830
+ var _a, _b, _c, _d, _e;
10831
+ return ({
10832
+ id,
10833
+ name: g.name,
10834
+ type: g.type,
10835
+ lights: (_a = g.lights) !== null && _a !== void 0 ? _a : [],
10836
+ allOn: (_c = (_b = g.state) === null || _b === void 0 ? void 0 : _b.all_on) !== null && _c !== void 0 ? _c : false,
10837
+ anyOn: (_e = (_d = g.state) === null || _d === void 0 ? void 0 : _d.any_on) !== null && _e !== void 0 ? _e : false,
10838
+ });
10839
+ })
10840
+ .sort((a, b) => a.name.localeCompare(b.name));
10841
+ }
10842
+ /**
10843
+ * The group called `name`, matched without regard to case.
10844
+ */
10845
+ async groupNamed(name) {
10846
+ const groups = await this.groups();
10847
+ const wanted = name.trim().toLowerCase();
10848
+ const group = groups.find(g => g.name.trim().toLowerCase() === wanted);
10849
+ if (group === undefined) {
10850
+ throw new HueNoGroupError(name, groups);
10851
+ }
10852
+ return group;
10853
+ }
10854
+ /**
10855
+ * Switch every light (and plug) in the group.
10856
+ */
10857
+ async setGroupOn(groupId, on) {
10858
+ const reply = await request(__classPrivateFieldGet(this, _Hue_instances, "m", _Hue_url).call(this, `groups/${groupId}/action`), {
10859
+ method: 'PUT',
10860
+ body: JSON.stringify({ on }),
10861
+ });
10862
+ bridgeErrors(reply);
10863
+ }
10864
+ }
10865
+ exports.Hue = Hue;
10866
+ _Hue_config = new WeakMap(), _Hue_instances = new WeakSet(), _Hue_url = function _Hue_url(tail) {
10867
+ return `http://${__classPrivateFieldGet(this, _Hue_config, "f").bridge}/api/${__classPrivateFieldGet(this, _Hue_config, "f").appKey}/${tail}`;
10868
+ };
10869
+ /**
10870
+ * Ask the bridge for an application key. The bridge only issues one in the
10871
+ * thirty seconds after its link button is pressed, so this asks once a second
10872
+ * until it does or `waitMs` runs out.
10873
+ */
10874
+ async function pairHue(bridge, waitMs = 60000) {
10875
+ var _a, _b, _c, _d;
10876
+ const deadline = Date.now() + waitMs;
10877
+ // The bridge takes `<application>#<device>` with the device name at most
10878
+ // 19 characters, so the host's name is cut at its first dot and there.
10879
+ const host = (0, os_1.hostname)().split('.')[0].slice(0, 19);
10880
+ const body = JSON.stringify({ devicetype: `alphatheta-remote#${host}` });
10881
+ for (;;) {
10882
+ const reply = await request(`http://${bridge}/api`, { method: 'POST', body });
10883
+ const key = (_b = (_a = reply === null || reply === void 0 ? void 0 : reply[0]) === null || _a === void 0 ? void 0 : _a.success) === null || _b === void 0 ? void 0 : _b.username;
10884
+ if (typeof key === 'string') {
10885
+ return { bridge, appKey: key };
10886
+ }
10887
+ const linkButtonNotPressed = ((_d = (_c = reply === null || reply === void 0 ? void 0 : reply[0]) === null || _c === void 0 ? void 0 : _c.error) === null || _d === void 0 ? void 0 : _d.type) === 101;
10888
+ if (!linkButtonNotPressed) {
10889
+ bridgeErrors(reply);
10890
+ }
10891
+ if (Date.now() >= deadline) {
10892
+ throw new HueError('hue bridge: link button was not pressed in time');
10893
+ }
10894
+ await new Promise(r => setTimeout(r, 1000));
10895
+ }
10896
+ }
10897
+ /**
10898
+ * Where the bridge is: `HUE_BRIDGE`, else Signify's discovery service, which
10899
+ * lists the bridges that have phoned home from this public address.
10900
+ */
10901
+ async function discoverHueBridge() {
10902
+ var _a;
10903
+ if (process.env.HUE_BRIDGE) {
10904
+ return process.env.HUE_BRIDGE;
10905
+ }
10906
+ const found = await request('https://discovery.meethue.com');
10907
+ const bridge = (_a = found[0]) === null || _a === void 0 ? void 0 : _a.internalipaddress;
10908
+ if (bridge === undefined) {
10909
+ throw new HueError('no Hue bridge found; set HUE_BRIDGE to its address');
10910
+ }
10911
+ return bridge;
10912
+ }
10913
+
10914
+
10915
+ /***/ },
10916
+
10917
+ /***/ "./src/remote/index.ts"
10918
+ /*!*****************************!*\
10919
+ !*** ./src/remote/index.ts ***!
10920
+ \*****************************/
10921
+ (__unused_webpack_module, exports, __webpack_require__) {
10922
+
10923
+ "use strict";
10924
+
10925
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
10926
+ if (kind === "m") throw new TypeError("Private method is not writable");
10927
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
10928
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10929
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
10930
+ };
10931
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
10932
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10933
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10934
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
10935
+ };
10936
+ var _Remote_instances, _Remote_network, _Remote_statuses, _Remote_cueOp, _Remote_status_get, _Remote_control_get, _Remote_me_get;
10937
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10938
+ exports.Remote = exports.CueUnknownError = exports.NoPlayerError = exports.RemoteError = exports.TransportOp = void 0;
10939
+ /**
10940
+ * The remote: on the Pro DJ Link network as a Stagehand, the way the iPad app
10941
+ * joins, driving players with the transport packets the app's Sound Check
10942
+ * sends and remembering the last status each player reported. The REST API
10943
+ * over it is in `./server`.
10944
+ */
10945
+ const os_1 = __webpack_require__(/*! os */ "os");
10946
+ const network_1 = __webpack_require__(/*! src/network */ "./src/network.ts");
10947
+ const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
10948
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
10949
+ /**
10950
+ * The Stagehand transport opcodes (packet 0x07) the iPad app was captured
10951
+ * sending, besides play (0x0f then 0x14 pressed) and pause (0x14 released),
10952
+ * which `Control` sends itself. These are held while pressed.
10953
+ */
10954
+ exports.TransportOp = {
10955
+ SkipForward: 0x18,
10956
+ SkipBackward: 0x19,
10957
+ SeekForward: 0x1a,
10958
+ SeekBackward: 0x1b,
10959
+ };
10960
+ /**
10961
+ * The device numbers the Stagehand app was seen using: 141..=211.
10962
+ */
10963
+ const STAGEHAND_FIRST_NUMBER = 141;
10964
+ const STAGEHAND_NUMBER_COUNT = 71;
10965
+ const TAP_MS = 100;
10966
+ class RemoteError extends Error {
10967
+ }
10968
+ exports.RemoteError = RemoteError;
10969
+ class NoPlayerError extends RemoteError {
10970
+ constructor(number) {
10971
+ super(`no player ${number} on the link`);
10972
+ }
10973
+ }
10974
+ exports.NoPlayerError = NoPlayerError;
10975
+ class CueUnknownError extends RemoteError {
10976
+ constructor() {
10977
+ super('cue has no known Stagehand opcode; set the cue opcode once one is found (probe with POST /players/<n>/op)');
10978
+ }
10979
+ }
10980
+ exports.CueUnknownError = CueUnknownError;
10981
+ function resolveIface(hint) {
10982
+ const all = (0, os_1.networkInterfaces)();
10983
+ for (const [name, infos] of Object.entries(all)) {
10984
+ for (const info of infos !== null && infos !== void 0 ? infos : []) {
10985
+ if (info.family !== 'IPv4' || info.internal) {
10986
+ continue;
10987
+ }
10988
+ if (name === hint || info.address === hint) {
10989
+ return info;
10990
+ }
10991
+ }
10992
+ }
10993
+ const available = Object.entries(all)
10994
+ .flatMap(([name, infos]) => (infos !== null && infos !== void 0 ? infos : [])
10995
+ .filter(i => i.family === 'IPv4' && !i.internal)
10996
+ .map(i => `${name}:${i.address}`))
10997
+ .join(', ');
10998
+ throw new RemoteError(`no interface "${hint}"; available: ${available}`);
10999
+ }
11000
+ /**
11001
+ * A device number that does not change between runs. A CDJ-3000 keeps one
11002
+ * record per peer address and ignores a later claim from that address under
11003
+ * another number or MAC until it reboots, so a random number per run may
11004
+ * never be served.
11005
+ */
11006
+ function numberFor(iface) {
11007
+ var _a;
11008
+ const mac = iface.mac.split(':').map(b => parseInt(b, 16));
11009
+ const last = (_a = mac[5]) !== null && _a !== void 0 ? _a : 0;
11010
+ return STAGEHAND_FIRST_NUMBER + (last % STAGEHAND_NUMBER_COUNT);
11011
+ }
11012
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
11013
+ class Remote {
11014
+ constructor(network, cueOp) {
11015
+ _Remote_instances.add(this);
11016
+ _Remote_network.set(this, void 0);
11017
+ _Remote_statuses.set(this, new Map());
11018
+ _Remote_cueOp.set(this, void 0);
11019
+ __classPrivateFieldSet(this, _Remote_network, network, "f");
11020
+ __classPrivateFieldSet(this, _Remote_cueOp, cueOp, "f");
11021
+ __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_status_get).on('status', s => __classPrivateFieldGet(this, _Remote_statuses, "f").set(s.deviceId, s));
11022
+ }
11023
+ /**
11024
+ * Join the link and start collecting status. Resolves once the Stagehand
11025
+ * join sequence is through and keep-alives are running.
11026
+ */
11027
+ static async join(opts = {}) {
11028
+ var _a, _b, _c;
11029
+ const network = await (0, network_1.bringOnlineStagehand)({
11030
+ logger: opts.logger,
11031
+ });
11032
+ let iface;
11033
+ if (opts.iface !== undefined) {
11034
+ iface = resolveIface(opts.iface);
11035
+ }
11036
+ else {
11037
+ (_a = opts.logger) === null || _a === void 0 ? void 0 : _a.info('waiting for a player to pick the interface');
11038
+ const dm = network.deviceManager;
11039
+ const first = await new Promise(resolve => dm.once('connected', resolve));
11040
+ const match = (0, utils_1.getMatchingInterface)(first.ip);
11041
+ if (match === null) {
11042
+ throw new RemoteError(`no local interface matches ${first.ip.address}`);
11043
+ }
11044
+ iface = match;
11045
+ }
11046
+ network.configure({ iface, vcdjId: (_b = opts.number) !== null && _b !== void 0 ? _b : numberFor(iface) });
11047
+ network.connect();
11048
+ if (!network.isConnected()) {
11049
+ throw new RemoteError('failed to connect to the link');
11050
+ }
11051
+ await network.startupReady;
11052
+ const me = network.virtualDevice;
11053
+ (_c = opts.logger) === null || _c === void 0 ? void 0 : _c.info(`on the link as "${me === null || me === void 0 ? void 0 : me.name}" #${me === null || me === void 0 ? void 0 : me.id} at ${iface.address}`);
11054
+ return new Remote(network, opts.cueOp);
11055
+ }
11056
+ get network() {
11057
+ return __classPrivateFieldGet(this, _Remote_network, "f");
11058
+ }
11059
+ /**
11060
+ * Our own device as announced.
11061
+ */
11062
+ get me() {
11063
+ return __classPrivateFieldGet(this, _Remote_network, "f").virtualDevice;
11064
+ }
11065
+ /**
11066
+ * Every device heard on the link, by number.
11067
+ */
11068
+ get devices() {
11069
+ return [...__classPrivateFieldGet(this, _Remote_network, "f").deviceManager.devices.values()].sort((a, b) => a.id - b.id);
11070
+ }
11071
+ /**
11072
+ * The players, with the last status each one sent.
11073
+ */
11074
+ get players() {
11075
+ return this.devices
11076
+ .filter(d => d.type === types_1.DeviceType.CDJ)
11077
+ .map(d => ({
11078
+ number: d.id,
11079
+ name: d.name,
11080
+ ip: d.ip.address,
11081
+ status: __classPrivateFieldGet(this, _Remote_statuses, "f").get(d.id),
11082
+ }));
11083
+ }
11084
+ player(number) {
11085
+ const device = __classPrivateFieldGet(this, _Remote_network, "f").deviceManager.devices.get(number);
11086
+ if (device === undefined || device.type !== types_1.DeviceType.CDJ) {
11087
+ throw new NoPlayerError(number);
11088
+ }
11089
+ return device;
11090
+ }
11091
+ play(number) {
11092
+ return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).play(this.player(number));
11093
+ }
11094
+ pause(number) {
11095
+ return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).pause(this.player(number));
11096
+ }
11097
+ /**
11098
+ * Press and release the cue button, once its opcode is known.
11099
+ */
11100
+ cue(number) {
11101
+ if (__classPrivateFieldGet(this, _Remote_cueOp, "f") === undefined) {
11102
+ throw new CueUnknownError();
11103
+ }
11104
+ return this.tap(number, __classPrivateFieldGet(this, _Remote_cueOp, "f"), TAP_MS);
11105
+ }
11106
+ /**
11107
+ * Hold fast-forward for `holdMs`.
11108
+ */
11109
+ fastForward(number, holdMs) {
11110
+ return this.tap(number, exports.TransportOp.SeekForward, holdMs);
11111
+ }
11112
+ /**
11113
+ * Hold rewind for `holdMs`.
11114
+ */
11115
+ rewind(number, holdMs) {
11116
+ return this.tap(number, exports.TransportOp.SeekBackward, holdMs);
11117
+ }
11118
+ skipForward(number) {
11119
+ return this.tap(number, exports.TransportOp.SkipForward, TAP_MS);
11120
+ }
11121
+ skipBackward(number) {
11122
+ return this.tap(number, exports.TransportOp.SkipBackward, TAP_MS);
11123
+ }
11124
+ /**
11125
+ * One transport packet: `op` pressed or released.
11126
+ */
11127
+ transport(number, op, press) {
11128
+ return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).transport(this.player(number), op, press);
11129
+ }
11130
+ /**
11131
+ * Press `op`, hold it for `holdMs`, release it.
11132
+ */
11133
+ async tap(number, op, holdMs) {
11134
+ await this.transport(number, op, true);
11135
+ await sleep(holdMs);
11136
+ await this.transport(number, op, false);
11137
+ }
11138
+ /**
11139
+ * What is in a player's USB and SD slots, as the player answers a media
11140
+ * query; a slot it does not answer for is left out.
11141
+ */
11142
+ async media(number) {
11143
+ const device = this.player(number);
11144
+ const slots = [];
11145
+ for (const slot of [types_1.MediaSlot.USB, types_1.MediaSlot.SD]) {
11146
+ try {
11147
+ slots.push(await __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_status_get).queryMediaSlot({ hostDevice: __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_me_get), device, slot }));
11148
+ }
11149
+ catch {
11150
+ // an empty slot is not answered for
11151
+ }
11152
+ }
11153
+ return slots;
11154
+ }
11155
+ /**
11156
+ * Tell a player to load a track from `source` (another player's stick, or a
11157
+ * library served over the link).
11158
+ */
11159
+ loadTrack(number, trackId, source) {
11160
+ return __classPrivateFieldGet(this, _Remote_instances, "a", _Remote_control_get).loadTrack(this.player(number), trackId, source);
11161
+ }
11162
+ async leave() {
11163
+ __classPrivateFieldGet(this, _Remote_network, "f").disconnect();
11164
+ await __classPrivateFieldGet(this, _Remote_network, "f").close();
11165
+ }
11166
+ }
11167
+ exports.Remote = Remote;
11168
+ _Remote_network = new WeakMap(), _Remote_statuses = new WeakMap(), _Remote_cueOp = new WeakMap(), _Remote_instances = new WeakSet(), _Remote_status_get = function _Remote_status_get() {
11169
+ const emitter = __classPrivateFieldGet(this, _Remote_network, "f").statusEmitter;
11170
+ if (emitter === null) {
11171
+ throw new RemoteError('not connected to the link');
11172
+ }
11173
+ return emitter;
11174
+ }, _Remote_control_get = function _Remote_control_get() {
11175
+ const control = __classPrivateFieldGet(this, _Remote_network, "f").control;
11176
+ if (control === null) {
11177
+ throw new RemoteError('not connected to the link');
11178
+ }
11179
+ return control;
11180
+ }, _Remote_me_get = function _Remote_me_get() {
11181
+ const me = __classPrivateFieldGet(this, _Remote_network, "f").virtualDevice;
11182
+ if (me === null) {
11183
+ throw new RemoteError('not connected to the link');
11184
+ }
11185
+ return me;
11186
+ };
11187
+
11188
+
11189
+ /***/ },
11190
+
11191
+ /***/ "./src/remote/server.ts"
11192
+ /*!******************************!*\
11193
+ !*** ./src/remote/server.ts ***!
11194
+ \******************************/
11195
+ (__unused_webpack_module, exports, __webpack_require__) {
11196
+
11197
+ "use strict";
11198
+
11199
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11200
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11201
+ };
11202
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
11203
+ exports.createServer = createServer;
11204
+ /**
11205
+ * The REST API over a {@link Remote}. JSON in and out; every error is
11206
+ * `{"error": "…"}` with a status that says whose fault it was.
11207
+ *
11208
+ * GET / who we are, every device heard, the booth's power
11209
+ * GET /players the players with their last status
11210
+ * GET /players/{n}/media what is in the player's USB and SD slots
11211
+ * POST /players/{n}/play
11212
+ * POST /players/{n}/pause
11213
+ * POST /players/{n}/cue
11214
+ * POST /players/{n}/ff {"hold_ms": 500} hold fast-forward, then release
11215
+ * POST /players/{n}/rew {"hold_ms": 500}
11216
+ * POST /players/{n}/skip {"direction": "forward" | "backward"}
11217
+ * POST /players/{n}/op {"op": 26, "press": true} one raw transport packet
11218
+ * POST /players/{n}/load {"track_id": 1, "source": {"device": 2, "slot": "usb"}}
11219
+ * GET /booth the Hue group and whether it is on
11220
+ * PUT /booth {"on": true}
11221
+ * POST /booth/on
11222
+ * POST /booth/off
11223
+ * GET /hue/groups every room and zone the bridge has
11224
+ */
11225
+ const http_1 = __importDefault(__webpack_require__(/*! http */ "http"));
11226
+ const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
11227
+ const hue_1 = __webpack_require__(/*! ./hue */ "./src/remote/hue.ts");
11228
+ const index_1 = __webpack_require__(/*! ./index */ "./src/remote/index.ts");
11229
+ class HttpError extends Error {
11230
+ constructor(status, message) {
11231
+ super(message);
11232
+ this.status = status;
11233
+ }
11234
+ }
11235
+ const MAX_HOLD_MS = 30000;
11236
+ const DEFAULT_HOLD_MS = 500;
11237
+ const SLOTS = {
11238
+ rekordbox: types_1.MediaSlot.RB,
11239
+ rb: types_1.MediaSlot.RB,
11240
+ usb: types_1.MediaSlot.USB,
11241
+ sd: types_1.MediaSlot.SD,
11242
+ };
11243
+ function statusOf(e) {
11244
+ if (e instanceof HttpError) {
11245
+ return e.status;
11246
+ }
11247
+ if (e instanceof index_1.NoPlayerError || e instanceof hue_1.HueNoGroupError) {
11248
+ return 404;
11249
+ }
11250
+ if (e instanceof index_1.CueUnknownError || e instanceof hue_1.HueUnconfiguredError) {
11251
+ return 501;
11252
+ }
11253
+ return 502;
11254
+ }
11255
+ async function readJson(req) {
11256
+ const chunks = [];
11257
+ for await (const chunk of req) {
11258
+ chunks.push(chunk);
11259
+ }
11260
+ const text = Buffer.concat(chunks).toString('utf8').trim();
11261
+ if (text === '') {
11262
+ return {};
11263
+ }
11264
+ try {
11265
+ return JSON.parse(text);
11266
+ }
11267
+ catch (e) {
11268
+ throw new HttpError(400, `body is not JSON: ${e.message}`);
11269
+ }
11270
+ }
11271
+ function integer(value, name, min = 0, max = Number.MAX_SAFE_INTEGER) {
11272
+ if (typeof value !== 'number' ||
11273
+ !Number.isInteger(value) ||
11274
+ value < min ||
11275
+ value > max) {
11276
+ throw new HttpError(400, `${name} must be an integer in ${min}..${max}`);
11277
+ }
11278
+ return value;
11279
+ }
11280
+ function holdMs(body) {
11281
+ if (body.hold_ms === undefined) {
11282
+ return DEFAULT_HOLD_MS;
11283
+ }
11284
+ return integer(body.hold_ms, 'hold_ms', 0, MAX_HOLD_MS);
11285
+ }
11286
+ function createServer({ remote, hue, boothGroup, logger }) {
11287
+ const needHue = () => {
11288
+ if (hue === null) {
11289
+ throw new hue_1.HueUnconfiguredError();
11290
+ }
11291
+ return hue;
11292
+ };
11293
+ const boothState = async () => {
11294
+ const group = await needHue().groupNamed(boothGroup);
11295
+ return {
11296
+ group: group.name,
11297
+ id: group.id,
11298
+ type: group.type,
11299
+ on: group.anyOn,
11300
+ all_on: group.allOn,
11301
+ lights: group.lights,
11302
+ };
11303
+ };
11304
+ const setBooth = async (on) => {
11305
+ const bridge = needHue();
11306
+ const group = await bridge.groupNamed(boothGroup);
11307
+ await bridge.setGroupOn(group.id, on);
11308
+ logger === null || logger === void 0 ? void 0 : logger.info(`booth "${group.name}" switched ${on ? 'on' : 'off'}`);
11309
+ return { group: group.name, on };
11310
+ };
11311
+ const ok = { ok: true };
11312
+ const route = async (method, path, body) => {
11313
+ var _a, _b;
11314
+ const parts = path.split('/').filter(p => p !== '');
11315
+ if (parts.length === 0 && method === 'GET') {
11316
+ const me = remote.me;
11317
+ const booth = await boothState().catch(e => ({
11318
+ group: boothGroup,
11319
+ error: e.message,
11320
+ }));
11321
+ return {
11322
+ stagehand: me && { name: me.name, number: me.id, ip: me.ip.address },
11323
+ devices: remote.devices.map(d => ({
11324
+ number: d.id,
11325
+ name: d.name,
11326
+ type: d.type,
11327
+ ip: d.ip.address,
11328
+ })),
11329
+ players: remote.players,
11330
+ booth,
11331
+ };
11332
+ }
11333
+ if (parts[0] === 'players') {
11334
+ if (parts.length === 1 && method === 'GET') {
11335
+ return remote.players;
11336
+ }
11337
+ const n = parseInt(parts[1], 10);
11338
+ if (!Number.isInteger(n)) {
11339
+ throw new HttpError(404, `no route ${method} ${path}`);
11340
+ }
11341
+ const action = parts[2];
11342
+ if (parts.length !== 3) {
11343
+ throw new HttpError(404, `no route ${method} ${path}`);
11344
+ }
11345
+ if (method === 'GET' && action === 'media') {
11346
+ return remote.media(n);
11347
+ }
11348
+ if (method !== 'POST') {
11349
+ throw new HttpError(405, `${method} not allowed on ${path}`);
11350
+ }
11351
+ switch (action) {
11352
+ case 'play':
11353
+ await remote.play(n);
11354
+ return ok;
11355
+ case 'pause':
11356
+ await remote.pause(n);
11357
+ return ok;
11358
+ case 'cue':
11359
+ await remote.cue(n);
11360
+ return ok;
11361
+ case 'ff':
11362
+ await remote.fastForward(n, holdMs(await body()));
11363
+ return ok;
11364
+ case 'rew':
11365
+ await remote.rewind(n, holdMs(await body()));
11366
+ return ok;
11367
+ case 'skip': {
11368
+ const { direction } = await body();
11369
+ if (direction === 'forward') {
11370
+ await remote.skipForward(n);
11371
+ }
11372
+ else if (direction === 'backward') {
11373
+ await remote.skipBackward(n);
11374
+ }
11375
+ else {
11376
+ throw new HttpError(400, 'direction must be "forward" or "backward"');
11377
+ }
11378
+ return ok;
11379
+ }
11380
+ case 'op': {
11381
+ const b = await body();
11382
+ await remote.transport(n, integer(b.op, 'op', 0, 255), b.press !== false);
11383
+ return ok;
11384
+ }
11385
+ case 'load': {
11386
+ const b = await body();
11387
+ const trackId = integer(b.track_id, 'track_id', 1, 0xffffffff);
11388
+ const source = (_a = b.source) !== null && _a !== void 0 ? _a : {};
11389
+ const device = integer(source.device, 'source.device', 1, 255);
11390
+ const slotName = String((_b = source.slot) !== null && _b !== void 0 ? _b : 'rekordbox').toLowerCase();
11391
+ const slot = SLOTS[slotName];
11392
+ if (slot === undefined) {
11393
+ throw new HttpError(400, 'source.slot must be "rekordbox", "usb" or "sd"');
11394
+ }
11395
+ await remote.loadTrack(n, trackId, { deviceId: device, slot });
11396
+ return ok;
11397
+ }
11398
+ default:
11399
+ throw new HttpError(404, `no route ${method} ${path}`);
11400
+ }
11401
+ }
11402
+ if (parts[0] === 'booth') {
11403
+ if (parts.length === 1) {
11404
+ if (method === 'GET') {
11405
+ return boothState();
11406
+ }
11407
+ if (method === 'PUT') {
11408
+ const { on } = await body();
11409
+ if (typeof on !== 'boolean') {
11410
+ throw new HttpError(400, 'on must be true or false');
11411
+ }
11412
+ return setBooth(on);
11413
+ }
11414
+ }
11415
+ if (parts.length === 2 && method === 'POST') {
11416
+ if (parts[1] === 'on') {
11417
+ return setBooth(true);
11418
+ }
11419
+ if (parts[1] === 'off') {
11420
+ return setBooth(false);
11421
+ }
11422
+ }
11423
+ }
11424
+ if (parts[0] === 'hue' &&
11425
+ parts[1] === 'groups' &&
11426
+ parts.length === 2 &&
11427
+ method === 'GET') {
11428
+ return needHue().groups();
11429
+ }
11430
+ throw new HttpError(404, `no route ${method} ${path}`);
11431
+ };
11432
+ return http_1.default.createServer(async (req, res) => {
11433
+ var _a, _b;
11434
+ const method = (_a = req.method) !== null && _a !== void 0 ? _a : 'GET';
11435
+ const path = ((_b = req.url) !== null && _b !== void 0 ? _b : '/').split('?')[0];
11436
+ const send = (status, payload) => {
11437
+ res.writeHead(status, { 'content-type': 'application/json' });
11438
+ res.end(`${JSON.stringify(payload)}\n`);
11439
+ };
11440
+ try {
11441
+ send(200, await route(method, path, () => readJson(req)));
11442
+ }
11443
+ catch (e) {
11444
+ const status = statusOf(e);
11445
+ const message = e instanceof Error ? e.message : String(e);
11446
+ if (status >= 500) {
11447
+ logger === null || logger === void 0 ? void 0 : logger.warn(`${method} ${path}: ${message}`);
11448
+ }
11449
+ send(status, { error: message });
11450
+ }
11451
+ });
11452
+ }
11453
+
11454
+
10554
11455
  /***/ },
10555
11456
 
10556
11457
  /***/ "./src/remotedb/constants.ts"
@@ -14468,6 +15369,17 @@ module.exports = require("fs");
14468
15369
 
14469
15370
  /***/ },
14470
15371
 
15372
+ /***/ "http"
15373
+ /*!***********************!*\
15374
+ !*** external "http" ***!
15375
+ \***********************/
15376
+ (module) {
15377
+
15378
+ "use strict";
15379
+ module.exports = require("http");
15380
+
15381
+ /***/ },
15382
+
14471
15383
  /***/ "ip-address"
14472
15384
  /*!*****************************!*\
14473
15385
  !*** external "ip-address" ***!