alphatheta-connect 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,22 +23,22 @@ Alternative implementations of the Prolink protocol: [Java](https://github.com/D
23
23
 
24
24
  ## Features
25
25
 
26
- - **Streaming Service Detection** *(new)* - Detect when tracks are loaded from
26
+ - **Streaming Service Detection** _(new)_ - Detect when tracks are loaded from
27
27
  streaming services (Beatport, Streaming Direct Play, TIDAL, Apple Music) via
28
28
  `MediaSlot` enum. Supports CDJ-3000X CloudDirectPlay streaming features.
29
29
 
30
- - **Support for AlphaTheta Opus Quad, XDJ-RX3, XDJ-RX2, XDJ-RX, and XDJ-XZ** *(new)* -
30
+ - **Support for AlphaTheta Opus Quad, XDJ-RX3, XDJ-RX2, XDJ-RX, and XDJ-XZ** _(new)_ -
31
31
  Passive mode monitoring via pcap-based packet capture for all-in-one controllers
32
32
  where traditional virtual CDJ connection isn't possible.
33
33
  See [ALL_IN_ONE_UNITS.md](docs/ALL_IN_ONE_UNITS.md) for details.
34
34
 
35
- - **Pioneer Stagehand Connection Mode** *(new)* - Actively join the Pro DJ Link
35
+ - **Pioneer Stagehand Connection Mode** _(new)_ - Actively join the Pro DJ Link
36
36
  network as a virtual Stagehand iPad device. Access rich, high-frequency telemetry
37
37
  events (mixer faders, EQ values, high-frequency VU level samples) and direct remote
38
38
  control over CDJs (play, pause, seek, track skip, preference writes like On-Air).
39
39
  See [STAGEHAND.md](docs/STAGEHAND.md) for details.
40
40
 
41
- - **OneLibrary Support** *(new)* - Full support for rekordbox 7.x's new OneLibrary
41
+ - **OneLibrary Support** _(new)_ - Full support for rekordbox 7.x's new OneLibrary
42
42
  format (exportLibrary.db) with SQLCipher encryption, including tracks, playlists,
43
43
  cues, hot cue banks, myTags, and history.
44
44
 
@@ -56,15 +56,15 @@ Alternative implementations of the Prolink protocol: [Java](https://github.com/D
56
56
  sequence for better compatibility with certain device configurations.
57
57
  See [FULL_STARTUP.md](docs/FULL_STARTUP.md) for details.
58
58
 
59
- - **Metadata Extraction via NFS** *(new)* - Extract complete track metadata (title,
59
+ - **Metadata Extraction via NFS** _(new)_ - Extract complete track metadata (title,
60
60
  artist, album, BPM, key, artwork) directly from audio files on connected media
61
61
  using partial file reads — only file headers are downloaded, not entire files.
62
62
 
63
- - **3-Band Waveforms & Vocal Detection** *(new)* - Parse `.2EX` analysis files for
63
+ - **3-Band Waveforms & Vocal Detection** _(new)_ - Parse `.2EX` analysis files for
64
64
  3-band color waveform preview (PWV6), 3-band color detail waveform (PWV7), and
65
65
  vocal detection config (PWVC).
66
66
 
67
- - **Track Analysis API** *(new)* - Unified `getTrackAnalysis()` method that retrieves
67
+ - **Track Analysis API** _(new)_ - Unified `getTrackAnalysis()` method that retrieves
68
68
  all analysis data (extended cues, song structure, waveforms, 3-band waveforms,
69
69
  vocal config) from both EXT and 2EX files in a single call.
70
70
 
@@ -74,6 +74,8 @@ Alternative implementations of the Prolink protocol: [Java](https://github.com/D
74
74
  - Multiple waveform formats (PWAV, PWV2, PWV3, PWV4, PWV5, PWV6, PWV7)
75
75
  - Vocal detection config (PWVC)
76
76
  - See [EXTENDED_ANLZ.md](docs/EXTENDED_ANLZ.md) for details
77
+ - See [USB_EXPORT.md](docs/USB_EXPORT.md) for complete USB format tables,
78
+ analysis paths, cue palettes, OneLibrary schema and reader limitations
77
79
 
78
80
  - **CDJ-3000 Features** - Complete support for CDJ-3000 specific features:
79
81
  - Absolute position tracking (30ms updates)
package/lib/cli.js CHANGED
@@ -1443,13 +1443,51 @@ async function viaLocal(local, device, opts) {
1443
1443
  "use strict";
1444
1444
 
1445
1445
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1446
+ exports.describeBlankMetadata = describeBlankMetadata;
1446
1447
  exports.viaRemote = viaRemote;
1447
1448
  exports.viaLocal = viaLocal;
1448
1449
  const rekordbox_1 = __webpack_require__(/*! src/localdb/rekordbox */ "./src/localdb/rekordbox.ts");
1450
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
1449
1451
  const remotedb_1 = __webpack_require__(/*! src/remotedb */ "./src/remotedb/index.ts");
1450
1452
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
1451
- const utils_1 = __webpack_require__(/*! ./utils */ "./src/db/utils.ts");
1452
- async function viaRemote(remote, opts) {
1453
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
1454
+ const utils_2 = __webpack_require__(/*! ./utils */ "./src/db/utils.ts");
1455
+ /**
1456
+ * The longest a single item is allowed to run in a blank-metadata report, so
1457
+ * one oversized field cannot swamp the log line.
1458
+ */
1459
+ const MAX_ITEM_REPORT_LENGTH = 200;
1460
+ /**
1461
+ * Explain a remote lookup that came back with neither a title nor an artist:
1462
+ * what the player was asked, how many items it sent, and each item's type and
1463
+ * fields as they arrived. A player that files a track under an unfamiliar slot
1464
+ * can answer this way (NP3-416), and without the raw items there is no telling
1465
+ * an empty answer from one we do not know how to read.
1466
+ */
1467
+ function describeBlankMetadata(opts, response, trackInfo) {
1468
+ var _a, _b, _c;
1469
+ const { deviceId, trackSlot, trackType, trackId } = opts;
1470
+ const slotName = (_a = (0, utils_1.getSlotName)(trackSlot)) !== null && _a !== void 0 ? _a : 'unknown';
1471
+ const typeName = (_b = (0, utils_1.getTrackTypeName)(trackType)) !== null && _b !== void 0 ? _b : 'unknown';
1472
+ const itemReports = ((_c = response === null || response === void 0 ? void 0 : response.items) !== null && _c !== void 0 ? _c : []).map(({ type, ...fields }) => {
1473
+ const hex = `0x${type.toString(16).padStart(4, '0')}`;
1474
+ const report = `${hex} ${JSON.stringify(fields)}`;
1475
+ return report.length > MAX_ITEM_REPORT_LENGTH
1476
+ ? `${report.slice(0, MAX_ITEM_REPORT_LENGTH)}…`
1477
+ : report;
1478
+ });
1479
+ const items = response === null
1480
+ ? 'no response recorded'
1481
+ : [
1482
+ `${response.items.length} of ${response.itemsAvailable} item(s) received`,
1483
+ ...itemReports,
1484
+ ].join('; ');
1485
+ return (`Device ${deviceId} answered the metadata query for track ${trackId} ` +
1486
+ `(slot ${trackSlot} ${slotName}, type ${trackType} ${typeName}) with no ` +
1487
+ `title or artist: ${items}; track info: ${trackInfo}`);
1488
+ }
1489
+ async function viaRemote(remote, opts, logger = logger_1.noopLogger) {
1490
+ var _a, _b, _c;
1453
1491
  const { deviceId, trackSlot, trackType, trackId, span } = opts;
1454
1492
  const conn = await remote.get(deviceId);
1455
1493
  if (conn === null) {
@@ -1465,14 +1503,23 @@ async function viaRemote(remote, opts) {
1465
1503
  const skipLocalFileLookups = isUnanalyzed || isStreaming;
1466
1504
  // Unanalyzed tracks use GetGenericMetadata (reads ID3 tags from the audio file).
1467
1505
  // Streaming tracks (Beatport) use the regular GetMetadata query.
1468
- const track = await conn.query({
1469
- queryDescriptor,
1470
- query: isUnanalyzed ? remotedb_1.Query.GetGenericMetadata : remotedb_1.Query.GetMetadata,
1471
- args: { trackId },
1472
- span,
1473
- });
1506
+ let response = null;
1507
+ const track = isUnanalyzed
1508
+ ? await conn.query({
1509
+ queryDescriptor,
1510
+ query: remotedb_1.Query.GetGenericMetadata,
1511
+ args: { trackId },
1512
+ span,
1513
+ })
1514
+ : await conn.query({
1515
+ queryDescriptor,
1516
+ query: remotedb_1.Query.GetMetadata,
1517
+ args: { trackId, onResponse: r => (response = r) },
1518
+ span,
1519
+ });
1474
1520
  // Try to get file path — for streaming tracks this returns the Beatport track ID
1475
1521
  // (e.g. "/26883657.m4a") which we use for Beatport API lookups
1522
+ let trackInfoError = null;
1476
1523
  try {
1477
1524
  track.filePath = await conn.query({
1478
1525
  queryDescriptor,
@@ -1485,6 +1532,13 @@ async function viaRemote(remote, opts) {
1485
1532
  if (!skipLocalFileLookups) {
1486
1533
  throw err;
1487
1534
  }
1535
+ trackInfoError = err instanceof Error ? err.message : String(err);
1536
+ }
1537
+ if (!((_a = track.title) === null || _a === void 0 ? void 0 : _a.trim()) && !((_c = (_b = track.artist) === null || _b === void 0 ? void 0 : _b.name) === null || _c === void 0 ? void 0 : _c.trim())) {
1538
+ const trackInfo = trackInfoError === null
1539
+ ? JSON.stringify(track.filePath)
1540
+ : `failed (${trackInfoError})`;
1541
+ logger.warn(describeBlankMetadata(opts, response, trackInfo));
1488
1542
  }
1489
1543
  // Beat grid is only available for analyzed local tracks
1490
1544
  if (!skipLocalFileLookups) {
@@ -1509,7 +1563,7 @@ async function viaLocal(local, device, opts) {
1509
1563
  if (lookup.track === null) {
1510
1564
  return { track: null, miss: 'track-absent', switchedTo: null };
1511
1565
  }
1512
- const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1566
+ const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_2.anlzLoader)({ device, slot: trackSlot }));
1513
1567
  const track = {
1514
1568
  ...lookup.track,
1515
1569
  beatGrid: anlz.beatGrid,
@@ -1857,7 +1911,7 @@ class Database {
1857
1911
  const strategy = __classPrivateFieldGet(this, _Database_getTrackLookupStrategy, "f").call(this, device, trackType);
1858
1912
  let track = null;
1859
1913
  if (strategy === LookupStrategy.Remote) {
1860
- track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), callOpts);
1914
+ track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), callOpts, __classPrivateFieldGet(this, _Database_logger, "f"));
1861
1915
  }
1862
1916
  if (strategy === LookupStrategy.Local) {
1863
1917
  const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
@@ -2084,7 +2138,7 @@ _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap()
2084
2138
  */
2085
2139
  async function _Database_metadataViaRemoteFallback(opts) {
2086
2140
  try {
2087
- const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts);
2141
+ const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts, __classPrivateFieldGet(this, _Database_logger, "f"));
2088
2142
  if (track === null) {
2089
2143
  __classPrivateFieldGet(this, _Database_logger, "f").warn(`Remote fallback found no track ${opts.trackId} on device ${opts.deviceId}`);
2090
2144
  return null;
@@ -9373,8 +9427,8 @@ const transformItem = {
9373
9427
  [ItemType.Rating]: (a) => ({ rating: a.mainId }),
9374
9428
  [ItemType.Tempo]: (a) => ({ bpm: a.mainId / 100 }),
9375
9429
  [ItemType.Duration]: (a) => ({ duration: a.mainId }),
9376
- [ItemType.Unknown01]: (_) => null,
9377
- [ItemType.Unknown02]: (_) => null,
9430
+ [ItemType.Unknown01]: (a) => a,
9431
+ [ItemType.Unknown02]: (a) => a,
9378
9432
  [ItemType.ColorNone]: mapIdName,
9379
9433
  [ItemType.ColorPink]: mapIdName,
9380
9434
  [ItemType.ColorRed]: mapIdName,
@@ -9435,9 +9489,10 @@ const fieldsToItem = (args) => {
9435
9489
  const { type } = itemData;
9436
9490
  let transformer = transformItem[type];
9437
9491
  // Typescript gives us safety, but it is possible there is an itemType we're
9438
- // not aware of yet.
9492
+ // not aware of yet. Keep its raw fields so whoever reads the response can
9493
+ // still see what the player sent.
9439
9494
  if (transformer === undefined) {
9440
- transformer = () => null;
9495
+ transformer = (a) => a;
9441
9496
  Telemetry.captureMessage(`No item transformer registered for item type ${type}`, Telemetry.Severity.Error);
9442
9497
  }
9443
9498
  return { ...transformer(itemData), type };
@@ -9759,7 +9814,7 @@ const utils_1 = __webpack_require__(/*! ./utils */ "./src/remotedb/utils.ts");
9759
9814
  async function getMetadata(opts) {
9760
9815
  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;
9761
9816
  const { conn, lookupDescriptor, span, args } = opts;
9762
- const { trackId } = args;
9817
+ const { trackId, onResponse } = args;
9763
9818
  const request = new message_1.Message({
9764
9819
  type: types_1.Request.GetMetadata,
9765
9820
  args: [(0, utils_1.fieldFromDescriptor)(lookupDescriptor), new fields_1.UInt32(trackId)],
@@ -9770,9 +9825,12 @@ async function getMetadata(opts) {
9770
9825
  // NOTE: We do a bit of any-ing here to help typescript understand we're
9771
9826
  // discriminating the type by our object key
9772
9827
  const trackItems = {};
9828
+ const received = [];
9773
9829
  for await (const item of items) {
9774
9830
  trackItems[item.type] = item;
9831
+ received.push(item);
9775
9832
  }
9833
+ onResponse === null || onResponse === void 0 ? void 0 : onResponse({ itemsAvailable: resp.data.itemsAvailable, items: received });
9776
9834
  // Translate our trackItems into a (partial) Track entity.
9777
9835
  // Use optional chaining for all fields since streaming tracks may omit some.
9778
9836
  const track = {
@@ -9974,7 +10032,11 @@ async function getTrackInfo(opts) {
9974
10032
  for await (const item of items) {
9975
10033
  infoItems[item.type] = item;
9976
10034
  }
9977
- return infoItems[item_1.ItemType.Path].path;
10035
+ const path = infoItems[item_1.ItemType.Path];
10036
+ if (path === undefined) {
10037
+ throw new Error(`Player sent no file path (${resp.data.itemsAvailable} item(s) available)`);
10038
+ }
10039
+ return path.path;
9978
10040
  }
9979
10041
  /**
9980
10042
  * Lookup playlist entries