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/server.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { discoverLibraries, defaultRoots, probeLibraries } from "./discovery.js";
|
|
7
|
-
import { libraryCandidates, sidecarDir } from "./paths.js";
|
|
7
|
+
import { libraryCandidates, libraryTag, sidecarDir } from "./paths.js";
|
|
8
8
|
import { LibraryArg, findLibrary, libraryNotFound, pickDefaultLibrary, } from "./library-select.js";
|
|
9
9
|
import { hasHotJournal } from "./store/connections.js";
|
|
10
10
|
import { QueryProcess } from "./proc/query-client.js";
|
|
11
11
|
import { IndexManager } from "./store/index-manager.js";
|
|
12
12
|
import { searchTracks, SearchInput } from "./tools/search.js";
|
|
13
13
|
import { getTracks, GetTracksInput } from "./tools/tracks.js";
|
|
14
|
+
import { getPlaylists, GetPlaylistsInput, getPlaylistTracks, GetPlaylistTracksInput, } from "./tools/playlists.js";
|
|
14
15
|
import { getTrackPerformance, PerformanceInput } from "./tools/performance.js";
|
|
15
16
|
import { auditLibrary, AuditInput, AUDIT_CHECKS } from "./tools/audit.js";
|
|
16
17
|
import { runSql, RunSqlInput } from "./tools/sql.js";
|
|
17
18
|
import { listLibraries } from "./tools/libraries.js";
|
|
18
19
|
import { refreshIndex } from "./tools/refresh.js";
|
|
20
|
+
import { CreatePlaylistInput, runCreatePlaylist } from "./tools/write-playlist.js";
|
|
19
21
|
import { err, isEngineError, libraryNeedsRecovery } from "./errors.js";
|
|
20
22
|
const RO = { readOnlyHint: true, destructiveHint: false, idempotentHint: true };
|
|
23
|
+
const RW = { readOnlyHint: false, destructiveHint: false, idempotentHint: false };
|
|
21
24
|
/**
|
|
22
25
|
* name/version reported to every client on initialize. Read from
|
|
23
26
|
* package.json rather than typed here, so the two cannot re-diverge the way
|
|
@@ -62,8 +65,10 @@ function reply(value) {
|
|
|
62
65
|
* This walks the same candidate paths independently, purely to tell those
|
|
63
66
|
* two cases apart, so `ready()` below can report library_needs_recovery
|
|
64
67
|
* instead of the misleading library_not_found -- never to open the file:
|
|
65
|
-
* recovering a hot journal requires a write, and
|
|
66
|
-
* to
|
|
68
|
+
* recovering a hot journal requires a write, and nothing here opens a
|
|
69
|
+
* library writably to heal one. Not even create_playlist, which refuses a
|
|
70
|
+
* library in this state outright (store/write.ts) rather than letting
|
|
71
|
+
* SQLite roll the journal forward on its way in.
|
|
67
72
|
*/
|
|
68
73
|
export function findHotJournalCandidate(roots) {
|
|
69
74
|
for (const root of roots) {
|
|
@@ -151,8 +156,7 @@ export async function createServer(opts = {}) {
|
|
|
151
156
|
const first = knownList().find((l) => l.uuid === lib.uuid);
|
|
152
157
|
if (!first || first.path === lib.path)
|
|
153
158
|
return opts.sidecarBaseDir;
|
|
154
|
-
|
|
155
|
-
return join(opts.sidecarBaseDir ?? sidecarDir(""), "duplicate-uuid", tag);
|
|
159
|
+
return join(opts.sidecarBaseDir ?? sidecarDir(""), "duplicate-uuid", libraryTag(lib.path));
|
|
156
160
|
};
|
|
157
161
|
/** Lazily creates -- and thereafter reuses -- one query child per library. */
|
|
158
162
|
const stateFor = (lib) => {
|
|
@@ -254,6 +258,8 @@ export async function createServer(opts = {}) {
|
|
|
254
258
|
"flags.has_cues means a hot cue is actually set (the blob is decoded when the index " +
|
|
255
259
|
"is built), not merely that Engine analysed the track; flags.has_beatgrid means a " +
|
|
256
260
|
"beatData blob is present. " +
|
|
261
|
+
"playlist: {id} or {name} narrows the search to one playlist -- results still come " +
|
|
262
|
+
"back by relevance or id, not in playlist order; use get_playlist_tracks for that. " +
|
|
257
263
|
LIBRARY_SELECTION_NOTE,
|
|
258
264
|
inputSchema: { ...SearchInput.shape, library: LibraryArg },
|
|
259
265
|
annotations: RO,
|
|
@@ -275,19 +281,64 @@ export async function createServer(opts = {}) {
|
|
|
275
281
|
return reply(state);
|
|
276
282
|
return reply(await getTracks(state.qp, args));
|
|
277
283
|
});
|
|
284
|
+
server.registerTool("get_playlists", {
|
|
285
|
+
title: "List playlists",
|
|
286
|
+
description: "The library's playlist tree, in the order Engine DJ displays it -- taken from the " +
|
|
287
|
+
"Playlist.nextListId chain, which is where that order actually lives (the PlaylistPath " +
|
|
288
|
+
"view's `position` column runs the other way). " +
|
|
289
|
+
"Flat and in pre-order, so reading top to bottom is exactly the sidebar: `depth` and " +
|
|
290
|
+
"`path` carry the nesting, `parent_id` names the folder. " +
|
|
291
|
+
"is_folder means the list has child lists (Engine has no folder flag; a folder is a " +
|
|
292
|
+
"playlist other playlists sit under), so an emptied folder reads as an empty playlist. " +
|
|
293
|
+
"track_count is entries in that list alone, never rolled up from children, and " +
|
|
294
|
+
"missing_count is how many of them name a track that is not in this library. " +
|
|
295
|
+
"`warnings` appears when a link chain is broken or cyclic; nothing is ever dropped " +
|
|
296
|
+
"from the list because of one. " +
|
|
297
|
+
LIBRARY_SELECTION_NOTE,
|
|
298
|
+
inputSchema: { ...GetPlaylistsInput.shape, library: LibraryArg },
|
|
299
|
+
annotations: RO,
|
|
300
|
+
}, async (args) => {
|
|
301
|
+
const state = await acquire(args.library);
|
|
302
|
+
if (isEngineError(state))
|
|
303
|
+
return reply(state);
|
|
304
|
+
return reply(await getPlaylists(state.qp, args));
|
|
305
|
+
});
|
|
306
|
+
server.registerTool("get_playlist_tracks", {
|
|
307
|
+
title: "Get the tracks in a playlist",
|
|
308
|
+
description: "The tracks of one playlist, in playlist order -- from the PlaylistEntity.nextEntityId " +
|
|
309
|
+
"chain, not from row ids, so a track dragged up the list comes back where the DJ put it. " +
|
|
310
|
+
"Name the playlist with playlist_id, or with playlist_name (exactly one of the two). A " +
|
|
311
|
+
"name that matches several playlists is refused with every candidate's id and full path " +
|
|
312
|
+
"rather than picked between -- names are unique only within a folder, so pass the full " +
|
|
313
|
+
"`path` from get_playlists to disambiguate. " +
|
|
314
|
+
"Each row carries `position`, its 1-based place in the playlist. An entry whose track is " +
|
|
315
|
+
"not in this library comes back as { position, entry_id, track_id, missing: true } and " +
|
|
316
|
+
"keeps its slot, so entry_count still matches the playlist's own length; missing_count " +
|
|
317
|
+
"says how many of those there are. That is ordinary, not corruption -- entries outlive " +
|
|
318
|
+
"their tracks and arrive from other drives (see audit_library's orphan_entries). " +
|
|
319
|
+
"Same fields, limit and cursor conventions as search_tracks. " +
|
|
320
|
+
LIBRARY_SELECTION_NOTE,
|
|
321
|
+
inputSchema: { ...GetPlaylistTracksInput.shape, library: LibraryArg },
|
|
322
|
+
annotations: RO,
|
|
323
|
+
}, async (args) => {
|
|
324
|
+
const state = await acquire(args.library);
|
|
325
|
+
if (isEngineError(state))
|
|
326
|
+
return reply(state);
|
|
327
|
+
return reply(await getPlaylistTracks(state.qp, args));
|
|
328
|
+
});
|
|
278
329
|
server.registerTool("get_track_performance", {
|
|
279
330
|
title: "Get cues, loops and beatgrid",
|
|
280
331
|
description: "Decode PerformanceData for one track: hot cues, the main cue, saved loops, the " +
|
|
281
332
|
"beatgrid and a coarse waveform profile. Each field carries its own decode status " +
|
|
282
333
|
"and its own layout marker. " +
|
|
283
|
-
"layout: \"verified\" (
|
|
334
|
+
"layout: \"verified\" (every field) means the binary layout was " +
|
|
284
335
|
"confirmed against a real Engine DJ library -- cue positions land inside the track, " +
|
|
285
|
-
"the beatgrid's implied tempo matches the analysed BPM,
|
|
286
|
-
"point spacing multiplies back out to the track's sample count
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
336
|
+
"the beatgrid's implied tempo matches the analysed BPM, the waveform's declared " +
|
|
337
|
+
"point spacing multiplies back out to the track's sample count, and a saved loop " +
|
|
338
|
+
"spans a whole number of beats at that same analysed BPM -- so status: \"ok\" " +
|
|
339
|
+
"is a claim about the values, not just about the parse. " +
|
|
340
|
+
"layout: \"unverified\" would mean only that the bytes parsed; no field returns it " +
|
|
341
|
+
"today. " +
|
|
291
342
|
"Positions are sample offsets; sample_rate at the top level converts them to " +
|
|
292
343
|
"seconds, and cue/loop items carry the seconds already. Only hot-cue and loop slots " +
|
|
293
344
|
"that hold something are listed -- slots is how many the track has in total, so " +
|
|
@@ -362,6 +413,40 @@ export async function createServer(opts = {}) {
|
|
|
362
413
|
return reply(lib);
|
|
363
414
|
return reply(await refreshIndex(stateFor(lib).mgr));
|
|
364
415
|
});
|
|
416
|
+
// Registered only under --allow-writes. A client that never enables it sees
|
|
417
|
+
// exactly the read-only server it saw before this feature existed, which is
|
|
418
|
+
// what keeps the README's promise true by default.
|
|
419
|
+
if (opts.allowWrites) {
|
|
420
|
+
server.registerTool("create_playlist", {
|
|
421
|
+
title: "Create a playlist",
|
|
422
|
+
description: "Create a new playlist in this Engine DJ library from track ids returned by " +
|
|
423
|
+
"search_tracks or get_tracks -- track_ids sets both membership and order. " +
|
|
424
|
+
"Unlike every other tool here, this WRITES to the library, so do not call it " +
|
|
425
|
+
"speculatively: only call it once you actually intend to add the playlist. " +
|
|
426
|
+
"To undo it, delete the playlist in Engine DJ -- Engine's own delete trigger and " +
|
|
427
|
+
"cascade remove the playlist and its entries cleanly. " +
|
|
428
|
+
"backup_path in the result names a whole-database snapshot taken before the first " +
|
|
429
|
+
"write of this session; it is a recovery route for a damaged library, NOT an undo. " +
|
|
430
|
+
"Restoring it reverts the entire library to that moment, discarding everything " +
|
|
431
|
+
"Engine DJ has written since (play counts, imports, cue and beatgrid edits). " +
|
|
432
|
+
"No existing playlist or entry is ever modified; this only adds a new one. " +
|
|
433
|
+
"Fails with playlist_exists if a top-level playlist already has that title, and " +
|
|
434
|
+
"with library_busy if Engine DJ or a player is holding a conflicting lock on the " +
|
|
435
|
+
"library right then -- nothing is written in that case, so retry rather than " +
|
|
436
|
+
"treating it as permanent. On any error, `detail` is \"not_committed\" when the " +
|
|
437
|
+
"library is unchanged and \"committed_unverified\" when the write may have gone " +
|
|
438
|
+
"through but could not be verified. track_ids may be empty (an empty playlist); a " +
|
|
439
|
+
"track id may appear at most once. " +
|
|
440
|
+
LIBRARY_SELECTION_NOTE,
|
|
441
|
+
inputSchema: { ...CreatePlaylistInput.shape, library: LibraryArg },
|
|
442
|
+
annotations: RW,
|
|
443
|
+
}, async (args) => {
|
|
444
|
+
const state = await acquire(args.library);
|
|
445
|
+
if (isEngineError(state))
|
|
446
|
+
return reply(state);
|
|
447
|
+
return reply(await runCreatePlaylist(state.lib.path, state.lib.uuid, args, join(homedir(), ".engine-dj-mcp", "backups")));
|
|
448
|
+
});
|
|
449
|
+
}
|
|
365
450
|
/**
|
|
366
451
|
* There was previously no way to shut this down at all: createServer
|
|
367
452
|
* forked a query child and handed back an McpServer whose close() knows
|
|
@@ -400,9 +485,11 @@ Tables live in \`m.db\` (attached as \`main\`); the search index lives in \`side
|
|
|
400
485
|
More than one library can be connected at once — the local one under
|
|
401
486
|
\`~/Music\` and one per USB drive. \`list_libraries\` reports each with a
|
|
402
487
|
\`uuid\` and a \`path\`, and every tool that reads library data
|
|
403
|
-
(\`search_tracks\`, \`get_tracks\`, \`
|
|
404
|
-
\`
|
|
405
|
-
\`
|
|
488
|
+
(\`search_tracks\`, \`get_tracks\`, \`get_playlists\`,
|
|
489
|
+
\`get_playlist_tracks\`, \`get_track_performance\`, \`audit_library\`,
|
|
490
|
+
\`run_sql\`, \`refresh_index\`) takes an optional
|
|
491
|
+
\`library\` argument naming one of them — as does \`create_playlist\`, when
|
|
492
|
+
the server was started with \`--allow-writes\` — either the \`uuid\` or the
|
|
406
493
|
\`path\`, in the \`~/...\` form \`list_libraries\` prints or the absolute
|
|
407
494
|
one. A value matching neither comes back as \`library_not_found\` listing
|
|
408
495
|
the libraries that are selectable.
|
|
@@ -430,8 +517,6 @@ other.
|
|
|
430
517
|
- \`Track.path\` is relative to the \`Engine Library\` folder and usually
|
|
431
518
|
contains \`..\`. The SQL function \`abs_path(path)\` resolves it against
|
|
432
519
|
this library's location; the home prefix comes back folded to \`~\`.
|
|
433
|
-
- Playlists are singly linked lists: order lives in \`Playlist.nextListId\`
|
|
434
|
-
and \`PlaylistEntity.nextEntityId\`, not in any position column.
|
|
435
520
|
- A track's natural key across drives is \`(originDatabaseUuid, originTrackId)\`.
|
|
436
521
|
- \`PerformanceData\`'s blob columns are binary and cannot be read with SQL.
|
|
437
522
|
Engine writes \`quickCues\`, \`loops\`, \`beatData\` and
|
|
@@ -441,6 +526,41 @@ other.
|
|
|
441
526
|
\`get_track_performance\` decodes one track's blobs; for the whole library,
|
|
442
527
|
\`side.track_derived.has_cues\` below holds the decoded answer.
|
|
443
528
|
|
|
529
|
+
## Playlists
|
|
530
|
+
Order is a **singly linked list**, in both directions of the structure, and
|
|
531
|
+
there is no position column anywhere:
|
|
532
|
+
- \`Playlist.nextListId\` orders sibling lists within one \`parentListId\`.
|
|
533
|
+
- \`PlaylistEntity.nextEntityId\` orders the entries within one \`listId\`.
|
|
534
|
+
Both chains terminate at \`0\`.
|
|
535
|
+
|
|
536
|
+
The schema ships a \`PlaylistPath\` view with a column named \`position\`.
|
|
537
|
+
**Do not use it for display order.** Its \`OrderedList\` CTE anchors on
|
|
538
|
+
\`WHERE nextListId = 0\` and counts upwards from the *tail*, so ordering by
|
|
539
|
+
it yields the sidebar reversed — measured on a real library of 16 playlists,
|
|
540
|
+
\`PlaylistPath\` order is exactly the reverse of the chain, and the chain is
|
|
541
|
+
what Engine DJ draws. \`get_playlists\` and \`get_playlist_tracks\` walk the
|
|
542
|
+
chains; write the same recursive walk if you go via \`run_sql\`, and never
|
|
543
|
+
\`ORDER BY id\` — ids happen to agree with the chain until the first time
|
|
544
|
+
someone drags a track up a playlist.
|
|
545
|
+
|
|
546
|
+
Nesting is \`Playlist.parentListId\` (\`0\` at the top level). There is no
|
|
547
|
+
folder flag: an Engine folder is just a \`Playlist\` row that other rows
|
|
548
|
+
name as their parent. \`Playlist.isPersisted\` marks a list saved to the
|
|
549
|
+
device rather than a transient one; both values occur on lists Engine
|
|
550
|
+
displays, so nothing is filtered on it. Titles are unique only within a
|
|
551
|
+
parent (\`UNIQUE (title, parentListId)\`), so a bare name can match several
|
|
552
|
+
playlists — the full \`path\` \`get_playlists\` reports is unique.
|
|
553
|
+
|
|
554
|
+
\`PlaylistEntity.trackId\` may name a track that is not in this library:
|
|
555
|
+
entries outlive their tracks and travel between drives (\`databaseUuid\`
|
|
556
|
+
records which library an entry came from). On the reference library 105 of
|
|
557
|
+
202 entries are such holes. \`audit_library\`'s \`orphan_entries\` counts
|
|
558
|
+
them; \`get_playlist_tracks\` keeps them in place, flagged \`missing\`.
|
|
559
|
+
|
|
560
|
+
Rule-based **smartlists** live in a separate \`Smartlist\` table, keyed by
|
|
561
|
+
uuid with its own \`nextListUuid\` ordering and a JSON \`rules\` column.
|
|
562
|
+
Nothing here reads it — a smartlist is not reported by \`get_playlists\`.
|
|
563
|
+
|
|
444
564
|
## SQL functions
|
|
445
565
|
Registered on the query connection, all deterministic:
|
|
446
566
|
\`camelot(key)\`, \`key_name(key)\`, \`tempo(bpmAnalyzed, bpm)\`,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/store/backup.ts
|
|
2
|
+
//
|
|
3
|
+
// A snapshot taken before the first write of a session, so a user who does
|
|
4
|
+
// not like the result has a known way back. It is taken through SQLite's own
|
|
5
|
+
// backup API rather than a file copy: that is correct even while Engine DJ
|
|
6
|
+
// holds the database open, which a cp is not.
|
|
7
|
+
//
|
|
8
|
+
// Snapshots live under ~/.engine-dj-mcp/backups/ and never inside the user's
|
|
9
|
+
// Engine Library folder -- the rule that no file is created there holds for
|
|
10
|
+
// writes exactly as it did for reads.
|
|
11
|
+
import { DatabaseSync, backup } from "node:sqlite";
|
|
12
|
+
import { mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { err } from "../errors.js";
|
|
15
|
+
import { libraryTag } from "../paths.js";
|
|
16
|
+
/** How many snapshots to keep per library before deleting the oldest. */
|
|
17
|
+
const KEEP = 10;
|
|
18
|
+
/**
|
|
19
|
+
* Monotonic counter to ensure unique, sortable filenames even in tight loops.
|
|
20
|
+
* The ISO string alone provides only millisecond precision, so rapid-fire calls
|
|
21
|
+
* in the same millisecond would collide. The counter serves as an infallible
|
|
22
|
+
* tiebreaker: calls within the same millisecond are ordered by counter value,
|
|
23
|
+
* and calls across milliseconds are already separated by the ISO string. This
|
|
24
|
+
* ordering is essential: rotation immediately deletes the text-sorted oldest,
|
|
25
|
+
* so an inverted sort would delete the snapshot we just handed back.
|
|
26
|
+
*/
|
|
27
|
+
let counter = 0;
|
|
28
|
+
/**
|
|
29
|
+
* A filename-safe, sortable stamp. Sorting the directory listing as text
|
|
30
|
+
* therefore orders snapshots by age, which is what rotation relies on.
|
|
31
|
+
*/
|
|
32
|
+
function stamp() {
|
|
33
|
+
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
34
|
+
return `${iso}-${String(++counter).padStart(10, "0")}`;
|
|
35
|
+
}
|
|
36
|
+
export async function snapshotLibrary(mdbPath, uuid, baseDir) {
|
|
37
|
+
let src;
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(baseDir, { recursive: true });
|
|
40
|
+
src = new DatabaseSync(mdbPath, { readOnly: true });
|
|
41
|
+
// uuid *and* a hash of the file's own path. Keyed on uuid alone, a
|
|
42
|
+
// library and its clone on a second drive -- same uuid, different drive,
|
|
43
|
+
// an ordinary thing for a DJ to have -- shared one namespace and one
|
|
44
|
+
// KEEP-slot window: writes to either evicted the other's snapshots, and
|
|
45
|
+
// a returned backup_path did not say which drive it came from. This is
|
|
46
|
+
// the same tag server.ts's sidecarBaseFor uses to keep two such
|
|
47
|
+
// libraries' indexes apart (see paths.ts).
|
|
48
|
+
const prefix = `${uuid}-${libraryTag(mdbPath)}-`;
|
|
49
|
+
const dest = join(baseDir, `${prefix}${stamp()}.db`);
|
|
50
|
+
await backup(src, dest);
|
|
51
|
+
src.close();
|
|
52
|
+
src = undefined;
|
|
53
|
+
const mine = readdirSync(baseDir)
|
|
54
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith(".db"))
|
|
55
|
+
.sort();
|
|
56
|
+
for (const old of mine.slice(0, Math.max(0, mine.length - KEEP))) {
|
|
57
|
+
rmSync(join(baseDir, old), { force: true });
|
|
58
|
+
}
|
|
59
|
+
return dest;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return err("library_unreadable", `Could not snapshot ${mdbPath} before writing: ${String(e)}`);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
try {
|
|
66
|
+
src?.close();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* already closed */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type EngineError } from "../errors.js";
|
|
3
|
+
export interface CreatePlaylistResult {
|
|
4
|
+
playlist_id: number;
|
|
5
|
+
title: string;
|
|
6
|
+
tracks_added: number;
|
|
7
|
+
backup_path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface OriginRef {
|
|
10
|
+
uuid: string;
|
|
11
|
+
trackId: number;
|
|
12
|
+
}
|
|
13
|
+
/** Test seam only: forget this process's snapshots so a test can start clean. */
|
|
14
|
+
export declare function resetSessionSnapshots(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Read the chain back starting from a row we know is the head, because we
|
|
17
|
+
* inserted it first. Re-deriving the head as "the row nothing points at"
|
|
18
|
+
* would be the same assumption the write just made, so it could not catch a
|
|
19
|
+
* write that made it wrongly.
|
|
20
|
+
*
|
|
21
|
+
* This, together with `sameOrder`, confirms that the *links* survived the
|
|
22
|
+
* round trip in the order given -- it does not independently confirm the
|
|
23
|
+
* *values* are correct. The comparison target is `refs`, the same array
|
|
24
|
+
* `resolveOrigins` produced and the write consumed, so a `resolveOrigins`
|
|
25
|
+
* that resolved every id wrongly (e.g. to the local row id instead of the
|
|
26
|
+
* origin pair) would write wrong values, read the same wrong values back,
|
|
27
|
+
* and pass this check. Catching that class of bug is what the
|
|
28
|
+
* re-originated-track test is for, not this readback.
|
|
29
|
+
*/
|
|
30
|
+
export declare function walkFrom(db: DatabaseSync, listId: number, headId: number): OriginRef[];
|
|
31
|
+
export declare function sameOrder(a: OriginRef[], b: OriginRef[]): boolean;
|
|
32
|
+
export declare function createPlaylist(mdbPath: string, uuid: string, input: {
|
|
33
|
+
title: string;
|
|
34
|
+
trackIds: number[];
|
|
35
|
+
}, opts: {
|
|
36
|
+
backupDir: string;
|
|
37
|
+
}): Promise<CreatePlaylistResult | EngineError>;
|