deep-roon-mcp 0.3.8 → 0.4.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.
@@ -3635,12 +3635,12 @@ var require_utils = __commonJS({
3635
3635
  let output2 = "";
3636
3636
  for (let i = 0; i < input2.length; i++) {
3637
3637
  if (input2[i] === "%" && i + 2 < input2.length) {
3638
- const hex3 = input2.slice(i + 1, i + 3);
3639
- if (isHexPair(hex3)) {
3640
- const normalizedHex = hex3.toUpperCase();
3641
- const decoded3 = String.fromCharCode(parseInt(normalizedHex, 16));
3642
- if (decodeUnreserved && isUnreserved(decoded3)) {
3643
- output2 += decoded3;
3638
+ const hex4 = input2.slice(i + 1, i + 3);
3639
+ if (isHexPair(hex4)) {
3640
+ const normalizedHex = hex4.toUpperCase();
3641
+ const decoded4 = String.fromCharCode(parseInt(normalizedHex, 16));
3642
+ if (decodeUnreserved && isUnreserved(decoded4)) {
3643
+ output2 += decoded4;
3644
3644
  } else {
3645
3645
  output2 += "%" + normalizedHex;
3646
3646
  }
@@ -3658,12 +3658,12 @@ var require_utils = __commonJS({
3658
3658
  for (let i = 0; i < input2.length; i++) {
3659
3659
  const ch = input2[i];
3660
3660
  if (ch === "%" && i + 2 < input2.length) {
3661
- const hex3 = input2.slice(i + 1, i + 3);
3662
- if (isHexPair(hex3)) {
3663
- const normalizedHex = hex3.toUpperCase();
3664
- const decoded3 = String.fromCharCode(parseInt(normalizedHex, 16));
3665
- if (decoded3 !== "." && isUnreserved(decoded3)) {
3666
- output2 += decoded3;
3661
+ const hex4 = input2.slice(i + 1, i + 3);
3662
+ if (isHexPair(hex4)) {
3663
+ const normalizedHex = hex4.toUpperCase();
3664
+ const decoded4 = String.fromCharCode(parseInt(normalizedHex, 16));
3665
+ if (decoded4 !== "." && isUnreserved(decoded4)) {
3666
+ output2 += decoded4;
3667
3667
  } else {
3668
3668
  output2 += "%" + normalizedHex;
3669
3669
  }
@@ -3701,9 +3701,9 @@ var require_utils = __commonJS({
3701
3701
  for (let i = 0; i < input2.length; i++) {
3702
3702
  const ch = input2[i];
3703
3703
  if (ch === "%" && i + 2 < input2.length) {
3704
- const hex3 = input2.slice(i + 1, i + 3);
3705
- if (isHexPair(hex3)) {
3706
- output2 += "%" + hex3.toUpperCase();
3704
+ const hex4 = input2.slice(i + 1, i + 3);
3705
+ if (isHexPair(hex4)) {
3706
+ output2 += "%" + hex4.toUpperCase();
3707
3707
  i += 2;
3708
3708
  continue;
3709
3709
  }
@@ -3740,9 +3740,9 @@ var require_utils = __commonJS({
3740
3740
  for (let i = 0; i < input2.length; i++) {
3741
3741
  const ch = input2[i];
3742
3742
  if (ch === "%" && i + 2 < input2.length) {
3743
- const hex3 = input2.slice(i + 1, i + 3);
3744
- if (isHexPair(hex3)) {
3745
- output2 += "%" + hex3.toUpperCase();
3743
+ const hex4 = input2.slice(i + 1, i + 3);
3744
+ if (isHexPair(hex4)) {
3745
+ output2 += "%" + hex4.toUpperCase();
3746
3746
  i += 2;
3747
3747
  continue;
3748
3748
  }
@@ -3792,12 +3792,12 @@ var require_utils = __commonJS({
3792
3792
  for (let i = 0; i < input2.length; i++) {
3793
3793
  const ch = input2[i];
3794
3794
  if (ch === "%" && i + 2 < input2.length) {
3795
- const hex3 = input2.slice(i + 1, i + 3);
3796
- if (isHexPair(hex3)) {
3797
- const normalizedHex = hex3.toUpperCase();
3798
- const decoded3 = String.fromCharCode(parseInt(normalizedHex, 16));
3799
- if (isUnreserved(decoded3)) {
3800
- output2 += decoded3;
3795
+ const hex4 = input2.slice(i + 1, i + 3);
3796
+ if (isHexPair(hex4)) {
3797
+ const normalizedHex = hex4.toUpperCase();
3798
+ const decoded4 = String.fromCharCode(parseInt(normalizedHex, 16));
3799
+ if (isUnreserved(decoded4)) {
3800
+ output2 += decoded4;
3801
3801
  } else {
3802
3802
  output2 += "%" + normalizedHex;
3803
3803
  }
@@ -3833,9 +3833,9 @@ var require_utils = __commonJS({
3833
3833
  let output2 = "";
3834
3834
  for (let i = 0; i < input2.length; i++) {
3835
3835
  if (input2[i] === "%" && i + 2 < input2.length) {
3836
- const hex3 = input2.slice(i + 1, i + 3);
3837
- if (isHexPair(hex3)) {
3838
- output2 += "%" + hex3.toUpperCase();
3836
+ const hex4 = input2.slice(i + 1, i + 3);
3837
+ if (isHexPair(hex4)) {
3838
+ output2 += "%" + hex4.toUpperCase();
3839
3839
  i += 2;
3840
3840
  continue;
3841
3841
  }
@@ -9225,24 +9225,24 @@ function editField(info, fieldSuffix, list = false) {
9225
9225
  const metadataValue = list ? decodeStringList(rawMeta) : rawMeta;
9226
9226
  return { value, metadataValue, edited: member(wrapper, "::HasEditLayer") === true };
9227
9227
  }
9228
- function parseAlbumEditInfo(decoded3) {
9228
+ function parseAlbumEditInfo(decoded4) {
9229
9229
  return {
9230
- title: editField(decoded3, "Title"),
9231
- version: editField(decoded3, "Version"),
9232
- performedBy: editField(decoded3, "PerformedBy"),
9233
- genres: editField(decoded3, "Genres", true),
9234
- labels: editField(decoded3, "Labels", true),
9235
- rating: editField(decoded3, "Rating"),
9236
- type: editField(decoded3, "Type"),
9237
- isCompilation: editField(decoded3, "IsCompilation"),
9238
- isLive: editField(decoded3, "IsLive"),
9239
- isPick: editField(decoded3, "IsPick"),
9240
- containsExplicitContent: editField(decoded3, "ContainsExplicitContent"),
9241
- isUserHidden: editField(decoded3, "IsUserHidden"),
9242
- country: editField(decoded3, "Country"),
9243
- catalogNumber: editField(decoded3, "CatalogNumber"),
9244
- productCode: editField(decoded3, "ProductCode"),
9245
- raw: decoded3
9230
+ title: editField(decoded4, "Title"),
9231
+ version: editField(decoded4, "Version"),
9232
+ performedBy: editField(decoded4, "PerformedBy"),
9233
+ genres: editField(decoded4, "Genres", true),
9234
+ labels: editField(decoded4, "Labels", true),
9235
+ rating: editField(decoded4, "Rating"),
9236
+ type: editField(decoded4, "Type"),
9237
+ isCompilation: editField(decoded4, "IsCompilation"),
9238
+ isLive: editField(decoded4, "IsLive"),
9239
+ isPick: editField(decoded4, "IsPick"),
9240
+ containsExplicitContent: editField(decoded4, "ContainsExplicitContent"),
9241
+ isUserHidden: editField(decoded4, "IsUserHidden"),
9242
+ country: editField(decoded4, "Country"),
9243
+ catalogNumber: editField(decoded4, "CatalogNumber"),
9244
+ productCode: editField(decoded4, "ProductCode"),
9245
+ raw: decoded4
9246
9246
  };
9247
9247
  }
9248
9248
  var ROOT_SERVICE_GUID, RoonClient, ALBUM_EDIT, EDIT_OPTIONAL_VAL_INT, EDIT_REQUIRED_REF_STR, EDIT_LIST_STR, LIBRARY_EDIT;
@@ -9532,14 +9532,14 @@ var init_client = __esm({
9532
9532
  * VirtualAlbumLiteQuery oid -> RetainPage(0) -> items push.
9533
9533
  */
9534
9534
  async searchAlbums(term, pageSize = 40) {
9535
- const criteria = this.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [
9535
+ const criteria2 = this.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [
9536
9536
  { name: "UiLanguage", propType: 20 /* String */, value: new BinaryWriter().string("en").toBuffer() },
9537
9537
  { name: "TextFilter", propType: 20 /* String */, value: new BinaryWriter().string(term).toBuffer() }
9538
9538
  ]);
9539
9539
  const params = this.structArg("Sooloos.Broker.Api.VirtualQueryParameters", [
9540
9540
  { name: "PageSize", propType: 0 /* Int */, value: new BinaryWriter().integer(pageSize).toBuffer() }
9541
9541
  ]);
9542
- const args = Buffer.concat([buildArgs([Arg.sooid(this.profile())]), criteria, params]);
9542
+ const args = Buffer.concat([buildArgs([Arg.sooid(this.profile())]), criteria2, params]);
9543
9543
  const res = await this.call(
9544
9544
  "Library",
9545
9545
  "VirtualAlbumQuery",
@@ -9716,8 +9716,8 @@ var init_client = __esm({
9716
9716
  this.serviceOid("Library")
9717
9717
  );
9718
9718
  if (!res.success) throw new Error(`GetAlbumEditInfo failed: ${res.status}`);
9719
- const decoded3 = this.graph.decodeReturnValue(Uint8Array.from(res.payload));
9720
- return parseAlbumEditInfo(decoded3);
9719
+ const decoded4 = this.graph.decodeReturnValue(Uint8Array.from(res.payload));
9720
+ return parseAlbumEditInfo(decoded4);
9721
9721
  }
9722
9722
  // --- metadata editing: write side (Phase E) ---
9723
9723
  /**
@@ -9913,7 +9913,7 @@ async function exportPlayHistory(client2, options) {
9913
9913
  if (client2.graph.findByType("HistoryPlay").length > 0) {
9914
9914
  throw new Error("history export requires a fresh client graph with no HistoryPlay objects");
9915
9915
  }
9916
- const criteria = inlineStruct(
9916
+ const criteria2 = inlineStruct(
9917
9917
  client2.remoting.defineType("Sooloos.Broker.Api.HistoryQueryCriteria", [])
9918
9918
  );
9919
9919
  const params = client2.structArg("Sooloos.Broker.Api.VirtualQueryParameters", [
@@ -9923,7 +9923,7 @@ async function exportPlayHistory(client2, options) {
9923
9923
  value: new BinaryWriter().integer(pageSize).toBuffer()
9924
9924
  }
9925
9925
  ]);
9926
- const args = Buffer.concat([buildArgs([Arg.sooid(client2.profile())]), criteria, params]);
9926
+ const args = Buffer.concat([buildArgs([Arg.sooid(client2.profile())]), criteria2, params]);
9927
9927
  const result = await client2.call(
9928
9928
  "Library",
9929
9929
  "VirtualHistoryQuery",
@@ -13953,9 +13953,9 @@ function coreInfo(host, port = 9330, ms = 5e3) {
13953
13953
  });
13954
13954
  }
13955
13955
  function brokerId(coreId) {
13956
- const hex3 = coreId.replace(/-/g, "");
13957
- if (!/^[0-9a-f]{32}$/i.test(hex3)) throw new Error(`not a Roon core_id: ${coreId}`);
13958
- const b = Buffer.from(hex3, "hex");
13956
+ const hex4 = coreId.replace(/-/g, "");
13957
+ if (!/^[0-9a-f]{32}$/i.test(hex4)) throw new Error(`not a Roon core_id: ${coreId}`);
13958
+ const b = Buffer.from(hex4, "hex");
13959
13959
  return Buffer.concat([Buffer.from(b.subarray(0, 4)).reverse(), Buffer.from(b.subarray(4, 6)).reverse(), Buffer.from(b.subarray(6, 8)).reverse(), b.subarray(8)]);
13960
13960
  }
13961
13961
  var import_dgram, import_crypto, ROON_SERVICE_ID, SOOD_PORT;
@@ -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.8";
15528
+ version2 = "0.4.0";
15529
15529
  }
15530
15530
  });
15531
15531
 
@@ -16207,7 +16207,7 @@ async function findAlbum(c, key) {
16207
16207
  }
16208
16208
  throw new RoonError(`Roon knows no album with key ${key}`, "not_found");
16209
16209
  }
16210
- async function findTracks(c, keys) {
16210
+ async function findTracks(c, keys, lenient = false) {
16211
16211
  const look = /* @__PURE__ */ __name(() => {
16212
16212
  const all = [...c.graph.objects.values()].filter((o) => /\.Track(Lite)?$/.test(o.typeName) && field2(o, "Title") !== void 0);
16213
16213
  return keys.map((k) => all.find((o) => String(field2(o, "TrackId") ?? "") === k));
@@ -16227,6 +16227,7 @@ async function findTracks(c, keys) {
16227
16227
  tracks2 = look();
16228
16228
  }
16229
16229
  const gone = keys.filter((_, i) => !tracks2[i]);
16230
+ if (gone.length && lenient) return tracks2;
16230
16231
  if (gone.length) throw new RoonError(`Roon doesn't know ${gone.length} of the ${keys.length} tracks (keys ${gone.join(", ")})`, "not_found");
16231
16232
  return tracks2;
16232
16233
  }
@@ -16486,13 +16487,13 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
16486
16487
  const missing = members2.find((f) => !schema.some((m) => m.shortName === f.name));
16487
16488
  if (missing) throw new RoonError(`The filter ${missing.name} doesn't exist for ${kind.toLowerCase()}s in this Roon version`, "invalid");
16488
16489
  const size = Math.min(Math.max(1, limit3), 100), page = Math.floor(Math.max(0, offset2) / size);
16489
- const criteria = c.structArg(`Sooloos.Broker.Api.${kind}QueryCriteria`, [english(), ...members2]);
16490
+ const criteria2 = c.structArg(`Sooloos.Broker.Api.${kind}QueryCriteria`, [english(), ...members2]);
16490
16491
  const params = c.structArg("Sooloos.Broker.Api.VirtualQueryParameters", [{ name: "PageSize", propType: 0, value: new BinaryWriter().integer(size).toBuffer() }]);
16491
16492
  const res = await c.call(
16492
16493
  "Library",
16493
16494
  `Virtual${kind}Query`,
16494
16495
  [{ type: "Sooid", name: "profileid" }, { type: `${kind}QueryCriteria`, name: "criteria" }, { type: "VirtualQueryParameters", name: "queryparams" }, { type: `ResultCallback<Virtual${kind}LiteQuery>`, name: "cb_result" }],
16495
- Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria, params]),
16496
+ Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2, params]),
16496
16497
  c.serviceOid("Library")
16497
16498
  );
16498
16499
  if (!res.success) throw new RoonError(`Roon refused the ${kind.toLowerCase()} query (${res.status})`);
@@ -16527,12 +16528,12 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
16527
16528
  }
16528
16529
  }
16529
16530
  async function albumInfo(c, members2) {
16530
- const criteria = c.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [english(), ...members2]);
16531
+ const criteria2 = c.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [english(), ...members2]);
16531
16532
  const res = await c.call(
16532
16533
  "Library",
16533
16534
  "GetAlbumQueryInfo",
16534
16535
  [{ type: "Sooid", name: "profileid" }, { type: "AlbumQueryCriteria", name: "criteria" }, { type: "ResultCallback<AlbumQueryInfo>", name: "cb_result" }],
16535
- Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria]),
16536
+ Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2]),
16536
16537
  c.serviceOid("Library")
16537
16538
  );
16538
16539
  if (!res.success) throw new RoonError(`Roon refused the Focus panel (${res.status})`);
@@ -17751,14 +17752,14 @@ function isValidJWT(jwt2, alg) {
17751
17752
  if (!header)
17752
17753
  return false;
17753
17754
  const base643 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "=");
17754
- const decoded3 = JSON.parse(atob(base643));
17755
- if (typeof decoded3 !== "object" || decoded3 === null)
17755
+ const decoded4 = JSON.parse(atob(base643));
17756
+ if (typeof decoded4 !== "object" || decoded4 === null)
17756
17757
  return false;
17757
- if ("typ" in decoded3 && decoded3?.typ !== "JWT")
17758
+ if ("typ" in decoded4 && decoded4?.typ !== "JWT")
17758
17759
  return false;
17759
- if (!decoded3.alg)
17760
+ if (!decoded4.alg)
17760
17761
  return false;
17761
- if (alg && decoded3.alg !== alg)
17762
+ if (alg && decoded4.alg !== alg)
17762
17763
  return false;
17763
17764
  return true;
17764
17765
  } catch {
@@ -21983,8 +21984,8 @@ function uint8ArrayToBase64url(bytes) {
21983
21984
  return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
21984
21985
  }
21985
21986
  __name(uint8ArrayToBase64url, "uint8ArrayToBase64url");
21986
- function hexToUint8Array(hex3) {
21987
- const cleanHex = hex3.replace(/^0x/, "");
21987
+ function hexToUint8Array(hex4) {
21988
+ const cleanHex = hex4.replace(/^0x/, "");
21988
21989
  if (cleanHex.length % 2 !== 0) {
21989
21990
  throw new Error("Invalid hex string length");
21990
21991
  }
@@ -49295,22 +49296,32 @@ init_library();
49295
49296
  init_intern();
49296
49297
  init_session();
49297
49298
  init_graph();
49299
+ init_library();
49298
49300
  async function history(limit3) {
49299
49301
  return withCore(async (first) => {
49300
49302
  const c = first.graph.findByType("HistoryPlay").length > 0 ? await fresh() : first;
49301
49303
  const out = await exportPlayHistory(c, { limit: Math.min(Math.max(0, limit3), 500) });
49304
+ const keys = [...new Set(out.events.map((e) => e.roonTrackId).filter((k) => !!k))].slice(0, 100);
49305
+ const byKey = /* @__PURE__ */ new Map();
49306
+ if (keys.length) {
49307
+ const tracks2 = await findTracks(c, keys, true).catch(() => []);
49308
+ for (const t of tracks2) if (t) byKey.set(String(field2(t, "TrackId")), t);
49309
+ }
49302
49310
  closeSoon("after the history");
49303
49311
  return {
49304
49312
  total: out.total ?? null,
49305
- plays: out.events.map((e) => ({
49306
- at: localTime(e.playedAt),
49307
- title: e.title,
49308
- artist: clean(e.artist),
49309
- album: e.album ?? void 0,
49310
- played_pct: e.completionPct ?? void 0,
49311
- track_key: e.roonTrackId ?? void 0
49312
- }))
49313
- // for roon_play_tracks (13 Oct 2026, Rosso's history)
49313
+ plays: out.events.map((e) => {
49314
+ const r = { at: localTime(e.playedAt), title: e.title, artist: clean(e.artist), album: e.album ?? void 0, played_pct: e.completionPct ?? void 0, track_key: e.roonTrackId ?? void 0 };
49315
+ const t = e.roonTrackId ? byKey.get(e.roonTrackId) : void 0;
49316
+ if (t) {
49317
+ const al = obj(c, field2(t, "Album"));
49318
+ const ak = field2(al, "AlbumId");
49319
+ if (ak !== void 0 && ak !== null) r.album_key = String(ak);
49320
+ const img = imageKey(c, al);
49321
+ if (img) r.image = img;
49322
+ }
49323
+ return r;
49324
+ })
49314
49325
  };
49315
49326
  });
49316
49327
  }
@@ -49463,312 +49474,13 @@ async function nowPlaying() {
49463
49474
  }
49464
49475
  __name(nowPlaying, "nowPlaying");
49465
49476
 
49466
- // src/core/officialtools.ts
49467
- init_errors();
49468
- init_official();
49469
- var call = /* @__PURE__ */ __name((fn) => new Promise((res, rej) => fn((err) => err ? rej(new RoonError(`Roon refused: ${err}`)) : res())), "call");
49470
- var duration4 = /* @__PURE__ */ __name((s) => {
49471
- const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), r = t % 60;
49472
- return h ? `${h}:${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}` : `${m}:${String(r).padStart(2, "0")}`;
49473
- }, "duration");
49474
- var wait2 = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "wait");
49475
- async function zoneOf(name) {
49476
- const t = await paired();
49477
- await officialReady();
49478
- const z2 = officialZone(name);
49479
- if (!z2) throw new RoonError(`No Roon zone called "${name}"; Roon has: ${officialZones().map((x) => x.display_name).join(", ")}`, "not_found");
49480
- return { t, z: z2 };
49481
- }
49482
- __name(zoneOf, "zoneOf");
49483
- async function zones3() {
49484
- await officialReady();
49485
- const all = officialZones();
49486
- return {
49487
- zones: [...new Set(all.flatMap((z2) => z2.outputs.map((o) => o.display_name)))].sort(),
49488
- groups: all.filter((z2) => z2.outputs.length > 1).map((z2) => z2.outputs.map((o) => o.display_name))
49489
- };
49490
- }
49491
- __name(zones3, "zones");
49492
- async function nowPlaying2() {
49493
- await officialReady();
49494
- return { zones: officialZones().map((z2) => {
49495
- const r = { zone: z2.display_name, state: z2.state };
49496
- const v = z2.outputs.find((o) => o.volume)?.volume;
49497
- if (v) {
49498
- r.volume = v.value;
49499
- r.volume_max = v.max;
49500
- if (v.is_muted) r.muted = true;
49501
- }
49502
- if (z2.settings?.shuffle) r.shuffle = true;
49503
- if (z2.settings?.loop && z2.settings.loop !== "disabled") r.repeat = z2.settings.loop === "loop_one" ? "one" : "all";
49504
- if (z2.settings?.auto_radio) r.roon_radio = true;
49505
- const np = z2.now_playing;
49506
- if (np && z2.state !== "stopped") {
49507
- if (np.three_line?.line1) {
49508
- r.title = np.three_line.line1;
49509
- if (np.three_line.line2) r.artist = np.three_line.line2;
49510
- if (np.three_line.line3) r.album = np.three_line.line3;
49511
- }
49512
- if (typeof np.seek_position === "number") r.position = duration4(np.seek_position);
49513
- if (typeof np.length === "number") r.length = duration4(np.length);
49514
- }
49515
- return r;
49516
- }) };
49517
- }
49518
- __name(nowPlaying2, "nowPlaying");
49519
- async function control(name, action) {
49520
- const { t, z: z2 } = await zoneOf(name);
49521
- await call((cb) => t.control(z2.zone_id, action, cb));
49522
- const expect = { pause: ["paused"], stop: ["stopped", "paused"], play: ["playing", "loading"] };
49523
- let state = officialZone(z2.display_name)?.state;
49524
- if (expect[action]) {
49525
- const end = Date.now() + 3e3;
49526
- while (Date.now() < end && !expect[action].includes(state ?? "")) {
49527
- await wait2(150);
49528
- state = officialZone(z2.display_name)?.state;
49529
- }
49530
- }
49531
- return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: z2.display_name, action, state };
49532
- }
49533
- __name(control, "control");
49534
- async function seek(name, seconds) {
49535
- const { t, z: z2 } = await zoneOf(name);
49536
- await call((cb) => t.seek(z2.zone_id, "absolute", Math.max(0, Math.round(seconds)), cb));
49537
- return { ok: true, zone: z2.display_name, position: duration4(seconds) };
49538
- }
49539
- __name(seek, "seek");
49540
- async function settings(name, s) {
49541
- if (s.roon_radio === "now") throw new RoonError('"Start Roon Radio now" needs the internal protocol (switched off)', "invalid");
49542
- const { t, z: z2 } = await zoneOf(name);
49543
- const change = {};
49544
- if (s.shuffle !== void 0) change.shuffle = s.shuffle;
49545
- if (s.repeat !== void 0) change.loop = { off: "disabled", all: "loop", one: "loop_one" }[s.repeat];
49546
- if (s.roon_radio !== void 0) change.auto_radio = s.roon_radio;
49547
- if (!Object.keys(change).length) throw new RoonError("Nothing to set: give shuffle, repeat or roon_radio", "invalid");
49548
- await call((cb) => t.change_settings(z2.zone_id, change, cb));
49549
- return { ok: true, zone: z2.display_name, ...s };
49550
- }
49551
- __name(settings, "settings");
49552
- async function transfer(from, to) {
49553
- const a = await zoneOf(from), b = await zoneOf(to);
49554
- if (a.z.zone_id === b.z.zone_id) throw new RoonError("From and to are the same zone", "invalid");
49555
- await call((cb) => a.t.transfer_zone(a.z.zone_id, b.z.zone_id, cb));
49556
- return { ok: true, from: a.z.display_name, to: b.z.display_name };
49557
- }
49558
- __name(transfer, "transfer");
49559
- async function group(main2, join) {
49560
- const m = await zoneOf(main2);
49561
- const outs = [...m.z.outputs.map((o) => o.output_id)];
49562
- for (const n of join) {
49563
- const j = await zoneOf(n);
49564
- for (const o of j.z.outputs) if (!outs.includes(o.output_id)) outs.push(o.output_id);
49565
- }
49566
- if (outs.length === m.z.outputs.length) throw new RoonError("Nothing to group: name at least one other zone", "invalid");
49567
- await call((cb) => m.t.group_outputs(outs, cb));
49568
- return { ok: true, group: outs.map((id) => officialZones().flatMap((z2) => z2.outputs).find((o) => o.output_id === id)?.display_name ?? id) };
49569
- }
49570
- __name(group, "group");
49571
- async function ungroup(name, only) {
49572
- const { t, z: z2 } = await zoneOf(name);
49573
- if (z2.outputs.length < 2) throw new RoonError(`${z2.display_name} isn't in a group`, "invalid");
49574
- const off = only ? z2.outputs.filter((o) => o.display_name.toLowerCase().includes(only.toLowerCase())) : z2.outputs;
49575
- if (!off.length) throw new RoonError(`"${only}" isn't in this group`, "not_found");
49576
- await call((cb) => t.ungroup_outputs(off.map((o) => o.output_id), cb));
49577
- return { ok: true, released: off.map((o) => o.display_name) };
49578
- }
49579
- __name(ungroup, "ungroup");
49580
- async function standby(name, on) {
49581
- const { t, z: z2 } = await zoneOf(name);
49582
- for (const o of z2.outputs) await call((cb) => on ? t.standby(o.output_id, {}, cb) : t.convenience_switch(o.output_id, {}, cb));
49583
- return { ok: true, zone: z2.display_name, standby: on };
49584
- }
49585
- __name(standby, "standby");
49586
- async function cover(target2, name) {
49587
- await officialReady();
49588
- const z2 = name ? officialZone(name) : officialZones().find((x) => x.state === "playing" && x.now_playing?.image_key) ?? officialZones().find((x) => x.state === "paused" && x.now_playing?.image_key);
49589
- if (!z2) throw new RoonError(name ? `No Roon zone called "${name}"` : "Nothing plays in Roon right now", "not_found");
49590
- const key = z2.now_playing?.image_key;
49591
- if (!key) throw new RoonError(`Roon has no cover for what plays on ${z2.display_name}`, "not_found");
49592
- const r = await fetch(`http://${target2.host}:${target2.port}/api/image/${key}?scale=fit&width=600&height=600`).catch(() => null);
49593
- if (!r?.ok) throw new RoonError("Roon doesn't hand over the cover right now", "unavailable");
49594
- const l = z2.now_playing?.three_line;
49595
- return {
49596
- info: { zone: z2.display_name, title: l?.line1, artist: l?.line2, album: l?.line3 },
49597
- image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
49598
- };
49599
- }
49600
- __name(cover, "cover");
49601
-
49602
- // src/tools/run.ts
49477
+ // src/roon/radio.ts
49478
+ init_intern();
49479
+ init_serializer();
49480
+ init_writer();
49603
49481
  init_session();
49604
49482
  init_log();
49605
-
49606
- // src/core/status.ts
49607
- init_package();
49608
- init_log();
49609
- var checks = null;
49610
- var checking = null;
49611
- function selfTest(run2) {
49612
- checking ??= run2().then((c) => {
49613
- checks = c;
49614
- const bad = c.filter((x) => !x.ok);
49615
- log(bad.length ? `self-test: ${bad.map((x) => `${x.name} failed (${x.detail})`).join("; ")}` : `self-test: ${c.length} reads ok`);
49616
- }).catch((e) => {
49617
- log(`self-test couldn't run: ${e.message}`);
49618
- });
49619
- return checking;
49620
- }
49621
- __name(selfTest, "selfTest");
49622
- var selfTestResults = /* @__PURE__ */ __name(() => checks, "selfTestResults");
49623
- var newer = null;
49624
- var told = false;
49625
- var parse3 = /* @__PURE__ */ __name((v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0), "parse");
49626
- var isNewer = /* @__PURE__ */ __name((a, b) => {
49627
- const x = parse3(a), y = parse3(b);
49628
- for (let i = 0; i < 3; i++) {
49629
- if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
49630
- }
49631
- return false;
49632
- }, "isNewer");
49633
- async function checkForUpdate() {
49634
- if (process.env.DEEP_ROON_NO_UPDATE_CHECK === "1") return;
49635
- try {
49636
- const r = await fetch("https://api.github.com/repos/renebouwmeester/deep-roon-mcp/releases/latest", {
49637
- headers: { "User-Agent": `deep-roon-mcp/${version2}`, Accept: "application/vnd.github+json" },
49638
- signal: AbortSignal.timeout(5e3)
49639
- });
49640
- if (!r.ok) return;
49641
- const j = await r.json();
49642
- if (j.tag_name && isNewer(j.tag_name, version2)) {
49643
- newer = { version: j.tag_name.replace(/^v/, ""), url: j.html_url ?? "" };
49644
- log(`a newer version is out: ${newer.version}`);
49645
- }
49646
- } catch {
49647
- }
49648
- }
49649
- __name(checkForUpdate, "checkForUpdate");
49650
- var updateInfo = /* @__PURE__ */ __name(() => newer, "updateInfo");
49651
- function updateNotice() {
49652
- if (!newer || told) return null;
49653
- told = true;
49654
- return `Deep Roon MCP ${newer.version} is available (this is ${version2}): ${newer.url}`;
49655
- }
49656
- __name(updateNotice, "updateNotice");
49657
-
49658
- // src/tools/run.ts
49659
- var compact = /* @__PURE__ */ __name((_k, v) => v === void 0 ? void 0 : typeof v === "bigint" ? v.toString() : v, "compact");
49660
- async function run(work2) {
49661
- try {
49662
- const result = await work2();
49663
- const notice = updateNotice();
49664
- return { content: [{ type: "text", text: JSON.stringify(result, compact) }, ...notice ? [{ type: "text", text: notice }] : []] };
49665
- } catch (e) {
49666
- const err = e;
49667
- let text2;
49668
- if (err instanceof RoonError) text2 = err.message;
49669
- else if (/MissingMethod|method not found|unknown method/i.test(err.message)) {
49670
- text2 = `Roon changed this part of its internal protocol in this version (${err.message}). Check for an update of Deep Roon MCP.`;
49671
- } else text2 = `Roon didn't answer as expected: ${err.message}`;
49672
- log(`tool error: ${err.message}`);
49673
- return { isError: true, content: [{ type: "text", text: text2 }] };
49674
- }
49675
- }
49676
- __name(run, "run");
49677
- async function runImage(work2) {
49678
- let image = null;
49679
- const r = await run(async () => {
49680
- const x = await work2();
49681
- image = x.image;
49682
- return x.info;
49683
- });
49684
- if (r.isError || !image) return r;
49685
- const { data, mimeType } = image;
49686
- return { content: [...r.content, { type: "image", data, mimeType }] };
49687
- }
49688
- __name(runImage, "runImage");
49689
-
49690
- // src/tools/read.ts
49691
- var READ = { readOnlyHint: true, openWorldHint: false };
49692
- var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
49693
- var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
49694
- function registerReadTools(server, internal = true) {
49695
- const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
49696
- const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
49697
- reg("roon_search", {
49698
- title: "Search Roon",
49699
- 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.',
49700
- inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
49701
- annotations: READ
49702
- }, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
49703
- reg("roon_new", {
49704
- title: "Streaming shelves in Roon",
49705
- description: 'The editorial shelves of Qobuz or TIDAL as Roon shows them (the same for everyone). Qobuz (`service: "qobuz"`): `new`, `qobuzissimes`, `press_awards`, `top`, `trending`, `ideal_discography` \u2014 albums; `genre` narrows a shelf to one or more Qobuz genre ids from `roon_genres`. TIDAL (`service: "tidal"`): `new`, `recommended`, `top`, `rising`, `masters` \u2014 albums, or tracks with `what: "tracks"` (not masters); `genre` (one TIDAL genre from `roon_genres`, e.g. "Jazz") gives TIDAL\'s page for that genre instead of a shelf. Use `genre` for "new jazz", "what\'s new in classical"; TIDAL\'s playlists are in roon_tidal_playlists. Albums carry their format and a `key` for `roon_album`; tracks a `key` for roon_play_tracks. `offset` pages through the shelf; `total` says how many there are.',
49706
- inputSchema: {
49707
- service,
49708
- section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
49709
- genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
49710
- what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
49711
- limit: limit(100, 30),
49712
- offset: external_exports.number().int().min(0).optional()
49713
- },
49714
- annotations: READ
49715
- }, ({ service: sv, section: section2, genre, what, limit: n, offset: offset2 }) => run(() => section(section2 ?? "new", n ?? 30, offset2 ?? 0, (genre ?? "").split(",").map((x) => x.trim()).filter(Boolean), sv, what ?? "albums")));
49716
- reg("roon_genres", {
49717
- title: "Streaming genres",
49718
- description: "The genres of Qobuz or TIDAL, for the `genre` filter of `roon_new` (and, for TIDAL, of `roon_tidal_playlists`). Qobuz: the main genres with their ids (names in the account's language); with `parent` (a genre id) its sub-genres, e.g. the kinds of jazz. TIDAL: its genres and its moods (Relax, Focus \u2026), each with an `id` to pass on.",
49719
- inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
49720
- annotations: READ
49721
- }, ({ service: sv, parent }) => run(() => genres(parent, sv)));
49722
- reg("roon_album", {
49723
- title: "Album page",
49724
- description: "One album in Roon, by its `key` (from roon_search, roon_new, roon_artist): the album with year, format, label and whether it is in the library, its genres, release date (and the date of this edition), barcode and \u2014 when Roon has them \u2014 catalogue number and country, its main artist(s) with a `key` for `roon_artist` / `roon_bio`, and every track with its own `key`. A streaming album that is also in the library comes back as the library copy (its key may differ; both work). `credits: true` adds who played, produced, engineered and wrote, grouped by role \u2014 Roon's own data.",
49725
- inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
49726
- annotations: READ
49727
- }, ({ key, credits }) => run(() => album(key, credits === true)));
49728
- reg("roon_review", {
49729
- title: "Album review",
49730
- description: "The review Roon shows on an album's page, in full, with its author \u2014 by album `key`. When the user asks for the review (\"what does Roon say about this album\", \"show me the review\"), they want to read the review itself: show the text complete and exactly as returned, as a quote with the author's name \u2014 no paraphrase or shortening unless they ask for a summary, and don't go to the web for it. `wikipedia: true` adds the album's Wikipedia text; it also comes by itself when Roon has no review of its own. `max_chars` only when a shorter excerpt is asked for.",
49731
- inputSchema: { key: external_exports.string().describe("An album key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
49732
- annotations: READ
49733
- }, ({ key, wikipedia, max_chars }) => run(() => review(key, max_chars ?? 1e9, wikipedia === true)));
49734
- reg("roon_bio", {
49735
- title: "Artist biography",
49736
- description: 'The biography Roon shows on an artist\'s page, in full, with its author \u2014 by artist `key` \u2014 plus where they are from, active since, genres, and the related artists Roon lists: `similar`, `influenced_by`, `influenced`, `worked_with`, `member_of`, `members`, each with a `key` for `roon_artist` / `roon_bio`. When the user asks for the bio ("who is \u2026", "tell me about this artist"), show the text complete and exactly as returned, with the author\'s name \u2014 no paraphrase or shortening unless they ask for a summary. For a narrower question ("who influenced them") answer from the lists. `wikipedia: true` adds the Wikipedia text; `max_chars` only when a shorter excerpt is asked for.',
49737
- inputSchema: { key: external_exports.string().describe("An artist key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
49738
- annotations: READ
49739
- }, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
49740
- reg("roon_artist", {
49741
- title: "Discography",
49742
- description: 'An artist\'s discography in Roon: the main albums Qobuz (or, with `service: "tidal"`, TIDAL) has, oldest first, each with year, format and a `key` for `roon_album`. The artist `key` comes from roon_search (`kind: "artist"`) or from the `artists` of roon_album. Use this for "the album before/after \u2026", "their debut", "what else did they make" \u2014 don\'t answer discography questions from memory. The year is the original release where Roon knows it; reissues and compilations are in the list too.',
49743
- inputSchema: { key: external_exports.string().describe("An artist key."), service },
49744
- annotations: READ
49745
- }, ({ key, service: sv }) => run(() => discography(key, sv)));
49746
- reg("roon_zones", {
49747
- title: "Roon zones",
49748
- description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
49749
- inputSchema: {},
49750
- annotations: READ
49751
- }, () => run(() => internal ? zones2() : zones3()));
49752
- reg("roon_now_playing", {
49753
- title: "Now playing in Roon",
49754
- description: "What plays in Roon, per zone: state (playing, loading, paused, stopped), the track with album, artist and format, the position, how many tracks are left in the queue, shuffle, repeat, Roon Radio and a sleep timer. Read this before saying anything about what Roon plays \u2014 it changes all the time.",
49755
- inputSchema: {},
49756
- annotations: READ
49757
- }, () => run(() => internal ? nowPlaying() : nowPlaying2()));
49758
- reg("roon_history", {
49759
- title: "Play history",
49760
- description: `What was played in Roon, newest first \u2014 Roon's own play history, with how much of each track was heard. For "most played" questions use roon_top instead of counting this.`,
49761
- inputSchema: { limit: limit(300, 30) },
49762
- annotations: READ
49763
- }, ({ limit: n }) => run(() => history(n ?? 30)));
49764
- reg("roon_top", {
49765
- title: "Most played",
49766
- description: 'The most played albums, artists or tracks in Roon, counted over the WHOLE play history (a play counts when at least half the track was heard). Use this for "most listened" questions \u2014 don\'t tally roon_history yourself. `since` (YYYY-MM-DD) limits the period.',
49767
- inputSchema: { what: external_exports.enum(["albums", "artists", "tracks"]).optional(), limit: limit(100, 20), since: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only plays from this date on (YYYY-MM-DD).") },
49768
- annotations: READ
49769
- }, ({ what, limit: n, since }) => run(() => top(what ?? "albums", n ?? 20, since)));
49770
- }
49771
- __name(registerReadTools, "registerReadTools");
49483
+ init_graph();
49772
49484
 
49773
49485
  // src/roon/control.ts
49774
49486
  init_intern();
@@ -49902,7 +49614,7 @@ async function playTracks(keys, zoneName, how) {
49902
49614
  }
49903
49615
  __name(playTracks, "playTracks");
49904
49616
  var BUTTONS = { pause: "Pause", play: "Play", playpause: "PlayPause", stop: "Stop", next: "Next", previous: "Previous" };
49905
- async function control2(zoneName, action) {
49617
+ async function control(zoneName, action) {
49906
49618
  if (action === "release") {
49907
49619
  return withCore(async (c) => {
49908
49620
  const z2 = findZone(c, zoneName);
@@ -49930,8 +49642,8 @@ async function control2(zoneName, action) {
49930
49642
  }
49931
49643
  return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: r.zone, action, state };
49932
49644
  }
49933
- __name(control2, "control");
49934
- async function seek2(zoneName, seconds) {
49645
+ __name(control, "control");
49646
+ async function seek(zoneName, seconds) {
49935
49647
  return withCore(async (c) => {
49936
49648
  const z2 = findZone(c, zoneName);
49937
49649
  const res = await c.call("Zone", "Seek", [{ type: "int", name: "seconds" }, { type: "ResultCallback", name: "cb_done" }], buildArgs([Arg.int(Math.max(0, Math.round(seconds)))]), z2.oid);
@@ -49939,8 +49651,8 @@ async function seek2(zoneName, seconds) {
49939
49651
  return { ok: true, zone: z2.name, position: duration3(seconds) };
49940
49652
  });
49941
49653
  }
49942
- __name(seek2, "seek");
49943
- async function settings2(zoneName, s) {
49654
+ __name(seek, "seek");
49655
+ async function settings(zoneName, s) {
49944
49656
  return withCore(async (c) => {
49945
49657
  const z2 = findZone(c, zoneName);
49946
49658
  const send = /* @__PURE__ */ __name((method, params, args) => c.remoting.callMethodNoReply(z2.oid, formatMethodSignature("Zone", method, params), args), "send");
@@ -49965,7 +49677,7 @@ async function settings2(zoneName, s) {
49965
49677
  return { ok: true, zone: z2.name, ...done };
49966
49678
  });
49967
49679
  }
49968
- __name(settings2, "settings");
49680
+ __name(settings, "settings");
49969
49681
  function queueOf(c, zoneName) {
49970
49682
  const z2 = findZone(c, zoneName);
49971
49683
  const q = obj(c, field2(c.graph.getObject(z2.oid), "Queue"));
@@ -50034,6 +49746,29 @@ async function queueRemove(zoneName, ns) {
50034
49746
  });
50035
49747
  }
50036
49748
  __name(queueRemove, "queueRemove");
49749
+ var INSERTION = { before: 3, after: 2 };
49750
+ async function queueMove(zoneName, n, to, where) {
49751
+ return withCore(async (c) => {
49752
+ const { z: z2, q, oid, refs, at } = await queueItemAt(c, zoneName, n);
49753
+ const rel = refs[at + to - 1];
49754
+ if (rel === void 0 || to === n) throw new RoonError(`The queue of ${z2.name} has no item ${to} to move next to`, "invalid");
49755
+ const point = INSERTION[where];
49756
+ const res = await c.call(
49757
+ "Queue",
49758
+ "MoveItems",
49759
+ [{ type: "IEnumerable<TransportItem>", name: "items" }, { type: "InsertionPoint", name: "insertionpoint" }, { type: "TransportItem", name: "relative_to_item" }, { type: "ResultCallback", name: "cb_done" }],
49760
+ Buffer.concat([buildArgs([Arg.collection([buildArgs([Arg.ref(oid)])]), Arg.enum_(point), Arg.ref(rel)])]),
49761
+ q.oid
49762
+ );
49763
+ if (!res.success) throw new RoonError(`Roon refused to move the item (${res.status})`);
49764
+ log(`queue_move: ${z2.name} item ${n} \u2192 ${where} ${to} (point ${point})`);
49765
+ await wait(300);
49766
+ const after = await queueItems(c, q);
49767
+ const now = after.indexOf(oid) - at + 1;
49768
+ return { ok: true, zone: z2.name, moved: n, now_at: now > 0 ? now : void 0 };
49769
+ });
49770
+ }
49771
+ __name(queueMove, "queueMove");
50037
49772
  async function queue2(zoneName, limit3) {
50038
49773
  return withCore(async (c) => {
50039
49774
  const { z: z2, q } = queueOf(c, zoneName);
@@ -50069,7 +49804,7 @@ var outputsOf = /* @__PURE__ */ __name((c, zone3) => c.graph.findByType("Endpoin
50069
49804
  const z2 = field2(e, "Zone");
50070
49805
  return isRef2(z2) && String(z2.$ref) === String(zone3) && typeof field2(e, "Name") === "string";
50071
49806
  }), "outputsOf");
50072
- async function group2(main2, join) {
49807
+ async function group(main2, join) {
50073
49808
  return withCore(async (c) => {
50074
49809
  const h = findZone(c, main2);
50075
49810
  const own2 = outputsOf(c, h.oid);
@@ -50095,8 +49830,8 @@ async function group2(main2, join) {
50095
49830
  return { ok: true, group: [...own2, ...added].map((e) => field2(e, "Name")) };
50096
49831
  });
50097
49832
  }
50098
- __name(group2, "group");
50099
- async function ungroup2(zoneName, only) {
49833
+ __name(group, "group");
49834
+ async function ungroup(zoneName, only) {
50100
49835
  return withCore(async (c) => {
50101
49836
  const z2 = findZone(c, zoneName);
50102
49837
  const all = outputsOf(c, z2.oid);
@@ -50114,8 +49849,8 @@ async function ungroup2(zoneName, only) {
50114
49849
  return { ok: true, released: off.map((e) => field2(e, "Name")) };
50115
49850
  });
50116
49851
  }
50117
- __name(ungroup2, "ungroup");
50118
- async function transfer2(from, to) {
49852
+ __name(ungroup, "ungroup");
49853
+ async function transfer(from, to) {
50119
49854
  return withCore(async (c) => {
50120
49855
  const a = findZone(c, from), b = findZone(c, to);
50121
49856
  if (a.oid === b.oid) throw new RoonError("From and to are the same zone", "invalid");
@@ -50130,7 +49865,7 @@ async function transfer2(from, to) {
50130
49865
  return { ok: true, from: a.name, to: b.name };
50131
49866
  });
50132
49867
  }
50133
- __name(transfer2, "transfer");
49868
+ __name(transfer, "transfer");
50134
49869
  async function sleep(zoneName, minutes) {
50135
49870
  return withCore(async (c) => {
50136
49871
  const z2 = findZone(c, zoneName);
@@ -50143,7 +49878,7 @@ async function sleep(zoneName, minutes) {
50143
49878
  });
50144
49879
  }
50145
49880
  __name(sleep, "sleep");
50146
- async function standby2(zoneName, on) {
49881
+ async function standby(zoneName, on) {
50147
49882
  return withCore(async (c) => {
50148
49883
  const z2 = findZone(c, zoneName);
50149
49884
  const outs = outputsOf(c, z2.oid).filter((e) => field2(e, "SupportsStandby") === true);
@@ -50155,8 +49890,8 @@ async function standby2(zoneName, on) {
50155
49890
  return { ok: true, zone: z2.name, standby: on };
50156
49891
  });
50157
49892
  }
50158
- __name(standby2, "standby");
50159
- async function cover2(zoneName) {
49893
+ __name(standby, "standby");
49894
+ async function cover(zoneName) {
50160
49895
  const t = await findTarget();
50161
49896
  const info = await withCore(async (c) => {
50162
49897
  const pick2 = zoneName ? findZone(c, zoneName) : null;
@@ -50185,8 +49920,430 @@ async function cover2(zoneName) {
50185
49920
  image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
50186
49921
  };
50187
49922
  }
49923
+ __name(cover, "cover");
49924
+
49925
+ // src/roon/radio.ts
49926
+ var hex3 = /* @__PURE__ */ __name((v) => Buffer.isBuffer(v) ? v.toString("hex") : "", "hex");
49927
+ var decoded = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
49928
+ var radioOid = /* @__PURE__ */ __name((c) => c.serviceOid("Radio"), "radioOid");
49929
+ function stationRow(c, o) {
49930
+ const r = { key: hex3(field2(o, "ChannelId")), name: field2(o, "Name") ?? "" };
49931
+ const loc = field2(o, "Location");
49932
+ if (loc) r.location = loc;
49933
+ const d = field2(o, "Description");
49934
+ if (d) r.description = String(d).slice(0, 200);
49935
+ if (field2(o, "IsInLibrary") === true) r.in_library = true;
49936
+ const img = imageKey(c, o);
49937
+ if (img) r.image = img;
49938
+ return r;
49939
+ }
49940
+ __name(stationRow, "stationRow");
49941
+ var categoryRow = /* @__PURE__ */ __name((c, o) => ({ key: hex3(field2(o, "CategoryId")), name: field2(o, "Name") ?? "" }), "categoryRow");
49942
+ function criteria(c, category, genre) {
49943
+ const members2 = [];
49944
+ if (category) {
49945
+ const cat = c.structArg("Sooloos.Broker.Api.RadioCategory", [{ name: "System.Sooid Sooloos.Broker.Api.RadioCategory::CategoryId", propType: 4, value: new BinaryWriter().sooid(Buffer.from(category, "hex")).toBuffer() }]);
49946
+ members2.push({ name: "Sooloos.Broker.Api.RadioCategory Sooloos.Broker.Api.RadioBrowseCriteria::Category", propType: 23, value: cat });
49947
+ }
49948
+ if (genre) members2.push({ name: "string Sooloos.Broker.Api.RadioBrowseCriteria::Genre", propType: 20, value: new BinaryWriter().string(genre).toBuffer() });
49949
+ return c.structArg("Sooloos.Broker.Api.RadioBrowseCriteria", members2);
49950
+ }
49951
+ __name(criteria, "criteria");
49952
+ async function myStations(c) {
49953
+ const crit = c.structArg("Sooloos.Broker.Api.ChannelQueryCriteria", [{ name: "bool Sooloos.Broker.Api.ChannelQueryCriteria::RequireInLibrary", propType: 2, value: new BinaryWriter().boolean(true).toBuffer() }]);
49954
+ const res = await c.call("Radio", "ChannelQuery", [{ type: "Sooid", name: "profileid" }, { type: "ChannelQueryCriteria", name: "criteria" }, { type: "ResultCallback<Query<Channel>>", name: "cb_result" }], Buffer.concat([buildArgs([Arg.sooid(c.profile())]), crit]), radioOid(c));
49955
+ if (!res.success) throw new RoonError(`Roon refused the radio stations (${res.status})`);
49956
+ const v = decoded(c, res.payload);
49957
+ const end = Date.now() + 3e3;
49958
+ let list = [];
49959
+ while (Date.now() < end) {
49960
+ list = (isRef2(v) ? c.graph.getObject(v.$ref)?.fields?.$items : null) ?? [];
49961
+ if (!list.length || list.every((x) => field2(obj(c, x), "Name") !== void 0)) break;
49962
+ await wait(60);
49963
+ }
49964
+ return list.map((x) => obj(c, x)).filter(Boolean);
49965
+ }
49966
+ __name(myStations, "myStations");
49967
+ async function radio(category, genre, limit3 = 40) {
49968
+ return withCore(async (c) => {
49969
+ const out = {};
49970
+ if (!category && !genre) out.my_stations = (await myStations(c)).map((o) => stationRow(c, o));
49971
+ const crit = criteria(c, category, genre);
49972
+ if (!genre) {
49973
+ const res = await c.call("Radio", "BrowseCategories", [{ type: "Sooid", name: "profileid" }, { type: "RadioBrowseCriteria", name: "criterid" }, { type: "ResultCallback<DataList<RadioCategory>>", name: "cb_result" }], Buffer.concat([buildArgs([Arg.sooid(c.profile())]), crit]), radioOid(c));
49974
+ if (!res.success) throw new RoonError(`Roon refused the radio categories (${res.status})`);
49975
+ const cats = items(c, decoded(c, res.payload));
49976
+ await untilLoaded(c, cats.map((x) => ({ $ref: x.oid })), 2500);
49977
+ out.categories = cats.map((x) => categoryRow(c, x));
49978
+ }
49979
+ if (category || genre) {
49980
+ const n = Math.min(Math.max(1, limit3), 100);
49981
+ const res = await c.call(
49982
+ "Radio",
49983
+ "BrowseChannels",
49984
+ [{ type: "Sooid", name: "profileid" }, { type: "RadioBrowseCriteria", name: "criteria" }, { type: "int?", name: "count" }, { type: "ResultCallback<DataList<Channel>>", name: "cb_result" }],
49985
+ Buffer.concat([buildArgs([Arg.sooid(c.profile())]), crit, Buffer.from([1]), new BinaryWriter().integer(n).toBuffer()]),
49986
+ radioOid(c)
49987
+ );
49988
+ if (!res.success) throw new RoonError(`Roon refused the radio stations (${res.status})`);
49989
+ const chs = items(c, decoded(c, res.payload));
49990
+ await untilLoaded(c, chs.map((x) => ({ $ref: x.oid })), 3500);
49991
+ out.stations = chs.map((x) => stationRow(c, x));
49992
+ }
49993
+ return out;
49994
+ });
49995
+ }
49996
+ __name(radio, "radio");
49997
+ async function findStation(c, key) {
49998
+ if (/^[0-9a-f]{20,}$/i.test(key)) {
49999
+ const res = await c.call("Radio", "GetChannel", [{ type: "Sooid", name: "channelid" }, { type: "ResultCallback<Channel>", name: "cb_result" }], buildArgs([Arg.sooid(Buffer.from(key, "hex"))]), radioOid(c));
50000
+ const v = res.success ? decoded(c, res.payload) : null;
50001
+ if (isRef2(v)) {
50002
+ await untilLoaded(c, [v], 2500);
50003
+ const o2 = c.graph.getObject(v.$ref);
50004
+ if (o2 && field2(o2, "Name") !== void 0) return o2;
50005
+ }
50006
+ throw new RoonError("Roon knows no station with that key", "not_found");
50007
+ }
50008
+ const mine = await myStations(c);
50009
+ const want = key.toLowerCase();
50010
+ const o = mine.find((x) => String(field2(x, "Name") ?? "").toLowerCase() === want) ?? mine.find((x) => String(field2(x, "Name") ?? "").toLowerCase().includes(want));
50011
+ if (!o) throw new RoonError(`No station called "${key}" among your own; give a key from roon_radio`, "not_found");
50012
+ return o;
50013
+ }
50014
+ __name(findStation, "findStation");
50015
+ async function playRadio(key, zoneName) {
50016
+ const r = await withCore(async (c) => {
50017
+ const st = await findStation(c, key);
50018
+ const z2 = findZone(c, zoneName);
50019
+ const res = await c.call(
50020
+ "Transport",
50021
+ "PlayChannel",
50022
+ [{ type: "Zone", name: "zone" }, { type: "Sooid", name: "profileid" }, { type: "Channel", name: "channel" }, { type: "ResultCallback<PlayFeedback>", name: "cb_result" }],
50023
+ buildArgs([Arg.ref(z2.oid), Arg.sooid(c.profile()), Arg.ref(st.oid)]),
50024
+ c.serviceOid("Transport")
50025
+ );
50026
+ log(`play_radio: ${field2(st, "Name")} on ${z2.name} \u2192 ${res.success ? "ok" : res.status}`);
50027
+ if (!res.success) throw new RoonError(`Roon refused to play the station (${res.status})`);
50028
+ return { zone: z2.name, station: field2(st, "Name") };
50029
+ });
50030
+ return { ok: true, ...r, ...await started2(r.zone, 0) };
50031
+ }
50032
+ __name(playRadio, "playRadio");
50033
+
50034
+ // src/core/officialtools.ts
50035
+ init_errors();
50036
+ init_official();
50037
+ var call = /* @__PURE__ */ __name((fn) => new Promise((res, rej) => fn((err) => err ? rej(new RoonError(`Roon refused: ${err}`)) : res())), "call");
50038
+ var duration4 = /* @__PURE__ */ __name((s) => {
50039
+ const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), r = t % 60;
50040
+ return h ? `${h}:${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}` : `${m}:${String(r).padStart(2, "0")}`;
50041
+ }, "duration");
50042
+ var wait2 = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "wait");
50043
+ async function zoneOf(name) {
50044
+ const t = await paired();
50045
+ await officialReady();
50046
+ const z2 = officialZone(name);
50047
+ if (!z2) throw new RoonError(`No Roon zone called "${name}"; Roon has: ${officialZones().map((x) => x.display_name).join(", ")}`, "not_found");
50048
+ return { t, z: z2 };
50049
+ }
50050
+ __name(zoneOf, "zoneOf");
50051
+ async function zones3() {
50052
+ await officialReady();
50053
+ const all = officialZones();
50054
+ return {
50055
+ zones: [...new Set(all.flatMap((z2) => z2.outputs.map((o) => o.display_name)))].sort(),
50056
+ groups: all.filter((z2) => z2.outputs.length > 1).map((z2) => z2.outputs.map((o) => o.display_name))
50057
+ };
50058
+ }
50059
+ __name(zones3, "zones");
50060
+ async function nowPlaying2() {
50061
+ await officialReady();
50062
+ return { zones: officialZones().map((z2) => {
50063
+ const r = { zone: z2.display_name, state: z2.state };
50064
+ const v = z2.outputs.find((o) => o.volume)?.volume;
50065
+ if (v) {
50066
+ r.volume = v.value;
50067
+ r.volume_max = v.max;
50068
+ if (v.is_muted) r.muted = true;
50069
+ }
50070
+ if (z2.settings?.shuffle) r.shuffle = true;
50071
+ if (z2.settings?.loop && z2.settings.loop !== "disabled") r.repeat = z2.settings.loop === "loop_one" ? "one" : "all";
50072
+ if (z2.settings?.auto_radio) r.roon_radio = true;
50073
+ const np = z2.now_playing;
50074
+ if (np && z2.state !== "stopped") {
50075
+ if (np.three_line?.line1) {
50076
+ r.title = np.three_line.line1;
50077
+ if (np.three_line.line2) r.artist = np.three_line.line2;
50078
+ if (np.three_line.line3) r.album = np.three_line.line3;
50079
+ }
50080
+ if (typeof np.seek_position === "number") r.position = duration4(np.seek_position);
50081
+ if (typeof np.length === "number") r.length = duration4(np.length);
50082
+ }
50083
+ return r;
50084
+ }) };
50085
+ }
50086
+ __name(nowPlaying2, "nowPlaying");
50087
+ async function control2(name, action) {
50088
+ const { t, z: z2 } = await zoneOf(name);
50089
+ await call((cb) => t.control(z2.zone_id, action, cb));
50090
+ const expect = { pause: ["paused"], stop: ["stopped", "paused"], play: ["playing", "loading"] };
50091
+ let state = officialZone(z2.display_name)?.state;
50092
+ if (expect[action]) {
50093
+ const end = Date.now() + 3e3;
50094
+ while (Date.now() < end && !expect[action].includes(state ?? "")) {
50095
+ await wait2(150);
50096
+ state = officialZone(z2.display_name)?.state;
50097
+ }
50098
+ }
50099
+ return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: z2.display_name, action, state };
50100
+ }
50101
+ __name(control2, "control");
50102
+ async function seek2(name, seconds) {
50103
+ const { t, z: z2 } = await zoneOf(name);
50104
+ await call((cb) => t.seek(z2.zone_id, "absolute", Math.max(0, Math.round(seconds)), cb));
50105
+ return { ok: true, zone: z2.display_name, position: duration4(seconds) };
50106
+ }
50107
+ __name(seek2, "seek");
50108
+ async function settings2(name, s) {
50109
+ if (s.roon_radio === "now") throw new RoonError('"Start Roon Radio now" needs the internal protocol (switched off)', "invalid");
50110
+ const { t, z: z2 } = await zoneOf(name);
50111
+ const change = {};
50112
+ if (s.shuffle !== void 0) change.shuffle = s.shuffle;
50113
+ if (s.repeat !== void 0) change.loop = { off: "disabled", all: "loop", one: "loop_one" }[s.repeat];
50114
+ if (s.roon_radio !== void 0) change.auto_radio = s.roon_radio;
50115
+ if (!Object.keys(change).length) throw new RoonError("Nothing to set: give shuffle, repeat or roon_radio", "invalid");
50116
+ await call((cb) => t.change_settings(z2.zone_id, change, cb));
50117
+ return { ok: true, zone: z2.display_name, ...s };
50118
+ }
50119
+ __name(settings2, "settings");
50120
+ async function transfer2(from, to) {
50121
+ const a = await zoneOf(from), b = await zoneOf(to);
50122
+ if (a.z.zone_id === b.z.zone_id) throw new RoonError("From and to are the same zone", "invalid");
50123
+ await call((cb) => a.t.transfer_zone(a.z.zone_id, b.z.zone_id, cb));
50124
+ return { ok: true, from: a.z.display_name, to: b.z.display_name };
50125
+ }
50126
+ __name(transfer2, "transfer");
50127
+ async function group2(main2, join) {
50128
+ const m = await zoneOf(main2);
50129
+ const outs = [...m.z.outputs.map((o) => o.output_id)];
50130
+ for (const n of join) {
50131
+ const j = await zoneOf(n);
50132
+ for (const o of j.z.outputs) if (!outs.includes(o.output_id)) outs.push(o.output_id);
50133
+ }
50134
+ if (outs.length === m.z.outputs.length) throw new RoonError("Nothing to group: name at least one other zone", "invalid");
50135
+ await call((cb) => m.t.group_outputs(outs, cb));
50136
+ return { ok: true, group: outs.map((id) => officialZones().flatMap((z2) => z2.outputs).find((o) => o.output_id === id)?.display_name ?? id) };
50137
+ }
50138
+ __name(group2, "group");
50139
+ async function ungroup2(name, only) {
50140
+ const { t, z: z2 } = await zoneOf(name);
50141
+ if (z2.outputs.length < 2) throw new RoonError(`${z2.display_name} isn't in a group`, "invalid");
50142
+ const off = only ? z2.outputs.filter((o) => o.display_name.toLowerCase().includes(only.toLowerCase())) : z2.outputs;
50143
+ if (!off.length) throw new RoonError(`"${only}" isn't in this group`, "not_found");
50144
+ await call((cb) => t.ungroup_outputs(off.map((o) => o.output_id), cb));
50145
+ return { ok: true, released: off.map((o) => o.display_name) };
50146
+ }
50147
+ __name(ungroup2, "ungroup");
50148
+ async function standby2(name, on) {
50149
+ const { t, z: z2 } = await zoneOf(name);
50150
+ for (const o of z2.outputs) await call((cb) => on ? t.standby(o.output_id, {}, cb) : t.convenience_switch(o.output_id, {}, cb));
50151
+ return { ok: true, zone: z2.display_name, standby: on };
50152
+ }
50153
+ __name(standby2, "standby");
50154
+ async function cover2(target2, name) {
50155
+ await officialReady();
50156
+ const z2 = name ? officialZone(name) : officialZones().find((x) => x.state === "playing" && x.now_playing?.image_key) ?? officialZones().find((x) => x.state === "paused" && x.now_playing?.image_key);
50157
+ if (!z2) throw new RoonError(name ? `No Roon zone called "${name}"` : "Nothing plays in Roon right now", "not_found");
50158
+ const key = z2.now_playing?.image_key;
50159
+ if (!key) throw new RoonError(`Roon has no cover for what plays on ${z2.display_name}`, "not_found");
50160
+ const r = await fetch(`http://${target2.host}:${target2.port}/api/image/${key}?scale=fit&width=600&height=600`).catch(() => null);
50161
+ if (!r?.ok) throw new RoonError("Roon doesn't hand over the cover right now", "unavailable");
50162
+ const l = z2.now_playing?.three_line;
50163
+ return {
50164
+ info: { zone: z2.display_name, title: l?.line1, artist: l?.line2, album: l?.line3 },
50165
+ image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
50166
+ };
50167
+ }
50188
50168
  __name(cover2, "cover");
50189
50169
 
50170
+ // src/tools/run.ts
50171
+ init_session();
50172
+ init_log();
50173
+
50174
+ // src/core/status.ts
50175
+ init_package();
50176
+ init_log();
50177
+ var checks = null;
50178
+ var checking = null;
50179
+ function selfTest(run2) {
50180
+ checking ??= run2().then((c) => {
50181
+ checks = c;
50182
+ const bad = c.filter((x) => !x.ok);
50183
+ log(bad.length ? `self-test: ${bad.map((x) => `${x.name} failed (${x.detail})`).join("; ")}` : `self-test: ${c.length} reads ok`);
50184
+ }).catch((e) => {
50185
+ log(`self-test couldn't run: ${e.message}`);
50186
+ });
50187
+ return checking;
50188
+ }
50189
+ __name(selfTest, "selfTest");
50190
+ var selfTestResults = /* @__PURE__ */ __name(() => checks, "selfTestResults");
50191
+ var newer = null;
50192
+ var told = false;
50193
+ var parse3 = /* @__PURE__ */ __name((v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0), "parse");
50194
+ var isNewer = /* @__PURE__ */ __name((a, b) => {
50195
+ const x = parse3(a), y = parse3(b);
50196
+ for (let i = 0; i < 3; i++) {
50197
+ if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
50198
+ }
50199
+ return false;
50200
+ }, "isNewer");
50201
+ async function checkForUpdate() {
50202
+ if (process.env.DEEP_ROON_NO_UPDATE_CHECK === "1") return;
50203
+ try {
50204
+ const r = await fetch("https://api.github.com/repos/renebouwmeester/deep-roon-mcp/releases/latest", {
50205
+ headers: { "User-Agent": `deep-roon-mcp/${version2}`, Accept: "application/vnd.github+json" },
50206
+ signal: AbortSignal.timeout(5e3)
50207
+ });
50208
+ if (!r.ok) return;
50209
+ const j = await r.json();
50210
+ if (j.tag_name && isNewer(j.tag_name, version2)) {
50211
+ newer = { version: j.tag_name.replace(/^v/, ""), url: j.html_url ?? "" };
50212
+ log(`a newer version is out: ${newer.version}`);
50213
+ }
50214
+ } catch {
50215
+ }
50216
+ }
50217
+ __name(checkForUpdate, "checkForUpdate");
50218
+ var updateInfo = /* @__PURE__ */ __name(() => newer, "updateInfo");
50219
+ function updateNotice() {
50220
+ if (!newer || told) return null;
50221
+ told = true;
50222
+ return `Deep Roon MCP ${newer.version} is available (this is ${version2}): ${newer.url}`;
50223
+ }
50224
+ __name(updateNotice, "updateNotice");
50225
+
50226
+ // src/tools/run.ts
50227
+ var compact = /* @__PURE__ */ __name((_k, v) => v === void 0 ? void 0 : typeof v === "bigint" ? v.toString() : v, "compact");
50228
+ async function run(work2) {
50229
+ try {
50230
+ const result = await work2();
50231
+ const notice = updateNotice();
50232
+ return { content: [{ type: "text", text: JSON.stringify(result, compact) }, ...notice ? [{ type: "text", text: notice }] : []] };
50233
+ } catch (e) {
50234
+ const err = e;
50235
+ let text2;
50236
+ if (err instanceof RoonError) text2 = err.message;
50237
+ else if (/MissingMethod|method not found|unknown method/i.test(err.message)) {
50238
+ text2 = `Roon changed this part of its internal protocol in this version (${err.message}). Check for an update of Deep Roon MCP.`;
50239
+ } else text2 = `Roon didn't answer as expected: ${err.message}`;
50240
+ log(`tool error: ${err.message}`);
50241
+ return { isError: true, content: [{ type: "text", text: text2 }] };
50242
+ }
50243
+ }
50244
+ __name(run, "run");
50245
+ async function runImage(work2) {
50246
+ let image = null;
50247
+ const r = await run(async () => {
50248
+ const x = await work2();
50249
+ image = x.image;
50250
+ return x.info;
50251
+ });
50252
+ if (r.isError || !image) return r;
50253
+ const { data, mimeType } = image;
50254
+ return { content: [...r.content, { type: "image", data, mimeType }] };
50255
+ }
50256
+ __name(runImage, "runImage");
50257
+
50258
+ // src/tools/read.ts
50259
+ var READ = { readOnlyHint: true, openWorldHint: false };
50260
+ var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
50261
+ var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
50262
+ function registerReadTools(server, internal = true) {
50263
+ const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
50264
+ const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
50265
+ reg("roon_search", {
50266
+ title: "Search Roon",
50267
+ 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.',
50268
+ inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
50269
+ annotations: READ
50270
+ }, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
50271
+ reg("roon_new", {
50272
+ title: "Streaming shelves in Roon",
50273
+ description: 'The editorial shelves of Qobuz or TIDAL as Roon shows them (the same for everyone). Qobuz (`service: "qobuz"`): `new`, `qobuzissimes`, `press_awards`, `top`, `trending`, `ideal_discography` \u2014 albums; `genre` narrows a shelf to one or more Qobuz genre ids from `roon_genres`. TIDAL (`service: "tidal"`): `new`, `recommended`, `top`, `rising`, `masters` \u2014 albums, or tracks with `what: "tracks"` (not masters); `genre` (one TIDAL genre from `roon_genres`, e.g. "Jazz") gives TIDAL\'s page for that genre instead of a shelf. Use `genre` for "new jazz", "what\'s new in classical"; TIDAL\'s playlists are in roon_tidal_playlists. Albums carry their format and a `key` for `roon_album`; tracks a `key` for roon_play_tracks. `offset` pages through the shelf; `total` says how many there are.',
50274
+ inputSchema: {
50275
+ service,
50276
+ section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
50277
+ genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
50278
+ what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
50279
+ limit: limit(100, 30),
50280
+ offset: external_exports.number().int().min(0).optional()
50281
+ },
50282
+ annotations: READ
50283
+ }, ({ service: sv, section: section2, genre, what, limit: n, offset: offset2 }) => run(() => section(section2 ?? "new", n ?? 30, offset2 ?? 0, (genre ?? "").split(",").map((x) => x.trim()).filter(Boolean), sv, what ?? "albums")));
50284
+ reg("roon_genres", {
50285
+ title: "Streaming genres",
50286
+ description: "The genres of Qobuz or TIDAL, for the `genre` filter of `roon_new` (and, for TIDAL, of `roon_tidal_playlists`). Qobuz: the main genres with their ids (names in the account's language); with `parent` (a genre id) its sub-genres, e.g. the kinds of jazz. TIDAL: its genres and its moods (Relax, Focus \u2026), each with an `id` to pass on.",
50287
+ inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
50288
+ annotations: READ
50289
+ }, ({ service: sv, parent }) => run(() => genres(parent, sv)));
50290
+ reg("roon_album", {
50291
+ title: "Album page",
50292
+ description: "One album in Roon, by its `key` (from roon_search, roon_new, roon_artist): the album with year, format, label and whether it is in the library, its genres, release date (and the date of this edition), barcode and \u2014 when Roon has them \u2014 catalogue number and country, its main artist(s) with a `key` for `roon_artist` / `roon_bio`, and every track with its own `key`. A streaming album that is also in the library comes back as the library copy (its key may differ; both work). `credits: true` adds who played, produced, engineered and wrote, grouped by role \u2014 Roon's own data.",
50293
+ inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
50294
+ annotations: READ
50295
+ }, ({ key, credits }) => run(() => album(key, credits === true)));
50296
+ reg("roon_review", {
50297
+ title: "Album review",
50298
+ description: "The review Roon shows on an album's page, in full, with its author \u2014 by album `key`. When the user asks for the review (\"what does Roon say about this album\", \"show me the review\"), they want to read the review itself: show the text complete and exactly as returned, as a quote with the author's name \u2014 no paraphrase or shortening unless they ask for a summary, and don't go to the web for it. `wikipedia: true` adds the album's Wikipedia text; it also comes by itself when Roon has no review of its own. `max_chars` only when a shorter excerpt is asked for.",
50299
+ inputSchema: { key: external_exports.string().describe("An album key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
50300
+ annotations: READ
50301
+ }, ({ key, wikipedia, max_chars }) => run(() => review(key, max_chars ?? 1e9, wikipedia === true)));
50302
+ reg("roon_bio", {
50303
+ title: "Artist biography",
50304
+ description: 'The biography Roon shows on an artist\'s page, in full, with its author \u2014 by artist `key` \u2014 plus where they are from, active since, genres, and the related artists Roon lists: `similar`, `influenced_by`, `influenced`, `worked_with`, `member_of`, `members`, each with a `key` for `roon_artist` / `roon_bio`. When the user asks for the bio ("who is \u2026", "tell me about this artist"), show the text complete and exactly as returned, with the author\'s name \u2014 no paraphrase or shortening unless they ask for a summary. For a narrower question ("who influenced them") answer from the lists. `wikipedia: true` adds the Wikipedia text; `max_chars` only when a shorter excerpt is asked for.',
50305
+ inputSchema: { key: external_exports.string().describe("An artist key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
50306
+ annotations: READ
50307
+ }, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
50308
+ reg("roon_artist", {
50309
+ title: "Discography",
50310
+ description: 'An artist\'s discography in Roon: the main albums Qobuz (or, with `service: "tidal"`, TIDAL) has, oldest first, each with year, format and a `key` for `roon_album`. The artist `key` comes from roon_search (`kind: "artist"`) or from the `artists` of roon_album. Use this for "the album before/after \u2026", "their debut", "what else did they make" \u2014 don\'t answer discography questions from memory. The year is the original release where Roon knows it; reissues and compilations are in the list too.',
50311
+ inputSchema: { key: external_exports.string().describe("An artist key."), service },
50312
+ annotations: READ
50313
+ }, ({ key, service: sv }) => run(() => discography(key, sv)));
50314
+ reg("roon_zones", {
50315
+ title: "Roon zones",
50316
+ description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
50317
+ inputSchema: {},
50318
+ annotations: READ
50319
+ }, () => run(() => internal ? zones2() : zones3()));
50320
+ reg("roon_now_playing", {
50321
+ title: "Now playing in Roon",
50322
+ description: "What plays in Roon, per zone: state (playing, loading, paused, stopped), the track with album, artist and format, the position, how many tracks are left in the queue, shuffle, repeat, Roon Radio and a sleep timer. Read this before saying anything about what Roon plays \u2014 it changes all the time.",
50323
+ inputSchema: {},
50324
+ annotations: READ
50325
+ }, () => run(() => internal ? nowPlaying() : nowPlaying2()));
50326
+ reg("roon_radio", {
50327
+ title: "Live Radio",
50328
+ description: 'Roon\'s Live Radio directory. Without arguments: the user\'s own stations (`my_stations`) and the top categories (Music, News & Talk, Sports, Locations), each with a `key`. With `category` (a key): its sub-categories and stations. With `genre` (a category\'s name as written, e.g. "Jazz", "Classical"): stations of that genre. Stations carry a `key` for roon_play_radio, their location and `image`.',
50329
+ inputSchema: { category: external_exports.string().optional().describe("A category key from a previous answer."), genre: external_exports.string().optional().describe("A genre, as roon_radio names it (e.g. Jazz)."), limit: limit(100, 40) },
50330
+ annotations: READ
50331
+ }, ({ category, genre, limit: n }) => run(() => radio(category, genre, n ?? 40)));
50332
+ reg("roon_history", {
50333
+ title: "Play history",
50334
+ description: `What was played in Roon, newest first \u2014 Roon's own play history, with how much of each track was heard. For "most played" questions use roon_top instead of counting this.`,
50335
+ inputSchema: { limit: limit(300, 30) },
50336
+ annotations: READ
50337
+ }, ({ limit: n }) => run(() => history(n ?? 30)));
50338
+ reg("roon_top", {
50339
+ title: "Most played",
50340
+ description: 'The most played albums, artists or tracks in Roon, counted over the WHOLE play history (a play counts when at least half the track was heard). Use this for "most listened" questions \u2014 don\'t tally roon_history yourself. `since` (YYYY-MM-DD) limits the period.',
50341
+ inputSchema: { what: external_exports.enum(["albums", "artists", "tracks"]).optional(), limit: limit(100, 20), since: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only plays from this date on (YYYY-MM-DD).") },
50342
+ annotations: READ
50343
+ }, ({ what, limit: n, since }) => run(() => top(what ?? "albums", n ?? 20, since)));
50344
+ }
50345
+ __name(registerReadTools, "registerReadTools");
50346
+
50190
50347
  // src/tools/control.ts
50191
50348
  init_official();
50192
50349
  init_session();
@@ -50210,18 +50367,24 @@ function registerControlTools(server, internal = true) {
50210
50367
  inputSchema: { keys: external_exports.array(external_exports.string()).min(1).max(60).describe("Track keys, in playing order."), zone, mode },
50211
50368
  annotations: PLAYS
50212
50369
  }, ({ keys, zone: zn, mode: m }) => run(() => playTracks(keys, zn, m ?? "now")));
50370
+ reg("roon_play_radio", {
50371
+ title: "Play a radio station",
50372
+ description: "Play a Live Radio station on a Roon zone: `key` from roon_radio, or the name of one of the user's own stations. Says whether the zone started. Only on an explicit request, never as a probe.",
50373
+ inputSchema: { key: external_exports.string().describe("A station key, or the name of an own station."), zone },
50374
+ annotations: PLAYS
50375
+ }, ({ key, zone: zn }) => run(() => playRadio(key, zn)));
50213
50376
  reg("roon_control", {
50214
50377
  title: "Transport",
50215
50378
  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.",
50216
50379
  inputSchema: { zone, action: external_exports.enum(["pause", "play", "playpause", "stop", "next", "previous", "release"]) },
50217
50380
  annotations: PLAYS
50218
- }, ({ 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)));
50381
+ }, ({ zone: zn, action }) => run(() => internal ? control(zn, action) : action === "release" ? Promise.reject(new Error("release needs Roon's internal protocol (switched off)")) : control2(zn, action)));
50219
50382
  reg("roon_seek", {
50220
50383
  title: "Seek",
50221
50384
  description: "Jump to a position in the track that plays on a Roon zone, in seconds from the start. Only on an explicit request.",
50222
50385
  inputSchema: { zone, seconds: external_exports.number().int().min(0) },
50223
50386
  annotations: PLAYS
50224
- }, ({ zone: zn, seconds }) => run(() => internal ? seek2(zn, seconds) : seek(zn, seconds)));
50387
+ }, ({ zone: zn, seconds }) => run(() => internal ? seek(zn, seconds) : seek2(zn, seconds)));
50225
50388
  reg("roon_volume", {
50226
50389
  title: "Volume",
50227
50390
  description: "Set the volume of a Roon zone to an absolute level, in that zone's own scale (roon_now_playing shows `volume` and `volume_max`), and/or mute or unmute it. Only on an explicit request; small steps unless a level is named. Needs Deep Roon MCP enabled in Roon (Settings \u203A Extensions).",
@@ -50235,7 +50398,7 @@ function registerControlTools(server, internal = true) {
50235
50398
  annotations: PLAYS
50236
50399
  }, ({ zone: zn, shuffle, repeat, roon_radio }) => {
50237
50400
  const s = { shuffle, repeat, roon_radio: roon_radio === void 0 ? void 0 : roon_radio === "now" ? "now" : roon_radio === "on" };
50238
- return run(() => internal ? settings2(zn, s) : settings(zn, s));
50401
+ return run(() => internal ? settings(zn, s) : settings2(zn, s));
50239
50402
  });
50240
50403
  reg("roon_queue", {
50241
50404
  title: "Queue",
@@ -50255,6 +50418,12 @@ function registerControlTools(server, internal = true) {
50255
50418
  inputSchema: { zone, ns: external_exports.array(external_exports.number().int().min(2)).min(1).max(100).describe("Positions from roon_queue (2 and up: 1 is what plays).") },
50256
50419
  annotations: CHANGES_QUEUE
50257
50420
  }, ({ zone: zn, ns }) => run(() => queueRemove(zn, ns)));
50421
+ reg("roon_queue_move", {
50422
+ title: "Move in the queue",
50423
+ description: "Move an item of the zone's queue next to another: `n` goes `before` or `after` item `to` (numbers from roon_queue; 1 is what plays). Only on an explicit request.",
50424
+ inputSchema: { zone, n: external_exports.number().int().min(2), to: external_exports.number().int().min(2), where: external_exports.enum(["before", "after"]).optional().describe("before (default) or after item `to`.") },
50425
+ annotations: CHANGES_QUEUE
50426
+ }, ({ zone: zn, n, to, where }) => run(() => queueMove(zn, n, to, where ?? "before")));
50258
50427
  reg("roon_queue_clear", {
50259
50428
  title: "Clear the queue",
50260
50429
  description: "Clear the queue of a Roon zone. By default the track that plays keeps playing and everything after it is removed; `everything: true` also removes the playing track (the zone falls silent). Only on an explicit request.",
@@ -50266,19 +50435,19 @@ function registerControlTools(server, internal = true) {
50266
50435
  description: `Move what plays \u2014 with its queue and position \u2014 from one Roon zone to another (Roon's own "Transfer zone"). The tool for "move the music to \u2026": never restart the album with roon_play for that. Read roon_now_playing first if the source zone isn't named.`,
50267
50436
  inputSchema: { from: external_exports.string().describe("The zone that plays now."), to: external_exports.string().describe("The zone it should move to.") },
50268
50437
  annotations: PLAYS
50269
- }, ({ from, to }) => run(() => internal ? transfer2(from, to) : transfer(from, to)));
50438
+ }, ({ from, to }) => run(() => internal ? transfer(from, to) : transfer2(from, to)));
50270
50439
  reg("roon_group", {
50271
50440
  title: "Group zones",
50272
50441
  description: 'Group Roon zones so they play the same music in sync (Roon\'s own "Group zones"). `zone` keeps its music and queue; `with` are the zones that join. Roon groups only outputs of the same kind. Only on an explicit request.',
50273
50442
  inputSchema: { zone: external_exports.string().describe("The zone whose music continues."), with: external_exports.array(external_exports.string()).min(1).describe("The zones that join.") },
50274
50443
  annotations: PLAYS
50275
- }, (a) => run(() => internal ? group2(a.zone, a.with) : group(a.zone, a.with)));
50444
+ }, (a) => run(() => internal ? group(a.zone, a.with) : group2(a.zone, a.with)));
50276
50445
  reg("roon_ungroup", {
50277
50446
  title: "Ungroup zones",
50278
50447
  description: "Undo a Roon zone group: `zone` is any member. Without `only` the whole group is dissolved; with `only` (a member's name) just that one leaves. Only on an explicit request.",
50279
50448
  inputSchema: { zone: external_exports.string().describe("A member of the group."), only: external_exports.string().optional() },
50280
50449
  annotations: PLAYS
50281
- }, ({ zone: zn, only }) => run(() => internal ? ungroup2(zn, only) : ungroup(zn, only)));
50450
+ }, ({ zone: zn, only }) => run(() => internal ? ungroup(zn, only) : ungroup2(zn, only)));
50282
50451
  reg("roon_sleep", {
50283
50452
  title: "Sleep timer",
50284
50453
  description: 'The sleep timer of a Roon zone: stop the music after `minutes` (1\u2013600), or `off: true` to cancel it. roon_now_playing shows a running timer. Only on an explicit request ("stop in half an hour").',
@@ -50293,13 +50462,13 @@ function registerControlTools(server, internal = true) {
50293
50462
  description: 'Put a Roon zone\'s device in standby (`action: "standby"`) or wake it (`action: "wake"`), where the device supports it. Only on an explicit request.',
50294
50463
  inputSchema: { zone, action: external_exports.enum(["standby", "wake"]) },
50295
50464
  annotations: PLAYS
50296
- }, ({ zone: zn, action }) => run(() => internal ? standby2(zn, action === "standby") : standby(zn, action === "standby")));
50465
+ }, ({ zone: zn, action }) => run(() => internal ? standby(zn, action === "standby") : standby2(zn, action === "standby")));
50297
50466
  reg("roon_cover", {
50298
50467
  title: "Cover",
50299
50468
  description: "The album cover of what plays in a Roon zone, as an image (about 600 px), with the title in text. Without `zone` it takes the zone that plays. Read-only.",
50300
50469
  inputSchema: { zone: external_exports.string().optional().describe("A Roon zone name (default: the zone that plays).") },
50301
50470
  annotations: { readOnlyHint: true, openWorldHint: false }
50302
- }, ({ zone: zn }) => runImage(async () => internal ? cover2(zn) : cover(await findTarget(), zn)));
50471
+ }, ({ zone: zn }) => runImage(async () => internal ? cover(zn) : cover2(await findTarget(), zn)));
50303
50472
  }
50304
50473
  __name(registerControlTools, "registerControlTools");
50305
50474
 
@@ -50317,7 +50486,7 @@ var localNow = /* @__PURE__ */ __name(() => {
50317
50486
  const d = /* @__PURE__ */ new Date(), t = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "t");
50318
50487
  return `${d.getFullYear()}-${t(d.getMonth() + 1)}-${t(d.getDate())}T${t(d.getHours())}:${t(d.getMinutes())}:${t(d.getSeconds())}`;
50319
50488
  }, "localNow");
50320
- var decoded = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
50489
+ var decoded2 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
50321
50490
  async function dailyPicks(perBox) {
50322
50491
  return withCore(async (c) => {
50323
50492
  const DP = "Sooloos.Broker.Api.DailyPicksParameters";
@@ -50333,7 +50502,7 @@ async function dailyPicks(perBox) {
50333
50502
  c.serviceOid("Library")
50334
50503
  );
50335
50504
  if (!res.success) throw new RoonError(`Roon refused the Daily Picks (${res.status})`);
50336
- const boxes = items(c, decoded(c, res.payload));
50505
+ const boxes = items(c, decoded2(c, res.payload));
50337
50506
  const things = /* @__PURE__ */ __name((b) => {
50338
50507
  for (const k of ["DisplayAlbums", "Albums", "Tracks", "Performers"]) {
50339
50508
  const l = obj(c, field2(b, k))?.fields?.$items ?? [];
@@ -50361,7 +50530,7 @@ async function newForYou(limit3, offset2) {
50361
50530
  return withCore(async (c) => {
50362
50531
  const res = await c.call("Library", "GetNewReleasesForYou", [{ type: "Sooid", name: "profileid" }, { type: "ResultCallback<DataList<AlbumWithExtras>>", name: "cb_result" }], buildArgs([Arg.sooid(c.profile())]), c.serviceOid("Library"));
50363
50532
  if (!res.success) throw new RoonError(`Roon refused New releases for you (${res.status})`);
50364
- const all = items(c, decoded(c, res.payload));
50533
+ const all = items(c, decoded2(c, res.payload));
50365
50534
  const part = all.slice(Math.max(0, offset2), Math.max(0, offset2) + Math.min(Math.max(1, limit3), 100));
50366
50535
  await untilLoaded(c, part.map((x) => field2(x, "Album")).filter(isRef2), 3e3);
50367
50536
  return { total: all.length, offset: Math.max(0, offset2), rows: part.map((x) => {
@@ -50389,7 +50558,7 @@ async function readMixes(c) {
50389
50558
  c.serviceOid("Library")
50390
50559
  );
50391
50560
  if (!res.success) throw new RoonError(`Roon refused the Daily Mixes (${res.status})`);
50392
- return items(c, decoded(c, res.payload));
50561
+ return items(c, decoded2(c, res.payload));
50393
50562
  }
50394
50563
  __name(readMixes, "readMixes");
50395
50564
  async function mixes() {
@@ -50520,7 +50689,7 @@ var profileObject = /* @__PURE__ */ __name((c) => {
50520
50689
  if (!p) throw new RoonError("Roon gave no profile");
50521
50690
  return p;
50522
50691
  }, "profileObject");
50523
- var decoded2 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
50692
+ var decoded3 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
50524
50693
  var PLAYLIST_KIND = { 1: "itunes", 5: "roon", 6: "roon (read-only)", 10: "TIDAL", 11: "file", 12: "qobuz", 13: "roon editorial", 15: "smart" };
50525
50694
  function allPlaylists(c) {
50526
50695
  const service2 = c.graph.findByType("Playlists")[0];
@@ -50550,7 +50719,7 @@ async function playlist(key, limit3) {
50550
50719
  const p = await findPlaylist2(c, key);
50551
50720
  const res = await c.call("Playlist", "GetItems", [{ type: "ResultCallback<Query<PlaylistItem>>", name: "cb_result" }], Buffer.alloc(0), p.oid);
50552
50721
  if (!res.success) throw new RoonError(`Roon refused the playlist (${res.status})`);
50553
- const v = decoded2(c, res.payload);
50722
+ const v = decoded3(c, res.payload);
50554
50723
  const n = Math.min(Math.max(1, limit3), 300);
50555
50724
  const end = Date.now() + 3e3;
50556
50725
  let list = [];
@@ -50684,7 +50853,7 @@ async function listenLater(includePlayed) {
50684
50853
  c.serviceOid("Library")
50685
50854
  );
50686
50855
  if (!res.success) throw new RoonError(`Roon refused Listen Later (${res.status})`);
50687
- const list = items(c, decoded2(c, res.payload));
50856
+ const list = items(c, decoded3(c, res.payload));
50688
50857
  const thing = /* @__PURE__ */ __name((it) => field2(it, "Album") ?? field2(it, "Track") ?? field2(it, "Performer"), "thing");
50689
50858
  await untilLoaded(c, list.map(thing).filter(isRef2), 2500);
50690
50859
  const rows = list.map((it) => {
@@ -50794,7 +50963,7 @@ async function similarAlbum(key, limit3) {
50794
50963
  buildArgs([Arg.ref(al.oid), Arg.sooid(c.profile())]),
50795
50964
  c.serviceOid("Library")
50796
50965
  ), 8e3);
50797
- const recommended = res?.success ? items(c, decoded2(c, res.payload)) : [];
50966
+ const recommended = res?.success ? items(c, decoded3(c, res.payload)) : [];
50798
50967
  const similar = ["SimilarAlbums", "SoundsSimilar", "SeeAlso", "WasInfluencedBy", "WasInspiredBy"].flatMap((k) => items(c, field2(A, k)));
50799
50968
  await untilLoaded(c, [...recommended, ...similar].map((o) => ({ $ref: o.oid })), 2500);
50800
50969
  return { album: albumRow(c, al), similar: unique(similar), recommended: unique(recommended) };
@@ -50812,7 +50981,7 @@ async function similarArtist(key, limit3) {
50812
50981
  buildArgs([Arg.ref(lv.$ref), Arg.sooid(c.profile())]),
50813
50982
  c.serviceOid("Library")
50814
50983
  ), 8e3);
50815
- const w = res?.success ? obj(c, decoded2(c, res.payload)) : null;
50984
+ const w = res?.success ? obj(c, decoded3(c, res.payload)) : null;
50816
50985
  const list = /* @__PURE__ */ __name((k) => items(c, field2(w, k)), "list");
50817
50986
  await untilLoaded(c, ["RecommendedAlbums", "LatestAlbums", "AppearsOnAlbums"].flatMap(list).map((o) => ({ $ref: o.oid })), 2500);
50818
50987
  const rows = /* @__PURE__ */ __name((k) => list(k).filter((o) => field2(o, "Title") !== void 0).slice(0, limit3).map((o) => albumRow(c, o)), "rows");
@@ -50941,7 +51110,7 @@ async function dspOf(c, endpoint) {
50941
51110
  const cc = obj(c, field2(obj(c, field2(endpoint, "Device")), "CommonConfig"));
50942
51111
  if (!cc || field2(cc, "SupportsDspConfig") !== true) return null;
50943
51112
  const res = await short(c.call("AudioDeviceCommonConfig", "GetDspConfig", [{ type: "ResultCallback<DspConfig>", name: "conf" }], Buffer.alloc(0), cc.oid), 6e3);
50944
- const v = res?.success ? decoded2(c, res.payload) : null;
51113
+ const v = res?.success ? decoded3(c, res.payload) : null;
50945
51114
  if (!isRef2(v)) return null;
50946
51115
  const end = Date.now() + 2500;
50947
51116
  while (Date.now() < end && field2(c.graph.getObject(v.$ref), "BeforeItems") === void 0 && field2(c.graph.getObject(v.$ref), "Items") === void 0) await wait(50);