alphatheta-connect 0.25.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -1379,25 +1379,24 @@ async function viaRemote(remote, opts) {
1379
1379
  return track;
1380
1380
  }
1381
1381
  async function viaLocal(local, device, opts) {
1382
- const { deviceId, trackSlot, trackId } = opts;
1382
+ const { deviceId, trackSlot, trackId, trackBPM } = opts;
1383
1383
  if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
1384
1384
  throw new Error('Expected USB or SD slot for local database query');
1385
1385
  }
1386
- const adapter = await local.get(deviceId, trackSlot);
1387
- if (adapter === null) {
1388
- return { track: null, miss: 'no-database' };
1386
+ const lookup = await local.findTrack(deviceId, trackSlot, trackId, { trackBPM });
1387
+ if (lookup.adapter === null) {
1388
+ return { track: null, miss: 'no-database', switchedTo: null };
1389
1389
  }
1390
- const dbTrack = adapter.findTrack(trackId);
1391
- if (dbTrack === null) {
1392
- return { track: null, miss: 'track-absent' };
1390
+ if (lookup.track === null) {
1391
+ return { track: null, miss: 'track-absent', switchedTo: null };
1393
1392
  }
1394
- const anlz = await (0, rekordbox_1.loadAnlz)(dbTrack, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1393
+ const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1395
1394
  const track = {
1396
- ...dbTrack,
1395
+ ...lookup.track,
1397
1396
  beatGrid: anlz.beatGrid,
1398
1397
  waveformHd: null,
1399
1398
  };
1400
- return { track, miss: null };
1399
+ return { track, miss: null, switchedTo: lookup.switchedTo };
1401
1400
  }
1402
1401
 
1403
1402
 
@@ -1721,6 +1720,7 @@ class Database {
1721
1720
  * Retrieve metadata for a track on a specific device slot.
1722
1721
  */
1723
1722
  async getMetadata(opts) {
1723
+ var _a;
1724
1724
  const { deviceId, trackType, trackSlot, span } = opts;
1725
1725
  const tx = span
1726
1726
  ? span.startChild({ op: 'dbGetMetadata' })
@@ -1728,7 +1728,7 @@ class Database {
1728
1728
  tx.setTag('deviceId', deviceId.toString());
1729
1729
  tx.setTag('trackType', (0, utils_1.getTrackTypeName)(trackType));
1730
1730
  tx.setTag('trackSlot', (0, utils_1.getSlotName)(trackSlot));
1731
- const callOpts = { ...opts, span: tx };
1731
+ const callOpts = { ...opts, trackBPM: (_a = opts.trackBPM) !== null && _a !== void 0 ? _a : null, span: tx };
1732
1732
  const device = await __classPrivateFieldGet(this, _Database_deviceManager, "f").getDeviceEnsured(deviceId);
1733
1733
  if (device === null) {
1734
1734
  return null;
@@ -1741,6 +1741,12 @@ class Database {
1741
1741
  if (strategy === LookupStrategy.Local) {
1742
1742
  const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
1743
1743
  track = local.track;
1744
+ if (local.switchedTo !== null) {
1745
+ __classPrivateFieldGet(this, _Database_logger, "f").info(`Device ${deviceId} ${(0, utils_1.getSlotName)(trackSlot)} is now read from the ` +
1746
+ `${local.switchedTo === 'pdb' ? 'legacy PDB' : 'OneLibrary'} database: ` +
1747
+ `it is the one that holds track ${opts.trackId} as the player reports it ` +
1748
+ `(NP3-399)`);
1749
+ }
1744
1750
  // A local miss used to end the lookup, so the DJ's track silently never
1745
1751
  // reached the overlay for the rest of the set (NP3-361). Say what was
1746
1752
  // missed, then give the CDJ's own database a chance to answer.
@@ -2250,7 +2256,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
2250
2256
  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");
2251
2257
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
2252
2258
  };
2253
- 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;
2259
+ var _LocalDatabase_instances, _LocalDatabase_hostDevice, _LocalDatabase_deviceManager, _LocalDatabase_statusEmitter, _LocalDatabase_emitter, _LocalDatabase_slotLocks, _LocalDatabase_dbs, _LocalDatabase_preference, _LocalDatabase_handleDeviceRemoved, _LocalDatabase_closeLoaded, _LocalDatabase_fetchFileWithFallback, _LocalDatabase_tryLoadOneLibrary, _LocalDatabase_loadPdbDatabase, _LocalDatabase_hydrateDatabase, _LocalDatabase_loadAlternate, _LocalDatabase_getItem;
2254
2260
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2255
2261
  const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
2256
2262
  const onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
@@ -2266,6 +2272,18 @@ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
2266
2272
  const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
2267
2273
  const orm_1 = __webpack_require__(/*! ./orm */ "./src/localdb/orm.ts");
2268
2274
  const rekordbox_1 = __webpack_require__(/*! ./rekordbox */ "./src/localdb/rekordbox.ts");
2275
+ /**
2276
+ * A row is only trusted when the player's reported BPM agrees with it. Either
2277
+ * side missing (unanalysed track, or a player that reports no BPM) is
2278
+ * inconclusive and counts as agreement.
2279
+ */
2280
+ const trackAgreesWithPlayer = (track, hint) => {
2281
+ const reported = hint.trackBPM;
2282
+ if (reported === null || reported === undefined || !track.tempo) {
2283
+ return true;
2284
+ }
2285
+ return Math.abs(track.tempo - reported) < 0.05;
2286
+ };
2269
2287
  /**
2270
2288
  * Compute the identifier for media device in a CDJ. This is used to determine
2271
2289
  * if we have already hydrated the device or not into our local database.
@@ -2292,6 +2310,7 @@ const getMediaId = (info) => {
2292
2310
  */
2293
2311
  class LocalDatabase {
2294
2312
  constructor(hostDevice, deviceManager, statusEmitter, preference = 'auto') {
2313
+ _LocalDatabase_instances.add(this);
2295
2314
  _LocalDatabase_hostDevice.set(this, void 0);
2296
2315
  _LocalDatabase_deviceManager.set(this, void 0);
2297
2316
  _LocalDatabase_statusEmitter.set(this, void 0);
@@ -2323,19 +2342,25 @@ class LocalDatabase {
2323
2342
  _LocalDatabase_handleDeviceRemoved.set(this, (device) => {
2324
2343
  const db = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.media.deviceId === device.id);
2325
2344
  if (db) {
2326
- db.adapter.close();
2327
- // Clean up temp file if it exists (OneLibrary databases)
2328
- if (db.tempFile) {
2329
- try {
2330
- fs.unlinkSync(db.tempFile);
2331
- }
2332
- catch {
2333
- // Ignore cleanup errors
2334
- }
2345
+ __classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db);
2346
+ if (db.alternate) {
2347
+ __classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db.alternate);
2335
2348
  }
2336
2349
  }
2337
2350
  __classPrivateFieldSet(this, _LocalDatabase_dbs, __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").filter(db => db.media.deviceId !== device.id), "f");
2338
2351
  });
2352
+ _LocalDatabase_closeLoaded.set(this, (loaded) => {
2353
+ loaded.adapter.close();
2354
+ // Clean up temp file if it exists (OneLibrary databases)
2355
+ if (loaded.tempFile) {
2356
+ try {
2357
+ fs.unlinkSync(loaded.tempFile);
2358
+ }
2359
+ catch {
2360
+ // Ignore cleanup errors
2361
+ }
2362
+ }
2363
+ });
2339
2364
  /**
2340
2365
  * Helper to fetch a file from device, trying both dotted and non-dotted paths
2341
2366
  */
@@ -2442,11 +2467,54 @@ class LocalDatabase {
2442
2467
  }
2443
2468
  }
2444
2469
  __classPrivateFieldGet(this, _LocalDatabase_emitter, "f").emit('hydrationDone', { device, slot });
2445
- const db = { adapter, media, id: getMediaId(media), tempFile };
2470
+ const db = {
2471
+ adapter,
2472
+ media,
2473
+ device,
2474
+ slot,
2475
+ id: getMediaId(media),
2476
+ tempFile,
2477
+ alternateLock: new async_mutex_1.Mutex(),
2478
+ };
2446
2479
  __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").push(db);
2447
2480
  tx.finish();
2448
2481
  return db;
2449
2482
  });
2483
+ /**
2484
+ * Loads the database format the slot is *not* currently served from, so a
2485
+ * lookup can be checked against it. Only meaningful under the 'auto'
2486
+ * preference: a forced format has no alternate. The result (or its absence)
2487
+ * is remembered so the media is never downloaded twice.
2488
+ */
2489
+ _LocalDatabase_loadAlternate.set(this, (db) => db.alternateLock.runExclusive(async () => {
2490
+ if (db.alternate !== undefined) {
2491
+ return db.alternate;
2492
+ }
2493
+ if (__classPrivateFieldGet(this, _LocalDatabase_preference, "f") !== 'auto') {
2494
+ db.alternate = null;
2495
+ return null;
2496
+ }
2497
+ const tx = Telemetry.startTransaction({ name: 'hydrateAlternateDatabase' });
2498
+ tx.setTag('slot', (0, utils_1.getSlotName)(db.media.slot));
2499
+ tx.setTag('activeDbType', db.adapter.type);
2500
+ try {
2501
+ if (db.adapter.type === 'oneLibrary') {
2502
+ const adapter = await __classPrivateFieldGet(this, _LocalDatabase_loadPdbDatabase, "f").call(this, db.device, db.slot, tx);
2503
+ db.alternate = { adapter };
2504
+ tx.setTag('dbType', 'pdb');
2505
+ }
2506
+ else {
2507
+ db.alternate = await __classPrivateFieldGet(this, _LocalDatabase_tryLoadOneLibrary, "f").call(this, db.device, db.slot, tx);
2508
+ tx.setTag('dbType', db.alternate ? 'oneLibrary' : 'none');
2509
+ }
2510
+ }
2511
+ catch {
2512
+ // The media simply has no such file (or it could not be read).
2513
+ db.alternate = null;
2514
+ }
2515
+ tx.finish();
2516
+ return db.alternate;
2517
+ }));
2450
2518
  __classPrivateFieldSet(this, _LocalDatabase_hostDevice, hostDevice, "f");
2451
2519
  __classPrivateFieldSet(this, _LocalDatabase_deviceManager, deviceManager, "f");
2452
2520
  __classPrivateFieldSet(this, _LocalDatabase_statusEmitter, statusEmitter, "f");
@@ -2471,6 +2539,45 @@ class LocalDatabase {
2471
2539
  disconnectForDevice(device) {
2472
2540
  __classPrivateFieldGet(this, _LocalDatabase_handleDeviceRemoved, "f").call(this, device);
2473
2541
  }
2542
+ /**
2543
+ * Looks a track up in the databases of a device slot, checking the answer
2544
+ * against what the player reports.
2545
+ *
2546
+ * A Device Library Plus export carries both `exportLibrary.db` and the
2547
+ * legacy `export.pdb`, and their track IDs are different number spaces. A
2548
+ * player that reads one while we loaded the other resolves most IDs to a
2549
+ * different track and some to nothing at all (NP3-399). So when the active
2550
+ * database has no such row, or its row's BPM is not the BPM the player is
2551
+ * showing, the other format is loaded and asked. If it agrees with the
2552
+ * player, the slot switches to it for every later lookup — artwork,
2553
+ * analysis and the next track all follow the database the player is using.
2554
+ *
2555
+ * When neither database can be confirmed, the active database's row (if any)
2556
+ * is returned as before.
2557
+ */
2558
+ async findTrack(deviceId, slot, trackId, hint = {}) {
2559
+ const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
2560
+ if (db === null) {
2561
+ return { adapter: null, track: null, switchedTo: null };
2562
+ }
2563
+ const active = db.adapter.findTrack(trackId);
2564
+ if (active !== null && trackAgreesWithPlayer(active, hint)) {
2565
+ return { adapter: db.adapter, track: active, switchedTo: null };
2566
+ }
2567
+ const alternate = await __classPrivateFieldGet(this, _LocalDatabase_loadAlternate, "f").call(this, db);
2568
+ if (alternate !== null) {
2569
+ const other = alternate.adapter.findTrack(trackId);
2570
+ if (other !== null && trackAgreesWithPlayer(other, hint)) {
2571
+ db.alternate = { adapter: db.adapter, tempFile: db.tempFile };
2572
+ db.adapter = alternate.adapter;
2573
+ db.tempFile = alternate.tempFile;
2574
+ return { adapter: db.adapter, track: other, switchedTo: db.adapter.type };
2575
+ }
2576
+ }
2577
+ return active === null
2578
+ ? { adapter: db.adapter, track: null, switchedTo: null }
2579
+ : { adapter: db.adapter, track: active, switchedTo: null };
2580
+ }
2474
2581
  /**
2475
2582
  * Gets the database adapter for the media metadata in the provided device slot.
2476
2583
  *
@@ -2481,43 +2588,8 @@ class LocalDatabase {
2481
2588
  */
2482
2589
  async get(deviceId, slot) {
2483
2590
  var _a;
2484
- const lockKey = `${deviceId}-${slot}`;
2485
- const lock = (_a = __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").get(lockKey)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").set(lockKey, new async_mutex_1.Mutex()).get(lockKey);
2486
- const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
2487
- if (device === undefined) {
2488
- return null;
2489
- }
2490
- if (device.type !== types_1.DeviceType.CDJ ||
2491
- device.id < constants_1.MIN_CDJ_DEVICE_ID ||
2492
- device.id > constants_1.MAX_CDJ_DEVICE_ID) {
2493
- return null;
2494
- }
2495
- let media;
2496
- try {
2497
- media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
2498
- hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
2499
- device,
2500
- slot,
2501
- });
2502
- }
2503
- catch {
2504
- // Timeout or other error - treat as no media
2505
- return null;
2506
- }
2507
- if (media.tracksType !== types_1.TrackType.RB) {
2508
- return null;
2509
- }
2510
- const id = getMediaId(media);
2511
- // Acquire a lock for this device slot that will not release until we've
2512
- // guaranteed the existence of the database.
2513
- const db = await lock.runExclusive(() => {
2514
- const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
2515
- if (cached) {
2516
- return cached;
2517
- }
2518
- return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
2519
- });
2520
- return db.adapter;
2591
+ const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
2592
+ return (_a = db === null || db === void 0 ? void 0 : db.adapter) !== null && _a !== void 0 ? _a : null;
2521
2593
  }
2522
2594
  /**
2523
2595
  * Get the database type for an already-loaded device slot.
@@ -2543,7 +2615,50 @@ class LocalDatabase {
2543
2615
  await Promise.all(loaders);
2544
2616
  }
2545
2617
  }
2546
- _LocalDatabase_hostDevice = new WeakMap(), _LocalDatabase_deviceManager = new WeakMap(), _LocalDatabase_statusEmitter = new WeakMap(), _LocalDatabase_emitter = new WeakMap(), _LocalDatabase_slotLocks = new WeakMap(), _LocalDatabase_dbs = new WeakMap(), _LocalDatabase_preference = new WeakMap(), _LocalDatabase_handleDeviceRemoved = new WeakMap(), _LocalDatabase_fetchFileWithFallback = new WeakMap(), _LocalDatabase_tryLoadOneLibrary = new WeakMap(), _LocalDatabase_loadPdbDatabase = new WeakMap(), _LocalDatabase_hydrateDatabase = new WeakMap();
2618
+ _LocalDatabase_hostDevice = new WeakMap(), _LocalDatabase_deviceManager = new WeakMap(), _LocalDatabase_statusEmitter = new WeakMap(), _LocalDatabase_emitter = new WeakMap(), _LocalDatabase_slotLocks = new WeakMap(), _LocalDatabase_dbs = new WeakMap(), _LocalDatabase_preference = new WeakMap(), _LocalDatabase_handleDeviceRemoved = new WeakMap(), _LocalDatabase_closeLoaded = new WeakMap(), _LocalDatabase_fetchFileWithFallback = new WeakMap(), _LocalDatabase_tryLoadOneLibrary = new WeakMap(), _LocalDatabase_loadPdbDatabase = new WeakMap(), _LocalDatabase_hydrateDatabase = new WeakMap(), _LocalDatabase_loadAlternate = new WeakMap(), _LocalDatabase_instances = new WeakSet(), _LocalDatabase_getItem =
2619
+ /**
2620
+ * The loaded database entry for a device slot, hydrating it first if needed.
2621
+ */
2622
+ async function _LocalDatabase_getItem(deviceId, slot) {
2623
+ var _a;
2624
+ const lockKey = `${deviceId}-${slot}`;
2625
+ const lock = (_a = __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").get(lockKey)) !== null && _a !== void 0 ? _a : __classPrivateFieldGet(this, _LocalDatabase_slotLocks, "f").set(lockKey, new async_mutex_1.Mutex()).get(lockKey);
2626
+ const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
2627
+ if (device === undefined) {
2628
+ return null;
2629
+ }
2630
+ if (device.type !== types_1.DeviceType.CDJ ||
2631
+ device.id < constants_1.MIN_CDJ_DEVICE_ID ||
2632
+ device.id > constants_1.MAX_CDJ_DEVICE_ID) {
2633
+ return null;
2634
+ }
2635
+ let media;
2636
+ try {
2637
+ media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
2638
+ hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
2639
+ device,
2640
+ slot,
2641
+ });
2642
+ }
2643
+ catch {
2644
+ // Timeout or other error - treat as no media
2645
+ return null;
2646
+ }
2647
+ if (media.tracksType !== types_1.TrackType.RB) {
2648
+ return null;
2649
+ }
2650
+ const id = getMediaId(media);
2651
+ // Acquire a lock for this device slot that will not release until we've
2652
+ // guaranteed the existence of the database.
2653
+ const db = await lock.runExclusive(() => {
2654
+ const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
2655
+ if (cached) {
2656
+ return cached;
2657
+ }
2658
+ return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
2659
+ });
2660
+ return db;
2661
+ };
2547
2662
  exports["default"] = LocalDatabase;
2548
2663
 
2549
2664