deep-roon-mcp 0.3.6 → 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.6";
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
  }
@@ -16353,7 +16354,12 @@ async function bio(key, max, wiki) {
16353
16354
  for (const n of names2) for (const x of list(n)) {
16354
16355
  const o = obj(c, x);
16355
16356
  const name = field2(o, "Name");
16356
- if (name && !out.some((y) => y.name === name)) out.push({ key: String(field2(o, "PerformerId") ?? "") || void 0, name });
16357
+ if (name && !out.some((y) => y.name === name)) {
16358
+ const r = { key: String(field2(o, "PerformerId") ?? "") || void 0, name };
16359
+ const img2 = imageKey(c, o, "Photo") ?? imageKey(c, o, "Avatar");
16360
+ if (img2) r.image = img2;
16361
+ out.push(r);
16362
+ }
16357
16363
  }
16358
16364
  return out.length ? out.slice(0, 12) : void 0;
16359
16365
  }, "people");
@@ -16481,13 +16487,13 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
16481
16487
  const missing = members2.find((f) => !schema.some((m) => m.shortName === f.name));
16482
16488
  if (missing) throw new RoonError(`The filter ${missing.name} doesn't exist for ${kind.toLowerCase()}s in this Roon version`, "invalid");
16483
16489
  const size = Math.min(Math.max(1, limit3), 100), page = Math.floor(Math.max(0, offset2) / size);
16484
- const criteria = c.structArg(`Sooloos.Broker.Api.${kind}QueryCriteria`, [english(), ...members2]);
16490
+ const criteria2 = c.structArg(`Sooloos.Broker.Api.${kind}QueryCriteria`, [english(), ...members2]);
16485
16491
  const params = c.structArg("Sooloos.Broker.Api.VirtualQueryParameters", [{ name: "PageSize", propType: 0, value: new BinaryWriter().integer(size).toBuffer() }]);
16486
16492
  const res = await c.call(
16487
16493
  "Library",
16488
16494
  `Virtual${kind}Query`,
16489
16495
  [{ type: "Sooid", name: "profileid" }, { type: `${kind}QueryCriteria`, name: "criteria" }, { type: "VirtualQueryParameters", name: "queryparams" }, { type: `ResultCallback<Virtual${kind}LiteQuery>`, name: "cb_result" }],
16490
- Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria, params]),
16496
+ Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2, params]),
16491
16497
  c.serviceOid("Library")
16492
16498
  );
16493
16499
  if (!res.success) throw new RoonError(`Roon refused the ${kind.toLowerCase()} query (${res.status})`);
@@ -16522,12 +16528,12 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
16522
16528
  }
16523
16529
  }
16524
16530
  async function albumInfo(c, members2) {
16525
- const criteria = c.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [english(), ...members2]);
16531
+ const criteria2 = c.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [english(), ...members2]);
16526
16532
  const res = await c.call(
16527
16533
  "Library",
16528
16534
  "GetAlbumQueryInfo",
16529
16535
  [{ type: "Sooid", name: "profileid" }, { type: "AlbumQueryCriteria", name: "criteria" }, { type: "ResultCallback<AlbumQueryInfo>", name: "cb_result" }],
16530
- Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria]),
16536
+ Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2]),
16531
16537
  c.serviceOid("Library")
16532
16538
  );
16533
16539
  if (!res.success) throw new RoonError(`Roon refused the Focus panel (${res.status})`);
@@ -17746,14 +17752,14 @@ function isValidJWT(jwt2, alg) {
17746
17752
  if (!header)
17747
17753
  return false;
17748
17754
  const base643 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "=");
17749
- const decoded3 = JSON.parse(atob(base643));
17750
- if (typeof decoded3 !== "object" || decoded3 === null)
17755
+ const decoded4 = JSON.parse(atob(base643));
17756
+ if (typeof decoded4 !== "object" || decoded4 === null)
17751
17757
  return false;
17752
- if ("typ" in decoded3 && decoded3?.typ !== "JWT")
17758
+ if ("typ" in decoded4 && decoded4?.typ !== "JWT")
17753
17759
  return false;
17754
- if (!decoded3.alg)
17760
+ if (!decoded4.alg)
17755
17761
  return false;
17756
- if (alg && decoded3.alg !== alg)
17762
+ if (alg && decoded4.alg !== alg)
17757
17763
  return false;
17758
17764
  return true;
17759
17765
  } catch {
@@ -21978,8 +21984,8 @@ function uint8ArrayToBase64url(bytes) {
21978
21984
  return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
21979
21985
  }
21980
21986
  __name(uint8ArrayToBase64url, "uint8ArrayToBase64url");
21981
- function hexToUint8Array(hex3) {
21982
- const cleanHex = hex3.replace(/^0x/, "");
21987
+ function hexToUint8Array(hex4) {
21988
+ const cleanHex = hex4.replace(/^0x/, "");
21983
21989
  if (cleanHex.length % 2 !== 0) {
21984
21990
  throw new Error("Invalid hex string length");
21985
21991
  }
@@ -49290,14 +49296,32 @@ init_library();
49290
49296
  init_intern();
49291
49297
  init_session();
49292
49298
  init_graph();
49299
+ init_library();
49293
49300
  async function history(limit3) {
49294
49301
  return withCore(async (first) => {
49295
49302
  const c = first.graph.findByType("HistoryPlay").length > 0 ? await fresh() : first;
49296
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
+ }
49297
49310
  closeSoon("after the history");
49298
49311
  return {
49299
49312
  total: out.total ?? null,
49300
- plays: out.events.map((e) => ({ at: localTime(e.playedAt), title: e.title, artist: clean(e.artist), album: e.album ?? void 0, played_pct: e.completionPct ?? void 0 }))
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
+ })
49301
49325
  };
49302
49326
  });
49303
49327
  }
