alphatheta-connect 0.15.0 → 0.18.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.
Files changed (44) hide show
  1. package/README.md +29 -3
  2. package/lib/artwork/index.d.ts +2 -1
  3. package/lib/cli.js +1122 -1332
  4. package/lib/cli.js.map +1 -1
  5. package/lib/constants.d.ts +7 -1
  6. package/lib/db/getArtworkFromFile.d.ts +2 -0
  7. package/lib/db/getFile.d.ts +7 -2
  8. package/lib/db/getMetadata.d.ts +3 -2
  9. package/lib/db/getTrackAnalysis.d.ts +28 -0
  10. package/lib/db/index.d.ts +8 -8
  11. package/lib/entities.d.ts +13 -111
  12. package/lib/index.d.ts +10 -2
  13. package/lib/index.js +1356 -1352
  14. package/lib/index.js.map +1 -1
  15. package/lib/localdb/database-adapter.d.ts +2 -47
  16. package/lib/localdb/index.d.ts +1 -1
  17. package/lib/localdb/onelibrary/adapter.d.ts +110 -0
  18. package/lib/localdb/onelibrary/connection.d.ts +8 -0
  19. package/lib/localdb/onelibrary/encryption.d.ts +10 -0
  20. package/lib/localdb/onelibrary/index.d.ts +5 -0
  21. package/lib/localdb/onelibrary/types.d.ts +67 -0
  22. package/lib/localdb/onelibrary.d.ts +3 -181
  23. package/lib/localdb/rekordbox/anlz-parsers.d.ts +37 -0
  24. package/lib/localdb/rekordbox/entity-creators.d.ts +27 -0
  25. package/lib/localdb/rekordbox/hydrator.d.ts +20 -0
  26. package/lib/localdb/rekordbox/index.d.ts +13 -0
  27. package/lib/localdb/rekordbox/table-mappings.d.ts +10 -0
  28. package/lib/localdb/rekordbox/types.d.ts +118 -0
  29. package/lib/localdb/rekordbox.d.ts +6 -110
  30. package/lib/logger.d.ts +13 -0
  31. package/lib/metadata.d.ts +45 -0
  32. package/lib/network.d.ts +25 -0
  33. package/lib/nfs/index.d.ts +35 -10
  34. package/lib/nfs/index.test.d.ts +1 -0
  35. package/lib/nfs/programs.d.ts +10 -1
  36. package/lib/passive/index.d.ts +1 -1
  37. package/lib/passive/remotedb.d.ts +12 -1
  38. package/lib/remotedb/queries.d.ts +2 -2
  39. package/lib/types.d.ts +55 -78
  40. package/lib/types.js +21 -38
  41. package/lib/types.js.map +1 -1
  42. package/lib/utils/index.d.ts +9 -0
  43. package/lib/virtualcdj/index.d.ts +7 -1
  44. package/package.json +23 -2
package/lib/index.js CHANGED
@@ -14,6 +14,7 @@ exports.PictureType = exports.createNfsFileReaderWithInfo = exports.createNfsFil
14
14
  exports.isArtworkExtractionSupported = isArtworkExtractionSupported;
15
15
  exports.extractArtwork = extractArtwork;
16
16
  exports.extractArtworkFromDevice = extractArtworkFromDevice;
17
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
17
18
  const nfs_1 = __webpack_require__(/*! src/nfs */ "./src/nfs/index.ts");
18
19
  const parsers_1 = __webpack_require__(/*! ./parsers */ "./src/artwork/parsers/index.ts");
19
20
  const reader_1 = __webpack_require__(/*! ./reader */ "./src/artwork/reader.ts");
@@ -50,10 +51,14 @@ async function extractArtwork(reader) {
50
51
  }
51
52
  }
52
53
  }
53
- async function extractArtworkFromDevice(device, slot, filePath) {
54
+ async function extractArtworkFromDevice(device, slot, filePath, logger = logger_1.noopLogger) {
55
+ logger.debug(`[artwork-nfs] getFileInfo: device=${device.ip.address}, slot=${slot}, path=${filePath}`);
54
56
  const fileInfo = await (0, nfs_1.getFileInfo)({ device, slot, path: filePath });
57
+ logger.debug(`[artwork-nfs] File found: ${fileInfo.size} bytes`);
55
58
  const reader = (0, reader_1.createNfsFileReader)(device, slot, filePath, fileInfo.size);
56
- return extractArtwork(reader);
59
+ const result = await extractArtwork(reader);
60
+ logger.debug(`[artwork-nfs] extractArtwork result: ${result ? `${result.mimeType} (${result.data.length}b)` : 'null'}`);
61
+ return result;
57
62
  }
58
63
 
59
64
 
@@ -563,7 +568,7 @@ var PictureType;
563
568
  "use strict";
564
569
 
565
570
  Object.defineProperty(exports, "__esModule", ({ value: true }));
566
- exports.VIRTUAL_CDJ_FIRMWARE = exports.VIRTUAL_CDJ_NAME = exports.PROLINK_HEADER = exports.STARTUP_STAGE_INTERVAL = exports.ANNOUNCE_INTERVAL = exports.STATUS_PORT = exports.BEAT_PORT = exports.ANNOUNCE_PORT = exports.DEFAULT_VCDJ_ID = void 0;
571
+ exports.MAX_CDJ_DEVICE_ID = exports.MIN_CDJ_DEVICE_ID = exports.VIRTUAL_CDJ_FIRMWARE = exports.VIRTUAL_CDJ_NAME = exports.PROLINK_HEADER = exports.STARTUP_STAGE_INTERVAL = exports.ANNOUNCE_INTERVAL = exports.STATUS_PORT = exports.BEAT_PORT = exports.ANNOUNCE_PORT = exports.DEFAULT_VCDJ_ID = void 0;
567
572
  /**
568
573
  * The default virtual CDJ ID to use.
569
574
  *
@@ -604,7 +609,13 @@ exports.VIRTUAL_CDJ_NAME = 'ProLink-Connect';
604
609
  * VirtualCDJFirmware is a string indicating the firmware version reported with
605
610
  * status packets.
606
611
  */
607
- exports.VIRTUAL_CDJ_FIRMWARE = '1.43';
612
+ exports.VIRTUAL_CDJ_FIRMWARE = '3.20';
613
+ /**
614
+ * CDJs use device IDs 1-6. Devices outside this range (e.g. Stagehand at 154)
615
+ * should not be queried for media slots or databases as they may crash.
616
+ */
617
+ exports.MIN_CDJ_DEVICE_ID = 1;
618
+ exports.MAX_CDJ_DEVICE_ID = 6;
608
619
 
609
620
 
610
621
  /***/ },
