deep-roon-mcp 0.1.0 → 0.2.1
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 +35 -14
- package/dist/deep-roon-mcp.js +379 -220
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Deep Roon MCP
|
|
2
2
|
|
|
3
3
|
The parts of Roon the official API doesn't reach, for Claude and any other MCP client: the reviews and biographies Roon
|
|
4
|
-
shows, search across your library and Qobuz at once,
|
|
5
|
-
discographies, and your own play history with real "most played" counts.
|
|
4
|
+
shows, search across your library and Qobuz or TIDAL at once, their editorial shelves and TIDAL's playlists and mixes,
|
|
5
|
+
album pages with credits, artist discographies, and your own play history with real "most played" counts.
|
|
6
6
|
|
|
7
7
|
> **Read this first.** Deep Roon MCP is unofficial: it is not made, endorsed or supported by Roon Labs. It uses Roon's
|
|
8
8
|
> internal protocol — the one Roon's own apps use — through Arthur Soares'
|
|
@@ -22,19 +22,19 @@ server of its own and sends nothing anywhere else. What you ask goes to your MCP
|
|
|
22
22
|
|
|
23
23
|
## Status
|
|
24
24
|
|
|
25
|
-
Version 0.
|
|
25
|
+
Version 0.2: fifty-one tools. Qobuz, TIDAL or both — whatever is signed in on your Core.
|
|
26
26
|
|
|
27
27
|
**Reading** — changes nothing in Roon:
|
|
28
28
|
|
|
29
29
|
| Tool | What it does |
|
|
30
30
|
|---|---|
|
|
31
|
-
| `roon_search` | Your library and
|
|
32
|
-
| `roon_new` | Qobuz's shelves
|
|
33
|
-
| `roon_genres` | Qobuz's genres and their ids |
|
|
31
|
+
| `roon_search` | Your library, Qobuz and TIDAL at once: albums (with format), artists, tracks — each says where it's from |
|
|
32
|
+
| `roon_new` | Qobuz's shelves (new, qobuzissimes, press awards, top, trending, ideal discography) or TIDAL's (new, recommended, top, rising, masters; albums or tracks) — by genre |
|
|
33
|
+
| `roon_genres` | Qobuz's genres and their ids, or TIDAL's genres and moods |
|
|
34
34
|
| `roon_album` | An album's page: tracks, release dates, label, barcode, credits |
|
|
35
35
|
| `roon_review` | The review Roon shows, in full, with its author |
|
|
36
36
|
| `roon_bio` | The biography Roon shows, in full, with related artists |
|
|
37
|
-
| `roon_artist` | An artist's discography, oldest first |
|
|
37
|
+
| `roon_artist` | An artist's discography at Qobuz or TIDAL, oldest first |
|
|
38
38
|
| `roon_status` | How Deep Roon MCP stands: Core, pairing, internal protocol, self-test, newer version |
|
|
39
39
|
| `roon_zones` | Roon's zones and groups |
|
|
40
40
|
| `roon_now_playing` | What plays in each zone, with the volume |
|
|
@@ -50,9 +50,10 @@ Version 0.1: fifty tools.
|
|
|
50
50
|
| `roon_new_for_you` | New releases Roon picked for you |
|
|
51
51
|
| `roon_mixes` | Today's Daily Mixes |
|
|
52
52
|
| `roon_playlists`, `roon_playlist` | Your playlists, and one playlist's tracks |
|
|
53
|
-
| `
|
|
53
|
+
| `roon_tidal_playlists` | TIDAL's playlists: its editorial shelves, by genre or mood, and your own TIDAL mixes |
|
|
54
|
+
| `roon_favorites` | Albums with a heart in Roon, or your Qobuz or TIDAL favourites |
|
|
54
55
|
| `roon_listen_later` | Your Listen Later list |
|
|
55
|
-
| `roon_versions` | Other versions of an album: remasters, hi-res, in your library and
|
|
56
|
+
| `roon_versions` | Other versions of an album: remasters, hi-res, in your library and streaming |
|
|
56
57
|
| `roon_similar` | More like this, for an album or an artist |
|
|
57
58
|
| `roon_signal_path` | Roon's signal path for what plays: is it bit-perfect, what happens on the way |
|
|
58
59
|
| `roon_dsp` | MUSE: the DSP set up for a zone |
|
|
@@ -72,7 +73,7 @@ Version 0.1: fifty tools.
|
|
|
72
73
|
| `roon_group`, `roon_ungroup` | Group zones, and undo it |
|
|
73
74
|
| `roon_sleep` | A sleep timer |
|
|
74
75
|
| `roon_standby` | Put a device in standby, or wake it |
|
|
75
|
-
| `roon_play_playlist` | A playlist on a zone, optionally shuffled |
|
|
76
|
+
| `roon_play_playlist` | A playlist on a zone (also a TIDAL one), optionally shuffled |
|
|
76
77
|
| `roon_play_mix` | One of today's Daily Mixes |
|
|
77
78
|
|
|
78
79
|
**Changing your library or sound** — only on your request; each one reads the result back and says if Roon didn't follow:
|
|
@@ -81,7 +82,7 @@ Version 0.1: fifty tools.
|
|
|
81
82
|
|---|---|
|
|
82
83
|
| `roon_favorite` | A heart on an album or track, or off |
|
|
83
84
|
| `roon_listen_later_set` | On or off the Listen Later list |
|
|
84
|
-
| `roon_library_add` | A Qobuz album into your library |
|
|
85
|
+
| `roon_library_add` | A Qobuz or TIDAL album into your library |
|
|
85
86
|
| `roon_playlist_create` | A new playlist from tracks |
|
|
86
87
|
| `roon_dsp_set` | MUSE on or off, one part on or off, or a preset |
|
|
87
88
|
|
|
@@ -103,7 +104,7 @@ The settings (Claude Desktop › Settings › Extensions › Deep Roon MCP):
|
|
|
103
104
|
| Setting | What it does |
|
|
104
105
|
|---|---|
|
|
105
106
|
| Roon Core address | Empty: found on your network. Fill in the Core's address for another subnet or a private VPN (Tailscale) |
|
|
106
|
-
| Essentials only | Seventeen tools instead of fifty, for small models |
|
|
107
|
+
| Essentials only | Seventeen tools instead of fifty-one, for small models |
|
|
107
108
|
| Use Roon's internal protocol | Off: only the official API — zones, playback, volume, grouping; no search, reviews, Focus or history |
|
|
108
109
|
|
|
109
110
|
### Other clients
|
|
@@ -116,6 +117,25 @@ Needs [Node.js](https://nodejs.org) 18 or newer. The package runs straight from
|
|
|
116
117
|
claude mcp add deep-roon -- npx -y deep-roon-mcp
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
**ChatGPT desktop app and Codex** — they share one configuration. In the ChatGPT desktop app: Settings › MCP servers ›
|
|
121
|
+
Add server, name `deep-roon`, type STDIO, command `npx`, arguments `-y deep-roon-mcp`; save, then Restart. Or from a
|
|
122
|
+
terminal:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
codex mcp add deep-roon -- npx -y deep-roon-mcp
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
or by hand in `~/.codex/config.toml`:
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
[mcp_servers.deep-roon]
|
|
132
|
+
command = "npx"
|
|
133
|
+
args = ["-y", "deep-roon-mcp"]
|
|
134
|
+
# env = { ROON_HOST = "", DEEP_ROON_TOOLS = "", DEEP_ROON_INTERNAL = "" }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
ChatGPT on the web can't run a server on your computer — use the desktop app.
|
|
138
|
+
|
|
119
139
|
**Cursor, VS Code, Windsurf, Cline, LM Studio, Claude Desktop by hand …** — in the client's MCP configuration:
|
|
120
140
|
|
|
121
141
|
```json
|
|
@@ -146,7 +166,8 @@ cd deep-roon-mcp && npm install && npm run build
|
|
|
146
166
|
## When something's wrong
|
|
147
167
|
|
|
148
168
|
**"No Roon Core found" on a Mac:** macOS asks once whether the app may use the local network — for Claude Desktop that's
|
|
149
|
-
Claude and Claude Helper. Allow
|
|
169
|
+
Claude and Claude Helper, for the ChatGPT desktop app ChatGPT. Allow it (System Settings › Privacy & Security › Local
|
|
170
|
+
Network), then try again.
|
|
150
171
|
|
|
151
172
|
Ask your client to run **`roon_status`**: the Core it found, whether it's enabled in Roon, whether the internal protocol
|
|
152
173
|
works, and a self-test of a few reads with the tools each stands for. After a Roon update a tool may break until Deep Roon
|
|
@@ -171,4 +192,4 @@ Arthur Soares for the protocol work (MIT, in `src/intern`). Roon Labs for allowi
|
|
|
171
192
|
|
|
172
193
|
## License
|
|
173
194
|
|
|
174
|
-
Apache 2.0 — see `LICENSE` and `NOTICE`. Roon is a trademark of Roon Labs LLC; Qobuz of Xandrie SA.
|
|
195
|
+
Apache 2.0 — see `LICENSE` and `NOTICE`. Roon is a trademark of Roon Labs LLC; Qobuz of Xandrie SA; TIDAL of TIDAL Music AS.
|
package/dist/deep-roon-mcp.js
CHANGED
|
@@ -8628,9 +8628,9 @@ function formatType(t) {
|
|
|
8628
8628
|
if (EXPLICIT[t]) return EXPLICIT[t];
|
|
8629
8629
|
return `${API_NS}${t}`;
|
|
8630
8630
|
}
|
|
8631
|
-
function formatMethodSignature(
|
|
8631
|
+
function formatMethodSignature(service2, method, params) {
|
|
8632
8632
|
const args = params.map((p) => formatType(p.type)).join(", ");
|
|
8633
|
-
return `${API_NS}${
|
|
8633
|
+
return `${API_NS}${service2}::${method}(${args})`;
|
|
8634
8634
|
}
|
|
8635
8635
|
var API_NS, KEYWORD_PRIMITIVES, EXPLICIT, GENERIC_NS;
|
|
8636
8636
|
var init_signature = __esm({
|
|
@@ -9250,9 +9250,9 @@ var init_client = __esm({
|
|
|
9250
9250
|
}
|
|
9251
9251
|
// --- generic call ---
|
|
9252
9252
|
/** Call a method by service + name + params + pre-built args; returns the result. */
|
|
9253
|
-
call(
|
|
9254
|
-
const sig = formatMethodSignature(
|
|
9255
|
-
return this.remoting.callMethod(objectId ?? this.serviceOid(
|
|
9253
|
+
call(service2, method, params, args, objectId) {
|
|
9254
|
+
const sig = formatMethodSignature(service2, method, params);
|
|
9255
|
+
return this.remoting.callMethod(objectId ?? this.serviceOid(service2), sig, args);
|
|
9256
9256
|
}
|
|
9257
9257
|
/** Build a sparse value using the shared full schema and stable field indexes. */
|
|
9258
9258
|
structArg(typeName, fields) {
|
|
@@ -9348,7 +9348,7 @@ var init_client = __esm({
|
|
|
9348
9348
|
rootMembers.splice(3, 0, "Playlists", "Genres");
|
|
9349
9349
|
for (const type of ["Playlist", "BrowserGenre", "GenreLite"]) leaves.add(type);
|
|
9350
9350
|
}
|
|
9351
|
-
const
|
|
9351
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
9352
9352
|
const out = [];
|
|
9353
9353
|
let complete = true;
|
|
9354
9354
|
const member2 = /* @__PURE__ */ __name((fields, name) => Object.entries(fields).find(([key]) => key.endsWith(`::${name}`))?.[1], "member");
|
|
@@ -9357,8 +9357,8 @@ var init_client = __esm({
|
|
|
9357
9357
|
let type;
|
|
9358
9358
|
let fields;
|
|
9359
9359
|
if (isRef2(value)) {
|
|
9360
|
-
if (
|
|
9361
|
-
|
|
9360
|
+
if (seen2.has(value.$ref)) return;
|
|
9361
|
+
seen2.add(value.$ref);
|
|
9362
9362
|
const object3 = this.graph.getObject(value.$ref);
|
|
9363
9363
|
if (!object3) {
|
|
9364
9364
|
complete = false;
|
|
@@ -15416,7 +15416,7 @@ var require_lib2 = __commonJS({
|
|
|
15416
15416
|
var version2;
|
|
15417
15417
|
var init_package = __esm({
|
|
15418
15418
|
"package.json"() {
|
|
15419
|
-
version2 = "0.1
|
|
15419
|
+
version2 = "0.2.1";
|
|
15420
15420
|
}
|
|
15421
15421
|
});
|
|
15422
15422
|
|
|
@@ -15731,41 +15731,12 @@ var init_session = __esm({
|
|
|
15731
15731
|
});
|
|
15732
15732
|
|
|
15733
15733
|
// src/roon/graph.ts
|
|
15734
|
-
var graph_exports = {};
|
|
15735
|
-
__export(graph_exports, {
|
|
15736
|
-
albumRow: () => albumRow,
|
|
15737
|
-
clean: () => clean,
|
|
15738
|
-
dateText: () => dateText,
|
|
15739
|
-
dotnetDate: () => dotnetDate,
|
|
15740
|
-
duration: () => duration3,
|
|
15741
|
-
field: () => field2,
|
|
15742
|
-
flag: () => flag,
|
|
15743
|
-
imageKey: () => imageKey,
|
|
15744
|
-
items: () => items,
|
|
15745
|
-
localTime: () => localTime,
|
|
15746
|
-
obj: () => obj,
|
|
15747
|
-
partialList: () => partialList,
|
|
15748
|
-
refList: () => refList,
|
|
15749
|
-
resultRow: () => resultRow,
|
|
15750
|
-
short: () => short,
|
|
15751
|
-
strings: () => strings,
|
|
15752
|
-
typeOf: () => typeOf,
|
|
15753
|
-
untilField: () => untilField,
|
|
15754
|
-
untilLoaded: () => untilLoaded,
|
|
15755
|
-
wait: () => wait,
|
|
15756
|
-
yearOf: () => yearOf
|
|
15757
|
-
});
|
|
15758
15734
|
function field2(o, name) {
|
|
15759
15735
|
if (!o) return void 0;
|
|
15760
15736
|
const f = o.fields || o;
|
|
15761
15737
|
for (const [k, v] of Object.entries(f)) if (k.endsWith("::" + name)) return v;
|
|
15762
15738
|
return void 0;
|
|
15763
15739
|
}
|
|
15764
|
-
function imageKey(c, o, name = "Image") {
|
|
15765
|
-
const url2 = field2(obj(c, field2(o, name)), "Url");
|
|
15766
|
-
const m = /^broker:\/\/\/image\/([^.]+)\./.exec(String(url2 ?? ""));
|
|
15767
|
-
return m ? m[1] : void 0;
|
|
15768
|
-
}
|
|
15769
15740
|
function dateParts(v) {
|
|
15770
15741
|
if (!Buffer.isBuffer(v) || v.length < 2) return void 0;
|
|
15771
15742
|
const n = new BinaryReader(Uint8Array.from(v)).flexInt();
|
|
@@ -15842,12 +15813,20 @@ function localTime(iso) {
|
|
|
15842
15813
|
const t = /* @__PURE__ */ __name((x) => String(x).padStart(2, "0"), "t");
|
|
15843
15814
|
return `${d.getFullYear()}-${t(d.getMonth() + 1)}-${t(d.getDate())} ${t(d.getHours())}:${t(d.getMinutes())}`;
|
|
15844
15815
|
}
|
|
15816
|
+
function sourceOf(...os2) {
|
|
15817
|
+
for (const o of os2) {
|
|
15818
|
+
const v = field2(o, "ContentSource") ?? field2(o, "Source");
|
|
15819
|
+
if (v === 128) return "TIDAL";
|
|
15820
|
+
if (v === 256) return "Qobuz";
|
|
15821
|
+
if (v === 1) return "library";
|
|
15822
|
+
}
|
|
15823
|
+
return void 0;
|
|
15824
|
+
}
|
|
15845
15825
|
function albumRow(c, o) {
|
|
15846
15826
|
const fm = obj(c, field2(o, "AlbumFormat"));
|
|
15847
|
-
const r = { kind: "album", key: String(field2(o, "AlbumId") ?? ""), title: field2(o, "Title") ?? "", artist: clean(field2(o, "PerformedBy")), source: "library" };
|
|
15827
|
+
const r = { kind: "album", key: String(field2(o, "AlbumId") ?? ""), title: field2(o, "Title") ?? "", artist: clean(field2(o, "PerformedBy")), source: sourceOf(o) ?? (field2(fm, "IsStreaming") === true ? "streaming" : "library") };
|
|
15848
15828
|
const f = field2(fm, "FormatString");
|
|
15849
15829
|
if (f) r.format = f;
|
|
15850
|
-
if (field2(fm, "IsStreaming") === true) r.source = "Qobuz";
|
|
15851
15830
|
const y = yearOf(field2(o, "OriginalReleaseDate")) ?? yearOf(field2(o, "ReleaseDate"));
|
|
15852
15831
|
if (y) r.year = y;
|
|
15853
15832
|
const st = field2(o, "Rating");
|
|
@@ -15881,7 +15860,7 @@ function resultRow(c, o) {
|
|
|
15881
15860
|
if (f) r.format = f;
|
|
15882
15861
|
const l = field2(o, "LengthSeconds");
|
|
15883
15862
|
if (typeof l === "number") r.length = duration3(l);
|
|
15884
|
-
r.source =
|
|
15863
|
+
r.source = sourceOf(o, al) ?? "library";
|
|
15885
15864
|
return r;
|
|
15886
15865
|
}
|
|
15887
15866
|
return { kind: t, title: field2(o, "Title") ?? field2(o, "Name") ?? "" };
|
|
@@ -15902,7 +15881,6 @@ var init_graph = __esm({
|
|
|
15902
15881
|
return (o?.fields?.$items ?? o?.$items ?? []).filter((x) => typeof x === "string");
|
|
15903
15882
|
}, "strings");
|
|
15904
15883
|
flag = /* @__PURE__ */ __name((v) => v === true || Buffer.isBuffer(v) && v.length > 1 && v[0] >= 1, "flag");
|
|
15905
|
-
__name(imageKey, "imageKey");
|
|
15906
15884
|
__name(dateParts, "dateParts");
|
|
15907
15885
|
yearOf = /* @__PURE__ */ __name((v) => dateParts(v)?.[0], "yearOf");
|
|
15908
15886
|
__name(dateText, "dateText");
|
|
@@ -15915,12 +15893,44 @@ var init_graph = __esm({
|
|
|
15915
15893
|
__name(short, "short");
|
|
15916
15894
|
__name(duration3, "duration");
|
|
15917
15895
|
__name(localTime, "localTime");
|
|
15896
|
+
__name(sourceOf, "sourceOf");
|
|
15918
15897
|
__name(albumRow, "albumRow");
|
|
15919
15898
|
__name(resultRow, "resultRow");
|
|
15920
15899
|
}
|
|
15921
15900
|
});
|
|
15922
15901
|
|
|
15923
15902
|
// src/roon/library.ts
|
|
15903
|
+
function signedIn(c, s) {
|
|
15904
|
+
const o = c.graph.findByType(SERVICE_TYPE[s])[0];
|
|
15905
|
+
if (!o) return null;
|
|
15906
|
+
const st = field2(o, "LoginStatus");
|
|
15907
|
+
return st === void 0 || st === 4 ? o : null;
|
|
15908
|
+
}
|
|
15909
|
+
function streaming(c, wanted) {
|
|
15910
|
+
if (wanted) {
|
|
15911
|
+
const o = signedIn(c, wanted);
|
|
15912
|
+
if (!o) throw new RoonError(`${SERVICE_NAME[wanted]} isn't signed in on this Roon Core`, "unavailable");
|
|
15913
|
+
return { service: wanted, o };
|
|
15914
|
+
}
|
|
15915
|
+
for (const s of ["qobuz", "tidal"]) {
|
|
15916
|
+
const o = signedIn(c, s);
|
|
15917
|
+
if (o) return { service: s, o };
|
|
15918
|
+
}
|
|
15919
|
+
throw new RoonError("Neither Qobuz nor TIDAL is signed in on this Roon Core", "unavailable");
|
|
15920
|
+
}
|
|
15921
|
+
async function shelf(c, s, o, method, of, offset2, count, before = NONE2, after = NONE2) {
|
|
15922
|
+
const res = await c.call(
|
|
15923
|
+
SERVICE_TYPE[s],
|
|
15924
|
+
method,
|
|
15925
|
+
[...before.types, { type: "int", name: "offset" }, { type: "int", name: "count" }, ...after.types, { type: `ResultCallback<PartialList<${of}>>`, name: "cb_result" }],
|
|
15926
|
+
Buffer.concat([before.args, buildArgs([Arg.int(Math.max(0, offset2)), Arg.int(Math.min(Math.max(1, count), 100))]), after.args]),
|
|
15927
|
+
o.oid
|
|
15928
|
+
);
|
|
15929
|
+
if (!res.success) throw new RoonError(`${SERVICE_NAME[s]}::${method}: ${res.status}`);
|
|
15930
|
+
const list = partialList(c, c.graph.decodeReturnValue(Uint8Array.from(res.payload)));
|
|
15931
|
+
await untilLoaded(c, list.items);
|
|
15932
|
+
return { total: list.total, items: list.items.map((x) => obj(c, x)).filter(Boolean) };
|
|
15933
|
+
}
|
|
15924
15934
|
async function search(query, limit3, kind) {
|
|
15925
15935
|
return withCore(async (c) => {
|
|
15926
15936
|
const n = Math.min(Math.max(1, limit3), 100);
|
|
@@ -15930,39 +15940,107 @@ async function search(query, limit3, kind) {
|
|
|
15930
15940
|
return { query, rows };
|
|
15931
15941
|
});
|
|
15932
15942
|
}
|
|
15933
|
-
async function section(name, limit3, offset2, genres2 = []) {
|
|
15934
|
-
const method = SECTIONS[name];
|
|
15935
|
-
if (!method) throw new RoonError(`Unknown section; choose from ${Object.keys(SECTIONS).join(", ")}`, "invalid");
|
|
15943
|
+
async function section(name, limit3, offset2, genres2 = [], wanted, what = "albums") {
|
|
15936
15944
|
return withCore(async (c) => {
|
|
15937
|
-
const
|
|
15938
|
-
const
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15945
|
+
const { service: service2, o } = streaming(c, wanted);
|
|
15946
|
+
const methods = SECTIONS[service2];
|
|
15947
|
+
if (!methods[name] && !(service2 === "tidal" && genres2.length)) throw new RoonError(`${SERVICE_NAME[service2]} has no shelf "${name}"; choose from ${Object.keys(methods).join(", ")}`, "invalid");
|
|
15948
|
+
if (what === "tracks" && service2 !== "tidal") throw new RoonError("Shelves of tracks are TIDAL's; Qobuz's shelves are albums", "invalid");
|
|
15949
|
+
if (what === "tracks" && name === "masters") throw new RoonError("TIDAL's masters shelf is albums only", "invalid");
|
|
15950
|
+
let res;
|
|
15951
|
+
if (service2 === "qobuz") {
|
|
15952
|
+
if (genres2.some((g) => !/^\d+$/.test(g))) throw new RoonError("Qobuz genres are ids (numbers) \u2014 see roon_genres", "invalid");
|
|
15953
|
+
res = await shelf(
|
|
15954
|
+
c,
|
|
15955
|
+
service2,
|
|
15956
|
+
o,
|
|
15957
|
+
methods[name],
|
|
15958
|
+
"AlbumLite",
|
|
15959
|
+
offset2,
|
|
15960
|
+
limit3,
|
|
15961
|
+
NONE2,
|
|
15962
|
+
{ types: [{ type: "IList<string>", name: "filter_genre_ids" }], args: buildArgs([Arg.collection(genres2.map((g) => buildArgs([Arg.str(g)])))]) }
|
|
15963
|
+
);
|
|
15964
|
+
} else if (genres2.length) {
|
|
15965
|
+
const g = await tidalPick(c, o, "GetGenres", genres2[0]);
|
|
15966
|
+
res = await shelf(
|
|
15967
|
+
c,
|
|
15968
|
+
service2,
|
|
15969
|
+
o,
|
|
15970
|
+
what === "tracks" ? "GetGenreTracks" : "GetGenreAlbums",
|
|
15971
|
+
what === "tracks" ? "TrackLite" : "AlbumLite",
|
|
15972
|
+
offset2,
|
|
15973
|
+
limit3,
|
|
15974
|
+
{ types: [{ type: "TidalGenre", name: "genre" }], args: tidalStruct(c, "TidalGenre", g) }
|
|
15975
|
+
);
|
|
15976
|
+
} else {
|
|
15977
|
+
res = await shelf(c, service2, o, methods[name] + (what === "tracks" ? "Tracks" : "Albums"), what === "tracks" ? "TrackLite" : "AlbumLite", offset2, limit3);
|
|
15978
|
+
}
|
|
15948
15979
|
return {
|
|
15949
|
-
|
|
15980
|
+
service: SERVICE_NAME[service2],
|
|
15981
|
+
section: genres2.length && service2 === "tidal" ? void 0 : name,
|
|
15950
15982
|
genre: genres2.length ? genres2 : void 0,
|
|
15951
|
-
|
|
15983
|
+
what: service2 === "tidal" ? what : void 0,
|
|
15984
|
+
total: res.total,
|
|
15952
15985
|
offset: offset2,
|
|
15953
|
-
rows:
|
|
15986
|
+
rows: res.items.map((x) => resultRow(c, x))
|
|
15954
15987
|
};
|
|
15955
15988
|
});
|
|
15956
15989
|
}
|
|
15957
|
-
async function
|
|
15990
|
+
async function tidalList(c, o, method) {
|
|
15991
|
+
const res = await c.call("Tidal", method, [{ type: `ResultCallback<IList<${method === "GetGenres" ? "TidalGenre" : "TidalMood"}>>`, name: "cb_result" }], Buffer.alloc(0), o.oid);
|
|
15992
|
+
if (!res.success) throw new RoonError(`TIDAL::${method}: ${res.status}`);
|
|
15993
|
+
const rows = [];
|
|
15994
|
+
if (res.payload.length > 2) {
|
|
15995
|
+
const r = new BinaryReader(Uint8Array.from(res.payload));
|
|
15996
|
+
r.integer();
|
|
15997
|
+
const n = r.flexInt();
|
|
15998
|
+
for (let i = 0; i < n && r.remaining > 0; i++) {
|
|
15999
|
+
r.flexInt();
|
|
16000
|
+
r.flexInt();
|
|
16001
|
+
const len = r.flexInt();
|
|
16002
|
+
const m = new BinaryReader(r.bytes(len));
|
|
16003
|
+
const g = {};
|
|
16004
|
+
while (m.remaining > 0) {
|
|
16005
|
+
const idx = m.flexInt();
|
|
16006
|
+
if (idx !== 1 && idx !== 2) break;
|
|
16007
|
+
const t = m.string();
|
|
16008
|
+
if (idx === 1) g.name = t;
|
|
16009
|
+
else g.path = t;
|
|
16010
|
+
}
|
|
16011
|
+
if (g.path) rows.push({ name: g.name ?? g.path, path: g.path });
|
|
16012
|
+
}
|
|
16013
|
+
}
|
|
16014
|
+
return rows;
|
|
16015
|
+
}
|
|
16016
|
+
async function tidalPick(c, o, method, want) {
|
|
16017
|
+
const all = await tidalList(c, o, method);
|
|
16018
|
+
const w = want.trim().toLowerCase();
|
|
16019
|
+
const g = all.find((x) => x.path.toLowerCase() === w) ?? all.find((x) => x.name.toLowerCase() === w);
|
|
16020
|
+
if (!g) throw new RoonError(`TIDAL has no ${method === "GetGenres" ? "genre" : "mood"} "${want}"; choose from ${all.map((x) => x.path).join(", ")}`, "invalid");
|
|
16021
|
+
return g;
|
|
16022
|
+
}
|
|
16023
|
+
function tidalStruct(c, type, g) {
|
|
16024
|
+
const T = `Sooloos.Broker.Api.${type}`;
|
|
16025
|
+
return c.structArg(T, [
|
|
16026
|
+
{ name: `string ${T}::Name`, propType: 20, value: new BinaryWriter().string(g.name).toBuffer() },
|
|
16027
|
+
{ name: `string ${T}::Path`, propType: 20, value: new BinaryWriter().string(g.path).toBuffer() }
|
|
16028
|
+
]);
|
|
16029
|
+
}
|
|
16030
|
+
async function genres(parent, wanted) {
|
|
15958
16031
|
return withCore(async (c) => {
|
|
15959
|
-
const
|
|
16032
|
+
const { service: service2, o } = streaming(c, wanted);
|
|
16033
|
+
if (service2 === "tidal") {
|
|
16034
|
+
if (parent) throw new RoonError("TIDAL's genres have no sub-genres", "invalid");
|
|
16035
|
+
const [g, m] = [await tidalList(c, o, "GetGenres"), await tidalList(c, o, "GetMoods")];
|
|
16036
|
+
return { service: "TIDAL", genres: g.map((x) => ({ id: x.path, name: x.name })), moods: m.map((x) => ({ id: x.path, name: x.name })) };
|
|
16037
|
+
}
|
|
15960
16038
|
const res = await c.call(
|
|
15961
16039
|
"Qobuz",
|
|
15962
16040
|
"GetGenres",
|
|
15963
16041
|
[{ type: "string", name: "parentGenreId" }, { type: "ResultCallback<IList<QobuzGenre>>", name: "cb_result" }],
|
|
15964
16042
|
buildArgs([Arg.str(parent || null)]),
|
|
15965
|
-
|
|
16043
|
+
o.oid
|
|
15966
16044
|
);
|
|
15967
16045
|
if (!res.success) throw new RoonError(`Qobuz::GetGenres: ${res.status}`);
|
|
15968
16046
|
const rows = [];
|
|
@@ -15986,7 +16064,7 @@ async function genres(parent) {
|
|
|
15986
16064
|
if (g.id) rows.push({ id: g.id, name: g.name });
|
|
15987
16065
|
}
|
|
15988
16066
|
}
|
|
15989
|
-
return { parent: parent || void 0, genres: rows };
|
|
16067
|
+
return { service: "Qobuz", parent: parent || void 0, genres: rows };
|
|
15990
16068
|
});
|
|
15991
16069
|
}
|
|
15992
16070
|
async function findAlbum(c, key) {
|
|
@@ -16177,16 +16255,16 @@ async function bio(key, max, wiki) {
|
|
|
16177
16255
|
};
|
|
16178
16256
|
});
|
|
16179
16257
|
}
|
|
16180
|
-
async function discography(key) {
|
|
16258
|
+
async function discography(key, wanted) {
|
|
16181
16259
|
return withCore(async (c) => {
|
|
16182
|
-
const q =
|
|
16260
|
+
const { service: service2, o: q } = streaming(c, wanted);
|
|
16183
16261
|
const pv = await performer(c, key);
|
|
16184
16262
|
const end = Date.now() + 1500;
|
|
16185
16263
|
while (Date.now() < end && field2(c.graph.getObject(pv.$ref), "Name") === void 0) await wait(40);
|
|
16186
16264
|
const p = c.graph.getObject(pv.$ref);
|
|
16187
16265
|
if (!p || field2(p, "Name") === void 0) throw new RoonError(`Roon knows no artist with key ${key}`, "not_found");
|
|
16188
|
-
const res = await c.call(
|
|
16189
|
-
if (!res.success) throw new RoonError(
|
|
16266
|
+
const res = await c.call(SERVICE_TYPE[service2], "GetPerformerMainAlbumLites", [{ type: "long", name: "performerid" }, { type: "ResultCallback<IList<AlbumLite>>", name: "cb_result" }], buildArgs([Arg.long(BigInt(key))]), q.oid);
|
|
16267
|
+
if (!res.success) throw new RoonError(`${SERVICE_NAME[service2]}::GetPerformerMainAlbumLites: ${res.status}`);
|
|
16190
16268
|
const refs = refList(res.payload);
|
|
16191
16269
|
await untilLoaded(c, refs.map((oid) => ({ $ref: oid })), 3e3);
|
|
16192
16270
|
const albums = refs.map((oid) => c.graph.getObject(oid)).filter((o) => o && field2(o, "Title") !== void 0).map((o) => albumRow(c, o)).map((r, i) => ({ r, i })).sort((a, b) => (a.r.year ?? 9999) - (b.r.year ?? 9999) || a.i - b.i).map((x) => x.r);
|
|
@@ -16195,10 +16273,10 @@ async function discography(key) {
|
|
|
16195
16273
|
if (typeof n === "number") artist.albums_in_library = n;
|
|
16196
16274
|
const from = field2(p, "BirthPlace");
|
|
16197
16275
|
if (from) artist.from = from;
|
|
16198
|
-
return { artist, albums };
|
|
16276
|
+
return { service: SERVICE_NAME[service2], artist, albums };
|
|
16199
16277
|
});
|
|
16200
16278
|
}
|
|
16201
|
-
var
|
|
16279
|
+
var SERVICE_NAME, SERVICE_TYPE, NONE2, SECTIONS, library, CREDIT_KIND, cut, choose;
|
|
16202
16280
|
var init_library = __esm({
|
|
16203
16281
|
"src/roon/library.ts"() {
|
|
16204
16282
|
"use strict";
|
|
@@ -16208,21 +16286,28 @@ var init_library = __esm({
|
|
|
16208
16286
|
init_writer();
|
|
16209
16287
|
init_session();
|
|
16210
16288
|
init_graph();
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16289
|
+
SERVICE_NAME = { qobuz: "Qobuz", tidal: "TIDAL" };
|
|
16290
|
+
SERVICE_TYPE = { qobuz: "Qobuz", tidal: "Tidal" };
|
|
16291
|
+
__name(signedIn, "signedIn");
|
|
16292
|
+
__name(streaming, "streaming");
|
|
16293
|
+
NONE2 = { types: [], args: Buffer.alloc(0) };
|
|
16294
|
+
__name(shelf, "shelf");
|
|
16216
16295
|
__name(search, "search");
|
|
16217
16296
|
SECTIONS = {
|
|
16218
|
-
|
|
16219
|
-
|
|
16220
|
-
|
|
16221
|
-
|
|
16222
|
-
|
|
16223
|
-
|
|
16297
|
+
qobuz: {
|
|
16298
|
+
new: "GetNewAlbums",
|
|
16299
|
+
qobuzissimes: "GetQobuzissimesAlbums",
|
|
16300
|
+
press_awards: "GetPressAwardsAlbums",
|
|
16301
|
+
top: "GetTopAlbums",
|
|
16302
|
+
trending: "GetStillTrendingAlbums",
|
|
16303
|
+
ideal_discography: "GetIdealDiscographyAlbums"
|
|
16304
|
+
},
|
|
16305
|
+
tidal: { new: "GetNew", recommended: "GetRecommended", top: "GetTop", rising: "GetRising", masters: "GetMasters" }
|
|
16224
16306
|
};
|
|
16225
16307
|
__name(section, "section");
|
|
16308
|
+
__name(tidalList, "tidalList");
|
|
16309
|
+
__name(tidalPick, "tidalPick");
|
|
16310
|
+
__name(tidalStruct, "tidalStruct");
|
|
16226
16311
|
__name(genres, "genres");
|
|
16227
16312
|
library = /* @__PURE__ */ __name((c) => c.serviceOid("Library"), "library");
|
|
16228
16313
|
__name(findAlbum, "findAlbum");
|
|
@@ -24017,12 +24102,12 @@ function handleCatchall(proms, input2, payload, ctx, def, inst, abortEarly) {
|
|
|
24017
24102
|
const t = _catchall.def.type;
|
|
24018
24103
|
const optin = _catchall.optin;
|
|
24019
24104
|
const optout = _catchall.optout;
|
|
24020
|
-
let
|
|
24105
|
+
let seen2 = 0;
|
|
24021
24106
|
for (const key in input2) {
|
|
24022
|
-
if (abortEarly && payload.issues.length !==
|
|
24023
|
-
if (aborted(payload,
|
|
24107
|
+
if (abortEarly && payload.issues.length !== seen2) {
|
|
24108
|
+
if (aborted(payload, seen2))
|
|
24024
24109
|
break;
|
|
24025
|
-
|
|
24110
|
+
seen2 = payload.issues.length;
|
|
24026
24111
|
}
|
|
24027
24112
|
if (keySet.has(key))
|
|
24028
24113
|
continue;
|
|
@@ -24112,12 +24197,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
24112
24197
|
const proms = [];
|
|
24113
24198
|
const shape = value.shape;
|
|
24114
24199
|
const abortEarly = ctx?.abortEarly;
|
|
24115
|
-
let
|
|
24200
|
+
let seen2 = payload.issues.length;
|
|
24116
24201
|
for (const key of value.allKeys) {
|
|
24117
|
-
if (abortEarly && payload.issues.length !==
|
|
24118
|
-
if (aborted(payload,
|
|
24202
|
+
if (abortEarly && payload.issues.length !== seen2) {
|
|
24203
|
+
if (aborted(payload, seen2))
|
|
24119
24204
|
break;
|
|
24120
|
-
|
|
24205
|
+
seen2 = payload.issues.length;
|
|
24121
24206
|
}
|
|
24122
24207
|
if (key === "__proto__")
|
|
24123
24208
|
continue;
|
|
@@ -24663,12 +24748,12 @@ var $ZodTuple = /* @__PURE__ */ $constructor("$ZodTuple", (inst, def) => {
|
|
|
24663
24748
|
if (def.rest && !itemAborted) {
|
|
24664
24749
|
let i = items2.length - 1;
|
|
24665
24750
|
const rest = input2.slice(items2.length);
|
|
24666
|
-
let
|
|
24751
|
+
let seen2 = payload.issues.length;
|
|
24667
24752
|
for (const el of rest) {
|
|
24668
|
-
if (abortEarly && payload.issues.length !==
|
|
24669
|
-
if (aborted(payload,
|
|
24753
|
+
if (abortEarly && payload.issues.length !== seen2) {
|
|
24754
|
+
if (aborted(payload, seen2))
|
|
24670
24755
|
break;
|
|
24671
|
-
|
|
24756
|
+
seen2 = payload.issues.length;
|
|
24672
24757
|
}
|
|
24673
24758
|
i++;
|
|
24674
24759
|
const result = def.rest._zod.run({ value: el, issues: [] }, ctx);
|
|
@@ -24901,12 +24986,12 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
|
|
|
24901
24986
|
const proms = [];
|
|
24902
24987
|
payload.value = memo2 ? memo2.alloc(inst, payload, /* @__PURE__ */ new Map(), ctx) : /* @__PURE__ */ new Map();
|
|
24903
24988
|
const abortEarly = ctx?.abortEarly;
|
|
24904
|
-
let
|
|
24989
|
+
let seen2 = payload.issues.length;
|
|
24905
24990
|
for (const [key, value] of input2) {
|
|
24906
|
-
if (abortEarly && payload.issues.length !==
|
|
24907
|
-
if (aborted(payload,
|
|
24991
|
+
if (abortEarly && payload.issues.length !== seen2) {
|
|
24992
|
+
if (aborted(payload, seen2))
|
|
24908
24993
|
break;
|
|
24909
|
-
|
|
24994
|
+
seen2 = payload.issues.length;
|
|
24910
24995
|
}
|
|
24911
24996
|
const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
|
|
24912
24997
|
const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
|
|
@@ -24973,12 +25058,12 @@ var $ZodSet = /* @__PURE__ */ $constructor("$ZodSet", (inst, def) => {
|
|
|
24973
25058
|
const proms = [];
|
|
24974
25059
|
payload.value = memo2 ? memo2.alloc(inst, payload, /* @__PURE__ */ new Set(), ctx) : /* @__PURE__ */ new Set();
|
|
24975
25060
|
const abortEarly = ctx?.abortEarly;
|
|
24976
|
-
let
|
|
25061
|
+
let seen2 = payload.issues.length;
|
|
24977
25062
|
for (const item of input2) {
|
|
24978
|
-
if (abortEarly && payload.issues.length !==
|
|
24979
|
-
if (aborted(payload,
|
|
25063
|
+
if (abortEarly && payload.issues.length !== seen2) {
|
|
25064
|
+
if (aborted(payload, seen2))
|
|
24980
25065
|
break;
|
|
24981
|
-
|
|
25066
|
+
seen2 = payload.issues.length;
|
|
24982
25067
|
}
|
|
24983
25068
|
const result = def.valueType._zod.run({ value: item, issues: [] }, ctx);
|
|
24984
25069
|
if (result instanceof Promise) {
|
|
@@ -36558,14 +36643,14 @@ __name(handleUnrepresentable, "handleUnrepresentable");
|
|
|
36558
36643
|
function processSchema(schema, ctx, _params = { path: [], schemaPath: [] }) {
|
|
36559
36644
|
var _a3;
|
|
36560
36645
|
const def = schema._zod.def;
|
|
36561
|
-
const
|
|
36562
|
-
if (
|
|
36563
|
-
|
|
36646
|
+
const seen2 = ctx.seen.get(schema);
|
|
36647
|
+
if (seen2) {
|
|
36648
|
+
seen2.count++;
|
|
36564
36649
|
const isCycle = _params.schemaPath.includes(schema);
|
|
36565
36650
|
if (isCycle) {
|
|
36566
|
-
|
|
36651
|
+
seen2.cycle = _params.path;
|
|
36567
36652
|
}
|
|
36568
|
-
return
|
|
36653
|
+
return seen2.schema;
|
|
36569
36654
|
}
|
|
36570
36655
|
const result = { schema: {}, count: 1, cycle: void 0, path: _params.path };
|
|
36571
36656
|
ctx.seen.set(schema, result);
|
|
@@ -36657,12 +36742,12 @@ function extractDefs(ctx, schema) {
|
|
|
36657
36742
|
if (entry[1].schema.$ref) {
|
|
36658
36743
|
return;
|
|
36659
36744
|
}
|
|
36660
|
-
const
|
|
36745
|
+
const seen2 = entry[1];
|
|
36661
36746
|
const { ref, defId } = makeURI(entry);
|
|
36662
|
-
|
|
36747
|
+
seen2.def = { ...seen2.schema };
|
|
36663
36748
|
if (defId)
|
|
36664
|
-
|
|
36665
|
-
const schema2 =
|
|
36749
|
+
seen2.defId = defId;
|
|
36750
|
+
const schema2 = seen2.schema;
|
|
36666
36751
|
for (const key in schema2) {
|
|
36667
36752
|
delete schema2[key];
|
|
36668
36753
|
}
|
|
@@ -36670,16 +36755,16 @@ function extractDefs(ctx, schema) {
|
|
|
36670
36755
|
}, "extractToDef");
|
|
36671
36756
|
if (ctx.cycles === "throw") {
|
|
36672
36757
|
for (const entry of ctx.seen.entries()) {
|
|
36673
|
-
const
|
|
36674
|
-
if (
|
|
36675
|
-
throw new Error(`Cycle detected: #/${
|
|
36758
|
+
const seen2 = entry[1];
|
|
36759
|
+
if (seen2.cycle) {
|
|
36760
|
+
throw new Error(`Cycle detected: #/${seen2.cycle?.join("/")}/<root>
|
|
36676
36761
|
|
|
36677
36762
|
Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.`);
|
|
36678
36763
|
}
|
|
36679
36764
|
}
|
|
36680
36765
|
}
|
|
36681
36766
|
for (const entry of ctx.seen.entries()) {
|
|
36682
|
-
const
|
|
36767
|
+
const seen2 = entry[1];
|
|
36683
36768
|
if (schema === entry[0]) {
|
|
36684
36769
|
extractToDef(entry);
|
|
36685
36770
|
continue;
|
|
@@ -36696,11 +36781,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
36696
36781
|
extractToDef(entry);
|
|
36697
36782
|
continue;
|
|
36698
36783
|
}
|
|
36699
|
-
if (
|
|
36784
|
+
if (seen2.cycle) {
|
|
36700
36785
|
extractToDef(entry);
|
|
36701
36786
|
continue;
|
|
36702
36787
|
}
|
|
36703
|
-
if (
|
|
36788
|
+
if (seen2.count > 1) {
|
|
36704
36789
|
if (ctx.reused === "ref") {
|
|
36705
36790
|
extractToDef(entry);
|
|
36706
36791
|
}
|
|
@@ -36765,7 +36850,7 @@ function foldObjects(members2) {
|
|
|
36765
36850
|
const part = other.properties?.[key] ?? undeclaredConstraint(other);
|
|
36766
36851
|
if (part === null || part === void 0)
|
|
36767
36852
|
continue;
|
|
36768
|
-
if (!parts.some((
|
|
36853
|
+
if (!parts.some((seen2) => JSON.stringify(seen2) === JSON.stringify(part)))
|
|
36769
36854
|
parts.push(part);
|
|
36770
36855
|
}
|
|
36771
36856
|
const merged = parts.length === 1 ? parts[0] : foldObjects(parts) ?? { allOf: parts };
|
|
@@ -36783,7 +36868,7 @@ function foldObjects(members2) {
|
|
|
36783
36868
|
const constraints = [];
|
|
36784
36869
|
for (const object3 of objects) {
|
|
36785
36870
|
const constraint = undeclaredConstraint(object3);
|
|
36786
|
-
if (constraint && !constraints.some((
|
|
36871
|
+
if (constraint && !constraints.some((seen2) => JSON.stringify(seen2) === JSON.stringify(constraint)))
|
|
36787
36872
|
constraints.push(constraint);
|
|
36788
36873
|
}
|
|
36789
36874
|
if (constraints.length === 1)
|
|
@@ -36827,13 +36912,13 @@ function finalize(ctx, schema) {
|
|
|
36827
36912
|
if (!root)
|
|
36828
36913
|
throw new Error("Unprocessed schema. This is a bug in Zod.");
|
|
36829
36914
|
const flattenRef = /* @__PURE__ */ __name((zodSchema) => {
|
|
36830
|
-
const
|
|
36831
|
-
if (
|
|
36915
|
+
const seen2 = ctx.seen.get(zodSchema);
|
|
36916
|
+
if (seen2.ref === null)
|
|
36832
36917
|
return;
|
|
36833
|
-
const schema2 =
|
|
36918
|
+
const schema2 = seen2.def ?? seen2.schema;
|
|
36834
36919
|
const _cached = { ...schema2 };
|
|
36835
|
-
const ref =
|
|
36836
|
-
|
|
36920
|
+
const ref = seen2.ref;
|
|
36921
|
+
seen2.ref = null;
|
|
36837
36922
|
if (ref) {
|
|
36838
36923
|
flattenRef(ref);
|
|
36839
36924
|
const refSeen = ctx.seen.get(ref);
|
|
@@ -36885,7 +36970,7 @@ function finalize(ctx, schema) {
|
|
|
36885
36970
|
ctx.override({
|
|
36886
36971
|
zodSchema,
|
|
36887
36972
|
jsonSchema: schema2,
|
|
36888
|
-
path:
|
|
36973
|
+
path: seen2.path ?? []
|
|
36889
36974
|
});
|
|
36890
36975
|
}, "flattenRef");
|
|
36891
36976
|
if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
|
|
@@ -36901,8 +36986,8 @@ function finalize(ctx, schema) {
|
|
|
36901
36986
|
rewrite();
|
|
36902
36987
|
if (ctx.intersections.length) {
|
|
36903
36988
|
const carriers = /* @__PURE__ */ new Map();
|
|
36904
|
-
for (const
|
|
36905
|
-
for (const json2 of [
|
|
36989
|
+
for (const seen2 of ctx.seen.values()) {
|
|
36990
|
+
for (const json2 of [seen2.schema, seen2.def]) {
|
|
36906
36991
|
const allOf = json2?.allOf;
|
|
36907
36992
|
if (!Array.isArray(allOf))
|
|
36908
36993
|
continue;
|
|
@@ -36942,11 +37027,11 @@ function finalize(ctx, schema) {
|
|
|
36942
37027
|
const defs = ctx.external?.defs ?? {};
|
|
36943
37028
|
if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
|
|
36944
37029
|
for (const entry of ctx.seen.entries()) {
|
|
36945
|
-
const
|
|
36946
|
-
if (
|
|
36947
|
-
if (
|
|
36948
|
-
delete
|
|
36949
|
-
assignProp(defs,
|
|
37030
|
+
const seen2 = entry[1];
|
|
37031
|
+
if (seen2.def && seen2.defId) {
|
|
37032
|
+
if (seen2.def.id === seen2.defId)
|
|
37033
|
+
delete seen2.def.id;
|
|
37034
|
+
assignProp(defs, seen2.defId, seen2.def);
|
|
36950
37035
|
}
|
|
36951
37036
|
}
|
|
36952
37037
|
}
|
|
@@ -37565,8 +37650,8 @@ function rewriteKeyNames(ctx) {
|
|
|
37565
37650
|
}
|
|
37566
37651
|
const rewrites = /* @__PURE__ */ new Map();
|
|
37567
37652
|
for (const record2 of pendingRecords.get(ctx) ?? []) {
|
|
37568
|
-
const
|
|
37569
|
-
const names2 = (
|
|
37653
|
+
const seen2 = ctx.seen.get(record2);
|
|
37654
|
+
const names2 = (seen2?.def ?? seen2?.schema)?.propertyNames;
|
|
37570
37655
|
if (!names2 || names2 === true || rewrites.has(names2))
|
|
37571
37656
|
continue;
|
|
37572
37657
|
const rewritten = stringifyKeyNames(bySchema, names2, /* @__PURE__ */ new Set());
|
|
@@ -37630,9 +37715,9 @@ var recordProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
|
37630
37715
|
var nullableProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
37631
37716
|
const def = schema._zod.def;
|
|
37632
37717
|
const inner = processSchema(def.innerType, ctx, params);
|
|
37633
|
-
const
|
|
37718
|
+
const seen2 = ctx.seen.get(schema);
|
|
37634
37719
|
if (ctx.target === "openapi-3.0") {
|
|
37635
|
-
|
|
37720
|
+
seen2.ref = def.innerType;
|
|
37636
37721
|
json2.nullable = true;
|
|
37637
37722
|
} else {
|
|
37638
37723
|
json2.anyOf = [inner, { type: "null" }];
|
|
@@ -37641,8 +37726,8 @@ var nullableProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
|
37641
37726
|
var nonoptionalProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
37642
37727
|
const def = schema._zod.def;
|
|
37643
37728
|
processSchema(def.innerType, ctx, params);
|
|
37644
|
-
const
|
|
37645
|
-
|
|
37729
|
+
const seen2 = ctx.seen.get(schema);
|
|
37730
|
+
seen2.ref = def.innerType;
|
|
37646
37731
|
}, "nonoptionalProcessor");
|
|
37647
37732
|
var UNREPRESENTABLE_DEFAULT = /* @__PURE__ */ Symbol();
|
|
37648
37733
|
function serializeDefaultValue(value, schema, ctx, json2, params) {
|
|
@@ -37662,8 +37747,8 @@ __name(serializeDefaultValue, "serializeDefaultValue");
|
|
|
37662
37747
|
var defaultProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
37663
37748
|
const def = schema._zod.def;
|
|
37664
37749
|
processSchema(def.innerType, ctx, params);
|
|
37665
|
-
const
|
|
37666
|
-
|
|
37750
|
+
const seen2 = ctx.seen.get(schema);
|
|
37751
|
+
seen2.ref = def.innerType;
|
|
37667
37752
|
const value = serializeDefaultValue(def.defaultValue, schema, ctx, json2, params);
|
|
37668
37753
|
if (value !== UNREPRESENTABLE_DEFAULT)
|
|
37669
37754
|
json2.default = value;
|
|
@@ -37671,8 +37756,8 @@ var defaultProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
|
37671
37756
|
var prefaultProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
37672
37757
|
const def = schema._zod.def;
|
|
37673
37758
|
processSchema(def.innerType, ctx, params);
|
|
37674
|
-
const
|
|
37675
|
-
|
|
37759
|
+
const seen2 = ctx.seen.get(schema);
|
|
37760
|
+
seen2.ref = def.innerType;
|
|
37676
37761
|
if (ctx.io !== "input")
|
|
37677
37762
|
return;
|
|
37678
37763
|
const value = serializeDefaultValue(def.defaultValue, schema, ctx, json2, params);
|
|
@@ -37682,8 +37767,8 @@ var prefaultProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
|
37682
37767
|
var catchProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
37683
37768
|
const def = schema._zod.def;
|
|
37684
37769
|
processSchema(def.innerType, ctx, params);
|
|
37685
|
-
const
|
|
37686
|
-
|
|
37770
|
+
const seen2 = ctx.seen.get(schema);
|
|
37771
|
+
seen2.ref = def.innerType;
|
|
37687
37772
|
let catchValue;
|
|
37688
37773
|
try {
|
|
37689
37774
|
catchValue = def.catchValue(void 0);
|
|
@@ -37698,33 +37783,33 @@ var pipeProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
|
37698
37783
|
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
|
|
37699
37784
|
const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
|
|
37700
37785
|
processSchema(innerType, ctx, params);
|
|
37701
|
-
const
|
|
37702
|
-
|
|
37786
|
+
const seen2 = ctx.seen.get(schema);
|
|
37787
|
+
seen2.ref = innerType;
|
|
37703
37788
|
}, "pipeProcessor");
|
|
37704
37789
|
var readonlyProcessor = /* @__PURE__ */ __name((schema, ctx, json2, params) => {
|
|
37705
37790
|
const def = schema._zod.def;
|
|
37706
37791
|
processSchema(def.innerType, ctx, params);
|
|
37707
|
-
const
|
|
37708
|
-
|
|
37792
|
+
const seen2 = ctx.seen.get(schema);
|
|
37793
|
+
seen2.ref = def.innerType;
|
|
37709
37794
|
json2.readOnly = true;
|
|
37710
37795
|
}, "readonlyProcessor");
|
|
37711
37796
|
var promiseProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
37712
37797
|
const def = schema._zod.def;
|
|
37713
37798
|
processSchema(def.innerType, ctx, params);
|
|
37714
|
-
const
|
|
37715
|
-
|
|
37799
|
+
const seen2 = ctx.seen.get(schema);
|
|
37800
|
+
seen2.ref = def.innerType;
|
|
37716
37801
|
}, "promiseProcessor");
|
|
37717
37802
|
var optionalProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
37718
37803
|
const def = schema._zod.def;
|
|
37719
37804
|
processSchema(def.innerType, ctx, params);
|
|
37720
|
-
const
|
|
37721
|
-
|
|
37805
|
+
const seen2 = ctx.seen.get(schema);
|
|
37806
|
+
seen2.ref = def.innerType;
|
|
37722
37807
|
}, "optionalProcessor");
|
|
37723
37808
|
var lazyProcessor = /* @__PURE__ */ __name((schema, ctx, _json, params) => {
|
|
37724
37809
|
const innerType = schema._zod.innerType;
|
|
37725
37810
|
processSchema(innerType, ctx, params);
|
|
37726
|
-
const
|
|
37727
|
-
|
|
37811
|
+
const seen2 = ctx.seen.get(schema);
|
|
37812
|
+
seen2.ref = innerType;
|
|
37728
37813
|
}, "lazyProcessor");
|
|
37729
37814
|
var allProcessors = {
|
|
37730
37815
|
string: stringProcessor,
|
|
@@ -40609,7 +40694,7 @@ function checkObjectGuards(objectSchema, guards) {
|
|
|
40609
40694
|
return guard.pipe(objectSchema);
|
|
40610
40695
|
}
|
|
40611
40696
|
__name(checkObjectGuards, "checkObjectGuards");
|
|
40612
|
-
function canonicalKey(value,
|
|
40697
|
+
function canonicalKey(value, seen2) {
|
|
40613
40698
|
if (value === null)
|
|
40614
40699
|
return "z";
|
|
40615
40700
|
const type = typeof value;
|
|
@@ -40619,14 +40704,14 @@ function canonicalKey(value, seen) {
|
|
|
40619
40704
|
const raw = String(value);
|
|
40620
40705
|
return `${type[0]}${raw.length}:${raw}`;
|
|
40621
40706
|
}
|
|
40622
|
-
if (
|
|
40707
|
+
if (seen2.has(value))
|
|
40623
40708
|
return null;
|
|
40624
|
-
|
|
40709
|
+
seen2.add(value);
|
|
40625
40710
|
try {
|
|
40626
40711
|
if (Array.isArray(value)) {
|
|
40627
40712
|
const parts2 = [];
|
|
40628
40713
|
for (const item of value) {
|
|
40629
|
-
const key = canonicalKey(item,
|
|
40714
|
+
const key = canonicalKey(item, seen2);
|
|
40630
40715
|
if (key === null)
|
|
40631
40716
|
return null;
|
|
40632
40717
|
parts2.push(key);
|
|
@@ -40636,14 +40721,14 @@ function canonicalKey(value, seen) {
|
|
|
40636
40721
|
const keys = Object.keys(value).sort();
|
|
40637
40722
|
const parts = [];
|
|
40638
40723
|
for (const k of keys) {
|
|
40639
|
-
const key = canonicalKey(value[k],
|
|
40724
|
+
const key = canonicalKey(value[k], seen2);
|
|
40640
40725
|
if (key === null)
|
|
40641
40726
|
return null;
|
|
40642
40727
|
parts.push(`${k.length}:${k}=${key}`);
|
|
40643
40728
|
}
|
|
40644
40729
|
return `o${parts.length}:{${parts.join(",")}}`;
|
|
40645
40730
|
} finally {
|
|
40646
|
-
|
|
40731
|
+
seen2.delete(value);
|
|
40647
40732
|
}
|
|
40648
40733
|
}
|
|
40649
40734
|
__name(canonicalKey, "canonicalKey");
|
|
@@ -47297,36 +47382,39 @@ __name(runImage, "runImage");
|
|
|
47297
47382
|
|
|
47298
47383
|
// src/tools/read.ts
|
|
47299
47384
|
var READ = { readOnlyHint: true, openWorldHint: false };
|
|
47385
|
+
var service = external_exports.enum(["qobuz", "tidal"]).optional().describe("qobuz or tidal \u2014 default: Qobuz when it is signed in in Roon, else TIDAL.");
|
|
47300
47386
|
var limit = /* @__PURE__ */ __name((max, dflt) => external_exports.number().int().min(1).max(max).optional().describe(`How many (default ${dflt}, at most ${max}).`), "limit");
|
|
47301
47387
|
function registerReadTools(server, internal = true) {
|
|
47302
47388
|
const R = /* @__PURE__ */ __name((name) => internal || name === "roon_zones" || name === "roon_now_playing", "R");
|
|
47303
47389
|
const reg = /* @__PURE__ */ __name(((name, cfg, cb) => R(name) ? server.registerTool.call(server, name, cfg, cb) : void 0), "reg");
|
|
47304
47390
|
reg("roon_search", {
|
|
47305
47391
|
title: "Search Roon",
|
|
47306
|
-
description: 'Search Roon \u2014 the user\'s Roon library and the
|
|
47392
|
+
description: 'Search Roon \u2014 the user\'s Roon library and the catalogues of the streaming services signed in (Qobuz, TIDAL) at once; each row says its `source` (library, Qobuz, TIDAL). Returns albums (with format, e.g. FLAC 96kHz 24bit), artists and tracks. `kind` narrows to album, track or artist \u2014 use `kind: "track"` with the song title (and artist) to find one specific track; a broad query without `kind` can fill up with artists. Albums and tracks carry a `key` for `roon_album`; artists carry a `key` for `roon_artist` and `roon_bio`. Albums say their `year`, and `in_library` when the user has them in Roon. Keys stay valid between calls.',
|
|
47307
47393
|
inputSchema: { query: external_exports.string().min(1), kind: external_exports.enum(["album", "track", "artist"]).optional(), limit: limit(100, 30) },
|
|
47308
47394
|
annotations: READ
|
|
47309
47395
|
}, ({ query, kind, limit: n }) => run(() => search(query, n ?? 30, kind)));
|
|
47310
47396
|
reg("roon_new", {
|
|
47311
|
-
title: "
|
|
47312
|
-
description: '
|
|
47397
|
+
title: "Streaming shelves in Roon",
|
|
47398
|
+
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.',
|
|
47313
47399
|
inputSchema: {
|
|
47314
|
-
|
|
47315
|
-
|
|
47400
|
+
service,
|
|
47401
|
+
section: external_exports.string().optional().describe("The shelf (default new) \u2014 see the list for each service."),
|
|
47402
|
+
genre: external_exports.string().optional().describe("Qobuz: genre id(s) from roon_genres, comma-separated. TIDAL: one genre from roon_genres."),
|
|
47403
|
+
what: external_exports.enum(["albums", "tracks"]).optional().describe("TIDAL only: albums (default) or tracks."),
|
|
47316
47404
|
limit: limit(100, 30),
|
|
47317
47405
|
offset: external_exports.number().int().min(0).optional()
|
|
47318
47406
|
},
|
|
47319
47407
|
annotations: READ
|
|
47320
|
-
}, ({ section: section2, genre, limit: n, offset: offset2 }) => run(() => section(section2 ?? "new", n ?? 30, offset2 ?? 0, (genre ?? "").split(",").map((x) => x.trim()).filter(Boolean))));
|
|
47408
|
+
}, ({ 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")));
|
|
47321
47409
|
reg("roon_genres", {
|
|
47322
|
-
title: "
|
|
47323
|
-
description: "The genres of Qobuz
|
|
47324
|
-
inputSchema: { parent: external_exports.string().optional().describe("
|
|
47410
|
+
title: "Streaming genres",
|
|
47411
|
+
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.",
|
|
47412
|
+
inputSchema: { service, parent: external_exports.string().optional().describe("Qobuz: a genre id, to list its sub-genres.") },
|
|
47325
47413
|
annotations: READ
|
|
47326
|
-
}, ({ parent }) => run(() => genres(parent)));
|
|
47414
|
+
}, ({ service: sv, parent }) => run(() => genres(parent, sv)));
|
|
47327
47415
|
reg("roon_album", {
|
|
47328
47416
|
title: "Album page",
|
|
47329
|
-
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
|
|
47417
|
+
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.",
|
|
47330
47418
|
inputSchema: { key: external_exports.string().describe("An album key."), credits: external_exports.boolean().optional() },
|
|
47331
47419
|
annotations: READ
|
|
47332
47420
|
}, ({ key, credits }) => run(() => album(key, credits === true)));
|
|
@@ -47344,10 +47432,10 @@ function registerReadTools(server, internal = true) {
|
|
|
47344
47432
|
}, ({ key, wikipedia, max_chars }) => run(() => bio(key, max_chars ?? 1e9, wikipedia === true)));
|
|
47345
47433
|
reg("roon_artist", {
|
|
47346
47434
|
title: "Discography",
|
|
47347
|
-
description: 'An artist\'s discography in Roon: the main albums Qobuz 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.',
|
|
47348
|
-
inputSchema: { key: external_exports.string().describe("An artist key.") },
|
|
47435
|
+
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.',
|
|
47436
|
+
inputSchema: { key: external_exports.string().describe("An artist key."), service },
|
|
47349
47437
|
annotations: READ
|
|
47350
|
-
}, ({ key }) => run(() => discography(key)));
|
|
47438
|
+
}, ({ key, service: sv }) => run(() => discography(key, sv)));
|
|
47351
47439
|
reg("roon_zones", {
|
|
47352
47440
|
title: "Roon zones",
|
|
47353
47441
|
description: "The zones Roon can play on (Roon's own names), and which are grouped right now (`groups`).",
|
|
@@ -47988,31 +48076,100 @@ init_official();
|
|
|
47988
48076
|
init_log();
|
|
47989
48077
|
init_graph();
|
|
47990
48078
|
init_library();
|
|
48079
|
+
|
|
48080
|
+
// src/roon/tidal.ts
|
|
48081
|
+
init_session();
|
|
48082
|
+
init_graph();
|
|
48083
|
+
init_library();
|
|
48084
|
+
var PLAYLIST_SHELVES = {
|
|
48085
|
+
new: "GetNewPlaylists",
|
|
48086
|
+
recommended: "GetRecommendedPlaylists",
|
|
48087
|
+
exclusive: "GetExclusivePlaylists",
|
|
48088
|
+
masters: "GetMastersPlaylists",
|
|
48089
|
+
popular: "GetPopularPlaylists",
|
|
48090
|
+
essential: "GetEssentialPlaylists",
|
|
48091
|
+
hires_classics: "GetHiResHeadphoneClassicsPlaylists",
|
|
48092
|
+
recently_updated: "GetRecentlyUpdatedPlaylists",
|
|
48093
|
+
forgotten_favorites: "GetForgottenFavoritePlaylists",
|
|
48094
|
+
my_mixes: "GetDailyMixes",
|
|
48095
|
+
history_mixes: "GetHistoryMixes"
|
|
48096
|
+
};
|
|
48097
|
+
var seen = /* @__PURE__ */ new Map();
|
|
48098
|
+
var playlistKey = /* @__PURE__ */ __name((p) => {
|
|
48099
|
+
const id = field2(p, "PlaylistId");
|
|
48100
|
+
return Buffer.isBuffer(id) ? id.toString("hex") : String(id ?? "");
|
|
48101
|
+
}, "playlistKey");
|
|
48102
|
+
async function fetch2(c, w) {
|
|
48103
|
+
const { o } = streaming(c, "tidal");
|
|
48104
|
+
if (w.genre) {
|
|
48105
|
+
const g = await tidalPick(c, o, "GetGenres", w.genre);
|
|
48106
|
+
const r2 = await shelf(c, "tidal", o, "GetGenrePlaylists", "Playlist", w.offset, w.count, { types: [{ type: "TidalGenre", name: "genre" }], args: tidalStruct(c, "TidalGenre", g) });
|
|
48107
|
+
return Object.assign(r2.items, { total: r2.total });
|
|
48108
|
+
}
|
|
48109
|
+
if (w.mood) {
|
|
48110
|
+
const m = await tidalPick(c, o, "GetMoods", w.mood);
|
|
48111
|
+
const r2 = await shelf(c, "tidal", o, "GetMoodPlaylists", "Playlist", w.offset, w.count, { types: [{ type: "TidalMood", name: "mood" }], args: tidalStruct(c, "TidalMood", m) });
|
|
48112
|
+
return Object.assign(r2.items, { total: r2.total });
|
|
48113
|
+
}
|
|
48114
|
+
const method = PLAYLIST_SHELVES[w.shelf];
|
|
48115
|
+
if (!method) throw new RoonError(`TIDAL has no playlist shelf "${w.shelf}"; choose from ${Object.keys(PLAYLIST_SHELVES).join(", ")}`, "invalid");
|
|
48116
|
+
const r = await shelf(c, "tidal", o, method, "Playlist", w.offset, w.count);
|
|
48117
|
+
return Object.assign(r.items, { total: r.total });
|
|
48118
|
+
}
|
|
48119
|
+
__name(fetch2, "fetch");
|
|
48120
|
+
async function playlists(shelfName, genre, mood, limit3, offset2) {
|
|
48121
|
+
return withCore(async (c) => {
|
|
48122
|
+
const w = { shelf: genre ? "genre" : mood ? "mood" : shelfName, genre, mood, offset: Math.max(0, offset2), count: Math.min(Math.max(1, limit3), 100) };
|
|
48123
|
+
const list = await fetch2(c, w);
|
|
48124
|
+
const rows = list.map((p) => {
|
|
48125
|
+
const key = playlistKey(p);
|
|
48126
|
+
seen.set(key, w);
|
|
48127
|
+
const r = { key, name: String(field2(p, "Name") ?? "").trim() };
|
|
48128
|
+
const d = field2(p, "Description");
|
|
48129
|
+
if (d) r.description = clean(d).replace(/\s+/g, " ").trim().slice(0, 240);
|
|
48130
|
+
const n = field2(p, "TrackCount") ?? field2(p, "ItemCount");
|
|
48131
|
+
if (typeof n === "number") r.tracks = n;
|
|
48132
|
+
const l = field2(p, "LengthSeconds");
|
|
48133
|
+
if (typeof l === "number") r.length = duration3(l);
|
|
48134
|
+
const a = strings(c, field2(p, "SampleArtistNames"));
|
|
48135
|
+
if (a.length) r.artists = a.slice(0, 5);
|
|
48136
|
+
return r;
|
|
48137
|
+
});
|
|
48138
|
+
return { service: "TIDAL", shelf: genre || mood ? void 0 : shelfName, genre, mood, total: list.total ?? void 0, offset: w.offset, rows };
|
|
48139
|
+
});
|
|
48140
|
+
}
|
|
48141
|
+
__name(playlists, "playlists");
|
|
48142
|
+
async function findPlaylist(c, key) {
|
|
48143
|
+
const here = [...c.graph.objects.values()].find((o) => /\.Playlist$/.test(o.typeName) && playlistKey(o) === key && field2(o, "Name") !== void 0);
|
|
48144
|
+
if (here) return here;
|
|
48145
|
+
const w = seen.get(key);
|
|
48146
|
+
if (!w) return null;
|
|
48147
|
+
return (await fetch2(c, w)).map((p) => obj(c, p)).find((p) => playlistKey(p) === key) ?? null;
|
|
48148
|
+
}
|
|
48149
|
+
__name(findPlaylist, "findPlaylist");
|
|
48150
|
+
|
|
48151
|
+
// src/roon/more.ts
|
|
47991
48152
|
var profileObject = /* @__PURE__ */ __name((c) => {
|
|
47992
48153
|
const p = [...c.graph.objects.values()].find((o) => o.typeName.endsWith(".Profile"));
|
|
47993
48154
|
if (!p) throw new RoonError("Roon gave no profile");
|
|
47994
48155
|
return p;
|
|
47995
48156
|
}, "profileObject");
|
|
47996
48157
|
var decoded2 = /* @__PURE__ */ __name((c, payload) => payload.length ? c.graph.decodeReturnValue(Uint8Array.from(payload)) : null, "decoded");
|
|
47997
|
-
var PLAYLIST_KIND = { 1: "itunes", 5: "roon", 6: "roon (read-only)", 10: "
|
|
48158
|
+
var PLAYLIST_KIND = { 1: "itunes", 5: "roon", 6: "roon (read-only)", 10: "TIDAL", 11: "file", 12: "qobuz", 13: "roon editorial", 15: "smart" };
|
|
47998
48159
|
function allPlaylists(c) {
|
|
47999
|
-
const
|
|
48000
|
-
if (!
|
|
48001
|
-
return { service, list: items(c, field2(
|
|
48160
|
+
const service2 = c.graph.findByType("Playlists")[0];
|
|
48161
|
+
if (!service2) throw new RoonError("Roon gives no playlists service", "unavailable");
|
|
48162
|
+
return { service: service2, list: items(c, field2(service2, "AllPlaylists")) };
|
|
48002
48163
|
}
|
|
48003
48164
|
__name(allPlaylists, "allPlaylists");
|
|
48004
|
-
|
|
48005
|
-
const id = field2(p, "PlaylistId");
|
|
48006
|
-
return Buffer.isBuffer(id) ? id.toString("hex") : String(id ?? "");
|
|
48007
|
-
}, "playlistKey");
|
|
48008
|
-
function findPlaylist(c, key) {
|
|
48165
|
+
async function findPlaylist2(c, key) {
|
|
48009
48166
|
const { list } = allPlaylists(c);
|
|
48010
|
-
const p = list.find((x) => playlistKey(x) === key) ?? list.find((x) => String(field2(x, "Name") ?? "").toLowerCase() === key.toLowerCase());
|
|
48011
|
-
if (!p) throw new RoonError("Unknown playlist \u2014 read roon_playlists for the keys", "not_found");
|
|
48167
|
+
const p = list.find((x) => playlistKey(x) === key) ?? list.find((x) => String(field2(x, "Name") ?? "").toLowerCase() === key.toLowerCase()) ?? await findPlaylist(c, key);
|
|
48168
|
+
if (!p) throw new RoonError("Unknown playlist \u2014 read roon_playlists (or roon_tidal_playlists) for the keys", "not_found");
|
|
48012
48169
|
return p;
|
|
48013
48170
|
}
|
|
48014
|
-
__name(
|
|
48015
|
-
async function
|
|
48171
|
+
__name(findPlaylist2, "findPlaylist");
|
|
48172
|
+
async function playlists2() {
|
|
48016
48173
|
return withCore(async (c) => ({ playlists: allPlaylists(c).list.map((p) => ({
|
|
48017
48174
|
key: playlistKey(p),
|
|
48018
48175
|
name: field2(p, "Name") ?? "",
|
|
@@ -48021,10 +48178,10 @@ async function playlists() {
|
|
|
48021
48178
|
read_only: field2(p, "IsReadOnly") === true || void 0
|
|
48022
48179
|
})) }));
|
|
48023
48180
|
}
|
|
48024
|
-
__name(
|
|
48181
|
+
__name(playlists2, "playlists");
|
|
48025
48182
|
async function playlist(key, limit3) {
|
|
48026
48183
|
return withCore(async (c) => {
|
|
48027
|
-
const p =
|
|
48184
|
+
const p = await findPlaylist2(c, key);
|
|
48028
48185
|
const res = await c.call("Playlist", "GetItems", [{ type: "ResultCallback<Query<PlaylistItem>>", name: "cb_result" }], Buffer.alloc(0), p.oid);
|
|
48029
48186
|
if (!res.success) throw new RoonError(`Roon refused the playlist (${res.status})`);
|
|
48030
48187
|
const v = decoded2(c, res.payload);
|
|
@@ -48047,7 +48204,7 @@ __name(playlist, "playlist");
|
|
|
48047
48204
|
async function playPlaylist(key, zoneName, how, shuffle) {
|
|
48048
48205
|
const p = PRIORITY[how];
|
|
48049
48206
|
const r = await withCore(async (c) => {
|
|
48050
|
-
const pl =
|
|
48207
|
+
const pl = await findPlaylist2(c, key);
|
|
48051
48208
|
const z2 = findZone(c, zoneName);
|
|
48052
48209
|
const args = Buffer.concat([buildArgs([Arg.ref(z2.oid), Arg.sooid(c.profile())]), playParams(c, p, shuffle), buildArgs([Arg.ref(pl.oid), Arg.ref(0n)])]);
|
|
48053
48210
|
const res = await c.call(
|
|
@@ -48073,7 +48230,7 @@ async function playPlaylist(key, zoneName, how, shuffle) {
|
|
|
48073
48230
|
__name(playPlaylist, "playPlaylist");
|
|
48074
48231
|
async function playlistCreate(name, keys) {
|
|
48075
48232
|
return withCore(async (c) => {
|
|
48076
|
-
const { service } = allPlaylists(c);
|
|
48233
|
+
const { service: service2 } = allPlaylists(c);
|
|
48077
48234
|
await findTracks(c, keys);
|
|
48078
48235
|
const res = await c.call(
|
|
48079
48236
|
"Playlists",
|
|
@@ -48087,7 +48244,7 @@ async function playlistCreate(name, keys) {
|
|
|
48087
48244
|
{ type: "ResultCallback<Playlist>", name: "cb_result" }
|
|
48088
48245
|
],
|
|
48089
48246
|
buildArgs([Arg.ref(profileObject(c).oid), Arg.str(name), Arg.str(""), Arg.bool(false), Arg.collection(keys.map((k) => buildArgs([Arg.long(BigInt(k))])))]),
|
|
48090
|
-
|
|
48247
|
+
service2.oid
|
|
48091
48248
|
);
|
|
48092
48249
|
if (!res.success) throw new RoonError(`Roon refused the playlist (${res.status})`);
|
|
48093
48250
|
const end = Date.now() + 3e3;
|
|
@@ -48106,20 +48263,9 @@ async function favorites(source, limit3) {
|
|
|
48106
48263
|
return { source, total: r.total, rows: r.rows };
|
|
48107
48264
|
}
|
|
48108
48265
|
return withCore(async (c) => {
|
|
48109
|
-
const
|
|
48110
|
-
|
|
48111
|
-
|
|
48112
|
-
"Qobuz",
|
|
48113
|
-
"GetFavoriteAlbums",
|
|
48114
|
-
[{ type: "int", name: "offset" }, { type: "int", name: "count" }, { type: "ResultCallback<PartialList<AlbumLite>>", name: "cb_result" }],
|
|
48115
|
-
buildArgs([Arg.int(0), Arg.int(n)]),
|
|
48116
|
-
q.oid
|
|
48117
|
-
);
|
|
48118
|
-
if (!res.success) throw new RoonError(`Roon refused the Qobuz favourites (${res.status})`);
|
|
48119
|
-
const { partialList: partialList2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
|
|
48120
|
-
const list = partialList2(c, c.graph.decodeReturnValue(Uint8Array.from(res.payload)));
|
|
48121
|
-
await untilLoaded(c, list.items);
|
|
48122
|
-
return { source, total: list.total, rows: list.items.map((x) => obj(c, x)).filter(Boolean).map((o) => albumRow(c, o)) };
|
|
48266
|
+
const { service: service2, o } = streaming(c, source);
|
|
48267
|
+
const list = await shelf(c, service2, o, "GetFavoriteAlbums", "AlbumLite", 0, n);
|
|
48268
|
+
return { source, total: list.total, rows: list.items.map((x) => albumRow(c, x)) };
|
|
48123
48269
|
});
|
|
48124
48270
|
}
|
|
48125
48271
|
__name(favorites, "favorites");
|
|
@@ -48272,8 +48418,8 @@ async function similarAlbum(key, limit3) {
|
|
|
48272
48418
|
const A = isRef2(full) ? c.graph.getObject(full.$ref) : null;
|
|
48273
48419
|
const self = String(field2(al, "AlbumId"));
|
|
48274
48420
|
const unique = /* @__PURE__ */ __name((l) => {
|
|
48275
|
-
const
|
|
48276
|
-
return l.filter((o) => o && field2(o, "Title") !== void 0 && !
|
|
48421
|
+
const seen2 = /* @__PURE__ */ new Set([self]);
|
|
48422
|
+
return l.filter((o) => o && field2(o, "Title") !== void 0 && !seen2.has(String(field2(o, "AlbumId"))) && seen2.add(String(field2(o, "AlbumId")))).slice(0, limit3).map((o) => albumRow(c, o));
|
|
48277
48423
|
}, "unique");
|
|
48278
48424
|
const res = await short(c.call(
|
|
48279
48425
|
"Library",
|
|
@@ -48657,13 +48803,13 @@ function registerLibraryTools(server) {
|
|
|
48657
48803
|
}, ({ limit: n }) => run(() => dailyPicks(n ?? 8)));
|
|
48658
48804
|
server.registerTool("roon_new_for_you", {
|
|
48659
48805
|
title: "New releases for you",
|
|
48660
|
-
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (
|
|
48806
|
+
description: '"New releases for you" from Roon\'s Home: new albums Roon picked for this user, from the artists and music they listen to \u2014 personal, unlike roon_new (the streaming services\' editorial shelf, the same for everyone). Use it for "my new releases", "what\'s new for me". Albums carry year, format, sometimes a `review`, and a `key`.',
|
|
48661
48807
|
inputSchema: { limit: limit2(100, 30), offset },
|
|
48662
48808
|
annotations: READ2
|
|
48663
48809
|
}, ({ limit: n, offset: o }) => run(() => newForYou(n ?? 30, o ?? 0)));
|
|
48664
48810
|
server.registerTool("roon_mixes", {
|
|
48665
48811
|
title: "Daily Mixes",
|
|
48666
|
-
description:
|
|
48812
|
+
description: 'Roon\'s own Daily Mixes, from Roon\'s Home \u2014 NOT TIDAL\'s mixes: for "TIDAL mixes", "My Mix", "My Daily Discovery" or TIDAL\'s daily mixes use roon_tidal_playlists with `shelf: "my_mixes"`. These are today\'s personal Roon mixes, each built around an artist (or genre) the user listens to, with a few `touchstones` and a `key` for roon_play_mix. They change every day.',
|
|
48667
48813
|
inputSchema: {},
|
|
48668
48814
|
annotations: READ2
|
|
48669
48815
|
}, () => run(() => mixes()));
|
|
@@ -48675,19 +48821,31 @@ function registerLibraryTools(server) {
|
|
|
48675
48821
|
}, ({ key, zone: zn, mode: m }) => run(() => playMix(key, zn, m ?? "now")));
|
|
48676
48822
|
server.registerTool("roon_playlists", {
|
|
48677
48823
|
title: "Playlists",
|
|
48678
|
-
description: "The playlists
|
|
48824
|
+
description: "The playlists in the user's Roon: their own Roon playlists, Qobuz and TIDAL playlists they added, and editorial ones \u2014 each with a `key` for roon_playlist and roon_play_playlist, the number of tracks, and whether it is read-only.",
|
|
48679
48825
|
inputSchema: {},
|
|
48680
48826
|
annotations: READ2
|
|
48681
|
-
}, () => run(() =>
|
|
48827
|
+
}, () => run(() => playlists2()));
|
|
48828
|
+
server.registerTool("roon_tidal_playlists", {
|
|
48829
|
+
title: "TIDAL playlists",
|
|
48830
|
+
description: "TIDAL's playlists as Roon shows them, when TIDAL is signed in: the editorial shelves `new`, `recommended`, `exclusive`, `masters`, `popular`, `essential`, `hires_classics`, `recently_updated`, `forgotten_favorites`; the user's own TIDAL mixes `my_mixes` (My Daily Discovery, My Mix 1 \u2026, My New Arrivals \u2014 what the user means by their TIDAL (daily) mixes; not roon_mixes, which are Roon's) and `history_mixes` (My Most Listened, the yearly ones \u2026); or the playlists of one TIDAL `genre` or `mood` (ids from roon_genres with `service: \"tidal\"`). Each playlist has a `key` for roon_playlist (its tracks \u2014 readable without playing) and roon_play_playlist, a description, the number of tracks and a few of its artists.",
|
|
48831
|
+
inputSchema: {
|
|
48832
|
+
shelf: external_exports.enum(Object.keys(PLAYLIST_SHELVES)).optional().describe("Default new."),
|
|
48833
|
+
genre: external_exports.string().optional().describe("A TIDAL genre (roon_genres, service tidal)."),
|
|
48834
|
+
mood: external_exports.string().optional().describe("A TIDAL mood (roon_genres, service tidal)."),
|
|
48835
|
+
limit: limit2(100, 20),
|
|
48836
|
+
offset
|
|
48837
|
+
},
|
|
48838
|
+
annotations: READ2
|
|
48839
|
+
}, ({ shelf: shelf2, genre, mood, limit: n, offset: o }) => run(() => playlists(shelf2 ?? "new", genre, mood, n ?? 20, o ?? 0)));
|
|
48682
48840
|
server.registerTool("roon_playlist", {
|
|
48683
48841
|
title: "A playlist's tracks",
|
|
48684
|
-
description: "The tracks of one Roon playlist (by its `key` from roon_playlists, or its exact name), each with a `key` for roon_play_tracks.",
|
|
48842
|
+
description: "The tracks of one Roon playlist (by its `key` from roon_playlists or roon_tidal_playlists, or its exact name), each with a `key` for roon_play_tracks.",
|
|
48685
48843
|
inputSchema: { key: external_exports.string(), limit: limit2(300, 100) },
|
|
48686
48844
|
annotations: READ2
|
|
48687
48845
|
}, ({ key, limit: n }) => run(() => playlist(key, n ?? 100)));
|
|
48688
48846
|
server.registerTool("roon_play_playlist", {
|
|
48689
48847
|
title: "Play a playlist",
|
|
48690
|
-
description: 'Play a Roon playlist on a Roon zone (`key` from roon_playlists, or its exact name). `mode`: now (default), next or queue. `shuffle: true` starts it shuffled from the very first track (Roon\'s own Shuffle button) \u2014 THE way to "shuffle a playlist": one call, no roon_settings, no skip. Only on an explicit request, never as a probe.',
|
|
48848
|
+
description: 'Play a Roon playlist on a Roon zone (`key` from roon_playlists or roon_tidal_playlists, or its exact name). `mode`: now (default), next or queue. `shuffle: true` starts it shuffled from the very first track (Roon\'s own Shuffle button) \u2014 THE way to "shuffle a playlist": one call, no roon_settings, no skip. Only on an explicit request, never as a probe.',
|
|
48691
48849
|
inputSchema: { key: external_exports.string(), zone: zone2, mode: mode2, shuffle: external_exports.boolean().optional() },
|
|
48692
48850
|
annotations: PLAYS2
|
|
48693
48851
|
}, ({ key, zone: zn, mode: m, shuffle }) => run(() => playPlaylist(key, zn, m ?? "now", shuffle === true)));
|
|
@@ -48699,8 +48857,8 @@ function registerLibraryTools(server) {
|
|
|
48699
48857
|
}, ({ name, keys }) => run(() => playlistCreate(name, keys)));
|
|
48700
48858
|
server.registerTool("roon_favorites", {
|
|
48701
48859
|
title: "Favourite albums",
|
|
48702
|
-
description: 'The user\'s favourite albums: `source: "roon"` (default) are the albums with a heart in Roon, newest first; `source: "qobuz"` the favourites at
|
|
48703
|
-
inputSchema: { source: external_exports.enum(["roon", "qobuz"]).optional(), limit: limit2(100, 50) },
|
|
48860
|
+
description: 'The user\'s favourite albums: `source: "roon"` (default) are the albums with a heart in Roon, newest first; `source: "qobuz"` or `"tidal"` the favourites at that service. Albums carry a `key`.',
|
|
48861
|
+
inputSchema: { source: external_exports.enum(["roon", "qobuz", "tidal"]).optional(), limit: limit2(100, 50) },
|
|
48704
48862
|
annotations: READ2
|
|
48705
48863
|
}, ({ source, limit: n }) => run(() => favorites(source ?? "roon", n ?? 50)));
|
|
48706
48864
|
server.registerTool("roon_favorite", {
|
|
@@ -48723,13 +48881,13 @@ function registerLibraryTools(server) {
|
|
|
48723
48881
|
}, ({ kind, key, on }) => run(() => listenLaterSet(kind, key, on !== false)));
|
|
48724
48882
|
server.registerTool("roon_library_add", {
|
|
48725
48883
|
title: "Add to the library",
|
|
48726
|
-
description: "Add a Qobuz album to the user's Roon library (the album `key` from a search, shelf or discography). Changes the library: only on an explicit request.",
|
|
48884
|
+
description: "Add a Qobuz or TIDAL album to the user's Roon library (the album `key` from a search, shelf or discography). Changes the library: only on an explicit request.",
|
|
48727
48885
|
inputSchema: { key: external_exports.string() },
|
|
48728
48886
|
annotations: CHANGES
|
|
48729
48887
|
}, ({ key }) => run(() => libraryAdd(key)));
|
|
48730
48888
|
server.registerTool("roon_versions", {
|
|
48731
48889
|
title: "Other versions",
|
|
48732
|
-
description: 'The other versions Roon knows of an album (by album `key`): remasters, deluxe editions, mono/stereo, hi-res \u2014 `in_library` and `streaming` (at Qobuz), best format first, each with year, format, number of tracks and a `key`. For "is there a hi-res version", "which editions exist".',
|
|
48890
|
+
description: 'The other versions Roon knows of an album (by album `key`): remasters, deluxe editions, mono/stereo, hi-res \u2014 `in_library` and `streaming` (at Qobuz or TIDAL \u2014 each row says its `source`), best format first, each with year, format, number of tracks and a `key`. For "is there a hi-res version", "which editions exist".',
|
|
48733
48891
|
inputSchema: { key: external_exports.string().describe("An album key.") },
|
|
48734
48892
|
annotations: READ2
|
|
48735
48893
|
}, ({ key }) => run(() => versions(key)));
|
|
@@ -48792,12 +48950,12 @@ async function runSelfTest() {
|
|
|
48792
48950
|
if (!a.tracks?.length) throw new Error("an album without tracks");
|
|
48793
48951
|
return `${a.tracks.length} tracks`;
|
|
48794
48952
|
}));
|
|
48795
|
-
out.push(await timed("
|
|
48953
|
+
out.push(await timed("streaming shelf", "roon_new, roon_genres, roon_artist, roon_tidal_playlists", async () => {
|
|
48796
48954
|
try {
|
|
48797
48955
|
const s = await section("new", 1, 0);
|
|
48798
|
-
return `${s.total ?? "?"} new albums`;
|
|
48956
|
+
return `${s.total ?? "?"} new albums at ${s.service}`;
|
|
48799
48957
|
} catch (e) {
|
|
48800
|
-
if (/
|
|
48958
|
+
if (/Neither Qobuz nor TIDAL/i.test(e.message)) return "no Qobuz or TIDAL signed in on this Core (not an error)";
|
|
48801
48959
|
throw e;
|
|
48802
48960
|
}
|
|
48803
48961
|
}));
|
|
@@ -48836,10 +48994,11 @@ init_session();
|
|
|
48836
48994
|
init_session();
|
|
48837
48995
|
init_log();
|
|
48838
48996
|
init_package();
|
|
48839
|
-
var INSTRUCTIONS = `Deep Roon MCP connects to the user's own Roon Core (music player software, with Qobuz behind it when the user has
|
|
48997
|
+
var INSTRUCTIONS = `Deep Roon MCP connects to the user's own Roon Core (music player software, with Qobuz and/or TIDAL behind it when the user has them).
|
|
48840
48998
|
- Albums, tracks and artists come with a \`key\`; keys stay valid between calls \u2014 no need to search again.
|
|
48841
48999
|
- Reviews and biographies are Roon's own texts: when asked for them, show them in full with the author's name.
|
|
48842
49000
|
- What plays changes all the time: read roon_now_playing afresh before describing it.
|
|
49001
|
+
- "Daily Mixes" are Roon's own (roon_mixes); the user's TIDAL mixes (My Mix 1 \u2026, My Daily Discovery) are roon_tidal_playlists with shelf my_mixes, and their tracks can be read with roon_playlist.
|
|
48843
49002
|
- Unofficial: this uses Roon's internal protocol, which a Roon update can change; if a tool says so, tell the user to check for an update.`;
|
|
48844
49003
|
async function main() {
|
|
48845
49004
|
const server = new McpServer({ name: "deep-roon-mcp", title: "Deep Roon MCP", version: version2, websiteUrl: "https://github.com/renebouwmeester/deep-roon-mcp" }, { instructions: INSTRUCTIONS });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deep-roon-mcp",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "An MCP server for Roon that reaches the parts the official API doesn't: reviews, biographies, Qobuz's shelves, your history, search across your library and
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "An MCP server for Roon that reaches the parts the official API doesn't: reviews, biographies, Qobuz's and TIDAL's shelves, TIDAL's playlists, your history, search across your library and the streaming services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "René Bouwmeester <21277119+renebouwmeester@users.noreply.github.com>",
|
|
7
7
|
"homepage": "https://github.com/renebouwmeester/deep-roon-mcp",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"model-context-protocol",
|
|
16
16
|
"claude",
|
|
17
17
|
"qobuz",
|
|
18
|
+
"tidal",
|
|
18
19
|
"music"
|
|
19
20
|
],
|
|
20
21
|
"type": "commonjs",
|