@@ -49450,312 +49474,13 @@ async function nowPlaying() {
49450
49474
  }
49451
49475
  __name(nowPlaying, "nowPlaying");
49452
49476
 
49453
- // src/core/officialtools.ts
49454
- init_errors();
49455
- init_official();
49456
- var call = /* @__PURE__ */ __name((fn) => new Promise((res, rej) => fn((err) => err ? rej(new RoonError(`Roon refused: ${err}`)) : res())), "call");
49457
- var duration4 = /* @__PURE__ */ __name((s) => {
49458
- const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), r = t % 60;
49459
- return h ? `${h}:${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}` : `${m}:${String(r).padStart(2, "0")}`;
49460
- }, "duration");
49461
- var wait2 = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "wait");
49462
- async function zoneOf(name) {
49463
- const t = await paired();
49464
- await officialReady();
49465
- const z2 = officialZone(name);
49466
- if (!z2) throw new RoonError(`No Roon zone called "${name}"; Roon has: ${officialZones().map((x) => x.display_name).join(", ")}`, "not_found");
49467
- return { t, z: z2 };
49468
- }
49469
- __name(zoneOf, "zoneOf");
49470
- async function zones3() {
49471
- await officialReady();
49472
- const all = officialZones();
49473
- return {
49474
- zones: [...new Set(all.flatMap((z2) => z2.outputs.map((o) => o.display_name)))].sort(),
49475
- groups: all.filter((z2) => z2.outputs.length > 1).map((z2) => z2.outputs.map((o) => o.display_name))
49476
- };
49477
- }
49478
- __name(zones3, "zones");
49479
- async function nowPlaying2() {
49480
- await officialReady();
49481
- return { zones: officialZones().map((z2) => {
49482
- const r = { zone: z2.display_name, state: z2.state };
49483
- const v = z2.outputs.find((o) => o.volume)?.volume;
49484
- if (v) {
49485
- r.volume = v.value;
49486
- r.volume_max = v.max;
49487
- if (v.is_muted) r.muted = true;
49488
- }
49489
- if (z2.settings?.shuffle) r.shuffle = true;
49490
- if (z2.settings?.loop && z2.settings.loop !== "disabled") r.repeat = z2.settings.loop === "loop_one" ? "one" : "all";
49491
- if (z2.settings?.auto_radio) r.roon_radio = true;
49492
- const np = z2.now_playing;
49493
- if (np && z2.state !== "stopped") {
49494
- if (np.three_line?.line1) {
49495
- r.title = np.three_line.line1;
49496
- if (np.three_line.line2) r.artist = np.three_line.line2;
49497
- if (np.three_line.line3) r.album = np.three_line.line3;
49498
- }
49499
- if (typeof np.seek_position === "number") r.position = duration4(np.seek_position);
49500
- if (typeof np.length === "number") r.length = duration4(np.length);
49501
- }
49502
- return r;
49503
- }) };
49504
- }
49505
- __name(nowPlaying2, "nowPlaying");
49506
- async function control(name, action) {
49507
- const { t, z: z2 } = await zoneOf(name);
49508
- await call((cb) => t.control(z2.zone_id, action, cb));
49509
- const expect = { pause: ["paused"], stop: ["stopped", "paused"], play: ["playing", "loading"] };
49510
- let state = officialZone(z2.display_name)?.state;
49511
- if (expect[action]) {
49512
- const end = Date.now() + 3e3;
49513
- while (Date.now() < end && !expect[action].includes(state ?? "")) {
49514
- await wait2(150);
49515
- state = officialZone(z2.display_name)?.state;
49516
- }
49517
- }
49518
- return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: z2.display_name, action, state };
49519
- }
49520
- __name(control, "control");
49521
- async function seek(name, seconds) {
49522
- const { t, z: z2 } = await zoneOf(name);
49523
- await call((cb) => t.seek(z2.zone_id, "absolute", Math.max(0, Math.round(seconds)), cb));
49524
- return { ok: true, zone: z2.display_name, position: duration4(seconds) };
49525
- }
49526
- __name(seek, "seek");
49527
- async function settings(name, s) {
49528
- if (s.roon_radio === "now") throw new RoonError('"Start Roon Radio now" needs the internal protocol (switched off)', "invalid");
49529
- const { t, z: z2 } = await zoneOf(name);
49530
- const change = {};
49531
- if (s.shuffle !== void 0) change.shuffle = s.shuffle;
49532
- if (s.repeat !== void 0) change.loop = { off: "disabled", all: "loop", one: "loop_one" }[s.repeat];
49533
- if (s.roon_radio !== void 0) change.auto_radio = s.roon_radio;
49534
- if (!Object.keys(change).length) throw new RoonError("Nothing to set: give shuffle, repeat or roon_radio", "invalid");
49535
- await call((cb) => t.change_settings(z2.zone_id, change, cb));
49536
- return { ok: true, zone: z2.display_name, ...s };
49537
- }
49538
- __name(settings, "settings");
49539
- async function transfer(from, to) {
49540
- const a = await zoneOf(from), b = await zoneOf(to);
49541
- if (a.z.zone_id === b.z.zone_id) throw new RoonError("From and to are the same zone", "invalid");
49542
- await call((cb) => a.t.transfer_zone(a.z.zone_id, b.z.zone_id, cb));
49543
- return { ok: true, from: a.z.display_name, to: b.z.display_name };
49544
- }
49545
- __name(transfer, "transfer");
49546
- async function group(main2, join) {
49547
- const m = await zoneOf(main2);
49548
- const outs = [...m.z.outputs.map((o) => o.output_id)];
49549
- for (const n of join) {
49550
- const j = await zoneOf(n);
49551
- for (const o of j.z.outputs) if (!outs.includes(o.output_id)) outs.push(o.output_id);
49552
- }
49553
- if (outs.length === m.z.outputs.length) throw new RoonError("Nothing to group: name at least one other zone", "invalid");
49554
- await call((cb) => m.t.group_outputs(outs, cb));
49555
- return { ok: true, group: outs.map((id) => officialZones().flatMap((z2) => z2.outputs).find((o) => o.output_id === id)?.display_name ?? id) };
49556
- }
49557
- __name(group, "group");
49558
- async function ungroup(name, only) {
49559
- const { t, z: z2 } = await zoneOf(name);
49560
- if (z2.outputs.length < 2) throw new RoonError(`${z2.display_name} isn't in a group`, "invalid");
49561
- const off = only ? z2.outputs.filter((o) => o.display_name.toLowerCase().includes(only.toLowerCase())) : z2.outputs;
49562
- if (!off.length) throw new RoonError(`"${only}" isn't in this group`, "not_found");
49563
- await call((cb) => t.ungroup_outputs(off.map((o) => o.output_id), cb));
49564
- return { ok: true, released: off.map((o) => o.display_name) };
49565
- }
49566
- __name(ungroup, "ungroup");
49567
- async function standby(name, on) {
49568
- const { t, z: z2 } = await zoneOf(name);
49569
- for (const o of z2.outputs) await call((cb) => on ? t.standby(o.output_id, {}, cb) : t.convenience_switch(o.output_id, {}, cb));
49570
- return { ok: true, zone: z2.display_name, standby: on };
49571
- }
49572
- __name(standby, "standby");
49573
- async function cover(target2, name) {
49574
- await officialReady();
49575
- 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);
49576
- if (!z2) throw new RoonError(name ? `No Roon zone called "${name}"` : "Nothing plays in Roon right now", "not_found");
49577
- const key = z2.now_playing?.image_key;
49578
- if (!key) throw new RoonError(`Roon has no cover for what plays on ${z2.display_name}`, "not_found");
49579
- const r = await fetch(`http://${target2.host}:${target2.port}/api/image/${key}?scale=fit&width=600&height=600`).catch(() => null);
49580
- if (!r?.ok) throw new RoonError("Roon doesn't hand over the cover right now", "unavailable");
49581
- const l = z2.now_playing?.three_line;
49582
- return {
49583
- info: { zone: z2.display_name, title: l?.line1, artist: l?.line2, album: l?.line3 },
49584
- image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
49585
- };
49586
- }
49587
- __name(cover, "cover");
49588
-
49589
- // src/tools/run.ts
49477
+ // src/roon/radio.ts
49478
+ init_intern();
49479
+ init_serializer();
49480
+ init_writer();
49590
49481
  init_session();
