deep-roon-mcp 0.3.8 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/deep-roon-mcp.js +796 -429
- package/package.json +1 -1
package/dist/deep-roon-mcp.js
CHANGED
|
@@ -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
|
|
3639
|
-
if (isHexPair(
|
|
3640
|
-
const normalizedHex =
|
|
3641
|
-
const
|
|
3642
|
-
if (decodeUnreserved && isUnreserved(
|
|
3643
|
-
output2 +=
|
|
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
|
|
3662
|
-
if (isHexPair(
|
|
3663
|
-
const normalizedHex =
|
|
3664
|
-
const
|
|
3665
|
-
if (
|
|
3666
|
-
output2 +=
|
|
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
|
|
3705
|
-
if (isHexPair(
|
|
3706
|
-
output2 += "%" +
|
|
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
|
|
3744
|
-
if (isHexPair(
|
|
3745
|
-
output2 += "%" +
|
|
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
|
|
3796
|
-
if (isHexPair(
|
|
3797
|
-
const normalizedHex =
|
|
3798
|
-
const
|
|
3799
|
-
if (isUnreserved(
|
|
3800
|
-
output2 +=
|
|
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
|
|
3837
|
-
if (isHexPair(
|
|
3838
|
-
output2 += "%" +
|
|
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(
|
|
9228
|
+
function parseAlbumEditInfo(decoded4) {
|
|
9229
9229
|
return {
|
|
9230
|
-
title: editField(
|
|
9231
|
-
version: editField(
|
|
9232
|
-
performedBy: editField(
|
|
9233
|
-
genres: editField(
|
|
9234
|
-
labels: editField(
|
|
9235
|
-
rating: editField(
|
|
9236
|
-
type: editField(
|
|
9237
|
-
isCompilation: editField(
|
|
9238
|
-
isLive: editField(
|
|
9239
|
-
isPick: editField(
|
|
9240
|
-
containsExplicitContent: editField(
|
|
9241
|
-
isUserHidden: editField(
|
|
9242
|
-
country: editField(
|
|
9243
|
-
catalogNumber: editField(
|
|
9244
|
-
productCode: editField(
|
|
9245
|
-
raw:
|
|
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
|
|
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())]),
|
|
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
|
|
9720
|
-
return parseAlbumEditInfo(
|
|
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
|
|
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())]),
|
|
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
|
|
13957
|
-
if (!/^[0-9a-f]{32}$/i.test(
|
|
13958
|
-
const b = Buffer.from(
|
|
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.
|
|
15528
|
+
version2 = "0.4.5";
|
|
15529
15529
|
}
|
|
15530
15530
|
});
|
|
15531
15531
|
|
|
@@ -15852,6 +15852,15 @@ function field2(o, name) {
|
|
|
15852
15852
|
for (const [k, v] of Object.entries(f)) if (k.endsWith("::" + name)) return v;
|
|
15853
15853
|
return void 0;
|
|
15854
15854
|
}
|
|
15855
|
+
function imageKeys(c, o, name = "Images", max = 4) {
|
|
15856
|
+
return items(c, field2(o, name)).map((im) => {
|
|
15857
|
+
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(field2(im, "Url") ?? ""));
|
|
15858
|
+
return m ? m[1] : "";
|
|
15859
|
+
}).filter(Boolean).slice(0, max);
|
|
15860
|
+
}
|
|
15861
|
+
function imageUrls(c, o, name = "Images", max = 4) {
|
|
15862
|
+
return items(c, field2(o, name)).map((im) => String(field2(im, "Url") ?? "").replace(/^http:\/\//, "https://")).filter((u) => /^https:\/\//.test(u)).slice(0, max);
|
|
15863
|
+
}
|
|
15855
15864
|
function imageKey(c, o, name = "Image") {
|
|
15856
15865
|
const url2 = field2(obj(c, field2(o, name)), "Url");
|
|
15857
15866
|
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(url2 ?? ""));
|
|
@@ -15953,6 +15962,8 @@ function albumRow(c, o) {
|
|
|
15953
15962
|
if (typeof st === "number" && st > 0) r.stars = st / 2;
|
|
15954
15963
|
const lb = strings(c, field2(o, "Labels"));
|
|
15955
15964
|
if (lb.length) r.label = lb.join(" / ");
|
|
15965
|
+
const gs = strings(c, field2(o, "Genres"));
|
|
15966
|
+
if (gs.length) r.genres = gs.slice(0, 8);
|
|
15956
15967
|
if (field2(o, "Source") === 1) r.in_library = true;
|
|
15957
15968
|
if (flag(field2(o, "IsFavorite"))) r.favorite = true;
|
|
15958
15969
|
if (flag(field2(o, "IsListenLater"))) r.listen_later = true;
|
|
@@ -16007,6 +16018,8 @@ var init_graph = __esm({
|
|
|
16007
16018
|
return (o?.fields?.$items ?? o?.$items ?? []).filter((x) => typeof x === "string");
|
|
16008
16019
|
}, "strings");
|
|
16009
16020
|
flag = /* @__PURE__ */ __name((v) => v === true || Buffer.isBuffer(v) && v.length > 1 && v[0] >= 1, "flag");
|
|
16021
|
+
__name(imageKeys, "imageKeys");
|
|
16022
|
+
__name(imageUrls, "imageUrls");
|
|
16010
16023
|
__name(imageKey, "imageKey");
|
|
16011
16024
|
__name(dateParts, "dateParts");
|
|
16012
16025
|
yearOf = /* @__PURE__ */ __name((v) => dateParts(v)?.[0], "yearOf");
|
|
@@ -16207,7 +16220,7 @@ async function findAlbum(c, key) {
|
|
|
16207
16220
|
}
|
|
16208
16221
|
throw new RoonError(`Roon knows no album with key ${key}`, "not_found");
|
|
16209
16222
|
}
|
|
16210
|
-
async function findTracks(c, keys) {
|
|
16223
|
+
async function findTracks(c, keys, lenient = false) {
|
|
16211
16224
|
const look = /* @__PURE__ */ __name(() => {
|
|
16212
16225
|
const all = [...c.graph.objects.values()].filter((o) => /\.Track(Lite)?$/.test(o.typeName) && field2(o, "Title") !== void 0);
|
|
16213
16226
|
return keys.map((k) => all.find((o) => String(field2(o, "TrackId") ?? "") === k));
|
|
@@ -16227,6 +16240,7 @@ async function findTracks(c, keys) {
|
|
|
16227
16240
|
tracks2 = look();
|
|
16228
16241
|
}
|
|
16229
16242
|
const gone = keys.filter((_, i) => !tracks2[i]);
|
|
16243
|
+
if (gone.length && lenient) return tracks2;
|
|
16230
16244
|
if (gone.length) throw new RoonError(`Roon doesn't know ${gone.length} of the ${keys.length} tracks (keys ${gone.join(", ")})`, "not_found");
|
|
16231
16245
|
return tracks2;
|
|
16232
16246
|
}
|
|
@@ -16477,6 +16491,7 @@ __export(focus_exports, {
|
|
|
16477
16491
|
artists: () => artists,
|
|
16478
16492
|
focus: () => focus,
|
|
16479
16493
|
library: () => library2,
|
|
16494
|
+
toplevelGenres: () => toplevelGenres,
|
|
16480
16495
|
tracks: () => tracks,
|
|
16481
16496
|
work: () => work,
|
|
16482
16497
|
works: () => works
|
|
@@ -16486,13 +16501,13 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
|
|
|
16486
16501
|
const missing = members2.find((f) => !schema.some((m) => m.shortName === f.name));
|
|
16487
16502
|
if (missing) throw new RoonError(`The filter ${missing.name} doesn't exist for ${kind.toLowerCase()}s in this Roon version`, "invalid");
|
|
16488
16503
|
const size = Math.min(Math.max(1, limit3), 100), page = Math.floor(Math.max(0, offset2) / size);
|
|
16489
|
-
const
|
|
16504
|
+
const criteria2 = c.structArg(`Sooloos.Broker.Api.${kind}QueryCriteria`, [english(), ...members2]);
|
|
16490
16505
|
const params = c.structArg("Sooloos.Broker.Api.VirtualQueryParameters", [{ name: "PageSize", propType: 0, value: new BinaryWriter().integer(size).toBuffer() }]);
|
|
16491
16506
|
const res = await c.call(
|
|
16492
16507
|
"Library",
|
|
16493
16508
|
`Virtual${kind}Query`,
|
|
16494
16509
|
[{ type: "Sooid", name: "profileid" }, { type: `${kind}QueryCriteria`, name: "criteria" }, { type: "VirtualQueryParameters", name: "queryparams" }, { type: `ResultCallback<Virtual${kind}LiteQuery>`, name: "cb_result" }],
|
|
16495
|
-
Buffer.concat([buildArgs([Arg.sooid(c.profile())]),
|
|
16510
|
+
Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2, params]),
|
|
16496
16511
|
c.serviceOid("Library")
|
|
16497
16512
|
);
|
|
16498
16513
|
if (!res.success) throw new RoonError(`Roon refused the ${kind.toLowerCase()} query (${res.status})`);
|
|
@@ -16527,12 +16542,12 @@ async function virtualQuery(c, kind, members2, limit3, offset2, row) {
|
|
|
16527
16542
|
}
|
|
16528
16543
|
}
|
|
16529
16544
|
async function albumInfo(c, members2) {
|
|
16530
|
-
const
|
|
16545
|
+
const criteria2 = c.structArg("Sooloos.Broker.Api.AlbumQueryCriteria", [english(), ...members2]);
|
|
16531
16546
|
const res = await c.call(
|
|
16532
16547
|
"Library",
|
|
16533
16548
|
"GetAlbumQueryInfo",
|
|
16534
16549
|
[{ type: "Sooid", name: "profileid" }, { type: "AlbumQueryCriteria", name: "criteria" }, { type: "ResultCallback<AlbumQueryInfo>", name: "cb_result" }],
|
|
16535
|
-
Buffer.concat([buildArgs([Arg.sooid(c.profile())]),
|
|
16550
|
+
Buffer.concat([buildArgs([Arg.sooid(c.profile())]), criteria2]),
|
|
16536
16551
|
c.serviceOid("Library")
|
|
16537
16552
|
);
|
|
16538
16553
|
if (!res.success) throw new RoonError(`Roon refused the Focus panel (${res.status})`);
|
|
@@ -16575,6 +16590,28 @@ async function focusMembers(c, f, people = ["artist", "composer", "producer"]) {
|
|
|
16575
16590
|
if (f.not_played_within_days) m.push({ name: "ExcludePlayedInLastNDays", propType: 10, value: maybeInt(f.not_played_within_days) });
|
|
16576
16591
|
if (f.added_within_days) m.push({ name: "RequireImportedInLastNDays", propType: 10, value: maybeInt(f.added_within_days) });
|
|
16577
16592
|
if (f.hires) m.push({ name: "RequireSampleRates", propType: 23, value: intList([88200, 96e3, 176400, 192e3, 352800, 384e3]) });
|
|
16593
|
+
if (f.sample_rate) {
|
|
16594
|
+
const hz = names(f.sample_rate).map((x) => Math.round(parseFloat(x) * (parseFloat(x) < 1e3 ? 1e3 : 1))).filter((x) => x > 0);
|
|
16595
|
+
if (hz.length) m.push({ name: "RequireSampleRates", propType: 23, value: intList(hz) });
|
|
16596
|
+
}
|
|
16597
|
+
if (f.bits) {
|
|
16598
|
+
const b = names(f.bits).map(Number).filter((x) => x > 0);
|
|
16599
|
+
if (b.length) m.push({ name: "RequireBitsPerSample", propType: 23, value: intList(b) });
|
|
16600
|
+
}
|
|
16601
|
+
if (f.mqa === true) m.push(yes("RequireIsMQA"));
|
|
16602
|
+
else if (f.mqa === false) m.push(yes("ExcludeIsMQA"));
|
|
16603
|
+
if (f.storage) {
|
|
16604
|
+
const whole = await albumInfo(c, []);
|
|
16605
|
+
const want = f.storage.toLowerCase();
|
|
16606
|
+
const locs = items(c, field2(whole, "Locations")).map((it) => field2(it, "Value")).filter(isRef2);
|
|
16607
|
+
await untilLoaded(c, locs, 1500);
|
|
16608
|
+
const hit = locs.find((v) => {
|
|
16609
|
+
const n = String(field2(obj(c, v), "Name") ?? "").toLowerCase();
|
|
16610
|
+
return want === "local" ? !/tidal|qobuz/.test(n) : n.includes(want);
|
|
16611
|
+
});
|
|
16612
|
+
if (!hit) throw new RoonError(`No "${f.storage}" in this library's locations`, "not_found");
|
|
16613
|
+
m.push({ name: "RequireLocations", propType: 23, value: refList2([hit.$ref]) });
|
|
16614
|
+
}
|
|
16578
16615
|
if (f.live === true) m.push(yes("RequireIsLive"));
|
|
16579
16616
|
else if (f.live === false) m.push(yes("ExcludeIsLive"));
|
|
16580
16617
|
if (f.compilation === true) m.push(yes("RequireIsCompilation"));
|
|
@@ -16606,10 +16643,20 @@ function ordering(c, kind, table, sort, ascending, descendingByDefault) {
|
|
|
16606
16643
|
if (sort === "random") members2.push({ name: "RandomSeed", propType: 0, value: new BinaryWriter().integer(Math.floor(Math.random() * 2147483647)).toBuffer() });
|
|
16607
16644
|
return [{ name: "Ordering", propType: 23, value: c.structArg(`Sooloos.Broker.Api.${kind}QueryOrdering`, members2) }];
|
|
16608
16645
|
}
|
|
16646
|
+
async function toplevelGenres(c) {
|
|
16647
|
+
if (toplevelNames) return toplevelNames;
|
|
16648
|
+
const { members: members2 } = await focusMembers(c, {});
|
|
16649
|
+
const info = await albumInfo(c, members2);
|
|
16650
|
+
const hist = items(c, field2(info, "ToplevelGenres")).map((it) => field2(it, "Value"));
|
|
16651
|
+
await untilLoaded(c, hist.filter(isRef2), 2e3);
|
|
16652
|
+
const names2 = hist.map((w) => isRef2(w) ? field2(obj(c, w), "Name") : w).filter((n) => typeof n === "string");
|
|
16653
|
+
if (names2.length) toplevelNames = new Set(names2);
|
|
16654
|
+
return new Set(names2);
|
|
16655
|
+
}
|
|
16609
16656
|
async function library2(f, sort, ascending, limit3, offset2) {
|
|
16610
16657
|
return withCore(async (c) => {
|
|
16611
16658
|
const { members: members2, matched } = await focusMembers(c, f);
|
|
16612
|
-
members2.push(...ordering(c, "Album", ALBUM_ORDER, sort, ascending, ["added", "most_played", "popular"]));
|
|
16659
|
+
members2.push(...ordering(c, "Album", ALBUM_ORDER, sort, ascending, ["added", "released", "most_played", "popular"]));
|
|
16613
16660
|
const out = await virtualQuery(c, "Album", members2, limit3, offset2, (o) => albumRow(c, o));
|
|
16614
16661
|
return { ...out, matched };
|
|
16615
16662
|
});
|
|
@@ -16651,7 +16698,13 @@ async function focus(f, more, search2, limit3) {
|
|
|
16651
16698
|
bit_depths: numbers("BitsPerSample").map((x) => ({ bits: x.w, albums: x.n })),
|
|
16652
16699
|
live: field2(info, "IsLiveCount") ?? 0,
|
|
16653
16700
|
compilations: field2(info, "IsCompilationCount") ?? 0,
|
|
16654
|
-
roon_picks: field2(info, "IsPickCount") ?? 0
|
|
16701
|
+
roon_picks: field2(info, "IsPickCount") ?? 0,
|
|
16702
|
+
mqa: field2(info, "IsMQACount") ?? 0,
|
|
16703
|
+
// Raw histograms, to be named once measured (13 Oct 2026): where the albums live, and their file formats.
|
|
16704
|
+
storage: hist("Locations").map((x) => {
|
|
16705
|
+
const n = isRef2(x.w) ? String(field2(obj(c, x.w), "Name") ?? "") : String(x.w);
|
|
16706
|
+
return { name: /tidal/i.test(n) ? "TIDAL" : /qobuz/i.test(n) ? "Qobuz" : "Local", albums: x.n };
|
|
16707
|
+
})
|
|
16655
16708
|
};
|
|
16656
16709
|
});
|
|
16657
16710
|
}
|
|
@@ -16672,7 +16725,7 @@ async function tracks(f, sort, ascending, limit3, offset2) {
|
|
|
16672
16725
|
text: f.text
|
|
16673
16726
|
}, ["artist", "composer"]);
|
|
16674
16727
|
if (f.title) members2.push(text("TrackTitleTextFilter", f.title));
|
|
16675
|
-
members2.push(...ordering(c, "Track", TRACK_ORDER, sort, ascending, ["most_played", "last_played", "added", "popular"]));
|
|
16728
|
+
members2.push(...ordering(c, "Track", TRACK_ORDER, sort, ascending, ["released", "most_played", "last_played", "added", "popular"]));
|
|
16676
16729
|
const out = await virtualQuery(c, "Track", members2, limit3, offset2, (o) => resultRow(c, o));
|
|
16677
16730
|
return { ...out, matched };
|
|
16678
16731
|
});
|
|
@@ -16710,6 +16763,8 @@ function workRow(c, w) {
|
|
|
16710
16763
|
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
16711
16764
|
const n = field2(w, "PerformanceCount");
|
|
16712
16765
|
if (typeof n === "number") r.performances = n;
|
|
16766
|
+
const imgs = imageKeys(c, w);
|
|
16767
|
+
if (imgs.length) r.images = imgs;
|
|
16713
16768
|
return r;
|
|
16714
16769
|
}
|
|
16715
16770
|
async function works(f, sort, ascending, limit3, offset2) {
|
|
@@ -16776,7 +16831,7 @@ async function work(key, max, wiki) {
|
|
|
16776
16831
|
};
|
|
16777
16832
|
});
|
|
16778
16833
|
}
|
|
16779
|
-
var intList, textList, refList2, maybeInt, dateValue, yes, text, names, english, PANEL, ALBUM_ORDER, TRACK_ORDER, ARTIST_ORDER, WORK_ORDER;
|
|
16834
|
+
var intList, textList, refList2, maybeInt, dateValue, yes, text, names, english, PANEL, ALBUM_ORDER, TRACK_ORDER, ARTIST_ORDER, WORK_ORDER, toplevelNames;
|
|
16780
16835
|
var init_focus = __esm({
|
|
16781
16836
|
"src/roon/focus.ts"() {
|
|
16782
16837
|
"use strict";
|
|
@@ -16826,6 +16881,8 @@ var init_focus = __esm({
|
|
|
16826
16881
|
TRACK_ORDER = { title: 0, released: 1, most_played: 2, last_played: 3, artist: 4, album: 5, length: 6, added: 7, popular: 14, random: 16 };
|
|
16827
16882
|
ARTIST_ORDER = { name: 0, albums: 2, works: 3, most_played: 4, popular: 6 };
|
|
16828
16883
|
WORK_ORDER = { title: 0, composer: 1, composed: 3, period: 4, performances: 5, popular: 9 };
|
|
16884
|
+
toplevelNames = null;
|
|
16885
|
+
__name(toplevelGenres, "toplevelGenres");
|
|
16829
16886
|
__name(library2, "library");
|
|
16830
16887
|
__name(focus, "focus");
|
|
16831
16888
|
__name(tracks, "tracks");
|
|
@@ -17751,14 +17808,14 @@ function isValidJWT(jwt2, alg) {
|
|
|
17751
17808
|
if (!header)
|
|
17752
17809
|
return false;
|
|
17753
17810
|
const base643 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "=");
|
|
17754
|
-
const
|
|
17755
|
-
if (typeof
|
|
17811
|
+
const decoded4 = JSON.parse(atob(base643));
|
|
17812
|
+
if (typeof decoded4 !== "object" || decoded4 === null)
|
|
17756
17813
|
return false;
|
|
17757
|
-
if ("typ" in
|
|
17814
|
+
if ("typ" in decoded4 && decoded4?.typ !== "JWT")
|
|
17758
17815
|
return false;
|
|
17759
|
-
if (!
|
|
17816
|
+
if (!decoded4.alg)
|
|
17760
17817
|
return false;
|
|
17761
|
-
if (alg &&
|
|
17818
|
+
if (alg && decoded4.alg !== alg)
|
|
17762
17819
|
return false;
|
|
17763
17820
|
return true;
|
|
17764
17821
|
} catch {
|
|
@@ -21983,8 +22040,8 @@ function uint8ArrayToBase64url(bytes) {
|
|
|
21983
22040
|
return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
21984
22041
|
}
|
|
21985
22042
|
__name(uint8ArrayToBase64url, "uint8ArrayToBase64url");
|
|
21986
|
-
function hexToUint8Array(
|
|
21987
|
-
const cleanHex =
|
|
22043
|
+
function hexToUint8Array(hex4) {
|
|
22044
|
+
const cleanHex = hex4.replace(/^0x/, "");
|
|
21988
22045
|
if (cleanHex.length % 2 !== 0) {
|
|
21989
22046
|
throw new Error("Invalid hex string length");
|
|
21990
22047
|
}
|
|
@@ -49295,22 +49352,32 @@ init_library();
|
|
|
49295
49352
|
init_intern();
|
|
49296
49353
|
init_session();
|
|
49297
49354
|
init_graph();
|
|
49355
|
+
init_library();
|
|
49298
49356
|
async function history(limit3) {
|
|
49299
49357
|
return withCore(async (first) => {
|
|
49300
49358
|
const c = first.graph.findByType("HistoryPlay").length > 0 ? await fresh() : first;
|
|
49301
49359
|
const out = await exportPlayHistory(c, { limit: Math.min(Math.max(0, limit3), 500) });
|
|
49360
|
+
const keys = [...new Set(out.events.map((e) => e.roonTrackId).filter((k) => !!k))].slice(0, 100);
|
|
49361
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
49362
|
+
if (keys.length) {
|
|
49363
|
+
const tracks2 = await findTracks(c, keys, true).catch(() => []);
|
|
49364
|
+
for (const t of tracks2) if (t) byKey.set(String(field2(t, "TrackId")), t);
|
|
49365
|
+
}
|
|
49302
49366
|
closeSoon("after the history");
|
|
49303
49367
|
return {
|
|
49304
49368
|
total: out.total ?? null,
|
|
49305
|
-
plays: out.events.map((e) =>
|
|
49306
|
-
at: localTime(e.playedAt),
|
|
49307
|
-
|
|
49308
|
-
|
|
49309
|
-
|
|
49310
|
-
|
|
49311
|
-
|
|
49312
|
-
|
|
49313
|
-
|
|
49369
|
+
plays: out.events.map((e) => {
|
|
49370
|
+
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 };
|
|
49371
|
+
const t = e.roonTrackId ? byKey.get(e.roonTrackId) : void 0;
|
|
49372
|
+
if (t) {
|
|
49373
|
+
const al = obj(c, field2(t, "Album"));
|
|
49374
|
+
const ak = field2(al, "AlbumId");
|
|
49375
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
49376
|
+
const img = imageKey(c, al);
|
|
49377
|
+
if (img) r.image = img;
|
|
49378
|
+
}
|
|
49379
|
+
return r;
|
|
49380
|
+
})
|
|
49314
49381
|
};
|
|
49315
49382
|
});
|
|
49316
49383
|
}
|
|
@@ -49463,312 +49530,14 @@ async function nowPlaying() {
|
|
|
49463
49530
|
}
|
|
49464
49531
|
__name(nowPlaying, "nowPlaying");
|
|
49465
49532
|
|
|
49466
|
-
// src/
|
|
49467
|
-
|
|
49468
|
-
|
|
49469
|
-
|
|
49470
|
-
|
|
49471
|
-
const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), r = t % 60;
|
|
49472
|
-
return h ? `${h}:${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}` : `${m}:${String(r).padStart(2, "0")}`;
|
|
49473
|
-
}, "duration");
|
|
49474
|
-
var wait2 = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "wait");
|
|
49475
|
-
async function zoneOf(name) {
|
|
49476
|
-
const t = await paired();
|
|
49477
|
-
await officialReady();
|
|
49478
|
-
const z2 = officialZone(name);
|
|
49479
|
-
if (!z2) throw new RoonError(`No Roon zone called "${name}"; Roon has: ${officialZones().map((x) => x.display_name).join(", ")}`, "not_found");
|
|
49480
|
-
return { t, z: z2 };
|
|
49481
|
-
}
|
|
49482
|
-
__name(zoneOf, "zoneOf");
|
|
49483
|
-
async function zones3() {
|
|
49484
|
-
await officialReady();
|
|
49485
|
-
const all = officialZones();
|
|
49486
|
-
return {
|
|
49487
|
-
zones: [...new Set(all.flatMap((z2) => z2.outputs.map((o) => o.display_name)))].sort(),
|
|
49488
|
-
groups: all.filter((z2) => z2.outputs.length > 1).map((z2) => z2.outputs.map((o) => o.display_name))
|
|
49489
|
-
};
|
|
49490
|
-
}
|
|
49491
|
-
__name(zones3, "zones");
|
|
49492
|
-
async function nowPlaying2() {
|
|
49493
|
-
await officialReady();
|
|
49494
|
-
return { zones: officialZones().map((z2) => {
|
|
49495
|
-
const r = { zone: z2.display_name, state: z2.state };
|
|
49496
|
-
const v = z2.outputs.find((o) => o.volume)?.volume;
|
|
49497
|
-
if (v) {
|
|
49498
|
-
r.volume = v.value;
|
|
49499
|
-
r.volume_max = v.max;
|
|
49500
|
-
if (v.is_muted) r.muted = true;
|
|
49501
|
-
}
|
|
49502
|
-
if (z2.settings?.shuffle) r.shuffle = true;
|
|
49503
|
-
if (z2.settings?.loop && z2.settings.loop !== "disabled") r.repeat = z2.settings.loop === "loop_one" ? "one" : "all";
|
|
49504
|
-
if (z2.settings?.auto_radio) r.roon_radio = true;
|
|
49505
|
-
const np = z2.now_playing;
|
|
49506
|
-
if (np && z2.state !== "stopped") {
|
|
49507
|
-
if (np.three_line?.line1) {
|
|
49508
|
-
r.title = np.three_line.line1;
|
|
49509
|
-
if (np.three_line.line2) r.artist = np.three_line.line2;
|
|
49510
|
-
if (np.three_line.line3) r.album = np.three_line.line3;
|
|
49511
|
-
}
|
|
49512
|
-
if (typeof np.seek_position === "number") r.position = duration4(np.seek_position);
|
|
49513
|
-
if (typeof np.length === "number") r.length = duration4(np.length);
|
|
49514
|
-
}
|
|
49515
|
-
return r;
|
|
49516
|
-
}) };
|
|
49517
|
-
}
|
|
49518
|
-
__name(nowPlaying2, "nowPlaying");
|
|
49519
|
-
async function control(name, action) {
|
|
49520
|
-
const { t, z: z2 } = await zoneOf(name);
|
|
49521
|
-
await call((cb) => t.control(z2.zone_id, action, cb));
|
|
49522
|
-
const expect = { pause: ["paused"], stop: ["stopped", "paused"], play: ["playing", "loading"] };
|
|
49523
|
-
let state = officialZone(z2.display_name)?.state;
|
|
49524
|
-
if (expect[action]) {
|
|
49525
|
-
const end = Date.now() + 3e3;
|
|
49526
|
-
while (Date.now() < end && !expect[action].includes(state ?? "")) {
|
|
49527
|
-
await wait2(150);
|
|
49528
|
-
state = officialZone(z2.display_name)?.state;
|
|
49529
|
-
}
|
|
49530
|
-
}
|
|
49531
|
-
return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: z2.display_name, action, state };
|
|
49532
|
-
}
|
|
49533
|
-
__name(control, "control");
|
|
49534
|
-
async function seek(name, seconds) {
|
|
49535
|
-
const { t, z: z2 } = await zoneOf(name);
|
|
49536
|
-
await call((cb) => t.seek(z2.zone_id, "absolute", Math.max(0, Math.round(seconds)), cb));
|
|
49537
|
-
return { ok: true, zone: z2.display_name, position: duration4(seconds) };
|
|
49538
|
-
}
|
|
49539
|
-
__name(seek, "seek");
|
|
49540
|
-
async function settings(name, s) {
|
|
49541
|
-
if (s.roon_radio === "now") throw new RoonError('"Start Roon Radio now" needs the internal protocol (switched off)', "invalid");
|
|
49542
|
-
const { t, z: z2 } = await zoneOf(name);
|
|
49543
|
-
const change = {};
|
|
49544
|
-
if (s.shuffle !== void 0) change.shuffle = s.shuffle;
|
|
49545
|
-
if (s.repeat !== void 0) change.loop = { off: "disabled", all: "loop", one: "loop_one" }[s.repeat];
|
|
49546
|
-
if (s.roon_radio !== void 0) change.auto_radio = s.roon_radio;
|
|
49547
|
-
if (!Object.keys(change).length) throw new RoonError("Nothing to set: give shuffle, repeat or roon_radio", "invalid");
|
|
49548
|
-
await call((cb) => t.change_settings(z2.zone_id, change, cb));
|
|
49549
|
-
return { ok: true, zone: z2.display_name, ...s };
|
|
49550
|
-
}
|
|
49551
|
-
__name(settings, "settings");
|
|
49552
|
-
async function transfer(from, to) {
|
|
49553
|
-
const a = await zoneOf(from), b = await zoneOf(to);
|
|
49554
|
-
if (a.z.zone_id === b.z.zone_id) throw new RoonError("From and to are the same zone", "invalid");
|
|
49555
|
-
await call((cb) => a.t.transfer_zone(a.z.zone_id, b.z.zone_id, cb));
|
|
49556
|
-
return { ok: true, from: a.z.display_name, to: b.z.display_name };
|
|
49557
|
-
}
|
|
49558
|
-
__name(transfer, "transfer");
|
|
49559
|
-
async function group(main2, join) {
|
|
49560
|
-
const m = await zoneOf(main2);
|
|
49561
|
-
const outs = [...m.z.outputs.map((o) => o.output_id)];
|
|
49562
|
-
for (const n of join) {
|
|
49563
|
-
const j = await zoneOf(n);
|
|
49564
|
-
for (const o of j.z.outputs) if (!outs.includes(o.output_id)) outs.push(o.output_id);
|
|
49565
|
-
}
|
|
49566
|
-
if (outs.length === m.z.outputs.length) throw new RoonError("Nothing to group: name at least one other zone", "invalid");
|
|
49567
|
-
await call((cb) => m.t.group_outputs(outs, cb));
|
|
49568
|
-
return { ok: true, group: outs.map((id) => officialZones().flatMap((z2) => z2.outputs).find((o) => o.output_id === id)?.display_name ?? id) };
|
|
49569
|
-
}
|
|
49570
|
-
__name(group, "group");
|
|
49571
|
-
async function ungroup(name, only) {
|
|
49572
|
-
const { t, z: z2 } = await zoneOf(name);
|
|
49573
|
-
if (z2.outputs.length < 2) throw new RoonError(`${z2.display_name} isn't in a group`, "invalid");
|
|
49574
|
-
const off = only ? z2.outputs.filter((o) => o.display_name.toLowerCase().includes(only.toLowerCase())) : z2.outputs;
|
|
49575
|
-
if (!off.length) throw new RoonError(`"${only}" isn't in this group`, "not_found");
|
|
49576
|
-
await call((cb) => t.ungroup_outputs(off.map((o) => o.output_id), cb));
|
|
49577
|
-
return { ok: true, released: off.map((o) => o.display_name) };
|
|
49578
|
-
}
|
|
49579
|
-
__name(ungroup, "ungroup");
|
|
49580
|
-
async function standby(name, on) {
|
|
49581
|
-
const { t, z: z2 } = await zoneOf(name);
|
|
49582
|
-
for (const o of z2.outputs) await call((cb) => on ? t.standby(o.output_id, {}, cb) : t.convenience_switch(o.output_id, {}, cb));
|
|
49583
|
-
return { ok: true, zone: z2.display_name, standby: on };
|
|
49584
|
-
}
|
|
49585
|
-
__name(standby, "standby");
|
|
49586
|
-
async function cover(target2, name) {
|
|
49587
|
-
await officialReady();
|
|
49588
|
-
const z2 = name ? officialZone(name) : officialZones().find((x) => x.state === "playing" && x.now_playing?.image_key) ?? officialZones().find((x) => x.state === "paused" && x.now_playing?.image_key);
|
|
49589
|
-
if (!z2) throw new RoonError(name ? `No Roon zone called "${name}"` : "Nothing plays in Roon right now", "not_found");
|
|
49590
|
-
const key = z2.now_playing?.image_key;
|
|
49591
|
-
if (!key) throw new RoonError(`Roon has no cover for what plays on ${z2.display_name}`, "not_found");
|
|
49592
|
-
const r = await fetch(`http://${target2.host}:${target2.port}/api/image/${key}?scale=fit&width=600&height=600`).catch(() => null);
|
|
49593
|
-
if (!r?.ok) throw new RoonError("Roon doesn't hand over the cover right now", "unavailable");
|
|
49594
|
-
const l = z2.now_playing?.three_line;
|
|
49595
|
-
return {
|
|
49596
|
-
info: { zone: z2.display_name, title: l?.line1, artist: l?.line2, album: l?.line3 },
|
|
49597
|
-
image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
|
|
49598
|
-
};
|
|
49599
|
-
}
|
|
49600
|
-
__name(cover, "cover");
|
|
49601
|
-
|
|
49602
|
-
// src/tools/run.ts
|
|
49533
|
+
// src/roon/radio.ts
|
|
49534
|
+
init_intern();
|
|
49535
|
+
init_serializer();
|
|
49536
|
+
init_writer();
|
|
49537
|
+
init_signature();
|
|
49603
49538
|
init_session();
|
|
49604
49539
|
init_log();
|
|
49605
|
-
|
|
49606
|
-
// src/core/status.ts
|
|
49607
|
-
init_package();
|
|
49608
|
-
init_log();
|
|
49609
|
-
var checks = null;
|
|
49610
|
-
var checking = null;
|
|
49611
|
-
function selfTest(run2) {
|
|
49612
|
-
checking ??= run2().then((c) => {
|
|
49613
|
-
checks = c;
|
|
49614
|
-
const bad = c.filter((x) => !x.ok);
|
|
49615
|
-
log(bad.length ? `self-test: ${bad.map((x) => `${x.name} failed (${x.detail})`).join("; ")}` : `self-test: ${c.length} reads ok`);
|
|
49616
|
-
}).catch((e) => {
|
|
49617
|
-
log(`self-test couldn't run: ${e.message}`);
|
|
49618
|
-
});
|
|
49619
|
-
return checking;
|
|
49620
|
-
}
|
|
49621
|
-
__name(selfTest, "selfTest");
|
|
49622
|
-
var selfTestResults = /* @__PURE__ */ __name(() => checks, "selfTestResults");
|
|
49623
|
-
var newer = null;
|
|
49624
|
-
var told = false;
|
|
49625
|
-
var parse3 = /* @__PURE__ */ __name((v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0), "parse");
|
|
49626
|
-
var isNewer = /* @__PURE__ */ __name((a, b) => {
|
|
49627
|
-
const x = parse3(a), y = parse3(b);
|
|
49628
|
-
for (let i = 0; i < 3; i++) {
|
|
49629
|
-
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
49630
|
-
}
|
|
49631
|
-
return false;
|
|
49632
|
-
}, "isNewer");
|
|
49633
|
-
async function checkForUpdate() {
|
|
49634
|
-
if (process.env.DEEP_ROON_NO_UPDATE_CHECK === "1") return;
|
|
49635
|
-
try {
|
|
49636
|
-
const r = await fetch("https://api.github.com/repos/renebouwmeester/deep-roon-mcp/releases/latest", {
|
|
49637
|
-
headers: { "User-Agent": `deep-roon-mcp/${version2}`, Accept: "application/vnd.github+json" },
|
|
49638
|
-
signal: AbortSignal.timeout(5e3)
|
|
49639
|
-
});
|
|
49640
|
-
if (!r.ok) return;
|
|
49641
|
-
const j = await r.json();
|
|
49642
|
-
if (j.tag_name && isNewer(j.tag_name, version2)) {
|
|
49643
|
-
newer = { version: j.tag_name.replace(/^v/, ""), url: j.html_url ?? "" };
|
|
49644
|
-
log(`a newer version is out: ${newer.version}`);
|
|
49645
|
-
}
|
|
49646
|
-
} catch {
|
|
49647
|
-
}
|
|
49648
|
-
}
|
|
49649
|
-
__name(checkForUpdate, "checkForUpdate");
|
|
49650
|
-
var updateInfo = /* @__PURE__ */ __name(() => newer, "updateInfo");
|
|
49651
|
-
function updateNotice() {
|
|
49652
|
-
if (!newer || told) return null;
|
|
49653
|
-
told = true;
|
|
49654
|
-
return `Deep Roon MCP ${newer.version} is available (this is ${version2}): ${newer.url}`;
|
|
49655
|
-
}
|
|
49656
|
-
__name(updateNotice, "updateNotice");
|
|
49657
|
-
|
|
49658
|
-
// src/tools/run.ts
|
|
49659
|
-
var compact = /* @__PURE__ */ __name((_k, v) => v === void 0 ? void 0 : typeof v === "bigint" ? v.toString() : v, "compact");
|
|
49660
|
-
async function run(work2) {
|
|
49661
|
-
try {
|
|
49662
|
-
const result = await work2();
|
|
49663
|
-
const notice = updateNotice();
|
|
49664
|
-
return { content: [{ type: "text", text: JSON.stringify(result, compact) }, ...notice ? [{ type: "text", text: notice }] : []] };
|
|
49665
|
-
} catch (e) {
|
|
49666
|
-
const err = e;
|
|
49667
|
-
let text2;
|
|
49668
|
-
if (err instanceof RoonError) text2 = err.message;
|
|
49669
|
-
else if (/MissingMethod|method not found|unknown method/i.test(err.message)) {
|
|
49670
|
-
text2 = `Roon changed this part of its internal protocol in this version (${err.message}). Check for an update of Deep Roon MCP.`;
|
|
49671
|
-
} else text2 = `Roon didn't answer as expected: ${err.message}`;
|
|
49672
|
-
log(`tool error: ${err.message}`);
|
|
49673
|
-
return { isError: true, content: [{ type: "text", text: text2 }] };
|
|
49674
|
-
}
|
|
49675
|
-
}
|
|
49676
|
-
__name(run, "run");
|
|
49677
|
-
async function runImage(work2) {
|
|
49678
|
-
let image = null;
|
|
49679
|
-
const r = await run(async () => {
|
|
49680
|
-
const x = await work2();
|
|
49681
|
-
image = x.image;
|
|
49682
|
-
return x.info;
|
|
49683
|
-
});
|
|
49684
|
-
if (r.isError || !image) return r;
|
|
49685
|
-
const { data, mimeType } = image;
|
|
49686
|
-
return { content: [...r.content, { type: "image", data, mimeType }] };
|
|
49687
|
-
}
|
|
49688
|
-
__name(runImage, "runImage");
|
|
49689
|
-
|
|
49690
|
-
// src/tools/read.ts
|
|
49691
|
-
var READ = { readOnlyHint: true, openWorldHint: false };
|
|
49692
|
-
var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
|
|
49693
|
-
var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
|
|
49694
|
-
function registerReadTools(server, internal = true) {
|
|
49695
|
-
const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
|
|
49696
|
-
const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
|
|
49697
|
-
reg("roon_search", {
|
|
49698
|
-
title: "Search Roon",
|
|
49699
|
-
description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Rows carry `image`, the key of the cover: the Core serves it at http://<core address>:9330/image/<key>.jpg, or scaled at /image/<key>.300.jpg (for clients that draw; a chat shows covers with roon_cover). The `image` of roon_now_playing is an official key, served at /api/image/<key>?scale=fit&width=600&height=600. Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
|
|
49700
|
-
inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
|
|
49701
|
-
annotations: READ
|
|
49702
|
-
}, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
|
|
49703
|
-
reg("roon_new", {
|
|
49704
|
-
title: "Streaming shelves in Roon",
|
|
49705
|
-
description: 'The editorial shelves of Qobuz or TIDAL as Roon shows them (the same for everyone). Qobuz (`service: "qobuz"`): `new`, `qobuzissimes`, `press_awards`, `top`, `trending`, `ideal_discography` \u2014 albums; `genre` narrows a shelf to one or more Qobuz genre ids from `roon_genres`. TIDAL (`service: "tidal"`): `new`, `recommended`, `top`, `rising`, `masters` \u2014 albums, or tracks with `what: "tracks"` (not masters); `genre` (one TIDAL genre from `roon_genres`, e.g. "Jazz") gives TIDAL\'s page for that genre instead of a shelf. Use `genre` for "new jazz", "what\'s new in classical"; TIDAL\'s playlists are in roon_tidal_playlists. Albums carry their format and a `key` for `roon_album`; tracks a `key` for roon_play_tracks. `offset` pages through the shelf; `total` says how many there are.',
|
|
49706
|
-
inputSchema: {
|
|
49707
|
-
service,
|
|
49708
|
-
section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
|
|
49709
|
-
genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
|
|
49710
|
-
what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
|
|
49711
|
-
limit: limit(100, 30),
|
|
49712
|
-
offset: external_exports.number().int().min(0).optional()
|
|
49713
|
-
},
|
|
49714
|
-
annotations: READ
|
|
49715
|
-
}, ({ service: sv, section: section2, genre, what, limit: n, offset: offset2 }) => run(() => section(section2 ?? "new", n ?? 30, offset2 ?? 0, (genre ?? "").split(",").map((x) => x.trim()).filter(Boolean), sv, what ?? "albums")));
|
|
49716
|
-
reg("roon_genres", {
|
|
49717
|
-
title: "Streaming genres",
|
|
49718
|
-
description: "The genres of Qobuz or TIDAL, for the `genre` filter of `roon_new` (and, for TIDAL, of `roon_tidal_playlists`). Qobuz: the main genres with their ids (names in the account's language); with `parent` (a genre id) its sub-genres, e.g. the kinds of jazz. TIDAL: its genres and its moods (Relax, Focus \u2026), each with an `id` to pass on.",
|
|
49719
|
-
inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
|
|
49720
|
-
annotations: READ
|
|
49721
|
-
}, ({ service: sv, parent }) => run(() => genres(parent, sv)));
|
|
49722
|
-
reg("roon_album", {
|
|
49723
|
-
title: "Album page",
|
|
49724
|
-
description: "One album in Roon, by its `key` (from roon_search, roon_new, roon_artist): the album with year, format, label and whether it is in the library, its genres, release date (and the date of this edition), barcode and \u2014 when Roon has them \u2014 catalogue number and country, its main artist(s) with a `key` for `roon_artist` / `roon_bio`, and every track with its own `key`. A streaming album that is also in the library comes back as the library copy (its key may differ; both work). `credits: true` adds who played, produced, engineered and wrote, grouped by role \u2014 Roon's own data.",
|
|
49725
|
-
inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
|
|
49726
|
-
annotations: READ
|
|
49727
|
-
}, ({ key, credits }) => run(() => album(key, credits === true)));
|
|
49728
|
-
reg("roon_review", {
|
|
49729
|
-
title: "Album review",
|
|
49730
|
-
description: "The review Roon shows on an album's page, in full, with its author \u2014 by album `key`. When the user asks for the review (\"what does Roon say about this album\", \"show me the review\"), they want to read the review itself: show the text complete and exactly as returned, as a quote with the author's name \u2014 no paraphrase or shortening unless they ask for a summary, and don't go to the web for it. `wikipedia: true` adds the album's Wikipedia text; it also comes by itself when Roon has no review of its own. `max_chars` only when a shorter excerpt is asked for.",
|
|
49731
|
-
inputSchema: { key: external_exports.string().describe("An album key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
|
|
49732
|
-
annotations: READ
|
|
49733
|
-
}, ({ key, wikipedia, max_chars }) => run(() => review(key, max_chars ?? 1e9, wikipedia === true)));
|
|
49734
|
-
reg("roon_bio", {
|
|
49735
|
-
title: "Artist biography",
|
|
49736
|
-
description: 'The biography Roon shows on an artist\'s page, in full, with its author \u2014 by artist `key` \u2014 plus where they are from, active since, genres, and the related artists Roon lists: `similar`, `influenced_by`, `influenced`, `worked_with`, `member_of`, `members`, each with a `key` for `roon_artist` / `roon_bio`. When the user asks for the bio ("who is \u2026", "tell me about this artist"), show the text complete and exactly as returned, with the author\'s name \u2014 no paraphrase or shortening unless they ask for a summary. For a narrower question ("who influenced them") answer from the lists. `wikipedia: true` adds the Wikipedia text; `max_chars` only when a shorter excerpt is asked for.',
|
|
49737
|
-
inputSchema: { key: external_exports.string().describe("An artist key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
|
|
49738
|
-
annotations: READ
|
|
49739
|
-
}, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
|
|
49740
|
-
reg("roon_artist", {
|
|
49741
|
-
title: "Discography",
|
|
49742
|
-
description: 'An artist\'s discography in Roon: the main albums Qobuz (or, with `service: "tidal"`, TIDAL) has, oldest first, each with year, format and a `key` for `roon_album`. The artist `key` comes from roon_search (`kind: "artist"`) or from the `artists` of roon_album. Use this for "the album before/after \u2026", "their debut", "what else did they make" \u2014 don\'t answer discography questions from memory. The year is the original release where Roon knows it; reissues and compilations are in the list too.',
|
|
49743
|
-
inputSchema: { key: external_exports.string().describe("An artist key."), service },
|
|
49744
|
-
annotations: READ
|
|
49745
|
-
}, ({ key, service: sv }) => run(() => discography(key, sv)));
|
|
49746
|
-
reg("roon_zones", {
|
|
49747
|
-
title: "Roon zones",
|
|
49748
|
-
description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
|
|
49749
|
-
inputSchema: {},
|
|
49750
|
-
annotations: READ
|
|
49751
|
-
}, () => run(() => internal ? zones2() : zones3()));
|
|
49752
|
-
reg("roon_now_playing", {
|
|
49753
|
-
title: "Now playing in Roon",
|
|
49754
|
-
description: "What plays in Roon, per zone: state (playing, loading, paused, stopped), the track with album, artist and format, the position, how many tracks are left in the queue, shuffle, repeat, Roon Radio and a sleep timer. Read this before saying anything about what Roon plays \u2014 it changes all the time.",
|
|
49755
|
-
inputSchema: {},
|
|
49756
|
-
annotations: READ
|
|
49757
|
-
}, () => run(() => internal ? nowPlaying() : nowPlaying2()));
|
|
49758
|
-
reg("roon_history", {
|
|
49759
|
-
title: "Play history",
|
|
49760
|
-
description: `What was played in Roon, newest first \u2014 Roon's own play history, with how much of each track was heard. For "most played" questions use roon_top instead of counting this.`,
|
|
49761
|
-
inputSchema: { limit: limit(300, 30) },
|
|
49762
|
-
annotations: READ
|
|
49763
|
-
}, ({ limit: n }) => run(() => history(n ?? 30)));
|
|
49764
|
-
reg("roon_top", {
|
|
49765
|
-
title: "Most played",
|
|
49766
|
-
description: 'The most played albums, artists or tracks in Roon, counted over the WHOLE play history (a play counts when at least half the track was heard). Use this for "most listened" questions \u2014 don\'t tally roon_history yourself. `since` (YYYY-MM-DD) limits the period.',
|
|
49767
|
-
inputSchema: { what: external_exports.enum(["albums", "artists", "tracks"]).optional(), limit: limit(100, 20), since: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only plays from this date on (YYYY-MM-DD).") },
|
|
49768
|
-
annotations: READ
|
|
49769
|
-
}, ({ what, limit: n, since }) => run(() => top(what ?? "albums", n ?? 20, since)));
|
|
49770
|
-
}
|
|
49771
|
-
__name(registerReadTools, "registerReadTools");
|
|
49540
|
+
init_graph();
|
|
49772
49541
|
|
|
49773
49542
|
// src/roon/control.ts
|
|
49774
49543
|
init_intern();
|
|
@@ -49902,7 +49671,7 @@ async function playTracks(keys, zoneName, how) {
|
|
|
49902
49671
|
}
|
|
49903
49672
|
__name(playTracks, "playTracks");
|
|
49904
49673
|
var BUTTONS = { pause: "Pause", play: "Play", playpause: "PlayPause", stop: "Stop", next: "Next", previous: "Previous" };
|
|
49905
|
-
async function
|
|
49674
|
+
async function control(zoneName, action) {
|
|
49906
49675
|
if (action === "release") {
|
|
49907
49676
|
return withCore(async (c) => {
|
|
49908
49677
|
const z2 = findZone(c, zoneName);
|
|
@@ -49930,8 +49699,8 @@ async function control2(zoneName, action) {
|
|
|
49930
49699
|
}
|
|
49931
49700
|
return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: r.zone, action, state };
|
|
49932
49701
|
}
|
|
49933
|
-
__name(
|
|
49934
|
-
async function
|
|
49702
|
+
__name(control, "control");
|
|
49703
|
+
async function seek(zoneName, seconds) {
|
|
49935
49704
|
return withCore(async (c) => {
|
|
49936
49705
|
const z2 = findZone(c, zoneName);
|
|
49937
49706
|
const res = await c.call("Zone", "Seek", [{ type: "int", name: "seconds" }, { type: "ResultCallback", name: "cb_done" }], buildArgs([Arg.int(Math.max(0, Math.round(seconds)))]), z2.oid);
|
|
@@ -49939,8 +49708,8 @@ async function seek2(zoneName, seconds) {
|
|
|
49939
49708
|
return { ok: true, zone: z2.name, position: duration3(seconds) };
|
|
49940
49709
|
});
|
|
49941
49710
|
}
|
|
49942
|
-
__name(
|
|
49943
|
-
async function
|
|
49711
|
+
__name(seek, "seek");
|
|
49712
|
+
async function settings(zoneName, s) {
|
|
49944
49713
|
return withCore(async (c) => {
|
|
49945
49714
|
const z2 = findZone(c, zoneName);
|
|
49946
49715
|
const send = /* @__PURE__ */ __name((method, params, args) => c.remoting.callMethodNoReply(z2.oid, formatMethodSignature("Zone", method, params), args), "send");
|
|
@@ -49965,7 +49734,7 @@ async function settings2(zoneName, s) {
|
|
|
49965
49734
|
return { ok: true, zone: z2.name, ...done };
|
|
49966
49735
|
});
|
|
49967
49736
|
}
|
|
49968
|
-
__name(
|
|
49737
|
+
__name(settings, "settings");
|
|
49969
49738
|
function queueOf(c, zoneName) {
|
|
49970
49739
|
const z2 = findZone(c, zoneName);
|
|
49971
49740
|
const q = obj(c, field2(c.graph.getObject(z2.oid), "Queue"));
|
|
@@ -50034,6 +49803,29 @@ async function queueRemove(zoneName, ns) {
|
|
|
50034
49803
|
});
|
|
50035
49804
|
}
|
|
50036
49805
|
__name(queueRemove, "queueRemove");
|
|
49806
|
+
var INSERTION = { before: 3, after: 2 };
|
|
49807
|
+
async function queueMove(zoneName, n, to, where) {
|
|
49808
|
+
return withCore(async (c) => {
|
|
49809
|
+
const { z: z2, q, oid, refs, at } = await queueItemAt(c, zoneName, n);
|
|
49810
|
+
const rel = refs[at + to - 1];
|
|
49811
|
+
if (rel === void 0 || to === n) throw new RoonError(`The queue of ${z2.name} has no item ${to} to move next to`, "invalid");
|
|
49812
|
+
const point = INSERTION[where];
|
|
49813
|
+
const res = await c.call(
|
|
49814
|
+
"Queue",
|
|
49815
|
+
"MoveItems",
|
|
49816
|
+
[{ type: "IEnumerable<TransportItem>", name: "items" }, { type: "InsertionPoint", name: "insertionpoint" }, { type: "TransportItem", name: "relative_to_item" }, { type: "ResultCallback", name: "cb_done" }],
|
|
49817
|
+
Buffer.concat([buildArgs([Arg.collection([buildArgs([Arg.ref(oid)])]), Arg.enum_(point), Arg.ref(rel)])]),
|
|
49818
|
+
q.oid
|
|
49819
|
+
);
|
|
49820
|
+
if (!res.success) throw new RoonError(`Roon refused to move the item (${res.status})`);
|
|
49821
|
+
log(`queue_move: ${z2.name} item ${n} \u2192 ${where} ${to} (point ${point})`);
|
|
49822
|
+
await wait(300);
|
|
49823
|
+
const after = await queueItems(c, q);
|
|
49824
|
+
const now = after.indexOf(oid) - at + 1;
|
|
49825
|
+
return { ok: true, zone: z2.name, moved: n, now_at: now > 0 ? now : void 0 };
|
|
49826
|
+
});
|
|
49827
|
+
}
|
|
49828
|
+
__name(queueMove, "queueMove");
|
|
50037
49829
|
async function queue2(zoneName, limit3) {
|
|
50038
49830
|
return withCore(async (c) => {
|
|
50039
49831
|
const { z: z2, q } = queueOf(c, zoneName);
|
|
@@ -50069,7 +49861,7 @@ var outputsOf = /* @__PURE__ */ __name((c, zone3) => c.graph.findByType("Endpoin
|
|
|
50069
49861
|
const z2 = field2(e, "Zone");
|
|
50070
49862
|
return isRef2(z2) && String(z2.$ref) === String(zone3) && typeof field2(e, "Name") === "string";
|
|
50071
49863
|
}), "outputsOf");
|
|
50072
|
-
async function
|
|
49864
|
+
async function group(main2, join) {
|
|
50073
49865
|
return withCore(async (c) => {
|
|
50074
49866
|
const h = findZone(c, main2);
|
|
50075
49867
|
const own2 = outputsOf(c, h.oid);
|
|
@@ -50095,8 +49887,8 @@ async function group2(main2, join) {
|
|
|
50095
49887
|
return { ok: true, group: [...own2, ...added].map((e) => field2(e, "Name")) };
|
|
50096
49888
|
});
|
|
50097
49889
|
}
|
|
50098
|
-
__name(
|
|
50099
|
-
async function
|
|
49890
|
+
__name(group, "group");
|
|
49891
|
+
async function ungroup(zoneName, only) {
|
|
50100
49892
|
return withCore(async (c) => {
|
|
50101
49893
|
const z2 = findZone(c, zoneName);
|
|
50102
49894
|
const all = outputsOf(c, z2.oid);
|
|
@@ -50114,8 +49906,8 @@ async function ungroup2(zoneName, only) {
|
|
|
50114
49906
|
return { ok: true, released: off.map((e) => field2(e, "Name")) };
|
|
50115
49907
|
});
|
|
50116
49908
|
}
|
|
50117
|
-
__name(
|
|
50118
|
-
async function
|
|
49909
|
+
__name(ungroup, "ungroup");
|
|
49910
|
+
async function transfer(from, to) {
|
|
50119
49911
|
return withCore(async (c) => {
|
|
50120
49912
|
const a = findZone(c, from), b = findZone(c, to);
|
|
50121
49913
|
if (a.oid === b.oid) throw new RoonError("From and to are the same zone", "invalid");
|
|
@@ -50130,7 +49922,7 @@ async function transfer2(from, to) {
|
|
|
50130
49922
|
return { ok: true, from: a.name, to: b.name };
|
|
50131
49923
|
});
|
|
50132
49924
|
}
|
|
50133
|
-
__name(
|
|
49925
|
+
__name(transfer, "transfer");
|
|
50134
49926
|
async function sleep(zoneName, minutes) {
|
|
50135
49927
|
return withCore(async (c) => {
|
|
50136
49928
|
const z2 = findZone(c, zoneName);
|
|
@@ -50143,7 +49935,7 @@ async function sleep(zoneName, minutes) {
|
|
|
50143
49935
|
});
|
|
50144
49936
|
}
|
|
50145
49937
|
__name(sleep, "sleep");
|
|
50146
|
-
async function
|
|
49938
|
+
async function standby(zoneName, on) {
|
|
50147
49939
|
return withCore(async (c) => {
|
|
50148
49940
|
const z2 = findZone(c, zoneName);
|
|
50149
49941
|
const outs = outputsOf(c, z2.oid).filter((e) => field2(e, "SupportsStandby") === true);
|
|
@@ -50155,8 +49947,8 @@ async function standby2(zoneName, on) {
|
|
|
50155
49947
|
return { ok: true, zone: z2.name, standby: on };
|
|
50156
49948
|
});
|
|
50157
49949
|
}
|
|
50158
|
-
__name(
|
|
50159
|
-
async function
|
|
49950
|
+
__name(standby, "standby");
|
|
49951
|
+
async function cover(zoneName) {
|
|
50160
49952
|
const t = await findTarget();
|
|
50161
49953
|
const info = await withCore(async (c) => {
|
|
50162
49954
|
const pick2 = zoneName ? findZone(c, zoneName) : null;
|
|
@@ -50185,8 +49977,456 @@ async function cover2(zoneName) {
|
|
|
50185
49977
|
image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
|
|
50186
49978
|
};
|
|
50187
49979
|
}
|
|
49980
|
+
__name(cover, "cover");
|
|
49981
|
+
|
|
49982
|
+
// src/roon/radio.ts
|
|
49983
|
+
var hex3 = /* @__PURE__ */ __name((v) => Buffer.isBuffer(v) ? v.toString("hex") : "", "hex");
|
|
49984
|
+
var decoded = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
|
|
49985
|
+
var radioOid = /* @__PURE__ */ __name((c) => c.serviceOid("Radio"), "radioOid");
|
|
49986
|
+
function stationRow(c, o) {
|
|
49987
|
+
const r = { key: hex3(field2(o, "ChannelId")), name: field2(o, "Name") ?? "" };
|
|
49988
|
+
const loc = field2(o, "Location");
|
|
49989
|
+
if (loc) r.location = loc;
|
|
49990
|
+
const d = field2(o, "Description");
|
|
49991
|
+
if (d) r.description = String(d).slice(0, 200);
|
|
49992
|
+
if (field2(o, "IsInLibrary") === true) r.in_library = true;
|
|
49993
|
+
const img = imageKey(c, o);
|
|
49994
|
+
if (img) r.image = img;
|
|
49995
|
+
const url2 = field2(obj(c, field2(o, "Image")), "Url");
|
|
49996
|
+
if (typeof url2 === "string" && /^https?:/.test(url2)) r.image_url = url2;
|
|
49997
|
+
return r;
|
|
49998
|
+
}
|
|
49999
|
+
__name(stationRow, "stationRow");
|
|
50000
|
+
var categoryRow = /* @__PURE__ */ __name((c, o) => ({ key: hex3(field2(o, "CategoryId")), name: field2(o, "Name") ?? "" }), "categoryRow");
|
|
50001
|
+
function criteria(c, category, genre) {
|
|
50002
|
+
const members2 = [];
|
|
50003
|
+
if (category) {
|
|
50004
|
+
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() }]);
|
|
50005
|
+
members2.push({ name: "Sooloos.Broker.Api.RadioCategory Sooloos.Broker.Api.RadioBrowseCriteria::Category", propType: 23, value: cat });
|
|
50006
|
+
}
|
|
50007
|
+
if (genre) members2.push({ name: "string Sooloos.Broker.Api.RadioBrowseCriteria::Genre", propType: 20, value: new BinaryWriter().string(genre).toBuffer() });
|
|
50008
|
+
return c.structArg("Sooloos.Broker.Api.RadioBrowseCriteria", members2);
|
|
50009
|
+
}
|
|
50010
|
+
__name(criteria, "criteria");
|
|
50011
|
+
async function myStations(c) {
|
|
50012
|
+
const crit = c.structArg("Sooloos.Broker.Api.ChannelQueryCriteria", [{ name: "bool Sooloos.Broker.Api.ChannelQueryCriteria::RequireInLibrary", propType: 2, value: new BinaryWriter().boolean(true).toBuffer() }]);
|
|
50013
|
+
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));
|
|
50014
|
+
if (!res.success) throw new RoonError(`Roon refused the radio stations (${res.status})`);
|
|
50015
|
+
const v = decoded(c, res.payload);
|
|
50016
|
+
const end = Date.now() + 3e3;
|
|
50017
|
+
let list = [];
|
|
50018
|
+
while (Date.now() < end) {
|
|
50019
|
+
list = (isRef2(v) ? c.graph.getObject(v.$ref)?.fields?.$items : null) ?? [];
|
|
50020
|
+
if (!list.length || list.every((x) => field2(obj(c, x), "Name") !== void 0)) break;
|
|
50021
|
+
await wait(60);
|
|
50022
|
+
}
|
|
50023
|
+
return list.map((x) => obj(c, x)).filter(Boolean);
|
|
50024
|
+
}
|
|
50025
|
+
__name(myStations, "myStations");
|
|
50026
|
+
async function radio(category, genre, limit3 = 40) {
|
|
50027
|
+
return withCore(async (c) => {
|
|
50028
|
+
const out = {};
|
|
50029
|
+
if (!category && !genre) out.my_stations = (await myStations(c)).map((o) => stationRow(c, o));
|
|
50030
|
+
const crit = criteria(c, category, genre);
|
|
50031
|
+
if (!genre) {
|
|
50032
|
+
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));
|
|
50033
|
+
if (!res.success) throw new RoonError(`Roon refused the radio categories (${res.status})`);
|
|
50034
|
+
const cats = items(c, decoded(c, res.payload));
|
|
50035
|
+
await untilLoaded(c, cats.map((x) => ({ $ref: x.oid })), 2500);
|
|
50036
|
+
out.categories = cats.map((x) => categoryRow(c, x));
|
|
50037
|
+
}
|
|
50038
|
+
if (category || genre) {
|
|
50039
|
+
const n = Math.min(Math.max(1, limit3), 100);
|
|
50040
|
+
const res = await c.call(
|
|
50041
|
+
"Radio",
|
|
50042
|
+
"BrowseChannels",
|
|
50043
|
+
[{ type: "Sooid", name: "profileid" }, { type: "RadioBrowseCriteria", name: "criteria" }, { type: "int?", name: "count" }, { type: "ResultCallback<DataList<Channel>>", name: "cb_result" }],
|
|
50044
|
+
Buffer.concat([buildArgs([Arg.sooid(c.profile())]), crit, Buffer.from([1]), new BinaryWriter().integer(n).toBuffer()]),
|
|
50045
|
+
radioOid(c)
|
|
50046
|
+
);
|
|
50047
|
+
if (!res.success) throw new RoonError(`Roon refused the radio stations (${res.status})`);
|
|
50048
|
+
const chs = items(c, decoded(c, res.payload));
|
|
50049
|
+
await untilLoaded(c, chs.map((x) => ({ $ref: x.oid })), 3500);
|
|
50050
|
+
out.stations = chs.map((x) => stationRow(c, x));
|
|
50051
|
+
}
|
|
50052
|
+
return out;
|
|
50053
|
+
});
|
|
50054
|
+
}
|
|
50055
|
+
__name(radio, "radio");
|
|
50056
|
+
async function findStation(c, key) {
|
|
50057
|
+
if (/^[0-9a-f]{20,}$/i.test(key)) {
|
|
50058
|
+
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));
|
|
50059
|
+
const v = res.success ? decoded(c, res.payload) : null;
|
|
50060
|
+
if (isRef2(v)) {
|
|
50061
|
+
await untilLoaded(c, [v], 2500);
|
|
50062
|
+
const o2 = c.graph.getObject(v.$ref);
|
|
50063
|
+
if (o2 && field2(o2, "Name") !== void 0) return o2;
|
|
50064
|
+
}
|
|
50065
|
+
throw new RoonError("Roon knows no station with that key", "not_found");
|
|
50066
|
+
}
|
|
50067
|
+
const mine = await myStations(c);
|
|
50068
|
+
const want = key.toLowerCase();
|
|
50069
|
+
const o = mine.find((x) => String(field2(x, "Name") ?? "").toLowerCase() === want) ?? mine.find((x) => String(field2(x, "Name") ?? "").toLowerCase().includes(want));
|
|
50070
|
+
if (!o) throw new RoonError(`No station called "${key}" among your own; give a key from roon_radio`, "not_found");
|
|
50071
|
+
return o;
|
|
50072
|
+
}
|
|
50073
|
+
__name(findStation, "findStation");
|
|
50074
|
+
async function playRadio(key, zoneName) {
|
|
50075
|
+
const r = await withCore(async (c) => {
|
|
50076
|
+
const st = await findStation(c, key);
|
|
50077
|
+
const z2 = findZone(c, zoneName);
|
|
50078
|
+
const res = await c.call(
|
|
50079
|
+
"Transport",
|
|
50080
|
+
"PlayChannel",
|
|
50081
|
+
[{ type: "Zone", name: "zone" }, { type: "Sooid", name: "profileid" }, { type: "Channel", name: "channel" }, { type: "ResultCallback<PlayFeedback>", name: "cb_result" }],
|
|
50082
|
+
buildArgs([Arg.ref(z2.oid), Arg.sooid(c.profile()), Arg.ref(st.oid)]),
|
|
50083
|
+
c.serviceOid("Transport")
|
|
50084
|
+
);
|
|
50085
|
+
log(`play_radio: ${field2(st, "Name")} on ${z2.name} \u2192 ${res.success ? "ok" : res.status}`);
|
|
50086
|
+
if (!res.success) throw new RoonError(`Roon refused to play the station (${res.status})`);
|
|
50087
|
+
return { zone: z2.name, station: field2(st, "Name") };
|
|
50088
|
+
});
|
|
50089
|
+
return { ok: true, ...r, ...await started2(r.zone, 0) };
|
|
50090
|
+
}
|
|
50091
|
+
__name(playRadio, "playRadio");
|
|
50092
|
+
async function radioFavorite(key, on) {
|
|
50093
|
+
return withCore(async (c) => {
|
|
50094
|
+
const st = await findStation(c, key);
|
|
50095
|
+
const args = buildArgs([Arg.collection([buildArgs([Arg.ref(st.oid)])])]);
|
|
50096
|
+
if (on) {
|
|
50097
|
+
const res = await c.call("Radio", "AddToLibrary", [{ type: "IList<Channel>", name: "channels" }, { type: "ResultCallback", name: "cb_result" }], args, radioOid(c));
|
|
50098
|
+
if (!res.success) throw new RoonError(`Roon refused (${res.status})`);
|
|
50099
|
+
} else {
|
|
50100
|
+
c.remoting.callMethodNoReply(radioOid(c), formatMethodSignature("Radio", "RemoveFromLibrary", [{ type: "IList<Channel>", name: "channels" }, { type: "ResultCallback", name: "cb_result" }]), args);
|
|
50101
|
+
}
|
|
50102
|
+
log(`radio_favorite: ${field2(st, "Name")} ${on ? "+" : "\u2212"}`);
|
|
50103
|
+
await wait(800);
|
|
50104
|
+
const id = hex3(field2(st, "ChannelId"));
|
|
50105
|
+
const mine = await myStations(c).catch(() => null);
|
|
50106
|
+
const now = mine ? mine.some((x) => hex3(field2(x, "ChannelId")) === id) : void 0;
|
|
50107
|
+
return {
|
|
50108
|
+
ok: now === void 0 || now === on,
|
|
50109
|
+
station: field2(st, "Name"),
|
|
50110
|
+
in_library: now ?? on,
|
|
50111
|
+
note: now === void 0 ? "Roon said ok; it couldn't be read back" : now !== on ? "Roon said ok, but nothing changed" : void 0
|
|
50112
|
+
};
|
|
50113
|
+
});
|
|
50114
|
+
}
|
|
50115
|
+
__name(radioFavorite, "radioFavorite");
|
|
50116
|
+
|
|
50117
|
+
// src/core/officialtools.ts
|
|
50118
|
+
init_errors();
|
|
50119
|
+
init_official();
|
|
50120
|
+
var call = /* @__PURE__ */ __name((fn) => new Promise((res, rej) => fn((err) => err ? rej(new RoonError(`Roon refused: ${err}`)) : res())), "call");
|
|
50121
|
+
var duration4 = /* @__PURE__ */ __name((s) => {
|
|
50122
|
+
const t = Math.round(s), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), r = t % 60;
|
|
50123
|
+
return h ? `${h}:${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}` : `${m}:${String(r).padStart(2, "0")}`;
|
|
50124
|
+
}, "duration");
|
|
50125
|
+
var wait2 = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "wait");
|
|
50126
|
+
async function zoneOf(name) {
|
|
50127
|
+
const t = await paired();
|
|
50128
|
+
await officialReady();
|
|
50129
|
+
const z2 = officialZone(name);
|
|
50130
|
+
if (!z2) throw new RoonError(`No Roon zone called "${name}"; Roon has: ${officialZones().map((x) => x.display_name).join(", ")}`, "not_found");
|
|
50131
|
+
return { t, z: z2 };
|
|
50132
|
+
}
|
|
50133
|
+
__name(zoneOf, "zoneOf");
|
|
50134
|
+
async function zones3() {
|
|
50135
|
+
await officialReady();
|
|
50136
|
+
const all = officialZones();
|
|
50137
|
+
return {
|
|
50138
|
+
zones: [...new Set(all.flatMap((z2) => z2.outputs.map((o) => o.display_name)))].sort(),
|
|
50139
|
+
groups: all.filter((z2) => z2.outputs.length > 1).map((z2) => z2.outputs.map((o) => o.display_name))
|
|
50140
|
+
};
|
|
50141
|
+
}
|
|
50142
|
+
__name(zones3, "zones");
|
|
50143
|
+
async function nowPlaying2() {
|
|
50144
|
+
await officialReady();
|
|
50145
|
+
return { zones: officialZones().map((z2) => {
|
|
50146
|
+
const r = { zone: z2.display_name, state: z2.state };
|
|
50147
|
+
const v = z2.outputs.find((o) => o.volume)?.volume;
|
|
50148
|
+
if (v) {
|
|
50149
|
+
r.volume = v.value;
|
|
50150
|
+
r.volume_max = v.max;
|
|
50151
|
+
if (v.is_muted) r.muted = true;
|
|
50152
|
+
}
|
|
50153
|
+
if (z2.settings?.shuffle) r.shuffle = true;
|
|
50154
|
+
if (z2.settings?.loop && z2.settings.loop !== "disabled") r.repeat = z2.settings.loop === "loop_one" ? "one" : "all";
|
|
50155
|
+
if (z2.settings?.auto_radio) r.roon_radio = true;
|
|
50156
|
+
const np = z2.now_playing;
|
|
50157
|
+
if (np && z2.state !== "stopped") {
|
|
50158
|
+
if (np.three_line?.line1) {
|
|
50159
|
+
r.title = np.three_line.line1;
|
|
50160
|
+
if (np.three_line.line2) r.artist = np.three_line.line2;
|
|
50161
|
+
if (np.three_line.line3) r.album = np.three_line.line3;
|
|
50162
|
+
}
|
|
50163
|
+
if (typeof np.seek_position === "number") r.position = duration4(np.seek_position);
|
|
50164
|
+
if (typeof np.length === "number") r.length = duration4(np.length);
|
|
50165
|
+
}
|
|
50166
|
+
return r;
|
|
50167
|
+
}) };
|
|
50168
|
+
}
|
|
50169
|
+
__name(nowPlaying2, "nowPlaying");
|
|
50170
|
+
async function control2(name, action) {
|
|
50171
|
+
const { t, z: z2 } = await zoneOf(name);
|
|
50172
|
+
await call((cb) => t.control(z2.zone_id, action, cb));
|
|
50173
|
+
const expect = { pause: ["paused"], stop: ["stopped", "paused"], play: ["playing", "loading"] };
|
|
50174
|
+
let state = officialZone(z2.display_name)?.state;
|
|
50175
|
+
if (expect[action]) {
|
|
50176
|
+
const end = Date.now() + 3e3;
|
|
50177
|
+
while (Date.now() < end && !expect[action].includes(state ?? "")) {
|
|
50178
|
+
await wait2(150);
|
|
50179
|
+
state = officialZone(z2.display_name)?.state;
|
|
50180
|
+
}
|
|
50181
|
+
}
|
|
50182
|
+
return { ok: !expect[action] || expect[action].includes(state ?? ""), zone: z2.display_name, action, state };
|
|
50183
|
+
}
|
|
50184
|
+
__name(control2, "control");
|
|
50185
|
+
async function seek2(name, seconds) {
|
|
50186
|
+
const { t, z: z2 } = await zoneOf(name);
|
|
50187
|
+
await call((cb) => t.seek(z2.zone_id, "absolute", Math.max(0, Math.round(seconds)), cb));
|
|
50188
|
+
return { ok: true, zone: z2.display_name, position: duration4(seconds) };
|
|
50189
|
+
}
|
|
50190
|
+
__name(seek2, "seek");
|
|
50191
|
+
async function settings2(name, s) {
|
|
50192
|
+
if (s.roon_radio === "now") throw new RoonError('"Start Roon Radio now" needs the internal protocol (switched off)', "invalid");
|
|
50193
|
+
const { t, z: z2 } = await zoneOf(name);
|
|
50194
|
+
const change = {};
|
|
50195
|
+
if (s.shuffle !== void 0) change.shuffle = s.shuffle;
|
|
50196
|
+
if (s.repeat !== void 0) change.loop = { off: "disabled", all: "loop", one: "loop_one" }[s.repeat];
|
|
50197
|
+
if (s.roon_radio !== void 0) change.auto_radio = s.roon_radio;
|
|
50198
|
+
if (!Object.keys(change).length) throw new RoonError("Nothing to set: give shuffle, repeat or roon_radio", "invalid");
|
|
50199
|
+
await call((cb) => t.change_settings(z2.zone_id, change, cb));
|
|
50200
|
+
return { ok: true, zone: z2.display_name, ...s };
|
|
50201
|
+
}
|
|
50202
|
+
__name(settings2, "settings");
|
|
50203
|
+
async function transfer2(from, to) {
|
|
50204
|
+
const a = await zoneOf(from), b = await zoneOf(to);
|
|
50205
|
+
if (a.z.zone_id === b.z.zone_id) throw new RoonError("From and to are the same zone", "invalid");
|
|
50206
|
+
await call((cb) => a.t.transfer_zone(a.z.zone_id, b.z.zone_id, cb));
|
|
50207
|
+
return { ok: true, from: a.z.display_name, to: b.z.display_name };
|
|
50208
|
+
}
|
|
50209
|
+
__name(transfer2, "transfer");
|
|
50210
|
+
async function group2(main2, join) {
|
|
50211
|
+
const m = await zoneOf(main2);
|
|
50212
|
+
const outs = [...m.z.outputs.map((o) => o.output_id)];
|
|
50213
|
+
for (const n of join) {
|
|
50214
|
+
const j = await zoneOf(n);
|
|
50215
|
+
for (const o of j.z.outputs) if (!outs.includes(o.output_id)) outs.push(o.output_id);
|
|
50216
|
+
}
|
|
50217
|
+
if (outs.length === m.z.outputs.length) throw new RoonError("Nothing to group: name at least one other zone", "invalid");
|
|
50218
|
+
await call((cb) => m.t.group_outputs(outs, cb));
|
|
50219
|
+
return { ok: true, group: outs.map((id) => officialZones().flatMap((z2) => z2.outputs).find((o) => o.output_id === id)?.display_name ?? id) };
|
|
50220
|
+
}
|
|
50221
|
+
__name(group2, "group");
|
|
50222
|
+
async function ungroup2(name, only) {
|
|
50223
|
+
const { t, z: z2 } = await zoneOf(name);
|
|
50224
|
+
if (z2.outputs.length < 2) throw new RoonError(`${z2.display_name} isn't in a group`, "invalid");
|
|
50225
|
+
const off = only ? z2.outputs.filter((o) => o.display_name.toLowerCase().includes(only.toLowerCase())) : z2.outputs;
|
|
50226
|
+
if (!off.length) throw new RoonError(`"${only}" isn't in this group`, "not_found");
|
|
50227
|
+
await call((cb) => t.ungroup_outputs(off.map((o) => o.output_id), cb));
|
|
50228
|
+
return { ok: true, released: off.map((o) => o.display_name) };
|
|
50229
|
+
}
|
|
50230
|
+
__name(ungroup2, "ungroup");
|
|
50231
|
+
async function standby2(name, on) {
|
|
50232
|
+
const { t, z: z2 } = await zoneOf(name);
|
|
50233
|
+
for (const o of z2.outputs) await call((cb) => on ? t.standby(o.output_id, {}, cb) : t.convenience_switch(o.output_id, {}, cb));
|
|
50234
|
+
return { ok: true, zone: z2.display_name, standby: on };
|
|
50235
|
+
}
|
|
50236
|
+
__name(standby2, "standby");
|
|
50237
|
+
async function cover2(target2, name) {
|
|
50238
|
+
await officialReady();
|
|
50239
|
+
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);
|
|
50240
|
+
if (!z2) throw new RoonError(name ? `No Roon zone called "${name}"` : "Nothing plays in Roon right now", "not_found");
|
|
50241
|
+
const key = z2.now_playing?.image_key;
|
|
50242
|
+
if (!key) throw new RoonError(`Roon has no cover for what plays on ${z2.display_name}`, "not_found");
|
|
50243
|
+
const r = await fetch(`http://${target2.host}:${target2.port}/api/image/${key}?scale=fit&width=600&height=600`).catch(() => null);
|
|
50244
|
+
if (!r?.ok) throw new RoonError("Roon doesn't hand over the cover right now", "unavailable");
|
|
50245
|
+
const l = z2.now_playing?.three_line;
|
|
50246
|
+
return {
|
|
50247
|
+
info: { zone: z2.display_name, title: l?.line1, artist: l?.line2, album: l?.line3 },
|
|
50248
|
+
image: { data: Buffer.from(await r.arrayBuffer()).toString("base64"), mimeType: r.headers.get("content-type") ?? "image/jpeg" }
|
|
50249
|
+
};
|
|
50250
|
+
}
|
|
50188
50251
|
__name(cover2, "cover");
|
|
50189
50252
|
|
|
50253
|
+
// src/tools/run.ts
|
|
50254
|
+
init_session();
|
|
50255
|
+
init_log();
|
|
50256
|
+
|
|
50257
|
+
// src/core/status.ts
|
|
50258
|
+
init_package();
|
|
50259
|
+
init_log();
|
|
50260
|
+
var checks = null;
|
|
50261
|
+
var checking = null;
|
|
50262
|
+
function selfTest(run2) {
|
|
50263
|
+
checking ??= run2().then((c) => {
|
|
50264
|
+
checks = c;
|
|
50265
|
+
const bad = c.filter((x) => !x.ok);
|
|
50266
|
+
log(bad.length ? `self-test: ${bad.map((x) => `${x.name} failed (${x.detail})`).join("; ")}` : `self-test: ${c.length} reads ok`);
|
|
50267
|
+
}).catch((e) => {
|
|
50268
|
+
log(`self-test couldn't run: ${e.message}`);
|
|
50269
|
+
});
|
|
50270
|
+
return checking;
|
|
50271
|
+
}
|
|
50272
|
+
__name(selfTest, "selfTest");
|
|
50273
|
+
var selfTestResults = /* @__PURE__ */ __name(() => checks, "selfTestResults");
|
|
50274
|
+
var newer = null;
|
|
50275
|
+
var told = false;
|
|
50276
|
+
var parse3 = /* @__PURE__ */ __name((v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0), "parse");
|
|
50277
|
+
var isNewer = /* @__PURE__ */ __name((a, b) => {
|
|
50278
|
+
const x = parse3(a), y = parse3(b);
|
|
50279
|
+
for (let i = 0; i < 3; i++) {
|
|
50280
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
50281
|
+
}
|
|
50282
|
+
return false;
|
|
50283
|
+
}, "isNewer");
|
|
50284
|
+
async function checkForUpdate() {
|
|
50285
|
+
if (process.env.DEEP_ROON_NO_UPDATE_CHECK === "1") return;
|
|
50286
|
+
try {
|
|
50287
|
+
const r = await fetch("https://api.github.com/repos/renebouwmeester/deep-roon-mcp/releases/latest", {
|
|
50288
|
+
headers: { "User-Agent": `deep-roon-mcp/${version2}`, Accept: "application/vnd.github+json" },
|
|
50289
|
+
signal: AbortSignal.timeout(5e3)
|
|
50290
|
+
});
|
|
50291
|
+
if (!r.ok) return;
|
|
50292
|
+
const j = await r.json();
|
|
50293
|
+
if (j.tag_name && isNewer(j.tag_name, version2)) {
|
|
50294
|
+
newer = { version: j.tag_name.replace(/^v/, ""), url: j.html_url ?? "" };
|
|
50295
|
+
log(`a newer version is out: ${newer.version}`);
|
|
50296
|
+
}
|
|
50297
|
+
} catch {
|
|
50298
|
+
}
|
|
50299
|
+
}
|
|
50300
|
+
__name(checkForUpdate, "checkForUpdate");
|
|
50301
|
+
var updateInfo = /* @__PURE__ */ __name(() => newer, "updateInfo");
|
|
50302
|
+
function updateNotice() {
|
|
50303
|
+
if (!newer || told) return null;
|
|
50304
|
+
told = true;
|
|
50305
|
+
return `Deep Roon MCP ${newer.version} is available (this is ${version2}): ${newer.url}`;
|
|
50306
|
+
}
|
|
50307
|
+
__name(updateNotice, "updateNotice");
|
|
50308
|
+
|
|
50309
|
+
// src/tools/run.ts
|
|
50310
|
+
var compact = /* @__PURE__ */ __name((_k, v) => v === void 0 ? void 0 : typeof v === "bigint" ? v.toString() : v, "compact");
|
|
50311
|
+
async function run(work2) {
|
|
50312
|
+
try {
|
|
50313
|
+
const result = await work2();
|
|
50314
|
+
const notice = updateNotice();
|
|
50315
|
+
return { content: [{ type: "text", text: JSON.stringify(result, compact) }, ...notice ? [{ type: "text", text: notice }] : []] };
|
|
50316
|
+
} catch (e) {
|
|
50317
|
+
const err = e;
|
|
50318
|
+
let text2;
|
|
50319
|
+
if (err instanceof RoonError) text2 = err.message;
|
|
50320
|
+
else if (/MissingMethod|method not found|unknown method/i.test(err.message)) {
|
|
50321
|
+
text2 = `Roon changed this part of its internal protocol in this version (${err.message}). Check for an update of Deep Roon MCP.`;
|
|
50322
|
+
} else text2 = `Roon didn't answer as expected: ${err.message}`;
|
|
50323
|
+
log(`tool error: ${err.message}`);
|
|
50324
|
+
return { isError: true, content: [{ type: "text", text: text2 }] };
|
|
50325
|
+
}
|
|
50326
|
+
}
|
|
50327
|
+
__name(run, "run");
|
|
50328
|
+
async function runImage(work2) {
|
|
50329
|
+
let image = null;
|
|
50330
|
+
const r = await run(async () => {
|
|
50331
|
+
const x = await work2();
|
|
50332
|
+
image = x.image;
|
|
50333
|
+
return x.info;
|
|
50334
|
+
});
|
|
50335
|
+
if (r.isError || !image) return r;
|
|
50336
|
+
const { data, mimeType } = image;
|
|
50337
|
+
return { content: [...r.content, { type: "image", data, mimeType }] };
|
|
50338
|
+
}
|
|
50339
|
+
__name(runImage, "runImage");
|
|
50340
|
+
|
|
50341
|
+
// src/tools/read.ts
|
|
50342
|
+
var READ = { readOnlyHint: true, openWorldHint: false };
|
|
50343
|
+
var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
|
|
50344
|
+
var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
|
|
50345
|
+
function registerReadTools(server, internal = true) {
|
|
50346
|
+
const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
|
|
50347
|
+
const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
|
|
50348
|
+
reg("roon_search", {
|
|
50349
|
+
title: "Search Roon",
|
|
50350
|
+
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.',
|
|
50351
|
+
inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
|
|
50352
|
+
annotations: READ
|
|
50353
|
+
}, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
|
|
50354
|
+
reg("roon_new", {
|
|
50355
|
+
title: "Streaming shelves in Roon",
|
|
50356
|
+
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.',
|
|
50357
|
+
inputSchema: {
|
|
50358
|
+
service,
|
|
50359
|
+
section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
|
|
50360
|
+
genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
|
|
50361
|
+
what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
|
|
50362
|
+
limit: limit(100, 30),
|
|
50363
|
+
offset: external_exports.number().int().min(0).optional()
|
|
50364
|
+
},
|
|
50365
|
+
annotations: READ
|
|
50366
|
+
}, ({ 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")));
|
|
50367
|
+
reg("roon_genres", {
|
|
50368
|
+
title: "Streaming genres",
|
|
50369
|
+
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.",
|
|
50370
|
+
inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
|
|
50371
|
+
annotations: READ
|
|
50372
|
+
}, ({ service: sv, parent }) => run(() => genres(parent, sv)));
|
|
50373
|
+
reg("roon_album", {
|
|
50374
|
+
title: "Album page",
|
|
50375
|
+
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.",
|
|
50376
|
+
inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
|
|
50377
|
+
annotations: READ
|
|
50378
|
+
}, ({ key, credits }) => run(() => album(key, credits === true)));
|
|
50379
|
+
reg("roon_review", {
|
|
50380
|
+
title: "Album review",
|
|
50381
|
+
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.",
|
|
50382
|
+
inputSchema: { key: external_exports.string().describe("An album key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
|
|
50383
|
+
annotations: READ
|
|
50384
|
+
}, ({ key, wikipedia, max_chars }) => run(() => review(key, max_chars ?? 1e9, wikipedia === true)));
|
|
50385
|
+
reg("roon_bio", {
|
|
50386
|
+
title: "Artist biography",
|
|
50387
|
+
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.',
|
|
50388
|
+
inputSchema: { key: external_exports.string().describe("An artist key."), wikipedia: external_exports.boolean().optional(), max_chars: external_exports.number().int().min(200).optional() },
|
|
50389
|
+
annotations: READ
|
|
50390
|
+
}, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
|
|
50391
|
+
reg("roon_artist", {
|
|
50392
|
+
title: "Discography",
|
|
50393
|
+
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.',
|
|
50394
|
+
inputSchema: { key: external_exports.string().describe("An artist key."), service },
|
|
50395
|
+
annotations: READ
|
|
50396
|
+
}, ({ key, service: sv }) => run(() => discography(key, sv)));
|
|
50397
|
+
reg("roon_zones", {
|
|
50398
|
+
title: "Roon zones",
|
|
50399
|
+
description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
|
|
50400
|
+
inputSchema: {},
|
|
50401
|
+
annotations: READ
|
|
50402
|
+
}, () => run(() => internal ? zones2() : zones3()));
|
|
50403
|
+
reg("roon_now_playing", {
|
|
50404
|
+
title: "Now playing in Roon",
|
|
50405
|
+
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.",
|
|
50406
|
+
inputSchema: {},
|
|
50407
|
+
annotations: READ
|
|
50408
|
+
}, () => run(() => internal ? nowPlaying() : nowPlaying2()));
|
|
50409
|
+
reg("roon_radio", {
|
|
50410
|
+
title: "Live Radio",
|
|
50411
|
+
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 their logo: `image_url` (a plain https picture) for the directory, `image` (a key at the Core) for an own station.',
|
|
50412
|
+
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) },
|
|
50413
|
+
annotations: READ
|
|
50414
|
+
}, ({ category, genre, limit: n }) => run(() => radio(category, genre, n ?? 40)));
|
|
50415
|
+
reg("roon_history", {
|
|
50416
|
+
title: "Play history",
|
|
50417
|
+
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.`,
|
|
50418
|
+
inputSchema: { limit: limit(300, 30) },
|
|
50419
|
+
annotations: READ
|
|
50420
|
+
}, ({ limit: n }) => run(() => history(n ?? 30)));
|
|
50421
|
+
reg("roon_top", {
|
|
50422
|
+
title: "Most played",
|
|
50423
|
+
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.',
|
|
50424
|
+
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).") },
|
|
50425
|
+
annotations: READ
|
|
50426
|
+
}, ({ what, limit: n, since }) => run(() => top(what ?? "albums", n ?? 20, since)));
|
|
50427
|
+
}
|
|
50428
|
+
__name(registerReadTools, "registerReadTools");
|
|
50429
|
+
|
|
50190
50430
|
// src/tools/control.ts
|
|
50191
50431
|
init_official();
|
|
50192
50432
|
init_session();
|
|
@@ -50210,18 +50450,24 @@ function registerControlTools(server, internal = true) {
|
|
|
50210
50450
|
inputSchema: { keys: external_exports.array(external_exports.string()).min(1).max(60).describe("Track keys, in playing order."), zone, mode },
|
|
50211
50451
|
annotations: PLAYS
|
|
50212
50452
|
}, ({ keys, zone: zn, mode: m }) => run(() => playTracks(keys, zn, m ?? "now")));
|
|
50453
|
+
reg("roon_play_radio", {
|
|
50454
|
+
title: "Play a radio station",
|
|
50455
|
+
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.",
|
|
50456
|
+
inputSchema: { key: external_exports.string().describe("A station key, or the name of an own station."), zone },
|
|
50457
|
+
annotations: PLAYS
|
|
50458
|
+
}, ({ key, zone: zn }) => run(() => playRadio(key, zn)));
|
|
50213
50459
|
reg("roon_control", {
|
|
50214
50460
|
title: "Transport",
|
|
50215
50461
|
description: "The buttons of a Roon zone: pause, play (resume), playpause, stop, next, previous \u2014 and release: let go of the audio device right away (what Roon does by itself a few seconds after a pause), so another source can use the speaker. Only on an explicit request, never as a probe.",
|
|
50216
50462
|
inputSchema: { zone, action: external_exports.enum(["pause", "play", "playpause", "stop", "next", "previous", "release"]) },
|
|
50217
50463
|
annotations: PLAYS
|
|
50218
|
-
}, ({ zone: zn, action }) => run(() => internal ?
|
|
50464
|
+
}, ({ zone: zn, action }) => run(() => internal ? control(zn, action) : action === "release" ? Promise.reject(new Error("release needs Roon's internal protocol (switched off)")) : control2(zn, action)));
|
|
50219
50465
|
reg("roon_seek", {
|
|
50220
50466
|
title: "Seek",
|
|
50221
50467
|
description: "Jump to a position in the track that plays on a Roon zone, in seconds from the start. Only on an explicit request.",
|
|
50222
50468
|
inputSchema: { zone, seconds: external_exports.number().int().min(0) },
|
|
50223
50469
|
annotations: PLAYS
|
|
50224
|
-
}, ({ zone: zn, seconds }) => run(() => internal ?
|
|
50470
|
+
}, ({ zone: zn, seconds }) => run(() => internal ? seek(zn, seconds) : seek2(zn, seconds)));
|
|
50225
50471
|
reg("roon_volume", {
|
|
50226
50472
|
title: "Volume",
|
|
50227
50473
|
description: "Set the volume of a Roon zone to an absolute level, in that zone's own scale (roon_now_playing shows `volume` and `volume_max`), and/or mute or unmute it. Only on an explicit request; small steps unless a level is named. Needs Deep Roon MCP enabled in Roon (Settings \u203A Extensions).",
|
|
@@ -50235,7 +50481,7 @@ function registerControlTools(server, internal = true) {
|
|
|
50235
50481
|
annotations: PLAYS
|
|
50236
50482
|
}, ({ zone: zn, shuffle, repeat, roon_radio }) => {
|
|
50237
50483
|
const s = { shuffle, repeat, roon_radio: roon_radio === void 0 ? void 0 : roon_radio === "now" ? "now" : roon_radio === "on" };
|
|
50238
|
-
return run(() => internal ?
|
|
50484
|
+
return run(() => internal ? settings(zn, s) : settings2(zn, s));
|
|
50239
50485
|
});
|
|
50240
50486
|
reg("roon_queue", {
|
|
50241
50487
|
title: "Queue",
|
|
@@ -50255,6 +50501,12 @@ function registerControlTools(server, internal = true) {
|
|
|
50255
50501
|
inputSchema: { zone, ns: external_exports.array(external_exports.number().int().min(2)).min(1).max(100).describe("Positions from roon_queue (2 and up: 1 is what plays).") },
|
|
50256
50502
|
annotations: CHANGES_QUEUE
|
|
50257
50503
|
}, ({ zone: zn, ns }) => run(() => queueRemove(zn, ns)));
|
|
50504
|
+
reg("roon_queue_move", {
|
|
50505
|
+
title: "Move in the queue",
|
|
50506
|
+
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.",
|
|
50507
|
+
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`.") },
|
|
50508
|
+
annotations: CHANGES_QUEUE
|
|
50509
|
+
}, ({ zone: zn, n, to, where }) => run(() => queueMove(zn, n, to, where ?? "before")));
|
|
50258
50510
|
reg("roon_queue_clear", {
|
|
50259
50511
|
title: "Clear the queue",
|
|
50260
50512
|
description: "Clear the queue of a Roon zone. By default the track that plays keeps playing and everything after it is removed; `everything: true` also removes the playing track (the zone falls silent). Only on an explicit request.",
|
|
@@ -50266,19 +50518,19 @@ function registerControlTools(server, internal = true) {
|
|
|
50266
50518
|
description: `Move what plays \u2014 with its queue and position \u2014 from one Roon zone to another (Roon's own "Transfer zone"). The tool for "move the music to \u2026": never restart the album with roon_play for that. Read roon_now_playing first if the source zone isn't named.`,
|
|
50267
50519
|
inputSchema: { from: external_exports.string().describe("The zone that plays now."), to: external_exports.string().describe("The zone it should move to.") },
|
|
50268
50520
|
annotations: PLAYS
|
|
50269
|
-
}, ({ from, to }) => run(() => internal ?
|
|
50521
|
+
}, ({ from, to }) => run(() => internal ? transfer(from, to) : transfer2(from, to)));
|
|
50270
50522
|
reg("roon_group", {
|
|
50271
50523
|
title: "Group zones",
|
|
50272
50524
|
description: 'Group Roon zones so they play the same music in sync (Roon\'s own "Group zones"). `zone` keeps its music and queue; `with` are the zones that join. Roon groups only outputs of the same kind. Only on an explicit request.',
|
|
50273
50525
|
inputSchema: { zone: external_exports.string().describe("The zone whose music continues."), with: external_exports.array(external_exports.string()).min(1).describe("The zones that join.") },
|
|
50274
50526
|
annotations: PLAYS
|
|
50275
|
-
}, (a) => run(() => internal ?
|
|
50527
|
+
}, (a) => run(() => internal ? group(a.zone, a.with) : group2(a.zone, a.with)));
|
|
50276
50528
|
reg("roon_ungroup", {
|
|
50277
50529
|
title: "Ungroup zones",
|
|
50278
50530
|
description: "Undo a Roon zone group: `zone` is any member. Without `only` the whole group is dissolved; with `only` (a member's name) just that one leaves. Only on an explicit request.",
|
|
50279
50531
|
inputSchema: { zone: external_exports.string().describe("A member of the group."), only: external_exports.string().optional() },
|
|
50280
50532
|
annotations: PLAYS
|
|
50281
|
-
}, ({ zone: zn, only }) => run(() => internal ?
|
|
50533
|
+
}, ({ zone: zn, only }) => run(() => internal ? ungroup(zn, only) : ungroup2(zn, only)));
|
|
50282
50534
|
reg("roon_sleep", {
|
|
50283
50535
|
title: "Sleep timer",
|
|
50284
50536
|
description: 'The sleep timer of a Roon zone: stop the music after `minutes` (1\u2013600), or `off: true` to cancel it. roon_now_playing shows a running timer. Only on an explicit request ("stop in half an hour").',
|
|
@@ -50293,13 +50545,13 @@ function registerControlTools(server, internal = true) {
|
|
|
50293
50545
|
description: 'Put a Roon zone\'s device in standby (`action: "standby"`) or wake it (`action: "wake"`), where the device supports it. Only on an explicit request.',
|
|
50294
50546
|
inputSchema: { zone, action: external_exports.enum(["standby", "wake"]) },
|
|
50295
50547
|
annotations: PLAYS
|
|
50296
|
-
}, ({ zone: zn, action }) => run(() => internal ?
|
|
50548
|
+
}, ({ zone: zn, action }) => run(() => internal ? standby(zn, action === "standby") : standby2(zn, action === "standby")));
|
|
50297
50549
|
reg("roon_cover", {
|
|
50298
50550
|
title: "Cover",
|
|
50299
50551
|
description: "The album cover of what plays in a Roon zone, as an image (about 600 px), with the title in text. Without `zone` it takes the zone that plays. Read-only.",
|
|
50300
50552
|
inputSchema: { zone: external_exports.string().optional().describe("A Roon zone name (default: the zone that plays).") },
|
|
50301
50553
|
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
50302
|
-
}, ({ zone: zn }) => runImage(async () => internal ?
|
|
50554
|
+
}, ({ zone: zn }) => runImage(async () => internal ? cover(zn) : cover2(await findTarget(), zn)));
|
|
50303
50555
|
}
|
|
50304
50556
|
__name(registerControlTools, "registerControlTools");
|
|
50305
50557
|
|
|
@@ -50313,11 +50565,12 @@ init_writer();
|
|
|
50313
50565
|
init_session();
|
|
50314
50566
|
init_log();
|
|
50315
50567
|
init_graph();
|
|
50568
|
+
init_focus();
|
|
50316
50569
|
var localNow = /* @__PURE__ */ __name(() => {
|
|
50317
50570
|
const d = /* @__PURE__ */ new Date(), t = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "t");
|
|
50318
50571
|
return `${d.getFullYear()}-${t(d.getMonth() + 1)}-${t(d.getDate())}T${t(d.getHours())}:${t(d.getMinutes())}:${t(d.getSeconds())}`;
|
|
50319
50572
|
}, "localNow");
|
|
50320
|
-
var
|
|
50573
|
+
var decoded2 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
|
|
50321
50574
|
async function dailyPicks(perBox) {
|
|
50322
50575
|
return withCore(async (c) => {
|
|
50323
50576
|
const DP = "Sooloos.Broker.Api.DailyPicksParameters";
|
|
@@ -50333,7 +50586,7 @@ async function dailyPicks(perBox) {
|
|
|
50333
50586
|
c.serviceOid("Library")
|
|
50334
50587
|
);
|
|
50335
50588
|
if (!res.success) throw new RoonError(`Roon refused the Daily Picks (${res.status})`);
|
|
50336
|
-
const boxes = items(c,
|
|
50589
|
+
const boxes = items(c, decoded2(c, res.payload));
|
|
50337
50590
|
const things = /* @__PURE__ */ __name((b) => {
|
|
50338
50591
|
for (const k of ["DisplayAlbums", "Albums", "Tracks", "Performers"]) {
|
|
50339
50592
|
const l = obj(c, field2(b, k))?.fields?.$items ?? [];
|
|
@@ -50357,14 +50610,25 @@ async function dailyPicks(perBox) {
|
|
|
50357
50610
|
});
|
|
50358
50611
|
}
|
|
50359
50612
|
__name(dailyPicks, "dailyPicks");
|
|
50360
|
-
async function newForYou(limit3, offset2) {
|
|
50613
|
+
async function newForYou(limit3, offset2, genre) {
|
|
50361
50614
|
return withCore(async (c) => {
|
|
50362
50615
|
const res = await c.call("Library", "GetNewReleasesForYou", [{ type: "Sooid", name: "profileid" }, { type: "ResultCallback<DataList<AlbumWithExtras>>", name: "cb_result" }], buildArgs([Arg.sooid(c.profile())]), c.serviceOid("Library"));
|
|
50363
50616
|
if (!res.success) throw new RoonError(`Roon refused New releases for you (${res.status})`);
|
|
50364
|
-
|
|
50617
|
+
let all = items(c, decoded2(c, res.payload));
|
|
50618
|
+
await untilLoaded(c, all.map((x) => field2(x, "Album")).filter(isRef2), 4e3);
|
|
50619
|
+
const top2 = await toplevelGenres(c);
|
|
50620
|
+
const counts = /* @__PURE__ */ new Map();
|
|
50621
|
+
for (const x of all) for (const n of new Set(strings(c, field2(obj(c, field2(x, "Album")), "Genres")))) if (top2.has(n)) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
50622
|
+
const genres2 = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name, albums]) => ({ name, albums }));
|
|
50623
|
+
if (genre) {
|
|
50624
|
+
const g = genre.toLowerCase();
|
|
50625
|
+
const names2 = /* @__PURE__ */ __name((x) => strings(c, field2(obj(c, field2(x, "Album")), "Genres")).map((n) => n.toLowerCase()), "names");
|
|
50626
|
+
const exact = all.filter((x) => names2(x).includes(g));
|
|
50627
|
+
all = exact.length ? exact : all.filter((x) => names2(x).some((n) => n.includes(g)));
|
|
50628
|
+
}
|
|
50365
50629
|
const part = all.slice(Math.max(0, offset2), Math.max(0, offset2) + Math.min(Math.max(1, limit3), 100));
|
|
50366
50630
|
await untilLoaded(c, part.map((x) => field2(x, "Album")).filter(isRef2), 3e3);
|
|
50367
|
-
return { total: all.length, offset: Math.max(0, offset2), rows: part.map((x) => {
|
|
50631
|
+
return { total: all.length, offset: Math.max(0, offset2), genres: genres2, rows: part.map((x) => {
|
|
50368
50632
|
const a = obj(c, field2(x, "Album"));
|
|
50369
50633
|
if (!a || field2(a, "Title") === void 0) return null;
|
|
50370
50634
|
const r = albumRow(c, a);
|
|
@@ -50389,7 +50653,7 @@ async function readMixes(c) {
|
|
|
50389
50653
|
c.serviceOid("Library")
|
|
50390
50654
|
);
|
|
50391
50655
|
if (!res.success) throw new RoonError(`Roon refused the Daily Mixes (${res.status})`);
|
|
50392
|
-
return items(c,
|
|
50656
|
+
return items(c, decoded2(c, res.payload));
|
|
50393
50657
|
}
|
|
50394
50658
|
__name(readMixes, "readMixes");
|
|
50395
50659
|
async function mixes() {
|
|
@@ -50398,10 +50662,43 @@ async function mixes() {
|
|
|
50398
50662
|
const r = { key: mixKey(m), kind: gd && field2(gd, "GenreName") ? "genre" : "artist", name: mixName(c, m) };
|
|
50399
50663
|
const ts = (obj(c, field2(pd ?? gd, "Touchstones"))?.fields?.$items ?? []).filter((x) => typeof x === "string").map(clean).slice(0, 8);
|
|
50400
50664
|
if (ts.length) r.touchstones = ts;
|
|
50665
|
+
const img = pd ? imageKey(c, pd, "Photo") ?? imageKey(c, pd, "Avatar") : void 0;
|
|
50666
|
+
if (img) r.image = img;
|
|
50667
|
+
const imgs = gd ? imageKeys(c, gd) : [];
|
|
50668
|
+
if (imgs.length) r.images = imgs;
|
|
50401
50669
|
return r;
|
|
50402
50670
|
}) }));
|
|
50403
50671
|
}
|
|
50404
50672
|
__name(mixes, "mixes");
|
|
50673
|
+
async function mix(key, limit3) {
|
|
50674
|
+
return withCore(async (c) => {
|
|
50675
|
+
const all = await readMixes(c);
|
|
50676
|
+
const m = all.find((x) => mixKey(x) === key) ?? all.find((x) => String(mixName(c, x)).toLowerCase() === key.toLowerCase());
|
|
50677
|
+
if (!m?.oid) throw new RoonError("Unknown mix \u2014 read roon_mixes for today's keys", "not_found");
|
|
50678
|
+
const res = await c.call("Mix", "GetItems", [{ type: "ResultCallback<DataList<MixItem>>", name: "cb_result" }], Buffer.alloc(0), m.oid);
|
|
50679
|
+
if (!res.success) throw new RoonError(`Roon refused the mix (${res.status})`);
|
|
50680
|
+
const its = items(c, decoded2(c, res.payload));
|
|
50681
|
+
const refs = its.flatMap((it) => obj(c, field2(it, "Tracks"))?.fields?.$items ?? []);
|
|
50682
|
+
await untilLoaded(c, refs, 3e3);
|
|
50683
|
+
const tracks2 = its.flatMap((it) => items(c, field2(it, "Tracks"))).filter((t) => field2(t, "Title") !== void 0);
|
|
50684
|
+
const pd = obj(c, field2(m, "PerformerMixDescription"));
|
|
50685
|
+
const head = { key: mixKey(m), name: mixName(c, m) };
|
|
50686
|
+
const img = pd ? imageKey(c, pd, "Photo") ?? imageKey(c, pd, "Avatar") : void 0;
|
|
50687
|
+
if (img) head.image = img;
|
|
50688
|
+
return { mix: head, total: tracks2.length, rows: tracks2.slice(0, Math.min(Math.max(1, limit3), 300)).map((t, i) => {
|
|
50689
|
+
const al = obj(c, field2(t, "Album"));
|
|
50690
|
+
const r = { n: i + 1, key: String(field2(t, "TrackId") ?? ""), title: field2(t, "Title") ?? "", artist: clean(field2(al, "PerformedBy") ?? field2(t, "PerformedBy") ?? ""), album: field2(al, "Title") ?? void 0 };
|
|
50691
|
+
const ak = field2(al, "AlbumId");
|
|
50692
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
50693
|
+
const l = field2(t, "LengthSeconds");
|
|
50694
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50695
|
+
const im = imageKey(c, al);
|
|
50696
|
+
if (im) r.image = im;
|
|
50697
|
+
return r;
|
|
50698
|
+
}) };
|
|
50699
|
+
});
|
|
50700
|
+
}
|
|
50701
|
+
__name(mix, "mix");
|
|
50405
50702
|
async function playMix(key, zoneName, how) {
|
|
50406
50703
|
const p = PRIORITY[how];
|
|
50407
50704
|
const r = await withCore(async (c) => {
|
|
@@ -50491,6 +50788,10 @@ async function playlists(shelfName, genre, mood, limit3, offset2) {
|
|
|
50491
50788
|
const key = playlistKey(p);
|
|
50492
50789
|
seen.set(key, w);
|
|
50493
50790
|
const r = { key, name: String(field2(p, "Name") ?? "").trim() };
|
|
50791
|
+
const imgs = imageKeys(c, p);
|
|
50792
|
+
if (imgs.length) r.images = imgs;
|
|
50793
|
+
const urls = imageUrls(c, p);
|
|
50794
|
+
if (urls.length) r.image_urls = urls;
|
|
50494
50795
|
const d = field2(p, "Description");
|
|
50495
50796
|
if (d) r.description = clean(d).replace(/\s+/g, " ").trim().slice(0, 240);
|
|
50496
50797
|
const n = field2(p, "TrackCount") ?? field2(p, "ItemCount");
|
|
@@ -50520,7 +50821,7 @@ var profileObject = /* @__PURE__ */ __name((c) => {
|
|
|
50520
50821
|
if (!p) throw new RoonError("Roon gave no profile");
|
|
50521
50822
|
return p;
|
|
50522
50823
|
}, "profileObject");
|
|
50523
|
-
var
|
|
50824
|
+
var decoded3 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
|
|
50524
50825
|
var PLAYLIST_KIND = { 1: "itunes", 5: "roon", 6: "roon (read-only)", 10: "TIDAL", 11: "file", 12: "qobuz", 13: "roon editorial", 15: "smart" };
|
|
50525
50826
|
function allPlaylists(c) {
|
|
50526
50827
|
const service2 = c.graph.findByType("Playlists")[0];
|
|
@@ -50536,13 +50837,24 @@ async function findPlaylist2(c, key) {
|
|
|
50536
50837
|
}
|
|
50537
50838
|
__name(findPlaylist2, "findPlaylist");
|
|
50538
50839
|
async function playlists2() {
|
|
50539
|
-
return withCore(async (c) => ({ playlists: allPlaylists(c).list.map((p) =>
|
|
50540
|
-
|
|
50541
|
-
|
|
50542
|
-
|
|
50543
|
-
|
|
50544
|
-
|
|
50545
|
-
|
|
50840
|
+
return withCore(async (c) => ({ playlists: allPlaylists(c).list.map((p) => {
|
|
50841
|
+
const r = {
|
|
50842
|
+
key: playlistKey(p),
|
|
50843
|
+
name: field2(p, "Name") ?? "",
|
|
50844
|
+
kind: PLAYLIST_KIND[field2(p, "Type")] ?? String(field2(p, "Type") ?? ""),
|
|
50845
|
+
tracks: field2(p, "TrackCount") ?? field2(p, "ItemCount") ?? void 0,
|
|
50846
|
+
read_only: field2(p, "IsReadOnly") === true || void 0
|
|
50847
|
+
};
|
|
50848
|
+
const imgs = imageKeys(c, p);
|
|
50849
|
+
if (imgs.length) r.images = imgs;
|
|
50850
|
+
const l = field2(p, "LengthSeconds");
|
|
50851
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50852
|
+
const names2 = (obj(c, field2(p, "SampleArtistNames"))?.fields?.$items ?? []).filter((x) => typeof x === "string").slice(0, 4);
|
|
50853
|
+
if (names2.length) r.artists = names2;
|
|
50854
|
+
const d = field2(p, "Description");
|
|
50855
|
+
if (d) r.description = clean(d).replace(/\s+/g, " ").slice(0, 240);
|
|
50856
|
+
return r;
|
|
50857
|
+
}) }));
|
|
50546
50858
|
}
|
|
50547
50859
|
__name(playlists2, "playlists");
|
|
50548
50860
|
async function playlist(key, limit3) {
|
|
@@ -50550,7 +50862,7 @@ async function playlist(key, limit3) {
|
|
|
50550
50862
|
const p = await findPlaylist2(c, key);
|
|
50551
50863
|
const res = await c.call("Playlist", "GetItems", [{ type: "ResultCallback<Query<PlaylistItem>>", name: "cb_result" }], Buffer.alloc(0), p.oid);
|
|
50552
50864
|
if (!res.success) throw new RoonError(`Roon refused the playlist (${res.status})`);
|
|
50553
|
-
const v =
|
|
50865
|
+
const v = decoded3(c, res.payload);
|
|
50554
50866
|
const n = Math.min(Math.max(1, limit3), 300);
|
|
50555
50867
|
const end = Date.now() + 3e3;
|
|
50556
50868
|
let list = [];
|
|
@@ -50562,7 +50874,14 @@ async function playlist(key, limit3) {
|
|
|
50562
50874
|
return { name: field2(p, "Name"), total: list.length, rows: list.slice(0, n).map((x, i) => {
|
|
50563
50875
|
const tr = obj(c, field2(obj(c, x), "Track"));
|
|
50564
50876
|
const al = obj(c, field2(tr, "Album"));
|
|
50565
|
-
|
|
50877
|
+
const r = { n: i + 1, key: String(field2(tr, "TrackId") ?? ""), title: field2(tr, "Title") ?? null, artist: clean(field2(al, "PerformedBy") ?? ""), album: field2(al, "Title") ?? void 0 };
|
|
50878
|
+
const ak = field2(al, "AlbumId");
|
|
50879
|
+
if (ak !== void 0 && ak !== null) r.album_key = String(ak);
|
|
50880
|
+
const im = imageKey(c, al);
|
|
50881
|
+
if (im) r.image = im;
|
|
50882
|
+
const l = field2(tr, "LengthSeconds");
|
|
50883
|
+
if (typeof l === "number" && l > 0) r.length = duration3(l);
|
|
50884
|
+
return r;
|
|
50566
50885
|
}) };
|
|
50567
50886
|
});
|
|
50568
50887
|
}
|
|
@@ -50684,7 +51003,7 @@ async function listenLater(includePlayed) {
|
|
|
50684
51003
|
c.serviceOid("Library")
|
|
50685
51004
|
);
|
|
50686
51005
|
if (!res.success) throw new RoonError(`Roon refused Listen Later (${res.status})`);
|
|
50687
|
-
const list = items(c,
|
|
51006
|
+
const list = items(c, decoded3(c, res.payload));
|
|
50688
51007
|
const thing = /* @__PURE__ */ __name((it) => field2(it, "Album") ?? field2(it, "Track") ?? field2(it, "Performer"), "thing");
|
|
50689
51008
|
await untilLoaded(c, list.map(thing).filter(isRef2), 2500);
|
|
50690
51009
|
const rows = list.map((it) => {
|
|
@@ -50794,7 +51113,7 @@ async function similarAlbum(key, limit3) {
|
|
|
50794
51113
|
buildArgs([Arg.ref(al.oid), Arg.sooid(c.profile())]),
|
|
50795
51114
|
c.serviceOid("Library")
|
|
50796
51115
|
), 8e3);
|
|
50797
|
-
const recommended = res?.success ? items(c,
|
|
51116
|
+
const recommended = res?.success ? items(c, decoded3(c, res.payload)) : [];
|
|
50798
51117
|
const similar = ["SimilarAlbums", "SoundsSimilar", "SeeAlso", "WasInfluencedBy", "WasInspiredBy"].flatMap((k) => items(c, field2(A, k)));
|
|
50799
51118
|
await untilLoaded(c, [...recommended, ...similar].map((o) => ({ $ref: o.oid })), 2500);
|
|
50800
51119
|
return { album: albumRow(c, al), similar: unique(similar), recommended: unique(recommended) };
|
|
@@ -50812,7 +51131,7 @@ async function similarArtist(key, limit3) {
|
|
|
50812
51131
|
buildArgs([Arg.ref(lv.$ref), Arg.sooid(c.profile())]),
|
|
50813
51132
|
c.serviceOid("Library")
|
|
50814
51133
|
), 8e3);
|
|
50815
|
-
const w = res?.success ? obj(c,
|
|
51134
|
+
const w = res?.success ? obj(c, decoded3(c, res.payload)) : null;
|
|
50816
51135
|
const list = /* @__PURE__ */ __name((k) => items(c, field2(w, k)), "list");
|
|
50817
51136
|
await untilLoaded(c, ["RecommendedAlbums", "LatestAlbums", "AppearsOnAlbums"].flatMap(list).map((o) => ({ $ref: o.oid })), 2500);
|
|
50818
51137
|
const rows = /* @__PURE__ */ __name((k) => list(k).filter((o) => field2(o, "Title") !== void 0).slice(0, limit3).map((o) => albumRow(c, o)), "rows");
|
|
@@ -50941,7 +51260,7 @@ async function dspOf(c, endpoint) {
|
|
|
50941
51260
|
const cc = obj(c, field2(obj(c, field2(endpoint, "Device")), "CommonConfig"));
|
|
50942
51261
|
if (!cc || field2(cc, "SupportsDspConfig") !== true) return null;
|
|
50943
51262
|
const res = await short(c.call("AudioDeviceCommonConfig", "GetDspConfig", [{ type: "ResultCallback<DspConfig>", name: "conf" }], Buffer.alloc(0), cc.oid), 6e3);
|
|
50944
|
-
const v = res?.success ?
|
|
51263
|
+
const v = res?.success ? decoded3(c, res.payload) : null;
|
|
50945
51264
|
if (!isRef2(v)) return null;
|
|
50946
51265
|
const end = Date.now() + 2500;
|
|
50947
51266
|
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);
|
|
@@ -51079,6 +51398,10 @@ var FOCUS = {
|
|
|
51079
51398
|
picks: external_exports.boolean().optional().describe("Roon's own picks."),
|
|
51080
51399
|
text: external_exports.string().optional().describe("A word in the title or artist."),
|
|
51081
51400
|
favorites: external_exports.boolean().optional(),
|
|
51401
|
+
sample_rate: external_exports.string().optional().describe('Sample rates in kHz, comma-separated: "44.1, 96".'),
|
|
51402
|
+
bits: external_exports.string().optional().describe('Bit depths, comma-separated: "16, 24".'),
|
|
51403
|
+
mqa: external_exports.boolean().optional(),
|
|
51404
|
+
storage: external_exports.enum(["tidal", "qobuz", "local"]).optional().describe("Where the albums live."),
|
|
51082
51405
|
played_within_days: days,
|
|
51083
51406
|
not_played_within_days: days,
|
|
51084
51407
|
added_within_days: days
|
|
@@ -51167,12 +51490,18 @@ function registerLibraryTools(server) {
|
|
|
51167
51490
|
inputSchema: { limit: limit2(30, 8) },
|
|
51168
51491
|
annotations: READ2
|
|
51169
51492
|
}, ({ limit: n }) => run(() => dailyPicks(n ?? 8)));
|
|
51493
|
+
server.registerTool("roon_mix", {
|
|
51494
|
+
title: "A Daily Mix's tracks",
|
|
51495
|
+
description: "The tracks inside one of Roon's Daily Mixes (by `key` or name from roon_mixes), as Roon's mix page shows them before playing \u2014 each with a `key` for roon_play_tracks. To play the whole mix, roon_play_mix.",
|
|
51496
|
+
inputSchema: { key: external_exports.string().describe("A mix key or its name, from roon_mixes."), limit: limit2(300, 100) },
|
|
51497
|
+
annotations: READ2
|
|
51498
|
+
}, ({ key, limit: n }) => run(() => mix(key, n ?? 100)));
|
|
51170
51499
|
server.registerTool("roon_new_for_you", {
|
|
51171
51500
|
title: "New releases for you",
|
|
51172
|
-
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (the streaming services\' editorial shelf, the same for everyone). Use it for "my new releases", "what\'s new for me". Albums carry year, format, sometimes a `review`, and a `key
|
|
51173
|
-
inputSchema: { limit: limit2(100, 30), offset },
|
|
51501
|
+
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (the streaming services\' editorial shelf, the same for everyone). Use it for "my new releases", "what\'s new for me". Albums carry year, format, `genres`, sometimes a `review`, and a `key`; `genres` at the top counts the shelf\'s top-level genres (the choices for `genre`).',
|
|
51502
|
+
inputSchema: { limit: limit2(100, 30), offset, genre: external_exports.string().optional().describe("Only albums of this genre (a Roon genre name, e.g. Jazz; sub-genres match too).") },
|
|
51174
51503
|
annotations: READ2
|
|
51175
|
-
}, ({ limit: n, offset: o }) => run(() => newForYou(n ?? 30, o ?? 0)));
|
|
51504
|
+
}, ({ limit: n, offset: o, genre }) => run(() => newForYou(n ?? 30, o ?? 0, genre)));
|
|
51176
51505
|
server.registerTool("roon_mixes", {
|
|
51177
51506
|
title: "Daily Mixes",
|
|
51178
51507
|
description: 'Roon\'s own Daily Mixes, from Roon\'s Home \u2014 NOT TIDAL\'s mixes: for "TIDAL mixes", "My Mix", "My Daily Discovery" or TIDAL\'s daily mixes use roon_tidal_playlists with `shelf: "my_mixes"`. These are today\'s personal Roon mixes, each built around an artist (or genre) the user listens to, with a few `touchstones` and a `key` for roon_play_mix. They change every day.',
|
|
@@ -51227,6 +51556,12 @@ function registerLibraryTools(server) {
|
|
|
51227
51556
|
inputSchema: { source: external_exports.enum(["roon", "qobuz", "tidal"]).optional(), limit: limit2(100, 50) },
|
|
51228
51557
|
annotations: READ2
|
|
51229
51558
|
}, ({ source, limit: n }) => run(() => favorites(source ?? "roon", n ?? 50)));
|
|
51559
|
+
server.registerTool("roon_radio_favorite", {
|
|
51560
|
+
title: "A station in My Live Radio",
|
|
51561
|
+
description: "Put a Live Radio station in the user's own list (My Live Radio), or take it out (`on: false`). The `key` comes from roon_radio. Changes the user's library: only on an explicit request. Reads the result back.",
|
|
51562
|
+
inputSchema: { key: external_exports.string(), on: external_exports.boolean().optional() },
|
|
51563
|
+
annotations: CHANGES
|
|
51564
|
+
}, ({ key, on }) => run(() => radioFavorite(key, on !== false)));
|
|
51230
51565
|
server.registerTool("roon_favorite", {
|
|
51231
51566
|
title: "Heart an album or track",
|
|
51232
51567
|
description: "Put a heart on an album or a track in Roon, or take it off (`on: false`). The `key` comes from a search, shelf, album or playlist result. Changes the user's library: only on an explicit request. Reads the heart back and says if Roon didn't change it.",
|
|
@@ -51330,7 +51665,39 @@ async function runSelfTest() {
|
|
|
51330
51665
|
__name(runSelfTest, "runSelfTest");
|
|
51331
51666
|
|
|
51332
51667
|
// src/tools/status.ts
|
|
51668
|
+
init_session();
|
|
51669
|
+
function registerDebugTool(server) {
|
|
51670
|
+
server.registerTool("roon_debug", {
|
|
51671
|
+
title: "Graph fields (development)",
|
|
51672
|
+
description: "Development only: without `type`, the graph's type names with counts (`match` narrows); with `type`, the first `limit` objects of that type with their fields.",
|
|
51673
|
+
inputSchema: { type: external_exports.string().optional(), match: external_exports.string().optional(), limit: external_exports.number().int().min(1).max(200).optional() },
|
|
51674
|
+
annotations: { readOnlyHint: true }
|
|
51675
|
+
}, ({ type, match, limit: limit3 }) => run(() => withCore(async (c) => {
|
|
51676
|
+
const preview = /* @__PURE__ */ __name((v) => {
|
|
51677
|
+
if (v && typeof v === "object" && "$ref" in v) {
|
|
51678
|
+
const o = c.graph.getObject(v.$ref);
|
|
51679
|
+
return { $ref: String(v.$ref), type: o?.typeName };
|
|
51680
|
+
}
|
|
51681
|
+
if (Array.isArray(v)) return v.slice(0, 3).map(preview);
|
|
51682
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).slice(0, 6).map(([k, x]) => [k, preview(x)]));
|
|
51683
|
+
return typeof v === "string" ? v.slice(0, 80) : typeof v === "bigint" ? String(v) : v;
|
|
51684
|
+
}, "preview");
|
|
51685
|
+
if (!type) {
|
|
51686
|
+
const counts = /* @__PURE__ */ new Map();
|
|
51687
|
+
for (const o of c.graph.objects.values()) counts.set(o.typeName, (counts.get(o.typeName) ?? 0) + 1);
|
|
51688
|
+
return { types: [...counts].filter(([n]) => !match || n.toLowerCase().includes(match.toLowerCase())).sort((a, b) => b[1] - a[1]).map(([name, n]) => ({ name, n })) };
|
|
51689
|
+
}
|
|
51690
|
+
const all = c.graph.findByType(type);
|
|
51691
|
+
return { count: all.length, objects: all.slice(0, limit3 ?? 3).map((o) => ({
|
|
51692
|
+
oid: String(o.oid),
|
|
51693
|
+
type: o.typeName,
|
|
51694
|
+
fields: Object.fromEntries(Object.entries(o.fields ?? {}).map(([k, v]) => [k.replace(/^.*::/, ""), preview(v)]))
|
|
51695
|
+
})) };
|
|
51696
|
+
})));
|
|
51697
|
+
}
|
|
51698
|
+
__name(registerDebugTool, "registerDebugTool");
|
|
51333
51699
|
function registerStatusTool(server, internal) {
|
|
51700
|
+
if (process.env.DEEP_ROON_DEBUG === "1") registerDebugTool(server);
|
|
51334
51701
|
server.registerTool("roon_status", {
|
|
51335
51702
|
title: "Deep Roon MCP status",
|
|
51336
51703
|
description: "How Deep Roon MCP stands: its version (and whether a newer one is out), the Roon Core it talks to, whether it is enabled in Roon (Settings \u203A Extensions), whether Roon's internal protocol works, and a self-test of a few reads with the tools each one stands for. Use it when a tool fails, or when the user asks whether everything works.",
|