djay-connect 2.0.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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/detect.d.ts +24 -0
- package/dist/detect.js +59 -0
- package/dist/detect.js.map +1 -0
- package/dist/djayConnect.d.ts +41 -0
- package/dist/djayConnect.js +249 -0
- package/dist/djayConnect.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/sources.d.ts +47 -0
- package/dist/sources.js +113 -0
- package/dist/sources.js.map +1 -0
- package/dist/tsaf.d.ts +71 -0
- package/dist/tsaf.js +212 -0
- package/dist/tsaf.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/logger.d.ts +12 -0
- package/dist/types/logger.js +15 -0
- package/dist/types/logger.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chris Le
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# djay-connect
|
|
2
|
+
|
|
3
|
+
Library to read djay Pro's `MediaLibrary.db` and emit track change events.
|
|
4
|
+
Supports djay Pro on macOS and Windows.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install djay-connect
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { DjayConnect } from 'djay-connect';
|
|
16
|
+
|
|
17
|
+
const djay = new DjayConnect({
|
|
18
|
+
pollIntervalMs: 2000,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
djay.on('ready', (info) => {
|
|
22
|
+
console.log(`Watching: ${info.databasePath}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
djay.on('track', (payload) => {
|
|
26
|
+
const { track } = payload;
|
|
27
|
+
console.log(`Now playing: ${track.artist} - ${track.title} [deck ${track.deckNumber}]`);
|
|
28
|
+
if (track.filePath) console.log(`File: ${track.filePath}`);
|
|
29
|
+
if (track.originSourceID) console.log(`Source: ${track.originSourceID}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
djay.on('error', (err) => {
|
|
33
|
+
console.error('Error:', err);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
djay.start();
|
|
37
|
+
|
|
38
|
+
// Later...
|
|
39
|
+
djay.stop();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `new DjayConnect(options?)`
|
|
45
|
+
|
|
46
|
+
- `pollIntervalMs` — Polling interval in milliseconds (minimum 2000, default 2000)
|
|
47
|
+
- `databasePath` — Custom path to `MediaLibrary.db`. If omitted, uses the default per-platform path.
|
|
48
|
+
- `logger` — Logger instance implementing `{ trace, debug, info, warn, error }`
|
|
49
|
+
|
|
50
|
+
### Track fields
|
|
51
|
+
|
|
52
|
+
Every `track` event carries a `DjayNowPlayingTrack` with:
|
|
53
|
+
|
|
54
|
+
- `title`, `artist`, `duration`, `deckNumber`, `startTime`
|
|
55
|
+
- `uuid`, `sessionUUID` — history entry and session identifiers
|
|
56
|
+
- `titleID` — 32-hex id joining the track to location and analysis tables
|
|
57
|
+
- `originSourceID` — where the track came from (`explorer`, `music`, `beatport`,
|
|
58
|
+
`soundcloud`, `spotify`, `tidal`, `beatsource`, `applemusic`)
|
|
59
|
+
- `isrc` — International Standard Recording Code (streaming tracks)
|
|
60
|
+
- `filePath` — absolute path for local files, decoded from djay's URL-encoded
|
|
61
|
+
`file://` URIs
|
|
62
|
+
- `sourceURIs` — raw source URIs; may contain multiple entries when a track is
|
|
63
|
+
available on more than one streaming service
|
|
64
|
+
|
|
65
|
+
### Events
|
|
66
|
+
|
|
67
|
+
- `ready` — Emitted when monitoring starts
|
|
68
|
+
- `poll` — Emitted on each poll cycle
|
|
69
|
+
- `track` — Emitted when a new track is detected
|
|
70
|
+
- `error` — Emitted on errors
|
|
71
|
+
|
|
72
|
+
### Detection utilities
|
|
73
|
+
|
|
74
|
+
- `getDefaultDjayInstallPath()` — Returns the default djay Pro data folder
|
|
75
|
+
- `getDefaultDatabasePath()` — Returns the default `MediaLibrary.db` path for
|
|
76
|
+
the current platform
|
|
77
|
+
- `getDefaultDatabasePaths()` — Returns all candidate paths for the current
|
|
78
|
+
platform
|
|
79
|
+
- `detectDjayInstallation()` — Checks whether djay Pro is installed
|
|
80
|
+
|
|
81
|
+
### Source helpers
|
|
82
|
+
|
|
83
|
+
- `DJAY_SOURCES` — Catalog of every known `originSourceID` with `kind`,
|
|
84
|
+
`label`, and URI scheme prefix
|
|
85
|
+
- `isStreamingSource(id)` — True if the given source is a streaming service
|
|
86
|
+
- `toNowPlayingStreamingSource(id)` — Maps a djay Pro source to the
|
|
87
|
+
nowplaying streaming enum
|
|
88
|
+
|
|
89
|
+
### TSAF parser (advanced)
|
|
90
|
+
|
|
91
|
+
- `parseHistorySessionItem(blob)` — Parse a raw `historySessionItems` BLOB
|
|
92
|
+
- `extractTitleID(blob)` — Extract the nested `ADCMediaItemTitleID`
|
|
93
|
+
- `extractSourceURIs(blob)` — Extract all source URIs from a location blob
|
|
94
|
+
- `extractString`, `extractDouble`, `extractDate` — Low-level field readers
|
|
95
|
+
|
|
96
|
+
See [`docs/FORMAT.md`](docs/FORMAT.md) for the full on-disk format reference.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detection utilities for djay Pro installation and database path.
|
|
3
|
+
*
|
|
4
|
+
* djay Pro stores its entire library (including a running history of played
|
|
5
|
+
* tracks) in a YapDatabase-backed SQLite file named `MediaLibrary.db`. The
|
|
6
|
+
* folder that contains it is named differently on macOS and Windows:
|
|
7
|
+
*
|
|
8
|
+
* macOS: ~/Music/djay/djay Media Library.djayMediaLibrary/MediaLibrary.db
|
|
9
|
+
* Windows: %USERPROFILE%\Music\djay\djay Media Library\MediaLibrary.db
|
|
10
|
+
*
|
|
11
|
+
* Both paths sit under `~/Music/djay`, which is what we return as the install
|
|
12
|
+
* path for presence detection.
|
|
13
|
+
*/
|
|
14
|
+
/** Top-level djay Pro data folder (`~/Music/djay`). */
|
|
15
|
+
export declare function getDefaultDjayInstallPath(): string;
|
|
16
|
+
/** Candidate MediaLibrary.db paths, in priority order for the current platform. */
|
|
17
|
+
export declare function getDefaultDatabasePaths(): string[];
|
|
18
|
+
/** Resolve the MediaLibrary.db path for the current platform, picking the first that exists. */
|
|
19
|
+
export declare function getDefaultDatabasePath(): string;
|
|
20
|
+
/** Detect if djay Pro is installed by checking for the djay data folder. */
|
|
21
|
+
export declare function detectDjayInstallation(): {
|
|
22
|
+
found: boolean;
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Detection utilities for djay Pro installation and database path.
|
|
4
|
+
*
|
|
5
|
+
* djay Pro stores its entire library (including a running history of played
|
|
6
|
+
* tracks) in a YapDatabase-backed SQLite file named `MediaLibrary.db`. The
|
|
7
|
+
* folder that contains it is named differently on macOS and Windows:
|
|
8
|
+
*
|
|
9
|
+
* macOS: ~/Music/djay/djay Media Library.djayMediaLibrary/MediaLibrary.db
|
|
10
|
+
* Windows: %USERPROFILE%\Music\djay\djay Media Library\MediaLibrary.db
|
|
11
|
+
*
|
|
12
|
+
* Both paths sit under `~/Music/djay`, which is what we return as the install
|
|
13
|
+
* path for presence detection.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.getDefaultDjayInstallPath = getDefaultDjayInstallPath;
|
|
17
|
+
exports.getDefaultDatabasePaths = getDefaultDatabasePaths;
|
|
18
|
+
exports.getDefaultDatabasePath = getDefaultDatabasePath;
|
|
19
|
+
exports.detectDjayInstallation = detectDjayInstallation;
|
|
20
|
+
const fs_1 = require("fs");
|
|
21
|
+
const os_1 = require("os");
|
|
22
|
+
const path_1 = require("path");
|
|
23
|
+
const DJAY_ROOT_FOLDER = 'djay';
|
|
24
|
+
const DATABASE_FILE = 'MediaLibrary.db';
|
|
25
|
+
const DARWIN_LIBRARY_FOLDER = 'djay Media Library.djayMediaLibrary';
|
|
26
|
+
const WIN32_LIBRARY_FOLDER = 'djay Media Library';
|
|
27
|
+
/** Top-level djay Pro data folder (`~/Music/djay`). */
|
|
28
|
+
function getDefaultDjayInstallPath() {
|
|
29
|
+
return (0, path_1.join)((0, os_1.homedir)(), 'Music', DJAY_ROOT_FOLDER);
|
|
30
|
+
}
|
|
31
|
+
/** Candidate MediaLibrary.db paths, in priority order for the current platform. */
|
|
32
|
+
function getDefaultDatabasePaths() {
|
|
33
|
+
const root = getDefaultDjayInstallPath();
|
|
34
|
+
if (process.platform === 'darwin') {
|
|
35
|
+
return [(0, path_1.join)(root, DARWIN_LIBRARY_FOLDER, DATABASE_FILE)];
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
return [(0, path_1.join)(root, WIN32_LIBRARY_FOLDER, DATABASE_FILE)];
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
/** Resolve the MediaLibrary.db path for the current platform, picking the first that exists. */
|
|
43
|
+
function getDefaultDatabasePath() {
|
|
44
|
+
const candidates = getDefaultDatabasePaths();
|
|
45
|
+
for (const candidate of candidates) {
|
|
46
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
return candidates[0] ?? '';
|
|
50
|
+
}
|
|
51
|
+
/** Detect if djay Pro is installed by checking for the djay data folder. */
|
|
52
|
+
function detectDjayInstallation() {
|
|
53
|
+
const path = getDefaultDjayInstallPath();
|
|
54
|
+
return {
|
|
55
|
+
found: (0, fs_1.existsSync)(path),
|
|
56
|
+
path,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAYH,8DAEC;AAGD,0DASC;AAGD,wDAMC;AAGD,wDAMC;AA1CD,2BAAgC;AAChC,2BAA6B;AAC7B,+BAA4B;AAE5B,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAChC,MAAM,aAAa,GAAG,iBAAiB,CAAC;AACxC,MAAM,qBAAqB,GAAG,qCAAqC,CAAC;AACpE,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AAElD,uDAAuD;AACvD,SAAgB,yBAAyB;IACvC,OAAO,IAAA,WAAI,EAAC,IAAA,YAAO,GAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC;AACpD,CAAC;AAED,mFAAmF;AACnF,SAAgB,uBAAuB;IACrC,MAAM,IAAI,GAAG,yBAAyB,EAAE,CAAC;IACzC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,CAAC,IAAA,WAAI,EAAC,IAAI,EAAE,qBAAqB,EAAE,aAAa,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,CAAC,IAAA,WAAI,EAAC,IAAI,EAAE,oBAAoB,EAAE,aAAa,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,gGAAgG;AAChG,SAAgB,sBAAsB;IACpC,MAAM,UAAU,GAAG,uBAAuB,EAAE,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAA,eAAU,EAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;IACD,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,4EAA4E;AAC5E,SAAgB,sBAAsB;IACpC,MAAM,IAAI,GAAG,yBAAyB,EAAE,CAAC;IACzC,OAAO;QACL,KAAK,EAAE,IAAA,eAAU,EAAC,IAAI,CAAC;QACvB,IAAI;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DjayConnect - SQLite-backed reader for djay Pro's MediaLibrary.db.
|
|
3
|
+
*
|
|
4
|
+
* djay Pro writes each played track into the `historySessionItems` collection
|
|
5
|
+
* inside its YapDatabase-backed SQLite library (`MediaLibrary.db`). DjayConnect
|
|
6
|
+
* polls that collection for new rows, parses each row's TSAF blob, and emits
|
|
7
|
+
* `track` events as they arrive. The same schema is used on macOS and Windows.
|
|
8
|
+
*/
|
|
9
|
+
import type { DjayConnectOptions, TypedEmitter } from './types.js';
|
|
10
|
+
declare const DjayConnect_base: new () => TypedEmitter;
|
|
11
|
+
/**
|
|
12
|
+
* DjayConnect monitors djay Pro's MediaLibrary.db for new history entries
|
|
13
|
+
* and emits events when the currently playing track changes.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DjayConnect extends DjayConnect_base {
|
|
16
|
+
private pollIntervalMs;
|
|
17
|
+
private databasePath;
|
|
18
|
+
private logger;
|
|
19
|
+
private db;
|
|
20
|
+
private pollTimer;
|
|
21
|
+
private lastRowId;
|
|
22
|
+
private isRunning;
|
|
23
|
+
constructor(options?: DjayConnectOptions);
|
|
24
|
+
start(): void;
|
|
25
|
+
stop(): void;
|
|
26
|
+
get running(): boolean;
|
|
27
|
+
setPollInterval(intervalMs: number): void;
|
|
28
|
+
get pollInterval(): number;
|
|
29
|
+
get path(): string;
|
|
30
|
+
private openDatabase;
|
|
31
|
+
private poll;
|
|
32
|
+
private readLatestHistoryItem;
|
|
33
|
+
private toTrack;
|
|
34
|
+
/**
|
|
35
|
+
* Look up the raw `sourceURIs` for a titleID. Local files live in
|
|
36
|
+
* `localMediaItemLocations`, streaming tracks in `globalMediaItemLocations`.
|
|
37
|
+
* Returns every URI recorded for the track — can be empty, one, or many.
|
|
38
|
+
*/
|
|
39
|
+
private readSourceURIs;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview DjayConnect - SQLite-backed reader for djay Pro's MediaLibrary.db.
|
|
4
|
+
*
|
|
5
|
+
* djay Pro writes each played track into the `historySessionItems` collection
|
|
6
|
+
* inside its YapDatabase-backed SQLite library (`MediaLibrary.db`). DjayConnect
|
|
7
|
+
* polls that collection for new rows, parses each row's TSAF blob, and emits
|
|
8
|
+
* `track` events as they arrive. The same schema is used on macOS and Windows.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.DjayConnect = void 0;
|
|
15
|
+
const node_events_1 = __importDefault(require("node:events"));
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const better_sqlite3_multiple_ciphers_1 = __importDefault(require("better-sqlite3-multiple-ciphers"));
|
|
18
|
+
const detect_js_1 = require("./detect.js");
|
|
19
|
+
const tsaf_js_1 = require("./tsaf.js");
|
|
20
|
+
const logger_js_1 = require("./types/logger.js");
|
|
21
|
+
/**
|
|
22
|
+
* Decode a djay file:// URI into an absolute OS path.
|
|
23
|
+
* djay URL-encodes backslashes (`%5C`) and spaces (`%20`) on Windows.
|
|
24
|
+
*/
|
|
25
|
+
function fileUriToPath(uri) {
|
|
26
|
+
if (!uri.startsWith('file://'))
|
|
27
|
+
return undefined;
|
|
28
|
+
// Strip the scheme + the authority slashes; on Windows the authority is
|
|
29
|
+
// empty and the path begins with a drive letter, e.g. "file:///D:%5C..."
|
|
30
|
+
let path = uri.slice('file://'.length);
|
|
31
|
+
if (path.startsWith('/') && /^\/[A-Za-z]:/.test(path)) {
|
|
32
|
+
path = path.slice(1);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
path = decodeURIComponent(path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// On Windows, normalize forward slashes that came from URL decoding into
|
|
41
|
+
// backslashes so the path is usable by fs.readFile etc.
|
|
42
|
+
if (process.platform === 'win32') {
|
|
43
|
+
path = path.replace(/\//g, '\\');
|
|
44
|
+
}
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
const MIN_POLL_INTERVAL = 2000;
|
|
48
|
+
const DEFAULT_POLL_INTERVAL = 2000;
|
|
49
|
+
/**
|
|
50
|
+
* DjayConnect monitors djay Pro's MediaLibrary.db for new history entries
|
|
51
|
+
* and emits events when the currently playing track changes.
|
|
52
|
+
*/
|
|
53
|
+
class DjayConnect extends node_events_1.default {
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
super();
|
|
56
|
+
this.db = null;
|
|
57
|
+
this.pollTimer = null;
|
|
58
|
+
this.lastRowId = 0;
|
|
59
|
+
this.isRunning = false;
|
|
60
|
+
this.logger = options.logger ?? logger_js_1.noopLogger;
|
|
61
|
+
this.databasePath = options.databasePath ?? (0, detect_js_1.getDefaultDatabasePath)();
|
|
62
|
+
this.pollIntervalMs = Math.max(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL, MIN_POLL_INTERVAL);
|
|
63
|
+
}
|
|
64
|
+
start() {
|
|
65
|
+
if (this.isRunning) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
this.openDatabase();
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
73
|
+
this.logger.error('Failed to open djay Pro database: %s', error.message);
|
|
74
|
+
this.emit('error', error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.isRunning = true;
|
|
78
|
+
this.logger.info(`Watching djay Pro library at ${this.databasePath}`);
|
|
79
|
+
this.emit('ready', { databasePath: this.databasePath });
|
|
80
|
+
// Seed cursor with the most-recent row and emit it as the initial "current" track.
|
|
81
|
+
const initial = this.readLatestHistoryItem();
|
|
82
|
+
if (initial) {
|
|
83
|
+
this.lastRowId = initial.rowid;
|
|
84
|
+
const track = this.toTrack(initial.fields);
|
|
85
|
+
if (track) {
|
|
86
|
+
this.logger.debug(`Initial track: ${track.artist} - ${track.title} [deck ${track.deckNumber}]`);
|
|
87
|
+
this.emit('track', { track });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
91
|
+
}
|
|
92
|
+
stop() {
|
|
93
|
+
if (this.pollTimer) {
|
|
94
|
+
clearInterval(this.pollTimer);
|
|
95
|
+
this.pollTimer = null;
|
|
96
|
+
}
|
|
97
|
+
if (this.db) {
|
|
98
|
+
try {
|
|
99
|
+
this.db.close();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.logger.warn('Error closing database: %s', String(err));
|
|
103
|
+
}
|
|
104
|
+
this.db = null;
|
|
105
|
+
}
|
|
106
|
+
this.lastRowId = 0;
|
|
107
|
+
this.isRunning = false;
|
|
108
|
+
this.logger.debug('Stopped');
|
|
109
|
+
}
|
|
110
|
+
get running() {
|
|
111
|
+
return this.isRunning;
|
|
112
|
+
}
|
|
113
|
+
setPollInterval(intervalMs) {
|
|
114
|
+
this.pollIntervalMs = Math.max(intervalMs, MIN_POLL_INTERVAL);
|
|
115
|
+
if (this.isRunning && this.pollTimer) {
|
|
116
|
+
clearInterval(this.pollTimer);
|
|
117
|
+
this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
get pollInterval() {
|
|
121
|
+
return this.pollIntervalMs;
|
|
122
|
+
}
|
|
123
|
+
get path() {
|
|
124
|
+
return this.databasePath;
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Private
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
openDatabase() {
|
|
130
|
+
if (!(0, node_fs_1.existsSync)(this.databasePath)) {
|
|
131
|
+
throw new Error(`djay Pro database not found at: ${this.databasePath}`);
|
|
132
|
+
}
|
|
133
|
+
// Open read-only so we can never corrupt djay Pro's live library.
|
|
134
|
+
this.db = new better_sqlite3_multiple_ciphers_1.default(this.databasePath, { readonly: true });
|
|
135
|
+
// djay Pro keeps the database in WAL mode while running. Enabling
|
|
136
|
+
// read_uncommitted lets us observe writes that haven't been checkpointed
|
|
137
|
+
// yet, so new history rows appear in near real time.
|
|
138
|
+
this.db.pragma('read_uncommitted = true');
|
|
139
|
+
}
|
|
140
|
+
poll() {
|
|
141
|
+
if (!this.db)
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
this.emit('poll');
|
|
145
|
+
const rows = this.db
|
|
146
|
+
.prepare(`SELECT rowid, key, data
|
|
147
|
+
FROM database2
|
|
148
|
+
WHERE collection = 'historySessionItems'
|
|
149
|
+
AND rowid > ?
|
|
150
|
+
ORDER BY rowid ASC`)
|
|
151
|
+
.all(this.lastRowId);
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
this.lastRowId = Math.max(this.lastRowId, row.rowid);
|
|
154
|
+
const fields = (0, tsaf_js_1.parseHistorySessionItem)(row.data);
|
|
155
|
+
if (!fields) {
|
|
156
|
+
this.logger.warn(`Could not parse historySessionItem ${row.key} (${row.data.length} bytes)`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const track = this.toTrack(fields);
|
|
160
|
+
if (!track)
|
|
161
|
+
continue;
|
|
162
|
+
this.logger.debug(`New track: ${track.artist} - ${track.title} [deck ${track.deckNumber}]`);
|
|
163
|
+
this.emit('track', { track });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
168
|
+
this.logger.error('Poll error: %s', error.message);
|
|
169
|
+
this.emit('error', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
readLatestHistoryItem() {
|
|
173
|
+
if (!this.db)
|
|
174
|
+
return null;
|
|
175
|
+
const row = this.db
|
|
176
|
+
.prepare(`SELECT rowid, key, data
|
|
177
|
+
FROM database2
|
|
178
|
+
WHERE collection = 'historySessionItems'
|
|
179
|
+
ORDER BY rowid DESC
|
|
180
|
+
LIMIT 1`)
|
|
181
|
+
.get();
|
|
182
|
+
if (!row)
|
|
183
|
+
return null;
|
|
184
|
+
const fields = (0, tsaf_js_1.parseHistorySessionItem)(row.data);
|
|
185
|
+
if (!fields)
|
|
186
|
+
return null;
|
|
187
|
+
return { rowid: row.rowid, fields };
|
|
188
|
+
}
|
|
189
|
+
toTrack(fields) {
|
|
190
|
+
if (!fields.title && !fields.artist)
|
|
191
|
+
return null;
|
|
192
|
+
const track = {
|
|
193
|
+
title: fields.title,
|
|
194
|
+
artist: fields.artist,
|
|
195
|
+
duration: fields.duration,
|
|
196
|
+
deckNumber: fields.deckNumber,
|
|
197
|
+
startTime: fields.startTime,
|
|
198
|
+
uuid: fields.uuid,
|
|
199
|
+
sessionUUID: fields.sessionUUID,
|
|
200
|
+
titleID: fields.titleID,
|
|
201
|
+
originSourceID: fields.originSourceID,
|
|
202
|
+
isrc: fields.isrc,
|
|
203
|
+
};
|
|
204
|
+
// Enrich with location data (file path or streaming URIs) when we can
|
|
205
|
+
// resolve the titleID against the *MediaItemLocations collections.
|
|
206
|
+
if (fields.titleID) {
|
|
207
|
+
const uris = this.readSourceURIs(fields.titleID);
|
|
208
|
+
if (uris.length > 0) {
|
|
209
|
+
track.sourceURIs = uris;
|
|
210
|
+
for (const uri of uris) {
|
|
211
|
+
if (uri.startsWith('file://')) {
|
|
212
|
+
const decoded = fileUriToPath(uri);
|
|
213
|
+
if (decoded) {
|
|
214
|
+
track.filePath = decoded;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return track;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Look up the raw `sourceURIs` for a titleID. Local files live in
|
|
225
|
+
* `localMediaItemLocations`, streaming tracks in `globalMediaItemLocations`.
|
|
226
|
+
* Returns every URI recorded for the track — can be empty, one, or many.
|
|
227
|
+
*/
|
|
228
|
+
readSourceURIs(titleID) {
|
|
229
|
+
if (!this.db)
|
|
230
|
+
return [];
|
|
231
|
+
try {
|
|
232
|
+
const row = this.db
|
|
233
|
+
.prepare(`SELECT collection, data
|
|
234
|
+
FROM database2
|
|
235
|
+
WHERE collection IN ('localMediaItemLocations','globalMediaItemLocations')
|
|
236
|
+
AND key = ?`)
|
|
237
|
+
.get(titleID);
|
|
238
|
+
if (!row)
|
|
239
|
+
return [];
|
|
240
|
+
return (0, tsaf_js_1.extractSourceURIs)(row.data);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
this.logger.warn(`Failed to resolve location for titleID ${titleID}: ${String(err)}`);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.DjayConnect = DjayConnect;
|
|
249
|
+
//# sourceMappingURL=djayConnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"djayConnect.js","sourceRoot":"","sources":["../src/djayConnect.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* djay-connect - Library to read djay Pro's MediaLibrary.db and emit track change events.
|
|
3
|
+
*
|
|
4
|
+
* @module djay-connect
|
|
5
|
+
*/
|
|
6
|
+
export { DjayConnect } from './djayConnect.js';
|
|
7
|
+
export { getDefaultDjayInstallPath, getDefaultDatabasePath, getDefaultDatabasePaths, detectDjayInstallation, } from './detect.js';
|
|
8
|
+
export { extractString, extractDouble, extractDate, extractTitleID, extractSourceURIs, parseHistorySessionItem, type DjayHistoryItemFields, } from './tsaf.js';
|
|
9
|
+
export { DJAY_SOURCES, isStreamingSource, toNowPlayingStreamingSource, type DjayOriginSourceID, type DjaySourceInfo, } from './sources.js';
|
|
10
|
+
export type { Logger } from './types/logger.js';
|
|
11
|
+
export { noopLogger } from './types/logger.js';
|
|
12
|
+
export type { DjayConnectOptions, DjayConnectEvents, DjayNowPlayingTrack, DjayReadyInfo, DjayTrackPayload, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* djay-connect - Library to read djay Pro's MediaLibrary.db and emit track change events.
|
|
4
|
+
*
|
|
5
|
+
* @module djay-connect
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noopLogger = exports.toNowPlayingStreamingSource = exports.isStreamingSource = exports.DJAY_SOURCES = exports.parseHistorySessionItem = exports.extractSourceURIs = exports.extractTitleID = exports.extractDate = exports.extractDouble = exports.extractString = exports.detectDjayInstallation = exports.getDefaultDatabasePaths = exports.getDefaultDatabasePath = exports.getDefaultDjayInstallPath = exports.DjayConnect = void 0;
|
|
9
|
+
// Main connector class
|
|
10
|
+
var djayConnect_js_1 = require("./djayConnect.js");
|
|
11
|
+
Object.defineProperty(exports, "DjayConnect", { enumerable: true, get: function () { return djayConnect_js_1.DjayConnect; } });
|
|
12
|
+
// Detection utilities
|
|
13
|
+
var detect_js_1 = require("./detect.js");
|
|
14
|
+
Object.defineProperty(exports, "getDefaultDjayInstallPath", { enumerable: true, get: function () { return detect_js_1.getDefaultDjayInstallPath; } });
|
|
15
|
+
Object.defineProperty(exports, "getDefaultDatabasePath", { enumerable: true, get: function () { return detect_js_1.getDefaultDatabasePath; } });
|
|
16
|
+
Object.defineProperty(exports, "getDefaultDatabasePaths", { enumerable: true, get: function () { return detect_js_1.getDefaultDatabasePaths; } });
|
|
17
|
+
Object.defineProperty(exports, "detectDjayInstallation", { enumerable: true, get: function () { return detect_js_1.detectDjayInstallation; } });
|
|
18
|
+
// TSAF parser (exported for tests and advanced use)
|
|
19
|
+
var tsaf_js_1 = require("./tsaf.js");
|
|
20
|
+
Object.defineProperty(exports, "extractString", { enumerable: true, get: function () { return tsaf_js_1.extractString; } });
|
|
21
|
+
Object.defineProperty(exports, "extractDouble", { enumerable: true, get: function () { return tsaf_js_1.extractDouble; } });
|
|
22
|
+
Object.defineProperty(exports, "extractDate", { enumerable: true, get: function () { return tsaf_js_1.extractDate; } });
|
|
23
|
+
Object.defineProperty(exports, "extractTitleID", { enumerable: true, get: function () { return tsaf_js_1.extractTitleID; } });
|
|
24
|
+
Object.defineProperty(exports, "extractSourceURIs", { enumerable: true, get: function () { return tsaf_js_1.extractSourceURIs; } });
|
|
25
|
+
Object.defineProperty(exports, "parseHistorySessionItem", { enumerable: true, get: function () { return tsaf_js_1.parseHistorySessionItem; } });
|
|
26
|
+
// Track-source constants and helpers
|
|
27
|
+
var sources_js_1 = require("./sources.js");
|
|
28
|
+
Object.defineProperty(exports, "DJAY_SOURCES", { enumerable: true, get: function () { return sources_js_1.DJAY_SOURCES; } });
|
|
29
|
+
Object.defineProperty(exports, "isStreamingSource", { enumerable: true, get: function () { return sources_js_1.isStreamingSource; } });
|
|
30
|
+
Object.defineProperty(exports, "toNowPlayingStreamingSource", { enumerable: true, get: function () { return sources_js_1.toNowPlayingStreamingSource; } });
|
|
31
|
+
var logger_js_1 = require("./types/logger.js");
|
|
32
|
+
Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return logger_js_1.noopLogger; } });
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,uBAAuB;AACvB,mDAA+C;AAAtC,6GAAA,WAAW,OAAA;AAEpB,sBAAsB;AACtB,yCAKqB;AAJnB,sHAAA,yBAAyB,OAAA;AACzB,mHAAA,sBAAsB,OAAA;AACtB,oHAAA,uBAAuB,OAAA;AACvB,mHAAA,sBAAsB,OAAA;AAGxB,oDAAoD;AACpD,qCAQmB;AAPjB,wGAAA,aAAa,OAAA;AACb,wGAAA,aAAa,OAAA;AACb,sGAAA,WAAW,OAAA;AACX,yGAAA,cAAc,OAAA;AACd,4GAAA,iBAAiB,OAAA;AACjB,kHAAA,uBAAuB,OAAA;AAIzB,qCAAqC;AACrC,2CAMsB;AALpB,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,yHAAA,2BAA2B,OAAA;AAO7B,+CAA+C;AAAtC,uGAAA,UAAU,OAAA"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Known `originSourceID` values and URI schemes used by djay Pro.
|
|
3
|
+
*
|
|
4
|
+
* djay Pro for Windows 5.6.4 integrates with six streaming services and two
|
|
5
|
+
* "local" source categories. Not all of these were observable in the session
|
|
6
|
+
* used to reverse-engineer the format, so entries marked `observed: false`
|
|
7
|
+
* are educated guesses based on Algoriddim's naming convention (the service
|
|
8
|
+
* name in lowercase is used as both the URI scheme and the originSourceID).
|
|
9
|
+
*
|
|
10
|
+
* Verified: explorer, music, beatport, soundcloud, spotify
|
|
11
|
+
* Inferred: beatsource, tidal, applemusic
|
|
12
|
+
*/
|
|
13
|
+
/** Every track source djay Pro is known (or believed) to support. */
|
|
14
|
+
export type DjayOriginSourceID = 'explorer' | 'music' | 'beatport' | 'beatsource' | 'soundcloud' | 'spotify' | 'tidal' | 'applemusic';
|
|
15
|
+
export interface DjaySourceInfo {
|
|
16
|
+
/** The originSourceID string as written by djay Pro. */
|
|
17
|
+
id: DjayOriginSourceID;
|
|
18
|
+
/** Whether this value was observed in a live djay Pro 5.6.4 session. */
|
|
19
|
+
observed: boolean;
|
|
20
|
+
/** `local` = file on disk; `streaming` = streaming service. */
|
|
21
|
+
kind: 'local' | 'streaming';
|
|
22
|
+
/** Human-readable display label. */
|
|
23
|
+
label: string;
|
|
24
|
+
/**
|
|
25
|
+
* URI scheme prefix for tracks from this source in `sourceURIs`.
|
|
26
|
+
* Undefined for local sources (which use `file://` URIs).
|
|
27
|
+
*/
|
|
28
|
+
uriSchemePrefix?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map of `originSourceID` → descriptive info. Callers can iterate this to
|
|
32
|
+
* build dropdown lists, detection heuristics, or validation helpers.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DJAY_SOURCES: Record<DjayOriginSourceID, DjaySourceInfo>;
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if the given `originSourceID` represents a streaming service
|
|
37
|
+
* (as opposed to a local file). Unknown values default to `false`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isStreamingSource(id: string | undefined): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Narrow a djay Pro `originSourceID` to the nowplaying3 `streamingSource`
|
|
42
|
+
* enum (`beatport | tidal | soundcloud | streaming-direct-play`). Services
|
|
43
|
+
* that aren't in the nowplaying3 enum (Spotify, Beatsource, Apple Music)
|
|
44
|
+
* map to `streaming-direct-play` as a generic fallback. Local sources
|
|
45
|
+
* return `undefined`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function toNowPlayingStreamingSource(id: string | undefined): 'beatport' | 'tidal' | 'soundcloud' | 'streaming-direct-play' | undefined;
|
package/dist/sources.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Known `originSourceID` values and URI schemes used by djay Pro.
|
|
4
|
+
*
|
|
5
|
+
* djay Pro for Windows 5.6.4 integrates with six streaming services and two
|
|
6
|
+
* "local" source categories. Not all of these were observable in the session
|
|
7
|
+
* used to reverse-engineer the format, so entries marked `observed: false`
|
|
8
|
+
* are educated guesses based on Algoriddim's naming convention (the service
|
|
9
|
+
* name in lowercase is used as both the URI scheme and the originSourceID).
|
|
10
|
+
*
|
|
11
|
+
* Verified: explorer, music, beatport, soundcloud, spotify
|
|
12
|
+
* Inferred: beatsource, tidal, applemusic
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DJAY_SOURCES = void 0;
|
|
16
|
+
exports.isStreamingSource = isStreamingSource;
|
|
17
|
+
exports.toNowPlayingStreamingSource = toNowPlayingStreamingSource;
|
|
18
|
+
/**
|
|
19
|
+
* Map of `originSourceID` → descriptive info. Callers can iterate this to
|
|
20
|
+
* build dropdown lists, detection heuristics, or validation helpers.
|
|
21
|
+
*/
|
|
22
|
+
exports.DJAY_SOURCES = {
|
|
23
|
+
explorer: {
|
|
24
|
+
id: 'explorer',
|
|
25
|
+
observed: true,
|
|
26
|
+
kind: 'local',
|
|
27
|
+
label: 'File Explorer',
|
|
28
|
+
},
|
|
29
|
+
music: {
|
|
30
|
+
id: 'music',
|
|
31
|
+
observed: true,
|
|
32
|
+
kind: 'local',
|
|
33
|
+
label: 'OS Music Library',
|
|
34
|
+
},
|
|
35
|
+
beatport: {
|
|
36
|
+
id: 'beatport',
|
|
37
|
+
observed: true,
|
|
38
|
+
kind: 'streaming',
|
|
39
|
+
label: 'Beatport',
|
|
40
|
+
uriSchemePrefix: 'beatport:track:',
|
|
41
|
+
},
|
|
42
|
+
beatsource: {
|
|
43
|
+
id: 'beatsource',
|
|
44
|
+
observed: false,
|
|
45
|
+
kind: 'streaming',
|
|
46
|
+
label: 'Beatsource',
|
|
47
|
+
uriSchemePrefix: 'beatsource:track:',
|
|
48
|
+
},
|
|
49
|
+
soundcloud: {
|
|
50
|
+
id: 'soundcloud',
|
|
51
|
+
observed: true,
|
|
52
|
+
kind: 'streaming',
|
|
53
|
+
label: 'SoundCloud',
|
|
54
|
+
uriSchemePrefix: 'soundcloud:tracks:',
|
|
55
|
+
},
|
|
56
|
+
spotify: {
|
|
57
|
+
id: 'spotify',
|
|
58
|
+
observed: true,
|
|
59
|
+
kind: 'streaming',
|
|
60
|
+
label: 'Spotify',
|
|
61
|
+
uriSchemePrefix: 'spotify:track:',
|
|
62
|
+
},
|
|
63
|
+
tidal: {
|
|
64
|
+
id: 'tidal',
|
|
65
|
+
observed: false,
|
|
66
|
+
kind: 'streaming',
|
|
67
|
+
label: 'TIDAL',
|
|
68
|
+
uriSchemePrefix: 'tidal:track:',
|
|
69
|
+
},
|
|
70
|
+
applemusic: {
|
|
71
|
+
id: 'applemusic',
|
|
72
|
+
observed: false,
|
|
73
|
+
kind: 'streaming',
|
|
74
|
+
label: 'Apple Music',
|
|
75
|
+
uriSchemePrefix: 'applemusic:track:',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Returns true if the given `originSourceID` represents a streaming service
|
|
80
|
+
* (as opposed to a local file). Unknown values default to `false`.
|
|
81
|
+
*/
|
|
82
|
+
function isStreamingSource(id) {
|
|
83
|
+
if (!id)
|
|
84
|
+
return false;
|
|
85
|
+
const info = exports.DJAY_SOURCES[id];
|
|
86
|
+
return info?.kind === 'streaming';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Narrow a djay Pro `originSourceID` to the nowplaying3 `streamingSource`
|
|
90
|
+
* enum (`beatport | tidal | soundcloud | streaming-direct-play`). Services
|
|
91
|
+
* that aren't in the nowplaying3 enum (Spotify, Beatsource, Apple Music)
|
|
92
|
+
* map to `streaming-direct-play` as a generic fallback. Local sources
|
|
93
|
+
* return `undefined`.
|
|
94
|
+
*/
|
|
95
|
+
function toNowPlayingStreamingSource(id) {
|
|
96
|
+
if (!id)
|
|
97
|
+
return undefined;
|
|
98
|
+
switch (id) {
|
|
99
|
+
case 'beatport':
|
|
100
|
+
return 'beatport';
|
|
101
|
+
case 'tidal':
|
|
102
|
+
return 'tidal';
|
|
103
|
+
case 'soundcloud':
|
|
104
|
+
return 'soundcloud';
|
|
105
|
+
case 'spotify':
|
|
106
|
+
case 'beatsource':
|
|
107
|
+
case 'applemusic':
|
|
108
|
+
return 'streaming-direct-play';
|
|
109
|
+
default:
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.js","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AA8FH,8CAIC;AASD,kEAuBC;AArGD;;;GAGG;AACU,QAAA,YAAY,GAA+C;IACtE,QAAQ,EAAE;QACR,EAAE,EAAE,UAAU;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,eAAe;KACvB;IACD,KAAK,EAAE;QACL,EAAE,EAAE,OAAO;QACX,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,kBAAkB;KAC1B;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,UAAU;QACd,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,UAAU;QACjB,eAAe,EAAE,iBAAiB;KACnC;IACD,UAAU,EAAE;QACV,EAAE,EAAE,YAAY;QAChB,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,YAAY;QACnB,eAAe,EAAE,mBAAmB;KACrC;IACD,UAAU,EAAE;QACV,EAAE,EAAE,YAAY;QAChB,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,YAAY;QACnB,eAAe,EAAE,oBAAoB;KACtC;IACD,OAAO,EAAE;QACP,EAAE,EAAE,SAAS;QACb,QAAQ,EAAE,IAAI;QACd,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,SAAS;QAChB,eAAe,EAAE,gBAAgB;KAClC;IACD,KAAK,EAAE;QACL,EAAE,EAAE,OAAO;QACX,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,OAAO;QACd,eAAe,EAAE,cAAc;KAChC;IACD,UAAU,EAAE;QACV,EAAE,EAAE,YAAY;QAChB,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,aAAa;QACpB,eAAe,EAAE,mBAAmB;KACrC;CACF,CAAC;AAEF;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,EAAsB;IACtD,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,MAAM,IAAI,GAAG,oBAAY,CAAC,EAAwB,CAAC,CAAC;IACpD,OAAO,IAAI,EAAE,IAAI,KAAK,WAAW,CAAC;AACpC,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,2BAA2B,CACzC,EAAsB;IAOtB,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1B,QAAQ,EAAE,EAAE,CAAC;QACX,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,SAAS,CAAC;QACf,KAAK,YAAY,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,uBAAuB,CAAC;QACjC;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
|
package/dist/tsaf.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Minimal parser for Algoriddim's TSAF serialization format used inside
|
|
3
|
+
* djay Pro's MediaLibrary.db (YapDatabase `database2.data` column).
|
|
4
|
+
*
|
|
5
|
+
* TSAF records are a stream of typed values, where each value is followed by its key:
|
|
6
|
+
*
|
|
7
|
+
* `<value bytes> 0x08 <key-ascii> 0x00`
|
|
8
|
+
*
|
|
9
|
+
* Known value tags:
|
|
10
|
+
* - `0x08 <utf8-bytes> 0x00` — UTF-8 string
|
|
11
|
+
* - `0x14` + 8-byte little-endian IEEE 754 double (present as the 8 bytes immediately
|
|
12
|
+
* preceding the key tag, regardless of any leading padding/subtype bytes)
|
|
13
|
+
* - `0x30` + 8-byte little-endian CFAbsoluteTime double (seconds since 2001-01-01 UTC)
|
|
14
|
+
*
|
|
15
|
+
* We only need to extract a handful of known fields (title, artist, deckNumber, etc.),
|
|
16
|
+
* so rather than fully parsing the container, we locate each key tag and read the
|
|
17
|
+
* preceding value bytes according to the expected type. This approach has been verified
|
|
18
|
+
* against real `historySessionItems` rows on djay Pro for Windows.
|
|
19
|
+
*/
|
|
20
|
+
/** Extract a single typed field from a TSAF blob, or return undefined if not present. */
|
|
21
|
+
export declare function extractString(blob: Buffer, key: string): string | undefined;
|
|
22
|
+
export declare function extractDouble(blob: Buffer, key: string): number | undefined;
|
|
23
|
+
export declare function extractDate(blob: Buffer, key: string): Date | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Extract the nested `ADCMediaItemTitleID` — the 32-hex string that joins a
|
|
26
|
+
* history item to its row in `localMediaItemLocations` / `globalMediaItemLocations`
|
|
27
|
+
* / `mediaItemTitleIDs` / `mediaItemAnalyzedData` etc.
|
|
28
|
+
*
|
|
29
|
+
* Inside a TSAF blob the nested class looks like:
|
|
30
|
+
*
|
|
31
|
+
* `0x2B 0x08 'ADCMediaItemTitleID' 0x00 0x08 <32-hex chars> 0x00 ...`
|
|
32
|
+
*
|
|
33
|
+
* We locate the class marker and read the tagged string that immediately
|
|
34
|
+
* follows it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractTitleID(blob: Buffer): string | undefined;
|
|
37
|
+
/** Shape of the fields we extract from a historySessionItems blob. */
|
|
38
|
+
export interface DjayHistoryItemFields {
|
|
39
|
+
uuid: string;
|
|
40
|
+
sessionUUID: string;
|
|
41
|
+
/** 32-char hex titleID that joins into the location / analysis tables. */
|
|
42
|
+
titleID: string | undefined;
|
|
43
|
+
title: string;
|
|
44
|
+
artist: string;
|
|
45
|
+
duration: number;
|
|
46
|
+
deckNumber: number;
|
|
47
|
+
startTime: Date;
|
|
48
|
+
/** djay Pro's source identifier (e.g. 'explorer', 'spotify', 'beatport'). */
|
|
49
|
+
originSourceID: string | undefined;
|
|
50
|
+
/** International Standard Recording Code — present on streaming tracks. */
|
|
51
|
+
isrc: string | undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse a historySessionItems blob into a typed record.
|
|
55
|
+
* Returns `null` if the blob is missing required fields (title or artist).
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseHistorySessionItem(blob: Buffer): DjayHistoryItemFields | null;
|
|
58
|
+
/**
|
|
59
|
+
* Extract the `sourceURIs` strings from a `localMediaItemLocations` or
|
|
60
|
+
* `globalMediaItemLocations` blob. A single location record can carry more
|
|
61
|
+
* than one URI — e.g. *Good Catch (Black Caviar Remix)* in the test session
|
|
62
|
+
* has both `soundcloud:tracks:1150488265` and `beatport:track:15949981`.
|
|
63
|
+
*
|
|
64
|
+
* Each URI is stored as a distinctive `0x21 0x08 <utf8 bytes> 0x00` sequence,
|
|
65
|
+
* wrapped in an array header `0x0B 0x00 0x00 <count> 0x00 0x00 0x00`. The
|
|
66
|
+
* array immediately precedes a `0x08 'sourceURIs' 0x00` key tag. Rather than
|
|
67
|
+
* fully decoding the array framing, we scan forward through the blob for any
|
|
68
|
+
* `0x21 0x08 <uri> 0x00` pattern that sits *before* the key and return all
|
|
69
|
+
* matches in order.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractSourceURIs(blob: Buffer): string[];
|
package/dist/tsaf.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Minimal parser for Algoriddim's TSAF serialization format used inside
|
|
4
|
+
* djay Pro's MediaLibrary.db (YapDatabase `database2.data` column).
|
|
5
|
+
*
|
|
6
|
+
* TSAF records are a stream of typed values, where each value is followed by its key:
|
|
7
|
+
*
|
|
8
|
+
* `<value bytes> 0x08 <key-ascii> 0x00`
|
|
9
|
+
*
|
|
10
|
+
* Known value tags:
|
|
11
|
+
* - `0x08 <utf8-bytes> 0x00` — UTF-8 string
|
|
12
|
+
* - `0x14` + 8-byte little-endian IEEE 754 double (present as the 8 bytes immediately
|
|
13
|
+
* preceding the key tag, regardless of any leading padding/subtype bytes)
|
|
14
|
+
* - `0x30` + 8-byte little-endian CFAbsoluteTime double (seconds since 2001-01-01 UTC)
|
|
15
|
+
*
|
|
16
|
+
* We only need to extract a handful of known fields (title, artist, deckNumber, etc.),
|
|
17
|
+
* so rather than fully parsing the container, we locate each key tag and read the
|
|
18
|
+
* preceding value bytes according to the expected type. This approach has been verified
|
|
19
|
+
* against real `historySessionItems` rows on djay Pro for Windows.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.extractString = extractString;
|
|
23
|
+
exports.extractDouble = extractDouble;
|
|
24
|
+
exports.extractDate = extractDate;
|
|
25
|
+
exports.extractTitleID = extractTitleID;
|
|
26
|
+
exports.parseHistorySessionItem = parseHistorySessionItem;
|
|
27
|
+
exports.extractSourceURIs = extractSourceURIs;
|
|
28
|
+
/** CFAbsoluteTime epoch: 2001-01-01 00:00:00 UTC in milliseconds since Unix epoch. */
|
|
29
|
+
const CF_EPOCH_MS = Date.UTC(2001, 0, 1, 0, 0, 0);
|
|
30
|
+
const STRING_TAG = 0x08;
|
|
31
|
+
/** Maximum bytes to walk backwards when resolving a string value. */
|
|
32
|
+
const MAX_STRING_SCAN = 2048;
|
|
33
|
+
function findKey(blob, key) {
|
|
34
|
+
const needle = Buffer.alloc(key.length + 2);
|
|
35
|
+
needle[0] = STRING_TAG;
|
|
36
|
+
needle.write(key, 1, 'ascii');
|
|
37
|
+
needle[needle.length - 1] = 0x00;
|
|
38
|
+
return blob.indexOf(needle);
|
|
39
|
+
}
|
|
40
|
+
/** Read an 8-byte little-endian double that sits immediately before the given key tag. */
|
|
41
|
+
function readDoubleBeforeKey(blob, keyPos) {
|
|
42
|
+
if (keyPos < 8)
|
|
43
|
+
return undefined;
|
|
44
|
+
return blob.readDoubleLE(keyPos - 8);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read a UTF-8 string value that precedes the given key tag.
|
|
48
|
+
* The value is encoded as `0x08 <utf8-bytes> 0x00` immediately before the key.
|
|
49
|
+
*/
|
|
50
|
+
function readStringBeforeKey(blob, keyPos) {
|
|
51
|
+
if (keyPos < 2)
|
|
52
|
+
return undefined;
|
|
53
|
+
const nullPos = keyPos - 1;
|
|
54
|
+
if (blob[nullPos] !== 0x00)
|
|
55
|
+
return undefined;
|
|
56
|
+
const scanLimit = Math.max(0, nullPos - MAX_STRING_SCAN);
|
|
57
|
+
for (let i = nullPos - 1; i >= scanLimit; i--) {
|
|
58
|
+
if (blob[i] !== STRING_TAG)
|
|
59
|
+
continue;
|
|
60
|
+
const value = blob.subarray(i + 1, nullPos);
|
|
61
|
+
if (!isPrintableUtf8(value))
|
|
62
|
+
continue;
|
|
63
|
+
return value.toString('utf-8');
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function isPrintableUtf8(bytes) {
|
|
68
|
+
for (const b of bytes) {
|
|
69
|
+
// Allow common UTF-8 continuation/lead bytes (>= 0x80) and printable ASCII.
|
|
70
|
+
// Reject control characters except tab (rare in titles but allow for safety).
|
|
71
|
+
if (b === 0x09)
|
|
72
|
+
continue;
|
|
73
|
+
if (b >= 0x20 && b < 0x7f)
|
|
74
|
+
continue;
|
|
75
|
+
if (b >= 0x80)
|
|
76
|
+
continue;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/** Extract a single typed field from a TSAF blob, or return undefined if not present. */
|
|
82
|
+
function extractString(blob, key) {
|
|
83
|
+
const pos = findKey(blob, key);
|
|
84
|
+
if (pos < 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
return readStringBeforeKey(blob, pos);
|
|
87
|
+
}
|
|
88
|
+
function extractDouble(blob, key) {
|
|
89
|
+
const pos = findKey(blob, key);
|
|
90
|
+
if (pos < 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
return readDoubleBeforeKey(blob, pos);
|
|
93
|
+
}
|
|
94
|
+
function extractDate(blob, key) {
|
|
95
|
+
const pos = findKey(blob, key);
|
|
96
|
+
if (pos < 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
const cfAbsolute = readDoubleBeforeKey(blob, pos);
|
|
99
|
+
if (cfAbsolute === undefined || !Number.isFinite(cfAbsolute))
|
|
100
|
+
return undefined;
|
|
101
|
+
return new Date(CF_EPOCH_MS + cfAbsolute * 1000);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract the nested `ADCMediaItemTitleID` — the 32-hex string that joins a
|
|
105
|
+
* history item to its row in `localMediaItemLocations` / `globalMediaItemLocations`
|
|
106
|
+
* / `mediaItemTitleIDs` / `mediaItemAnalyzedData` etc.
|
|
107
|
+
*
|
|
108
|
+
* Inside a TSAF blob the nested class looks like:
|
|
109
|
+
*
|
|
110
|
+
* `0x2B 0x08 'ADCMediaItemTitleID' 0x00 0x08 <32-hex chars> 0x00 ...`
|
|
111
|
+
*
|
|
112
|
+
* We locate the class marker and read the tagged string that immediately
|
|
113
|
+
* follows it.
|
|
114
|
+
*/
|
|
115
|
+
function extractTitleID(blob) {
|
|
116
|
+
const marker = Buffer.concat([
|
|
117
|
+
Buffer.from([0x2b, 0x08]),
|
|
118
|
+
Buffer.from('ADCMediaItemTitleID', 'ascii'),
|
|
119
|
+
Buffer.from([0x00]),
|
|
120
|
+
]);
|
|
121
|
+
const markerPos = blob.indexOf(marker);
|
|
122
|
+
if (markerPos < 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const stringTagPos = markerPos + marker.length;
|
|
125
|
+
if (blob[stringTagPos] !== 0x08)
|
|
126
|
+
return undefined;
|
|
127
|
+
const stringStart = stringTagPos + 1;
|
|
128
|
+
const stringEnd = blob.indexOf(0x00, stringStart);
|
|
129
|
+
if (stringEnd < 0)
|
|
130
|
+
return undefined;
|
|
131
|
+
const candidate = blob.subarray(stringStart, stringEnd).toString('ascii');
|
|
132
|
+
// Sanity check: titleIDs are 32-char lowercase hex.
|
|
133
|
+
if (!/^[0-9a-f]{32}$/.test(candidate))
|
|
134
|
+
return undefined;
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parse a historySessionItems blob into a typed record.
|
|
139
|
+
* Returns `null` if the blob is missing required fields (title or artist).
|
|
140
|
+
*/
|
|
141
|
+
function parseHistorySessionItem(blob) {
|
|
142
|
+
const title = extractString(blob, 'title');
|
|
143
|
+
const artist = extractString(blob, 'artist');
|
|
144
|
+
if (!title || !artist)
|
|
145
|
+
return null;
|
|
146
|
+
const uuid = extractString(blob, 'uuid') ?? '';
|
|
147
|
+
const sessionUUID = extractString(blob, 'sessionUUID') ?? '';
|
|
148
|
+
const titleID = extractTitleID(blob);
|
|
149
|
+
const duration = extractDouble(blob, 'duration') ?? 0;
|
|
150
|
+
const deckRaw = extractDouble(blob, 'deckNumber') ?? 0;
|
|
151
|
+
const deckNumber = Number.isFinite(deckRaw) ? Math.round(deckRaw) : 0;
|
|
152
|
+
const startTime = extractDate(blob, 'startTime') ?? new Date(0);
|
|
153
|
+
const originSourceID = extractString(blob, 'originSourceID');
|
|
154
|
+
const isrc = extractString(blob, 'isrc');
|
|
155
|
+
return {
|
|
156
|
+
uuid,
|
|
157
|
+
sessionUUID,
|
|
158
|
+
titleID,
|
|
159
|
+
title,
|
|
160
|
+
artist,
|
|
161
|
+
duration,
|
|
162
|
+
deckNumber,
|
|
163
|
+
startTime,
|
|
164
|
+
originSourceID,
|
|
165
|
+
isrc,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Extract the `sourceURIs` strings from a `localMediaItemLocations` or
|
|
170
|
+
* `globalMediaItemLocations` blob. A single location record can carry more
|
|
171
|
+
* than one URI — e.g. *Good Catch (Black Caviar Remix)* in the test session
|
|
172
|
+
* has both `soundcloud:tracks:1150488265` and `beatport:track:15949981`.
|
|
173
|
+
*
|
|
174
|
+
* Each URI is stored as a distinctive `0x21 0x08 <utf8 bytes> 0x00` sequence,
|
|
175
|
+
* wrapped in an array header `0x0B 0x00 0x00 <count> 0x00 0x00 0x00`. The
|
|
176
|
+
* array immediately precedes a `0x08 'sourceURIs' 0x00` key tag. Rather than
|
|
177
|
+
* fully decoding the array framing, we scan forward through the blob for any
|
|
178
|
+
* `0x21 0x08 <uri> 0x00` pattern that sits *before* the key and return all
|
|
179
|
+
* matches in order.
|
|
180
|
+
*/
|
|
181
|
+
function extractSourceURIs(blob) {
|
|
182
|
+
const keyNeedle = Buffer.concat([
|
|
183
|
+
Buffer.from([0x08]),
|
|
184
|
+
Buffer.from('sourceURIs', 'ascii'),
|
|
185
|
+
Buffer.from([0x00]),
|
|
186
|
+
]);
|
|
187
|
+
const keyPos = blob.indexOf(keyNeedle);
|
|
188
|
+
if (keyPos < 0)
|
|
189
|
+
return [];
|
|
190
|
+
const uris = [];
|
|
191
|
+
const window = blob.subarray(0, keyPos);
|
|
192
|
+
// Each element starts with `0x21 0x08 <utf8 bytes> 0x00`. Scan for the
|
|
193
|
+
// two-byte prefix, then read until the next 0x00.
|
|
194
|
+
for (let i = 0; i < window.length - 2; i++) {
|
|
195
|
+
if (window[i] !== 0x21 || window[i + 1] !== 0x08)
|
|
196
|
+
continue;
|
|
197
|
+
const stringStart = i + 2;
|
|
198
|
+
const stringEnd = window.indexOf(0x00, stringStart);
|
|
199
|
+
if (stringEnd < 0)
|
|
200
|
+
break;
|
|
201
|
+
const candidate = window.subarray(stringStart, stringEnd).toString('utf-8');
|
|
202
|
+
// Must look like a URI — contain a scheme colon and be at least 5 chars.
|
|
203
|
+
if (candidate.length < 5 || candidate.indexOf(':') <= 0) {
|
|
204
|
+
i = stringEnd;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
uris.push(candidate);
|
|
208
|
+
i = stringEnd;
|
|
209
|
+
}
|
|
210
|
+
return uris;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=tsaf.js.map
|
package/dist/tsaf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsaf.js","sourceRoot":"","sources":["../src/tsaf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/detect.ts","../src/djayconnect.ts","../src/index.ts","../src/sources.ts","../src/tsaf.ts","../src/types.ts","../src/types/logger.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Logger interface for djay-connect.
|
|
3
|
+
* Allows consumers to inject their own logger implementation.
|
|
4
|
+
*/
|
|
5
|
+
export interface Logger {
|
|
6
|
+
trace(msg: string, ...args: unknown[]): void;
|
|
7
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
8
|
+
info(msg: string, ...args: unknown[]): void;
|
|
9
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
10
|
+
error(msg: string, ...args: unknown[]): void;
|
|
11
|
+
}
|
|
12
|
+
export declare const noopLogger: Logger;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Logger interface for djay-connect.
|
|
4
|
+
* Allows consumers to inject their own logger implementation.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.noopLogger = void 0;
|
|
8
|
+
exports.noopLogger = {
|
|
9
|
+
trace() { },
|
|
10
|
+
debug() { },
|
|
11
|
+
info() { },
|
|
12
|
+
warn() { },
|
|
13
|
+
error() { },
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/types/logger.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAUU,QAAA,UAAU,GAAW;IAChC,KAAK,KAAI,CAAC;IACV,KAAK,KAAI,CAAC;IACV,IAAI,KAAI,CAAC;IACT,IAAI,KAAI,CAAC;IACT,KAAK,KAAI,CAAC;CACX,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import EventEmitter from 'node:events';
|
|
2
|
+
import type { StrictEventEmitter } from 'strict-event-emitter-types';
|
|
3
|
+
import type { Logger } from './types/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for DjayConnect.
|
|
6
|
+
*/
|
|
7
|
+
export type DjayConnectOptions = {
|
|
8
|
+
/** Polling interval in milliseconds (minimum 2000). Default: 2000 */
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
/** Custom path to djay Pro's MediaLibrary.db. If omitted, uses the default per-platform path. */
|
|
11
|
+
databasePath?: string;
|
|
12
|
+
/** Logger instance. If omitted, logging is disabled. */
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A parsed track from djay Pro's history session.
|
|
17
|
+
*/
|
|
18
|
+
export interface DjayNowPlayingTrack {
|
|
19
|
+
/** Track title */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Artist name */
|
|
22
|
+
artist: string;
|
|
23
|
+
/** Duration in seconds */
|
|
24
|
+
duration: number;
|
|
25
|
+
/** Deck number the track is loaded on (1-4) */
|
|
26
|
+
deckNumber: number;
|
|
27
|
+
/** When the track was started in djay Pro */
|
|
28
|
+
startTime: Date;
|
|
29
|
+
/** UUID of this history entry */
|
|
30
|
+
uuid: string;
|
|
31
|
+
/** UUID of the containing history session */
|
|
32
|
+
sessionUUID: string;
|
|
33
|
+
/**
|
|
34
|
+
* 32-hex `titleID` that joins the track to the location / analysis tables.
|
|
35
|
+
* Present for every track but may be missing on malformed blobs.
|
|
36
|
+
*/
|
|
37
|
+
titleID?: string;
|
|
38
|
+
/**
|
|
39
|
+
* djay Pro's originSourceID for this track — identifies where the track
|
|
40
|
+
* came from. Known values include local sources (`explorer`, `music`)
|
|
41
|
+
* and streaming services (`beatport`, `beatsource`, `soundcloud`,
|
|
42
|
+
* `spotify`, `tidal`, `applemusic`).
|
|
43
|
+
*/
|
|
44
|
+
originSourceID?: string;
|
|
45
|
+
/** International Standard Recording Code — present on streaming tracks. */
|
|
46
|
+
isrc?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Absolute file path for tracks that live on local disk. Decoded from
|
|
49
|
+
* djay's URL-encoded `file://` URIs in `localMediaItemLocations`.
|
|
50
|
+
* Undefined for streaming tracks.
|
|
51
|
+
*/
|
|
52
|
+
filePath?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Raw `sourceURIs` from djay's location tables. Can contain more than one
|
|
55
|
+
* URI for tracks available on multiple streaming services, and the single
|
|
56
|
+
* `file://` URI for local tracks. Never undefined — empty array instead.
|
|
57
|
+
*/
|
|
58
|
+
sourceURIs?: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Info emitted when DjayConnect is ready.
|
|
62
|
+
*/
|
|
63
|
+
export type DjayReadyInfo = {
|
|
64
|
+
/** Path to the MediaLibrary.db file being monitored */
|
|
65
|
+
databasePath: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Payload for track events.
|
|
69
|
+
*/
|
|
70
|
+
export type DjayTrackPayload = {
|
|
71
|
+
/** The track that was detected */
|
|
72
|
+
track: DjayNowPlayingTrack;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Events emitted by DjayConnect.
|
|
76
|
+
*/
|
|
77
|
+
export interface DjayConnectEvents {
|
|
78
|
+
/** Emitted when the connector is ready and monitoring has started */
|
|
79
|
+
ready: (info: DjayReadyInfo) => void;
|
|
80
|
+
/** Emitted on each poll cycle */
|
|
81
|
+
poll: () => void;
|
|
82
|
+
/** Emitted when a new track is detected */
|
|
83
|
+
track: (payload: DjayTrackPayload) => void;
|
|
84
|
+
/** Emitted on errors */
|
|
85
|
+
error: (err: Error) => void;
|
|
86
|
+
}
|
|
87
|
+
export type TypedEmitter = StrictEventEmitter<EventEmitter, DjayConnectEvents>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "djay-connect",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Library to read djay Pro's MediaLibrary.db and emit track change events",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"build": "tsc --build tsconfig.json",
|
|
26
|
+
"watch": "tsc --build tsconfig.json -w",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"prepublishOnly": "npm run clean && npm run build && npm test",
|
|
29
|
+
"release": "node .github/release.js",
|
|
30
|
+
"release:patch": "node .github/release.js --patch",
|
|
31
|
+
"release:major": "node .github/release.js --major"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"better-sqlite3-multiple-ciphers": "^11.7.1",
|
|
38
|
+
"strict-event-emitter-types": "^2.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
42
|
+
"@types/node": "^22.15.29",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
44
|
+
"prettier": "^3.5.3",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.0.16"
|
|
47
|
+
},
|
|
48
|
+
"overrides": {
|
|
49
|
+
"rollup": ">=4.59.0"
|
|
50
|
+
}
|
|
51
|
+
}
|