49591
49482
  init_log();
49592
-
49593
- // src/core/status.ts
49594
- init_package();
49595
- init_log();
49596
- var checks = null;
49597
- var checking = null;
49598
- function selfTest(run2) {
49599
- checking ??= run2().then((c) => {
49600
- checks = c;
49601
- const bad = c.filter((x) => !x.ok);
49602
- log(bad.length ? `self-test: ${bad.map((x) => `${x.name} failed (${x.detail})`).join("; ")}` : `self-test: ${c.length} reads ok`);
49603
- }).catch((e) => {
49604
- log(`self-test couldn't run: ${e.message}`);
49605
- });
49606
- return checking;
49607
- }
49608
- __name(selfTest, "selfTest");
49609
- var selfTestResults = /* @__PURE__ */ __name(() => checks, "selfTestResults");
49610
- var newer = null;
49611
- var told = false;
49612
- var parse3 = /* @__PURE__ */ __name((v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0), "parse");
49613
- var isNewer = /* @__PURE__ */ __name((a, b) => {
49614
- const x = parse3(a), y = parse3(b);
49615
- for (let i = 0; i < 3; i++) {
49616
- if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
49617
- }
49618
- return false;
49619
- }, "isNewer");
49620
- async function checkForUpdate() {
49621
- if (process.env.DEEP_ROON_NO_UPDATE_CHECK === "1") return;
49622
- try {
49623
- const r = await fetch("https://api.github.com/repos/renebouwmeester/deep-roon-mcp/releases/latest", {
49624
- headers: { "User-Agent": `deep-roon-mcp/${version2}`, Accept: "application/vnd.github+json" },
49625
- signal: AbortSignal.timeout(5e3)
49626
- });
49627
- if (!r.ok) return;
49628
- const j = await r.json();
49629
- if (j.tag_name && isNewer(j.tag_name, version2)) {
49630
- newer = { version: j.tag_name.replace(/^v/, ""), url: j.html_url ?? "" };
49631
- log(`a newer version is out: ${newer.version}`);
49632
- }
49633
- } catch {
49634
- }
49635
- }
49636
- __name(checkForUpdate, "checkForUpdate");
49637
- var updateInfo = /* @__PURE__ */ __name(() => newer, "updateInfo");
49638
- function updateNotice() {
49639
- if (!newer || told) return null;
49640
- told = true;
49641
- return `Deep Roon MCP ${newer.version} is available (this is ${version2}): ${newer.url}`;
49642
- }
49643
- __name(updateNotice, "updateNotice");
49644
-
49645
- // src/tools/run.ts
49646
- var compact = /* @__PURE__ */ __name((_k, v) => v === void 0 ? void 0 : typeof v === "bigint" ? v.toString() : v, "compact");
49647
- async function run(work2) {
49648
- try {
49649
- const result = await work2();
49650
- const notice = updateNotice();
49651
- return { content: [{ type: "text", text: JSON.stringify(result, compact) }, ...notice ? [{ type: "text", text: notice }] : []] };
49652
- } catch (e) {
49653
- const err = e;
49654
- let text2;
49655
- if (err instanceof RoonError) text2 = err.message;
49656
- else if (/MissingMethod|method not found|unknown method/i.test(err.message)) {
49657
- text2 = `Roon changed this part of its internal protocol in this version (${err.message}). Check for an update of Deep Roon MCP.`;
49658
- } else text2 = `Roon didn't answer as expected: ${err.message}`;
49659
- log(`tool error: ${err.message}`);
49660
- return { isError: true, content: [{ type: "text", text: text2 }] };
49661
- }
49662
- }
49663
- __name(run, "run");
49664
- async function runImage(work2) {
49665
- let image = null;
49666
- const r = await run(async () => {
49667
- const x = await work2();
49668
- image = x.image;
49669
- return x.info;
49670
- });
49671
- if (r.isError || !image) return r;
49672
- const { data, mimeType } = image;
49673
- return { content: [...r.content, { type: "image", data, mimeType }] };
49674
- }
49675
- __name(runImage, "runImage");
49676
-
49677
- // src/tools/read.ts
49678
- var READ = { readOnlyHint: true, openWorldHint: false };
49679
- var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
49680
- var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
49681
- function registerReadTools(server, internal = true) {
49682
- const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
49683
- const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
49684
- reg("roon_search", {
49685
- title: "Search Roon",
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.',
49687
- inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
49688
- annotations: READ
49689
- }, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
49690
- reg("roon_new", {
49691
- title: "Streaming shelves in Roon",
49692
- 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.',
49693
- inputSchema: {
49694
- service,
49695
- section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
49696
- genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
49697
- what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
49698
- limit: limit(100, 30),
49699
- offset: external_exports.number().int().min(0).optional()
49700
- },
49701
- annotations: READ
49702
- }, ({ 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")));
49703
- reg("roon_genres", {
49704
- title: "Streaming genres",
49705
- 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.",
49706
- inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
49707
- annotations: READ
49708
- }, ({ service: sv, parent }) => run(() => genres(parent, sv)));
49709
- reg("roon_album", {
49710
- title: "Album page",
49711
- 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.",
49712
- inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
49713
- annotations: READ
49714
- }, ({ key, credits }) => run(() => album(key, credits === true)));
49715
- reg("roon_review", {
49716
- title: "Album review",
49717
- 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.",
49718
- inputSchema: { key: external_exports.string().describe("An album key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
49719
- annotations: READ
49720
- }, ({ key, wikipedia, max_chars }) => run(() => review(key, max_chars ?? 1e9, wikipedia === true)));
49721
- reg("roon_bio", {
49722
- title: "Artist biography",
49723
- 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.',
49724
- inputSchema: { key: external_exports.string().describe("An artist key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
49725
- annotations: READ
49726
- }, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
49727
- reg("roon_artist", {
49728
- title: "Discography",
49729
- 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.',
49730
- inputSchema: { key: external_exports.string().describe("An artist key."), service },
49731
- annotations: READ
49732
- }, ({ key, service: sv }) => run(() => discography(key, sv)));
49733
- reg("roon_zones", {
49734
- title: "Roon zones",
49735
- description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
49736
- inputSchema: {},
49737
- annotations: READ
49738
- }, () => run(() => internal ? zones2() : zones3()));
49739
- reg("roon_now_playing", {
49740
- title: "Now playing in Roon",
49741
- 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.",
49742
- inputSchema: {},
49743
- annotations: READ
49744
- }, () => run(() => internal ? nowPlaying() : nowPlaying2()));
49745
- reg("roon_history", {
49746
- title: "Play history",
49747
- 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.`,
49748
- inputSchema: { limit: limit(300, 30) },
49749
- annotations: READ
49750
- }, ({ limit: n }) => run(() => history(n ?? 30)));
49751
- reg("roon_top", {
49752
- title: "Most played",
49753
- 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.',
49754
- 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).") },
49755
- annotations: READ
49756
- }, ({ what, limit: n, since }) => run(() => top(what ?? "albums", n ?? 20, since)));
49757
- }
49758
- __name(registerReadTools, "registerReadTools");
49483
+ init_graph();
49759
49484
 
49760
49485
  // src/roon/control.ts
49761
49486
  init_intern();
@@ -49889,7 +49614,7 @@ async function playTracks(keys, zoneName, how) {
49889
49614
  }
49890
49615
  __name(playTracks, "playTracks");
49891
49616
  var BUTTONS = { pause: "Pause", play: "Play", playpause: "PlayPause", stop: "Stop", next: "Next", previous: "Previous" };
49892
- async function control2(zoneName, action) {
49617
+ async function control(zoneName, action) {
49893
49618
  if (action === "release") {
49894
49619
  return withCore(async (c) => {
49895
49620
  const z2 = findZone(c, zoneName);
@@ -49917,8 +49642,8 @@ async function control2(zoneName, action) {
49917
49642
  }
49918
49643
  return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: r.zone, action, state };
49919
49644
  }
49920
- __name(control2, "control");
49921
- async function seek2(zoneName, seconds) {
49645
+ __name(control, "control");
49646
+ async function seek(zoneName, seconds) {
49922
49647
  return withCore(async (c) => {
49923
49648
  const z2 = findZone(c, zoneName);
49924
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);
@@ -49926,8 +49651,8 @@ async function seek2(zoneName, seconds) {
49926
49651
  return { ok: true, zone: z2.name, position: duration3(seconds) };
49927
49652
  });
49928
49653
  }
49929
- __name(seek2, "seek");
49930
- async function settings2(zoneName, s) {
49654
+ __name(seek, "seek");
49655
+ async function settings(zoneName, s) {
49931
49656
  return withCore(async (c) => {
49932
49657
  const z2 = findZone(c, zoneName);
49933
49658
  const send = /* @__PURE__ */ __name((method, params, args) => c.remoting.callMethodNoReply(z2.oid, formatMethodSignature("Zone", method, params), args), "send");
@@ -49952,7 +49677,7 @@ async function settings2(zoneName, s) {
49952
49677
  return { ok: true, zone: z2.name, ...done };
49953
49678
  });
49954
49679
  }
49955
- __name(settings2, "settings");
49680
+ __name(settings, "settings");
49956
49681
  function queueOf(c, zoneName) {
49957
49682
  const z2 = findZone(c, zoneName);
49958
49683
  const q = obj(c, field2(c.graph.getObject(z2.oid), "Queue"));
@@ -49987,6 +49712,63 @@ function queueRow(c, oid) {
49987
49712
  return r;
49988
49713
  }
49989
49714
  __name(queueRow, "queueRow");
49715
+ async function queueItemAt(c, zoneName, n) {
49716
+ const { z: z2, q } = queueOf(c, zoneName);
49717
+ const refs = await queueItems(c, q);
49718
+ const at = Math.max(0, field2(c.graph.getObject(q.oid), "CurrentItemIndex") ?? 0);
49719
+ const oid = refs[at + n - 1];
49720
+ if (oid === void 0) throw new RoonError(`The queue of ${z2.name} has no item ${n} (${Math.max(0, refs.length - at)} from here)`, "invalid");
49721
+ return { z: z2, q, oid, refs, at };
49722
+ }
49723
+ __name(queueItemAt, "queueItemAt");
49724
+ async function queueJump(zoneName, n) {
49725
+ const r = await withCore(async (c) => {
49726
+ const { z: z2, q, oid } = await queueItemAt(c, zoneName, n);
49727
+ c.remoting.callMethodNoReply(q.oid, formatMethodSignature("Queue", "PlayFromHere", [{ type: "TransportItem", name: "item" }]), buildArgs([Arg.ref(oid)]));
49728
+ log(`queue_jump: ${z2.name} \u2192 item ${n}`);
49729
+ return { zone: z2.name, n };
49730
+ });
49731
+ return { ok: true, ...r, ...await started2(r.zone, PRIORITY["now"]) };
49732
+ }
49733
+ __name(queueJump, "queueJump");
49734
+ async function queueRemove(zoneName, ns) {
49735
+ return withCore(async (c) => {
49736
+ const { z: z2, q } = queueOf(c, zoneName);
49737
+ const refs = await queueItems(c, q);
49738
+ const at = Math.max(0, field2(c.graph.getObject(q.oid), "CurrentItemIndex") ?? 0);
49739
+ const oids = [...new Set(ns)].map((n) => refs[at + n - 1]).filter((o) => o !== void 0);
49740
+ if (!oids.length) throw new RoonError("No such items in the queue", "invalid");
49741
+ c.remoting.callMethodNoReply(q.oid, formatMethodSignature("Queue", "RemoveItems", [{ type: "IEnumerable<TransportItem>", name: "items" }]), buildArgs([Arg.collection(oids.map((o) => buildArgs([Arg.ref(o)])))]));
49742
+ log(`queue_remove: ${z2.name} \u2212 ${oids.length} item(s)`);
49743
+ await wait(300);
49744
+ const after = await queueItems(c, q);
49745
+ return { ok: true, zone: z2.name, removed: oids.length, left: Math.max(0, after.length - at) };
49746
+ });
49747
+ }
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");
49990
49772
  async function queue2(zoneName, limit3) {
49991
49773
  return withCore(async (c) => {
49992
49774
  const { z: z2, q } = queueOf(c, zoneName);
@@ -50022,7 +49804,7 @@ var outputsOf = /* @__PURE__ */ __name((c, zone3) => c.graph.findByType("Endpoin
50022
49804
  const z2 = field2(e, "Zone");
50023
49805
  return isRef2(z2) && String(z2.$ref) === String(zone3) && typeof field2(e, "Name") === "string";
50024
49806
  }), "outputsOf");
50025
- async function group2(main2, join) {
49807
+ async function group(main2, join) {
50026
49808
  return withCore(async (c) => {
50027
49809
  const h = findZone(c, main2);
50028
49810
  const own2 = outputsOf(c, h.oid);
@@ -50048,8 +49830,8 @@ async function group2(main2, join) {
50048
49830
  return { ok: true, group: [...own2, ...added].map((e) => field2(e, "Name")) };
50049
49831
  });
50050
49832
  }
50051
- __name(group2, "group");
50052
- async function ungroup2(zoneName, only) {
49833
+ __name(group, "group");
49834
+ async function ungroup(zoneName, only) {
50053
49835
  return withCore(async (c) => {
50054
49836
  const z2 = findZone(c, zoneName);
50055
49837
  const all = outputsOf(c, z2.oid);
@@ -50067,8 +49849,8 @@ async function ungroup2(zoneName, only) {
50067
49849
  return { ok: true, released: off.map((e) => field2(e, "Name")) };
50068
49850
  });
50069
49851
  }
50070
- __name(ungroup2, "ungroup");
50071
- async function transfer2(from, to) {
49852
+ __name(ungroup, "ungroup");
49853
+ async function transfer(from, to) {
50072
49854
  return withCore(async (c) => {
50073
49855
  const a = findZone(c, from), b = findZone(c, to);
50074
49856
  if (a.oid === b.oid) throw new RoonError("From and to are the same zone", "invalid");
@@ -50083,7 +49865,7 @@ async function transfer2(from, to) {
50083
49865
  return { ok: true, from: a.name, to: b.name };
50084
49866
  });
50085
49867
  }
50086
- __name(transfer2, "transfer");
49868
+ __name(transfer, "transfer");
50087
49869
  async function sleep(zoneName, minutes) {
50088
49870
  return withCore(async (c) => {
50089
49871
  const z2 = findZone(c, zoneName);
@@ -50096,7 +49878,7 @@ async function sleep(zoneName, minutes) {
50096
49878
  });
50097
49879
  }
50098
49880
  __name(sleep, "sleep");
50099
- async function standby2(zoneName, on) {
49881
+ async function standby(zoneName, on) {
50100
49882
  return withCore(async (c) => {
50101
49883
  const z2 = findZone(c, zoneName);
50102
49884
  const outs = outputsOf(c, z2.oid).filter((e) => field2(e, "SupportsStandby") === true);
@@ -50108,8 +49890,8 @@ async function standby2(zoneName, on) {
50108
49890
  return { ok: true, zone: z2.name, standby: on };
50109
49891
  });
50110
49892
  }
50111
- __name(standby2, "standby");
50112
- async function cover2(zoneName) {
49893
+ __name(standby, "standby");
49894
+ async function cover(zoneName) {
50113
49895
  const t = await findTarget();
50114
49896
  const info = await withCore(async (c) => {
50115
49897
  const pick2 = zoneName ? findZone(c, zoneName) : null;
@@ -50138,11 +49920,434 @@ async function cover2(zoneName) {
50138
49920
  image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
50139
49921
  };
50140
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
+ }
50141
50168
  __name(cover2, "cover");
50142
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
+
50143
50347
  // src/tools/control.ts
50144
50348
  init_official();
50145
50349
  init_session();
50350
+ var CHANGES_QUEUE = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
50146
50351
  var PLAYS = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
50147
50352
  var REMOVES = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
50148
50353
  var zone = external_exports.string().describe("A Roon zone name (see roon_zones).");
@@ -50162,18 +50367,24 @@ function registerControlTools(server, internal = true) {
50162
50367
  inputSchema: { keys: external_exports.array(external_exports.string()).min(1).max(60).describe("Track keys, in playing order."), zone, mode },
50163
50368
  annotations: PLAYS
50164
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)));
50165
50376
  reg("roon_control", {
50166
50377
  title: "Transport",
50167
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.",
50168
50379
  inputSchema: { zone, action: external_exports.enum(["pause", "play", "playpause", "stop", "next", "previous", "release"]) },
50169
50380
  annotations: PLAYS
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)));
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)));
50171
50382
  reg("roon_seek", {
50172
50383
  title: "Seek",
50173
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.",
50174
50385
  inputSchema: { zone, seconds: external_exports.number().int().min(0) },
50175
50386
  annotations: PLAYS
50176
- }, ({ zone: zn, seconds }) => run(() => internal ? seek2(zn, seconds) : seek(zn, seconds)));
50387
+ }, ({ zone: zn, seconds }) => run(() => internal ? seek(zn, seconds) : seek2(zn, seconds)));
50177
50388
  reg("roon_volume", {
50178
50389
  title: "Volume",
50179
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).",
@@ -50187,7 +50398,7 @@ function registerControlTools(server, internal = true) {
50187
50398
  annotations: PLAYS
50188
50399
  }, ({ zone: zn, shuffle, repeat, roon_radio }) => {
50189
50400
  const s = { shuffle, repeat, roon_radio: roon_radio === void 0 ? void 0 : roon_radio === "now" ? "now" : roon_radio === "on" };
50190
- return run(() => internal ? settings2(zn, s) : settings(zn, s));
50401
+ return run(() => internal ? settings(zn, s) : settings2(zn, s));
50191
50402
  });
50192
50403
  reg("roon_queue", {
50193
50404
  title: "Queue",
@@ -50195,6 +50406,24 @@ function registerControlTools(server, internal = true) {
50195
50406
  inputSchema: { zone, limit: external_exports.number().int().min(1).max(200).optional() },
50196
50407
  annotations: { readOnlyHint: true, openWorldHint: false }
50197
50408
  }, ({ zone: zn, limit: limit3 }) => run(() => queue2(zn, limit3 ?? 50)));
50409
+ reg("roon_queue_jump", {
50410
+ title: "Jump in the queue",
50411
+ description: "Play an item of the zone's queue now (`n` as roon_queue numbers them: 1 is what plays, 2 the next \u2026); what came before it is done. Only on an explicit request.",
50412
+ inputSchema: { zone, n: external_exports.number().int().min(1) },
50413
+ annotations: PLAYS
50414
+ }, ({ zone: zn, n }) => run(() => queueJump(zn, n)));
50415
+ reg("roon_queue_remove", {
50416
+ title: "Remove from the queue",
50417
+ description: "Take items out of the zone's queue, by their numbers from roon_queue (`ns`, e.g. [3, 4]). Only on an explicit request.",
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).") },
50419
+ annotations: CHANGES_QUEUE
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")));
50198
50427
  reg("roon_queue_clear", {
50199
50428
  title: "Clear the queue",
50200
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.",
@@ -50206,19 +50435,19 @@ function registerControlTools(server, internal = true) {
50206
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.`,
50207
50436
  inputSchema: { from: external_exports.string().describe("The zone that plays now."), to: external_exports.string().describe("The zone it should move to.") },
50208
50437
  annotations: PLAYS
50209
- }, ({ from, to }) => run(() => internal ? transfer2(from, to) : transfer(from, to)));
50438
+ }, ({ from, to }) => run(() => internal ? transfer(from, to) : transfer2(from, to)));
50210
50439
  reg("roon_group", {
50211
50440
  title: "Group zones",
50212
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.',
50213
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.") },
50214
50443
  annotations: PLAYS
50215
- }, (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)));
50216
50445
  reg("roon_ungroup", {
50217
50446
  title: "Ungroup zones",
50218
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.",
50219
50448
  inputSchema: { zone: external_exports.string().describe("A member of the group."), only: external_exports.string().optional() },
50220
50449
  annotations: PLAYS
50221
- }, ({ zone: zn, only }) => run(() => internal ? ungroup2(zn, only) : ungroup(zn, only)));
50450
+ }, ({ zone: zn, only }) => run(() => internal ? ungroup(zn, only) : ungroup2(zn, only)));
50222
50451
  reg("roon_sleep", {
50223
50452
  title: "Sleep timer",
50224
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").',
@@ -50233,13 +50462,13 @@ function registerControlTools(server, internal = true) {
50233
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.',
50234
50463
  inputSchema: { zone, action: external_exports.enum(["standby", "wake"]) },
50235
50464
  annotations: PLAYS
50236
- }, ({ 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")));
50237
50466
  reg("roon_cover", {
50238
50467
  title: "Cover",
50239
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.",
50240
50469
  inputSchema: { zone: external_exports.string().optional().describe("A Roon zone name (default: the zone that plays).") },
50241
50470
  annotations: { readOnlyHint: true, openWorldHint: false }
50242
- }, ({ zone: zn }) => runImage(async () => internal ? cover2(zn) : cover(await findTarget(), zn)));
50471
+ }, ({ zone: zn }) => runImage(async () => internal ? cover(zn) : cover2(await findTarget(), zn)));
50243
50472
  }
50244
50473
  __name(registerControlTools, "registerControlTools");
50245
50474
 
@@ -50257,7 +50486,7 @@ var localNow = /* @__PURE__ */ __name(() => {
50257
50486
  const d = /* @__PURE__ */ new Date(), t = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "t");
50258
50487
  return `${d.getFullYear()}-${t(d.getMonth() + 1)}-${t(d.getDate())}T${t(d.getHours())}:${t(d.getMinutes())}:${t(d.getSeconds())}`;
50259
50488
  }, "localNow");
50260
- 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");
50261
50490
  async function dailyPicks(perBox) {
50262
50491
  return withCore(async (c) => {
50263
50492
  const DP = "Sooloos.Broker.Api.DailyPicksParameters";
@@ -50273,7 +50502,7 @@ async function dailyPicks(perBox) {
50273
50502
  c.serviceOid("Library")
50274
50503
  );
50275
50504
  if (!res.success) throw new RoonError(`Roon refused the Daily Picks (${res.status})`);
50276
- const boxes = items(c, decoded(c, res.payload));
50505
+ const boxes = items(c, decoded2(c, res.payload));
50277
50506
  const things = /* @__PURE__ */ __name((b) => {
50278
50507
  for (const k of ["DisplayAlbums", "Albums", "Tracks", "Performers"]) {
50279
50508
  const l = obj(c, field2(b, k))?.fields?.$items ?? [];
@@ -50301,7 +50530,7 @@ async function newForYou(limit3, offset2) {
50301
50530
  return withCore(async (c) => {
50302
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"));
50303
50532
  if (!res.success) throw new RoonError(`Roon refused New releases for you (${res.status})`);
50304
- const all = items(c, decoded(c, res.payload));
50533
+ const all = items(c, decoded2(c, res.payload));
50305
50534
  const part = all.slice(Math.max(0, offset2), Math.max(0, offset2) + Math.min(Math.max(1, limit3), 100));
50306
50535
  await untilLoaded(c, part.map((x) => field2(x, "Album")).filter(isRef2), 3e3);
50307
50536
  return { total: all.length, offset: Math.max(0, offset2), rows: part.map((x) => {
@@ -50329,7 +50558,7 @@ async function readMixes(c) {
50329
50558
  c.serviceOid("Library")
50330
50559
  );
50331
50560
  if (!res.success) throw new RoonError(`Roon refused the Daily Mixes (${res.status})`);
50332
- return items(c, decoded(c, res.payload));
50561
+ return items(c, decoded2(c, res.payload));
50333
50562
  }
50334
50563
  __name(readMixes, "readMixes");
50335
50564
  async function mixes() {
@@ -50460,7 +50689,7 @@ var profileObject = /* @__PURE__ */ __name((c) => {
50460
50689
  if (!p) throw new RoonError("Roon gave no profile");
50461
50690
  return p;
50462
50691
  }, "profileObject");
50463
- 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");
50464
50693
  var PLAYLIST_KIND = { 1: "itunes", 5: "roon", 6: "roon (read-only)", 10: "TIDAL", 11: "file", 12: "qobuz", 13: "roon editorial", 15: "smart" };
50465
50694
  function allPlaylists(c) {
50466
50695
  const service2 = c.graph.findByType("Playlists")[0];
@@ -50490,7 +50719,7 @@ async function playlist(key, limit3) {
50490
50719
  const p = await findPlaylist2(c, key);
50491
50720
  const res = await c.call("Playlist", "GetItems", [{ type: "ResultCallback<Query<PlaylistItem>>", name: "cb_result" }], Buffer.alloc(0), p.oid);
50492
50721
  if (!res.success) throw new RoonError(`Roon refused the playlist (${res.status})`);
50493
- const v = decoded2(c, res.payload);
50722
+ const v = decoded3(c, res.payload);
50494
50723
  const n = Math.min(Math.max(1, limit3), 300);
50495
50724
  const end = Date.now() + 3e3;
50496
50725
  let list = [];
@@ -50624,7 +50853,7 @@ async function listenLater(includePlayed) {
50624
50853
  c.serviceOid("Library")
50625
50854
  );
50626
50855
  if (!res.success) throw new RoonError(`Roon refused Listen Later (${res.status})`);
50627
- const list = items(c, decoded2(c, res.payload));
50856
+ const list = items(c, decoded3(c, res.payload));
50628
50857
  const thing = /* @__PURE__ */ __name((it) => field2(it, "Album") ?? field2(it, "Track") ?? field2(it, "Performer"), "thing");
50629
50858
  await untilLoaded(c, list.map(thing).filter(isRef2), 2500);
50630
50859
  const rows = list.map((it) => {
@@ -50734,7 +50963,7 @@ async function similarAlbum(key, limit3) {
50734
50963
  buildArgs([Arg.ref(al.oid), Arg.sooid(c.profile())]),
50735
50964
  c.serviceOid("Library")
50736
50965
  ), 8e3);
50737
- const recommended = res?.success ? items(c, decoded2(c, res.payload)) : [];
50966
+ const recommended = res?.success ? items(c, decoded3(c, res.payload)) : [];
50738
50967
  const similar = ["SimilarAlbums", "SoundsSimilar", "SeeAlso", "WasInfluencedBy", "WasInspiredBy"].flatMap((k) => items(c, field2(A, k)));
50739
50968
  await untilLoaded(c, [...recommended, ...similar].map((o) => ({ $ref: o.oid })), 2500);
50740
50969
  return { album: albumRow(c, al), similar: unique(similar), recommended: unique(recommended) };
@@ -50752,7 +50981,7 @@ async function similarArtist(key, limit3) {
50752
50981
  buildArgs([Arg.ref(lv.$ref), Arg.sooid(c.profile())]),
50753
50982
  c.serviceOid("Library")
50754
50983
  ), 8e3);
50755
- const w = res?.success ? obj(c, decoded2(c, res.payload)) : null;
50984
+ const w = res?.success ? obj(c, decoded3(c, res.payload)) : null;
50756
50985
  const list = /* @__PURE__ */ __name((k) => items(c, field2(w, k)), "list");
50757
50986
  await untilLoaded(c, ["RecommendedAlbums", "LatestAlbums", "AppearsOnAlbums"].flatMap(list).map((o) => ({ $ref: o.oid })), 2500);
50758
50987
  const rows = /* @__PURE__ */ __name((k) => list(k).filter((o) => field2(o, "Title") !== void 0).slice(0, limit3).map((o) => albumRow(c, o)), "rows");
@@ -50881,7 +51110,7 @@ async function dspOf(c, endpoint) {
50881
51110
  const cc = obj(c, field2(obj(c, field2(endpoint, "Device")), "CommonConfig"));
50882
51111
  if (!cc || field2(cc, "SupportsDspConfig") !== true) return null;
50883
51112
  const res = await short(c.call("AudioDeviceCommonConfig", "GetDspConfig", [{ type: "ResultCallback<DspConfig>", name: "conf" }], Buffer.alloc(0), cc.oid), 6e3);
50884
- const v = res?.success ? decoded2(c, res.payload) : null;
51113
+ const v = res?.success ? decoded3(c, res.payload) : null;
50885
51114
  if (!isRef2(v)) return null;
50886
51115
  const end = Date.now() + 2500;
50887
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);