engine-dj-mcp 0.15.0 → 0.17.1
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 +120 -36
- package/dist/errors.d.ts +15 -1
- package/dist/errors.js +14 -5
- package/dist/library-select.d.ts +35 -17
- package/dist/library-select.js +79 -34
- package/dist/server.js +136 -38
- package/dist/store/track-metadata-plan.d.ts +82 -0
- package/dist/store/track-metadata-plan.js +236 -0
- package/dist/store/track-metadata.d.ts +22 -0
- package/dist/store/track-metadata.js +143 -0
- package/dist/store/write.d.ts +83 -0
- package/dist/store/write.js +35 -7
- package/dist/tools/search.js +11 -2
- package/dist/tools/write-track-metadata.d.ts +26 -0
- package/dist/tools/write-track-metadata.js +42 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# engine-dj-mcp
|
|
2
2
|
|
|
3
|
-
[](https://github.com/Venut-Technologies/engine-dj-mcp/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/engine-dj-mcp)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
@@ -54,7 +54,7 @@ the configuration you are reading:
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
|
56
56
|
"mcpServers": {
|
|
57
|
-
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp@0.
|
|
57
|
+
"engine-dj": { "command": "npx", "args": ["-y", "engine-dj-mcp@0.17.1", "--allow-writes"] }
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
```
|
|
@@ -87,7 +87,7 @@ tempo, key, rating, when a track was added and when it was last played.
|
|
|
87
87
|
| `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. |
|
|
88
88
|
| `bpm` | `{ min, max }` or `{ around, tolerance_pct }`. Resolved tempo, so an analysed BPM wins over the tag. |
|
|
89
89
|
| `key` | `{ camelot: [...] }` for exact keys, `{ compatible_with: "8A" }` for harmonic neighbours, `{ mode: "minor" }` for a whole side of the wheel. |
|
|
90
|
-
| `rating` | `{ min, max }`, 0–5. |
|
|
90
|
+
| `rating` | `{ min, max }`, in stars, 0–5. Engine stores 0, 20, 40, 60, 80, 100; the filter converts, so `{ min: 4 }` means four stars and up. The `rating` field hands back the stored number, and `rating_stars` the same thing in stars. |
|
|
91
91
|
| `played` | `{ never: true }`, or `{ before, after }` taking an ISO date or a relative form like `-6 months`. |
|
|
92
92
|
| `added` | `{ before, after }`, same date forms. |
|
|
93
93
|
| `flags` | `analyzed`, `available`, `has_cues`, `has_beatgrid`. `has_cues` means a hot cue is genuinely set — see [Limitations](#limitations). |
|
|
@@ -210,7 +210,7 @@ server checks staleness itself before answering.
|
|
|
210
210
|
|
|
211
211
|
### `create_playlist`
|
|
212
212
|
|
|
213
|
-
The first of the
|
|
213
|
+
The first of the five tools that write, none of which is registered at all
|
|
214
214
|
unless the server was started with `--allow-writes`.
|
|
215
215
|
|
|
216
216
|
Creates one new top-level playlist from track ids — `track_ids` sets both
|
|
@@ -337,6 +337,66 @@ Reordering to the order a playlist is already in is accepted and rewrites no
|
|
|
337
337
|
entry: it still stamps the playlist's `lastEditTime`, and still costs this
|
|
338
338
|
session's snapshot if nothing had been written yet.
|
|
339
339
|
|
|
340
|
+
### `update_track_metadata`
|
|
341
|
+
|
|
342
|
+
Changes genre, comment, label, year or rating on tracks — the values Engine
|
|
343
|
+
DJ shows in its columns. It writes to Engine's database, **not to the audio files' tags**. Engine itself writes a comment into the file when you edit it
|
|
344
|
+
there, but not a genre or a rating, so other software reading the tags will
|
|
345
|
+
not see these edits either way.
|
|
346
|
+
|
|
347
|
+
| Argument | |
|
|
348
|
+
| --- | --- |
|
|
349
|
+
| `updates` | Up to 200 entries, each `{ id, genre?, comment?, label?, year?, rating_stars? }`. Only the named fields change. `""` clears a text field; `year: 0` means unknown, as Engine stores it; `rating_stars` is 0–5. |
|
|
350
|
+
| `library` | Required when more than one library is connected — see [Choosing a library](#choosing-a-library). |
|
|
351
|
+
|
|
352
|
+
A track that already holds the requested values is not written, so repeating
|
|
353
|
+
a call changes nothing; it is counted in `unchanged`. The result carries
|
|
354
|
+
`updated`, `unchanged`, `changed` — which fields changed on which tracks —
|
|
355
|
+
`undo`, `undo_complete` (always `true`), `library`, and `backup_path` whenever
|
|
356
|
+
the write transaction ran — which can include `updated: 0`, if the tracks had
|
|
357
|
+
already changed to the requested values by the time the write lock was taken.
|
|
358
|
+
|
|
359
|
+
**Undo.** `undo` is one `update_track_metadata` call that restores the
|
|
360
|
+
previous values of exactly the fields that changed, and names the library.
|
|
361
|
+
It restores values, not `lastEditTime`: Engine's own trigger stamps every
|
|
362
|
+
edit, the undo included. Each entry carries `expect` set to what this call
|
|
363
|
+
wrote, so an undo replayed after someone edited the track again is refused as
|
|
364
|
+
`stale_value` instead of overwriting that edit. `rating_raw` and `expect`
|
|
365
|
+
exist for this; an ordinary edit needs neither.
|
|
366
|
+
|
|
367
|
+
Keep the undo from the first response. Repeating a call that already went
|
|
368
|
+
through finds nothing to change and returns an empty undo. For work spread
|
|
369
|
+
over several calls, replay the undos in reverse order.
|
|
370
|
+
|
|
371
|
+
Its own refusals: `unknown_track`; `track_not_editable` — a track whose origin
|
|
372
|
+
is empty (Engine's trigger rewrites an empty origin on any update, which would
|
|
373
|
+
detach it from playlist entries on other drives), or a field holding a value
|
|
374
|
+
this tool could not put back, such as a rating outside 0–255; `stale_value` —
|
|
375
|
+
the track changed after the values in `expect` were read (up to 20 mismatches
|
|
376
|
+
come back in a structured `mismatches` field, with the total count in the
|
|
377
|
+
prose message); and `invalid_argument`. On `stale_value`, tell the user which
|
|
378
|
+
tracks and fields changed — do not rebuild `expect` from a fresh read to force
|
|
379
|
+
the write without the user's consent, or it silently overwrites the edit the
|
|
380
|
+
DJ made since. Plus the ones
|
|
381
|
+
[every write tool shares](#refusals-every-write-tool-shares), except
|
|
382
|
+
`index_stale` and the query errors: this tool addresses tracks by id and never
|
|
383
|
+
touches the search index.
|
|
384
|
+
|
|
385
|
+
**Searching right after an edit.** Genre, comment and label are in the search
|
|
386
|
+
index, which is rebuilt on the next read. While Engine DJ holds the library
|
|
387
|
+
open it cannot be rebuilt, so a search can keep showing the old values, and
|
|
388
|
+
`refresh_index` cannot help until Engine lets go. The edit itself is in the
|
|
389
|
+
database.
|
|
390
|
+
|
|
391
|
+
**Smart playlists.** A smart playlist whose rules match on genre changes what
|
|
392
|
+
it contains when a genre is renamed, though none of its own rows were touched.
|
|
393
|
+
|
|
394
|
+
**Two connected libraries.** Do not assume a tag edit propagates the way a
|
|
395
|
+
playlist edit does (see [An undo covers one library](#an-undo-covers-one-library)):
|
|
396
|
+
measured once, a tag edit made on the USB library was not copied to the
|
|
397
|
+
computer's library on a fresh Engine DJ launch. The other direction has not
|
|
398
|
+
been measured for tags. Edited tracks are marked for sync (`isMetadataOfPackedTrackChanged`) the same way Engine DJ marks its own tag edits — measured 2026-09-15. That an explicit sync to a drive then carries the edit is what the flag appears to be for, but it has not been measured.
|
|
399
|
+
|
|
340
400
|
### Refusals every write tool shares
|
|
341
401
|
|
|
342
402
|
These come from what happens before the write itself — choosing the library,
|
|
@@ -347,7 +407,7 @@ own checks.
|
|
|
347
407
|
| --- | --- | --- |
|
|
348
408
|
| `invalid_argument` | The arguments do not make sense — both `playlist_id` and `playlist_name`, an empty list where one is required, or a `playlist_name` that matches several playlists (every candidate is listed). | yes |
|
|
349
409
|
| `library_not_found` | `library` names nothing connected — the refusal lists what is — or the library's header could not be read. | yes |
|
|
350
|
-
| `ambiguous_library` | No `library` given, and
|
|
410
|
+
| `ambiguous_library` | No `library` given, and more than one supported library is connected — or the uuid given is shared by copies on different drives. Lists them — see [Choosing a library](#choosing-a-library). | yes |
|
|
351
411
|
| `unsupported_schema` | The library's version is outside what this server supports. | yes |
|
|
352
412
|
| `library_needs_recovery` | Engine DJ left an unrecovered journal. Launch Engine once. | yes |
|
|
353
413
|
| `library_busy` | Something holds a conflicting lock right now. Retry. | yes |
|
|
@@ -387,20 +447,33 @@ tracks**. That matters: the local library Engine DJ creates on install is
|
|
|
387
447
|
scanned first and is often empty, so "the first one found" would hide the
|
|
388
448
|
drive you actually work from.
|
|
389
449
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
track
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
450
|
+
That rule is enough for a read, which changes nothing: with two libraries
|
|
451
|
+
connected a read picks one. Pass `library` when it matters which.
|
|
452
|
+
|
|
453
|
+
**A write refuses instead**, as soon as more than one supported library is
|
|
454
|
+
connected — whatever their track counts. `ambiguous_library` lists every
|
|
455
|
+
candidate with its track count, and `detail: "not_committed"`.
|
|
456
|
+
|
|
457
|
+
The count was never the right question. An earlier version refused only an
|
|
458
|
+
exact tie, reasoning that a USB drive and its copy tie precisely because one
|
|
459
|
+
is a copy of the other — measured 2026-09-01, both real libraries at 257. But
|
|
460
|
+
import one track on one side and the tie is gone, and the default quietly
|
|
461
|
+
takes the larger. A playlist written to the wrong drive is at least visible
|
|
462
|
+
there; a track's genre is not, and you are left believing the edit did not
|
|
463
|
+
work.
|
|
464
|
+
|
|
465
|
+
With a single library nothing changes: you never have to name it.
|
|
466
|
+
|
|
467
|
+
**Copies share a uuid.** Copy an `Engine Library` folder onto another drive —
|
|
468
|
+
a spare stick for a gig — and the copy keeps the original's uuid, so with both
|
|
469
|
+
connected one uuid names two libraries. A write naming that uuid is refused the
|
|
470
|
+
same way, with `ambiguous_library` listing both paths, rather than landing on
|
|
471
|
+
whichever drive was scanned first. Pass the path instead — it tells the copies
|
|
472
|
+
apart — and re-read from that path anything the write depends on, since a read
|
|
473
|
+
naming the uuid may have come from the other copy. Reads naming a shared uuid
|
|
474
|
+
are not refused: they answer from one of the copies. Before every write the
|
|
475
|
+
drives are scanned again, so a copy plugged in after the server started is
|
|
476
|
+
counted — as long as its library can be read.
|
|
404
477
|
|
|
405
478
|
The refusal tells the assistant to **ask you** rather than choose. Otherwise
|
|
406
479
|
"pass `library`, here are the two" is an invitation to take the first one,
|
|
@@ -425,20 +498,24 @@ created inside your `Engine Library` folder. The search index lives in
|
|
|
425
498
|
Without `--allow-writes` the server has no tool that can write, and the
|
|
426
499
|
paragraph above holds exactly as written: SQLite itself refuses.
|
|
427
500
|
|
|
428
|
-
With the flag,
|
|
501
|
+
With the flag, five tools appear. `create_playlist` adds a new playlist and
|
|
429
502
|
nothing else. `add_tracks_to_playlist`, `remove_tracks_from_playlist` and
|
|
430
503
|
`reorder_playlist` go further: with the flag, an **existing** playlist can
|
|
431
504
|
now be changed, not only created — its tracks added to, removed from, or put
|
|
432
|
-
in a different order. What
|
|
433
|
-
entries, plus exactly two rows elsewhere: that playlist's own
|
|
434
|
-
`lastEditTime` every edit stamps so Engine sees the change, and —
|
|
435
|
-
`create_playlist` only — the previous last playlist's link, made by
|
|
436
|
-
own insert trigger. No other playlist is renamed, emptied or deleted,
|
|
437
|
-
track, cue or beatgrid is touched by
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
505
|
+
in a different order. What these four playlist tools touch is the named
|
|
506
|
+
playlist's own entries, plus exactly two rows elsewhere: that playlist's own
|
|
507
|
+
row, whose `lastEditTime` every edit stamps so Engine sees the change, and —
|
|
508
|
+
for `create_playlist` only — the previous last playlist's link, made by
|
|
509
|
+
Engine's own insert trigger. No other playlist is renamed, emptied or deleted,
|
|
510
|
+
and no track, cue or beatgrid is touched by these four. `update_track_metadata`
|
|
511
|
+
changes genre, comment, label, year and rating on the tracks named — see its
|
|
512
|
+
section above — and nothing else: no playlist, cue, beatgrid, title, artist,
|
|
513
|
+
album, path or file is touched.
|
|
514
|
+
|
|
515
|
+
Every edit returns `undo` — the exact tool call that reverses it — and
|
|
516
|
+
`undo_complete`; for the playlist tools, `undo` is expressed against the
|
|
517
|
+
positions the edit itself produced, and `undo_complete` says whether
|
|
518
|
+
replaying it puts the playlist back exactly as it was. Replaying
|
|
442
519
|
`undo` is the right way back from an edit; restoring `backup_path` is not,
|
|
443
520
|
because it reverts the **whole library** to before this session's first
|
|
444
521
|
write, discarding every play count, import, cue and beatgrid change Engine
|
|
@@ -493,9 +570,16 @@ library the write actually landed in. Two libraries connected at once is the
|
|
|
493
570
|
ordinary setup: a USB drive and its copy on the computer. This is where you
|
|
494
571
|
check which of them a write went to.
|
|
495
572
|
|
|
496
|
-
**`undo` reverses the edit in that one library, and only there.**
|
|
497
|
-
|
|
498
|
-
|
|
573
|
+
**`undo` reverses the edit in that one library, and only there.** Each undo
|
|
574
|
+
step names it — by path, in the step's own `library` argument — so replaying
|
|
575
|
+
a step verbatim goes back to the library the edit was made in, not to whatever
|
|
576
|
+
the default is at replay time. That matters because a USB drive and its copy
|
|
577
|
+
hold the same playlist ids and the same track ids: a replay that resolved the
|
|
578
|
+
default could land on the wrong disk, and its `expect_track_ids` would agree,
|
|
579
|
+
both sides having been edited the same way.
|
|
580
|
+
|
|
581
|
+
Engine DJ moves playlist changes between connected libraries by itself, so a
|
|
582
|
+
copy of your edit can still end up somewhere `undo` cannot reach.
|
|
499
583
|
|
|
500
584
|
Measured 2026-09-01. A track was added to a playlist in the library on the
|
|
501
585
|
computer. Engine DJ was then launched with the USB drive attached, and the
|
|
@@ -533,9 +617,9 @@ cleared the next time that library is snapshotted.
|
|
|
533
617
|
**To undo a playlist you created, delete it in Engine DJ.** Engine's own
|
|
534
618
|
delete trigger repairs the playlist chain and cascades the entries away,
|
|
535
619
|
which is exactly what removing it should do and is not something restoring
|
|
536
|
-
a snapshot does better. **
|
|
537
|
-
the `undo` the edit returned instead** — it names
|
|
538
|
-
`add_tracks_to_playlist`, `remove_tracks_from_playlist` or
|
|
620
|
+
a snapshot does better. **For the playlist tools, to undo an edit to an
|
|
621
|
+
existing playlist, replay the `undo` the edit returned instead** — it names
|
|
622
|
+
the precise `add_tracks_to_playlist`, `remove_tracks_from_playlist` or
|
|
539
623
|
`reorder_playlist` call that puts the playlist back exactly as it was,
|
|
540
624
|
without touching anything else Engine DJ has recorded since.
|
|
541
625
|
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const ERROR_CODES: readonly ["library_busy", "library_not_found", "library_unreadable", "unsupported_schema", "query_timeout", "query_process_crashed", "index_stale", "decode_failed", "invalid_argument", "library_needs_recovery", "playlist_exists", "unknown_track", "duplicate_track", "playlist_chain_damaged", "playlist_not_found", "invalid_position", "ambiguous_library"];
|
|
1
|
+
export declare const ERROR_CODES: readonly ["library_busy", "library_not_found", "library_unreadable", "unsupported_schema", "query_timeout", "query_process_crashed", "index_stale", "decode_failed", "invalid_argument", "library_needs_recovery", "playlist_exists", "unknown_track", "duplicate_track", "playlist_chain_damaged", "playlist_not_found", "invalid_position", "ambiguous_library", "stale_value", "track_not_editable"];
|
|
2
2
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
3
3
|
export interface EngineError {
|
|
4
4
|
error: ErrorCode;
|
|
@@ -24,6 +24,20 @@ export interface EngineError {
|
|
|
24
24
|
* taken.
|
|
25
25
|
*/
|
|
26
26
|
backup_path?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Set only on stale_value: every `expect` that no longer matched, capped at
|
|
29
|
+
* 20 entries (the message carries the total). Structured so a caller can
|
|
30
|
+
* tell the user precisely which tracks and fields changed instead of
|
|
31
|
+
* parsing prose -- not so it can re-read and retry: the track changed
|
|
32
|
+
* after `expect` was read, and overwriting that silently, without the
|
|
33
|
+
* user's consent, is exactly what stale_value refuses.
|
|
34
|
+
*/
|
|
35
|
+
mismatches?: {
|
|
36
|
+
id: number;
|
|
37
|
+
field: string;
|
|
38
|
+
expected: string | number | null;
|
|
39
|
+
actual: string | number | null;
|
|
40
|
+
}[];
|
|
27
41
|
}
|
|
28
42
|
export declare function err(error: ErrorCode, message: string, extra?: Omit<EngineError, "error" | "message">): EngineError;
|
|
29
43
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -33,12 +33,21 @@ export const ERROR_CODES = [
|
|
|
33
33
|
"playlist_chain_damaged",
|
|
34
34
|
"playlist_not_found",
|
|
35
35
|
"invalid_position",
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
36
|
+
// A write cannot tell which physical library to change: no `library` was
|
|
37
|
+
// passed and more than one supported library is connected, or the uuid
|
|
38
|
+
// passed is shared by copies on different drives. Its own code rather than
|
|
39
|
+
// invalid_argument because the useful client response is specific: ask
|
|
40
|
+
// which drive, then retry with its path. Only writes raise it; see
|
|
41
|
+
// library-select.ts for why reads still choose.
|
|
41
42
|
"ambiguous_library",
|
|
43
|
+
// update_track_metadata (spec §7.2). stale_value: an `expect` no longer
|
|
44
|
+
// matches what is in the library. track_not_editable: this track, or one
|
|
45
|
+
// field of it, cannot be edited without harm -- an empty origin the Track
|
|
46
|
+
// trigger would rewrite, or a stored value this tool could not restore.
|
|
47
|
+
// Not unknown_track: the track exists, and telling a model it does not sends
|
|
48
|
+
// it back to search for a track it will find again.
|
|
49
|
+
"stale_value",
|
|
50
|
+
"track_not_editable",
|
|
42
51
|
];
|
|
43
52
|
export function err(error, message, extra = {}) {
|
|
44
53
|
return { error, message, ...extra };
|
package/dist/library-select.d.ts
CHANGED
|
@@ -35,26 +35,23 @@ export declare const LibraryArg: z.ZodOptional<z.ZodString>;
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function pickDefaultLibrary(libs: readonly LibraryInfo[]): LibraryInfo | null;
|
|
37
37
|
/**
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* The libraries a write must be told apart between: every supported one, as
|
|
39
|
+
* soon as there is more than one. Empty when there is nothing to choose --
|
|
40
|
+
* one supported library, or none -- so a DJ with a single library never has
|
|
41
|
+
* to name it.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* This used to ask a narrower question: which libraries *tied* for the
|
|
44
|
+
* default pick on track count. That was wrong, and measurably so. A USB drive
|
|
45
|
+
* and its copy tie only until one of them gains a track; at 258 against 257
|
|
46
|
+
* the tie is gone and `pickDefaultLibrary` silently takes the larger. For a
|
|
47
|
+
* playlist that is at least visible on the wrong drive. For a track's genre
|
|
48
|
+
* it is invisible, and the DJ is left believing the edit did not work.
|
|
45
49
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* physical disk changes, and one of them is the drive the DJ performs from.
|
|
50
|
-
* Hence: reads choose, writes refuse. Callers wanting the strict behaviour
|
|
51
|
-
* check this first.
|
|
52
|
-
*
|
|
53
|
-
* Skips unsupported libraries for the same reason `pickDefaultLibrary` does:
|
|
54
|
-
* one can never be chosen while a supported library exists, so it is not a
|
|
55
|
-
* competing candidate and must not make a write refuse.
|
|
50
|
+
* The count was never the question. Which physical disk changes is the
|
|
51
|
+
* caller's to say, and only reads -- which change nothing -- may be spared
|
|
52
|
+
* the question.
|
|
56
53
|
*/
|
|
57
|
-
export declare function
|
|
54
|
+
export declare function writeNeedsLibrary(libs: readonly LibraryInfo[]): LibraryInfo[];
|
|
58
55
|
/**
|
|
59
56
|
* The refusal for an ambiguous default on a write.
|
|
60
57
|
*
|
|
@@ -84,8 +81,29 @@ export declare function ambiguousLibrary(tied: readonly LibraryInfo[]): EngineEr
|
|
|
84
81
|
*
|
|
85
82
|
* A path match is exact on the m.db file, not a prefix: a value that merely
|
|
86
83
|
* *contains* a library path must not select it.
|
|
84
|
+
*
|
|
85
|
+
* A uuid shared by several libraries resolves to the first of them in this
|
|
86
|
+
* server's list of known libraries -- root-scan order at startup, arrival
|
|
87
|
+
* order for a drive plugged in later, but no order a caller can rely on. That
|
|
88
|
+
* is tolerable for a read, which changes no disk, and never for a write: see
|
|
89
|
+
* namedWriteLibrary.
|
|
87
90
|
*/
|
|
88
91
|
export declare function findLibrary(libs: readonly LibraryInfo[], requested: string): LibraryInfo | null;
|
|
92
|
+
/**
|
|
93
|
+
* Every library a `library` value names, in the order of `libs`.
|
|
94
|
+
* More than one only for a uuid: copying an Engine Library folder onto another
|
|
95
|
+
* drive copies its uuid with it, while each library's path is its own.
|
|
96
|
+
*/
|
|
97
|
+
export declare function findLibraries(libs: readonly LibraryInfo[], requested: string): LibraryInfo[];
|
|
98
|
+
/**
|
|
99
|
+
* The one library a write names, or the refusal.
|
|
100
|
+
*
|
|
101
|
+
* findLibrary would hand back the first of two libraries sharing a uuid, and
|
|
102
|
+
* which is first is only discovery order -- the very thing ambiguousLibrary
|
|
103
|
+
* exists so that a write does not rest on. A named uuid is no better an answer
|
|
104
|
+
* than an omitted `library` when it names both a USB drive and its copy.
|
|
105
|
+
*/
|
|
106
|
+
export declare function namedWriteLibrary(libs: readonly LibraryInfo[], requested: string): LibraryInfo | EngineError;
|
|
89
107
|
/**
|
|
90
108
|
* The error for a `library` value that matched nothing. It names what was
|
|
91
109
|
* passed and lists what is actually selectable, because the two ways to get
|
package/dist/library-select.js
CHANGED
|
@@ -14,10 +14,12 @@ import { expandHome, redactPath } from "./paths.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export const LIBRARY_ARG_DESCRIPTION = "Which library to use: either the uuid or the path reported by list_libraries " +
|
|
16
16
|
"(the reported ~/... form is accepted, as is the absolute path). Omit it to use " +
|
|
17
|
-
"the supported library holding the most tracks.
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
17
|
+
"the supported library holding the most tracks. A READ may always omit it. A WRITE " +
|
|
18
|
+
"may omit it only when a single supported library is connected: with two or more, a " +
|
|
19
|
+
"write refuses with ambiguous_library listing them, since the choice decides which " +
|
|
20
|
+
"disk changes; ask the user which, then pass it here. A library copied onto another drive " +
|
|
21
|
+
"keeps its uuid, so a uuid can name two connected libraries: a write naming such a uuid " +
|
|
22
|
+
"refuses with ambiguous_library as well. Pass the path to write to one of them.";
|
|
21
23
|
export const LibraryArg = z.string().min(1).optional().describe(LIBRARY_ARG_DESCRIPTION);
|
|
22
24
|
/**
|
|
23
25
|
* The default when no `library` was given: the supported library with the
|
|
@@ -51,32 +53,25 @@ export function pickDefaultLibrary(libs) {
|
|
|
51
53
|
return best ?? libs[0] ?? null;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* The libraries a write must be told apart between: every supported one, as
|
|
57
|
+
* soon as there is more than one. Empty when there is nothing to choose --
|
|
58
|
+
* one supported library, or none -- so a DJ with a single library never has
|
|
59
|
+
* to name it.
|
|
57
60
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
+
* This used to ask a narrower question: which libraries *tied* for the
|
|
62
|
+
* default pick on track count. That was wrong, and measurably so. A USB drive
|
|
63
|
+
* and its copy tie only until one of them gains a track; at 258 against 257
|
|
64
|
+
* the tie is gone and `pickDefaultLibrary` silently takes the larger. For a
|
|
65
|
+
* playlist that is at least visible on the wrong drive. For a track's genre
|
|
66
|
+
* it is invisible, and the DJ is left believing the edit did not work.
|
|
61
67
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* physical disk changes, and one of them is the drive the DJ performs from.
|
|
66
|
-
* Hence: reads choose, writes refuse. Callers wanting the strict behaviour
|
|
67
|
-
* check this first.
|
|
68
|
-
*
|
|
69
|
-
* Skips unsupported libraries for the same reason `pickDefaultLibrary` does:
|
|
70
|
-
* one can never be chosen while a supported library exists, so it is not a
|
|
71
|
-
* competing candidate and must not make a write refuse.
|
|
68
|
+
* The count was never the question. Which physical disk changes is the
|
|
69
|
+
* caller's to say, and only reads -- which change nothing -- may be spared
|
|
70
|
+
* the question.
|
|
72
71
|
*/
|
|
73
|
-
export function
|
|
72
|
+
export function writeNeedsLibrary(libs) {
|
|
74
73
|
const supported = libs.filter((l) => l.supported);
|
|
75
|
-
|
|
76
|
-
return [];
|
|
77
|
-
const best = Math.max(...supported.map((l) => l.trackCount ?? -1));
|
|
78
|
-
const tied = supported.filter((l) => (l.trackCount ?? -1) === best);
|
|
79
|
-
return tied.length > 1 ? tied : [];
|
|
74
|
+
return supported.length > 1 ? supported : [];
|
|
80
75
|
}
|
|
81
76
|
/**
|
|
82
77
|
* The refusal for an ambiguous default on a write.
|
|
@@ -94,10 +89,13 @@ export function defaultLibraryTies(libs) {
|
|
|
94
89
|
*/
|
|
95
90
|
export function ambiguousLibrary(tied) {
|
|
96
91
|
const list = tied.map((l) => `${l.uuid} -- ${redactPath(l.path)} (${l.trackCount} tracks)`).join("; ");
|
|
97
|
-
return err("ambiguous_library", `More than one library
|
|
98
|
-
`Nothing was written. ASK which one to write to, then retry with \`library\` set
|
|
99
|
-
`do not choose for them.
|
|
100
|
-
`
|
|
92
|
+
return err("ambiguous_library", `More than one library is connected, so there is no default to write to: ${list}. ` +
|
|
93
|
+
`Nothing was written. ASK which one to write to, then retry with \`library\` set to that ` +
|
|
94
|
+
`library's path -- do not choose for them. A copy keeps its uuid, so a uuid may name more ` +
|
|
95
|
+
`than one of these. They are usually a USB drive and its copy on the computer, ` +
|
|
96
|
+
`and one of them may be the drive they perform from. A read without \`library\` may have ` +
|
|
97
|
+
`come from a different one of these, so re-read anything the write depends on (track and ` +
|
|
98
|
+
`playlist ids, positions, current values) from the chosen library first.`, { detail: "not_committed" });
|
|
101
99
|
}
|
|
102
100
|
/**
|
|
103
101
|
* Resolves a caller-supplied `library` value: uuid first, then filesystem
|
|
@@ -113,19 +111,66 @@ export function ambiguousLibrary(tied) {
|
|
|
113
111
|
*
|
|
114
112
|
* A path match is exact on the m.db file, not a prefix: a value that merely
|
|
115
113
|
* *contains* a library path must not select it.
|
|
114
|
+
*
|
|
115
|
+
* A uuid shared by several libraries resolves to the first of them in this
|
|
116
|
+
* server's list of known libraries -- root-scan order at startup, arrival
|
|
117
|
+
* order for a drive plugged in later, but no order a caller can rely on. That
|
|
118
|
+
* is tolerable for a read, which changes no disk, and never for a write: see
|
|
119
|
+
* namedWriteLibrary.
|
|
116
120
|
*/
|
|
117
121
|
export function findLibrary(libs, requested) {
|
|
122
|
+
return findLibraries(libs, requested)[0] ?? null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every library a `library` value names, in the order of `libs`.
|
|
126
|
+
* More than one only for a uuid: copying an Engine Library folder onto another
|
|
127
|
+
* drive copies its uuid with it, while each library's path is its own.
|
|
128
|
+
*/
|
|
129
|
+
export function findLibraries(libs, requested) {
|
|
118
130
|
const wanted = requested.trim();
|
|
119
131
|
if (!wanted)
|
|
120
|
-
return
|
|
121
|
-
const byUuid = libs.
|
|
122
|
-
if (byUuid)
|
|
132
|
+
return [];
|
|
133
|
+
const byUuid = libs.filter((l) => l.uuid && l.uuid.toLowerCase() === wanted.toLowerCase());
|
|
134
|
+
if (byUuid.length > 0)
|
|
123
135
|
return byUuid;
|
|
124
136
|
// resolve() turns a relative value into something rooted at the process
|
|
125
137
|
// cwd, which matches no library path -- exactly the intended outcome for
|
|
126
138
|
// a value that is neither a uuid nor a real path.
|
|
127
139
|
const wantedPath = resolve(expandHome(wanted));
|
|
128
|
-
return libs.
|
|
140
|
+
return libs.filter((l) => resolve(l.path) === wantedPath);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The one library a write names, or the refusal.
|
|
144
|
+
*
|
|
145
|
+
* findLibrary would hand back the first of two libraries sharing a uuid, and
|
|
146
|
+
* which is first is only discovery order -- the very thing ambiguousLibrary
|
|
147
|
+
* exists so that a write does not rest on. A named uuid is no better an answer
|
|
148
|
+
* than an omitted `library` when it names both a USB drive and its copy.
|
|
149
|
+
*/
|
|
150
|
+
export function namedWriteLibrary(libs, requested) {
|
|
151
|
+
const matches = findLibraries(libs, requested);
|
|
152
|
+
if (matches.length > 1)
|
|
153
|
+
return sharedUuid(requested, matches);
|
|
154
|
+
if (matches[0])
|
|
155
|
+
return matches[0];
|
|
156
|
+
// The same refusal a read gets, with its list moved into `message`: on a
|
|
157
|
+
// write `detail` is reserved, for the reason given at ambiguousLibrary.
|
|
158
|
+
const miss = libraryNotFound(requested, libs);
|
|
159
|
+
return { ...miss, message: `${miss.message}. ${miss.detail}`, detail: "not_committed" };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The refusal for a uuid naming more than one library. Lists paths, since the
|
|
163
|
+
* uuid is the one thing the candidates do not differ in; `detail` stays
|
|
164
|
+
* exactly "not_committed" for the reason given at ambiguousLibrary.
|
|
165
|
+
*/
|
|
166
|
+
function sharedUuid(requested, matches) {
|
|
167
|
+
const list = matches.map((l) => `${redactPath(l.path)} (${l.trackCount} tracks)`).join("; ");
|
|
168
|
+
return err("ambiguous_library", `"${requested.trim()}" names more than one connected library -- a library copied onto another ` +
|
|
169
|
+
`drive keeps its uuid: ${list}. Nothing was written. ASK which one to write to, then retry ` +
|
|
170
|
+
`with \`library\` set to that one's path -- do not choose for them. One of them may be the ` +
|
|
171
|
+
`drive they perform from. A read naming this uuid may have come from another of these copies, ` +
|
|
172
|
+
`so re-read anything the write depends on (track and playlist ids, positions, current values) ` +
|
|
173
|
+
`from that path first.`, { detail: "not_committed" });
|
|
129
174
|
}
|
|
130
175
|
/**
|
|
131
176
|
* The error for a `library` value that matched nothing. It names what was
|