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/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++) {
|
package/dist/discovery.js
CHANGED
|
@@ -12,8 +12,9 @@ export function readLibraryInfo(mdbPath) {
|
|
|
12
12
|
}
|
|
13
13
|
if (hasHotJournal(mdbPath)) {
|
|
14
14
|
// Same check openQueryConnection makes before opening (store/connections.ts):
|
|
15
|
-
// recovering a hot journal needs a write,
|
|
16
|
-
//
|
|
15
|
+
// recovering a hot journal needs a write, and discovery never opens a
|
|
16
|
+
// library writably -- not to probe one, and not to heal one. Caught here
|
|
17
|
+
// first so the specific,
|
|
17
18
|
// actionable library_needs_recovery reaches the caller instead of the
|
|
18
19
|
// SELECT below failing with the raw "attempt to write a readonly
|
|
19
20
|
// database" and landing in the generic library_unreadable catch --
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
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"];
|
|
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"];
|
|
2
2
|
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
3
3
|
export interface EngineError {
|
|
4
4
|
error: ErrorCode;
|
|
5
5
|
message: string;
|
|
6
|
+
/**
|
|
7
|
+
* Free text almost everywhere (store/index-manager.ts passes a raw
|
|
8
|
+
* `e.message` through it), with one exception that is part of the tool
|
|
9
|
+
* contract: on an error from the write path (store/write.ts) this is
|
|
10
|
+
* always exactly `"not_committed"` -- the library is byte-for-byte what it
|
|
11
|
+
* was -- or `"committed_unverified"` -- the write may have landed and could
|
|
12
|
+
* not be confirmed, and `backup_path` below is then set. Those two strings
|
|
13
|
+
* are reserved on that path and must stay stable, because a client reads
|
|
14
|
+
* them to decide whether their library changed.
|
|
15
|
+
*/
|
|
6
16
|
detail?: string;
|
|
7
17
|
retry_after_ms?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Path to a pre-write snapshot the caller can restore from. Only ever set
|
|
20
|
+
* by the write path, and only on the errors a client cannot safely ignore:
|
|
21
|
+
* `detail === "committed_unverified"`. It is a whole-database snapshot, so
|
|
22
|
+
* restoring it is a recovery route for a damaged library and not an undo
|
|
23
|
+
* of one playlist -- it reverts everything Engine DJ wrote since it was
|
|
24
|
+
* taken.
|
|
25
|
+
*/
|
|
26
|
+
backup_path?: string;
|
|
8
27
|
}
|
|
9
28
|
export declare function err(error: ErrorCode, message: string, extra?: Omit<EngineError, "error" | "message">): EngineError;
|
|
10
29
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -19,6 +19,14 @@ export const ERROR_CODES = [
|
|
|
19
19
|
"decode_failed",
|
|
20
20
|
"invalid_argument",
|
|
21
21
|
"library_needs_recovery",
|
|
22
|
+
// Write-path codes. There is no writes_not_enabled code: the tool is not
|
|
23
|
+
// registered at all without --allow-writes, and the MCP SDK's own
|
|
24
|
+
// dispatcher rejects a call to an unregistered tool name before any
|
|
25
|
+
// handler in this project runs, so this project never gets the chance to
|
|
26
|
+
// report that condition itself.
|
|
27
|
+
"playlist_exists",
|
|
28
|
+
"unknown_track",
|
|
29
|
+
"duplicate_track",
|
|
22
30
|
];
|
|
23
31
|
export function err(error, message, extra = {}) {
|
|
24
32
|
return { error, message, ...extra };
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { createServer } from "./server.js";
|
|
5
5
|
async function main() {
|
|
6
|
-
|
|
6
|
+
// MCP clients configure a server as a command plus an args array, so a flag
|
|
7
|
+
// is visible in that configuration and greppable; an environment variable
|
|
8
|
+
// would not be. Writes are off unless this is present.
|
|
9
|
+
const allowWrites = process.argv.includes("--allow-writes");
|
|
10
|
+
// stderr is not the protocol channel, so this is safe for stdio transport,
|
|
11
|
+
// and it puts the mode in the client's log where a user can check it.
|
|
12
|
+
console.error(`engine-dj-mcp: writes ${allowWrites ? "ENABLED (--allow-writes)" : "disabled"}`);
|
|
13
|
+
const server = await createServer({ allowWrites });
|
|
7
14
|
await server.connect(new StdioServerTransport());
|
|
8
15
|
}
|
|
9
16
|
main().catch((e) => {
|
|
10
|
-
// stderr is not the protocol channel, so this is safe for stdio transport.
|
|
11
17
|
console.error("engine-dj-mcp failed to start:", e);
|
|
12
18
|
process.exit(1);
|
|
13
19
|
});
|
package/dist/paths.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export declare function sidecarDir(uuid: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* A short, stable tag for one library *file*, for use wherever a uuid alone
|
|
4
|
+
* would collide. A library copied onto a second drive carries the original's
|
|
5
|
+
* uuid -- an ordinary thing for a DJ to do -- so uuid is not unique across
|
|
6
|
+
* mounted volumes while the path of `m.db` always is.
|
|
7
|
+
*
|
|
8
|
+
* Shared by the sidecar layout (server.ts's sidecarBaseFor) and the backup
|
|
9
|
+
* filenames (store/backup.ts) so the two cannot drift into different ideas
|
|
10
|
+
* of which library they are talking about.
|
|
11
|
+
*/
|
|
12
|
+
export declare function libraryTag(mdbPath: string): string;
|
|
2
13
|
/** Engine stores Track.path relative to the `Engine Library` folder, usually with `..`. */
|
|
3
14
|
export declare function absTrackPath(mdbPath: string, relative: string): string;
|
|
4
15
|
/** Candidate locations of `m.db` beneath a filesystem root. */
|
package/dist/paths.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join, dirname, resolve } from "node:path";
|
|
3
4
|
export function sidecarDir(uuid) {
|
|
4
5
|
return join(homedir(), ".engine-dj-mcp", uuid);
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* A short, stable tag for one library *file*, for use wherever a uuid alone
|
|
9
|
+
* would collide. A library copied onto a second drive carries the original's
|
|
10
|
+
* uuid -- an ordinary thing for a DJ to do -- so uuid is not unique across
|
|
11
|
+
* mounted volumes while the path of `m.db` always is.
|
|
12
|
+
*
|
|
13
|
+
* Shared by the sidecar layout (server.ts's sidecarBaseFor) and the backup
|
|
14
|
+
* filenames (store/backup.ts) so the two cannot drift into different ideas
|
|
15
|
+
* of which library they are talking about.
|
|
16
|
+
*/
|
|
17
|
+
export function libraryTag(mdbPath) {
|
|
18
|
+
return createHash("sha256").update(mdbPath).digest("hex").slice(0, 12);
|
|
19
|
+
}
|
|
6
20
|
/** Engine stores Track.path relative to the `Engine Library` folder, usually with `..`. */
|
|
7
21
|
export function absTrackPath(mdbPath, relative) {
|
|
8
22
|
const engineLibrary = dirname(dirname(mdbPath)); // .../Engine Library/Database2/m.db
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { type EngineError } from "./errors.js";
|
|
2
|
+
import type { QueryProcess } from "./proc/query-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Engine stores both playlist order and playlist-entry order as singly
|
|
5
|
+
* linked lists, not as a position column:
|
|
6
|
+
*
|
|
7
|
+
* Playlist.nextListId — the next sibling under the same parentListId
|
|
8
|
+
* PlaylistEntity.nextEntityId — the next entry in the same listId
|
|
9
|
+
*
|
|
10
|
+
* Both terminate at 0.
|
|
11
|
+
*
|
|
12
|
+
* The schema also ships a `PlaylistPath` view carrying a column literally
|
|
13
|
+
* called `position`, which is the obvious thing to reach for and is wrong.
|
|
14
|
+
* Its `OrderedList` CTE anchors on `WHERE nextListId = 0` and counts
|
|
15
|
+
* *upwards from the tail*, then orders by that count ascending — so its
|
|
16
|
+
* `position` runs backwards. Measured on the reference library (16
|
|
17
|
+
* playlists): walking the chain yields
|
|
18
|
+
* ACID Beach, KatyaBar, Electro 1, Old Fasion 1A 120-130, Playlist, ...
|
|
19
|
+
* which is what Engine DJ shows in its sidebar, while the view's position
|
|
20
|
+
* order is that list exactly reversed (asserted in tests/playlists.test.ts
|
|
21
|
+
* against the same shape). The chain is the source of truth; the view is
|
|
22
|
+
* never read by this project.
|
|
23
|
+
*
|
|
24
|
+
* Everything here therefore walks the chain — defensively. These are linked
|
|
25
|
+
* lists inside a file this server does not own and never writes: a
|
|
26
|
+
* half-completed Engine write, a sync conflict or a partially restored
|
|
27
|
+
* backup can leave a cycle, a link to a row that is gone, or two
|
|
28
|
+
* disconnected runs. None of those may hang the walk, and none may come
|
|
29
|
+
* back as a silently short list that reads like a complete one.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The predicate that connects one `PlaylistEntity` (`e`) to its `Track` (`t`).
|
|
33
|
+
*
|
|
34
|
+
* A playlist entry does not name a local row id. It carries
|
|
35
|
+
* `(databaseUuid, trackId)`, which identifies the track **in the library the
|
|
36
|
+
* entry was made in**, and `Track` preserves that same identity in
|
|
37
|
+
* `(originDatabaseUuid, originTrackId)` — a pair that need not equal, and
|
|
38
|
+
* usually does not equal, `Track.id`. Engine's own schema says as much: the
|
|
39
|
+
* pair carries `CONSTRAINT C_originDatabaseUuid_originTrackId UNIQUE`, making
|
|
40
|
+
* it the track's natural key across drives, and a pair of triggers
|
|
41
|
+
* (`trigger_after_insert_Track_fix_origin` and its update twin) stamps it
|
|
42
|
+
* from `Information.uuid` whenever a row arrives without one. Engine's
|
|
43
|
+
* application binary embeds the same join verbatim:
|
|
44
|
+
*
|
|
45
|
+
* SELECT COUNT(DISTINCT databaseUuid || trackId) FROM PlaylistEntity
|
|
46
|
+
* JOIN Track ON (originDatabaseUuid, originTrackId) = (databaseUuid, trackId)
|
|
47
|
+
*
|
|
48
|
+
* Joining on `e.trackId = t.id` instead reads naturally and is wrong in both
|
|
49
|
+
* directions. Measured on the reference USB library (257 tracks, 16
|
|
50
|
+
* playlists, 202 entries): the id join reports 105 of 202 entries as orphans
|
|
51
|
+
* where the pair join finds 0, because 178 entries name a *third* library
|
|
52
|
+
* (`33be3313-…`) that is neither of the two attached and only 91 of the 257
|
|
53
|
+
* tracks carry this database's own uuid — a healthy library reported as
|
|
54
|
+
* riddled with holes, and one 43-entry playlist reduced to a single playable
|
|
55
|
+
* track. In the other direction it silently answers with the *wrong track*
|
|
56
|
+
* whenever a foreign entry's `trackId` happens to collide with a local row
|
|
57
|
+
* id, and calls a genuinely missing entry present.
|
|
58
|
+
*
|
|
59
|
+
* Written once and shared, so the three places that ask the question cannot
|
|
60
|
+
* drift apart. It names the aliases `t` and `e`; every call site uses them.
|
|
61
|
+
*/
|
|
62
|
+
export declare const ENTRY_TRACK_MATCH = "t.originDatabaseUuid = e.databaseUuid AND t.originTrackId = e.trackId";
|
|
63
|
+
/** One node of an Engine linked list. `next` is 0 at the end of the chain. */
|
|
64
|
+
export interface Linked {
|
|
65
|
+
id: number;
|
|
66
|
+
next: number;
|
|
67
|
+
}
|
|
68
|
+
export interface ChainOrder<T> {
|
|
69
|
+
order: T[];
|
|
70
|
+
warnings: string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Orders one linked-list group, and says so when it could not.
|
|
74
|
+
*
|
|
75
|
+
* The contract that matters: `order` always contains **every** input node,
|
|
76
|
+
* exactly once. A chain defect degrades the *order* and raises a warning; it
|
|
77
|
+
* never drops a node, because a caller cannot tell a truncated list from a
|
|
78
|
+
* short one, and `get_playlist_tracks` returning 30 of 43 entries with no
|
|
79
|
+
* complaint is a worse answer than returning 43 in a partly-guessed order
|
|
80
|
+
* with an explanation attached.
|
|
81
|
+
*
|
|
82
|
+
* Walk termination does not rely on a magic iteration cap: every step either
|
|
83
|
+
* marks one previously unseen node visited or stops, so the walk is bounded
|
|
84
|
+
* by the group size by construction. Anything the walk could not reach is
|
|
85
|
+
* appended in id order.
|
|
86
|
+
*
|
|
87
|
+
* `subject` prefixes the warnings, so a caller reading a whole tree can tell
|
|
88
|
+
* which playlist was malformed.
|
|
89
|
+
*/
|
|
90
|
+
export declare function orderByChain<T extends Linked>(nodes: readonly T[], subject: string): ChainOrder<T>;
|
|
91
|
+
/** A `Playlist` row, as this module needs it. */
|
|
92
|
+
export interface PlaylistRow extends Linked {
|
|
93
|
+
title: string;
|
|
94
|
+
parent: number;
|
|
95
|
+
isPersisted: boolean;
|
|
96
|
+
entryCount: number;
|
|
97
|
+
missingCount: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One playlist as reported to a caller.
|
|
101
|
+
*
|
|
102
|
+
* `is_folder` is derived, not stored: the schema has no folder flag, and
|
|
103
|
+
* Engine's own folders are simply `Playlist` rows that other `Playlist` rows
|
|
104
|
+
* name as their `parentListId`. So this means "has at least one child list",
|
|
105
|
+
* which is exactly what Engine draws as a folder -- with one honest
|
|
106
|
+
* consequence: a folder a DJ has emptied is indistinguishable from a
|
|
107
|
+
* playlist with no tracks, and is reported as the latter.
|
|
108
|
+
*
|
|
109
|
+
* `track_count` is entries in this list itself, never a total rolled up from
|
|
110
|
+
* children, so it matches the number Engine shows beside the playlist.
|
|
111
|
+
* `missing_count` is how many of those entries name a track that is not in
|
|
112
|
+
* this library -- see `get_playlist_tracks` for why that is routine rather
|
|
113
|
+
* than a corruption.
|
|
114
|
+
*/
|
|
115
|
+
export interface PlaylistItem {
|
|
116
|
+
id: number;
|
|
117
|
+
name: string;
|
|
118
|
+
/** Full path from the top of the tree, `/`-separated. Unique across the tree. */
|
|
119
|
+
path: string;
|
|
120
|
+
parent_id: number | null;
|
|
121
|
+
depth: number;
|
|
122
|
+
is_folder: boolean;
|
|
123
|
+
is_persisted: boolean;
|
|
124
|
+
track_count: number;
|
|
125
|
+
missing_count: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Flattens the playlist forest into Engine's own display order: each sibling
|
|
129
|
+
* group in `nextListId` chain order, each list immediately followed by its
|
|
130
|
+
* own children (pre-order), which is exactly what an expanded Engine sidebar
|
|
131
|
+
* shows top to bottom.
|
|
132
|
+
*
|
|
133
|
+
* Flat-plus-`depth`-plus-`path` rather than nested `children` arrays: the
|
|
134
|
+
* order Engine displays is then readable in a single pass down the array
|
|
135
|
+
* without reconstructing a traversal, the result truncates to a genuine
|
|
136
|
+
* prefix of the sidebar, and `path` gives every list a unique handle that a
|
|
137
|
+
* bare name does not (see `findPlaylistByName`).
|
|
138
|
+
*/
|
|
139
|
+
export declare function buildPlaylistTree(rows: readonly PlaylistRow[]): {
|
|
140
|
+
items: PlaylistItem[];
|
|
141
|
+
warnings: string[];
|
|
142
|
+
};
|
|
143
|
+
export interface PlaylistTree {
|
|
144
|
+
items: PlaylistItem[];
|
|
145
|
+
warnings: string[];
|
|
146
|
+
/** Playlists in the library, even when more were found than were returned. */
|
|
147
|
+
total: number;
|
|
148
|
+
truncated: boolean;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reads every playlist plus its entry counts, and orders the result.
|
|
152
|
+
*
|
|
153
|
+
* The counts come from one grouped pass over `PlaylistEntity` rather than a
|
|
154
|
+
* correlated subquery per playlist, and `missing_count` is computed in the
|
|
155
|
+
* same pass: "how many of these can I actually play" is not a rare question
|
|
156
|
+
* and must not cost a second round trip per list.
|
|
157
|
+
*
|
|
158
|
+
* `missing` is an EXISTS semi-join on the natural key (see
|
|
159
|
+
* ENTRY_TRACK_MATCH), not a LEFT JOIN to Track. Membership is the whole
|
|
160
|
+
* question, and a semi-join cannot inflate `n`: a joined row set would count
|
|
161
|
+
* one entry twice if two tracks ever answered to the same origin key, which
|
|
162
|
+
* Engine's UNIQUE constraint forbids but this server has no way to enforce
|
|
163
|
+
* on a file it does not own.
|
|
164
|
+
*/
|
|
165
|
+
export declare function loadPlaylistTree(qp: QueryProcess): Promise<PlaylistTree | EngineError>;
|
|
166
|
+
/**
|
|
167
|
+
* Every list matching a caller-supplied name, most specific interpretation
|
|
168
|
+
* first.
|
|
169
|
+
*
|
|
170
|
+
* `Playlist` is unique on `(title, parentListId)` only, so a bare name is
|
|
171
|
+
* genuinely ambiguous the moment a DJ uses folders -- "House" under
|
|
172
|
+
* *Warmup* and "House" under *Peak* are two different playlists and neither
|
|
173
|
+
* is the obvious winner. A full path is unique by construction, so it is
|
|
174
|
+
* tried first and gives a caller a way to say precisely which one they mean;
|
|
175
|
+
* a bare title is tried next and may legitimately return several, which the
|
|
176
|
+
* caller turns into an error rather than an arbitrary pick.
|
|
177
|
+
*
|
|
178
|
+
* Case-insensitive matching is a fallback tier, not the primary rule: a
|
|
179
|
+
* library containing both "Peak" and "peak" as siblings is legal, and an
|
|
180
|
+
* exact match must win outright rather than being reported as ambiguous
|
|
181
|
+
* against its own differently-cased neighbour.
|
|
182
|
+
*/
|
|
183
|
+
export declare function findPlaylistByName(items: readonly PlaylistItem[], name: string): PlaylistItem[];
|
|
184
|
+
export interface PlaylistSelector {
|
|
185
|
+
id?: number;
|
|
186
|
+
name?: string;
|
|
187
|
+
}
|
|
188
|
+
/** The argument names to quote back in an error, so each tool blames its own. */
|
|
189
|
+
export interface SelectorNames {
|
|
190
|
+
id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
}
|
|
193
|
+
export interface ResolvedPlaylist {
|
|
194
|
+
playlist: PlaylistItem;
|
|
195
|
+
/** Chain warnings raised while building the tree this playlist came from. */
|
|
196
|
+
warnings: string[];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Turns "id or name" into one specific playlist, or an actionable error.
|
|
200
|
+
*
|
|
201
|
+
* Ambiguity is never resolved by picking: two playlists really can share a
|
|
202
|
+
* name, and quietly answering about the wrong one is the failure mode this
|
|
203
|
+
* whole function exists to prevent. The error names every candidate with its
|
|
204
|
+
* id and its full path, so the retry is a copy-paste rather than a guess.
|
|
205
|
+
*
|
|
206
|
+
* Deliberately reuses `invalid_argument` rather than adding an error code:
|
|
207
|
+
* the taxonomy is closed (see errors.ts), and "the playlist you named is not
|
|
208
|
+
* in this library" is a problem with the argument, reported the same way an
|
|
209
|
+
* unknown field name is -- with the recognised values in `detail`.
|
|
210
|
+
*/
|
|
211
|
+
export declare function resolvePlaylist(qp: QueryProcess, sel: PlaylistSelector, names?: SelectorNames): Promise<ResolvedPlaylist | EngineError>;
|
|
212
|
+
/** One entry of a playlist, in playlist order. */
|
|
213
|
+
export interface OrderedEntry extends Linked {
|
|
214
|
+
/**
|
|
215
|
+
* `PlaylistEntity.trackId` — the track's `originTrackId` in the library
|
|
216
|
+
* `databaseUuid` names, **not** a local `Track.id`. The two halves only
|
|
217
|
+
* mean anything together (see ENTRY_TRACK_MATCH), and together they need
|
|
218
|
+
* not name a track this library holds.
|
|
219
|
+
*/
|
|
220
|
+
trackId: number;
|
|
221
|
+
/** The library this entry was made in. Null only in a malformed row. */
|
|
222
|
+
databaseUuid: string | null;
|
|
223
|
+
/** 1-based position within the playlist. */
|
|
224
|
+
position: number;
|
|
225
|
+
}
|
|
226
|
+
export interface PlaylistEntries {
|
|
227
|
+
entries: OrderedEntry[];
|
|
228
|
+
warnings: string[];
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Every entry of one playlist, in `nextEntityId` chain order.
|
|
232
|
+
*
|
|
233
|
+
* The whole playlist is read before a page is cut from it, because position
|
|
234
|
+
* *is* the ordering: there is no indexed column to seek on, so a page can
|
|
235
|
+
* only be taken from an already-ordered list. That is affordable -- the
|
|
236
|
+
* reference library's largest playlist is 43 entries and `PlaylistEntity` is
|
|
237
|
+
* three integers wide -- and it is bounded by MAX_ENTRIES rather than by
|
|
238
|
+
* hope.
|
|
239
|
+
*/
|
|
240
|
+
export declare function loadPlaylistEntries(qp: QueryProcess, playlist: PlaylistItem): Promise<PlaylistEntries | EngineError>;
|