deep-roon-mcp 0.3.0 → 0.3.6

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.
@@ -15525,7 +15525,7 @@ var require_lib2 = __commonJS({
15525
15525
  var version2;
15526
15526
  var init_package = __esm({
15527
15527
  "package.json"() {
15528
- version2 = "0.3.0";
15528
+ version2 = "0.3.6";
15529
15529
  }
15530
15530
  });
15531
15531
 
@@ -15852,6 +15852,11 @@ function field2(o, name) {
15852
15852
  for (const [k, v] of Object.entries(f)) if (k.endsWith("::" + name)) return v;
15853
15853
  return void 0;
15854
15854
  }
15855
+ function imageKey(c, o, name = "Image") {
15856
+ const url2 = field2(obj(c, field2(o, name)), "Url");
15857
+ const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(url2 ?? ""));
15858
+ return m ? m[1] : void 0;
15859
+ }
15855
15860
  function dateParts(v) {
15856
15861
  if (!Buffer.isBuffer(v) || v.length < 2) return void 0;
15857
15862
  const n = new BinaryReader(Uint8Array.from(v)).flexInt();
@@ -15953,6 +15958,8 @@ function albumRow(c, o) {
15953
15958
  if (flag(field2(o, "IsListenLater"))) r.listen_later = true;
15954
15959
  const p = field2(o, "Popularity");
15955
15960
  if (typeof p === "number") r.popularity = Math.round(p * 100) / 100;
15961
+ const img = imageKey(c, o);
15962
+ if (img) r.image = img;
15956
15963
  return r;
15957
15964
  }
15958
15965
  function resultRow(c, o) {
@@ -15962,6 +15969,8 @@ function resultRow(c, o) {
15962
15969
  const r = { kind: "artist", key: String(field2(o, "PerformerId") ?? ""), name: field2(o, "Name") ?? "" };
15963
15970
  const n = field2(o, "MainPerformerAlbumCount");
15964
15971
  if (typeof n === "number" && n > 0) r.albums_in_library = n;
15972
+ const img = imageKey(c, o, "Photo") ?? imageKey(c, o, "Avatar");
15973
+ if (img) r.image = img;
15965
15974
  return r;
15966
15975
  }
15967
15976
  if (t === "TrackLite" || t === "Track") {
@@ -15976,6 +15985,8 @@ function resultRow(c, o) {
15976
15985
  const l = field2(o, "LengthSeconds");
15977
15986
  if (typeof l === "number") r.length = duration3(l);
15978
15987
  r.source = sourceOf(o, al) ?? "library";
15988
+ const img = imageKey(c, al) ?? imageKey(c, o);
15989
+ if (img) r.image = img;
15979
15990
  return r;
15980
15991
  }
15981
15992
  return { kind: t, title: field2(o, "Title") ?? field2(o, "Name") ?? "" };
@@ -15996,6 +16007,7 @@ var init_graph = __esm({
15996
16007
  return (o?.fields?.$items ?? o?.$items ?? []).filter((x) => typeof x === "string");
15997
16008
  }, "strings");
15998
16009
  flag = /* @__PURE__ */ __name((v) => v === true || Buffer.isBuffer(v) && v.length > 1 && v[0] >= 1, "flag");
16010
+ __name(imageKey, "imageKey");
15999
16011
  __name(dateParts, "dateParts");
16000
16012
  yearOf = /* @__PURE__ */ __name((v) => dateParts(v)?.[0], "yearOf");
16001
16013
  __name(dateText, "dateText");
@@ -16357,6 +16369,8 @@ async function bio(key, max, wiki) {
16357
16369
  }
16358
16370
  const g = list("Genres").filter((x) => typeof x === "string");
16359
16371
  if (g.length) artist.genres = g;
16372
+ const img = imageKey(c, p, "Photo") ?? imageKey(c, p, "Avatar");
16373
+ if (img) artist.image = img;
16360
16374
  return {
16361
16375
  artist,
16362
16376
  texts,
@@ -16388,6 +16402,8 @@ async function discography(key, wanted) {
16388
16402
  if (typeof n === "number") artist.albums_in_library = n;
16389
16403
  const from = field2(p, "BirthPlace");
16390
16404
  if (from) artist.from = from;
16405
+ const img = imageKey(c, p, "Photo") ?? imageKey(c, p, "Avatar");
16406
+ if (img) artist.image = img;
16391
16407
  return { service: SERVICE_NAME[service2], artist, albums };
16392
16408
  });
16393
16409
  }
@@ -49384,6 +49400,8 @@ async function nowPlaying() {
49384
49400
  r.volume_max = v.max;
49385
49401
  if (v.is_muted) r.muted = true;
49386
49402
  }
49403
+ const img = off?.now_playing?.image_key;
49404
+ if (img) r.image = img;
49387
49405
  const outs = c.graph.findByType("Endpoint").filter((e) => {
49388
49406
  const zz = field2(e, "Zone");
49389
49407
  return isRef2(zz) && String(zz.$ref) === String(z2.oid);
@@ -49397,17 +49415,23 @@ async function nowPlaying() {
49397
49415
  if (r.state === "stopped") return r;
49398
49416
  const np = obj(c, field2(z2, "NowPlaying"));
49399
49417
  const tr = obj(c, field2(items(c, field2(np, "Tracks"))[0], "Track"));
49400
- if (tr && field2(tr, "Title")) {
49418
+ const lines = off?.now_playing?.three_line;
49419
+ const t = String(field2(tr, "Title") ?? "").toLowerCase(), l1 = String(lines?.line1 ?? "").toLowerCase();
49420
+ const stream = !!l1 && !!t && !l1.includes(t) && !t.includes(l1);
49421
+ if (tr && field2(tr, "Title") && !stream) {
49401
49422
  const al = obj(c, field2(tr, "Album"));
49402
49423
  r.title = field2(tr, "Title");
49403
49424
  r.artist = clean(field2(al, "PerformedBy"));
49404
49425
  r.album = field2(al, "Title");
49426
+ const tk = field2(tr, "TrackId");
49427
+ if (tk !== void 0 && tk !== null) r.track_key = String(tk);
49428
+ const ak = field2(al, "AlbumId");
49429
+ if (ak !== void 0 && ak !== null) r.album_key = String(ak);
49405
49430
  const f = field2(tr, "Format");
49406
49431
  if (f) r.format = f;
49407
49432
  const l = field2(tr, "LengthSeconds");
49408
49433
  if (typeof l === "number") r.length = duration3(l);
49409
49434
  } else {
49410
- const lines = off?.now_playing?.three_line;
49411
49435
  if (lines?.line1) {
49412
49436
  r.title = lines.line1;
49413
49437
  if (lines.line2) r.artist = lines.line2;
@@ -49418,7 +49442,7 @@ async function nowPlaying() {
49418
49442
  const pos = field2(z2, "SeekPosition");
49419
49443
  if (typeof pos === "number") r.position = duration3(pos);
49420
49444
  const left = field2(obj(c, field2(z2, "Queue")), "TrackCountRemaining");
49421
- if (typeof left === "number") r.tracks_left_in_queue = left;
49445
+ if (typeof left === "number" && !r.source) r.tracks_left_in_queue = left;
49422
49446
  return r;
49423
49447
  });
49424
49448
  return { zones: out };
@@ -49659,7 +49683,7 @@ function registerReadTools(server, internal = true) {
49659
49683
  const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
49660
49684
  reg("roon_search", {
49661
49685
  title: "Search Roon",
49662
- description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
49686
+ description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Rows carry `image`, the key of the cover: the Core serves it at http://<core address>:9330/image/<key>.jpg, or scaled at /image/<key>.300.jpg (for clients that draw; a chat shows covers with roon_cover). The `image` of roon_now_playing is an official key, served at /api/image/<key>?scale=fit&width=600&height=600. Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
49663
49687
  inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
49664
49688
  annotations: READ
49665
49689
  }, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
@@ -49866,6 +49890,14 @@ async function playTracks(keys, zoneName, how) {
49866
49890
  __name(playTracks, "playTracks");
49867
49891
  var BUTTONS = { pause: "Pause", play: "Play", playpause: "PlayPause", stop: "Stop", next: "Next", previous: "Previous" };
49868
49892
  async function control2(zoneName, action) {
49893
+ if (action === "release") {
49894
+ return withCore(async (c) => {
49895
+ const z2 = findZone(c, zoneName);
49896
+ c.remoting.callMethodNoReply(z2.oid, formatMethodSignature("Zone", "Suspend", []), Buffer.alloc(0));
49897
+ log(`control: Suspend on ${z2.name}`);
49898
+ return { ok: true, zone: z2.name, action };
49899
+ });
49900
+ }
49869
49901
  const m = BUTTONS[action];
49870
49902
  if (!m) throw new RoonError(`Unknown action; choose from ${Object.keys(BUTTONS).join(", ")}`, "invalid");
49871
49903
  const r = await withCore(async (c) => {
@@ -49948,6 +49980,10 @@ function queueRow(c, oid) {
49948
49980
  if (album2) r.album = album2;
49949
49981
  const l = field2(tr, "LengthSeconds");
49950
49982
  if (typeof l === "number") r.length = duration3(l);
49983
+ const k = field2(tr, "TrackId");
49984
+ if (k !== void 0 && k !== null) r.key = String(k);
49985
+ const img = imageKey(c, al) ?? imageKey(c, tr);
49986
+ if (img) r.image = img;
49951
49987
  return r;
49952
49988
  }
49953
49989
  __name(queueRow, "queueRow");
@@ -50128,10 +50164,10 @@ function registerControlTools(server, internal = true) {
50128
50164
  }, ({ keys, zone: zn, mode: m }) => run(() => playTracks(keys, zn, m ?? "now")));
50129
50165
  reg("roon_control", {
50130
50166
  title: "Transport",
50131
- description: "The buttons of a Roon zone: pause, play (resume), playpause, stop, next, previous. Only on an explicit request, never as a probe.",
50132
- inputSchema: { zone, action: external_exports.enum(["pause", "play", "playpause", "stop", "next", "previous"]) },
50167
+ description: "The buttons of a Roon zone: pause, play (resume), playpause, stop, next, previous \u2014 and release: let go of the audio device right away (what Roon does by itself a few seconds after a pause), so another source can use the speaker. Only on an explicit request, never as a probe.",
50168
+ inputSchema: { zone, action: external_exports.enum(["pause", "play", "playpause", "stop", "next", "previous", "release"]) },
50133
50169
  annotations: PLAYS
50134
- }, ({ zone: zn, action }) => run(() => internal ? control2(zn, action) : control(zn, action)));
50170
+ }, ({ zone: zn, action }) => run(() => internal ? control2(zn, action) : action === "release" ? Promise.reject(new Error("release needs Roon's internal protocol (switched off)")) : control(zn, action)));
50135
50171
  reg("roon_seek", {
50136
50172
  title: "Seek",
50137
50173
  description: "Jump to a position in the track that plays on a Roon zone, in seconds from the start. Only on an explicit request.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deep-roon-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.6",
4
4
  "description": "An MCP server for Roon that reaches the parts the official API doesn't: reviews, biographies, Qobuz's and TIDAL's shelves, TIDAL's playlists, your history, search across your library and the streaming services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "René Bouwmeester <21277119+renebouwmeester@users.noreply.github.com>",