@@ -722,37 +733,46 @@ var __importStar = (this && this.__importStar) || (function () {
722
733
  Object.defineProperty(exports, "__esModule", ({ value: true }));
723
734
  exports.viaFileExtraction = viaFileExtraction;
724
735
  const artwork_1 = __webpack_require__(/*! src/artwork */ "./src/artwork/index.ts");
736
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
725
737
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
738
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
726
739
  const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
727
740
  /**
728
741
  * Extract artwork directly from an audio file via NFS.
729
742
  */
730
743
  async function viaFileExtraction(device, opts) {
731
- var _a, _b;
744
+ var _a, _b, _c;
732
745
  const { trackSlot, track, span } = opts;
746
+ const logger = (_a = opts.logger) !== null && _a !== void 0 ? _a : logger_1.noopLogger;
733
747
  if (trackSlot !== types_1.MediaSlot.USB &&
734
748
  trackSlot !== types_1.MediaSlot.SD &&
735
749
  trackSlot !== types_1.MediaSlot.RB) {
750
+ logger.debug(`[artwork-nfs] Skipping: unsupported slot ${(0, utils_1.getSlotName)(trackSlot)} (device ${device.name})`);
736
751
  return null;
737
752
  }
738
753
  const slot = trackSlot;
739
754
  if (!track.filePath) {
755
+ logger.debug('[artwork-nfs] Skipping: no filePath on track');
740
756
  return null;
741
757
  }
742
- const extension = (_b = (_a = track.filePath.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
758
+ const extension = (_c = (_b = track.filePath.split('.').pop()) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : '';
743
759
  if (!(0, artwork_1.isArtworkExtractionSupported)(extension)) {
760
+ logger.debug(`[artwork-nfs] Skipping: unsupported extension ".${extension}" (${track.filePath})`);
744
761
  return null;
745
762
  }
763
+ logger.debug(`[artwork-nfs] Extracting from ${track.filePath} (slot=${(0, utils_1.getSlotName)(trackSlot)}, device=${device.name} @ ${device.ip.address})`);
746
764
  const tx = span
747
765
  ? span.startChild({ op: 'getArtworkFromFile' })
748
766
  : Telemetry.startTransaction({ name: 'getArtworkFromFile' });
749
767
  try {
750
- const artwork = await (0, artwork_1.extractArtworkFromDevice)(device, slot, track.filePath);
768
+ const artwork = await (0, artwork_1.extractArtworkFromDevice)(device, slot, track.filePath, logger);
751
769
  if (!artwork) {
770
+ logger.debug('[artwork-nfs] No embedded artwork found in file');
752
771
  tx.setData('result', 'no_artwork');
753
772
  tx.finish();
754
773
  return null;
755
774
  }
775
+ logger.debug(`[artwork-nfs] Success: ${artwork.mimeType} (${artwork.data.length} bytes)`);
756
776
  tx.setData('result', 'success');
757
777
  tx.setData('mimeType', artwork.mimeType);
758
778
  tx.setData('size', artwork.data.length);
@@ -760,6 +780,8 @@ async function viaFileExtraction(device, opts) {
760
780
  return artwork.data;
761
781
  }
762
782
  catch (error) {
783
+ const msg = error instanceof Error ? error.message : String(error);
784
+ logger.warn(`[artwork-nfs] NFS extraction failed: ${msg}`);
763
785
  tx.setData('result', 'error');
764
786
  tx.finish();
765
787
  Telemetry.captureException(error);
@@ -907,12 +929,15 @@ var __importStar = (this && this.__importStar) || (function () {
907
929
  Object.defineProperty(exports, "__esModule", ({ value: true }));
908
930
  exports.viaRemote = viaRemote;
909
931
  exports.viaLocal = viaLocal;
932
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
910
933
  const nfs_1 = __webpack_require__(/*! src/nfs */ "./src/nfs/index.ts");
911
934
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
912
935
  const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
913
936
  const CHUNK_SIZE = 8192; // Maximum allowed XDR read size
914
937
  function viaRemote(_remote, _device, _opts) {
915
- console.error('Getting a file from Rekordbox via ProDJ-Link is not yet supported.');
938
+ var _a;
939
+ const logger = (_a = _opts.logger) !== null && _a !== void 0 ? _a : logger_1.noopLogger;
940
+ logger.error('Getting a file from Rekordbox via ProDJ-Link is not yet supported.');
916
941
  return null;
917
942
  // const conn = await remote.get(deviceId);
918
943
  // if (conn === null) {
@@ -934,7 +959,9 @@ function viaRemote(_remote, _device, _opts) {
934
959
  // });
935
960
  }
936
961
  async function viaLocal(local, device, opts) {
962
+ var _a;
937
963
  const { deviceId, trackSlot, track } = opts;
964
+ const logger = (_a = opts.logger) !== null && _a !== void 0 ? _a : logger_1.noopLogger;
938
965
  if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
939
966
  throw new Error('Expected USB or SD or RB slot for remote database query');
940
967
  }
@@ -948,7 +975,7 @@ async function viaLocal(local, device, opts) {
948
975
  slot: trackSlot,
949
976
  path: track.filePath,
950
977
  onProgress: progress => {
951
- console.log(progress.read, progress.total);
978
+ logger.trace('%d %d', progress.read, progress.total);
952
979
  },
953
980
  chunkSize: CHUNK_SIZE,
954
981
  });
@@ -988,24 +1015,41 @@ async function viaRemote(remote, opts) {
988
1015
  trackType,
989
1016
  menuTarget: remotedb_1.MenuTarget.Main,
990
1017
  };
1018
+ const isUnanalyzed = trackType === types_1.TrackType.Unanalyzed || trackType === types_1.TrackType.AudioCD;
1019
+ const isStreaming = trackType === types_1.TrackType.Streaming;
1020
+ const skipLocalFileLookups = isUnanalyzed || isStreaming;
1021
+ // Unanalyzed tracks use GetGenericMetadata (reads ID3 tags from the audio file).
1022
+ // Streaming tracks (Beatport) use the regular GetMetadata query.
991
1023
  const track = await conn.query({
992
1024
  queryDescriptor,
993
- query: remotedb_1.Query.GetMetadata,
994
- args: { trackId },
995
- span,
996
- });
997
- track.filePath = await conn.query({
998
- queryDescriptor,
999
- query: remotedb_1.Query.GetTrackInfo,
1000
- args: { trackId },
1001
- span,
1002
- });
1003
- track.beatGrid = await conn.query({
1004
- queryDescriptor,
1005
- query: remotedb_1.Query.GetBeatGrid,
1025
+ query: isUnanalyzed ? remotedb_1.Query.GetGenericMetadata : remotedb_1.Query.GetMetadata,
1006
1026
  args: { trackId },
1007
1027
  span,
1008
1028
  });
1029
+ // Try to get file path — for streaming tracks this returns the Beatport track ID
1030
+ // (e.g. "/26883657.m4a") which we use for Beatport API lookups
1031
+ try {
1032
+ track.filePath = await conn.query({
1033
+ queryDescriptor,
1034
+ query: remotedb_1.Query.GetTrackInfo,
1035
+ args: { trackId },
1036
+ span,
1037
+ });
1038
+ }
1039
+ catch (err) {
1040
+ if (!skipLocalFileLookups) {
1041
+ throw err;
1042
+ }
1043
+ }
1044
+ // Beat grid is only available for analyzed local tracks
1045
+ if (!skipLocalFileLookups) {
1046
+ track.beatGrid = await conn.query({
1047
+ queryDescriptor,
1048
+ query: remotedb_1.Query.GetBeatGrid,
1049
+ args: { trackId },
1050
+ span,
1051
+ });
1052
+ }
1009
1053
  return track;
1010
1054
  }
1011
1055
  async function viaLocal(local, device, opts) {
@@ -1017,13 +1061,16 @@ async function viaLocal(local, device, opts) {
1017
1061
  if (adapter === null) {
1018
1062
  return null;
1019
1063
  }
1020
- const track = adapter.findTrack(trackId);
1021
- if (track === null) {
1064
+ const dbTrack = adapter.findTrack(trackId);
1065
+ if (dbTrack === null) {
1022
1066
  return null;
1023
1067
  }
1024
- const anlz = await (0, rekordbox_1.loadAnlz)(track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1025
- track.beatGrid = anlz.beatGrid;
1026
- track.cueAndLoops = anlz.cueAndLoops;
1068
+ const anlz = await (0, rekordbox_1.loadAnlz)(dbTrack, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1069
+ const track = {
1070
+ ...dbTrack,
1071
+ beatGrid: anlz.beatGrid,
1072
+ waveformHd: null,
1073
+ };
1027
1074
  return track;
1028
1075
  }
1029
1076
 
@@ -1104,6 +1151,48 @@ async function viaLocal(local, opts) {
1104
1151
  }
1105
1152
 
1106
1153
 
1154
+ /***/ },
1155
+
1156
+ /***/ "./src/db/getTrackAnalysis.ts"
1157
+ /*!************************************!*\
1158
+ !*** ./src/db/getTrackAnalysis.ts ***!
1159
+ \************************************/
1160
+ (__unused_webpack_module, exports, __webpack_require__) {
1161
+
1162
+ "use strict";
1163
+
1164
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1165
+ exports.viaLocal = viaLocal;
1166
+ const rekordbox_1 = __webpack_require__(/*! src/localdb/rekordbox */ "./src/localdb/rekordbox.ts");
1167
+ const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
1168
+ const utils_1 = __webpack_require__(/*! ./utils */ "./src/db/utils.ts");
1169
+ async function viaLocal(local, device, opts) {
1170
+ var _a, _b, _c, _d;
1171
+ const { deviceId, trackSlot, track } = opts;
1172
+ if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
1173
+ throw new Error('Expected USB or SD slot for local database query');
1174
+ }
1175
+ const conn = await local.get(deviceId, trackSlot);
1176
+ if (conn === null) {
1177
+ return null;
1178
+ }
1179
+ const resolver = (0, utils_1.anlzLoader)({ device, slot: trackSlot });
1180
+ const [extResult, twoxResult] = await Promise.all([
1181
+ (0, rekordbox_1.loadAnlz)(track, 'EXT', resolver),
1182
+ (0, rekordbox_1.loadAnlz)(track, '2EX', resolver).catch(() => null),
1183
+ ]);
1184
+ return {
1185
+ extendedCues: extResult.extendedCues,
1186
+ songStructure: extResult.songStructure,
1187
+ waveformColorPreview: (_a = extResult.waveformColorPreview) !== null && _a !== void 0 ? _a : undefined,
1188
+ waveformHd: extResult.waveformHd,
1189
+ waveform3BandPreview: (_b = twoxResult === null || twoxResult === void 0 ? void 0 : twoxResult.waveform3BandPreview) !== null && _b !== void 0 ? _b : null,
1190
+ waveform3BandDetail: (_c = twoxResult === null || twoxResult === void 0 ? void 0 : twoxResult.waveform3BandDetail) !== null && _c !== void 0 ? _c : null,
1191
+ vocalConfig: (_d = twoxResult === null || twoxResult === void 0 ? void 0 : twoxResult.vocalConfig) !== null && _d !== void 0 ? _d : null,
1192
+ };
1193
+ }
1194
+
1195
+
1107
1196
  /***/ },
1108
1197
 
1109
1198
  /***/ "./src/db/getWaveforms.ts"
@@ -1138,9 +1227,30 @@ async function viaRemote(remote, opts) {
1138
1227
  args: { trackId: track.id },
1139
1228
  span,
1140
1229
  });
1141
- return { waveformHd };
1230
+ const isStreaming = trackType === types_1.TrackType.Streaming;
1231
+ if (!isStreaming) {
1232
+ return { waveformHd };
1233
+ }
1234
+ // Streaming tracks (e.g. Beatport LINK) have no local ANLZ file but the CDJ
1235
+ // serves waveform preview and detailed via remotedb.
1236
+ const [waveformPreview, waveformDetailed] = await Promise.all([
1237
+ conn.query({
1238
+ queryDescriptor,
1239
+ query: remotedb_1.Query.GetWaveformPreview,
1240
+ args: { trackId: track.id },
1241
+ span,
1242
+ }),
1243
+ conn.query({
1244
+ queryDescriptor,
1245
+ query: remotedb_1.Query.GetWaveformDetailed,
1246
+ args: { trackId: track.id },
1247
+ span,
1248
+ }),
1249
+ ]);
1250
+ return { waveformHd, waveformPreview, waveformDetailed };
1142
1251
  }
1143
1252
  async function viaLocal(local, device, opts) {
1253
+ var _a;
1144
1254
  const { deviceId, trackSlot, track } = opts;
1145
1255
  if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
1146
1256
  throw new Error('Expected USB or SD slot for remote database query');
@@ -1150,7 +1260,10 @@ async function viaLocal(local, device, opts) {
1150
1260
  return null;
1151
1261
  }
1152
1262
  const anlz = await (0, rekordbox_1.loadAnlz)(track, 'EXT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1153
- return { waveformHd: anlz.waveformHd };
1263
+ return {
1264
+ waveformHd: anlz.waveformHd,
1265
+ waveformColorPreview: (_a = anlz.waveformColorPreview) !== null && _a !== void 0 ? _a : undefined,
1266
+ };
1154
1267
  }
1155
1268
 
1156
1269
 
@@ -1208,7 +1321,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
1208
1321
  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");
1209
1322
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1210
1323
  };
1211
- var _Database_hostDevice, _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy;
1324
+ var _Database_deviceManager, _Database_localDatabase, _Database_remoteDatabase, _Database_getTrackLookupStrategy, _Database_getMediaLookupStrategy;
1212
1325
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1213
1326
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
1214
1327
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
@@ -1219,6 +1332,7 @@ const GetArtworkThumbnail = __importStar(__webpack_require__(/*! ./getArtworkThu
1219
1332
  const GetFile = __importStar(__webpack_require__(/*! ./getFile */ "./src/db/getFile.ts"));
1220
1333
  const GetMetadata = __importStar(__webpack_require__(/*! ./getMetadata */ "./src/db/getMetadata.ts"));
1221
1334
  const GetPlaylist = __importStar(__webpack_require__(/*! ./getPlaylist */ "./src/db/getPlaylist.ts"));
1335
+ const GetTrackAnalysis = __importStar(__webpack_require__(/*! ./getTrackAnalysis */ "./src/db/getTrackAnalysis.ts"));
1222
1336
  const GetWaveforms = __importStar(__webpack_require__(/*! ./getWaveforms */ "./src/db/getWaveforms.ts"));
1223
1337
  var LookupStrategy;
1224
1338
  (function (LookupStrategy) {
@@ -1231,8 +1345,7 @@ var LookupStrategy;
1231
1345
  * network for information from their databases.
1232
1346
  */
1233
1347
  class Database {
1234
- constructor(hostDevice, local, remote, deviceManager) {
1235
- _Database_hostDevice.set(this, void 0);
1348
+ constructor(local, remote, deviceManager) {
1236
1349
  _Database_deviceManager.set(this, void 0);
1237
1350
  /**
1238
1351
  * The local database service, used when querying media devices connected
@@ -1246,8 +1359,13 @@ class Database {
1246
1359
  _Database_remoteDatabase.set(this, void 0);
1247
1360
  _Database_getTrackLookupStrategy.set(this, (device, type) => {
1248
1361
  const isUnanalyzed = type === types_1.TrackType.AudioCD || type === types_1.TrackType.Unanalyzed;
1249
- const requiresCdjRemote = device.type === types_1.DeviceType.CDJ && isUnanalyzed && this.cdjSupportsRemotedb;
1250
- return device.type === types_1.DeviceType.Rekordbox || requiresCdjRemote
1362
+ const isStreaming = type === types_1.TrackType.Streaming;
1363
+ // Unanalyzed and streaming tracks on CDJs must use RemoteDB
1364
+ // (streaming services like Beatport have no local database)
1365
+ if (device.type === types_1.DeviceType.CDJ && (isUnanalyzed || isStreaming)) {
1366
+ return LookupStrategy.Remote;
1367
+ }
1368
+ return device.type === types_1.DeviceType.Rekordbox
1251
1369
  ? LookupStrategy.Remote
1252
1370
  : device.type === types_1.DeviceType.CDJ && type === types_1.TrackType.RB
1253
1371
  ? LookupStrategy.Local
@@ -1258,19 +1376,10 @@ class Database {
1258
1376
  : device.type === types_1.DeviceType.Rekordbox
1259
1377
  ? LookupStrategy.NoneAvailable
1260
1378
  : LookupStrategy.Local);
1261
- __classPrivateFieldSet(this, _Database_hostDevice, hostDevice, "f");
1262
1379
  __classPrivateFieldSet(this, _Database_localDatabase, local, "f");
1263
1380
  __classPrivateFieldSet(this, _Database_remoteDatabase, remote, "f");
1264
1381
  __classPrivateFieldSet(this, _Database_deviceManager, deviceManager, "f");
1265
1382
  }
1266
- /**
1267
- * Reports weather or not the CDJs can be communicated to over the remote
1268
- * database protocol. This is important when trying to query for unanalyzed or
1269
- * compact disc tracks.
1270
- */
1271
- get cdjSupportsRemotedb() {
1272
- return __classPrivateFieldGet(this, _Database_hostDevice, "f").id > 0 && __classPrivateFieldGet(this, _Database_hostDevice, "f").id < 7;
1273
- }
1274
1383
  /**
1275
1384
  * Get the database type (oneLibrary or pdb) for a loaded device slot.
1276
1385
  * Returns null if the slot uses remote database or no database is loaded.
@@ -1431,6 +1540,35 @@ class Database {
1431
1540
  tx.finish();
1432
1541
  return waveforms;
1433
1542
  }
1543
+ /**
1544
+ * Retrieves all analysis data from the EXT file for a track.
1545
+ * Returns extended cues, song structure, waveform color preview, and HD waveform.
1546
+ */
1547
+ async getTrackAnalysis(opts) {
1548
+ const { deviceId, trackType, trackSlot, span } = opts;
1549
+ const tx = span
1550
+ ? span.startChild({ op: 'dbGetTrackAnalysis' })
1551
+ : Telemetry.startTransaction({ name: 'dbGetTrackAnalysis' });
1552
+ tx.setTag('deviceId', deviceId.toString());
1553
+ tx.setTag('trackType', (0, utils_1.getTrackTypeName)(trackType));
1554
+ tx.setTag('trackSlot', (0, utils_1.getSlotName)(trackSlot));
1555
+ const callOpts = { ...opts, span: tx };
1556
+ const device = await __classPrivateFieldGet(this, _Database_deviceManager, "f").getDeviceEnsured(deviceId);
1557
+ if (device === null) {
1558
+ tx.finish();
1559
+ return null;
1560
+ }
1561
+ const strategy = __classPrivateFieldGet(this, _Database_getTrackLookupStrategy, "f").call(this, device, trackType);
1562
+ let analysis = null;
1563
+ if (strategy === LookupStrategy.Local) {
1564
+ analysis = await GetTrackAnalysis.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
1565
+ }
1566
+ if (strategy === LookupStrategy.NoneAvailable || strategy === LookupStrategy.Remote) {
1567
+ tx.setStatus(telemetry_1.SpanStatus.Unavailable);
1568
+ }
1569
+ tx.finish();
1570
+ return analysis;
1571
+ }
1434
1572
  /**
1435
1573
  * Retrieve folders, playlists, and tracks within the playlist tree. The id
1436
1574
  * may be left undefined to query the root of the playlist tree.
@@ -1465,7 +1603,7 @@ class Database {
1465
1603
  return contents;
1466
1604
  }
1467
1605
  }
1468
- _Database_hostDevice = new WeakMap(), _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap(), _Database_remoteDatabase = new WeakMap(), _Database_getTrackLookupStrategy = new WeakMap(), _Database_getMediaLookupStrategy = new WeakMap();
1606
+ _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap(), _Database_remoteDatabase = new WeakMap(), _Database_getTrackLookupStrategy = new WeakMap(), _Database_getMediaLookupStrategy = new WeakMap();
1469
1607
  exports["default"] = Database;
1470
1608
 
1471
1609
 
@@ -1696,21 +1834,18 @@ function deviceFromPacket(packet) {
1696
1834
  /*!*************************!*\
1697
1835
  !*** ./src/entities.ts ***!
1698
1836
  \*************************/
1699
- (__unused_webpack_module, exports) {
1837
+ (__unused_webpack_module, exports, __webpack_require__) {
1700
1838
 
1701
1839
  "use strict";
1702
1840
 
1703
1841
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1704
1842
  exports.EntityFK = void 0;
1705
1843
  /**
1706
- * Documentation type strictly for use with entities that have foreign key
1707
- * attributes.
1844
+ * Re-export entity types from onelibrary-connect.
1845
+ * These types represent what is stored in the rekordbox database.
1708
1846
  */
1709
- var EntityFK;
1710
- (function (EntityFK) {
1711
- EntityFK[EntityFK["WithFKs"] = 0] = "WithFKs";
1712
- EntityFK[EntityFK["WithRelations"] = 1] = "WithRelations";
1713
- })(EntityFK || (exports.EntityFK = EntityFK = {}));
1847
+ var onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
1848
+ Object.defineProperty(exports, "EntityFK", ({ enumerable: true, get: function () { return onelibrary_connect_1.EntityFK; } }));
1714
1849
 
1715
1850
 
1716
1851
  /***/ },
@@ -1741,7 +1876,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
1741
1876
  return (mod && mod.__esModule) ? mod : { "default": mod };
1742
1877
  };
1743
1878
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1744
- exports.OneLibraryAdapter = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = void 0;
1879
+ exports.noopLogger = exports.OneLibraryAdapter = exports.fetchFile = exports.loadAnlz = exports.isMetadataExtractionSupported = exports.extractMetadataFromDevice = exports.extractFullMetadata = exports.PictureType = exports.isArtworkExtractionSupported = exports.extractArtworkFromDevice = exports.extractArtwork = exports.PositionEmitter = void 0;
1745
1880
  __exportStar(__webpack_require__(/*! ./entities */ "./src/entities.ts"), exports);
1746
1881
  __exportStar(__webpack_require__(/*! ./mixstatus */ "./src/mixstatus/index.ts"), exports);
1747
1882
  __exportStar(__webpack_require__(/*! ./network */ "./src/network.ts"), exports);
@@ -1755,8 +1890,21 @@ Object.defineProperty(exports, "extractArtwork", ({ enumerable: true, get: funct
1755
1890
  Object.defineProperty(exports, "extractArtworkFromDevice", ({ enumerable: true, get: function () { return artwork_1.extractArtworkFromDevice; } }));
1756
1891
  Object.defineProperty(exports, "isArtworkExtractionSupported", ({ enumerable: true, get: function () { return artwork_1.isArtworkExtractionSupported; } }));
1757
1892
  Object.defineProperty(exports, "PictureType", ({ enumerable: true, get: function () { return artwork_1.PictureType; } }));
1758
- var onelibrary_1 = __webpack_require__(/*! ./localdb/onelibrary */ "./src/localdb/onelibrary.ts");
1759
- Object.defineProperty(exports, "OneLibraryAdapter", ({ enumerable: true, get: function () { return onelibrary_1.OneLibraryAdapter; } }));
1893
+ // Full metadata extraction (title, artist, album, BPM, key, genre, artwork)
1894
+ var metadata_1 = __webpack_require__(/*! ./metadata */ "./src/metadata.ts");
1895
+ Object.defineProperty(exports, "extractFullMetadata", ({ enumerable: true, get: function () { return metadata_1.extractFullMetadata; } }));
1896
+ Object.defineProperty(exports, "extractMetadataFromDevice", ({ enumerable: true, get: function () { return metadata_1.extractMetadataFromDevice; } }));
1897
+ Object.defineProperty(exports, "isMetadataExtractionSupported", ({ enumerable: true, get: function () { return metadata_1.isMetadataExtractionSupported; } }));
1898
+ // ANLZ file loading (for analysis data: beat grid, cues, phrases, waveforms)
1899
+ var rekordbox_1 = __webpack_require__(/*! ./localdb/rekordbox */ "./src/localdb/rekordbox.ts");
1900
+ Object.defineProperty(exports, "loadAnlz", ({ enumerable: true, get: function () { return rekordbox_1.loadAnlz; } }));
1901
+ var nfs_1 = __webpack_require__(/*! ./nfs */ "./src/nfs/index.ts");
1902
+ Object.defineProperty(exports, "fetchFile", ({ enumerable: true, get: function () { return nfs_1.fetchFile; } }));
1903
+ var onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
1904
+ Object.defineProperty(exports, "OneLibraryAdapter", ({ enumerable: true, get: function () { return onelibrary_connect_1.OneLibraryAdapter; } }));
1905
+ // Logger interface for pluggable logging
1906
+ var logger_1 = __webpack_require__(/*! ./logger */ "./src/logger.ts");
1907
+ Object.defineProperty(exports, "noopLogger", ({ enumerable: true, get: function () { return logger_1.noopLogger; } }));
1760
1908
  // Types are exported last to avoid overwriting values with type-only exports
1761
1909
  __exportStar(__webpack_require__(/*! ./types */ "./src/types.ts"), exports);
1762
1910
 
@@ -1818,16 +1966,17 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
1818
1966
  var _LocalDatabase_hostDevice, _LocalDatabase_deviceManager, _LocalDatabase_statusEmitter, _LocalDatabase_emitter, _LocalDatabase_slotLocks, _LocalDatabase_dbs, _LocalDatabase_preference, _LocalDatabase_handleDeviceRemoved, _LocalDatabase_fetchFileWithFallback, _LocalDatabase_tryLoadOneLibrary, _LocalDatabase_loadPdbDatabase, _LocalDatabase_hydrateDatabase;
1819
1967
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1820
1968
  const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
1969
+ const onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
1821
1970
  const crypto_1 = __webpack_require__(/*! crypto */ "crypto");
1822
1971
  const events_1 = __webpack_require__(/*! events */ "events");
1823
1972
  const fs = __importStar(__webpack_require__(/*! fs */ "fs"));
1824
1973
  const os = __importStar(__webpack_require__(/*! os */ "os"));
1825
1974
  const path = __importStar(__webpack_require__(/*! path */ "path"));
1975
+ const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
1826
1976
  const nfs_1 = __webpack_require__(/*! src/nfs */ "./src/nfs/index.ts");
1827
1977
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
1828
1978
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
1829
1979
  const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
1830
- const onelibrary_1 = __webpack_require__(/*! ./onelibrary */ "./src/localdb/onelibrary.ts");
1831
1980
  const orm_1 = __webpack_require__(/*! ./orm */ "./src/localdb/orm.ts");
1832
1981
  const rekordbox_1 = __webpack_require__(/*! ./rekordbox */ "./src/localdb/rekordbox.ts");
1833
1982
  /**
@@ -1904,7 +2053,9 @@ class LocalDatabase {
1904
2053
  * Helper to fetch a file from device, trying both dotted and non-dotted paths
1905
2054
  */
1906
2055
  _LocalDatabase_fetchFileWithFallback.set(this, async (device, slot, basePath, tx) => {
1907
- const attemptOrder = process.platform === 'win32' ? [basePath, `.${basePath}`] : [`.${basePath}`, basePath];
2056
+ const attemptOrder = process.platform === 'win32'
2057
+ ? [basePath, `.${basePath}`]
2058
+ : [`.${basePath}`, basePath];
1908
2059
  try {
1909
2060
  return await (0, nfs_1.fetchFile)({
1910
2061
  device,
@@ -1915,7 +2066,7 @@ class LocalDatabase {
1915
2066
  });
1916
2067
  }
1917
2068
  catch {
1918
- return await (0, nfs_1.fetchFile)({
2069
+ return (0, nfs_1.fetchFile)({
1919
2070
  device,
1920
2071
  slot,
1921
2072
  path: attemptOrder[1],
@@ -1936,7 +2087,7 @@ class LocalDatabase {
1936
2087
  const tempDir = os.tmpdir();
1937
2088
  const tempFile = path.join(tempDir, `prolink-onelibrary-${device.id}-${slot}-${Date.now()}.db`);
1938
2089
  fs.writeFileSync(tempFile, dbData);
1939
- const adapter = new onelibrary_1.OneLibraryAdapter(tempFile);
2090
+ const adapter = new onelibrary_connect_1.OneLibraryAdapter(tempFile);
1940
2091
  return { adapter, tempFile };
1941
2092
  }
1942
2093
  catch {
@@ -2049,8 +2200,10 @@ class LocalDatabase {
2049
2200
  if (device === undefined) {
2050
2201
  return null;
2051
2202
  }
2052
- if (device.type !== types_1.DeviceType.CDJ) {
2053
- throw new Error('Cannot create database from devices that are not CDJs');
2203
+ if (device.type !== types_1.DeviceType.CDJ ||
2204
+ device.id < constants_1.MIN_CDJ_DEVICE_ID ||
2205
+ device.id > constants_1.MAX_CDJ_DEVICE_ID) {
2206
+ return null;
2054
2207
  }
2055
2208
  let media;
2056
2209
  try {
@@ -2093,7 +2246,9 @@ class LocalDatabase {
2093
2246
  */
2094
2247
  async preload() {
2095
2248
  const allDevices = [...__classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.values()];
2096
- const cdjDevices = allDevices.filter(device => device.type === types_1.DeviceType.CDJ);
2249
+ const cdjDevices = allDevices.filter(device => device.type === types_1.DeviceType.CDJ &&
2250
+ device.id >= constants_1.MIN_CDJ_DEVICE_ID &&
2251
+ device.id <= constants_1.MAX_CDJ_DEVICE_ID);
2097
2252
  if (cdjDevices.length === 0) {
2098
2253
  return;
2099
2254
  }
@@ -2214,6 +2369,9 @@ var RekordboxAnlz = (function() {
2214
2369
  WAVE_SCROLL: 1347900979,
2215
2370
  WAVE_COLOR_PREVIEW: 1347900980,
2216
2371
  WAVE_COLOR_SCROLL: 1347900981,
2372
+ WAVE_COLOR_3BAND_PREVIEW: 1347900982,
2373
+ WAVE_COLOR_3BAND_DETAIL: 1347900983,
2374
+ VOCAL_CONFIG: 1347900995,
2217
2375
 
2218
2376
  1346588466: "CUES_2",
2219
2377
  1346588482: "CUES",
@@ -2226,6 +2384,9 @@ var RekordboxAnlz = (function() {
2226
2384
  1347900979: "WAVE_SCROLL",
2227
2385
  1347900980: "WAVE_COLOR_PREVIEW",
2228
2386
  1347900981: "WAVE_COLOR_SCROLL",
2387
+ 1347900982: "WAVE_COLOR_3BAND_PREVIEW",
2388
+ 1347900983: "WAVE_COLOR_3BAND_DETAIL",
2389
+ 1347900995: "VOCAL_CONFIG",
2229
2390
  });
2230
2391
 
2231
2392
  RekordboxAnlz.TrackMood = Object.freeze({
@@ -2347,6 +2508,39 @@ var RekordboxAnlz = (function() {
2347
2508
  return PathTag;
2348
2509
  })();
2349
2510
 
2511
+ /**
2512
+ * A 3-band color waveform preview (PWV6, found in .2EX files).
2513
+ * Same resolution as PWV4 (typically 1200 entries) but with 3 bytes
2514
+ * per entry representing low, mid, and high frequency band amplitudes.
2515
+ * Layout is identical to wave_color_preview_tag (PWV4).
2516
+ * @see {@link https://djl-analysis.deepsymmetry.org/djl-analysis/track-metadata.html#color-3band-preview-waveform|Source}
2517
+ */
2518
+
2519
+ var WaveColor3bandPreviewTag = RekordboxAnlz.WaveColor3bandPreviewTag = (function() {
2520
+ function WaveColor3bandPreviewTag(_io, _parent, _root) {
2521
+ this._io = _io;
2522
+ this._parent = _parent;
2523
+ this._root = _root || this;
2524
+
2525
+ this._read();
2526
+ }
2527
+ WaveColor3bandPreviewTag.prototype._read = function() {
2528
+ this.lenEntryBytes = this._io.readU4be();
2529
+ this.lenEntries = this._io.readU4be();
2530
+ this.entries = this._io.readBytes((this.lenEntries * this.lenEntryBytes));
2531
+ }
2532
+
2533
+ /**
2534
+ * The size of each entry, in bytes. Always 3 (low, mid, high).
2535
+ */
2536
+
2537
+ /**
2538
+ * The number of waveform data points. Typically 1200.
2539
+ */
2540
+
2541
+ return WaveColor3bandPreviewTag;
2542
+ })();
2543
+
2350
2544
  /**
2351
2545
  * Stores a waveform preview image suitable for display above
2352
2546
  * the touch strip for jumping to a track position.
@@ -2492,6 +2686,76 @@ var RekordboxAnlz = (function() {
2492
2686
  return WaveColorPreviewTag;
2493
2687
  })();
2494
2688
 
2689
+ /**
2690
+ * A 3-band color detail waveform (PWV7, found in .2EX files).
2691
+ * Higher resolution than PWV6. Each entry is 3 bytes (low, mid, high).
2692
+ * Layout is identical to wave_color_scroll_tag (PWV5).
2693
+ * @see {@link https://djl-analysis.deepsymmetry.org/djl-analysis/track-metadata.html#color-3band-detail-waveform|Source}
2694
+ */
2695
+
2696
+ var WaveColor3bandDetailTag = RekordboxAnlz.WaveColor3bandDetailTag = (function() {
2697
+ function WaveColor3bandDetailTag(_io, _parent, _root) {
2698
+ this._io = _io;
2699
+ this._parent = _parent;
2700
+ this._root = _root || this;
2701
+
2702
+ this._read();
2703
+ }
2704
+ WaveColor3bandDetailTag.prototype._read = function() {
2705
+ this.lenEntryBytes = this._io.readU4be();
2706
+ this.lenEntries = this._io.readU4be();
2707
+ this._unnamed2 = this._io.readU4be();
2708
+ this.entries = this._io.readBytes((this.lenEntries * this.lenEntryBytes));
2709
+ }
2710
+
2711
+ /**
2712
+ * The size of each entry, in bytes. Always 3 (low, mid, high).
2713
+ */
2714
+
2715
+ /**
2716
+ * The number of columns of waveform data.
2717
+ */
2718
+
2719
+ return WaveColor3bandDetailTag;
2720
+ })();
2721
+
2722
+ /**
2723
+ * Vocal detection configuration (PWVC, found in .2EX files).
2724
+ * Contains threshold values used to classify frequency content
2725
+ * as vocal or non-vocal. Body is 8 bytes: unknown(2) + 3 thresholds(2 each).
2726
+ * Observed threshold ranges: low 80-114, mid 80-146, high 98-159.
2727
+ */
2728
+
2729
+ var VocalConfigTag = RekordboxAnlz.VocalConfigTag = (function() {
2730
+ function VocalConfigTag(_io, _parent, _root) {
2731
+ this._io = _io;
2732
+ this._parent = _parent;
2733
+ this._root = _root || this;
2734
+
2735
+ this._read();
2736
+ }
2737
+ VocalConfigTag.prototype._read = function() {
2738
+ this._unnamed0 = this._io.readU2be();
2739
+ this.thresholdLow = this._io.readU2be();
2740
+ this.thresholdMid = this._io.readU2be();
2741
+ this.thresholdHigh = this._io.readU2be();
2742
+ }
2743
+
2744
+ /**
2745
+ * Low frequency vocal detection threshold. Observed range: 80-114.
2746
+ */
2747
+
2748
+ /**
2749
+ * Mid frequency vocal detection threshold. Observed range: 80-146.
2750
+ */
2751
+
2752
+ /**
2753
+ * High frequency vocal detection threshold. Observed range: 98-159.
2754
+ */
2755
+
2756
+ return VocalConfigTag;
2757
+ })();
2758
+
2495
2759
  var PhraseHigh = RekordboxAnlz.PhraseHigh = (function() {
2496
2760
  function PhraseHigh(_io, _parent, _root) {
2497
2761
  this._io = _io;
@@ -2999,6 +3263,11 @@ var RekordboxAnlz = (function() {
2999
3263
  var _io__raw_body = new KaitaiStream(this._raw_body);
3000
3264
  this.body = new BeatGridTag(_io__raw_body, this, this._root);
3001
3265
  break;
3266
+ case RekordboxAnlz.SectionTags.VOCAL_CONFIG:
3267
+ this._raw_body = this._io.readBytes((this.lenTag - 12));
3268
+ var _io__raw_body = new KaitaiStream(this._raw_body);
3269
+ this.body = new VocalConfigTag(_io__raw_body, this, this._root);
3270
+ break;
3002
3271
  case RekordboxAnlz.SectionTags.WAVE_PREVIEW:
3003
3272
  this._raw_body = this._io.readBytes((this.lenTag - 12));
3004
3273
  var _io__raw_body = new KaitaiStream(this._raw_body);
@@ -3019,6 +3288,16 @@ var RekordboxAnlz = (function() {
3019
3288
  var _io__raw_body = new KaitaiStream(this._raw_body);
3020
3289
  this.body = new WavePreviewTag(_io__raw_body, this, this._root);
3021
3290
  break;
3291
+ case RekordboxAnlz.SectionTags.WAVE_COLOR_3BAND_DETAIL:
3292
+ this._raw_body = this._io.readBytes((this.lenTag - 12));
3293
+ var _io__raw_body = new KaitaiStream(this._raw_body);
3294
+ this.body = new WaveColor3bandDetailTag(_io__raw_body, this, this._root);
3295
+ break;
3296
+ case RekordboxAnlz.SectionTags.WAVE_COLOR_3BAND_PREVIEW:
3297
+ this._raw_body = this._io.readBytes((this.lenTag - 12));
3298
+ var _io__raw_body = new KaitaiStream(this._raw_body);
3299
+ this.body = new WaveColor3bandPreviewTag(_io__raw_body, this, this._root);
3300
+ break;
3022
3301
  default:
3023
3302
  this._raw_body = this._io.readBytes((this.lenTag - 12));
3024
3303
  var _io__raw_body = new KaitaiStream(this._raw_body);
@@ -4834,141 +5113,37 @@ return RekordboxPdb;
4834
5113
 
4835
5114
  /***/ },
4836
5115
 
4837
- /***/ "./src/localdb/onelibrary-schema.ts"
4838
- /*!******************************************!*\
4839
- !*** ./src/localdb/onelibrary-schema.ts ***!
4840
- \******************************************/
4841
- (__unused_webpack_module, exports) {
5116
+ /***/ "./src/localdb/onelibrary.ts"
5117
+ /*!***********************************!*\
5118
+ !*** ./src/localdb/onelibrary.ts ***!
5119
+ \***********************************/
5120
+ (__unused_webpack_module, exports, __webpack_require__) {
4842
5121
 
4843
5122
  "use strict";
4844
5123
 
4845
5124
  /**
4846
- * TypeScript interfaces for the OneLibrary (exportLibrary.db) SQLite schema.
4847
- *
4848
- * These match the actual column names in the SQLite database,
4849
- * which differ from the legacy PDB format.
5125
+ * OneLibrary Database Adapter
4850
5126
  *
4851
- * Note: Column names use camelCase in the database (e.g., bpmx100, titleForSearch)
5127
+ * Re-exports from onelibrary-connect for backward compatibility.
4852
5128
  */
4853
5129
  Object.defineProperty(exports, "__esModule", ({ value: true }));
4854
- exports.MenuItemKind = exports.MyTagAttribute = exports.PlaylistAttribute = exports.CueKind = void 0;
4855
- // ============================================================================
4856
- // Cue Kind Constants
4857
- // ============================================================================
4858
- /**
4859
- * Cue point types based on the 'kind' field in the cue table.
4860
- *
4861
- * Note: These values may vary. Verify with actual data.
4862
- */
4863
- exports.CueKind = {
4864
- MEMORY_CUE: 0,
4865
- HOT_CUE: 1,
4866
- // Additional types may exist
4867
- };
4868
- /**
4869
- * Playlist attribute types based on the 'attribute' field.
4870
- */
4871
- exports.PlaylistAttribute = {
4872
- PLAYLIST: 0,
4873
- FOLDER: 1,
4874
- };
4875
- /**
4876
- * MyTag attribute types based on the 'attribute' field.
4877
- */
4878
- exports.MyTagAttribute = {
4879
- TAG: 0,
4880
- FOLDER: 1,
4881
- };
4882
- // ============================================================================
4883
- // Menu Item Kind Constants
4884
- // ============================================================================
4885
- /**
4886
- * Menu item kinds for browsing categories.
4887
- * These match the 'kind' field in the menuItem table.
4888
- */
4889
- exports.MenuItemKind = {
4890
- GENRE: 128,
4891
- ARTIST: 129,
4892
- ALBUM: 130,
4893
- TRACK: 131,
4894
- PLAYLIST: 132,
4895
- BPM: 133,
4896
- RATING: 134,
4897
- YEAR: 135,
4898
- REMIXER: 136,
4899
- LABEL: 137,
4900
- ORIGINAL_ARTIST: 138,
4901
- KEY: 139,
4902
- DATE_ADDED: 140,
4903
- CUE: 141,
4904
- COLOR: 142,
4905
- FOLDER: 144,
4906
- SEARCH: 145,
4907
- TIME: 146,
4908
- BITRATE: 147,
4909
- FILE_NAME: 148,
4910
- HISTORY: 149,
4911
- COMMENTS: 150,
4912
- DJ_PLAY_COUNT: 151,
4913
- HOT_CUE_BANK: 152,
4914
- DEFAULT: 161,
4915
- ALPHABET: 162,
4916
- MATCHING: 170,
4917
- };
5130
+ exports.openOneLibraryDb = exports.OneLibraryAdapter = exports.getEncryptionKey = void 0;
5131
+ var onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
5132
+ Object.defineProperty(exports, "getEncryptionKey", ({ enumerable: true, get: function () { return onelibrary_connect_1.getEncryptionKey; } }));
5133
+ Object.defineProperty(exports, "OneLibraryAdapter", ({ enumerable: true, get: function () { return onelibrary_connect_1.OneLibraryAdapter; } }));
5134
+ Object.defineProperty(exports, "openOneLibraryDb", ({ enumerable: true, get: function () { return onelibrary_connect_1.openOneLibraryDb; } }));
4918
5135
 
4919
5136
 
4920
5137
  /***/ },
4921
5138
 
4922
- /***/ "./src/localdb/onelibrary.ts"
4923
- /*!***********************************!*\
4924
- !*** ./src/localdb/onelibrary.ts ***!
4925
- \***********************************/
5139
+ /***/ "./src/localdb/orm.ts"
5140
+ /*!****************************!*\
5141
+ !*** ./src/localdb/orm.ts ***!
5142
+ \****************************/
4926
5143
  (__unused_webpack_module, exports, __webpack_require__) {
4927
5144
 
4928
5145
  "use strict";
4929
5146
 
4930
- /**
4931
- * OneLibrary Database Adapter
4932
- *
4933
- * Provides an interface for reading the OneLibrary (exportLibrary.db) SQLite database
4934
- * used by modern rekordbox versions and Pioneer DJ devices.
4935
- *
4936
- * The database is encrypted with SQLCipher 4. The encryption key is derived from
4937
- * a hardcoded obfuscated blob.
4938
- */
4939
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4940
- if (k2 === undefined) k2 = k;
4941
- var desc = Object.getOwnPropertyDescriptor(m, k);
4942
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
4943
- desc = { enumerable: true, get: function() { return m[k]; } };
4944
- }
4945
- Object.defineProperty(o, k2, desc);
4946
- }) : (function(o, m, k, k2) {
4947
- if (k2 === undefined) k2 = k;
4948
- o[k2] = m[k];
4949
- }));
4950
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
4951
- Object.defineProperty(o, "default", { enumerable: true, value: v });
4952
- }) : function(o, v) {
4953
- o["default"] = v;
4954
- });
4955
- var __importStar = (this && this.__importStar) || (function () {
4956
- var ownKeys = function(o) {
4957
- ownKeys = Object.getOwnPropertyNames || function (o) {
4958
- var ar = [];
4959
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
4960
- return ar;
4961
- };
4962
- return ownKeys(o);
4963
- };
4964
- return function (mod) {
4965
- if (mod && mod.__esModule) return mod;
4966
- var result = {};
4967
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
4968
- __setModuleDefault(result, mod);
4969
- return result;
4970
- };
4971
- })();
4972
5147
  var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
4973
5148
  if (kind === "m") throw new TypeError("Private method is not writable");
4974
5149
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
@@ -4983,673 +5158,14 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
4983
5158
  var __importDefault = (this && this.__importDefault) || function (mod) {
4984
5159
  return (mod && mod.__esModule) ? mod : { "default": mod };
4985
5160
  };
4986
- var _OneLibraryAdapter_instances, _OneLibraryAdapter_db, _OneLibraryAdapter_stmtCache, _OneLibraryAdapter_getStmt, _OneLibraryAdapter_contentToTrack, _OneLibraryAdapter_cueToCueAndLoop, _OneLibraryAdapter_playlistRowToPlaylist, _OneLibraryAdapter_myTagRowToMyTag;
5161
+ var _MetadataORM_conn, _MetadataORM_stmtCache, _MetadataORM_inTransaction;
4987
5162
  Object.defineProperty(exports, "__esModule", ({ value: true }));
4988
- exports.OneLibraryAdapter = void 0;
4989
- exports.getEncryptionKey = getEncryptionKey;
4990
- exports.openOneLibraryDb = openOneLibraryDb;
5163
+ exports.MetadataORM = exports.Table = void 0;
4991
5164
  const better_sqlite3_multiple_ciphers_1 = __importDefault(__webpack_require__(/*! better-sqlite3-multiple-ciphers */ "better-sqlite3-multiple-ciphers"));
4992
- const zlib = __importStar(__webpack_require__(/*! zlib */ "zlib"));
4993
- const onelibrary_schema_1 = __webpack_require__(/*! ./onelibrary-schema */ "./src/localdb/onelibrary-schema.ts");
4994
- // ============================================================================
4995
- // Encryption Key Derivation
4996
- // ============================================================================
4997
- /**
4998
- * The obfuscated encryption key blob from pyrekordbox
4999
- */
5000
- const BLOB = Buffer.from('PN_1dH8$oLJY)16j_RvM6qphWw`476>;C1cWmI#se(PG`j}~xAjlufj?`#0i{;=glh(SkW)y0>n?YEiD`l%t(', 'ascii');
5001
- /**
5002
- * XOR key used for deobfuscation
5003
- */
5004
- const BLOB_KEY = Buffer.from('657f48f84c437cc1', 'ascii');
5005
- /**
5006
- * Base85 (RFC 1924) decode
5007
- */
5008
- function base85Decode(input) {
5009
- const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~';
5010
- const charToValue = new Map();
5011
- for (let i = 0; i < alphabet.length; i++) {
5012
- charToValue.set(alphabet[i], i);
5013
- }
5014
- const inputStr = input.toString('ascii');
5015
- const result = [];
5016
- for (let i = 0; i < inputStr.length; i += 5) {
5017
- const chunk = inputStr.slice(i, i + 5);
5018
- let value = 0;
5019
- for (const char of chunk) {
5020
- const v = charToValue.get(char);
5021
- if (v === undefined) {
5022
- throw new Error(`Invalid base85 character: ${char}`);
5023
- }
5024
- value = value * 85 + v;
5025
- }
5026
- const bytes = [
5027
- (value >> 24) & 0xff,
5028
- (value >> 16) & 0xff,
5029
- (value >> 8) & 0xff,
5030
- value & 0xff,
5031
- ];
5032
- const numBytes = chunk.length === 5 ? 4 : chunk.length - 1;
5033
- result.push(...bytes.slice(0, numBytes));
5034
- }
5035
- return Buffer.from(result);
5036
- }
5037
- /**
5038
- * Deobfuscate the blob to get the encryption key
5039
- */
5040
- function deobfuscate(blob) {
5041
- const decoded = base85Decode(blob);
5042
- const xored = Buffer.alloc(decoded.length);
5043
- for (let i = 0; i < decoded.length; i++) {
5044
- xored[i] = decoded[i] ^ BLOB_KEY[i % BLOB_KEY.length];
5045
- }
5046
- const decompressed = zlib.inflateSync(xored);
5047
- return decompressed.toString('utf-8');
5048
- }
5165
+ const lodash_1 = __webpack_require__(/*! lodash */ "lodash");
5166
+ const schema_1 = __webpack_require__(/*! ./schema */ "./src/localdb/schema.ts");
5049
5167
  /**
5050
- * Get the SQLCipher encryption key for OneLibrary databases
5051
- */
5052
- function getEncryptionKey() {
5053
- const key = deobfuscate(BLOB);
5054
- if (!key.startsWith('r8gd')) {
5055
- throw new Error('Invalid encryption key derived');
5056
- }
5057
- return key;
5058
- }
5059
- // ============================================================================
5060
- // Database Connection
5061
- // ============================================================================
5062
- /**
5063
- * Open a OneLibrary database with SQLCipher decryption
5064
- */
5065
- function openOneLibraryDb(dbPath) {
5066
- const key = getEncryptionKey();
5067
- const db = new better_sqlite3_multiple_ciphers_1.default(dbPath, { readonly: true });
5068
- db.pragma('cipher = sqlcipher');
5069
- db.pragma('legacy = 4');
5070
- db.pragma(`key = '${key}'`);
5071
- return db;
5072
- }
5073
- // ============================================================================
5074
- // OneLibrary Adapter
5075
- // ============================================================================
5076
- /**
5077
- * Adapter for OneLibrary database that matches the ORM interface.
5078
- * Queries the SQLite file directly instead of hydrating into memory.
5079
- */
5080
- class OneLibraryAdapter {
5081
- constructor(dbPath) {
5082
- _OneLibraryAdapter_instances.add(this);
5083
- this.type = 'oneLibrary';
5084
- _OneLibraryAdapter_db.set(this, void 0);
5085
- _OneLibraryAdapter_stmtCache.set(this, new Map());
5086
- __classPrivateFieldSet(this, _OneLibraryAdapter_db, openOneLibraryDb(dbPath), "f");
5087
- }
5088
- /**
5089
- * Close the database connection
5090
- */
5091
- close() {
5092
- __classPrivateFieldGet(this, _OneLibraryAdapter_stmtCache, "f").clear();
5093
- __classPrivateFieldGet(this, _OneLibraryAdapter_db, "f").close();
5094
- }
5095
- // ==========================================================================
5096
- // Track Queries
5097
- // ==========================================================================
5098
- /**
5099
- * Find a track by ID
5100
- */
5101
- findTrack(id) {
5102
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5103
- SELECT c.*,
5104
- a.name as artistName,
5105
- al.name as albumName,
5106
- g.name as genreName,
5107
- k.name as keyName,
5108
- col.name as colorName,
5109
- lbl.name as labelName,
5110
- img.path as artworkPath,
5111
- remix.name as remixerName,
5112
- orig.name as originalArtistName,
5113
- comp.name as composerName
5114
- FROM content c
5115
- LEFT JOIN artist a ON c.artist_id_artist = a.artist_id
5116
- LEFT JOIN album al ON c.album_id = al.album_id
5117
- LEFT JOIN genre g ON c.genre_id = g.genre_id
5118
- LEFT JOIN key k ON c.key_id = k.key_id
5119
- LEFT JOIN color col ON c.color_id = col.color_id
5120
- LEFT JOIN label lbl ON c.label_id = lbl.label_id
5121
- LEFT JOIN image img ON c.image_id = img.image_id
5122
- LEFT JOIN artist remix ON c.artist_id_remixer = remix.artist_id
5123
- LEFT JOIN artist orig ON c.artist_id_originalArtist = orig.artist_id
5124
- LEFT JOIN artist comp ON c.artist_id_composer = comp.artist_id
5125
- WHERE c.content_id = ?
5126
- `).get(id);
5127
- if (!row) {
5128
- return null;
5129
- }
5130
- return __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_contentToTrack).call(this, row);
5131
- }
5132
- /**
5133
- * Find all tracks in the database
5134
- */
5135
- findAllTracks() {
5136
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5137
- SELECT c.*,
5138
- a.name as artistName,
5139
- al.name as albumName,
5140
- g.name as genreName,
5141
- k.name as keyName,
5142
- col.name as colorName,
5143
- lbl.name as labelName,
5144
- img.path as artworkPath,
5145
- remix.name as remixerName,
5146
- orig.name as originalArtistName,
5147
- comp.name as composerName
5148
- FROM content c
5149
- LEFT JOIN artist a ON c.artist_id_artist = a.artist_id
5150
- LEFT JOIN album al ON c.album_id = al.album_id
5151
- LEFT JOIN genre g ON c.genre_id = g.genre_id
5152
- LEFT JOIN key k ON c.key_id = k.key_id
5153
- LEFT JOIN color col ON c.color_id = col.color_id
5154
- LEFT JOIN label lbl ON c.label_id = lbl.label_id
5155
- LEFT JOIN image img ON c.image_id = img.image_id
5156
- LEFT JOIN artist remix ON c.artist_id_remixer = remix.artist_id
5157
- LEFT JOIN artist orig ON c.artist_id_originalArtist = orig.artist_id
5158
- LEFT JOIN artist comp ON c.artist_id_composer = comp.artist_id
5159
- `).all();
5160
- return rows.map(row => __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_contentToTrack).call(this, row));
5161
- }
5162
- // ==========================================================================
5163
- // Cue Queries
5164
- // ==========================================================================
5165
- /**
5166
- * Find cue points for a track
5167
- */
5168
- findCues(trackId) {
5169
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5170
- SELECT * FROM cue WHERE content_id = ?
5171
- `).all(trackId);
5172
- return rows.map(row => __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_cueToCueAndLoop).call(this, row));
5173
- }
5174
- // ==========================================================================
5175
- // Playlist Queries
5176
- // ==========================================================================
5177
- /**
5178
- * Find a playlist by ID
5179
- */
5180
- findPlaylistById(playlistId) {
5181
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5182
- SELECT * FROM playlist WHERE playlist_id = ?
5183
- `).get(playlistId);
5184
- return row ? __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_playlistRowToPlaylist).call(this, row) : null;
5185
- }
5186
- /**
5187
- * Query for a list of {folders, playlists, tracks} given a playlist ID.
5188
- * If no ID is provided the root list is queried.
5189
- */
5190
- findPlaylist(playlistId) {
5191
- const parentCondition = playlistId === undefined ? 'playlist_id_parent IS NULL' : 'playlist_id_parent = ?';
5192
- // Lookup playlists / folders for this playlist ID
5193
- const playlistRows = (playlistId === undefined
5194
- ? __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `SELECT * FROM playlist WHERE ${parentCondition}`).all()
5195
- : __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `SELECT * FROM playlist WHERE ${parentCondition}`).all(playlistId));
5196
- const folders = [];
5197
- const playlists = [];
5198
- for (const row of playlistRows) {
5199
- const playlist = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_playlistRowToPlaylist).call(this, row);
5200
- if (playlist.isFolder) {
5201
- folders.push(playlist);
5202
- }
5203
- else {
5204
- playlists.push(playlist);
5205
- }
5206
- }
5207
- // Get track entries for this playlist
5208
- const entryRows = playlistId === undefined
5209
- ? []
5210
- : __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5211
- SELECT * FROM playlist_content WHERE playlist_id = ? ORDER BY sequenceNo
5212
- `).all(playlistId);
5213
- const trackEntries = entryRows.map((row, index) => ({
5214
- id: index, // playlist_content doesn't have a unique ID, use index
5215
- sortIndex: row.sequenceNo,
5216
- playlistId: row.playlist_id,
5217
- trackId: row.content_id,
5218
- }));
5219
- return { folders, playlists, trackEntries };
5220
- }
5221
- /**
5222
- * Get track IDs for a playlist in order
5223
- */
5224
- findPlaylistContents(playlistId) {
5225
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5226
- SELECT content_id FROM playlist_content
5227
- WHERE playlist_id = ? ORDER BY sequenceNo
5228
- `).all(playlistId);
5229
- return rows.map(r => r.content_id);
5230
- }
5231
- // ==========================================================================
5232
- // Reference Table Queries
5233
- // ==========================================================================
5234
- findArtist(artistId) {
5235
- var _a;
5236
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5237
- SELECT * FROM artist WHERE artist_id = ?
5238
- `).get(artistId);
5239
- return row ? { id: row.artist_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5240
- }
5241
- findAlbum(albumId) {
5242
- var _a;
5243
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5244
- SELECT * FROM album WHERE album_id = ?
5245
- `).get(albumId);
5246
- return row ? { id: row.album_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5247
- }
5248
- findGenre(genreId) {
5249
- var _a;
5250
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5251
- SELECT * FROM genre WHERE genre_id = ?
5252
- `).get(genreId);
5253
- return row ? { id: row.genre_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5254
- }
5255
- findKey(keyId) {
5256
- var _a;
5257
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5258
- SELECT * FROM key WHERE key_id = ?
5259
- `).get(keyId);
5260
- return row ? { id: row.key_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5261
- }
5262
- findColor(colorId) {
5263
- var _a;
5264
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5265
- SELECT * FROM color WHERE color_id = ?
5266
- `).get(colorId);
5267
- return row ? { id: row.color_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5268
- }
5269
- findLabel(labelId) {
5270
- var _a;
5271
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5272
- SELECT * FROM label WHERE label_id = ?
5273
- `).get(labelId);
5274
- return row ? { id: row.label_id, name: (_a = row.name) !== null && _a !== void 0 ? _a : '' } : null;
5275
- }
5276
- findArtwork(imageId) {
5277
- var _a;
5278
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5279
- SELECT * FROM image WHERE image_id = ?
5280
- `).get(imageId);
5281
- return row ? { id: row.image_id, path: (_a = row.path) !== null && _a !== void 0 ? _a : undefined } : null;
5282
- }
5283
- // ==========================================================================
5284
- // MyTag (User Tags) Queries
5285
- // ==========================================================================
5286
- /**
5287
- * Find all root-level MyTags (folders and tags with no parent)
5288
- */
5289
- findMyTags(parentId) {
5290
- const parentCondition = parentId === undefined
5291
- ? 'myTag_id_parent IS NULL OR myTag_id_parent = 0'
5292
- : 'myTag_id_parent = ?';
5293
- const rows = (parentId === undefined
5294
- ? __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `SELECT * FROM myTag WHERE ${parentCondition} ORDER BY sequenceNo`).all()
5295
- : __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `SELECT * FROM myTag WHERE ${parentCondition} ORDER BY sequenceNo`).all(parentId));
5296
- const folders = [];
5297
- const tags = [];
5298
- for (const row of rows) {
5299
- const tag = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_myTagRowToMyTag).call(this, row);
5300
- if (tag.isFolder) {
5301
- folders.push(tag);
5302
- }
5303
- else {
5304
- tags.push(tag);
5305
- }
5306
- }
5307
- return { folders, tags };
5308
- }
5309
- /**
5310
- * Find a MyTag by ID
5311
- */
5312
- findMyTagById(myTagId) {
5313
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5314
- SELECT * FROM myTag WHERE myTag_id = ?
5315
- `).get(myTagId);
5316
- return row ? __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_myTagRowToMyTag).call(this, row) : null;
5317
- }
5318
- /**
5319
- * Get track IDs for a MyTag
5320
- */
5321
- findMyTagContents(myTagId) {
5322
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5323
- SELECT content_id FROM myTag_content WHERE myTag_id = ?
5324
- `).all(myTagId);
5325
- return rows.map(r => r.content_id);
5326
- }
5327
- /**
5328
- * Get all MyTags assigned to a track
5329
- */
5330
- findMyTagsForTrack(trackId) {
5331
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5332
- SELECT t.* FROM myTag t
5333
- INNER JOIN myTag_content tc ON t.myTag_id = tc.myTag_id
5334
- WHERE tc.content_id = ?
5335
- `).all(trackId);
5336
- return rows.map(row => __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_myTagRowToMyTag).call(this, row));
5337
- }
5338
- // ==========================================================================
5339
- // History Queries
5340
- // ==========================================================================
5341
- /**
5342
- * Find all history sessions
5343
- */
5344
- findHistorySessions() {
5345
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5346
- SELECT * FROM history ORDER BY sequenceNo
5347
- `).all();
5348
- return rows.map(row => {
5349
- var _a;
5350
- return ({
5351
- id: row.history_id,
5352
- name: (_a = row.name) !== null && _a !== void 0 ? _a : '',
5353
- parentId: row.history_id_parent,
5354
- });
5355
- });
5356
- }
5357
- /**
5358
- * Get track IDs for a history session in order
5359
- */
5360
- findHistoryContents(historyId) {
5361
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5362
- SELECT content_id FROM history_content
5363
- WHERE history_id = ? ORDER BY sequenceNo
5364
- `).all(historyId);
5365
- return rows.map(r => r.content_id);
5366
- }
5367
- // ==========================================================================
5368
- // Hot Cue Bank Queries
5369
- // ==========================================================================
5370
- /**
5371
- * Find all hot cue bank lists
5372
- */
5373
- findHotCueBankLists() {
5374
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5375
- SELECT * FROM hotCueBankList ORDER BY sequenceNo
5376
- `).all();
5377
- return rows.map(row => {
5378
- var _a;
5379
- return ({
5380
- id: row.hotCueBankList_id,
5381
- name: (_a = row.name) !== null && _a !== void 0 ? _a : '',
5382
- parentId: row.hotCueBankList_id_parent,
5383
- });
5384
- });
5385
- }
5386
- /**
5387
- * Get cue IDs for a hot cue bank list
5388
- */
5389
- findHotCueBankListCues(bankListId) {
5390
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5391
- SELECT cue_id FROM hotCueBankList_cue
5392
- WHERE hotCueBankList_id = ? ORDER BY sequenceNo
5393
- `).all(bankListId);
5394
- return rows.map(r => r.cue_id);
5395
- }
5396
- // ==========================================================================
5397
- // Menu Configuration Queries
5398
- // ==========================================================================
5399
- /**
5400
- * Get all menu items (browse categories)
5401
- */
5402
- findMenuItems() {
5403
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5404
- SELECT * FROM menuItem ORDER BY menuItem_id
5405
- `).all();
5406
- return rows.map(row => {
5407
- var _a, _b;
5408
- return ({
5409
- id: row.menuItem_id,
5410
- kind: (_a = row.kind) !== null && _a !== void 0 ? _a : 0,
5411
- name: (_b = row.name) !== null && _b !== void 0 ? _b : '',
5412
- });
5413
- });
5414
- }
5415
- /**
5416
- * Get visible categories with their menu item info
5417
- */
5418
- findVisibleCategories() {
5419
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5420
- SELECT c.*, m.name as menuName, m.kind as menuKind
5421
- FROM category c
5422
- LEFT JOIN menuItem m ON c.menuItem_id = m.menuItem_id
5423
- WHERE c.isVisible = 1
5424
- ORDER BY c.sequenceNo
5425
- `).all();
5426
- return rows.map(row => {
5427
- var _a, _b, _c;
5428
- return ({
5429
- id: row.category_id,
5430
- menuItemId: (_a = row.menuItem_id) !== null && _a !== void 0 ? _a : 0,
5431
- name: (_b = row.menuName) !== null && _b !== void 0 ? _b : '',
5432
- kind: (_c = row.menuKind) !== null && _c !== void 0 ? _c : 0,
5433
- isVisible: !!row.isVisible,
5434
- });
5435
- });
5436
- }
5437
- /**
5438
- * Get visible sort options
5439
- */
5440
- findVisibleSortOptions() {
5441
- const rows = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5442
- SELECT s.*, m.name as menuName, m.kind as menuKind
5443
- FROM sort s
5444
- LEFT JOIN menuItem m ON s.menuItem_id = m.menuItem_id
5445
- WHERE s.isVisible = 1
5446
- ORDER BY s.sequenceNo
5447
- `).all();
5448
- return rows.map(row => {
5449
- var _a, _b, _c;
5450
- return ({
5451
- id: row.sort_id,
5452
- menuItemId: (_a = row.menuItem_id) !== null && _a !== void 0 ? _a : 0,
5453
- name: (_b = row.menuName) !== null && _b !== void 0 ? _b : '',
5454
- kind: (_c = row.menuKind) !== null && _c !== void 0 ? _c : 0,
5455
- isVisible: !!row.isVisible,
5456
- isSelectedAsSubColumn: !!row.isSelectedAsSubColumn,
5457
- });
5458
- });
5459
- }
5460
- // ==========================================================================
5461
- // Device Property Queries
5462
- // ==========================================================================
5463
- /**
5464
- * Get device properties
5465
- */
5466
- getProperty() {
5467
- var _a, _b, _c, _d, _e;
5468
- const row = __classPrivateFieldGet(this, _OneLibraryAdapter_instances, "m", _OneLibraryAdapter_getStmt).call(this, `
5469
- SELECT * FROM property LIMIT 1
5470
- `).get();
5471
- if (!row) {
5472
- return null;
5473
- }
5474
- return {
5475
- deviceName: (_a = row.deviceName) !== null && _a !== void 0 ? _a : '',
5476
- dbVersion: (_b = row.dbVersion) !== null && _b !== void 0 ? _b : '',
5477
- numberOfContents: (_c = row.numberOfContents) !== null && _c !== void 0 ? _c : 0,
5478
- createdDate: (_d = row.createdDate) !== null && _d !== void 0 ? _d : '',
5479
- backgroundColorType: (_e = row.backGroundColorType) !== null && _e !== void 0 ? _e : 0,
5480
- };
5481
- }
5482
- }
5483
- exports.OneLibraryAdapter = OneLibraryAdapter;
5484
- _OneLibraryAdapter_db = new WeakMap(), _OneLibraryAdapter_stmtCache = new WeakMap(), _OneLibraryAdapter_instances = new WeakSet(), _OneLibraryAdapter_getStmt = function _OneLibraryAdapter_getStmt(sql) {
5485
- let stmt = __classPrivateFieldGet(this, _OneLibraryAdapter_stmtCache, "f").get(sql);
5486
- if (!stmt) {
5487
- stmt = __classPrivateFieldGet(this, _OneLibraryAdapter_db, "f").prepare(sql);
5488
- __classPrivateFieldGet(this, _OneLibraryAdapter_stmtCache, "f").set(sql, stmt);
5489
- }
5490
- return stmt;
5491
- }, _OneLibraryAdapter_contentToTrack = function _OneLibraryAdapter_contentToTrack(row) {
5492
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
5493
- const track = {
5494
- id: row.content_id,
5495
- title: (_a = row.title) !== null && _a !== void 0 ? _a : '',
5496
- duration: row.length ? row.length / 1000 : 0, // ms to seconds
5497
- bitrate: (_b = row.bitrate) !== null && _b !== void 0 ? _b : undefined,
5498
- tempo: row.bpmx100 ? row.bpmx100 / 100 : 0,
5499
- rating: (_c = row.rating) !== null && _c !== void 0 ? _c : 0,
5500
- comment: (_d = row.djComment) !== null && _d !== void 0 ? _d : '',
5501
- filePath: (_e = row.path) !== null && _e !== void 0 ? _e : '',
5502
- fileName: (_f = row.fileName) !== null && _f !== void 0 ? _f : '',
5503
- trackNumber: (_g = row.trackNo) !== null && _g !== void 0 ? _g : undefined,
5504
- discNumber: (_h = row.discNo) !== null && _h !== void 0 ? _h : undefined,
5505
- sampleRate: (_j = row.samplingRate) !== null && _j !== void 0 ? _j : undefined,
5506
- sampleDepth: (_k = row.bitDepth) !== null && _k !== void 0 ? _k : undefined,
5507
- playCount: (_l = row.djPlayCount) !== null && _l !== void 0 ? _l : undefined,
5508
- year: (_m = row.releaseYear) !== null && _m !== void 0 ? _m : undefined,
5509
- mixName: (_o = row.subtitle) !== null && _o !== void 0 ? _o : undefined,
5510
- autoloadHotcues: !!row.isHotCueAutoLoadOn,
5511
- kuvoPublic: !!row.isKuvoDeliverStatusOn,
5512
- fileSize: (_p = row.fileSize) !== null && _p !== void 0 ? _p : undefined,
5513
- // Normalize analyzePath by trimming the .DAT extension (same as rekordbox.ts)
5514
- // loadAnlz() will append the appropriate extension (.DAT or .EXT) when loading
5515
- analyzePath: row.analysisDataFilePath
5516
- ? row.analysisDataFilePath.substring(0, row.analysisDataFilePath.length - 4)
5517
- : undefined,
5518
- releaseDate: (_q = row.releaseDate) !== null && _q !== void 0 ? _q : undefined,
5519
- dateAdded: row.dateAdded ? new Date(row.dateAdded) : undefined,
5520
- beatGrid: null,
5521
- cueAndLoops: null,
5522
- waveformHd: null,
5523
- artwork: row.image_id
5524
- ? { id: row.image_id, path: (_r = row.artworkPath) !== null && _r !== void 0 ? _r : undefined }
5525
- : null,
5526
- artist: row.artist_id_artist
5527
- ? { id: row.artist_id_artist, name: (_s = row.artistName) !== null && _s !== void 0 ? _s : '' }
5528
- : null,
5529
- originalArtist: row.artist_id_originalArtist
5530
- ? {
5531
- id: row.artist_id_originalArtist,
5532
- name: (_t = row.originalArtistName) !== null && _t !== void 0 ? _t : '',
5533
- }
5534
- : null,
5535
- remixer: row.artist_id_remixer
5536
- ? { id: row.artist_id_remixer, name: (_u = row.remixerName) !== null && _u !== void 0 ? _u : '' }
5537
- : null,
5538
- composer: row.artist_id_composer
5539
- ? { id: row.artist_id_composer, name: (_v = row.composerName) !== null && _v !== void 0 ? _v : '' }
5540
- : null,
5541
- album: row.album_id
5542
- ? { id: row.album_id, name: (_w = row.albumName) !== null && _w !== void 0 ? _w : '' }
5543
- : null,
5544
- label: row.label_id
5545
- ? { id: row.label_id, name: (_x = row.labelName) !== null && _x !== void 0 ? _x : '' }
5546
- : null,
5547
- genre: row.genre_id
5548
- ? { id: row.genre_id, name: (_y = row.genreName) !== null && _y !== void 0 ? _y : '' }
5549
- : null,
5550
- color: row.color_id
5551
- ? { id: row.color_id, name: (_z = row.colorName) !== null && _z !== void 0 ? _z : '' }
5552
- : null,
5553
- key: row.key_id ? { id: row.key_id, name: (_0 = row.keyName) !== null && _0 !== void 0 ? _0 : '' } : null,
5554
- };
5555
- return track;
5556
- }, _OneLibraryAdapter_cueToCueAndLoop = function _OneLibraryAdapter_cueToCueAndLoop(row) {
5557
- var _a, _b, _c, _d, _e, _f;
5558
- const offset = row.inUsec ? row.inUsec / 1000 : 0; // microseconds to ms
5559
- const outOffset = (_a = row.outUsec) !== null && _a !== void 0 ? _a : null;
5560
- const isLoop = outOffset !== null && outOffset > 0;
5561
- const length = isLoop ? (outOffset - ((_b = row.inUsec) !== null && _b !== void 0 ? _b : 0)) / 1000 : 0;
5562
- // Determine if this is a hot cue (kind value mapping may vary)
5563
- // Based on observation: kind 0 = memory cue, kind >= 1 may be hot cue
5564
- const isHotCue = ((_c = row.kind) !== null && _c !== void 0 ? _c : 0) >= 1 && ((_d = row.kind) !== null && _d !== void 0 ? _d : 0) <= 8;
5565
- const button = isHotCue ? ((_e = row.kind) !== null && _e !== void 0 ? _e : 0) : undefined;
5566
- const label = (_f = row.cueComment) !== null && _f !== void 0 ? _f : undefined;
5567
- const color = row.colorTableIndex;
5568
- if (isLoop && isHotCue) {
5569
- return {
5570
- type: 'hot_loop',
5571
- offset,
5572
- length,
5573
- button: button,
5574
- label,
5575
- color,
5576
- };
5577
- }
5578
- if (isLoop) {
5579
- return {
5580
- type: 'loop',
5581
- offset,
5582
- length,
5583
- label,
5584
- color,
5585
- };
5586
- }
5587
- if (isHotCue) {
5588
- return {
5589
- type: 'hot_cue',
5590
- offset,
5591
- button: button,
5592
- label,
5593
- color,
5594
- };
5595
- }
5596
- return {
5597
- type: 'cue_point',
5598
- offset,
5599
- label,
5600
- color,
5601
- };
5602
- }, _OneLibraryAdapter_playlistRowToPlaylist = function _OneLibraryAdapter_playlistRowToPlaylist(row) {
5603
- var _a;
5604
- return {
5605
- id: row.playlist_id,
5606
- name: (_a = row.name) !== null && _a !== void 0 ? _a : '',
5607
- isFolder: row.attribute === onelibrary_schema_1.PlaylistAttribute.FOLDER,
5608
- parentId: row.playlist_id_parent,
5609
- };
5610
- }, _OneLibraryAdapter_myTagRowToMyTag = function _OneLibraryAdapter_myTagRowToMyTag(row) {
5611
- var _a;
5612
- return {
5613
- id: row.myTag_id,
5614
- name: (_a = row.name) !== null && _a !== void 0 ? _a : '',
5615
- isFolder: row.attribute === onelibrary_schema_1.MyTagAttribute.FOLDER,
5616
- parentId: row.myTag_id_parent,
5617
- };
5618
- };
5619
-
5620
-
5621
- /***/ },
5622
-
5623
- /***/ "./src/localdb/orm.ts"
5624
- /*!****************************!*\
5625
- !*** ./src/localdb/orm.ts ***!
5626
- \****************************/
5627
- (__unused_webpack_module, exports, __webpack_require__) {
5628
-
5629
- "use strict";
5630
-
5631
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
5632
- if (kind === "m") throw new TypeError("Private method is not writable");
5633
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5634
- 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");
5635
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
5636
- };
5637
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
5638
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
5639
- 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");
5640
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5641
- };
5642
- var __importDefault = (this && this.__importDefault) || function (mod) {
5643
- return (mod && mod.__esModule) ? mod : { "default": mod };
5644
- };
5645
- var _MetadataORM_conn, _MetadataORM_stmtCache, _MetadataORM_inTransaction;
5646
- Object.defineProperty(exports, "__esModule", ({ value: true }));
5647
- exports.MetadataORM = exports.Table = void 0;
5648
- const better_sqlite3_multiple_ciphers_1 = __importDefault(__webpack_require__(/*! better-sqlite3-multiple-ciphers */ "better-sqlite3-multiple-ciphers"));
5649
- const lodash_1 = __webpack_require__(/*! lodash */ "lodash");
5650
- const schema_1 = __webpack_require__(/*! ./schema */ "./src/localdb/schema.ts");
5651
- /**
5652
- * Table names available
5168
+ * Table names available
5653
5169
  */
5654
5170
  var Table;
5655
5171
  (function (Table) {
@@ -5823,331 +5339,53 @@ _MetadataORM_conn = new WeakMap(), _MetadataORM_stmtCache = new WeakMap(), _Meta
5823
5339
 
5824
5340
  "use strict";
5825
5341
 
5826
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5827
- if (k2 === undefined) k2 = k;
5828
- var desc = Object.getOwnPropertyDescriptor(m, k);
5829
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
5830
- desc = { enumerable: true, get: function() { return m[k]; } };
5831
- }
5832
- Object.defineProperty(o, k2, desc);
5833
- }) : (function(o, m, k, k2) {
5834
- if (k2 === undefined) k2 = k;
5835
- o[k2] = m[k];
5836
- }));
5837
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
5838
- Object.defineProperty(o, "default", { enumerable: true, value: v });
5839
- }) : function(o, v) {
5840
- o["default"] = v;
5841
- });
5842
- var __importStar = (this && this.__importStar) || (function () {
5843
- var ownKeys = function(o) {
5844
- ownKeys = Object.getOwnPropertyNames || function (o) {
5845
- var ar = [];
5846
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
5847
- return ar;
5848
- };
5849
- return ownKeys(o);
5850
- };
5851
- return function (mod) {
5852
- if (mod && mod.__esModule) return mod;
5853
- var result = {};
5854
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
5855
- __setModuleDefault(result, mod);
5856
- return result;
5857
- };
5858
- })();
5859
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
5860
- if (kind === "m") throw new TypeError("Private method is not writable");
5861
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5862
- 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");
5863
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
5864
- };
5865
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
5866
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
5867
- 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");
5868
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5869
- };
5870
- var __importDefault = (this && this.__importDefault) || function (mod) {
5871
- return (mod && mod.__esModule) ? mod : { "default": mod };
5872
- };
5873
- var _RekordboxHydrator_orm, _RekordboxHydrator_onProgress;
5342
+ /**
5343
+ * Rekordbox Database Utilities
5344
+ *
5345
+ * Re-exports from the rekordbox/ folder for backward compatibility.
5346
+ * @see ./rekordbox/index.ts for the implementation
5347
+ */
5874
5348
  Object.defineProperty(exports, "__esModule", ({ value: true }));
5875
- exports.hydrateDatabase = hydrateDatabase;
5876
- exports.loadAnlz = loadAnlz;
5877
- const kaitai_struct_1 = __webpack_require__(/*! kaitai-struct */ "kaitai-struct");
5878
- const rekordbox_anlz_ksy_1 = __importDefault(__webpack_require__(/*! src/localdb/kaitai/rekordbox_anlz.ksy */ "./src/localdb/kaitai/rekordbox_anlz.ksy"));
5879
- const rekordbox_pdb_ksy_1 = __importDefault(__webpack_require__(/*! src/localdb/kaitai/rekordbox_pdb.ksy */ "./src/localdb/kaitai/rekordbox_pdb.ksy"));
5880
- const orm_1 = __webpack_require__(/*! src/localdb/orm */ "./src/localdb/orm.ts");
5349
+ exports.loadAnlz = exports.hydrateDatabase = void 0;
5350
+ var index_1 = __webpack_require__(/*! ./rekordbox/index */ "./src/localdb/rekordbox/index.ts");
5351
+ Object.defineProperty(exports, "hydrateDatabase", ({ enumerable: true, get: function () { return index_1.hydrateDatabase; } }));
5352
+ Object.defineProperty(exports, "loadAnlz", ({ enumerable: true, get: function () { return index_1.loadAnlz; } }));
5353
+
5354
+
5355
+ /***/ },
5356
+
5357
+ /***/ "./src/localdb/rekordbox/anlz-parsers.ts"
5358
+ /*!***********************************************!*\
5359
+ !*** ./src/localdb/rekordbox/anlz-parsers.ts ***!
5360
+ \***********************************************/
5361
+ (__unused_webpack_module, exports, __webpack_require__) {
5362
+
5363
+ "use strict";
5364
+
5365
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
5366
+ exports.makeBeatGrid = makeBeatGrid;
5367
+ exports.makeCueAndLoop = makeCueAndLoop;
5368
+ exports.makeWaveformHd = makeWaveformHd;
5369
+ exports.makeExtendedCues = makeExtendedCues;
5370
+ exports.makeSongStructure = makeSongStructure;
5371
+ exports.makeWaveformPreview = makeWaveformPreview;
5372
+ exports.makeWaveform3BandPreview = makeWaveform3BandPreview;
5373
+ exports.makeWaveform3BandDetail = makeWaveform3BandDetail;
5374
+ exports.makeVocalConfig = makeVocalConfig;
5881
5375
  const utils_1 = __webpack_require__(/*! src/localdb/utils */ "./src/localdb/utils.ts");
5882
5376
  const converters_1 = __webpack_require__(/*! src/utils/converters */ "./src/utils/converters.ts");
5883
- const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
5884
5377
  /**
5885
- * Given a rekordbox pdb file contents. This function will hydrate the provided
5886
- * database with all entities from the Rekordbox database. This includes all
5887
- * track metadata, including analyzed metadata (such as beatgrids and waveforms).
5378
+ * Fill beatgrid data from the ANLZ section
5888
5379
  */
5889
- async function hydrateDatabase({ pdbData, span, ...options }) {
5890
- const hydrator = new RekordboxHydrator(options);
5891
- await hydrator.hydrateFromPdb(pdbData, span);
5380
+ function makeBeatGrid(data) {
5381
+ return data.body.beats.map((beat) => ({
5382
+ offset: beat.time,
5383
+ bpm: beat.tempo / 100,
5384
+ count: beat.beatNumber,
5385
+ }));
5892
5386
  }
5893
5387
  /**
5894
- * Loads the ANLZ data of a Track entity from the analyzePath.
5895
- */
5896
- async function loadAnlz(track, type, anlzResolver) {
5897
- const path = `${track.analyzePath}.${type}`;
5898
- const anlzData = await anlzResolver(path);
5899
- const stream = new kaitai_struct_1.KaitaiStream(anlzData);
5900
- const anlz = new rekordbox_anlz_ksy_1.default(stream);
5901
- const result = {};
5902
- const resultDat = result;
5903
- const resultExt = result;
5904
- for (const section of anlz.sections) {
5905
- switch (section.fourcc) {
5906
- case SectionTags.BEAT_GRID:
5907
- resultDat.beatGrid = makeBeatGrid(section);
5908
- break;
5909
- case SectionTags.CUES:
5910
- resultDat.cueAndLoops = makeCueAndLoop(section);
5911
- break;
5912
- case SectionTags.CUES_2:
5913
- resultExt.extendedCues = makeExtendedCues(section);
5914
- break;
5915
- case SectionTags.WAVE_PREVIEW:
5916
- resultDat.waveformPreview = makeWaveformPreview(section);
5917
- break;
5918
- case SectionTags.WAVE_TINY:
5919
- resultDat.waveformTiny = makeWaveformPreview(section);
5920
- break;
5921
- case SectionTags.WAVE_SCROLL:
5922
- resultExt.waveformDetail = Buffer.from(section.body.entries);
5923
- break;
5924
- case SectionTags.WAVE_COLOR_PREVIEW:
5925
- resultExt.waveformColorPreview = Buffer.from(section.body.entries);
5926
- break;
5927
- case SectionTags.WAVE_COLOR_SCROLL:
5928
- resultExt.waveformHd = makeWaveformHd(section);
5929
- break;
5930
- case SectionTags.SONG_STRUCTURE:
5931
- resultExt.songStructure = makeSongStructure(section);
5932
- break;
5933
- // VBR and PATH tags are defined but not currently extracted
5934
- // as they're not commonly needed in the application
5935
- case SectionTags.VBR:
5936
- case SectionTags.PATH:
5937
- break;
5938
- }
5939
- }
5940
- return result;
5941
- }
5942
- /**
5943
- * This service provides utilities for translating rekordbox database (pdb_ and
5944
- * analysis (ANLZ) files into the common entity types used in this library.
5945
- */
5946
- class RekordboxHydrator {
5947
- constructor({ orm, onProgress }) {
5948
- _RekordboxHydrator_orm.set(this, void 0);
5949
- _RekordboxHydrator_onProgress.set(this, void 0);
5950
- __classPrivateFieldSet(this, _RekordboxHydrator_orm, orm, "f");
5951
- __classPrivateFieldSet(this, _RekordboxHydrator_onProgress, onProgress !== null && onProgress !== void 0 ? onProgress : (() => null), "f");
5952
- }
5953
- /**
5954
- * Extract entries from a rekordbox pdb file and hydrate the passed database
5955
- * connection with entities derived from the rekordbox entries.
5956
- */
5957
- async hydrateFromPdb(pdbData, span) {
5958
- const tx = span
5959
- ? span.startChild({ op: 'hydrateFromPdb' })
5960
- : Telemetry.startTransaction({ name: 'hydrateFromPdb' });
5961
- const parseTx = tx.startChild({ op: 'parsePdbData', data: { size: pdbData.length } });
5962
- const stream = new kaitai_struct_1.KaitaiStream(pdbData);
5963
- const db = new rekordbox_pdb_ksy_1.default(stream);
5964
- parseTx.finish();
5965
- const hydrateTx = tx.startChild({ op: 'hydration' });
5966
- await Promise.all(db.tables.map((table) => this.hydrateFromTable(table, hydrateTx)));
5967
- hydrateTx.finish();
5968
- tx.finish();
5969
- }
5970
- /**
5971
- * Hydrate the database with entities from the provided RekordboxPdb table.
5972
- * See pdbEntityCreators for how tables are mapped into database entities.
5973
- */
5974
- async hydrateFromTable(table, span) {
5975
- const tableName = pdbTables[table.type];
5976
- const createObject = pdbEntityCreators[table.type];
5977
- const tx = span.startChild({ op: 'hydrateFromTable', description: tableName });
5978
- if (createObject === undefined) {
5979
- return;
5980
- }
5981
- let totalSaved = 0;
5982
- let totalItems = 0;
5983
- for (const _row of tableRows(table)) {
5984
- void _row; // Intentionally unused - just counting
5985
- totalItems++;
5986
- }
5987
- tx.setData('items', totalItems);
5988
- // Use transaction for bulk inserts (10-100x faster)
5989
- __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").beginTransaction();
5990
- try {
5991
- for (const row of tableRows(table)) {
5992
- const entity = createObject(row);
5993
- __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").insertEntity(tableName, entity);
5994
- totalSaved++;
5995
- // Report progress and yield every 100 rows (instead of every row)
5996
- if (totalSaved % 100 === 0 || totalSaved === totalItems) {
5997
- __classPrivateFieldGet(this, _RekordboxHydrator_onProgress, "f").call(this, { complete: totalSaved, table: tableName, total: totalItems });
5998
- // Yield to event loop periodically to keep UI responsive
5999
- await new Promise(r => setTimeout(r, 0));
6000
- }
6001
- }
6002
- }
6003
- finally {
6004
- __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").commit();
6005
- }
6006
- tx.finish();
6007
- }
6008
- }
6009
- _RekordboxHydrator_orm = new WeakMap(), _RekordboxHydrator_onProgress = new WeakMap();
6010
- /**
6011
- * Utility generator that pages through a table and yields every present row.
6012
- * This flattens the concept of rowGroups and refs.
6013
- */
6014
- function* tableRows(table) {
6015
- const { firstPage, lastPage } = table;
6016
- let pageRef = firstPage;
6017
- do {
6018
- const page = pageRef.body;
6019
- // Adjust our page ref for the next iteration. We do this early in our loop
6020
- // so we can break without having to remember to update for the next iter.
6021
- pageRef = page.nextPage;
6022
- // Ignore non-data pages. Not sure what these are for?
6023
- if (!page.isDataPage) {
6024
- continue;
6025
- }
6026
- const rows = page.rowGroups
6027
- .map((group) => group.rows)
6028
- .flat()
6029
- .filter((row) => row.present);
6030
- for (const row of rows) {
6031
- yield row.body;
6032
- }
6033
- } while (pageRef.index <= lastPage.index);
6034
- }
6035
- const ensureDate = (date) => date instanceof Date && !isNaN(date.valueOf()) ? date : undefined;
6036
- /**
6037
- * Utility to create a hydrator that hydrates the provided entity with the id
6038
- * and name properties from the row.
6039
- */
6040
- const makeIdNameHydrator = () => (row) => {
6041
- var _a;
6042
- return ({
6043
- id: row.id,
6044
- name: (_a = row.name.body.text) !== null && _a !== void 0 ? _a : '',
6045
- });
6046
- };
6047
- /**
6048
- * Translates a pdb track row entry to a {@link Track} entity.
6049
- */
6050
- function createTrack(trackRow) {
6051
- const analyzePath = trackRow.analyzePath.body.text;
6052
- const track = {
6053
- id: trackRow.id,
6054
- title: trackRow.title.body.text,
6055
- trackNumber: trackRow.trackNumber,
6056
- discNumber: trackRow.discNumber,
6057
- duration: trackRow.duration,
6058
- sampleRate: trackRow.sampleRate,
6059
- sampleDepth: trackRow.sampleDepth,
6060
- bitrate: trackRow.bitrate,
6061
- tempo: trackRow.tempo / 100,
6062
- playCount: trackRow.playCount,
6063
- year: trackRow.year,
6064
- rating: trackRow.rating,
6065
- mixName: trackRow.mixName.body.text,
6066
- comment: trackRow.comment.body.text,
6067
- autoloadHotcues: trackRow.autoloadHotcues.body.text === 'ON',
6068
- kuvoPublic: trackRow.kuvoPublic.body.text === 'ON',
6069
- filePath: trackRow.filePath.body.text,
6070
- fileName: trackRow.filename.body.text,
6071
- fileSize: trackRow.fileSize,
6072
- releaseDate: trackRow.releaseDate.body.text,
6073
- analyzeDate: ensureDate(new Date(trackRow.analyzeDate.body.text)),
6074
- dateAdded: ensureDate(new Date(trackRow.dateAdded.body.text)),
6075
- // The analyze file comes in 3 forms
6076
- //
6077
- // 1. A `DAT` file, which is missing some extended information, for the older
6078
- // Pioneer equipment (likely due to memory constraints).
6079
- //
6080
- // 2. A `EXT` file which includes colored waveforms and other extended data.
6081
- //
6082
- // 3. A `EX2` file -- currently unknown
6083
- //
6084
- // We noramlize this path by trimming the DAT extension off. Later we will
6085
- // try and read whatever is available.
6086
- analyzePath: analyzePath === null || analyzePath === void 0 ? void 0 : analyzePath.substring(0, analyzePath.length - 4),
6087
- artworkId: trackRow.artworkId || null,
6088
- artistId: trackRow.artistId || null,
6089
- originalArtistId: trackRow.originalArtistId || null,
6090
- remixerId: trackRow.remixerId || null,
6091
- composerId: trackRow.composerId || null,
6092
- albumId: trackRow.albumId || null,
6093
- labelId: trackRow.labelId || null,
6094
- genreId: trackRow.genreId || null,
6095
- colorId: trackRow.colorId || null,
6096
- keyId: trackRow.keyId || null,
6097
- // NOTE: There are a few additional columns that will be hydrated through
6098
- // the analyze files (given the analyzePath) which we do not assign here.
6099
- beatGrid: null,
6100
- cueAndLoops: null,
6101
- waveformHd: null,
6102
- };
6103
- return track;
6104
- }
6105
- /**
6106
- * Translates a pdb playlist row entry into a {@link Playlist} entity.
6107
- */
6108
- function createPlaylist(playlistRow) {
6109
- const playlist = {
6110
- id: playlistRow.id,
6111
- name: playlistRow.name.body.text,
6112
- isFolder: playlistRow.rawIsFolder !== 0,
6113
- parentId: playlistRow.parentId || null,
6114
- };
6115
- return playlist;
6116
- }
6117
- /**
6118
- * Translates a pdb playlist track entry into a {@link PlaylistTrack} entity.
6119
- */
6120
- function createPlaylistEntry(playlistTrackRow) {
6121
- const entry = {
6122
- id: playlistTrackRow.id,
6123
- sortIndex: playlistTrackRow.entryIndex,
6124
- playlistId: playlistTrackRow.playlistId,
6125
- trackId: playlistTrackRow.trackId,
6126
- };
6127
- return entry;
6128
- }
6129
- /**
6130
- * Translates a pdb artwork entry into a {@link Artwork} entity.
6131
- */
6132
- function createArtworkEntry(artworkRow) {
6133
- const art = {
6134
- id: artworkRow.id,
6135
- path: artworkRow.path.body.text,
6136
- };
6137
- return art;
6138
- }
6139
- /**
6140
- * Fill beatgrid data from the ANLZ section
6141
- */
6142
- function makeBeatGrid(data) {
6143
- return data.body.beats.map((beat) => ({
6144
- offset: beat.time,
6145
- bpm: beat.tempo / 100,
6146
- count: beat.beatNumber,
6147
- }));
6148
- }
6149
- /**
6150
- * Fill cue and loop data from the ANLZ section
5388
+ * Fill cue and loop data from the ANLZ section
6151
5389
  */
6152
5390
  function makeCueAndLoop(data) {
6153
5391
  return data.body.cues.map((entry) => {
@@ -6294,12 +5532,433 @@ function makeWaveformPreview(data) {
6294
5532
  data: Buffer.from(data.body.data),
6295
5533
  };
6296
5534
  }
5535
+ /**
5536
+ * Parse 3-band color waveform preview (PWV6 from .2EX files)
5537
+ */
5538
+ function makeWaveform3BandPreview(data) {
5539
+ return {
5540
+ numEntries: data.body.lenEntries,
5541
+ data: Buffer.from(data.body.entries),
5542
+ };
5543
+ }
5544
+ /**
5545
+ * Parse 3-band color detail waveform (PWV7 from .2EX files)
5546
+ */
5547
+ function makeWaveform3BandDetail(data) {
5548
+ return {
5549
+ numEntries: data.body.lenEntries,
5550
+ data: Buffer.from(data.body.entries),
5551
+ };
5552
+ }
5553
+ /**
5554
+ * Parse vocal detection config (PWVC from .2EX files)
5555
+ */
5556
+ function makeVocalConfig(data) {
5557
+ return {
5558
+ thresholdLow: data.body.thresholdLow,
5559
+ thresholdMid: data.body.thresholdMid,
5560
+ thresholdHigh: data.body.thresholdHigh,
5561
+ };
5562
+ }
5563
+
5564
+
5565
+ /***/ },
5566
+
5567
+ /***/ "./src/localdb/rekordbox/entity-creators.ts"
5568
+ /*!**************************************************!*\
5569
+ !*** ./src/localdb/rekordbox/entity-creators.ts ***!
5570
+ \**************************************************/
5571
+ (__unused_webpack_module, exports) {
5572
+
5573
+ "use strict";
5574
+
5575
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
5576
+ exports.makeIdNameHydrator = void 0;
5577
+ exports.createTrack = createTrack;
5578
+ exports.createPlaylist = createPlaylist;
5579
+ exports.createPlaylistEntry = createPlaylistEntry;
5580
+ exports.createArtworkEntry = createArtworkEntry;
5581
+ const ensureDate = (date) => date instanceof Date && !isNaN(date.valueOf()) ? date : undefined;
5582
+ /**
5583
+ * Utility to create a hydrator that hydrates the provided entity with the id
5584
+ * and name properties from the row.
5585
+ */
5586
+ const makeIdNameHydrator = () => (row) => {
5587
+ var _a;
5588
+ return ({
5589
+ id: row.id,
5590
+ name: (_a = row.name.body.text) !== null && _a !== void 0 ? _a : '',
5591
+ });
5592
+ };
5593
+ exports.makeIdNameHydrator = makeIdNameHydrator;
5594
+ /**
5595
+ * Translates a pdb track row entry to a {@link Track} entity.
5596
+ */
5597
+ function createTrack(trackRow) {
5598
+ const analyzePath = trackRow.analyzePath.body.text;
5599
+ const track = {
5600
+ id: trackRow.id,
5601
+ title: trackRow.title.body.text,
5602
+ trackNumber: trackRow.trackNumber,
5603
+ discNumber: trackRow.discNumber,
5604
+ duration: trackRow.duration,
5605
+ sampleRate: trackRow.sampleRate,
5606
+ sampleDepth: trackRow.sampleDepth,
5607
+ bitrate: trackRow.bitrate,
5608
+ tempo: trackRow.tempo / 100,
5609
+ playCount: trackRow.playCount,
5610
+ year: trackRow.year,
5611
+ rating: trackRow.rating,
5612
+ mixName: trackRow.mixName.body.text,
5613
+ comment: trackRow.comment.body.text,
5614
+ autoloadHotcues: trackRow.autoloadHotcues.body.text === 'ON',
5615
+ kuvoPublic: trackRow.kuvoPublic.body.text === 'ON',
5616
+ filePath: trackRow.filePath.body.text,
5617
+ fileName: trackRow.filename.body.text,
5618
+ fileSize: trackRow.fileSize,
5619
+ releaseDate: trackRow.releaseDate.body.text,
5620
+ analyzeDate: ensureDate(new Date(trackRow.analyzeDate.body.text)),
5621
+ dateAdded: ensureDate(new Date(trackRow.dateAdded.body.text)),
5622
+ // The analyze file comes in 3 forms
5623
+ //
5624
+ // 1. A `DAT` file, which is missing some extended information, for the older
5625
+ // Pioneer equipment (likely due to memory constraints).
5626
+ //
5627
+ // 2. A `EXT` file which includes colored waveforms and other extended data.
5628
+ //
5629
+ // 3. A `EX2` file -- currently unknown
5630
+ //
5631
+ // We noramlize this path by trimming the DAT extension off. Later we will
5632
+ // try and read whatever is available.
5633
+ analyzePath: analyzePath === null || analyzePath === void 0 ? void 0 : analyzePath.substring(0, analyzePath.length - 4),
5634
+ artworkId: trackRow.artworkId || null,
5635
+ artistId: trackRow.artistId || null,
5636
+ originalArtistId: trackRow.originalArtistId || null,
5637
+ remixerId: trackRow.remixerId || null,
5638
+ composerId: trackRow.composerId || null,
5639
+ albumId: trackRow.albumId || null,
5640
+ labelId: trackRow.labelId || null,
5641
+ genreId: trackRow.genreId || null,
5642
+ colorId: trackRow.colorId || null,
5643
+ keyId: trackRow.keyId || null,
5644
+ // NOTE: There are a few additional columns that will be hydrated through
5645
+ // the analyze files (given the analyzePath) which we do not assign here.
5646
+ beatGrid: null,
5647
+ cueAndLoops: null,
5648
+ waveformHd: null,
5649
+ };
5650
+ return track;
5651
+ }
5652
+ /**
5653
+ * Translates a pdb playlist row entry into a {@link Playlist} entity.
5654
+ */
5655
+ function createPlaylist(playlistRow) {
5656
+ const playlist = {
5657
+ id: playlistRow.id,
5658
+ name: playlistRow.name.body.text,
5659
+ isFolder: playlistRow.rawIsFolder !== 0,
5660
+ parentId: playlistRow.parentId || null,
5661
+ };
5662
+ return playlist;
5663
+ }
5664
+ /**
5665
+ * Translates a pdb playlist track entry into a {@link PlaylistTrack} entity.
5666
+ */
5667
+ function createPlaylistEntry(playlistTrackRow) {
5668
+ const entry = {
5669
+ id: playlistTrackRow.id,
5670
+ sortIndex: playlistTrackRow.entryIndex,
5671
+ playlistId: playlistTrackRow.playlistId,
5672
+ trackId: playlistTrackRow.trackId,
5673
+ };
5674
+ return entry;
5675
+ }
5676
+ /**
5677
+ * Translates a pdb artwork entry into a {@link Artwork} entity.
5678
+ */
5679
+ function createArtworkEntry(artworkRow) {
5680
+ const art = {
5681
+ id: artworkRow.id,
5682
+ path: artworkRow.path.body.text,
5683
+ };
5684
+ return art;
5685
+ }
5686
+
5687
+
5688
+ /***/ },
5689
+
5690
+ /***/ "./src/localdb/rekordbox/hydrator.ts"
5691
+ /*!*******************************************!*\
5692
+ !*** ./src/localdb/rekordbox/hydrator.ts ***!
5693
+ \*******************************************/
5694
+ (__unused_webpack_module, exports, __webpack_require__) {
5695
+
5696
+ "use strict";
5697
+
5698
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5699
+ if (k2 === undefined) k2 = k;
5700
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5701
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
5702
+ desc = { enumerable: true, get: function() { return m[k]; } };
5703
+ }
5704
+ Object.defineProperty(o, k2, desc);
5705
+ }) : (function(o, m, k, k2) {
5706
+ if (k2 === undefined) k2 = k;
5707
+ o[k2] = m[k];
5708
+ }));
5709
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
5710
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
5711
+ }) : function(o, v) {
5712
+ o["default"] = v;
5713
+ });
5714
+ var __importStar = (this && this.__importStar) || (function () {
5715
+ var ownKeys = function(o) {
5716
+ ownKeys = Object.getOwnPropertyNames || function (o) {
5717
+ var ar = [];
5718
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
5719
+ return ar;
5720
+ };
5721
+ return ownKeys(o);
5722
+ };
5723
+ return function (mod) {
5724
+ if (mod && mod.__esModule) return mod;
5725
+ var result = {};
5726
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
5727
+ __setModuleDefault(result, mod);
5728
+ return result;
5729
+ };
5730
+ })();
5731
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
5732
+ if (kind === "m") throw new TypeError("Private method is not writable");
5733
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5734
+ 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");
5735
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
5736
+ };
5737
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
5738
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
5739
+ 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");
5740
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5741
+ };
5742
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5743
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5744
+ };
5745
+ var _RekordboxHydrator_orm, _RekordboxHydrator_onProgress;
5746
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
5747
+ exports.RekordboxHydrator = void 0;
5748
+ const kaitai_struct_1 = __webpack_require__(/*! kaitai-struct */ "kaitai-struct");
5749
+ const rekordbox_pdb_ksy_1 = __importDefault(__webpack_require__(/*! src/localdb/kaitai/rekordbox_pdb.ksy */ "./src/localdb/kaitai/rekordbox_pdb.ksy"));
5750
+ const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
5751
+ const table_mappings_1 = __webpack_require__(/*! ./table-mappings */ "./src/localdb/rekordbox/table-mappings.ts");
5752
+ /**
5753
+ * This service provides utilities for translating rekordbox database (pdb_ and
5754
+ * analysis (ANLZ) files into the common entity types used in this library.
5755
+ */
5756
+ class RekordboxHydrator {
5757
+ constructor({ orm, onProgress }) {
5758
+ _RekordboxHydrator_orm.set(this, void 0);
5759
+ _RekordboxHydrator_onProgress.set(this, void 0);
5760
+ __classPrivateFieldSet(this, _RekordboxHydrator_orm, orm, "f");
5761
+ __classPrivateFieldSet(this, _RekordboxHydrator_onProgress, onProgress !== null && onProgress !== void 0 ? onProgress : (() => null), "f");
5762
+ }
5763
+ /**
5764
+ * Extract entries from a rekordbox pdb file and hydrate the passed database
5765
+ * connection with entities derived from the rekordbox entries.
5766
+ */
5767
+ async hydrateFromPdb(pdbData, span) {
5768
+ const tx = span
5769
+ ? span.startChild({ op: 'hydrateFromPdb' })
5770
+ : Telemetry.startTransaction({ name: 'hydrateFromPdb' });
5771
+ const parseTx = tx.startChild({ op: 'parsePdbData', data: { size: pdbData.length } });
5772
+ const stream = new kaitai_struct_1.KaitaiStream(pdbData);
5773
+ const db = new rekordbox_pdb_ksy_1.default(stream);
5774
+ parseTx.finish();
5775
+ const hydrateTx = tx.startChild({ op: 'hydration' });
5776
+ await Promise.all(db.tables.map((table) => this.hydrateFromTable(table, hydrateTx)));
5777
+ hydrateTx.finish();
5778
+ tx.finish();
5779
+ }
5780
+ /**
5781
+ * Hydrate the database with entities from the provided RekordboxPdb table.
5782
+ * See pdbEntityCreators for how tables are mapped into database entities.
5783
+ */
5784
+ async hydrateFromTable(table, span) {
5785
+ const tableName = table_mappings_1.pdbTables[table.type];
5786
+ const createObject = table_mappings_1.pdbEntityCreators[table.type];
5787
+ const tx = span.startChild({ op: 'hydrateFromTable', description: tableName });
5788
+ if (createObject === undefined) {
5789
+ return;
5790
+ }
5791
+ let totalSaved = 0;
5792
+ let totalItems = 0;
5793
+ for (const _row of tableRows(table)) {
5794
+ void _row; // Intentionally unused - just counting
5795
+ totalItems++;
5796
+ }
5797
+ tx.setData('items', totalItems);
5798
+ // Use transaction for bulk inserts (10-100x faster)
5799
+ __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").beginTransaction();
5800
+ try {
5801
+ for (const row of tableRows(table)) {
5802
+ const entity = createObject(row);
5803
+ __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").insertEntity(tableName, entity);
5804
+ totalSaved++;
5805
+ // Report progress and yield every 100 rows (instead of every row)
5806
+ if (totalSaved % 100 === 0 || totalSaved === totalItems) {
5807
+ __classPrivateFieldGet(this, _RekordboxHydrator_onProgress, "f").call(this, { complete: totalSaved, table: tableName, total: totalItems });
5808
+ // Yield to event loop periodically to keep UI responsive
5809
+ await new Promise(r => setTimeout(r, 0));
5810
+ }
5811
+ }
5812
+ }
5813
+ finally {
5814
+ __classPrivateFieldGet(this, _RekordboxHydrator_orm, "f").commit();
5815
+ }
5816
+ tx.finish();
5817
+ }
5818
+ }
5819
+ exports.RekordboxHydrator = RekordboxHydrator;
5820
+ _RekordboxHydrator_orm = new WeakMap(), _RekordboxHydrator_onProgress = new WeakMap();
5821
+ /**
5822
+ * Utility generator that pages through a table and yields every present row.
5823
+ * This flattens the concept of rowGroups and refs.
5824
+ */
5825
+ function* tableRows(table) {
5826
+ const { firstPage, lastPage } = table;
5827
+ let pageRef = firstPage;
5828
+ do {
5829
+ const page = pageRef.body;
5830
+ // Adjust our page ref for the next iteration. We do this early in our loop
5831
+ // so we can break without having to remember to update for the next iter.
5832
+ pageRef = page.nextPage;
5833
+ // Ignore non-data pages. Not sure what these are for?
5834
+ if (!page.isDataPage) {
5835
+ continue;
5836
+ }
5837
+ const rows = page.rowGroups
5838
+ .map((group) => group.rows)
5839
+ .flat()
5840
+ .filter((row) => row.present);
5841
+ for (const row of rows) {
5842
+ yield row.body;
5843
+ }
5844
+ } while (pageRef.index <= lastPage.index);
5845
+ }
5846
+
5847
+
5848
+ /***/ },
5849
+
5850
+ /***/ "./src/localdb/rekordbox/index.ts"
5851
+ /*!****************************************!*\
5852
+ !*** ./src/localdb/rekordbox/index.ts ***!
5853
+ \****************************************/
5854
+ (__unused_webpack_module, exports, __webpack_require__) {
5855
+
5856
+ "use strict";
5857
+
5858
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5859
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5860
+ };
5861
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
5862
+ exports.hydrateDatabase = hydrateDatabase;
5863
+ exports.loadAnlz = loadAnlz;
5864
+ const kaitai_struct_1 = __webpack_require__(/*! kaitai-struct */ "kaitai-struct");
5865
+ const rekordbox_anlz_ksy_1 = __importDefault(__webpack_require__(/*! src/localdb/kaitai/rekordbox_anlz.ksy */ "./src/localdb/kaitai/rekordbox_anlz.ksy"));
5866
+ const anlz_parsers_1 = __webpack_require__(/*! ./anlz-parsers */ "./src/localdb/rekordbox/anlz-parsers.ts");
5867
+ const hydrator_1 = __webpack_require__(/*! ./hydrator */ "./src/localdb/rekordbox/hydrator.ts");
5868
+ const { SectionTags } = rekordbox_anlz_ksy_1.default;
5869
+ /**
5870
+ * Given a rekordbox pdb file contents. This function will hydrate the provided
5871
+ * database with all entities from the Rekordbox database. This includes all
5872
+ * track metadata, including analyzed metadata (such as beatgrids and waveforms).
5873
+ */
5874
+ async function hydrateDatabase({ pdbData, span, ...options }) {
5875
+ const hydrator = new hydrator_1.RekordboxHydrator(options);
5876
+ await hydrator.hydrateFromPdb(pdbData, span);
5877
+ }
5878
+ /**
5879
+ * Loads the ANLZ data of a Track entity from the analyzePath.
5880
+ */
5881
+ async function loadAnlz(track, type, anlzResolver) {
5882
+ const path = `${track.analyzePath}.${type}`;
5883
+ const anlzData = await anlzResolver(path);
5884
+ const stream = new kaitai_struct_1.KaitaiStream(anlzData);
5885
+ const anlz = new rekordbox_anlz_ksy_1.default(stream);
5886
+ const result = {};
5887
+ const resultDat = result;
5888
+ const resultExt = result;
5889
+ const result2ex = result;
5890
+ for (const section of anlz.sections) {
5891
+ switch (section.fourcc) {
5892
+ case SectionTags.BEAT_GRID:
5893
+ resultDat.beatGrid = (0, anlz_parsers_1.makeBeatGrid)(section);
5894
+ break;
5895
+ case SectionTags.CUES:
5896
+ resultDat.cueAndLoops = (0, anlz_parsers_1.makeCueAndLoop)(section);
5897
+ break;
5898
+ case SectionTags.CUES_2:
5899
+ resultExt.extendedCues = (0, anlz_parsers_1.makeExtendedCues)(section);
5900
+ break;
5901
+ case SectionTags.WAVE_PREVIEW:
5902
+ resultDat.waveformPreview = (0, anlz_parsers_1.makeWaveformPreview)(section);
5903
+ break;
5904
+ case SectionTags.WAVE_TINY:
5905
+ resultDat.waveformTiny = (0, anlz_parsers_1.makeWaveformPreview)(section);
5906
+ break;
5907
+ case SectionTags.WAVE_SCROLL:
5908
+ resultExt.waveformDetail = Buffer.from(section.body.entries);
5909
+ break;
5910
+ case SectionTags.WAVE_COLOR_PREVIEW:
5911
+ resultExt.waveformColorPreview = Buffer.from(section.body.entries);
5912
+ break;
5913
+ case SectionTags.WAVE_COLOR_SCROLL:
5914
+ resultExt.waveformHd = (0, anlz_parsers_1.makeWaveformHd)(section);
5915
+ break;
5916
+ case SectionTags.SONG_STRUCTURE:
5917
+ resultExt.songStructure = (0, anlz_parsers_1.makeSongStructure)(section);
5918
+ break;
5919
+ case SectionTags.WAVE_COLOR_3BAND_PREVIEW:
5920
+ result2ex.waveform3BandPreview = (0, anlz_parsers_1.makeWaveform3BandPreview)(section);
5921
+ break;
5922
+ case SectionTags.WAVE_COLOR_3BAND_DETAIL:
5923
+ result2ex.waveform3BandDetail = (0, anlz_parsers_1.makeWaveform3BandDetail)(section);
5924
+ break;
5925
+ case SectionTags.VOCAL_CONFIG:
5926
+ result2ex.vocalConfig = (0, anlz_parsers_1.makeVocalConfig)(section);
5927
+ break;
5928
+ // VBR and PATH tags are defined but not currently extracted
5929
+ // as they're not commonly needed in the application
5930
+ case SectionTags.VBR:
5931
+ case SectionTags.PATH:
5932
+ break;
5933
+ }
5934
+ }
5935
+ return result;
5936
+ }
5937
+
5938
+
5939
+ /***/ },
5940
+
5941
+ /***/ "./src/localdb/rekordbox/table-mappings.ts"
5942
+ /*!*************************************************!*\
5943
+ !*** ./src/localdb/rekordbox/table-mappings.ts ***!
5944
+ \*************************************************/
5945
+ (__unused_webpack_module, exports, __webpack_require__) {
5946
+
5947
+ "use strict";
5948
+
5949
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5950
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5951
+ };
5952
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
5953
+ exports.pdbEntityCreators = exports.pdbTables = void 0;
5954
+ const rekordbox_pdb_ksy_1 = __importDefault(__webpack_require__(/*! src/localdb/kaitai/rekordbox_pdb.ksy */ "./src/localdb/kaitai/rekordbox_pdb.ksy"));
5955
+ const orm_1 = __webpack_require__(/*! src/localdb/orm */ "./src/localdb/orm.ts");
5956
+ const entity_creators_1 = __webpack_require__(/*! ./entity-creators */ "./src/localdb/rekordbox/entity-creators.ts");
6297
5957
  const { PageType } = rekordbox_pdb_ksy_1.default;
6298
- const { SectionTags } = rekordbox_anlz_ksy_1.default;
6299
5958
  /**
6300
5959
  * Maps rekordbox pdb table types to orm table names.
6301
5960
  */
6302
- const pdbTables = {
5961
+ exports.pdbTables = {
6303
5962
  [PageType.TRACKS]: orm_1.Table.Track,
6304
5963
  [PageType.ARTISTS]: orm_1.Table.Artist,
6305
5964
  [PageType.GENRES]: orm_1.Table.Genre,
@@ -6315,17 +5974,17 @@ const pdbTables = {
6315
5974
  * Maps rekordbox pdb table types to functions that create entity objects for
6316
5975
  * the passed pdb row.
6317
5976
  */
6318
- const pdbEntityCreators = {
6319
- [PageType.TRACKS]: createTrack,
6320
- [PageType.ARTISTS]: makeIdNameHydrator(),
6321
- [PageType.GENRES]: makeIdNameHydrator(),
6322
- [PageType.ALBUMS]: makeIdNameHydrator(),
6323
- [PageType.LABELS]: makeIdNameHydrator(),
6324
- [PageType.COLORS]: makeIdNameHydrator(),
6325
- [PageType.KEYS]: makeIdNameHydrator(),
6326
- [PageType.ARTWORK]: createArtworkEntry,
6327
- [PageType.PLAYLIST_TREE]: createPlaylist,
6328
- [PageType.PLAYLIST_ENTRIES]: createPlaylistEntry,
5977
+ exports.pdbEntityCreators = {
5978
+ [PageType.TRACKS]: entity_creators_1.createTrack,
5979
+ [PageType.ARTISTS]: (0, entity_creators_1.makeIdNameHydrator)(),
5980
+ [PageType.GENRES]: (0, entity_creators_1.makeIdNameHydrator)(),
5981
+ [PageType.ALBUMS]: (0, entity_creators_1.makeIdNameHydrator)(),
5982
+ [PageType.LABELS]: (0, entity_creators_1.makeIdNameHydrator)(),
5983
+ [PageType.COLORS]: (0, entity_creators_1.makeIdNameHydrator)(),
5984
+ [PageType.KEYS]: (0, entity_creators_1.makeIdNameHydrator)(),
5985
+ [PageType.ARTWORK]: entity_creators_1.createArtworkEntry,
5986
+ [PageType.PLAYLIST_TREE]: entity_creators_1.createPlaylist,
5987
+ [PageType.PLAYLIST_ENTRIES]: entity_creators_1.createPlaylistEntry,
6329
5988
  // TODO: Register PageType.HISTORY
6330
5989
  };
6331
5990
 
@@ -6466,6 +6125,110 @@ const makeCueLoopEntry = (isCue, isLoop, offset, length, button) => button !== f
6466
6125
  exports.makeCueLoopEntry = makeCueLoopEntry;
6467
6126
 
6468
6127
 
6128
+ /***/ },
6129
+
6130
+ /***/ "./src/logger.ts"
6131
+ /*!***********************!*\
6132
+ !*** ./src/logger.ts ***!
6133
+ \***********************/
6134
+ (__unused_webpack_module, exports) {
6135
+
6136
+ "use strict";
6137
+
6138
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6139
+ exports.noopLogger = void 0;
6140
+ exports.noopLogger = {
6141
+ trace() { },
6142
+ debug() { },
6143
+ info() { },
6144
+ warn() { },
6145
+ error() { },
6146
+ };
6147
+
6148
+
6149
+ /***/ },
6150
+
6151
+ /***/ "./src/metadata.ts"
6152
+ /*!*************************!*\
6153
+ !*** ./src/metadata.ts ***!
6154
+ \*************************/
6155
+ (__unused_webpack_module, exports, __webpack_require__) {
6156
+
6157
+ "use strict";
6158
+
6159
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6160
+ exports.extractFullMetadata = extractFullMetadata;
6161
+ exports.isMetadataExtractionSupported = isMetadataExtractionSupported;
6162
+ exports.extractMetadataFromDevice = extractMetadataFromDevice;
6163
+ const metadata_connect_1 = __webpack_require__(/*! metadata-connect */ "metadata-connect");
6164
+ const nfs_1 = __webpack_require__(/*! ./nfs */ "./src/nfs/index.ts");
6165
+ const reader_1 = __webpack_require__(/*! ./artwork/reader */ "./src/artwork/reader.ts");
6166
+ /**
6167
+ * Extract full metadata from an audio file using a FileReader.
6168
+ *
6169
+ * This is the low-level API that works with any FileReader implementation.
6170
+ * For extracting from a Pro DJ Link device, use extractMetadataFromDevice.
6171
+ *
6172
+ * @param reader - FileReader interface for reading file data
6173
+ * @returns Extracted metadata, or null if extraction fails
6174
+ */
6175
+ async function extractFullMetadata(reader) {
6176
+ return (0, metadata_connect_1.extractMetadata)(reader);
6177
+ }
6178
+ /**
6179
+ * Check if metadata extraction is supported for a file extension.
6180
+ *
6181
+ * @param extension - File extension (with or without leading dot)
6182
+ * @returns true if the extension is supported
6183
+ */
6184
+ function isMetadataExtractionSupported(extension) {
6185
+ return (0, metadata_connect_1.isExtensionSupported)(extension);
6186
+ }
6187
+ /**
6188
+ * Extract full metadata from an audio file on a Pro DJ Link device.
6189
+ *
6190
+ * This function reads only the necessary bytes from the file header,
6191
+ * avoiding the need to transfer entire audio files over the network.
6192
+ *
6193
+ * @param device - The Pro DJ Link device
6194
+ * @param slot - The media slot containing the file (USB, SD, etc.)
6195
+ * @param filePath - The file path on the device
6196
+ * @returns Extracted metadata, or null if extraction fails
6197
+ *
6198
+ * @example
6199
+ * ```typescript
6200
+ * // Extract metadata when a track is loaded
6201
+ * const metadata = await extractMetadataFromDevice(device, slot, filePath);
6202
+ * if (metadata) {
6203
+ * console.log(metadata.title, metadata.artist, metadata.bpm);
6204
+ * if (metadata.artwork) {
6205
+ * // Display artwork
6206
+ * }
6207
+ * }
6208
+ * ```
6209
+ */
6210
+ async function extractMetadataFromDevice(device, slot, filePath) {
6211
+ var _a, _b;
6212
+ // Get file extension and check if supported
6213
+ const extension = (_b = (_a = filePath.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
6214
+ if (!(0, metadata_connect_1.isExtensionSupported)(extension)) {
6215
+ return null;
6216
+ }
6217
+ try {
6218
+ const fileInfo = await (0, nfs_1.getFileInfo)({ device, slot, path: filePath });
6219
+ if (fileInfo.size === 0) {
6220
+ return null;
6221
+ }
6222
+ const reader = (0, reader_1.createNfsFileReader)(device, slot, filePath, fileInfo.size);
6223
+ return (0, metadata_connect_1.extractMetadata)(reader);
6224
+ }
6225
+ catch {
6226
+ // Return null on any error (file not found, network issues, etc.)
6227
+ return null;
6228
+ }
6229
+ }
6230
+
6231
+
6469
6232
  /***/ },
6470
6233
 
6471
6234
  /***/ "./src/mixstatus/index.ts"
@@ -6487,7 +6250,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
6487
6250
  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");
6488
6251
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6489
6252
  };
6490
- var _MixstatusProcessor_emitter, _MixstatusProcessor_lastState, _MixstatusProcessor_lastStartTime, _MixstatusProcessor_lastStoppedTimes, _MixstatusProcessor_livePlayers, _MixstatusProcessor_isSetActive, _MixstatusProcessor_cancelSetEnding, _MixstatusProcessor_config, _MixstatusProcessor_onAir, _MixstatusProcessor_promotePlayer, _MixstatusProcessor_promoteNextPlayer, _MixstatusProcessor_markPlayerStopped, _MixstatusProcessor_setMayStop, _MixstatusProcessor_playerMayBeFirst, _MixstatusProcessor_playerMayStop, _MixstatusProcessor_handlePlaystateChange, _MixstatusProcessor_handleOnairChange;
6253
+ var _MixstatusProcessor_emitter, _MixstatusProcessor_lastState, _MixstatusProcessor_lastStartTime, _MixstatusProcessor_lastStoppedTimes, _MixstatusProcessor_livePlayers, _MixstatusProcessor_lastEmittedTrackId, _MixstatusProcessor_isSetActive, _MixstatusProcessor_cancelSetEnding, _MixstatusProcessor_config, _MixstatusProcessor_onAir, _MixstatusProcessor_promotePlayer, _MixstatusProcessor_promoteNextPlayer, _MixstatusProcessor_markPlayerStopped, _MixstatusProcessor_setMayStop, _MixstatusProcessor_playerMayBeFirst, _MixstatusProcessor_playerMayStop, _MixstatusProcessor_handlePlaystateChange, _MixstatusProcessor_handleOnairChange;
6491
6254
  Object.defineProperty(exports, "__esModule", ({ value: true }));
6492
6255
  exports.MixstatusProcessor = void 0;
6493
6256
  const events_1 = __webpack_require__(/*! events */ "events");
@@ -6543,6 +6306,14 @@ class MixstatusProcessor {
6543
6306
  * Records which players have been reported as 'live'
6544
6307
  */
6545
6308
  _MixstatusProcessor_livePlayers.set(this, new Set());
6309
+ /**
6310
+ * Records the last trackId emitted as 'nowPlaying' for each device. Used to
6311
+ * suppress duplicate emissions when a deck is briefly cued/loaded and then
6312
+ * resumed on the same track (a common pattern for House/Techno DJs who
6313
+ * cue-juggle mid-track). The entry is replaced when a different trackId is
6314
+ * emitted on the same deck, so subsequent track loads still emit normally.
6315
+ */
6316
+ _MixstatusProcessor_lastEmittedTrackId.set(this, new Map());
6546
6317
  /**
6547
6318
  * Incidates if we're currentiny in an active DJ set
6548
6319
  */
@@ -6577,6 +6348,14 @@ class MixstatusProcessor {
6577
6348
  if (__classPrivateFieldGet(this, _MixstatusProcessor_livePlayers, "f").has(deviceId)) {
6578
6349
  return;
6579
6350
  }
6351
+ // Suppress duplicate nowPlaying for the same track on the same deck. This
6352
+ // can happen when a DJ cues mid-track and resumes (Cued/Loading/Ended are
6353
+ // all stoppingStates that drop the deck from livePlayers, so the next play
6354
+ // of the same trackId would otherwise re-emit). When a different track is
6355
+ // loaded, the trackId differs and the new emission proceeds normally.
6356
+ if (__classPrivateFieldGet(this, _MixstatusProcessor_lastEmittedTrackId, "f").get(deviceId) === state.trackId) {
6357
+ return;
6358
+ }
6580
6359
  if (!__classPrivateFieldGet(this, _MixstatusProcessor_isSetActive, "f")) {
6581
6360
  __classPrivateFieldSet(this, _MixstatusProcessor_isSetActive, true, "f");
6582
6361
  __classPrivateFieldGet(this, _MixstatusProcessor_emitter, "f").emit('setStarted');
@@ -6585,6 +6364,7 @@ class MixstatusProcessor {
6585
6364
  __classPrivateFieldGet(this, _MixstatusProcessor_cancelSetEnding, "f").call(this);
6586
6365
  }
6587
6366
  __classPrivateFieldGet(this, _MixstatusProcessor_livePlayers, "f").add(deviceId);
6367
+ __classPrivateFieldGet(this, _MixstatusProcessor_lastEmittedTrackId, "f").set(deviceId, state.trackId);
6588
6368
  __classPrivateFieldGet(this, _MixstatusProcessor_emitter, "f").emit('nowPlaying', state);
6589
6369
  });
6590
6370
  /**
@@ -6778,7 +6558,7 @@ class MixstatusProcessor {
6778
6558
  }
6779
6559
  }
6780
6560
  exports.MixstatusProcessor = MixstatusProcessor;
6781
- _MixstatusProcessor_emitter = new WeakMap(), _MixstatusProcessor_lastState = new WeakMap(), _MixstatusProcessor_lastStartTime = new WeakMap(), _MixstatusProcessor_lastStoppedTimes = new WeakMap(), _MixstatusProcessor_livePlayers = new WeakMap(), _MixstatusProcessor_isSetActive = new WeakMap(), _MixstatusProcessor_cancelSetEnding = new WeakMap(), _MixstatusProcessor_config = new WeakMap(), _MixstatusProcessor_onAir = new WeakMap(), _MixstatusProcessor_promotePlayer = new WeakMap(), _MixstatusProcessor_promoteNextPlayer = new WeakMap(), _MixstatusProcessor_markPlayerStopped = new WeakMap(), _MixstatusProcessor_setMayStop = new WeakMap(), _MixstatusProcessor_playerMayBeFirst = new WeakMap(), _MixstatusProcessor_playerMayStop = new WeakMap(), _MixstatusProcessor_handlePlaystateChange = new WeakMap(), _MixstatusProcessor_handleOnairChange = new WeakMap();
6561
+ _MixstatusProcessor_emitter = new WeakMap(), _MixstatusProcessor_lastState = new WeakMap(), _MixstatusProcessor_lastStartTime = new WeakMap(), _MixstatusProcessor_lastStoppedTimes = new WeakMap(), _MixstatusProcessor_livePlayers = new WeakMap(), _MixstatusProcessor_lastEmittedTrackId = new WeakMap(), _MixstatusProcessor_isSetActive = new WeakMap(), _MixstatusProcessor_cancelSetEnding = new WeakMap(), _MixstatusProcessor_config = new WeakMap(), _MixstatusProcessor_onAir = new WeakMap(), _MixstatusProcessor_promotePlayer = new WeakMap(), _MixstatusProcessor_promoteNextPlayer = new WeakMap(), _MixstatusProcessor_markPlayerStopped = new WeakMap(), _MixstatusProcessor_setMayStop = new WeakMap(), _MixstatusProcessor_playerMayBeFirst = new WeakMap(), _MixstatusProcessor_playerMayStop = new WeakMap(), _MixstatusProcessor_handlePlaystateChange = new WeakMap(), _MixstatusProcessor_handleOnairChange = new WeakMap();
6782
6562
 
6783
6563
 
6784
6564
  /***/ },
@@ -6869,7 +6649,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
6869
6649
  var __importDefault = (this && this.__importDefault) || function (mod) {
6870
6650
  return (mod && mod.__esModule) ? mod : { "default": mod };
6871
6651
  };
6872
- var _ProlinkNetwork_state, _ProlinkNetwork_announceSocket, _ProlinkNetwork_beatSocket, _ProlinkNetwork_statusSocket, _ProlinkNetwork_deviceManager, _ProlinkNetwork_statusEmitter, _ProlinkNetwork_positionEmitter, _ProlinkNetwork_config, _ProlinkNetwork_connection, _ProlinkNetwork_mixstatus;
6652
+ var _ProlinkNetwork_state, _ProlinkNetwork_announceSocket, _ProlinkNetwork_beatSocket, _ProlinkNetwork_statusSocket, _ProlinkNetwork_deviceManager, _ProlinkNetwork_statusEmitter, _ProlinkNetwork_positionEmitter, _ProlinkNetwork_logger, _ProlinkNetwork_config, _ProlinkNetwork_connection, _ProlinkNetwork_mixstatus;
6873
6653
  Object.defineProperty(exports, "__esModule", ({ value: true }));
6874
6654
  exports.ProlinkNetwork = void 0;
6875
6655
  exports.bringOnline = bringOnline;
@@ -6879,6 +6659,7 @@ const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts
6879
6659
  const control_1 = __importDefault(__webpack_require__(/*! src/control */ "./src/control/index.ts"));
6880
6660
  const db_1 = __importDefault(__webpack_require__(/*! src/db */ "./src/db/index.ts"));
6881
6661
  const devices_1 = __importDefault(__webpack_require__(/*! src/devices */ "./src/devices/index.ts"));
6662
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
6882
6663
  const localdb_1 = __importDefault(__webpack_require__(/*! src/localdb */ "./src/localdb/index.ts"));
6883
6664
  const mixstatus_1 = __webpack_require__(/*! src/mixstatus */ "./src/mixstatus/index.ts");
6884
6665
  const remotedb_1 = __importDefault(__webpack_require__(/*! src/remotedb */ "./src/remotedb/index.ts"));
@@ -6909,6 +6690,9 @@ async function bringOnline(config) {
6909
6690
  await (0, udp_1.udpBind)(announceSocket, constants_1.ANNOUNCE_PORT, '0.0.0.0');
6910
6691
  await (0, udp_1.udpBind)(beatSocket, constants_1.BEAT_PORT, '0.0.0.0');
6911
6692
  await (0, udp_1.udpBind)(statusSocket, constants_1.STATUS_PORT, '0.0.0.0');
6693
+ // Enable broadcast so the announcer can reach the whole subnet before any
6694
+ // devices have been discovered.
6695
+ announceSocket.setBroadcast(true);
6912
6696
  }
6913
6697
  catch (err) {
6914
6698
  Telemetry.captureException(err);
@@ -6936,6 +6720,7 @@ class ProlinkNetwork {
6936
6720
  * @internal
6937
6721
  */
6938
6722
  constructor({ config, announceSocket, beatSocket, statusSocket, deviceManager, statusEmitter, positionEmitter, }) {
6723
+ var _a;
6939
6724
  _ProlinkNetwork_state.set(this, types_1.NetworkState.Online);
6940
6725
  _ProlinkNetwork_announceSocket.set(this, void 0);
6941
6726
  _ProlinkNetwork_beatSocket.set(this, void 0);
@@ -6943,10 +6728,12 @@ class ProlinkNetwork {
6943
6728
  _ProlinkNetwork_deviceManager.set(this, void 0);
6944
6729
  _ProlinkNetwork_statusEmitter.set(this, void 0);
6945
6730
  _ProlinkNetwork_positionEmitter.set(this, void 0);
6731
+ _ProlinkNetwork_logger.set(this, void 0);
6946
6732
  _ProlinkNetwork_config.set(this, void 0);
6947
6733
  _ProlinkNetwork_connection.set(this, void 0);
6948
6734
  _ProlinkNetwork_mixstatus.set(this, void 0);
6949
6735
  __classPrivateFieldSet(this, _ProlinkNetwork_config, config !== null && config !== void 0 ? config : null, "f");
6736
+ __classPrivateFieldSet(this, _ProlinkNetwork_logger, (_a = config === null || config === void 0 ? void 0 : config.logger) !== null && _a !== void 0 ? _a : logger_1.noopLogger, "f");
6950
6737
  __classPrivateFieldSet(this, _ProlinkNetwork_announceSocket, announceSocket, "f");
6951
6738
  __classPrivateFieldSet(this, _ProlinkNetwork_beatSocket, beatSocket, "f");
6952
6739
  __classPrivateFieldSet(this, _ProlinkNetwork_statusSocket, statusSocket, "f");
@@ -6966,6 +6753,9 @@ class ProlinkNetwork {
6966
6753
  */
6967
6754
  configure(config) {
6968
6755
  __classPrivateFieldSet(this, _ProlinkNetwork_config, { ...__classPrivateFieldGet(this, _ProlinkNetwork_config, "f"), ...config }, "f");
6756
+ if (config.logger !== undefined) {
6757
+ __classPrivateFieldSet(this, _ProlinkNetwork_logger, config.logger, "f");
6758
+ }
6969
6759
  }
6970
6760
  /**
6971
6761
  * Wait for another device to show up on the network to determine which network
@@ -6999,7 +6789,7 @@ class ProlinkNetwork {
6999
6789
  * or manual configuration). This will then initialize all the network services.
7000
6790
  */
7001
6791
  connect() {
7002
- var _a, _b;
6792
+ var _a, _b, _c;
7003
6793
  if (__classPrivateFieldGet(this, _ProlinkNetwork_config, "f") === null) {
7004
6794
  throw new Error(connectErrorHelp);
7005
6795
  }
@@ -7011,13 +6801,13 @@ class ProlinkNetwork {
7011
6801
  __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f").reconfigure({ vcdjName: __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").vcdjName });
7012
6802
  }
7013
6803
  // Start announcing
7014
- const announcer = new virtualcdj_1.Announcer(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_announceSocket, "f"), this.deviceManager, __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").iface, (_a = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").fullStartup) !== null && _a !== void 0 ? _a : false);
6804
+ const announcer = new virtualcdj_1.Announcer(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_announceSocket, "f"), this.deviceManager, __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").iface, (_a = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").fullStartup) !== null && _a !== void 0 ? _a : false, (_b = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").announceToStagehand) !== null && _b !== void 0 ? _b : false, __classPrivateFieldGet(this, _ProlinkNetwork_logger, "f"));
7015
6805
  announcer.start();
7016
6806
  // Create remote and local databases
7017
6807
  const remotedb = new remotedb_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), vcdj);
7018
- const localdb = new localdb_1.default(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_statusEmitter, "f"), (_b = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").databasePreference) !== null && _b !== void 0 ? _b : 'auto');
6808
+ const localdb = new localdb_1.default(vcdj, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"), __classPrivateFieldGet(this, _ProlinkNetwork_statusEmitter, "f"), (_c = __classPrivateFieldGet(this, _ProlinkNetwork_config, "f").databasePreference) !== null && _c !== void 0 ? _c : 'auto');
7019
6809
  // Create unified database
7020
- const database = new db_1.default(vcdj, localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"));
6810
+ const database = new db_1.default(localdb, remotedb, __classPrivateFieldGet(this, _ProlinkNetwork_deviceManager, "f"));
7021
6811
  // Create controller service
7022
6812
  const control = new control_1.default(__classPrivateFieldGet(this, _ProlinkNetwork_beatSocket, "f"), vcdj);
7023
6813
  __classPrivateFieldSet(this, _ProlinkNetwork_state, types_1.NetworkState.Connected, "f");
@@ -7147,6 +6937,14 @@ class ProlinkNetwork {
7147
6937
  var _a, _b;
7148
6938
  return (_b = (_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.remotedb) !== null && _b !== void 0 ? _b : null;
7149
6939
  }
6940
+ /**
6941
+ * Returns a promise that resolves when the full startup protocol completes.
6942
+ * Resolves immediately if fullStartup is disabled or not connected.
6943
+ */
6944
+ get startupReady() {
6945
+ var _a, _b;
6946
+ return (_b = (_a = __classPrivateFieldGet(this, _ProlinkNetwork_connection, "f")) === null || _a === void 0 ? void 0 : _a.announcer.ready) !== null && _b !== void 0 ? _b : Promise.resolve();
6947
+ }
7150
6948
  /**
7151
6949
  * Get (and initialize) the {@link MixstatusProcessor} service. This service can
7152
6950
  * be used to monitor the 'status' of devices on the network as a whole.
@@ -7165,7 +6963,7 @@ class ProlinkNetwork {
7165
6963
  }
7166
6964
  }
7167
6965
  exports.ProlinkNetwork = ProlinkNetwork;
7168
- _ProlinkNetwork_state = new WeakMap(), _ProlinkNetwork_announceSocket = new WeakMap(), _ProlinkNetwork_beatSocket = new WeakMap(), _ProlinkNetwork_statusSocket = new WeakMap(), _ProlinkNetwork_deviceManager = new WeakMap(), _ProlinkNetwork_statusEmitter = new WeakMap(), _ProlinkNetwork_positionEmitter = new WeakMap(), _ProlinkNetwork_config = new WeakMap(), _ProlinkNetwork_connection = new WeakMap(), _ProlinkNetwork_mixstatus = new WeakMap();
6966
+ _ProlinkNetwork_state = new WeakMap(), _ProlinkNetwork_announceSocket = new WeakMap(), _ProlinkNetwork_beatSocket = new WeakMap(), _ProlinkNetwork_statusSocket = new WeakMap(), _ProlinkNetwork_deviceManager = new WeakMap(), _ProlinkNetwork_statusEmitter = new WeakMap(), _ProlinkNetwork_positionEmitter = new WeakMap(), _ProlinkNetwork_logger = new WeakMap(), _ProlinkNetwork_config = new WeakMap(), _ProlinkNetwork_connection = new WeakMap(), _ProlinkNetwork_mixstatus = new WeakMap();
7169
6967
 
7170
6968
 
7171
6969
  /***/ },
@@ -7212,6 +7010,9 @@ var __importStar = (this && this.__importStar) || (function () {
7212
7010
  };
7213
7011
  })();
7214
7012
  Object.defineProperty(exports, "__esModule", ({ value: true }));
7013
+ exports.parseWindowsPath = parseWindowsPath;
7014
+ exports.resolveNfsPath = resolveNfsPath;
7015
+ exports.getPortmapPort = getPortmapPort;
7215
7016
  exports.fetchFileRange = fetchFileRange;
7216
7017
  exports.getFileInfo = getFileInfo;
7217
7018
  exports.fetchFile = fetchFile;
@@ -7229,8 +7030,54 @@ const xdr_1 = __webpack_require__(/*! ./xdr */ "./src/nfs/xdr.ts");
7229
7030
  const slotMountMapping = {
7230
7031
  [types_1.MediaSlot.USB]: '/C/',
7231
7032
  [types_1.MediaSlot.SD]: '/B/',
7232
- [types_1.MediaSlot.RB]: '/',
7233
7033
  };
7034
+ /**
7035
+ * Parse a Windows absolute path (e.g. `C:\Users\chris\Music\track.mp3`)
7036
+ * into the NFS mount path and relative file path.
7037
+ *
7038
+ * @internal Exported for testing
7039
+ */
7040
+ function parseWindowsPath(filePath) {
7041
+ const match = filePath.match(/^([A-Za-z]):[/\\](.*)$/);
7042
+ if (!match) {
7043
+ return null;
7044
+ }
7045
+ return {
7046
+ mountPath: `/${match[1].toUpperCase()}/`,
7047
+ nfsPath: match[2].replace(/\\/g, '/'),
7048
+ };
7049
+ }
7050
+ /**
7051
+ * Resolve the NFS mount path and file path for a given slot.
7052
+ *
7053
+ * For USB/SD slots, the mount path is well-known (/C/ and /B/).
7054
+ * For the RB slot, the mount path is extracted from the file path returned
7055
+ * by remotedb:
7056
+ * - Windows: `C:\Users\chris\Music\track.mp3` → mount `/C/`, path `Users/chris/Music/track.mp3`
7057
+ * - macOS: `/Users/chris/Music/track.mp3` → mount `/`, path `Users/chris/Music/track.mp3`
7058
+ *
7059
+ * @internal Exported for testing
7060
+ */
7061
+ function resolveNfsPath(slot, filePath) {
7062
+ if (slot === types_1.MediaSlot.RB) {
7063
+ // Windows: C:\Users\chris\Music\track.mp3 → mount /C/, path Users/...
7064
+ const parsed = parseWindowsPath(filePath);
7065
+ if (parsed) {
7066
+ return parsed;
7067
+ }
7068
+ // macOS: /Users/chris/Music/track.mp3 → mount /, path Users/...
7069
+ if (filePath.startsWith('/')) {
7070
+ return {
7071
+ mountPath: '/',
7072
+ nfsPath: filePath.slice(1),
7073
+ };
7074
+ }
7075
+ }
7076
+ return {
7077
+ mountPath: slotMountMapping[slot],
7078
+ nfsPath: filePath,
7079
+ };
7080
+ }
7234
7081
  /**
7235
7082
  * The module-level retry configuration for newly created RpcConnections.
7236
7083
  */
@@ -7241,6 +7088,18 @@ let retryConfig = {};
7241
7088
  * still be connected.
7242
7089
  */
7243
7090
  const clientsCache = new Map();
7091
+ /**
7092
+ * Get the portmapper port for the given device. Rekordbox software uses a
7093
+ * non-standard port (50111) while CDJs and other hardware use the standard
7094
+ * port (111).
7095
+ *
7096
+ * @internal Exported for testing
7097
+ */
7098
+ function getPortmapPort(device) {
7099
+ return device.type === types_1.DeviceType.Rekordbox
7100
+ ? programs_1.REKORDBOX_PORTMAP_PORT
7101
+ : programs_1.STANDARD_PORTMAP_PORT;
7102
+ }
7244
7103
  /**
7245
7104
  * Given a device address running a nfs and mountd RPC server, provide
7246
7105
  * RpcProgram clients that may be used to call these services.
@@ -7248,7 +7107,8 @@ const clientsCache = new Map();
7248
7107
  * NOTE: This function will cache the clients for the address, recreating the
7249
7108
  * connections if the cached clients have disconnected.
7250
7109
  */
7251
- async function getClients(address) {
7110
+ async function getClients(device) {
7111
+ const { address } = device.ip;
7252
7112
  const cachedSet = clientsCache.get(address);
7253
7113
  if (cachedSet !== undefined && cachedSet.conn.connected) {
7254
7114
  return cachedSet;
@@ -7258,49 +7118,50 @@ async function getClients(address) {
7258
7118
  clientsCache.delete(address);
7259
7119
  }
7260
7120
  const conn = new rpc_1.RpcConnection(address, retryConfig);
7121
+ const portmapPort = getPortmapPort(device);
7261
7122
  const mountClient = await (0, programs_1.makeProgramClient)(conn, {
7262
7123
  id: xdr_1.mount.Program,
7263
7124
  version: xdr_1.mount.Version,
7264
- });
7125
+ }, portmapPort);
7265
7126
  const nfsClient = await (0, programs_1.makeProgramClient)(conn, {
7266
7127
  id: xdr_1.nfs.Program,
7267
7128
  version: xdr_1.nfs.Version,
7268
- });
7129
+ }, portmapPort);
7269
7130
  const set = { conn, mountClient, nfsClient };
7270
7131
  clientsCache.set(address, set);
7271
7132
  return set;
7272
7133
  }
7273
7134
  /**
7274
- * This module maintains a singleton cached list of (device address + slot) -> file
7135
+ * This module maintains a singleton cached list of (device address + mount path) -> file
7275
7136
  * handles. The file handles may become stale in this list should the devices
7276
7137
  * connected to the players slot change.
7277
7138
  */
7278
7139
  const rootHandleCache = new Map();
7279
7140
  /**
7280
- * Locate the root filehandle of the given device slot.
7141
+ * Locate the root filehandle of the given device mount path.
7281
7142
  *
7282
- * NOTE: This function will cache the root handle for the device + slot. Should
7143
+ * NOTE: This function will cache the root handle for the device + mount path. Should
7283
7144
  * the device have changed the slot will not longer be valid (TODO,
7284
7145
  * verify this). It is up to the caller to clear the cache and get the
7285
7146
  * new root handle in that case.
7286
7147
  */
7287
- async function getRootHandle({ device, slot, mountClient, span }) {
7148
+ async function getRootHandle({ device, mountPath, mountClient, span, }) {
7288
7149
  var _a;
7289
7150
  const tx = span === null || span === void 0 ? void 0 : span.startChild({ op: 'getRootHandle' });
7290
7151
  const { address } = device.ip;
7291
- const deviceSlotCache = (_a = rootHandleCache.get(address)) !== null && _a !== void 0 ? _a : new Map();
7292
- const cachedRootHandle = deviceSlotCache.get(slot);
7152
+ const deviceMountCache = (_a = rootHandleCache.get(address)) !== null && _a !== void 0 ? _a : new Map();
7153
+ const cachedRootHandle = deviceMountCache.get(mountPath);
7293
7154
  if (cachedRootHandle !== undefined) {
7294
7155
  return cachedRootHandle;
7295
7156
  }
7296
7157
  const exports = await (0, programs_1.getExports)(mountClient, tx);
7297
- const targetExport = exports.find(e => e.filesystem === slotMountMapping[slot]);
7158
+ const targetExport = exports.find(e => e.filesystem === mountPath);
7298
7159
  if (targetExport === undefined) {
7299
7160
  return null;
7300
7161
  }
7301
7162
  const rootHandle = await (0, programs_1.mountFilesystem)(mountClient, targetExport, tx);
7302
- deviceSlotCache.set(slot, rootHandle);
7303
- rootHandleCache.set(address, deviceSlotCache);
7163
+ deviceMountCache.set(mountPath, rootHandle);
7164
+ rootHandleCache.set(address, deviceMountCache);
7304
7165
  tx === null || tx === void 0 ? void 0 : tx.finish();
7305
7166
  return rootHandle;
7306
7167
  }
@@ -7313,22 +7174,23 @@ async function fetchFileRange({ device, slot, path, offset, length, span, }) {
7313
7174
  const tx = span
7314
7175
  ? span.startChild({ op: 'fetchFileRange' })
7315
7176
  : Telemetry.startTransaction({ name: 'fetchFileRange' });
7316
- const { mountClient, nfsClient } = await getClients(device.ip.address);
7317
- const rootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7177
+ const { mountPath, nfsPath } = resolveNfsPath(slot, path);
7178
+ const { mountClient, nfsClient } = await getClients(device);
7179
+ const rootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7318
7180
  if (rootHandle === null) {
7319
7181
  throw badRoothandleError(slot, device.id);
7320
7182
  }
7321
7183
  let fileInfo = null;
7322
7184
  try {
7323
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, path, tx);
7185
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, nfsPath, tx);
7324
7186
  }
7325
7187
  catch {
7326
7188
  rootHandleCache.delete(device.ip.address);
7327
- const newRootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7189
+ const newRootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7328
7190
  if (newRootHandle === null) {
7329
7191
  throw badRoothandleError(slot, device.id);
7330
7192
  }
7331
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, newRootHandle, path, tx);
7193
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, newRootHandle, nfsPath, tx);
7332
7194
  }
7333
7195
  const actualOffset = Math.min(offset, fileInfo.size);
7334
7196
  const actualLength = Math.min(length, fileInfo.size - actualOffset);
@@ -7352,22 +7214,23 @@ async function getFileInfo({ device, slot, path, span, }) {
7352
7214
  const tx = span
7353
7215
  ? span.startChild({ op: 'getFileInfo' })
7354
7216
  : Telemetry.startTransaction({ name: 'getFileInfo' });
7355
- const { mountClient, nfsClient } = await getClients(device.ip.address);
7356
- const rootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7217
+ const { mountPath, nfsPath } = resolveNfsPath(slot, path);
7218
+ const { mountClient, nfsClient } = await getClients(device);
7219
+ const rootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7357
7220
  if (rootHandle === null) {
7358
7221
  throw badRoothandleError(slot, device.id);
7359
7222
  }
7360
7223
  let fileInfo;
7361
7224
  try {
7362
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, path, tx);
7225
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, nfsPath, tx);
7363
7226
  }
7364
7227
  catch {
7365
7228
  rootHandleCache.delete(device.ip.address);
7366
- const newRootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7229
+ const newRootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7367
7230
  if (newRootHandle === null) {
7368
7231
  throw badRoothandleError(slot, device.id);
7369
7232
  }
7370
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, newRootHandle, path, tx);
7233
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, newRootHandle, nfsPath, tx);
7371
7234
  }
7372
7235
  tx.setData('path', path);
7373
7236
  tx.setData('slot', (0, utils_1.getSlotName)(slot));
@@ -7387,8 +7250,9 @@ async function fetchFile({ device, slot, path, onProgress, span, chunkSize, }) {
7387
7250
  const tx = span
7388
7251
  ? span.startChild({ op: 'fetchFile' })
7389
7252
  : Telemetry.startTransaction({ name: 'fetchFile' });
7390
- const { mountClient, nfsClient } = await getClients(device.ip.address);
7391
- const rootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7253
+ const { mountPath, nfsPath } = resolveNfsPath(slot, path);
7254
+ const { mountClient, nfsClient } = await getClients(device);
7255
+ const rootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7392
7256
  if (rootHandle === null) {
7393
7257
  throw badRoothandleError(slot, device.id);
7394
7258
  }
@@ -7396,16 +7260,16 @@ async function fetchFile({ device, slot, path, onProgress, span, chunkSize, }) {
7396
7260
  // a path lets first try and clear our roothandle cache
7397
7261
  let fileInfo = null;
7398
7262
  try {
7399
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, path, tx);
7263
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, nfsPath, tx);
7400
7264
  }
7401
7265
  catch {
7402
7266
  rootHandleCache.delete(device.ip.address);
7403
- const rootHandle = await getRootHandle({ device, slot, mountClient, span: tx });
7267
+ const rootHandle = await getRootHandle({ device, mountPath, mountClient, span: tx });
7404
7268
  if (rootHandle === null) {
7405
7269
  throw badRoothandleError(slot, device.id);
7406
7270
  }
7407
7271
  // Desperately try once more to lookup the file
7408
- fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, path, tx);
7272
+ fileInfo = await (0, programs_1.lookupPath)(nfsClient, rootHandle, nfsPath, tx);
7409
7273
  }
7410
7274
  const file = await (0, programs_1.fetchFile)(nfsClient, fileInfo, onProgress, tx, chunkSize);
7411
7275
  tx.setData('path', path);
@@ -7443,6 +7307,7 @@ function configureRetryStrategy(config) {
7443
7307
  "use strict";
7444
7308
 
7445
7309
  Object.defineProperty(exports, "__esModule", ({ value: true }));
7310
+ exports.REKORDBOX_PORTMAP_PORT = exports.STANDARD_PORTMAP_PORT = void 0;
7446
7311
  exports.makeProgramClient = makeProgramClient;
7447
7312
  exports.getExports = getExports;
7448
7313
  exports.mountFilesystem = mountFilesystem;
@@ -7457,10 +7322,19 @@ const xdr_1 = __webpack_require__(/*! ./xdr */ "./src/nfs/xdr.ts");
7457
7322
  * How many bytes of a file should we read at once.
7458
7323
  */
7459
7324
  const READ_SIZE = 8192;
7325
+ /**
7326
+ * Standard portmapper port used by CDJs and other hardware.
7327
+ */
7328
+ exports.STANDARD_PORTMAP_PORT = 111;
7329
+ /**
7330
+ * Non-standard portmapper port used by rekordbox software.
7331
+ * Rekordbox registers its RPC services on this port instead of 111.
7332
+ */
7333
+ exports.REKORDBOX_PORTMAP_PORT = 50111;
7460
7334
  /**
7461
7335
  * Queries for the listening port of a RPC program
7462
7336
  */
7463
- async function makeProgramClient(conn, program) {
7337
+ async function makeProgramClient(conn, program, portmapPort = exports.STANDARD_PORTMAP_PORT) {
7464
7338
  const getPortData = new xdr_1.portmap.GetPort({
7465
7339
  program: program.id,
7466
7340
  version: program.version,
@@ -7468,7 +7342,7 @@ async function makeProgramClient(conn, program) {
7468
7342
  port: 0,
7469
7343
  });
7470
7344
  const data = await conn.call({
7471
- port: 111,
7345
+ port: portmapPort,
7472
7346
  program: xdr_1.portmap.Program,
7473
7347
  version: xdr_1.portmap.Version,
7474
7348
  procedure: xdr_1.portmap.Procedure.getPort().value,
@@ -8296,15 +8170,8 @@ function findAllAlphaThetaInterfaces() {
8296
8170
  return results;
8297
8171
  }
8298
8172
  /**
8299
- * macOS implementation: Uses ioreg to find AlphaTheta USB devices,
8300
- * then networksetup to map them to interface names.
8301
- */
8302
- function findAlphaThetaInterfaceMacOS() {
8303
- const results = findAllAlphaThetaInterfacesMacOS();
8304
- return results.length > 0 ? results[0] : null;
8305
- }
8306
- /**
8307
- * macOS implementation: Find ALL AlphaTheta USB interfaces.
8173
+ * macOS implementation: Find ALL AlphaTheta USB interfaces using ioreg
8174
+ * and networksetup to map them to interface names.
8308
8175
  */
8309
8176
  function findAllAlphaThetaInterfacesMacOS() {
8310
8177
  try {
@@ -8379,14 +8246,7 @@ function findAllAlphaThetaInterfacesMacOS() {
8379
8246
  }
8380
8247
  }
8381
8248
  /**
8382
- * Windows implementation: Uses PowerShell to find AlphaTheta USB network adapters.
8383
- */
8384
- function findAlphaThetaInterfaceWindows() {
8385
- const results = findAllAlphaThetaInterfacesWindows();
8386
- return results.length > 0 ? results[0] : null;
8387
- }
8388
- /**
8389
- * Windows implementation: Find ALL AlphaTheta USB interfaces.
8249
+ * Windows implementation: Find ALL AlphaTheta USB network adapters via PowerShell.
8390
8250
  */
8391
8251
  function findAllAlphaThetaInterfacesWindows() {
8392
8252
  var _a;
@@ -8571,14 +8431,6 @@ function getAlphaThetaArpEntries() {
8571
8431
  }
8572
8432
  return result;
8573
8433
  }
8574
- /**
8575
- * Find an AlphaTheta device connected via Ethernet by checking the ARP cache
8576
- * for known MAC address prefixes.
8577
- */
8578
- function findAlphaThetaViaEthernet() {
8579
- const results = findAllAlphaThetaViaEthernet();
8580
- return results.length > 0 ? results[0] : null;
8581
- }
8582
8434
  /**
8583
8435
  * Find ALL AlphaTheta devices connected via Ethernet by checking the ARP cache
8584
8436
  * for known MAC address prefixes.
@@ -8907,7 +8759,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
8907
8759
  };
8908
8760
  var _PassiveProlinkNetwork_adapter, _PassiveProlinkNetwork_deviceManager, _PassiveProlinkNetwork_statusEmitter, _PassiveProlinkNetwork_positionEmitter, _PassiveProlinkNetwork_localdb, _PassiveProlinkNetwork_remotedb, _PassiveProlinkNetwork_mixstatus;
8909
8761
  Object.defineProperty(exports, "__esModule", ({ value: true }));
8910
- exports.PassiveProlinkNetwork = exports.PassiveStatusEmitter = exports.PassiveRemoteDatabase = exports.PassivePositionEmitter = exports.PcapAdapter = exports.PassiveLocalDatabase = exports.PassiveDeviceManager = exports.getArpCacheForInterface = exports.findAllAlphaThetaInterfaces = exports.findAlphaThetaInterface = void 0;
8762
+ exports.PassiveProlinkNetwork = exports.PassiveStatusEmitter = exports.PassiveRemoteDatabase = exports.PassivePositionEmitter = exports.PcapAdapter = exports.PassiveLocalDatabase = exports.PassiveDeviceManager = exports.getArpCacheForInterface = exports.findAlphaThetaInterface = exports.findAllAlphaThetaInterfaces = void 0;
8911
8763
  exports.bringOnlinePassive = bringOnlinePassive;
8912
8764
  exports.listInterfaces = listInterfaces;
8913
8765
  const os_1 = __webpack_require__(/*! os */ "os");
@@ -8919,8 +8771,8 @@ const position_1 = __webpack_require__(/*! ./position */ "./src/passive/position
8919
8771
  const remotedb_1 = __webpack_require__(/*! ./remotedb */ "./src/passive/remotedb.ts");
8920
8772
  const status_1 = __webpack_require__(/*! ./status */ "./src/passive/status.ts");
8921
8773
  var alphatheta_1 = __webpack_require__(/*! ./alphatheta */ "./src/passive/alphatheta.ts");
8922
- Object.defineProperty(exports, "findAlphaThetaInterface", ({ enumerable: true, get: function () { return alphatheta_1.findAlphaThetaInterface; } }));
8923
8774
  Object.defineProperty(exports, "findAllAlphaThetaInterfaces", ({ enumerable: true, get: function () { return alphatheta_1.findAllAlphaThetaInterfaces; } }));
8775
+ Object.defineProperty(exports, "findAlphaThetaInterface", ({ enumerable: true, get: function () { return alphatheta_1.findAlphaThetaInterface; } }));
8924
8776
  Object.defineProperty(exports, "getArpCacheForInterface", ({ enumerable: true, get: function () { return alphatheta_1.getArpCacheForInterface; } }));
8925
8777
  var devices_2 = __webpack_require__(/*! ./devices */ "./src/passive/devices.ts");
8926
8778
  Object.defineProperty(exports, "PassiveDeviceManager", ({ enumerable: true, get: function () { return devices_2.PassiveDeviceManager; } }));
@@ -9205,6 +9057,7 @@ const events_1 = __webpack_require__(/*! events */ "events");
9205
9057
  const fs = __importStar(__webpack_require__(/*! fs */ "fs"));
9206
9058
  const os = __importStar(__webpack_require__(/*! os */ "os"));
9207
9059
  const path = __importStar(__webpack_require__(/*! path */ "path"));
9060
+ const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
9208
9061
  const onelibrary_1 = __webpack_require__(/*! src/localdb/onelibrary */ "./src/localdb/onelibrary.ts");
9209
9062
  const orm_1 = __webpack_require__(/*! src/localdb/orm */ "./src/localdb/orm.ts");
9210
9063
  const rekordbox_1 = __webpack_require__(/*! src/localdb/rekordbox */ "./src/localdb/rekordbox.ts");
@@ -9287,7 +9140,9 @@ class PassiveLocalDatabase {
9287
9140
  * Helper to fetch a file from device, trying both dotted and non-dotted paths
9288
9141
  */
9289
9142
  _PassiveLocalDatabase_fetchFileWithFallback.set(this, async (device, slot, basePath, tx) => {
9290
- const attemptOrder = process.platform === 'win32' ? [basePath, `.${basePath}`] : [`.${basePath}`, basePath];
9143
+ const attemptOrder = process.platform === 'win32'
9144
+ ? [basePath, `.${basePath}`]
9145
+ : [`.${basePath}`, basePath];
9291
9146
  try {
9292
9147
  return await (0, nfs_1.fetchFile)({
9293
9148
  device,
@@ -9298,7 +9153,7 @@ class PassiveLocalDatabase {
9298
9153
  });
9299
9154
  }
9300
9155
  catch {
9301
- return await (0, nfs_1.fetchFile)({
9156
+ return (0, nfs_1.fetchFile)({
9302
9157
  device,
9303
9158
  slot,
9304
9159
  path: attemptOrder[1],
@@ -9476,8 +9331,10 @@ class PassiveLocalDatabase {
9476
9331
  var _a;
9477
9332
  const lockKey = `${device.id}-${slot}`;
9478
9333
  const lock = (_a = __classPrivateFieldGet(this, _PassiveLocalDatabase_slotLocks, "f").get(lockKey)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _PassiveLocalDatabase_slotLocks, "f").set(lockKey, new async_mutex_1.Mutex()).get(lockKey);
9479
- if (device.type !== types_1.DeviceType.CDJ) {
9480
- throw new Error('Cannot create database from devices that are not CDJs');
9334
+ if (device.type !== types_1.DeviceType.CDJ ||
9335
+ device.id < constants_1.MIN_CDJ_DEVICE_ID ||
9336
+ device.id > constants_1.MAX_CDJ_DEVICE_ID) {
9337
+ return null;
9481
9338
  }
9482
9339
  if (media.tracksType !== types_1.TrackType.RB) {
9483
9340
  return null;
@@ -9506,7 +9363,9 @@ class PassiveLocalDatabase {
9506
9363
  var _a;
9507
9364
  const lockKey = `${device.id}-${slot}-nomedia`;
9508
9365
  const lock = (_a = __classPrivateFieldGet(this, _PassiveLocalDatabase_slotLocks, "f").get(lockKey)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _PassiveLocalDatabase_slotLocks, "f").set(lockKey, new async_mutex_1.Mutex()).get(lockKey);
9509
- if (device.type !== types_1.DeviceType.CDJ) {
9366
+ if (device.type !== types_1.DeviceType.CDJ ||
9367
+ device.id < constants_1.MIN_CDJ_DEVICE_ID ||
9368
+ device.id > constants_1.MAX_CDJ_DEVICE_ID) {
9510
9369
  return null;
9511
9370
  }
9512
9371
  // Check if we already have a cached database for this device/slot
@@ -9549,7 +9408,9 @@ class PassiveLocalDatabase {
9549
9408
  */
9550
9409
  async preload() {
9551
9410
  const allDevices = [...__classPrivateFieldGet(this, _PassiveLocalDatabase_deviceManager, "f").devices.values()];
9552
- const cdjDevices = allDevices.filter(device => device.type === types_1.DeviceType.CDJ);
9411
+ const cdjDevices = allDevices.filter(device => device.type === types_1.DeviceType.CDJ &&
9412
+ device.id >= constants_1.MIN_CDJ_DEVICE_ID &&
9413
+ device.id <= constants_1.MAX_CDJ_DEVICE_ID);
9553
9414
  if (cdjDevices.length === 0) {
9554
9415
  return;
9555
9416
  }
@@ -9587,6 +9448,7 @@ var _PcapAdapter_emitter, _PcapAdapter_cap, _PcapAdapter_buffer, _PcapAdapter_if
9587
9448
  Object.defineProperty(exports, "__esModule", ({ value: true }));
9588
9449
  exports.PcapAdapter = void 0;
9589
9450
  const events_1 = __webpack_require__(/*! events */ "events");
9451
+ const os_1 = __webpack_require__(/*! os */ "os");
9590
9452
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
9591
9453
  /**
9592
9454
  * PcapAdapter captures Pro DJ Link packets via pcap (libpcap).
@@ -9635,13 +9497,22 @@ class PcapAdapter {
9635
9497
  const { Cap, decoders } = cap;
9636
9498
  const PROTOCOL = decoders.PROTOCOL;
9637
9499
  __classPrivateFieldSet(this, _PcapAdapter_cap, new Cap(), "f");
9500
+ // On Windows, translate Node.js interface name (e.g. "Ethernet") to Npcap
9501
+ // device path (e.g. "\Device\NPF_{GUID}") by matching IPv4 addresses.
9502
+ let deviceName = __classPrivateFieldGet(this, _PcapAdapter_iface, "f");
9503
+ if (process.platform === 'win32' && !deviceName.startsWith('\\Device\\')) {
9504
+ const resolved = resolveWindowsInterface(__classPrivateFieldGet(this, _PcapAdapter_iface, "f"), Cap);
9505
+ if (resolved) {
9506
+ deviceName = resolved;
9507
+ }
9508
+ }
9638
9509
  // BPF filter for Pro DJ Link UDP ports
9639
9510
  const filter = `udp and (port ${constants_1.ANNOUNCE_PORT} or port ${constants_1.BEAT_PORT} or port ${constants_1.STATUS_PORT})`;
9640
9511
  try {
9641
- __classPrivateFieldGet(this, _PcapAdapter_cap, "f").open(__classPrivateFieldGet(this, _PcapAdapter_iface, "f"), filter, 65535, __classPrivateFieldGet(this, _PcapAdapter_buffer, "f"));
9512
+ __classPrivateFieldGet(this, _PcapAdapter_cap, "f").open(deviceName, filter, 65535, __classPrivateFieldGet(this, _PcapAdapter_buffer, "f"));
9642
9513
  }
9643
9514
  catch (err) {
9644
- throw new Error(`Failed to open interface "${__classPrivateFieldGet(this, _PcapAdapter_iface, "f")}" for packet capture. ` +
9515
+ throw new Error(`Failed to open interface "${__classPrivateFieldGet(this, _PcapAdapter_iface, "f")}" (device: ${deviceName}) for packet capture. ` +
9645
9516
  `Ensure you have root/sudo privileges and the interface exists.\n` +
9646
9517
  `Original error: ${err}`);
9647
9518
  }
@@ -9727,6 +9598,41 @@ class PcapAdapter {
9727
9598
  }
9728
9599
  exports.PcapAdapter = PcapAdapter;
9729
9600
  _PcapAdapter_emitter = new WeakMap(), _PcapAdapter_cap = new WeakMap(), _PcapAdapter_buffer = new WeakMap(), _PcapAdapter_iface = new WeakMap(), _PcapAdapter_started = new WeakMap();
9601
+ /**
9602
+ * Resolve a Node.js network interface name (e.g. "Ethernet") to an Npcap
9603
+ * device path (e.g. "\Device\NPF_{GUID}") on Windows.
9604
+ *
9605
+ * Node.js os.networkInterfaces() returns friendly names, but the cap module
9606
+ * requires Npcap device paths. This function matches by comparing IPv4
9607
+ * addresses between os.networkInterfaces() and Cap.deviceList().
9608
+ */
9609
+ function resolveWindowsInterface(ifaceName, Cap) {
9610
+ // Get the IPv4 addresses for the requested Node.js interface
9611
+ const nodeInterfaces = (0, os_1.networkInterfaces)();
9612
+ const nodeIface = nodeInterfaces[ifaceName];
9613
+ if (!nodeIface) {
9614
+ return null;
9615
+ }
9616
+ const nodeIpv4Addresses = new Set(nodeIface
9617
+ .filter(i => i.family === 'IPv4' || i.family === 4)
9618
+ .map(i => i.address));
9619
+ if (nodeIpv4Addresses.size === 0) {
9620
+ return null;
9621
+ }
9622
+ // Find the cap device whose addresses overlap with the Node.js interface
9623
+ const capDevices = Cap.deviceList();
9624
+ for (const device of capDevices) {
9625
+ if (!device.addresses) {
9626
+ continue;
9627
+ }
9628
+ for (const addr of device.addresses) {
9629
+ if (addr.addr && nodeIpv4Addresses.has(addr.addr)) {
9630
+ return device.name;
9631
+ }
9632
+ }
9633
+ }
9634
+ return null;
9635
+ }
9730
9636
  exports["default"] = PcapAdapter;
9731
9637
 
9732
9638
 
@@ -10038,6 +9944,17 @@ class PassiveRemoteDatabase {
10038
9944
  query: remotedb_1.Query.GetMetadata,
10039
9945
  args: { trackId },
10040
9946
  });
9947
+ // File path is not available for streaming tracks (Beatport, etc.)
9948
+ try {
9949
+ track.filePath = await conn.query({
9950
+ queryDescriptor,
9951
+ query: remotedb_1.Query.GetTrackInfo,
9952
+ args: { trackId },
9953
+ });
9954
+ }
9955
+ catch {
9956
+ // No-op — streaming tracks don't have a local file
9957
+ }
10041
9958
  return track;
10042
9959
  }
10043
9960
  catch (err) {
@@ -10046,6 +9963,50 @@ class PassiveRemoteDatabase {
10046
9963
  throw err;
10047
9964
  }
10048
9965
  }
9966
+ /**
9967
+ * Query metadata for an unanalyzed track (loaded directly from USB without
9968
+ * Rekordbox analysis). Uses GetGenericMetadata which reads ID3 tags from the
9969
+ * audio file via the CDJ.
9970
+ *
9971
+ * @param deviceId - The CDJ device to query
9972
+ * @param trackSlot - The media slot (USB or SD)
9973
+ * @param trackType - Should be TrackType.Unanalyzed
9974
+ * @param trackId - The track ID to look up
9975
+ */
9976
+ async getGenericTrackMetadata(deviceId, trackSlot, trackType, trackId) {
9977
+ const conn = await this.get(deviceId);
9978
+ if (conn === null) {
9979
+ return null;
9980
+ }
9981
+ const queryDescriptor = {
9982
+ trackSlot,
9983
+ trackType,
9984
+ menuTarget: remotedb_1.MenuTarget.Main,
9985
+ };
9986
+ try {
9987
+ const track = await conn.query({
9988
+ queryDescriptor,
9989
+ query: remotedb_1.Query.GetGenericMetadata,
9990
+ args: { trackId },
9991
+ });
9992
+ // Try to get file path for artwork extraction
9993
+ try {
9994
+ track.filePath = await conn.query({
9995
+ queryDescriptor,
9996
+ query: remotedb_1.Query.GetTrackInfo,
9997
+ args: { trackId },
9998
+ });
9999
+ }
10000
+ catch {
10001
+ // GetTrackInfo may not be available for unanalyzed tracks
10002
+ }
10003
+ return track;
10004
+ }
10005
+ catch (err) {
10006
+ __classPrivateFieldGet(this, _PassiveRemoteDatabase_connections, "f").delete(deviceId);
10007
+ throw err;
10008
+ }
10009
+ }
10049
10010
  /**
10050
10011
  * Stop all connections
10051
10012
  */
@@ -10504,10 +10465,14 @@ class QueryInterface {
10504
10465
  const anyArgs = args;
10505
10466
  const handler = queries_1.queryHandlers[query];
10506
10467
  const releaseLock = await __classPrivateFieldGet(this, _QueryInterface_lock, "f").acquire();
10507
- const response = await handler({ conn, lookupDescriptor, span: tx, args: anyArgs });
10508
- releaseLock();
10509
- tx.finish();
10510
- return response;
10468
+ try {
10469
+ const response = await handler({ conn, lookupDescriptor, span: tx, args: anyArgs });
10470
+ tx.finish();
10471
+ return response;
10472
+ }
10473
+ finally {
10474
+ releaseLock();
10475
+ }
10511
10476
  }
10512
10477
  }
10513
10478
  exports.QueryInterface = QueryInterface;
@@ -10535,6 +10500,11 @@ class RemoteDatabase {
10535
10500
  const { ip } = device;
10536
10501
  const dbPort = await getRemoteDBServerPort(ip);
10537
10502
  const socket = new promise_socket_1.default(new net_1.Socket());
10503
+ // Set a connection timeout to prevent hanging forever
10504
+ socket.stream.setTimeout(10000);
10505
+ socket.stream.once('timeout', () => {
10506
+ socket.stream.destroy(new Error(`RemoteDB connection to ${ip.address}:${dbPort} timed out`));
10507
+ });
10538
10508
  await socket.connect(dbPort, ip.address);
10539
10509
  // Send required preamble to open communications with the device
10540
10510
  const preamble = new fields_1.UInt32(0x01);
@@ -10556,6 +10526,9 @@ class RemoteDatabase {
10556
10526
  if (resp.type !== types_1.MessageType.Success) {
10557
10527
  throw new Error(`Failed to introduce self to device ID: ${device.id}`);
10558
10528
  }
10529
+ // Clear socket timeout after successful connection — the per-device mutex
10530
+ // and application-level Promise.race timeouts handle query timeouts
10531
+ socket.stream.setTimeout(0);
10559
10532
  __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").set(device.id, new Connection(device, socket));
10560
10533
  tx.finish();
10561
10534
  };
@@ -10597,15 +10570,19 @@ class RemoteDatabase {
10597
10570
  }
10598
10571
  const lock = (_a = __classPrivateFieldGet(this, _RemoteDatabase_deviceLocks, "f").get(device.id)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _RemoteDatabase_deviceLocks, "f").set(device.id, new async_mutex_1.Mutex()).get(device.id);
10599
10572
  const releaseLock = await lock.acquire();
10600
- let conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
10601
- if (conn === undefined) {
10602
- await this.connectToDevice(device);
10573
+ try {
10574
+ let conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
10575
+ if (conn === undefined) {
10576
+ await this.connectToDevice(device);
10577
+ }
10578
+ conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
10579
+ // NOTE: We pass the same lock we use for this device to the query
10580
+ // interface to ensure all query interfaces use the same lock.
10581
+ return new QueryInterface(conn, lock, __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f"));
10582
+ }
10583
+ finally {
10584
+ releaseLock();
10603
10585
  }
10604
- conn = __classPrivateFieldGet(this, _RemoteDatabase_connections, "f").get(deviceId);
10605
- releaseLock();
10606
- // NOTE: We pass the same lock we use for this device to the query
10607
- // interface to ensure all query interfaces use the same lock.
10608
- return new QueryInterface(conn, lock, __classPrivateFieldGet(this, _RemoteDatabase_hostDevice, "f"));
10609
10586
  }
10610
10587
  }
10611
10588
  _RemoteDatabase_hostDevice = new WeakMap(), _RemoteDatabase_deviceManager = new WeakMap(), _RemoteDatabase_connections = new WeakMap(), _RemoteDatabase_deviceLocks = new WeakMap();
@@ -11307,7 +11284,7 @@ const utils_1 = __webpack_require__(/*! ./utils */ "./src/remotedb/utils.ts");
11307
11284
  * Lookup track metadata from rekordbox and coerce it into a Track entity
11308
11285
  */
11309
11286
  async function getMetadata(opts) {
11310
- var _a, _b, _c, _d, _e;
11287
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
11311
11288
  const { conn, lookupDescriptor, span, args } = opts;
11312
11289
  const { trackId } = args;
11313
11290
  const request = new message_1.Message({
@@ -11324,24 +11301,25 @@ async function getMetadata(opts) {
11324
11301
  trackItems[item.type] = item;
11325
11302
  }
11326
11303
  // Translate our trackItems into a (partial) Track entity.
11304
+ // Use optional chaining for all fields since streaming tracks may omit some.
11327
11305
  const track = {
11328
- id: trackItems[item_1.ItemType.TrackTitle].id,
11329
- title: trackItems[item_1.ItemType.TrackTitle].title,
11330
- duration: trackItems[item_1.ItemType.Duration].duration,
11331
- tempo: trackItems[item_1.ItemType.Tempo].bpm,
11332
- comment: trackItems[item_1.ItemType.Comment].comment,
11333
- rating: trackItems[item_1.ItemType.Rating].rating,
11334
- year: (_a = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.Year]) === null || _a === void 0 ? void 0 : _a.year,
11335
- bitrate: (_b = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.BitRate]) === null || _b === void 0 ? void 0 : _b.bitrate,
11336
- artwork: { id: trackItems[item_1.ItemType.TrackTitle].artworkId },
11337
- album: trackItems[item_1.ItemType.AlbumTitle],
11338
- artist: trackItems[item_1.ItemType.Artist],
11339
- genre: trackItems[item_1.ItemType.Genre],
11340
- key: trackItems[item_1.ItemType.Key],
11341
- color: (0, utils_1.findColor)(Object.values(trackItems)),
11342
- label: (_c = trackItems[item_1.ItemType.Label]) !== null && _c !== void 0 ? _c : null,
11343
- remixer: (_d = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.Remixer]) !== null && _d !== void 0 ? _d : null,
11344
- originalArtist: (_e = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.OriginalArtist]) !== null && _e !== void 0 ? _e : null,
11306
+ id: (_b = (_a = trackItems[item_1.ItemType.TrackTitle]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : 0,
11307
+ title: (_d = (_c = trackItems[item_1.ItemType.TrackTitle]) === null || _c === void 0 ? void 0 : _c.title) !== null && _d !== void 0 ? _d : '',
11308
+ duration: (_f = (_e = trackItems[item_1.ItemType.Duration]) === null || _e === void 0 ? void 0 : _e.duration) !== null && _f !== void 0 ? _f : 0,
11309
+ tempo: (_h = (_g = trackItems[item_1.ItemType.Tempo]) === null || _g === void 0 ? void 0 : _g.bpm) !== null && _h !== void 0 ? _h : 0,
11310
+ comment: (_k = (_j = trackItems[item_1.ItemType.Comment]) === null || _j === void 0 ? void 0 : _j.comment) !== null && _k !== void 0 ? _k : '',
11311
+ rating: (_m = (_l = trackItems[item_1.ItemType.Rating]) === null || _l === void 0 ? void 0 : _l.rating) !== null && _m !== void 0 ? _m : 0,
11312
+ year: (_o = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.Year]) === null || _o === void 0 ? void 0 : _o.year,
11313
+ bitrate: (_p = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.BitRate]) === null || _p === void 0 ? void 0 : _p.bitrate,
11314
+ artwork: { id: (_r = (_q = trackItems[item_1.ItemType.TrackTitle]) === null || _q === void 0 ? void 0 : _q.artworkId) !== null && _r !== void 0 ? _r : 0 },
11315
+ album: (_s = trackItems[item_1.ItemType.AlbumTitle]) !== null && _s !== void 0 ? _s : null,
11316
+ artist: (_t = trackItems[item_1.ItemType.Artist]) !== null && _t !== void 0 ? _t : null,
11317
+ genre: (_u = trackItems[item_1.ItemType.Genre]) !== null && _u !== void 0 ? _u : null,
11318
+ key: (_v = trackItems[item_1.ItemType.Key]) !== null && _v !== void 0 ? _v : null,
11319
+ color: (_w = (0, utils_1.findColor)(Object.values(trackItems))) !== null && _w !== void 0 ? _w : null,
11320
+ label: (_x = trackItems[item_1.ItemType.Label]) !== null && _x !== void 0 ? _x : null,
11321
+ remixer: (_y = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.Remixer]) !== null && _y !== void 0 ? _y : null,
11322
+ originalArtist: (_z = trackItems === null || trackItems === void 0 ? void 0 : trackItems[item_1.ItemType.OriginalArtist]) !== null && _z !== void 0 ? _z : null,
11345
11323
  composer: null,
11346
11324
  fileName: '',
11347
11325
  filePath: '',
@@ -11949,7 +11927,7 @@ function mediaSlotFromPacket(packet) {
11949
11927
  return undefined;
11950
11928
  }
11951
11929
  const name = packet
11952
- .slice(0x2c, 0x0c + 40)
11930
+ .slice(0x2c, 0x2c + 40)
11953
11931
  .toString()
11954
11932
  .replace(/\0/g, '');
11955
11933
  const createdDate = new Date(packet
@@ -12126,7 +12104,7 @@ var __importStar = (this && this.__importStar) || (function () {
12126
12104
  };
12127
12105
  })();
12128
12106
  Object.defineProperty(exports, "__esModule", ({ value: true }));
12129
- exports.MixstatusMode = exports.NetworkState = exports.CueColor = exports.HotcueButton = exports.TrackType = exports.MediaSlot = exports.MediaColor = exports.DeviceType = exports.CDJStatus = void 0;
12107
+ exports.MixstatusMode = exports.NetworkState = exports.HotcueButton = exports.CueColor = exports.TrackType = exports.MediaSlot = exports.MediaColor = exports.DeviceType = exports.CDJStatus = void 0;
12130
12108
  exports.CDJStatus = __importStar(__webpack_require__(/*! src/status/types */ "./src/status/types.ts"));
12131
12109
  /**
12132
12110
  * Known device types on the network
@@ -12159,6 +12137,11 @@ var MediaSlot;
12159
12137
  MediaSlot[MediaSlot["SD"] = 2] = "SD";
12160
12138
  MediaSlot[MediaSlot["USB"] = 3] = "USB";
12161
12139
  MediaSlot[MediaSlot["RB"] = 4] = "RB";
12140
+ MediaSlot[MediaSlot["Unknown05"] = 5] = "Unknown05";
12141
+ MediaSlot[MediaSlot["StreamingDirectPlay"] = 6] = "StreamingDirectPlay";
12142
+ MediaSlot[MediaSlot["Unknown07"] = 7] = "Unknown07";
12143
+ MediaSlot[MediaSlot["Unknown08"] = 8] = "Unknown08";
12144
+ MediaSlot[MediaSlot["Beatport"] = 9] = "Beatport";
12162
12145
  })(MediaSlot || (exports.MediaSlot = MediaSlot = {}));
12163
12146
  /**
12164
12147
  * Track type flags
@@ -12169,46 +12152,14 @@ var TrackType;
12169
12152
  TrackType[TrackType["RB"] = 1] = "RB";
12170
12153
  TrackType[TrackType["Unanalyzed"] = 2] = "Unanalyzed";
12171
12154
  TrackType[TrackType["AudioCD"] = 5] = "AudioCD";
12155
+ TrackType[TrackType["Streaming"] = 6] = "Streaming";
12172
12156
  })(TrackType || (exports.TrackType = TrackType = {}));
12173
12157
  /**
12174
- * A hotcue button label
12175
- */
12176
- var HotcueButton;
12177
- (function (HotcueButton) {
12178
- HotcueButton[HotcueButton["A"] = 1] = "A";
12179
- HotcueButton[HotcueButton["B"] = 2] = "B";
12180
- HotcueButton[HotcueButton["C"] = 3] = "C";
12181
- HotcueButton[HotcueButton["D"] = 4] = "D";
12182
- HotcueButton[HotcueButton["E"] = 5] = "E";
12183
- HotcueButton[HotcueButton["F"] = 6] = "F";
12184
- HotcueButton[HotcueButton["G"] = 7] = "G";
12185
- HotcueButton[HotcueButton["H"] = 8] = "H";
12186
- })(HotcueButton || (exports.HotcueButton = HotcueButton = {}));
12187
- /**
12188
- * When a custom color is not configured the cue point will be one of these
12189
- * colors.
12158
+ * Re-export cue types from onelibrary-connect
12190
12159
  */
12191
- var CueColor;
12192
- (function (CueColor) {
12193
- CueColor[CueColor["None"] = 0] = "None";
12194
- CueColor[CueColor["Blank"] = 21] = "Blank";
12195
- CueColor[CueColor["Magenta"] = 49] = "Magenta";
12196
- CueColor[CueColor["Violet"] = 56] = "Violet";
12197
- CueColor[CueColor["Fuchsia"] = 60] = "Fuchsia";
12198
- CueColor[CueColor["LightSlateBlue"] = 62] = "LightSlateBlue";
12199
- CueColor[CueColor["Blue"] = 1] = "Blue";
12200
- CueColor[CueColor["SteelBlue"] = 5] = "SteelBlue";
12201
- CueColor[CueColor["Aqua"] = 9] = "Aqua";
12202
- CueColor[CueColor["SeaGreen"] = 14] = "SeaGreen";
12203
- CueColor[CueColor["Teal"] = 18] = "Teal";
12204
- CueColor[CueColor["Green"] = 22] = "Green";
12205
- CueColor[CueColor["Lime"] = 26] = "Lime";
12206
- CueColor[CueColor["Olive"] = 30] = "Olive";
12207
- CueColor[CueColor["Yellow"] = 32] = "Yellow";
12208
- CueColor[CueColor["Orange"] = 38] = "Orange";
12209
- CueColor[CueColor["Red"] = 42] = "Red";
12210
- CueColor[CueColor["Pink"] = 45] = "Pink";
12211
- })(CueColor || (exports.CueColor = CueColor = {}));
12160
+ var onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
12161
+ Object.defineProperty(exports, "CueColor", ({ enumerable: true, get: function () { return onelibrary_connect_1.CueColor; } }));
12162
+ Object.defineProperty(exports, "HotcueButton", ({ enumerable: true, get: function () { return onelibrary_connect_1.HotcueButton; } }));
12212
12163
  var NetworkState;
12213
12164
  (function (NetworkState) {
12214
12165
  /**
@@ -12366,6 +12317,7 @@ var __importStar = (this && this.__importStar) || (function () {
12366
12317
  Object.defineProperty(exports, "__esModule", ({ value: true }));
12367
12318
  exports.buildName = buildName;
12368
12319
  exports.getMatchingInterface = getMatchingInterface;
12320
+ exports.getBroadcastAddress = getBroadcastAddress;
12369
12321
  exports.bpmToSeconds = bpmToSeconds;
12370
12322
  exports.getSlotName = getSlotName;
12371
12323
  exports.getTrackTypeName = getTrackTypeName;
@@ -12403,6 +12355,18 @@ function getMatchingInterface(ipAddr) {
12403
12355
  }
12404
12356
  return matchedIface;
12405
12357
  }
12358
+ /**
12359
+ * Computes the IPv4 subnet broadcast address for a network interface.
12360
+ *
12361
+ * Builds the address from the interface CIDR so the broadcast covers the whole
12362
+ * subnet (e.g. x.x.x.255 for a /24). Falls back to the bare interface address
12363
+ * when no CIDR is available, which only reaches the host itself but avoids
12364
+ * throwing.
12365
+ */
12366
+ function getBroadcastAddress(iface) {
12367
+ var _a;
12368
+ return new ip.Address4((_a = iface.cidr) !== null && _a !== void 0 ? _a : iface.address).endAddress().address;
12369
+ }
12406
12370
  /**
12407
12371
  * Given a BPM and pitch value, compute how many seconds per beat
12408
12372
  */
@@ -12660,13 +12624,14 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12660
12624
  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");
12661
12625
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12662
12626
  };
12663
- var _Announcer_instances, _Announcer_announceSocket, _Announcer_deviceManager, _Announcer_vcdj, _Announcer_intervalHandle, _Announcer_fullStartup, _Announcer_currentStage, _Announcer_stageCounter, _Announcer_conflictListener, _Announcer_iface, _Announcer_startFullStartup, _Announcer_handleConflict, _Announcer_sendStagePackets, _Announcer_advanceStage, _Announcer_startKeepAlive;
12627
+ var _Announcer_instances, _Announcer_announceSocket, _Announcer_deviceManager, _Announcer_vcdj, _Announcer_intervalHandle, _Announcer_fullStartup, _Announcer_announceToStagehand, _Announcer_currentStage, _Announcer_stageCounter, _Announcer_conflictListener, _Announcer_iface, _Announcer_logger, _Announcer_onStartupComplete, _Announcer_startFullStartup, _Announcer_handleConflict, _Announcer_sendPacket, _Announcer_sendStagePackets, _Announcer_advanceStage, _Announcer_startKeepAlive;
12664
12628
  Object.defineProperty(exports, "__esModule", ({ value: true }));
12665
12629
  exports.Announcer = exports.getVirtualCDJ = void 0;
12666
12630
  exports.makeStatusPacket = makeStatusPacket;
12667
12631
  exports.makeAnnouncePacket = makeAnnouncePacket;
12668
12632
  const ip = __importStar(__webpack_require__(/*! ip-address */ "ip-address"));
12669
12633
  const constants_1 = __webpack_require__(/*! src/constants */ "./src/constants.ts");
12634
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
12670
12635
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
12671
12636
  const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
12672
12637
  /**
@@ -12914,7 +12879,7 @@ function parseConflictPacket(packet) {
12914
12879
  * as if it was a real CDJ.
12915
12880
  */
12916
12881
  class Announcer {
12917
- constructor(vcdj, announceSocket, deviceManager, iface, fullStartup = false) {
12882
+ constructor(vcdj, announceSocket, deviceManager, iface, fullStartup = false, announceToStagehand = false, logger = logger_1.noopLogger) {
12918
12883
  _Announcer_instances.add(this);
12919
12884
  /**
12920
12885
  * The announce socket to use to make the announcements
@@ -12937,6 +12902,11 @@ class Announcer {
12937
12902
  * Whether to use full startup protocol
12938
12903
  */
12939
12904
  _Announcer_fullStartup.set(this, void 0);
12905
+ /**
12906
+ * Whether to send announcer packets to Pioneer Stagehand devices, which are
12907
+ * normally excluded because they crash on our packets
12908
+ */
12909
+ _Announcer_announceToStagehand.set(this, void 0);
12940
12910
  /**
12941
12911
  * Current startup stage (only used when fullStartup is enabled)
12942
12912
  */
@@ -12953,11 +12923,36 @@ class Announcer {
12953
12923
  * Network interface for the virtual CDJ
12954
12924
  */
12955
12925
  _Announcer_iface.set(this, void 0);
12926
+ /**
12927
+ * Logger instance for diagnostic output
12928
+ */
12929
+ _Announcer_logger.set(this, void 0);
12930
+ /**
12931
+ * Callback invoked when the full startup protocol completes
12932
+ */
12933
+ _Announcer_onStartupComplete.set(this, void 0);
12956
12934
  __classPrivateFieldSet(this, _Announcer_vcdj, vcdj, "f");
12957
12935
  __classPrivateFieldSet(this, _Announcer_announceSocket, announceSocket, "f");
12958
12936
  __classPrivateFieldSet(this, _Announcer_deviceManager, deviceManager, "f");
12959
12937
  __classPrivateFieldSet(this, _Announcer_iface, iface, "f");
12960
12938
  __classPrivateFieldSet(this, _Announcer_fullStartup, fullStartup, "f");
12939
+ __classPrivateFieldSet(this, _Announcer_announceToStagehand, announceToStagehand, "f");
12940
+ __classPrivateFieldSet(this, _Announcer_logger, logger, "f");
12941
+ }
12942
+ /**
12943
+ * Returns a promise that resolves when the startup protocol completes.
12944
+ * Resolves immediately if fullStartup is disabled.
12945
+ */
12946
+ get ready() {
12947
+ if (!__classPrivateFieldGet(this, _Announcer_fullStartup, "f")) {
12948
+ return Promise.resolve();
12949
+ }
12950
+ if (__classPrivateFieldGet(this, _Announcer_currentStage, "f") === StartupStage.KeepAlive) {
12951
+ return Promise.resolve();
12952
+ }
12953
+ return new Promise(resolve => {
12954
+ __classPrivateFieldSet(this, _Announcer_onStartupComplete, resolve, "f");
12955
+ });
12961
12956
  }
12962
12957
  start() {
12963
12958
  if (__classPrivateFieldGet(this, _Announcer_fullStartup, "f")) {
@@ -12981,14 +12976,14 @@ class Announcer {
12981
12976
  }
12982
12977
  }
12983
12978
  exports.Announcer = Announcer;
12984
- _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMap(), _Announcer_vcdj = new WeakMap(), _Announcer_intervalHandle = new WeakMap(), _Announcer_fullStartup = new WeakMap(), _Announcer_currentStage = new WeakMap(), _Announcer_stageCounter = new WeakMap(), _Announcer_conflictListener = new WeakMap(), _Announcer_iface = new WeakMap(), _Announcer_instances = new WeakSet(), _Announcer_startFullStartup = function _Announcer_startFullStartup() {
12979
+ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMap(), _Announcer_vcdj = new WeakMap(), _Announcer_intervalHandle = new WeakMap(), _Announcer_fullStartup = new WeakMap(), _Announcer_announceToStagehand = new WeakMap(), _Announcer_currentStage = new WeakMap(), _Announcer_stageCounter = new WeakMap(), _Announcer_conflictListener = new WeakMap(), _Announcer_iface = new WeakMap(), _Announcer_logger = new WeakMap(), _Announcer_onStartupComplete = new WeakMap(), _Announcer_instances = new WeakSet(), _Announcer_startFullStartup = function _Announcer_startFullStartup() {
12985
12980
  __classPrivateFieldSet(this, _Announcer_currentStage, StartupStage.InitialAnnounce, "f");
12986
12981
  __classPrivateFieldSet(this, _Announcer_stageCounter, 0, "f");
12987
12982
  // Set up conflict detection listener
12988
12983
  __classPrivateFieldSet(this, _Announcer_conflictListener, (msg) => {
12989
12984
  const conflictDeviceId = parseConflictPacket(msg);
12990
12985
  if (conflictDeviceId === __classPrivateFieldGet(this, _Announcer_vcdj, "f").id) {
12991
- console.warn(`Device ID ${__classPrivateFieldGet(this, _Announcer_vcdj, "f").id} is already in use. Finding alternative...`);
12986
+ __classPrivateFieldGet(this, _Announcer_logger, "f").warn(`Device ID ${__classPrivateFieldGet(this, _Announcer_vcdj, "f").id} is already in use. Finding alternative...`);
12992
12987
  __classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_handleConflict).call(this);
12993
12988
  }
12994
12989
  }, "f");
@@ -13019,20 +13014,29 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
13019
13014
  }
13020
13015
  }
13021
13016
  if (newId === null) {
13022
- console.error('No available device IDs. All 32 slots are occupied.');
13017
+ __classPrivateFieldGet(this, _Announcer_logger, "f").error('No available device IDs. All 32 slots are occupied.');
13023
13018
  this.stop();
13024
13019
  return;
13025
13020
  }
13026
- console.log(`Switching to device ID ${newId}`);
13021
+ __classPrivateFieldGet(this, _Announcer_logger, "f").info(`Switching to device ID ${newId}`);
13027
13022
  // Update virtual CDJ with new ID
13028
13023
  __classPrivateFieldSet(this, _Announcer_vcdj, (0, exports.getVirtualCDJ)(__classPrivateFieldGet(this, _Announcer_iface, "f"), newId), "f");
13029
13024
  // Restart startup sequence
13030
13025
  __classPrivateFieldSet(this, _Announcer_currentStage, StartupStage.InitialAnnounce, "f");
13031
13026
  __classPrivateFieldSet(this, _Announcer_stageCounter, 0, "f");
13032
13027
  __classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_sendStagePackets).call(this);
13028
+ }, _Announcer_sendPacket = function _Announcer_sendPacket(packet) {
13029
+ const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()].filter(d => __classPrivateFieldGet(this, _Announcer_announceToStagehand, "f") || !d.name.toLowerCase().includes('stagehand'));
13030
+ devices.forEach(device => __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, device.ip.address));
13031
+ // Cold-start discovery broadcast — gated; see the method doc above.
13032
+ if (devices.length === 0 && __classPrivateFieldGet(this, _Announcer_announceToStagehand, "f")) {
13033
+ const broadcastAddr = (0, utils_1.getBroadcastAddress)(__classPrivateFieldGet(this, _Announcer_iface, "f"));
13034
+ __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, broadcastAddr);
13035
+ }
13033
13036
  }, _Announcer_sendStagePackets = function _Announcer_sendStagePackets() {
13034
13037
  var _a;
13035
- __classPrivateFieldSet(this, _Announcer_stageCounter, (_a = __classPrivateFieldGet(this, _Announcer_stageCounter, "f"), _a++, _a), "f");
13038
+ var _b;
13039
+ __classPrivateFieldSet(this, _Announcer_stageCounter, (_b = __classPrivateFieldGet(this, _Announcer_stageCounter, "f"), _b++, _b), "f");
13036
13040
  // Build packet for current stage
13037
13041
  let packet;
13038
13042
  switch (__classPrivateFieldGet(this, _Announcer_currentStage, "f")) {
@@ -13051,16 +13055,11 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
13051
13055
  case StartupStage.KeepAlive:
13052
13056
  // Transition to keep-alive mode
13053
13057
  __classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_startKeepAlive).call(this);
13058
+ (_a = __classPrivateFieldGet(this, _Announcer_onStartupComplete, "f")) === null || _a === void 0 ? void 0 : _a.call(this);
13059
+ __classPrivateFieldSet(this, _Announcer_onStartupComplete, undefined, "f");
13054
13060
  return;
13055
13061
  }
13056
- // Broadcast packet to all known devices
13057
- const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()];
13058
- devices.forEach(device => __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, device.ip.address));
13059
- // Also broadcast to network if no devices yet
13060
- if (devices.length === 0) {
13061
- const broadcastAddr = __classPrivateFieldGet(this, _Announcer_vcdj, "f").ip.endAddress().address;
13062
- __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, broadcastAddr);
13063
- }
13062
+ __classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_sendPacket).call(this, packet);
13064
13063
  // Progress to next packet or stage
13065
13064
  if (__classPrivateFieldGet(this, _Announcer_stageCounter, "f") >= 3) {
13066
13065
  // Move to next stage
@@ -13099,13 +13098,7 @@ _Announcer_announceSocket = new WeakMap(), _Announcer_deviceManager = new WeakMa
13099
13098
  const packet = __classPrivateFieldGet(this, _Announcer_fullStartup, "f")
13100
13099
  ? makeStage06Packet(__classPrivateFieldGet(this, _Announcer_vcdj, "f"), peerCount)
13101
13100
  : makeAnnouncePacket(__classPrivateFieldGet(this, _Announcer_vcdj, "f"));
13102
- const devices = [...__classPrivateFieldGet(this, _Announcer_deviceManager, "f").devices.values()];
13103
- devices.forEach(device => __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, device.ip.address));
13104
- // Also broadcast if no devices
13105
- if (devices.length === 0) {
13106
- const broadcastAddr = __classPrivateFieldGet(this, _Announcer_vcdj, "f").ip.endAddress().address;
13107
- __classPrivateFieldGet(this, _Announcer_announceSocket, "f").send(packet, constants_1.ANNOUNCE_PORT, broadcastAddr);
13108
- }
13101
+ __classPrivateFieldGet(this, _Announcer_instances, "m", _Announcer_sendPacket).call(this, packet);
13109
13102
  };
13110
13103
  // Send first keep-alive immediately
13111
13104
  sendKeepAlive();
@@ -13259,6 +13252,17 @@ module.exports = require("lodash");
13259
13252
 
13260
13253
  /***/ },
13261
13254
 
13255
+ /***/ "metadata-connect"
13256
+ /*!***********************************!*\
13257
+ !*** external "metadata-connect" ***!
13258
+ \***********************************/
13259
+ (module) {
13260
+
13261
+ "use strict";
13262
+ module.exports = require("metadata-connect");
13263
+
13264
+ /***/ },
13265
+
13262
13266
  /***/ "net"
13263
13267
  /*!**********************!*\
13264
13268
  !*** external "net" ***!
@@ -13270,6 +13274,17 @@ module.exports = require("net");
13270
13274
 
13271
13275
  /***/ },
13272
13276
 
13277
+ /***/ "onelibrary-connect"
13278
+ /*!*************************************!*\
13279
+ !*** external "onelibrary-connect" ***!
13280
+ \*************************************/
13281
+ (module) {
13282
+
13283
+ "use strict";
13284
+ module.exports = require("onelibrary-connect");
13285
+
13286
+ /***/ },
13287
+
13273
13288
  /***/ "os"
13274
13289
  /*!*********************!*\
13275
13290
  !*** external "os" ***!
@@ -13323,17 +13338,6 @@ module.exports = require("promise-socket");
13323
13338
  "use strict";
13324
13339
  module.exports = require("promise-timeout");
13325
13340
 
13326
- /***/ },
13327
-
13328
- /***/ "zlib"
13329
- /*!***********************!*\
13330
- !*** external "zlib" ***!
13331
- \***********************/
13332
- (module) {
13333
-
13334
- "use strict";
13335
- module.exports = require("zlib");
13336
-
13337
13341
  /***/ }
13338
13342
 
13339
13343
  /******/ });