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/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**
|
|
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**
|
|
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**
|
|
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**
|
|
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**
|
|
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**
|
|
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**
|
|
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__(/*!
|
|
1452
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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,
|
|
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]: (
|
|
9377
|
-
[ItemType.Unknown02]: (
|
|
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 = () =>
|
|
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
|
-
|
|
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
|