engine-dj-mcp 0.9.2 → 0.11.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 +366 -134
- package/dist/blobs/index.d.ts +18 -12
- package/dist/blobs/index.js +24 -16
- package/dist/discovery.js +3 -2
- package/dist/errors.d.ts +20 -1
- package/dist/errors.js +8 -0
- package/dist/index.js +8 -2
- package/dist/paths.d.ts +11 -0
- package/dist/paths.js +14 -0
- package/dist/playlists.d.ts +240 -0
- package/dist/playlists.js +447 -0
- package/dist/server.d.ts +5 -2
- package/dist/server.js +138 -18
- package/dist/store/backup.d.ts +2 -0
- package/dist/store/backup.js +72 -0
- package/dist/store/write.d.ts +37 -0
- package/dist/store/write.js +397 -0
- package/dist/tools/audit.js +12 -1
- package/dist/tools/playlists.d.ts +59 -0
- package/dist/tools/playlists.js +206 -0
- package/dist/tools/search.d.ts +10 -0
- package/dist/tools/search.js +58 -0
- package/dist/tools/write-playlist.d.ts +11 -0
- package/dist/tools/write-playlist.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,168 +1,400 @@
|
|
|
1
1
|
# engine-dj-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/engine-dj-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
3
6
|
An MCP server that lets an AI assistant search and audit your **Engine DJ**
|
|
4
|
-
libraries — the one on your
|
|
7
|
+
libraries — the one on your computer and the ones on your USB drives.
|
|
5
8
|
|
|
6
9
|
> **Not affiliated with, endorsed by, or sponsored by inMusic Brands, Denon
|
|
7
10
|
> DJ, or the Engine DJ product.** "Engine DJ" is used here only to name the
|
|
8
11
|
> software whose library this tool reads. No logos or brand artwork from
|
|
9
12
|
> inMusic or Denon DJ are used in this project.
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
Your library is opened **read-only at the operating-system level**. It is
|
|
15
|
+
never written to unless you start the server with `--allow-writes` — see
|
|
16
|
+
[Safety](#safety).
|
|
17
|
+
|
|
18
|
+
## What you can ask
|
|
19
|
+
|
|
20
|
+
Once connected, these are ordinary questions in chat:
|
|
21
|
+
|
|
22
|
+
- *"Something dark around 124 in a minor key I haven't played in six months."*
|
|
23
|
+
- *"Which tracks still have no hot cue set?"*
|
|
24
|
+
- *"Find me anything harmonically compatible with 8A between 138 and 142."*
|
|
25
|
+
- *"What's in my ACID Beach playlist, in order?"*
|
|
26
|
+
- *"Anything around 128 in ACID Beach?"*
|
|
27
|
+
- *"What's broken in my collection — missing files, duplicates, bad tempos?"*
|
|
28
|
+
- *"Where are the cue points on this track, and what tempo did Engine analyse?"*
|
|
29
|
+
- *"Build me a playlist of everything in 5A from 140 BPM up."* (needs `--allow-writes`)
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx engine-dj-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Claude Desktop — add to your configuration:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To let the assistant create playlists as well, add `--allow-writes` — a flag
|
|
48
|
+
in `args` rather than an environment variable precisely so it is visible in
|
|
49
|
+
the configuration you are reading:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp", "--allow-writes"] }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Requirements:** Node.js 22.13 or newer (for the unflagged `node:sqlite`;
|
|
60
|
+
there are no native dependencies), and an Engine DJ library at schema 3.0.0
|
|
61
|
+
through 3.0.2 — Engine DJ 4.5 and 5.x.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
Nine read-only tools, and a tenth — `create_playlist` — that appears only
|
|
66
|
+
when you start the server with `--allow-writes`. Every tool that reads
|
|
67
|
+
library data also accepts an optional `library` argument — see
|
|
68
|
+
[Choosing a library](#choosing-a-library).
|
|
69
|
+
|
|
70
|
+
### `search_tracks`
|
|
71
|
+
|
|
72
|
+
The main one. Full-text search with diacritics folded, plus filters for
|
|
73
|
+
tempo, key, rating, when a track was added and when it was last played.
|
|
74
|
+
|
|
75
|
+
| Argument | What it does |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `q` | Full text over title, artist, album, genre, comment and label. Diacritics are folded, so `bjork` matches `Björk` — Engine's own search does not. |
|
|
78
|
+
| `bpm` | `{ min, max }` or `{ around, tolerance_pct }`. Resolved tempo, so an analysed BPM wins over the tag. |
|
|
79
|
+
| `key` | `{ camelot: [...] }` for exact keys, `{ compatible_with: "8A" }` for harmonic neighbours, `{ mode: "minor" }` for a whole side of the wheel. |
|
|
80
|
+
| `rating` | `{ min, max }`, 0–5. |
|
|
81
|
+
| `played` | `{ never: true }`, or `{ before, after }` taking an ISO date or a relative form like `-6 months`. |
|
|
82
|
+
| `added` | `{ before, after }`, same date forms. |
|
|
83
|
+
| `flags` | `analyzed`, `available`, `has_cues`, `has_beatgrid`. `has_cues` means a hot cue is genuinely set — see [Limitations](#limitations). |
|
|
84
|
+
| `playlist` | `{ id }` or `{ name }` — search inside one playlist. Results still come back by relevance or id; `get_playlist_tracks` is what preserves playlist order. |
|
|
85
|
+
| `fields` | Which columns to return. Defaults to `id, artist, title, bpm, camelot, rating`. |
|
|
86
|
+
| `limit`, `cursor` | Page size (default 25, max 200) and an opaque cursor for the next page. |
|
|
87
|
+
| `include_total` | Off by default because counting costs far more than the page. Capped at 1000 — a capped result carries `total_capped: true` and means "at least 1000". |
|
|
88
|
+
|
|
89
|
+
### `get_tracks`
|
|
90
|
+
|
|
91
|
+
Full metadata for specific track ids, returned in the order you asked for.
|
|
92
|
+
Unknown ids are omitted rather than failing the call.
|
|
93
|
+
|
|
94
|
+
`ids` (required), `fields`, `redact_paths`.
|
|
95
|
+
|
|
96
|
+
### `get_playlists`
|
|
97
|
+
|
|
98
|
+
Your playlist tree, in the order Engine DJ shows it — folders included.
|
|
99
|
+
|
|
100
|
+
The list is flat and in the order you would read down the sidebar with every
|
|
101
|
+
folder expanded: `depth` and `path` carry the nesting, `parent_id` names the
|
|
102
|
+
folder a list sits in.
|
|
103
|
+
|
|
104
|
+
| Field | Meaning |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `name`, `id`, `path` | `path` is the full `Folder/Sub/Name`, and is unique — a bare `name` need not be. |
|
|
107
|
+
| `depth`, `parent_id` | The nesting. `parent_id` is `null` at the top level. |
|
|
108
|
+
| `is_folder` | The list has child lists. Engine has no folder flag — a folder *is* a playlist that other playlists sit under — so an emptied folder reads as an empty playlist. |
|
|
109
|
+
| `is_persisted` | Engine's own flag for a list saved to the device. Both values appear on lists Engine displays, so nothing is filtered on it. |
|
|
110
|
+
| `track_count` | Entries in that list alone, never rolled up from its children — the number Engine shows beside it. |
|
|
111
|
+
| `missing_count` | How many of those entries name a track this library does not have. |
|
|
112
|
+
|
|
113
|
+
`limit` (default 200, max 1000). `warnings` appears if a playlist's link
|
|
114
|
+
chain is damaged; nothing is ever dropped from the list because of one.
|
|
115
|
+
|
|
116
|
+
### `get_playlist_tracks`
|
|
117
|
+
|
|
118
|
+
The tracks of one playlist, **in playlist order**.
|
|
119
|
+
|
|
120
|
+
Name it with `playlist_id` or with `playlist_name` — exactly one of the two.
|
|
121
|
+
Playlist names are unique only within a folder, so a name matching more than
|
|
122
|
+
one is refused with every candidate's id and full path rather than guessed
|
|
123
|
+
at; pass the `path` from `get_playlists` to say which you meant.
|
|
124
|
+
|
|
125
|
+
Every row carries `position`, its 1-based place in the playlist. Same
|
|
126
|
+
`fields`, `limit` and `cursor` conventions as `search_tracks`.
|
|
127
|
+
|
|
128
|
+
An entry whose track is not in this library keeps its slot and comes back as
|
|
129
|
+
`{ position, entry_id, track_id, missing: true }`, with `missing_count`
|
|
130
|
+
alongside `entry_count`. That is ordinary rather than corruption — playlist
|
|
131
|
+
entries outlive their tracks and travel between drives — and they are kept in
|
|
132
|
+
place so the number of rows still matches the playlist's own length. On one
|
|
133
|
+
reference library a 43-entry playlist holds exactly one track that library
|
|
134
|
+
can actually play.
|
|
135
|
+
|
|
136
|
+
### `get_track_performance`
|
|
137
|
+
|
|
138
|
+
Decodes the binary `PerformanceData` Engine stores per track: hot cues, the
|
|
139
|
+
main cue, saved loops, the beatgrid, and a coarse waveform profile.
|
|
140
|
+
|
|
141
|
+
Every field carries its own decode status **and** its own `layout` marker.
|
|
142
|
+
`layout: "verified"` means the byte layout was confirmed against a real
|
|
143
|
+
library, so `status: "ok"` is a claim about the values. `layout: "unverified"`
|
|
144
|
+
would mean only that the bytes parsed; no field returns it today.
|
|
145
|
+
|
|
146
|
+
Positions are sample offsets; cue and loop items also carry seconds.
|
|
147
|
+
`items: []` with `slots: 8` means an analysed track with no cues set.
|
|
148
|
+
|
|
149
|
+
`id` (required).
|
|
150
|
+
|
|
151
|
+
### `audit_library`
|
|
152
|
+
|
|
153
|
+
Ten collection health checks. Returns a count and a small sample of ids per
|
|
154
|
+
check, never the full result set — a library with thousands of unanalysed
|
|
155
|
+
tracks should not fill an assistant's context.
|
|
156
|
+
|
|
157
|
+
| Check | Finds |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `missing_files` | Tracks whose file is gone from disk |
|
|
160
|
+
| `unavailable` | Tracks Engine has marked unavailable |
|
|
161
|
+
| `unanalyzed` | Tracks Engine has not analysed |
|
|
162
|
+
| `no_cues` | Tracks with no hot cue set |
|
|
163
|
+
| `no_beatgrid` | Tracks with no beatgrid data |
|
|
164
|
+
| `missing_key` | Tracks with no key detected |
|
|
165
|
+
| `suspicious_bpm` | Analysed and tagged tempo disagree, or tempo is outside 60–200 |
|
|
166
|
+
| `duplicates` | Same artist and title, or same size and length |
|
|
167
|
+
| `empty_metadata` | No artist or no title |
|
|
168
|
+
| `orphan_entries` | Playlist entries pointing at tracks not in this library — `get_playlist_tracks` shows where each one sits |
|
|
169
|
+
|
|
170
|
+
`checks` — omit it to run all ten.
|
|
171
|
+
|
|
172
|
+
### `run_sql`
|
|
173
|
+
|
|
174
|
+
An escape hatch for questions the tools above do not cover. Read-only is
|
|
175
|
+
enforced by the kernel, not by this tool. Results are bounded whatever the
|
|
176
|
+
query says.
|
|
177
|
+
|
|
178
|
+
Prefer `side.track_derived.camelot` and `side.track_derived.tempo` in `WHERE`
|
|
179
|
+
clauses over the `camelot()` and `tempo()` SQL functions — the functions run
|
|
180
|
+
per row and defeat the indexes.
|
|
181
|
+
|
|
182
|
+
`sql` (required), `params`, `limit`.
|
|
183
|
+
|
|
184
|
+
### `list_libraries`
|
|
185
|
+
|
|
186
|
+
Every library found, including ones whose schema is unsupported — listed
|
|
187
|
+
with their version, so you can tell a broken server from a missing library.
|
|
188
|
+
Re-scans on every call, so a drive plugged in after the server started shows
|
|
189
|
+
up without a restart. A library that is temporarily unreadable — Engine DJ
|
|
190
|
+
writing to it, say — stays listed with `status: "unreadable"` rather than
|
|
191
|
+
vanishing.
|
|
192
|
+
|
|
193
|
+
No arguments.
|
|
194
|
+
|
|
195
|
+
### `refresh_index`
|
|
196
|
+
|
|
197
|
+
Rebuilds the search index if the library changed. Normally unnecessary; the
|
|
198
|
+
server checks staleness itself before answering.
|
|
199
|
+
|
|
200
|
+
### `create_playlist`
|
|
201
|
+
|
|
202
|
+
The only tool that writes, and the only one that is not registered at all
|
|
203
|
+
unless the server was started with `--allow-writes`.
|
|
204
|
+
|
|
205
|
+
Creates one new top-level playlist from track ids — `track_ids` sets both
|
|
206
|
+
what is in it and the order it is in, so a list built by `search_tracks`
|
|
207
|
+
arrives in Engine DJ in the order the assistant chose. Nothing else changes:
|
|
208
|
+
no playlist is renamed, reordered, emptied or deleted, and no track, cue or
|
|
209
|
+
beatgrid is touched. The one existing row that moves is the previous last
|
|
210
|
+
playlist's link, and Engine's own insert trigger is what moves it.
|
|
211
|
+
|
|
212
|
+
| Argument | What it does |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `title` | Name of the new playlist. Must not already be taken at the top level — Engine allows one name per folder. |
|
|
215
|
+
| `track_ids` | Ids from `search_tracks` or `get_tracks`, in playlist order. May be empty, for an empty playlist. A track may appear at most once, which is Engine's own rule. |
|
|
216
|
+
|
|
217
|
+
Each entry stores the track's origin identity — `(originDatabaseUuid,
|
|
218
|
+
originTrackId)`, the pair Engine matches on — not the local row id, so a
|
|
219
|
+
playlist built here reads the same way Engine's own does.
|
|
220
|
+
|
|
221
|
+
The result carries `playlist_id`, `tracks_added` and `backup_path`. **To undo
|
|
222
|
+
it, delete the playlist in Engine DJ**; `backup_path` is a whole-library
|
|
223
|
+
snapshot for the case where something went wrong at a lower level, not an
|
|
224
|
+
undo — see [Restoring a snapshot](#restoring-a-snapshot).
|
|
225
|
+
|
|
226
|
+
Refusals name themselves: `playlist_exists` for a taken title,
|
|
227
|
+
`unknown_track` for an id this library does not have, `duplicate_track` for
|
|
228
|
+
the same id twice, `library_busy` if something else holds a conflicting lock
|
|
229
|
+
right then, `library_needs_recovery` if Engine DJ left an unrecovered
|
|
230
|
+
journal behind. Every error also carries `detail`: `not_committed` means the
|
|
231
|
+
library is exactly what it was, and `committed_unverified` — the rare one —
|
|
232
|
+
means the write may have landed but could not be verified afterwards, and is
|
|
233
|
+
the only case that hands back a `backup_path`.
|
|
234
|
+
|
|
235
|
+
## Resources
|
|
236
|
+
|
|
237
|
+
- **`engine://schema`** — the field semantics an assistant needs before
|
|
238
|
+
writing SQL: how Engine encodes musical key, why tempo is
|
|
239
|
+
`COALESCE(bpmAnalyzed, bpm)`, that `Track.path` is relative, where playlist
|
|
240
|
+
order really lives, and which helper columns are indexed.
|
|
241
|
+
- **`engine://libraries`** — what was discovered at startup and whether each
|
|
242
|
+
library's schema is supported. A snapshot; `list_libraries` is the live view.
|
|
34
243
|
|
|
35
244
|
## Choosing a library
|
|
36
245
|
|
|
37
246
|
Engine DJ keeps a library on your computer and another on every drive you
|
|
38
|
-
export to, so more than one is usually connected
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Each library gets its own search index and its own read-only connection,
|
|
54
|
-
opened the first time you ask that library a question — mounting four drives
|
|
55
|
-
does not cost four connections. Every query, audit and file-path check stays
|
|
56
|
-
inside the library selected for that call. Comparing two libraries against
|
|
57
|
-
each other — "what is on this drive but not that one?" — is **not** something
|
|
58
|
-
this server does.
|
|
247
|
+
export to, so more than one is usually connected. `list_libraries` reports
|
|
248
|
+
each with a `uuid` and a `path`, and every tool that reads library data takes
|
|
249
|
+
an optional `library` argument. Pass either form exactly as printed — the
|
|
250
|
+
`~/…` path is accepted alongside the absolute one. A value matching neither
|
|
251
|
+
comes back as `library_not_found`, listing what you can choose from.
|
|
252
|
+
|
|
253
|
+
Leave it out and the server uses **the supported library holding the most
|
|
254
|
+
tracks**. That matters: the local library Engine DJ creates on install is
|
|
255
|
+
scanned first and is often empty, so "the first one found" would hide the
|
|
256
|
+
drive you actually work from.
|
|
257
|
+
|
|
258
|
+
Each library gets its own index and its own connection, opened the first time
|
|
259
|
+
you ask that library something. Comparing two libraries against each other —
|
|
260
|
+
*"what is on this drive but not that one?"* — is **not** something this server
|
|
261
|
+
does.
|
|
59
262
|
|
|
60
263
|
## Safety
|
|
61
264
|
|
|
62
265
|
Your library is opened **read-only at the operating-system level**, not by
|
|
63
|
-
convention
|
|
64
|
-
refused by SQLite itself, and no file is ever
|
|
65
|
-
Library` folder. The
|
|
266
|
+
convention and not by a `PRAGMA` a query could turn back off. Writes are
|
|
267
|
+
refused by SQLite itself, and without `--allow-writes` no file is ever
|
|
268
|
+
created inside your `Engine Library` folder. The search index lives in
|
|
269
|
+
`~/.engine-dj-mcp/`.
|
|
270
|
+
|
|
271
|
+
### Writing
|
|
66
272
|
|
|
67
|
-
`
|
|
273
|
+
Without `--allow-writes` the server has no tool that can write, and the
|
|
274
|
+
paragraph above holds exactly as written: SQLite itself refuses.
|
|
275
|
+
|
|
276
|
+
With the flag, one tool appears — `create_playlist`. It adds a new playlist
|
|
277
|
+
and nothing else: no existing playlist is renamed, reordered, emptied or
|
|
278
|
+
deleted, and no track, cue or beatgrid is touched. The single change to an
|
|
279
|
+
existing row is the previous last playlist's link, made by Engine's own
|
|
280
|
+
trigger.
|
|
281
|
+
|
|
282
|
+
Before the first write of a session the database is snapshotted to
|
|
283
|
+
`~/.engine-dj-mcp/backups/`, and every write of that session returns its
|
|
284
|
+
path. Ten snapshots are kept per library — per library *file*, so a library
|
|
285
|
+
and its clone on another drive do not share the ten.
|
|
286
|
+
|
|
287
|
+
One file *is* created inside your `Engine Library` folder while a write is in
|
|
288
|
+
progress: SQLite's rollback journal, `m.db-journal`, next to `m.db`. It is
|
|
289
|
+
removed when the transaction commits, and it is what makes the write
|
|
290
|
+
all-or-nothing. If the process is killed mid-transaction the journal is left
|
|
291
|
+
behind, and both this server and Engine DJ then treat the library as needing
|
|
292
|
+
recovery — this server reports `library_needs_recovery` and refuses to touch
|
|
293
|
+
the library, including for reads, until you have launched Engine DJ once so
|
|
294
|
+
it can roll the journal back. Nothing else is ever written in that folder,
|
|
295
|
+
and without `--allow-writes` not even this.
|
|
296
|
+
|
|
297
|
+
The write takes SQLite's own write lock for the length of one transaction and
|
|
298
|
+
does not wait for it: if something else — Engine DJ mid-save, a player — is
|
|
299
|
+
holding a conflicting lock at that moment, the write is refused with
|
|
300
|
+
`library_busy` and nothing is changed. Merely having Engine DJ *open* is not
|
|
301
|
+
usually a conflict, and the write normally succeeds with Engine running;
|
|
302
|
+
Engine will show the new playlist after it next re-reads the library.
|
|
303
|
+
|
|
304
|
+
### Restoring a snapshot
|
|
305
|
+
|
|
306
|
+
`backup_path` is not an undo. It is a copy of the **whole** `m.db` from
|
|
307
|
+
before the session's first write, so putting it back reverts the entire
|
|
308
|
+
library to that moment: every play count, import, cue, beatgrid and rating
|
|
309
|
+
Engine DJ has written since is discarded along with the playlist you wanted
|
|
310
|
+
gone. Reach for it only if the library itself is damaged — the case where
|
|
311
|
+
`create_playlist` comes back with `detail: "committed_unverified"`.
|
|
312
|
+
|
|
313
|
+
**To undo a playlist, delete it in Engine DJ.** Engine's own delete trigger
|
|
314
|
+
repairs the playlist chain and cascades the entries away, which is exactly
|
|
315
|
+
what removing it should do and is not something restoring a snapshot does
|
|
316
|
+
better.
|
|
317
|
+
|
|
318
|
+
`run_sql` accepts arbitrary SQL, but only the first statement is ever
|
|
68
319
|
executed, and `VACUUM`, `ATTACH` and `DETACH` are rejected outright, so a
|
|
69
320
|
chained or exfiltrating statement cannot slip past the read-only connection.
|
|
70
321
|
|
|
71
322
|
If Engine DJ was closed uncleanly and left an unrecovered journal, this
|
|
72
|
-
server will
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
323
|
+
server will not open the library to "fix" it, with or without
|
|
324
|
+
`--allow-writes` — rolling a journal forward is a repair on someone else's
|
|
325
|
+
file, and `create_playlist` refuses such a library outright rather than
|
|
326
|
+
letting SQLite do it on the way in. It reports `library_needs_recovery` and
|
|
327
|
+
asks you to launch Engine DJ once so it can recover its own library.
|
|
76
328
|
|
|
77
329
|
## Limitations
|
|
78
330
|
|
|
79
331
|
Read this before deciding what to trust.
|
|
80
332
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
**beatgrid**, and the **waveform summary**. These were derived from and
|
|
88
|
-
checked against a real Engine DJ 3.0.x library of 281 analysed tracks:
|
|
89
|
-
cue offsets land inside the track, the beatgrid's implied tempo matches
|
|
90
|
-
`bpmAnalyzed` on all 281, and the waveform's declared point spacing
|
|
91
|
-
multiplies back out to the track's sample count on all 281. A
|
|
92
|
-
`status: "ok"` here is a claim about the values, not just the parse.
|
|
93
|
-
- `layout: "unverified"` — **loops**. The blob's slot structure is known
|
|
94
|
-
(eight slots, uncompressed, little-endian) and an empty one decodes
|
|
95
|
-
correctly, but not one track in that library had a loop saved, so the
|
|
96
|
-
meaning of a *populated* loop slot is untested. Do not report loop
|
|
97
|
-
bounds to a user as fact.
|
|
98
|
-
|
|
99
|
-
A `layout` marker is a claim about the **bytes** — which offset holds
|
|
100
|
-
which field, and what the number there means. It is not a claim about
|
|
101
|
-
every name this server puts on them. Three labels are inferred rather than
|
|
102
|
-
measured, and the code says so where each is defined: which of a cue's
|
|
103
|
-
four colour bytes is which channel (they are reported as stored, as one
|
|
104
|
-
32-bit value, with no channel claim attached); that the second beatgrid is
|
|
105
|
-
the one Engine calls "adjusted" (that it is the one Engine *plays* is
|
|
106
|
-
measured — on seven tracks the other grid runs at exactly half the
|
|
107
|
-
analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
|
|
108
|
-
and that the waveform's three bytes per point are the low, mid and high
|
|
109
|
-
bands in that order. None of these affects a value you get back.
|
|
110
|
-
|
|
111
|
-
Everything else the server reports — titles, artists, tempo, key, ratings,
|
|
112
|
-
play history, file paths — is read straight from the database and carries
|
|
113
|
-
no such caveat.
|
|
114
|
-
- **`has_cues` and `no_cues` mean "a hot cue is set", and cost a little to
|
|
115
|
-
say so.** Engine writes a `quickCues` blob to every analysed track whether
|
|
116
|
-
or not any pad is used, so the cheap SQL test (`quickCues` present and
|
|
117
|
-
non-empty) answers a question about *analysis*: in the 281-blob reference
|
|
118
|
-
library all 281 would count as having cues, while two tracks (three rows,
|
|
119
|
-
one track being exported to a second library) actually do. The blob is
|
|
120
|
-
therefore decoded when the search index is built, and
|
|
121
|
-
`search_tracks(flags: { has_cues: true })` and `audit_library`'s `no_cues`
|
|
122
|
-
both read the decoded answer. That costs about 100 ms of extra rebuild
|
|
123
|
-
time at 50,000 tracks, and a rebuild only runs when your library changes.
|
|
124
|
-
|
|
125
|
-
The track's **main cue** does not count towards it: Engine sets that as a
|
|
126
|
-
playback start marker rather than the DJ placing it (it is set on 159 of
|
|
127
|
-
the 281 blobs, including every track the library records as played), so
|
|
128
|
-
counting it would answer a third question again. `has_beatgrid` and
|
|
129
|
-
`no_beatgrid` do still test for the blob — a `beatData` blob has no
|
|
130
|
-
"written but empty" state, and on all 281 real blobs presence and a usable
|
|
131
|
-
grid have never disagreed.
|
|
132
|
-
- **It never writes to your library.** Not to add a cue, not to fix a tag,
|
|
133
|
-
not even to recover a journal Engine DJ left behind. The connection is
|
|
134
|
-
read-only at the kernel level, so a write is refused by SQLite itself
|
|
135
|
-
rather than by a rule this code could get wrong.
|
|
136
|
-
- **It does not read play history.** `Track.timeLastPlayed` is used to answer
|
|
137
|
-
"what have I not played in six months?", but the separate Engine history
|
|
138
|
-
database — individual sessions, decks, what followed what — is not opened
|
|
139
|
-
at all.
|
|
140
|
-
- **It does not build set lists**, reorder playlists, or suggest transitions.
|
|
141
|
-
It answers questions about the collection; the mixing is yours.
|
|
142
|
-
- **Schema 3.0.0 through 3.0.2 only** (Engine DJ 4.5 and 5.x). Older and
|
|
143
|
-
newer libraries are listed with their version and reported as unsupported
|
|
144
|
-
rather than read on a guess.
|
|
145
|
-
|
|
146
|
-
## Requirements
|
|
147
|
-
|
|
148
|
-
- Node.js 22 or newer
|
|
149
|
-
- Engine DJ with library schema 3.0.0–3.0.2 (Engine DJ 4.5 and 5.x)
|
|
333
|
+
**The `PerformanceData` layouts are reverse-engineered**, so every decoded
|
|
334
|
+
field says which kind it is. All four are marked `layout: "verified"` —
|
|
335
|
+
derived from and checked against a real Engine DJ 3.0.x library of 281
|
|
336
|
+
analysed tracks, where cue offsets land inside the track, the beatgrid's
|
|
337
|
+
implied tempo matches `bpmAnalyzed` on all 281, and the waveform's declared
|
|
338
|
+
point spacing multiplies back out to the track's sample count on all 281.
|
|
150
339
|
|
|
151
|
-
|
|
340
|
+
Loops were the last to earn it. The slot grid was pinned down by 2248
|
|
341
|
+
sentinels, but no library available had a loop saved in it, so a *populated*
|
|
342
|
+
slot stayed untested and loops carried `layout: "unverified"` through several
|
|
343
|
+
releases. One deliberately saved loop settled it: its slot spans 1.678321678 s
|
|
344
|
+
on a track Engine analysed at 143 BPM, which is four beats to within 4e-15 s.
|
|
345
|
+
Only the right field order, unit and endianness land on a whole beat count.
|
|
152
346
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
347
|
+
A `layout` marker is a claim about the bytes, not about every name put on
|
|
348
|
+
them. Four labels are inferred rather than measured, and the code says so
|
|
349
|
+
where each is defined: which of a cue's four colour bytes is which channel
|
|
350
|
+
(they are returned as stored, one 32-bit value, with no channel claim); that
|
|
351
|
+
the second beatgrid is the one Engine calls "adjusted" (that it is the one
|
|
352
|
+
Engine *plays* is measured — on seven tracks the other runs at exactly half
|
|
353
|
+
the analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
|
|
354
|
+
and that the waveform's three bytes per point are low, mid and high in that
|
|
355
|
+
order. None affects a value you get back.
|
|
156
356
|
|
|
157
|
-
|
|
357
|
+
Everything else — titles, artists, tempo, key, ratings, play history, file
|
|
358
|
+
paths — is read straight from the database and carries no such caveat.
|
|
158
359
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
360
|
+
**`has_cues` and `no_cues` mean "a hot cue is set".** Engine writes a
|
|
361
|
+
`quickCues` blob to every analysed track whether or not a pad is used, so the
|
|
362
|
+
cheap SQL test would answer a question about *analysis* instead: in the
|
|
363
|
+
reference library all 281 blobs would count as having cues, while two tracks
|
|
364
|
+
actually do. The blob is therefore decoded while the index is built. That
|
|
365
|
+
costs roughly 100 ms extra at 50,000 tracks, and only when your library
|
|
366
|
+
changes. The track's **main cue** does not count towards it — Engine sets
|
|
367
|
+
that as a playback marker rather than the DJ placing it. `has_beatgrid` does
|
|
368
|
+
still test for the blob: `beatData` has no "written but empty" state.
|
|
369
|
+
|
|
370
|
+
**It writes nothing but playlists, and only when you ask for it.** Without
|
|
371
|
+
`--allow-writes` the library is opened read-only at the OS level and there is
|
|
372
|
+
no tool that could write. With the flag, `create_playlist` adds playlists —
|
|
373
|
+
and that is the whole list. Not a cue, not a tag, not a rating, and not even
|
|
374
|
+
the recovery of a journal Engine DJ left behind.
|
|
375
|
+
|
|
376
|
+
**It does not read play history.** `Track.timeLastPlayed` answers "what have I
|
|
377
|
+
not played in six months?", but the separate Engine history database —
|
|
378
|
+
sessions, decks, what followed what — is not opened at all.
|
|
379
|
+
|
|
380
|
+
**Smartlists are not reported.** Engine's rule-based lists live in a separate
|
|
381
|
+
`Smartlist` table with its own ordering and a JSON rule column, and nothing
|
|
382
|
+
here reads it. `get_playlists` reports ordinary playlists and folders only, so
|
|
383
|
+
a smartlist you can see in Engine will not appear.
|
|
384
|
+
|
|
385
|
+
**An empty folder reads as an empty playlist.** Engine's schema has no folder
|
|
386
|
+
flag — a folder is simply a playlist that other playlists sit under — so
|
|
387
|
+
`is_folder` means "has child lists". A folder you have emptied is
|
|
388
|
+
indistinguishable from a playlist with no tracks.
|
|
389
|
+
|
|
390
|
+
**Playlists can be created, not edited.** With `--allow-writes` a new
|
|
391
|
+
playlist can be added; there is no reordering, renaming, deleting, or adding
|
|
392
|
+
a track to a playlist that already exists, and no set lists or suggested
|
|
393
|
+
transitions. It answers questions about the collection and writes down the
|
|
394
|
+
answer if you ask; the mixing is yours.
|
|
395
|
+
|
|
396
|
+
**Schema 3.0.0 through 3.0.2 only.** Older and newer libraries are listed with
|
|
397
|
+
their version and reported as unsupported rather than read on a guess.
|
|
166
398
|
|
|
167
399
|
## Licence
|
|
168
400
|
|
package/dist/blobs/index.d.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* sample count. `status: "ok"` on a verified field is a claim about the
|
|
9
9
|
* values, not merely about the parse.
|
|
10
10
|
*
|
|
11
|
-
* `"unverified"`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `"unverified"` means the bytes parsed without contradicting the layout, and
|
|
12
|
+
* nothing more. No field carries it today: `loops` was the last one, and it
|
|
13
|
+
* held the marker only for as long as no library available had a loop saved in
|
|
14
|
+
* it. One saved loop supplied the missing prediction — see `decodeLoops` — and
|
|
15
|
+
* the marker is kept here because it is what a future field decoded from an
|
|
16
|
+
* unpopulated example would deserve.
|
|
15
17
|
*
|
|
16
18
|
* The marker is about the *bytes*: which offset holds which field, and what
|
|
17
19
|
* the numbers there mean. It is not a claim about every English word this
|
|
@@ -215,19 +217,23 @@ export declare function hasCueSet(buf: Buffer | null): boolean;
|
|
|
215
217
|
* (23 bytes with an empty label)
|
|
216
218
|
*
|
|
217
219
|
* Evidence for the framing and the slot grid: 8 + 8×23 = 192, the size of
|
|
218
|
-
*
|
|
220
|
+
* every unpopulated real blob, each of which parsed to exactly its last byte;
|
|
219
221
|
* the little-endian count reads as 8, and the little-endian -1.0 sentinel
|
|
220
222
|
* (`000000000000f0bf`) appears at the offsets this layout predicts in all
|
|
221
223
|
* 2248 slots. Reading the count big-endian gives 134217728, which is what
|
|
222
224
|
* made every real track decode as `unsupported` before.
|
|
223
225
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
226
|
+
* The sentinels pin the slot grid, but an empty slot cannot say which double
|
|
227
|
+
* is the start, what unit the doubles are in, or what the six trailing bytes
|
|
228
|
+
* mean — so this layout carried `unverified` for as long as no library had a
|
|
229
|
+
* saved loop in it. The `saved-loop` golden fixture is that loop, and it
|
|
230
|
+
* settles all three at once. Its slot reads label "Loop 1", start 2129416.08
|
|
231
|
+
* and end 2203430.06, which at the 44100 Hz `beatData` declares for the track
|
|
232
|
+
* is 1.678321678 s — exactly four beats at the 143 BPM Engine analysed, to
|
|
233
|
+
* 3.6e-15 s. Nothing but the right field order, the right unit and the right
|
|
234
|
+
* endianness lands on a whole number of beats, so the layout is verified.
|
|
235
|
+
* The trailing bytes read `01 01` (start set, end set) and four colour bytes,
|
|
236
|
+
* which is the same tail `quickCues` slots carry.
|
|
231
237
|
*/
|
|
232
238
|
export declare function decodeLoops(buf: Buffer | null, sampleRate?: number | null): LoopsResult;
|
|
233
239
|
/**
|