engine-dj-mcp 0.9.2 → 0.10.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 +259 -133
- package/dist/blobs/index.d.ts +18 -12
- package/dist/blobs/index.js +24 -16
- package/dist/playlists.d.ts +240 -0
- package/dist/playlists.js +447 -0
- package/dist/server.js +93 -11
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,168 +1,294 @@
|
|
|
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 — see [Safety](#safety).
|
|
16
|
+
|
|
17
|
+
## What you can ask
|
|
18
|
+
|
|
19
|
+
Once connected, these are ordinary questions in chat:
|
|
20
|
+
|
|
21
|
+
- *"Something dark around 124 in a minor key I haven't played in six months."*
|
|
22
|
+
- *"Which tracks still have no hot cue set?"*
|
|
23
|
+
- *"Find me anything harmonically compatible with 8A between 138 and 142."*
|
|
24
|
+
- *"What's in my ACID Beach playlist, in order?"*
|
|
25
|
+
- *"Anything around 128 in ACID Beach?"*
|
|
26
|
+
- *"What's broken in my collection — missing files, duplicates, bad tempos?"*
|
|
27
|
+
- *"Where are the cue points on this track, and what tempo did Engine analyse?"*
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx engine-dj-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Claude Desktop — add to your configuration:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp"] }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Requirements:** Node.js 22.13 or newer (for the unflagged `node:sqlite`;
|
|
46
|
+
there are no native dependencies), and an Engine DJ library at schema 3.0.0
|
|
47
|
+
through 3.0.2 — Engine DJ 4.5 and 5.x.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
Nine tools, all read-only. Every tool that reads library data also accepts
|
|
52
|
+
an optional `library` argument — see [Choosing a library](#choosing-a-library).
|
|
53
|
+
|
|
54
|
+
### `search_tracks`
|
|
55
|
+
|
|
56
|
+
The main one. Full-text search with diacritics folded, plus filters for
|
|
57
|
+
tempo, key, rating, when a track was added and when it was last played.
|
|
58
|
+
|
|
59
|
+
| Argument | What it does |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `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. |
|
|
62
|
+
| `bpm` | `{ min, max }` or `{ around, tolerance_pct }`. Resolved tempo, so an analysed BPM wins over the tag. |
|
|
63
|
+
| `key` | `{ camelot: [...] }` for exact keys, `{ compatible_with: "8A" }` for harmonic neighbours, `{ mode: "minor" }` for a whole side of the wheel. |
|
|
64
|
+
| `rating` | `{ min, max }`, 0–5. |
|
|
65
|
+
| `played` | `{ never: true }`, or `{ before, after }` taking an ISO date or a relative form like `-6 months`. |
|
|
66
|
+
| `added` | `{ before, after }`, same date forms. |
|
|
67
|
+
| `flags` | `analyzed`, `available`, `has_cues`, `has_beatgrid`. `has_cues` means a hot cue is genuinely set — see [Limitations](#limitations). |
|
|
68
|
+
| `playlist` | `{ id }` or `{ name }` — search inside one playlist. Results still come back by relevance or id; `get_playlist_tracks` is what preserves playlist order. |
|
|
69
|
+
| `fields` | Which columns to return. Defaults to `id, artist, title, bpm, camelot, rating`. |
|
|
70
|
+
| `limit`, `cursor` | Page size (default 25, max 200) and an opaque cursor for the next page. |
|
|
71
|
+
| `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". |
|
|
72
|
+
|
|
73
|
+
### `get_tracks`
|
|
74
|
+
|
|
75
|
+
Full metadata for specific track ids, returned in the order you asked for.
|
|
76
|
+
Unknown ids are omitted rather than failing the call.
|
|
77
|
+
|
|
78
|
+
`ids` (required), `fields`, `redact_paths`.
|
|
79
|
+
|
|
80
|
+
### `get_playlists`
|
|
81
|
+
|
|
82
|
+
Your playlist tree, in the order Engine DJ shows it — folders included.
|
|
83
|
+
|
|
84
|
+
The list is flat and in the order you would read down the sidebar with every
|
|
85
|
+
folder expanded: `depth` and `path` carry the nesting, `parent_id` names the
|
|
86
|
+
folder a list sits in.
|
|
87
|
+
|
|
88
|
+
| Field | Meaning |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `name`, `id`, `path` | `path` is the full `Folder/Sub/Name`, and is unique — a bare `name` need not be. |
|
|
91
|
+
| `depth`, `parent_id` | The nesting. `parent_id` is `null` at the top level. |
|
|
92
|
+
| `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. |
|
|
93
|
+
| `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. |
|
|
94
|
+
| `track_count` | Entries in that list alone, never rolled up from its children — the number Engine shows beside it. |
|
|
95
|
+
| `missing_count` | How many of those entries name a track this library does not have. |
|
|
96
|
+
|
|
97
|
+
`limit` (default 200, max 1000). `warnings` appears if a playlist's link
|
|
98
|
+
chain is damaged; nothing is ever dropped from the list because of one.
|
|
99
|
+
|
|
100
|
+
### `get_playlist_tracks`
|
|
101
|
+
|
|
102
|
+
The tracks of one playlist, **in playlist order**.
|
|
103
|
+
|
|
104
|
+
Name it with `playlist_id` or with `playlist_name` — exactly one of the two.
|
|
105
|
+
Playlist names are unique only within a folder, so a name matching more than
|
|
106
|
+
one is refused with every candidate's id and full path rather than guessed
|
|
107
|
+
at; pass the `path` from `get_playlists` to say which you meant.
|
|
108
|
+
|
|
109
|
+
Every row carries `position`, its 1-based place in the playlist. Same
|
|
110
|
+
`fields`, `limit` and `cursor` conventions as `search_tracks`.
|
|
111
|
+
|
|
112
|
+
An entry whose track is not in this library keeps its slot and comes back as
|
|
113
|
+
`{ position, entry_id, track_id, missing: true }`, with `missing_count`
|
|
114
|
+
alongside `entry_count`. That is ordinary rather than corruption — playlist
|
|
115
|
+
entries outlive their tracks and travel between drives — and they are kept in
|
|
116
|
+
place so the number of rows still matches the playlist's own length. On one
|
|
117
|
+
reference library a 43-entry playlist holds exactly one track that library
|
|
118
|
+
can actually play.
|
|
119
|
+
|
|
120
|
+
### `get_track_performance`
|
|
121
|
+
|
|
122
|
+
Decodes the binary `PerformanceData` Engine stores per track: hot cues, the
|
|
123
|
+
main cue, saved loops, the beatgrid, and a coarse waveform profile.
|
|
124
|
+
|
|
125
|
+
Every field carries its own decode status **and** its own `layout` marker.
|
|
126
|
+
`layout: "verified"` means the byte layout was confirmed against a real
|
|
127
|
+
library, so `status: "ok"` is a claim about the values. `layout: "unverified"`
|
|
128
|
+
would mean only that the bytes parsed; no field returns it today.
|
|
129
|
+
|
|
130
|
+
Positions are sample offsets; cue and loop items also carry seconds.
|
|
131
|
+
`items: []` with `slots: 8` means an analysed track with no cues set.
|
|
132
|
+
|
|
133
|
+
`id` (required).
|
|
134
|
+
|
|
135
|
+
### `audit_library`
|
|
136
|
+
|
|
137
|
+
Ten collection health checks. Returns a count and a small sample of ids per
|
|
138
|
+
check, never the full result set — a library with thousands of unanalysed
|
|
139
|
+
tracks should not fill an assistant's context.
|
|
140
|
+
|
|
141
|
+
| Check | Finds |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `missing_files` | Tracks whose file is gone from disk |
|
|
144
|
+
| `unavailable` | Tracks Engine has marked unavailable |
|
|
145
|
+
| `unanalyzed` | Tracks Engine has not analysed |
|
|
146
|
+
| `no_cues` | Tracks with no hot cue set |
|
|
147
|
+
| `no_beatgrid` | Tracks with no beatgrid data |
|
|
148
|
+
| `missing_key` | Tracks with no key detected |
|
|
149
|
+
| `suspicious_bpm` | Analysed and tagged tempo disagree, or tempo is outside 60–200 |
|
|
150
|
+
| `duplicates` | Same artist and title, or same size and length |
|
|
151
|
+
| `empty_metadata` | No artist or no title |
|
|
152
|
+
| `orphan_entries` | Playlist entries pointing at tracks not in this library — `get_playlist_tracks` shows where each one sits |
|
|
153
|
+
|
|
154
|
+
`checks` — omit it to run all ten.
|
|
155
|
+
|
|
156
|
+
### `run_sql`
|
|
157
|
+
|
|
158
|
+
An escape hatch for questions the tools above do not cover. Read-only is
|
|
159
|
+
enforced by the kernel, not by this tool. Results are bounded whatever the
|
|
160
|
+
query says.
|
|
161
|
+
|
|
162
|
+
Prefer `side.track_derived.camelot` and `side.track_derived.tempo` in `WHERE`
|
|
163
|
+
clauses over the `camelot()` and `tempo()` SQL functions — the functions run
|
|
164
|
+
per row and defeat the indexes.
|
|
165
|
+
|
|
166
|
+
`sql` (required), `params`, `limit`.
|
|
167
|
+
|
|
168
|
+
### `list_libraries`
|
|
169
|
+
|
|
170
|
+
Every library found, including ones whose schema is unsupported — listed
|
|
171
|
+
with their version, so you can tell a broken server from a missing library.
|
|
172
|
+
Re-scans on every call, so a drive plugged in after the server started shows
|
|
173
|
+
up without a restart. A library that is temporarily unreadable — Engine DJ
|
|
174
|
+
writing to it, say — stays listed with `status: "unreadable"` rather than
|
|
175
|
+
vanishing.
|
|
176
|
+
|
|
177
|
+
No arguments.
|
|
178
|
+
|
|
179
|
+
### `refresh_index`
|
|
180
|
+
|
|
181
|
+
Rebuilds the search index if the library changed. Normally unnecessary; the
|
|
182
|
+
server checks staleness itself before answering.
|
|
183
|
+
|
|
184
|
+
## Resources
|
|
185
|
+
|
|
186
|
+
- **`engine://schema`** — the field semantics an assistant needs before
|
|
187
|
+
writing SQL: how Engine encodes musical key, why tempo is
|
|
188
|
+
`COALESCE(bpmAnalyzed, bpm)`, that `Track.path` is relative, where playlist
|
|
189
|
+
order really lives, and which helper columns are indexed.
|
|
190
|
+
- **`engine://libraries`** — what was discovered at startup and whether each
|
|
191
|
+
library's schema is supported. A snapshot; `list_libraries` is the live view.
|
|
34
192
|
|
|
35
193
|
## Choosing a library
|
|
36
194
|
|
|
37
195
|
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.
|
|
196
|
+
export to, so more than one is usually connected. `list_libraries` reports
|
|
197
|
+
each with a `uuid` and a `path`, and every tool that reads library data takes
|
|
198
|
+
an optional `library` argument. Pass either form exactly as printed — the
|
|
199
|
+
`~/…` path is accepted alongside the absolute one. A value matching neither
|
|
200
|
+
comes back as `library_not_found`, listing what you can choose from.
|
|
201
|
+
|
|
202
|
+
Leave it out and the server uses **the supported library holding the most
|
|
203
|
+
tracks**. That matters: the local library Engine DJ creates on install is
|
|
204
|
+
scanned first and is often empty, so "the first one found" would hide the
|
|
205
|
+
drive you actually work from.
|
|
206
|
+
|
|
207
|
+
Each library gets its own index and its own connection, opened the first time
|
|
208
|
+
you ask that library something. Comparing two libraries against each other —
|
|
209
|
+
*"what is on this drive but not that one?"* — is **not** something this server
|
|
210
|
+
does.
|
|
59
211
|
|
|
60
212
|
## Safety
|
|
61
213
|
|
|
62
214
|
Your library is opened **read-only at the operating-system level**, not by
|
|
63
|
-
convention
|
|
215
|
+
convention and not by a `PRAGMA` a query could turn back off. Writes are
|
|
64
216
|
refused by SQLite itself, and no file is ever created inside your `Engine
|
|
65
|
-
Library` folder. The
|
|
217
|
+
Library` folder. The search index lives in `~/.engine-dj-mcp/`.
|
|
66
218
|
|
|
67
|
-
`run_sql` accepts arbitrary SQL, but only
|
|
219
|
+
`run_sql` accepts arbitrary SQL, but only the first statement is ever
|
|
68
220
|
executed, and `VACUUM`, `ATTACH` and `DETACH` are rejected outright, so a
|
|
69
221
|
chained or exfiltrating statement cannot slip past the read-only connection.
|
|
70
222
|
|
|
71
223
|
If Engine DJ was closed uncleanly and left an unrecovered journal, this
|
|
72
|
-
server will
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
can recover the library itself.
|
|
224
|
+
server will not open the library writably to "fix" it — that would break the
|
|
225
|
+
one guarantee this project makes. It reports `library_needs_recovery` and
|
|
226
|
+
asks you to launch Engine DJ once so it can recover its own library.
|
|
76
227
|
|
|
77
228
|
## Limitations
|
|
78
229
|
|
|
79
230
|
Read this before deciding what to trust.
|
|
80
231
|
|
|
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)
|
|
232
|
+
**The `PerformanceData` layouts are reverse-engineered**, so every decoded
|
|
233
|
+
field says which kind it is. All four are marked `layout: "verified"` —
|
|
234
|
+
derived from and checked against a real Engine DJ 3.0.x library of 281
|
|
235
|
+
analysed tracks, where cue offsets land inside the track, the beatgrid's
|
|
236
|
+
implied tempo matches `bpmAnalyzed` on all 281, and the waveform's declared
|
|
237
|
+
point spacing multiplies back out to the track's sample count on all 281.
|
|
150
238
|
|
|
151
|
-
|
|
239
|
+
Loops were the last to earn it. The slot grid was pinned down by 2248
|
|
240
|
+
sentinels, but no library available had a loop saved in it, so a *populated*
|
|
241
|
+
slot stayed untested and loops carried `layout: "unverified"` through several
|
|
242
|
+
releases. One deliberately saved loop settled it: its slot spans 1.678321678 s
|
|
243
|
+
on a track Engine analysed at 143 BPM, which is four beats to within 4e-15 s.
|
|
244
|
+
Only the right field order, unit and endianness land on a whole beat count.
|
|
152
245
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
246
|
+
A `layout` marker is a claim about the bytes, not about every name put on
|
|
247
|
+
them. Four labels are inferred rather than measured, and the code says so
|
|
248
|
+
where each is defined: which of a cue's four colour bytes is which channel
|
|
249
|
+
(they are returned as stored, one 32-bit value, with no channel claim); that
|
|
250
|
+
the second beatgrid is the one Engine calls "adjusted" (that it is the one
|
|
251
|
+
Engine *plays* is measured — on seven tracks the other runs at exactly half
|
|
252
|
+
the analysed tempo); that `main_cue.is_adjusted` is what its flag byte means;
|
|
253
|
+
and that the waveform's three bytes per point are low, mid and high in that
|
|
254
|
+
order. None affects a value you get back.
|
|
156
255
|
|
|
157
|
-
|
|
256
|
+
Everything else — titles, artists, tempo, key, ratings, play history, file
|
|
257
|
+
paths — is read straight from the database and carries no such caveat.
|
|
158
258
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
259
|
+
**`has_cues` and `no_cues` mean "a hot cue is set".** Engine writes a
|
|
260
|
+
`quickCues` blob to every analysed track whether or not a pad is used, so the
|
|
261
|
+
cheap SQL test would answer a question about *analysis* instead: in the
|
|
262
|
+
reference library all 281 blobs would count as having cues, while two tracks
|
|
263
|
+
actually do. The blob is therefore decoded while the index is built. That
|
|
264
|
+
costs roughly 100 ms extra at 50,000 tracks, and only when your library
|
|
265
|
+
changes. The track's **main cue** does not count towards it — Engine sets
|
|
266
|
+
that as a playback marker rather than the DJ placing it. `has_beatgrid` does
|
|
267
|
+
still test for the blob: `beatData` has no "written but empty" state.
|
|
268
|
+
|
|
269
|
+
**It never writes to your library.** Not to add a cue, not to fix a tag, not
|
|
270
|
+
even to recover a journal Engine DJ left behind.
|
|
271
|
+
|
|
272
|
+
**It does not read play history.** `Track.timeLastPlayed` answers "what have I
|
|
273
|
+
not played in six months?", but the separate Engine history database —
|
|
274
|
+
sessions, decks, what followed what — is not opened at all.
|
|
275
|
+
|
|
276
|
+
**Smartlists are not reported.** Engine's rule-based lists live in a separate
|
|
277
|
+
`Smartlist` table with its own ordering and a JSON rule column, and nothing
|
|
278
|
+
here reads it. `get_playlists` reports ordinary playlists and folders only, so
|
|
279
|
+
a smartlist you can see in Engine will not appear.
|
|
280
|
+
|
|
281
|
+
**An empty folder reads as an empty playlist.** Engine's schema has no folder
|
|
282
|
+
flag — a folder is simply a playlist that other playlists sit under — so
|
|
283
|
+
`is_folder` means "has child lists". A folder you have emptied is
|
|
284
|
+
indistinguishable from a playlist with no tracks.
|
|
285
|
+
|
|
286
|
+
**It reads playlists; it does not write them.** No creating, reordering,
|
|
287
|
+
renaming or adding to a playlist, and no set lists or suggested transitions.
|
|
288
|
+
It answers questions about the collection; the mixing is yours.
|
|
289
|
+
|
|
290
|
+
**Schema 3.0.0 through 3.0.2 only.** Older and newer libraries are listed with
|
|
291
|
+
their version and reported as unsupported rather than read on a guess.
|
|
166
292
|
|
|
167
293
|
## Licence
|
|
168
294
|
|
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
|
/**
|
package/dist/blobs/index.js
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
// The layouts below were derived from, and checked against, a real Engine DJ
|
|
7
7
|
// library: 257 analysed tracks on a USB export plus 24 in the local history
|
|
8
8
|
// database, 281 blobs of each kind. What each check proved is recorded next
|
|
9
|
-
// to the decoder it justifies. `loops`
|
|
10
|
-
//
|
|
9
|
+
// to the decoder it justifies. `loops` was the last field to be checked: its
|
|
10
|
+
// contents needed a library with a loop actually saved in one, which arrived
|
|
11
|
+
// later than the rest — see decodeLoops.
|
|
11
12
|
//
|
|
12
13
|
// Framing: `quickCues`, `beatData` and `overviewWaveFormData` are Qt
|
|
13
14
|
// qCompress frames (4-byte big-endian uncompressed length, then a raw zlib
|
|
14
15
|
// stream); the declared length matched the inflated length for all 281 of
|
|
15
|
-
// each. `loops` is NOT compressed and NOT framed —
|
|
16
|
+
// each. `loops` is NOT compressed and NOT framed — 192 raw bytes plus any
|
|
17
|
+
// label text its slots carry, and
|
|
16
18
|
// running it through the qCompress path reads its little-endian int64 count
|
|
17
19
|
// of 8 as a big-endian length of 134217728 and hands zlib bytes it rejects.
|
|
18
20
|
//
|
|
@@ -35,10 +37,12 @@ import { qUncompress, Reader, DecodeError } from "./qcompress.js";
|
|
|
35
37
|
* sample count. `status: "ok"` on a verified field is a claim about the
|
|
36
38
|
* values, not merely about the parse.
|
|
37
39
|
*
|
|
38
|
-
* `"unverified"`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* `"unverified"` means the bytes parsed without contradicting the layout, and
|
|
41
|
+
* nothing more. No field carries it today: `loops` was the last one, and it
|
|
42
|
+
* held the marker only for as long as no library available had a loop saved in
|
|
43
|
+
* it. One saved loop supplied the missing prediction — see `decodeLoops` — and
|
|
44
|
+
* the marker is kept here because it is what a future field decoded from an
|
|
45
|
+
* unpopulated example would deserve.
|
|
42
46
|
*
|
|
43
47
|
* The marker is about the *bytes*: which offset holds which field, and what
|
|
44
48
|
* the numbers there mean. It is not a claim about every English word this
|
|
@@ -252,23 +256,27 @@ export function hasCueSet(buf) {
|
|
|
252
256
|
* (23 bytes with an empty label)
|
|
253
257
|
*
|
|
254
258
|
* Evidence for the framing and the slot grid: 8 + 8×23 = 192, the size of
|
|
255
|
-
*
|
|
259
|
+
* every unpopulated real blob, each of which parsed to exactly its last byte;
|
|
256
260
|
* the little-endian count reads as 8, and the little-endian -1.0 sentinel
|
|
257
261
|
* (`000000000000f0bf`) appears at the offsets this layout predicts in all
|
|
258
262
|
* 2248 slots. Reading the count big-endian gives 134217728, which is what
|
|
259
263
|
* made every real track decode as `unsupported` before.
|
|
260
264
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
265
|
+
* The sentinels pin the slot grid, but an empty slot cannot say which double
|
|
266
|
+
* is the start, what unit the doubles are in, or what the six trailing bytes
|
|
267
|
+
* mean — so this layout carried `unverified` for as long as no library had a
|
|
268
|
+
* saved loop in it. The `saved-loop` golden fixture is that loop, and it
|
|
269
|
+
* settles all three at once. Its slot reads label "Loop 1", start 2129416.08
|
|
270
|
+
* and end 2203430.06, which at the 44100 Hz `beatData` declares for the track
|
|
271
|
+
* is 1.678321678 s — exactly four beats at the 143 BPM Engine analysed, to
|
|
272
|
+
* 3.6e-15 s. Nothing but the right field order, the right unit and the right
|
|
273
|
+
* endianness lands on a whole number of beats, so the layout is verified.
|
|
274
|
+
* The trailing bytes read `01 01` (start set, end set) and four colour bytes,
|
|
275
|
+
* which is the same tail `quickCues` slots carry.
|
|
268
276
|
*/
|
|
269
277
|
export function decodeLoops(buf, sampleRate) {
|
|
270
278
|
const rate = usableRate(sampleRate);
|
|
271
|
-
return guard(buf,
|
|
279
|
+
return guard(buf, LAYOUT_VERIFIED, "raw", (r) => {
|
|
272
280
|
const slots = boundedCount(r, true, MAX_ITEMS, `loop slot count`);
|
|
273
281
|
const items = [];
|
|
274
282
|
for (let i = 0; i < slots; i++) {
|