engine-dj-mcp 0.15.0 → 0.17.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.
@@ -32,6 +32,12 @@ export const FIELD_SQL = {
32
32
  label: "t.label",
33
33
  year: "t.year",
34
34
  rating: "t.rating",
35
+ // Engine stores 0, 20, 40, 60, 80, 100 -- measured: four stars set in Engine
36
+ // came back as 80. `rating` hands that back as it is; `rating_stars` is the
37
+ // same thing in the units a person uses, so a caller never has to know the
38
+ // factor. A value no Engine writes (a third-party tagger's 55) rounds to the
39
+ // nearest star here and stays exact in `rating`.
40
+ rating_stars: "CAST(ROUND(COALESCE(t.rating, 0) / 20.0) AS INTEGER)",
35
41
  length: "t.length",
36
42
  path: "t.path",
37
43
  filename: "t.filename",
@@ -296,13 +302,16 @@ export async function searchTracks(qp, raw) {
296
302
  filterParams.push(input.key.mode === "minor" ? "%A" : "%B");
297
303
  }
298
304
  }
305
+ // In stars, which is what README has always documented and what a person
306
+ // means. Compared against the raw column, `min: 4` matched 20, 40, 60, 80
307
+ // and 100 -- every rated track -- and `max: 3` matched only unrated ones.
299
308
  if (input.rating?.min !== undefined) {
300
309
  filterWhere.push("t.rating >= ?");
301
- filterParams.push(input.rating.min);
310
+ filterParams.push(input.rating.min * 20);
302
311
  }
303
312
  if (input.rating?.max !== undefined) {
304
313
  filterWhere.push("t.rating <= ?");
305
- filterParams.push(input.rating.max);
314
+ filterParams.push(input.rating.max * 20);
306
315
  }
307
316
  if (input.played?.never)
308
317
  filterWhere.push("(t.timeLastPlayed IS NULL OR t.isPlayed = 0)");
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { type TrackUpdate } from "../store/track-metadata-plan.js";
3
+ export declare const UpdateTrackMetadataInput: z.ZodObject<{
4
+ updates: z.ZodArray<z.ZodObject<{
5
+ id: z.ZodNumber;
6
+ genre: z.ZodOptional<z.ZodString>;
7
+ comment: z.ZodOptional<z.ZodString>;
8
+ label: z.ZodOptional<z.ZodString>;
9
+ year: z.ZodOptional<z.ZodNumber>;
10
+ rating_stars: z.ZodOptional<z.ZodNumber>;
11
+ rating_raw: z.ZodOptional<z.ZodNumber>;
12
+ expect: z.ZodOptional<z.ZodObject<{
13
+ genre: z.ZodOptional<z.ZodString>;
14
+ comment: z.ZodOptional<z.ZodString>;
15
+ label: z.ZodOptional<z.ZodString>;
16
+ year: z.ZodOptional<z.ZodNumber>;
17
+ rating_raw: z.ZodOptional<z.ZodNumber>;
18
+ }, z.core.$strict>>;
19
+ }, z.core.$strict>>;
20
+ }, z.core.$strip>;
21
+ export declare function runUpdateTrackMetadata(mdbPath: string, uuid: string, args: {
22
+ updates: TrackUpdate[];
23
+ }, backupDir: string): Promise<import("../errors.js").EngineError | (import("../store/track-metadata.js").TrackMetadataResult & {
24
+ library: import("../store/write.js").LibraryRef;
25
+ backup_path?: string;
26
+ })>;
@@ -0,0 +1,42 @@
1
+ // src/tools/write-track-metadata.ts
2
+ import { z } from "zod";
3
+ import { updateTrackMetadata } from "../store/track-metadata.js";
4
+ import { MAX_UPDATES } from "../store/track-metadata-plan.js";
5
+ /**
6
+ * Types and the per-call cap only (spec §7.1). Every other rule -- ranges,
7
+ * which depend on whether `expect` is present, and the rating_raw restore
8
+ * rule -- lives in the store, so it comes back as a structured
9
+ * invalid_argument instead of a bare SDK validation message. `.strict()` makes
10
+ * a misnamed field such as `rating` an error rather than silently ignored.
11
+ */
12
+ const Expect = z
13
+ .object({
14
+ genre: z.string().optional(),
15
+ comment: z.string().optional(),
16
+ label: z.string().optional(),
17
+ year: z.number().int().optional(),
18
+ rating_raw: z.number().int().optional(),
19
+ })
20
+ .strict();
21
+ export const UpdateTrackMetadataInput = z.object({
22
+ updates: z
23
+ .array(z
24
+ .object({
25
+ id: z.number().int().positive(),
26
+ genre: z.string().optional(),
27
+ comment: z.string().optional(),
28
+ label: z.string().optional(),
29
+ year: z.number().int().optional(),
30
+ rating_stars: z.number().int().optional(),
31
+ rating_raw: z.number().int().optional(),
32
+ expect: Expect.optional(),
33
+ })
34
+ .strict())
35
+ .max(MAX_UPDATES)
36
+ .describe("One entry per track: its id from search_tracks or get_tracks, and only the fields to change. " +
37
+ '"" clears a text field. rating_stars is 0-5. rating_raw and expect exist for replaying an undo; ' +
38
+ "you do not need them for an ordinary edit."),
39
+ });
40
+ export async function runUpdateTrackMetadata(mdbPath, uuid, args, backupDir) {
41
+ return updateTrackMetadata(mdbPath, uuid, { updates: args.updates }, { backupDir });
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engine-dj-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "MCP server for an Engine DJ library: search and audit it, read cues and beatgrids, and build playlists when you ask. Not affiliated with inMusic or Denon DJ.",
5
5
  "keywords": [
6
6
  "mcp",