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 +9 -7
- package/lib/cli.js +79 -17
- package/lib/cli.js.map +1 -1
- package/lib/db/getMetadata.d.ts +11 -1
- package/lib/index.js +79 -17
- package/lib/index.js.map +1 -1
- package/lib/remotedb/message/item.d.ts +16 -2
- package/lib/remotedb/queries.d.ts +28 -2
- package/package.json +1 -1
package/lib/db/getMetadata.d.ts
CHANGED
|
@@ -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
|
-
|
|
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__(/*!
|
|
1341
|
-
|
|
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
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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,
|
|
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]: (
|
|
12296
|
-
[ItemType.Unknown02]: (
|
|
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 = () =>
|
|
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
|
-
|
|
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
|