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.
@@ -1,5 +1,6 @@
1
1
  import { Track } from "../entities";
2
2
  import LocalDatabase from "../localdb";
3
+ import { DatabaseType } from "../localdb/database-adapter";
3
4
  import RemoteDatabase from "../remotedb";
4
5
  import { Device, DeviceID, MediaSlot, TrackType } from "../types";
5
6
  import { TelemetrySpan as Span } from "../utils/telemetry";
@@ -20,6 +21,12 @@ export interface Options {
20
21
  * The track id to retrieve metadata for
21
22
  */
22
23
  trackId: number;
24
+ /**
25
+ * The track's BPM as the player reports it in its status packet, when known.
26
+ * Used to check that the local database row for `trackId` is really the
27
+ * track the player is showing (see {@link LocalDatabase.findTrack}).
28
+ */
29
+ trackBPM?: number | null;
23
30
  /**
24
31
  * The Sentry transaction span
25
32
  */
@@ -43,9 +50,15 @@ export type LocalMiss =
43
50
  export type LocalResult = {
44
51
  track: Track;
45
52
  miss: null;
53
+ /**
54
+ * Set when the slot had to switch to its other database format to
55
+ * answer: the format it is now served from.
56
+ */
57
+ switchedTo: DatabaseType | null;
46
58
  } | {
47
59
  track: null;
48
60
  miss: LocalMiss;
61
+ switchedTo: null;
49
62
  };
50
63
  export declare function viaRemote(remote: RemoteDatabase, opts: Required<Options>): Promise<Track<import("../entities").EntityFK.WithRelations> | null>;
51
64
  export declare function viaLocal(local: LocalDatabase, device: Device, opts: Required<Options>): Promise<LocalResult>;
package/lib/index.js CHANGED
@@ -1268,25 +1268,24 @@ async function viaRemote(remote, opts) {
1268
1268
  return track;
1269
1269
  }
1270
1270
  async function viaLocal(local, device, opts) {
1271
- const { deviceId, trackSlot, trackId } = opts;
1271
+ const { deviceId, trackSlot, trackId, trackBPM } = opts;
1272
1272
  if (trackSlot !== types_1.MediaSlot.USB && trackSlot !== types_1.MediaSlot.SD) {
1273
1273
  throw new Error('Expected USB or SD slot for local database query');
1274
1274
  }
1275
- const adapter = await local.get(deviceId, trackSlot);
1276
- if (adapter === null) {
1277
- return { track: null, miss: 'no-database' };
1275
+ const lookup = await local.findTrack(deviceId, trackSlot, trackId, { trackBPM });
1276
+ if (lookup.adapter === null) {
1277
+ return { track: null, miss: 'no-database', switchedTo: null };
1278
1278
  }
1279
- const dbTrack = adapter.findTrack(trackId);
1280
- if (dbTrack === null) {
1281
- return { track: null, miss: 'track-absent' };
1279
+ if (lookup.track === null) {
1280
+ return { track: null, miss: 'track-absent', switchedTo: null };
1282
1281
  }
1283
- const anlz = await (0, rekordbox_1.loadAnlz)(dbTrack, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1282
+ const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1284
1283
  const track = {
1285
- ...dbTrack,
1284
+ ...lookup.track,
1286
1285
  beatGrid: anlz.beatGrid,
1287
1286
  waveformHd: null,
1288
1287
  };
1289
- return { track, miss: null };
1288
+ return { track, miss: null, switchedTo: lookup.switchedTo };
1290
1289
  }
1291
1290
 
1292
1291
 
@@ -1610,6 +1609,7 @@ class Database {
1610
1609
  * Retrieve metadata for a track on a specific device slot.
1611
1610
  */
1612
1611
  async getMetadata(opts) {
1612
+ var _a;
1613
1613
  const { deviceId, trackType, trackSlot, span } = opts;
1614
1614
  const tx = span
1615
1615
  ? span.startChild({ op: 'dbGetMetadata' })
@@ -1617,7 +1617,7 @@ class Database {
1617
1617
  tx.setTag('deviceId', deviceId.toString());
1618
1618
  tx.setTag('trackType', (0, utils_1.getTrackTypeName)(trackType));
1619
1619
  tx.setTag('trackSlot', (0, utils_1.getSlotName)(trackSlot));
1620
- const callOpts = { ...opts, span: tx };
1620
+ const callOpts = { ...opts, trackBPM: (_a = opts.trackBPM) !== null && _a !== void 0 ? _a : null, span: tx };
1621
1621
  const device = await __classPrivateFieldGet(this, _Database_deviceManager, "f").getDeviceEnsured(deviceId);
1622
1622
  if (device === null) {
1623
1623
  return null;
@@ -1630,6 +1630,12 @@ class Database {
1630
1630
  if (strategy === LookupStrategy.Local) {
1631
1631
  const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
1632
1632
  track = local.track;
1633
+ if (local.switchedTo !== null) {
1634
+ __classPrivateFieldGet(this, _Database_logger, "f").info(`Device ${deviceId} ${(0, utils_1.getSlotName)(trackSlot)} is now read from the ` +
1635
+ `${local.switchedTo === 'pdb' ? 'legacy PDB' : 'OneLibrary'} database: ` +
1636
+ `it is the one that holds track ${opts.trackId} as the player reports it ` +
1637
+ `(NP3-399)`);
1638
+ }
1633
1639
  // A local miss used to end the lookup, so the DJ's track silently never
1634
1640
  // reached the overlay for the rest of the set (NP3-361). Say what was
1635
1641
  // missed, then give the CDJ's own database a chance to answer.
@@ -2220,7 +2226,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
2220
2226
  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");
2221
2227
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
2222
2228
  };
2223
- 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;
2229
+ 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;
2224
2230
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2225
2231
  const async_mutex_1 = __webpack_require__(/*! async-mutex */ "async-mutex");
2226
2232
  const onelibrary_connect_1 = __webpack_require__(/*! onelibrary-connect */ "onelibrary-connect");
@@ -2236,6 +2242,18 @@ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
2236
2242
  const Telemetry = __importStar(__webpack_require__(/*! src/utils/telemetry */ "./src/utils/telemetry.ts"));
2237
2243
  const orm_1 = __webpack_require__(/*! ./orm */ "./src/localdb/orm.ts");
2238
2244
  const rekordbox_1 = __webpack_require__(/*! ./rekordbox */ "./src/localdb/rekordbox.ts");
2245
+ /**
2246
+ * A row is only trusted when the player's reported BPM agrees with it. Either
2247
+ * side missing (unanalysed track, or a player that reports no BPM) is
2248
+ * inconclusive and counts as agreement.
2249
+ */
2250
+ const trackAgreesWithPlayer = (track, hint) => {
2251
+ const reported = hint.trackBPM;
2252
+ if (reported === null || reported === undefined || !track.tempo) {
2253
+ return true;
2254
+ }
2255
+ return Math.abs(track.tempo - reported) < 0.05;
2256
+ };
2239
2257
  /**
2240
2258
  * Compute the identifier for media device in a CDJ. This is used to determine
2241
2259
  * if we have already hydrated the device or not into our local database.
@@ -2262,6 +2280,7 @@ const getMediaId = (info) => {
2262
2280
  */
2263
2281
  class LocalDatabase {
2264
2282
  constructor(hostDevice, deviceManager, statusEmitter, preference = 'auto') {
2283
+ _LocalDatabase_instances.add(this);
2265
2284
  _LocalDatabase_hostDevice.set(this, void 0);
2266
2285
  _LocalDatabase_deviceManager.set(this, void 0);
2267
2286
  _LocalDatabase_statusEmitter.set(this, void 0);
@@ -2293,19 +2312,25 @@ class LocalDatabase {
2293
2312
  _LocalDatabase_handleDeviceRemoved.set(this, (device) => {
2294
2313
  const db = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.media.deviceId === device.id);
2295
2314
  if (db) {
2296
- db.adapter.close();
2297
- // Clean up temp file if it exists (OneLibrary databases)
2298
- if (db.tempFile) {
2299
- try {
2300
- fs.unlinkSync(db.tempFile);
2301
- }
2302
- catch {
2303
- // Ignore cleanup errors
2304
- }
2315
+ __classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db);
2316
+ if (db.alternate) {
2317
+ __classPrivateFieldGet(this, _LocalDatabase_closeLoaded, "f").call(this, db.alternate);
2305
2318
  }
2306
2319
  }
2307
2320
  __classPrivateFieldSet(this, _LocalDatabase_dbs, __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").filter(db => db.media.deviceId !== device.id), "f");
2308
2321
  });
2322
+ _LocalDatabase_closeLoaded.set(this, (loaded) => {
2323
+ loaded.adapter.close();
2324
+ // Clean up temp file if it exists (OneLibrary databases)
2325
+ if (loaded.tempFile) {
2326
+ try {
2327
+ fs.unlinkSync(loaded.tempFile);
2328
+ }
2329
+ catch {
2330
+ // Ignore cleanup errors
2331
+ }
2332
+ }
2333
+ });
2309
2334
  /**
2310
2335
  * Helper to fetch a file from device, trying both dotted and non-dotted paths
2311
2336
  */
@@ -2412,11 +2437,54 @@ class LocalDatabase {
2412
2437
  }
2413
2438
  }
2414
2439
  __classPrivateFieldGet(this, _LocalDatabase_emitter, "f").emit('hydrationDone', { device, slot });
2415
- const db = { adapter, media, id: getMediaId(media), tempFile };
2440
+ const db = {
2441
+ adapter,
2442
+ media,
2443
+ device,
2444
+ slot,
2445
+ id: getMediaId(media),
2446
+ tempFile,
2447
+ alternateLock: new async_mutex_1.Mutex(),
2448
+ };
2416
2449
  __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").push(db);
2417
2450
  tx.finish();
2418
2451
  return db;
2419
2452
  });
2453
+ /**
2454
+ * Loads the database format the slot is *not* currently served from, so a
2455
+ * lookup can be checked against it. Only meaningful under the 'auto'
2456
+ * preference: a forced format has no alternate. The result (or its absence)
2457
+ * is remembered so the media is never downloaded twice.
2458
+ */
2459
+ _LocalDatabase_loadAlternate.set(this, (db) => db.alternateLock.runExclusive(async () => {
2460
+ if (db.alternate !== undefined) {
2461
+ return db.alternate;
2462
+ }
2463
+ if (__classPrivateFieldGet(this, _LocalDatabase_preference, "f") !== 'auto') {
2464
+ db.alternate = null;
2465
+ return null;
2466
+ }
2467
+ const tx = Telemetry.startTransaction({ name: 'hydrateAlternateDatabase' });
2468
+ tx.setTag('slot', (0, utils_1.getSlotName)(db.media.slot));
2469
+ tx.setTag('activeDbType', db.adapter.type);
2470
+ try {
2471
+ if (db.adapter.type === 'oneLibrary') {
2472
+ const adapter = await __classPrivateFieldGet(this, _LocalDatabase_loadPdbDatabase, "f").call(this, db.device, db.slot, tx);
2473
+ db.alternate = { adapter };
2474
+ tx.setTag('dbType', 'pdb');
2475
+ }
2476
+ else {
2477
+ db.alternate = await __classPrivateFieldGet(this, _LocalDatabase_tryLoadOneLibrary, "f").call(this, db.device, db.slot, tx);
2478
+ tx.setTag('dbType', db.alternate ? 'oneLibrary' : 'none');
2479
+ }
2480
+ }
2481
+ catch {
2482
+ // The media simply has no such file (or it could not be read).
2483
+ db.alternate = null;
2484
+ }
2485
+ tx.finish();
2486
+ return db.alternate;
2487
+ }));
2420
2488
  __classPrivateFieldSet(this, _LocalDatabase_hostDevice, hostDevice, "f");
2421
2489
  __classPrivateFieldSet(this, _LocalDatabase_deviceManager, deviceManager, "f");
2422
2490
  __classPrivateFieldSet(this, _LocalDatabase_statusEmitter, statusEmitter, "f");
@@ -2441,6 +2509,45 @@ class LocalDatabase {
2441
2509
  disconnectForDevice(device) {
2442
2510
  __classPrivateFieldGet(this, _LocalDatabase_handleDeviceRemoved, "f").call(this, device);
2443
2511
  }
2512
+ /**
2513
+ * Looks a track up in the databases of a device slot, checking the answer
2514
+ * against what the player reports.
2515
+ *
2516
+ * A Device Library Plus export carries both `exportLibrary.db` and the
2517
+ * legacy `export.pdb`, and their track IDs are different number spaces. A
2518
+ * player that reads one while we loaded the other resolves most IDs to a
2519
+ * different track and some to nothing at all (NP3-399). So when the active
2520
+ * database has no such row, or its row's BPM is not the BPM the player is
2521
+ * showing, the other format is loaded and asked. If it agrees with the
2522
+ * player, the slot switches to it for every later lookup — artwork,
2523
+ * analysis and the next track all follow the database the player is using.
2524
+ *
2525
+ * When neither database can be confirmed, the active database's row (if any)
2526
+ * is returned as before.
2527
+ */
2528
+ async findTrack(deviceId, slot, trackId, hint = {}) {
2529
+ const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
2530
+ if (db === null) {
2531
+ return { adapter: null, track: null, switchedTo: null };
2532
+ }
2533
+ const active = db.adapter.findTrack(trackId);
2534
+ if (active !== null && trackAgreesWithPlayer(active, hint)) {
2535
+ return { adapter: db.adapter, track: active, switchedTo: null };
2536
+ }
2537
+ const alternate = await __classPrivateFieldGet(this, _LocalDatabase_loadAlternate, "f").call(this, db);
2538
+ if (alternate !== null) {
2539
+ const other = alternate.adapter.findTrack(trackId);
2540
+ if (other !== null && trackAgreesWithPlayer(other, hint)) {
2541
+ db.alternate = { adapter: db.adapter, tempFile: db.tempFile };
2542
+ db.adapter = alternate.adapter;
2543
+ db.tempFile = alternate.tempFile;
2544
+ return { adapter: db.adapter, track: other, switchedTo: db.adapter.type };
2545
+ }
2546
+ }
2547
+ return active === null
2548
+ ? { adapter: db.adapter, track: null, switchedTo: null }
2549
+ : { adapter: db.adapter, track: active, switchedTo: null };
2550
+ }
2444
2551
  /**
2445
2552
  * Gets the database adapter for the media metadata in the provided device slot.
2446
2553
  *
@@ -2451,43 +2558,8 @@ class LocalDatabase {
2451
2558
  */
2452
2559
  async get(deviceId, slot) {
2453
2560
  var _a;
2454
- const lockKey = `${deviceId}-${slot}`;
2455
- 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);
2456
- const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
2457
- if (device === undefined) {
2458
- return null;
2459
- }
2460
- if (device.type !== types_1.DeviceType.CDJ ||
2461
- device.id < constants_1.MIN_CDJ_DEVICE_ID ||
2462
- device.id > constants_1.MAX_CDJ_DEVICE_ID) {
2463
- return null;
2464
- }
2465
- let media;
2466
- try {
2467
- media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
2468
- hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
2469
- device,
2470
- slot,
2471
- });
2472
- }
2473
- catch {
2474
- // Timeout or other error - treat as no media
2475
- return null;
2476
- }
2477
- if (media.tracksType !== types_1.TrackType.RB) {
2478
- return null;
2479
- }
2480
- const id = getMediaId(media);
2481
- // Acquire a lock for this device slot that will not release until we've
2482
- // guaranteed the existence of the database.
2483
- const db = await lock.runExclusive(() => {
2484
- const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
2485
- if (cached) {
2486
- return cached;
2487
- }
2488
- return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
2489
- });
2490
- return db.adapter;
2561
+ const db = await __classPrivateFieldGet(this, _LocalDatabase_instances, "m", _LocalDatabase_getItem).call(this, deviceId, slot);
2562
+ return (_a = db === null || db === void 0 ? void 0 : db.adapter) !== null && _a !== void 0 ? _a : null;
2491
2563
  }
2492
2564
  /**
2493
2565
  * Get the database type for an already-loaded device slot.
@@ -2513,7 +2585,50 @@ class LocalDatabase {
2513
2585
  await Promise.all(loaders);
2514
2586
  }
2515
2587
  }
2516
- _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();
2588
+ _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 =
2589
+ /**
2590
+ * The loaded database entry for a device slot, hydrating it first if needed.
2591
+ */
2592
+ async function _LocalDatabase_getItem(deviceId, slot) {
2593
+ var _a;
2594
+ const lockKey = `${deviceId}-${slot}`;
2595
+ 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);
2596
+ const device = __classPrivateFieldGet(this, _LocalDatabase_deviceManager, "f").devices.get(deviceId);
2597
+ if (device === undefined) {
2598
+ return null;
2599
+ }
2600
+ if (device.type !== types_1.DeviceType.CDJ ||
2601
+ device.id < constants_1.MIN_CDJ_DEVICE_ID ||
2602
+ device.id > constants_1.MAX_CDJ_DEVICE_ID) {
2603
+ return null;
2604
+ }
2605
+ let media;
2606
+ try {
2607
+ media = await __classPrivateFieldGet(this, _LocalDatabase_statusEmitter, "f").queryMediaSlot({
2608
+ hostDevice: __classPrivateFieldGet(this, _LocalDatabase_hostDevice, "f"),
2609
+ device,
2610
+ slot,
2611
+ });
2612
+ }
2613
+ catch {
2614
+ // Timeout or other error - treat as no media
2615
+ return null;
2616
+ }
2617
+ if (media.tracksType !== types_1.TrackType.RB) {
2618
+ return null;
2619
+ }
2620
+ const id = getMediaId(media);
2621
+ // Acquire a lock for this device slot that will not release until we've
2622
+ // guaranteed the existence of the database.
2623
+ const db = await lock.runExclusive(() => {
2624
+ const cached = __classPrivateFieldGet(this, _LocalDatabase_dbs, "f").find(db => db.id === id);
2625
+ if (cached) {
2626
+ return cached;
2627
+ }
2628
+ return __classPrivateFieldGet(this, _LocalDatabase_hydrateDatabase, "f").call(this, device, slot, media);
2629
+ });
2630
+ return db;
2631
+ };
2517
2632
  exports["default"] = LocalDatabase;
2518
2633
 
2519
2634