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