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.
@@ -1,7 +1,9 @@
1
1
  import { Track } from "../entities";
2
2
  import LocalDatabase from "../localdb";
3
3
  import { DatabaseType } from "../localdb/database-adapter";
4
+ import { type Logger } from "../logger";
4
5
  import RemoteDatabase from "../remotedb";
6
+ import { MetadataResponse } from "../remotedb/queries";
5
7
  import { Device, DeviceID, MediaSlot, TrackType } from "../types";
6
8
  import { TelemetrySpan as Span } from "../utils/telemetry";
7
9
  export interface Options {
@@ -60,5 +62,13 @@ export type LocalResult = {
60
62
  miss: LocalMiss;
61
63
  switchedTo: null;
62
64
  };
63
- export declare function viaRemote(remote: RemoteDatabase, opts: Required<Options>): Promise<Track<import("../entities").EntityFK.WithRelations> | null>;
65
+ /**
66
+ * Explain a remote lookup that came back with neither a title nor an artist:
67
+ * what the player was asked, how many items it sent, and each item's type and
68
+ * fields as they arrived. A player that files a track under an unfamiliar slot
69
+ * can answer this way (NP3-416), and without the raw items there is no telling
70
+ * an empty answer from one we do not know how to read.
71
+ */
72
+ export declare function describeBlankMetadata(opts: Pick<Options, 'deviceId' | 'trackSlot' | 'trackType' | 'trackId'>, response: MetadataResponse | null, trackInfo: string): string;
73
+ export declare function viaRemote(remote: RemoteDatabase, opts: Required<Options>, logger?: Logger): Promise<Track<import("../entities").EntityFK.WithRelations> | null>;
64
74
  export declare function viaLocal(local: LocalDatabase, device: Device, opts: Required<Options>): Promise<LocalResult>;
package/lib/index.js CHANGED
@@ -1332,13 +1332,51 @@ async function viaLocal(local, device, opts) {
1332
1332
  "use strict";
1333
1333
 
1334
1334
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1335
+ exports.describeBlankMetadata = describeBlankMetadata;
1335
1336
  exports.viaRemote = viaRemote;
1336
1337
  exports.viaLocal = viaLocal;
1337
1338
  const rekordbox_1 = __webpack_require__(/*! src/localdb/rekordbox */ "./src/localdb/rekordbox.ts");
1339
+ const logger_1 = __webpack_require__(/*! src/logger */ "./src/logger.ts");
1338
1340
  const remotedb_1 = __webpack_require__(/*! src/remotedb */ "./src/remotedb/index.ts");
1339
1341
  const types_1 = __webpack_require__(/*! src/types */ "./src/types.ts");
1340
- const utils_1 = __webpack_require__(/*! ./utils */ "./src/db/utils.ts");
1341
- async function viaRemote(remote, opts) {
1342
+ const utils_1 = __webpack_require__(/*! src/utils */ "./src/utils/index.ts");
1343
+ const utils_2 = __webpack_require__(/*! ./utils */ "./src/db/utils.ts");
1344
+ /**
1345
+ * The longest a single item is allowed to run in a blank-metadata report, so
1346
+ * one oversized field cannot swamp the log line.
1347
+ */
1348
+ const MAX_ITEM_REPORT_LENGTH = 200;
1349
+ /**
1350
+ * Explain a remote lookup that came back with neither a title nor an artist:
1351
+ * what the player was asked, how many items it sent, and each item's type and
1352
+ * fields as they arrived. A player that files a track under an unfamiliar slot
1353
+ * can answer this way (NP3-416), and without the raw items there is no telling
1354
+ * an empty answer from one we do not know how to read.
1355
+ */
1356
+ function describeBlankMetadata(opts, response, trackInfo) {
1357
+ var _a, _b, _c;
1358
+ const { deviceId, trackSlot, trackType, trackId } = opts;
1359
+ const slotName = (_a = (0, utils_1.getSlotName)(trackSlot)) !== null && _a !== void 0 ? _a : 'unknown';
1360
+ const typeName = (_b = (0, utils_1.getTrackTypeName)(trackType)) !== null && _b !== void 0 ? _b : 'unknown';
1361
+ const itemReports = ((_c = response === null || response === void 0 ? void 0 : response.items) !== null && _c !== void 0 ? _c : []).map(({ type, ...fields }) => {
1362
+ const hex = `0x${type.toString(16).padStart(4, '0')}`;
1363
+ const report = `${hex} ${JSON.stringify(fields)}`;
1364
+ return report.length > MAX_ITEM_REPORT_LENGTH
1365
+ ? `${report.slice(0, MAX_ITEM_REPORT_LENGTH)}…`
1366
+ : report;
1367
+ });
1368
+ const items = response === null
1369
+ ? 'no response recorded'
1370
+ : [
1371
+ `${response.items.length} of ${response.itemsAvailable} item(s) received`,
1372
+ ...itemReports,
1373
+ ].join('; ');
1374
+ return (`Device ${deviceId} answered the metadata query for track ${trackId} ` +
1375
+ `(slot ${trackSlot} ${slotName}, type ${trackType} ${typeName}) with no ` +
1376
+ `title or artist: ${items}; track info: ${trackInfo}`);
1377
+ }
1378
+ async function viaRemote(remote, opts, logger = logger_1.noopLogger) {
1379
+ var _a, _b, _c;
1342
1380
  const { deviceId, trackSlot, trackType, trackId, span } = opts;
1343
1381
  const conn = await remote.get(deviceId);
1344
1382
  if (conn === null) {
@@ -1354,14 +1392,23 @@ async function viaRemote(remote, opts) {
1354
1392
  const skipLocalFileLookups = isUnanalyzed || isStreaming;
1355
1393
  // Unanalyzed tracks use GetGenericMetadata (reads ID3 tags from the audio file).
1356
1394
  // Streaming tracks (Beatport) use the regular GetMetadata query.
1357
- const track = await conn.query({
1358
- queryDescriptor,
1359
- query: isUnanalyzed ? remotedb_1.Query.GetGenericMetadata : remotedb_1.Query.GetMetadata,
1360
- args: { trackId },
1361
- span,
1362
- });
1395
+ let response = null;
1396
+ const track = isUnanalyzed
1397
+ ? await conn.query({
1398
+ queryDescriptor,
1399
+ query: remotedb_1.Query.GetGenericMetadata,
1400
+ args: { trackId },
1401
+ span,
1402
+ })
1403
+ : await conn.query({
1404
+ queryDescriptor,
1405
+ query: remotedb_1.Query.GetMetadata,
1406
+ args: { trackId, onResponse: r => (response = r) },
1407
+ span,
1408
+ });
1363
1409
  // Try to get file path — for streaming tracks this returns the Beatport track ID
1364
1410
  // (e.g. "/26883657.m4a") which we use for Beatport API lookups
1411
+ let trackInfoError = null;
1365
1412
  try {
1366
1413
  track.filePath = await conn.query({
1367
1414
  queryDescriptor,
@@ -1374,6 +1421,13 @@ async function viaRemote(remote, opts) {
1374
1421
  if (!skipLocalFileLookups) {
1375
1422
  throw err;
1376
1423
  }
1424
+ trackInfoError = err instanceof Error ? err.message : String(err);
1425
+ }
1426
+ 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())) {
1427
+ const trackInfo = trackInfoError === null
1428
+ ? JSON.stringify(track.filePath)
1429
+ : `failed (${trackInfoError})`;
1430
+ logger.warn(describeBlankMetadata(opts, response, trackInfo));
1377
1431
  }
1378
1432
  // Beat grid is only available for analyzed local tracks
1379
1433
  if (!skipLocalFileLookups) {
@@ -1398,7 +1452,7 @@ async function viaLocal(local, device, opts) {
1398
1452
  if (lookup.track === null) {
1399
1453
  return { track: null, miss: 'track-absent', switchedTo: null };
1400
1454
  }
1401
- const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_1.anlzLoader)({ device, slot: trackSlot }));
1455
+ const anlz = await (0, rekordbox_1.loadAnlz)(lookup.track, 'DAT', (0, utils_2.anlzLoader)({ device, slot: trackSlot }));
1402
1456
  const track = {
1403
1457
  ...lookup.track,
1404
1458
  beatGrid: anlz.beatGrid,
@@ -1746,7 +1800,7 @@ class Database {
1746
1800
  const strategy = __classPrivateFieldGet(this, _Database_getTrackLookupStrategy, "f").call(this, device, trackType);
1747
1801
  let track = null;
1748
1802
  if (strategy === LookupStrategy.Remote) {
1749
- track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), callOpts);
1803
+ track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), callOpts, __classPrivateFieldGet(this, _Database_logger, "f"));
1750
1804
  }
1751
1805
  if (strategy === LookupStrategy.Local) {
1752
1806
  const local = await GetMetadata.viaLocal(__classPrivateFieldGet(this, _Database_localDatabase, "f"), device, callOpts);
@@ -1973,7 +2027,7 @@ _Database_deviceManager = new WeakMap(), _Database_localDatabase = new WeakMap()
1973
2027
  */
1974
2028
  async function _Database_metadataViaRemoteFallback(opts) {
1975
2029
  try {
1976
- const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts);
2030
+ const track = await GetMetadata.viaRemote(__classPrivateFieldGet(this, _Database_remoteDatabase, "f"), opts, __classPrivateFieldGet(this, _Database_logger, "f"));
1977
2031
  if (track === null) {
1978
2032
  __classPrivateFieldGet(this, _Database_logger, "f").warn(`Remote fallback found no track ${opts.trackId} on device ${opts.deviceId}`);
1979
2033
  return null;
@@ -12292,8 +12346,8 @@ const transformItem = {
12292
12346
  [ItemType.Rating]: (a) => ({ rating: a.mainId }),
12293
12347
  [ItemType.Tempo]: (a) => ({ bpm: a.mainId / 100 }),
12294
12348
  [ItemType.Duration]: (a) => ({ duration: a.mainId }),
12295
- [ItemType.Unknown01]: (_) => null,
12296
- [ItemType.Unknown02]: (_) => null,
12349
+ [ItemType.Unknown01]: (a) => a,
12350
+ [ItemType.Unknown02]: (a) => a,
12297
12351
  [ItemType.ColorNone]: mapIdName,
12298
12352
  [ItemType.ColorPink]: mapIdName,
12299
12353
  [ItemType.ColorRed]: mapIdName,
@@ -12354,9 +12408,10 @@ const fieldsToItem = (args) => {
12354
12408
  const { type } = itemData;
12355
12409
  let transformer = transformItem[type];
12356
12410
  // Typescript gives us safety, but it is possible there is an itemType we're
12357
- // not aware of yet.
12411
+ // not aware of yet. Keep its raw fields so whoever reads the response can
12412
+ // still see what the player sent.
12358
12413
  if (transformer === undefined) {
12359
- transformer = () => null;
12414
+ transformer = (a) => a;
12360
12415
  Telemetry.captureMessage(`No item transformer registered for item type ${type}`, Telemetry.Severity.Error);
12361
12416
  }
12362
12417
  return { ...transformer(itemData), type };
@@ -12678,7 +12733,7 @@ const utils_1 = __webpack_require__(/*! ./utils */ "./src/remotedb/utils.ts");
12678
12733
  async function getMetadata(opts) {
12679
12734
  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;
12680
12735
  const { conn, lookupDescriptor, span, args } = opts;
12681
- const { trackId } = args;
12736
+ const { trackId, onResponse } = args;
12682
12737
  const request = new message_1.Message({
12683
12738
  type: types_1.Request.GetMetadata,
12684
12739
  args: [(0, utils_1.fieldFromDescriptor)(lookupDescriptor), new fields_1.UInt32(trackId)],
@@ -12689,9 +12744,12 @@ async function getMetadata(opts) {
12689
12744
  // NOTE: We do a bit of any-ing here to help typescript understand we're
12690
12745
  // discriminating the type by our object key
12691
12746
  const trackItems = {};
12747
+ const received = [];
12692
12748
  for await (const item of items) {
12693
12749
  trackItems[item.type] = item;
12750
+ received.push(item);
12694
12751
  }
12752
+ onResponse === null || onResponse === void 0 ? void 0 : onResponse({ itemsAvailable: resp.data.itemsAvailable, items: received });
12695
12753
  // Translate our trackItems into a (partial) Track entity.
12696
12754
  // Use optional chaining for all fields since streaming tracks may omit some.
12697
12755
  const track = {
@@ -12893,7 +12951,11 @@ async function getTrackInfo(opts) {
12893
12951
  for await (const item of items) {
12894
12952
  infoItems[item.type] = item;
12895
12953
  }
12896
- return infoItems[item_1.ItemType.Path].path;
12954
+ const path = infoItems[item_1.ItemType.Path];
12955
+ if (path === undefined) {
12956
+ throw new Error(`Player sent no file path (${resp.data.itemsAvailable} item(s) available)`);
12957
+ }
12958
+ return path.path;
12897
12959
  }
12898
12960
  /**
12899
12961
  * Lookup playlist entries