roon-mcp 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/BrowseSessionManager.d.ts +34 -0
- package/dist/BrowseSessionManager.js +97 -0
- package/dist/BrowseSessionManager.js.map +1 -0
- package/dist/BrowseSessionManager.test.d.ts +1 -0
- package/dist/BrowseSessionManager.test.js +83 -0
- package/dist/BrowseSessionManager.test.js.map +1 -0
- package/dist/GenreService.d.ts +22 -0
- package/dist/GenreService.js +177 -0
- package/dist/GenreService.js.map +1 -0
- package/dist/GenreService.test.d.ts +1 -0
- package/dist/GenreService.test.js +147 -0
- package/dist/GenreService.test.js.map +1 -0
- package/dist/PlaybackService.d.ts +43 -0
- package/dist/PlaybackService.js +291 -0
- package/dist/PlaybackService.js.map +1 -0
- package/dist/PlaybackService.test.d.ts +1 -0
- package/dist/PlaybackService.test.js +281 -0
- package/dist/PlaybackService.test.js.map +1 -0
- package/dist/RoonClient.d.ts +37 -0
- package/dist/RoonClient.js +105 -0
- package/dist/RoonClient.js.map +1 -0
- package/dist/RoonMcpServer.d.ts +21 -0
- package/dist/RoonMcpServer.js +218 -0
- package/dist/RoonMcpServer.js.map +1 -0
- package/dist/SearchNavigator.d.ts +38 -0
- package/dist/SearchNavigator.js +104 -0
- package/dist/SearchNavigator.js.map +1 -0
- package/dist/SearchService.d.ts +29 -0
- package/dist/SearchService.js +236 -0
- package/dist/SearchService.js.map +1 -0
- package/dist/SearchService.test.d.ts +1 -0
- package/dist/SearchService.test.js +197 -0
- package/dist/SearchService.test.js.map +1 -0
- package/dist/TrackExpansionService.d.ts +75 -0
- package/dist/TrackExpansionService.js +337 -0
- package/dist/TrackExpansionService.js.map +1 -0
- package/dist/TrackExpansionService.test.d.ts +1 -0
- package/dist/TrackExpansionService.test.js +382 -0
- package/dist/TrackExpansionService.test.js.map +1 -0
- package/dist/ZoneService.d.ts +52 -0
- package/dist/ZoneService.js +139 -0
- package/dist/ZoneService.js.map +1 -0
- package/dist/ZoneService.test.d.ts +1 -0
- package/dist/ZoneService.test.js +111 -0
- package/dist/ZoneService.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/locator.d.ts +58 -0
- package/dist/locator.js +88 -0
- package/dist/locator.js.map +1 -0
- package/dist/locator.test.d.ts +1 -0
- package/dist/locator.test.js +37 -0
- package/dist/locator.test.js.map +1 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +56 -0
- package/dist/logger.js.map +1 -0
- package/dist/logger.test.d.ts +1 -0
- package/dist/logger.test.js +53 -0
- package/dist/logger.test.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { BrowseSessionManager } from "./BrowseSessionManager.js";
|
|
4
|
+
import { GenreService } from "./GenreService.js";
|
|
5
|
+
import { decodeLocator, isGenreLocator } from "./locator.js";
|
|
6
|
+
import { SearchService } from "./SearchService.js";
|
|
7
|
+
import { TrackExpansionService } from "./TrackExpansionService.js";
|
|
8
|
+
/** Minimal stateful model of Roon's search hierarchy (levels + a stack). */
|
|
9
|
+
class FakeBrowse {
|
|
10
|
+
groups;
|
|
11
|
+
opts;
|
|
12
|
+
drills;
|
|
13
|
+
stack = [];
|
|
14
|
+
inputCalls = 0;
|
|
15
|
+
constructor(groups, opts = {},
|
|
16
|
+
// item_key → child rows, for drilling below the top groups (e.g. an album's
|
|
17
|
+
// track list). Lets the streaming-genre path expand albums into tracks.
|
|
18
|
+
drills = {}) {
|
|
19
|
+
this.groups = groups;
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
this.drills = drills;
|
|
22
|
+
}
|
|
23
|
+
browse(o, cb) {
|
|
24
|
+
if (o.pop_levels) {
|
|
25
|
+
for (let i = 0; i < o.pop_levels; i++)
|
|
26
|
+
this.stack.pop();
|
|
27
|
+
return cb(false, { action: "list" });
|
|
28
|
+
}
|
|
29
|
+
// Real Roon takes `input` together with `pop_all` in one call; the input
|
|
30
|
+
// branch resets the stack itself, so check it before a bare `pop_all`.
|
|
31
|
+
if (o.input !== undefined) {
|
|
32
|
+
this.inputCalls++;
|
|
33
|
+
if (this.opts.failInputTimes && this.inputCalls <= this.opts.failInputTimes) {
|
|
34
|
+
return cb("InvalidItemKey", undefined);
|
|
35
|
+
}
|
|
36
|
+
if (this.opts.messageOnInput) {
|
|
37
|
+
return cb(false, { action: "message", message: this.opts.messageOnInput });
|
|
38
|
+
}
|
|
39
|
+
this.stack = [
|
|
40
|
+
this.groups.map((g) => ({ title: g.title, item_key: g.key, hint: "list" })),
|
|
41
|
+
];
|
|
42
|
+
return cb(false, { action: "list" });
|
|
43
|
+
}
|
|
44
|
+
if (o.pop_all) {
|
|
45
|
+
this.stack = [];
|
|
46
|
+
return cb(false, { action: "list" });
|
|
47
|
+
}
|
|
48
|
+
if (o.item_key !== undefined) {
|
|
49
|
+
const g = this.groups.find((x) => x.key === o.item_key);
|
|
50
|
+
if (g && !g.failDrill) {
|
|
51
|
+
this.stack.push(g.items);
|
|
52
|
+
return cb(false, { action: "list" });
|
|
53
|
+
}
|
|
54
|
+
const drilled = this.drills[o.item_key];
|
|
55
|
+
if (drilled) {
|
|
56
|
+
this.stack.push(drilled);
|
|
57
|
+
return cb(false, { action: "list" });
|
|
58
|
+
}
|
|
59
|
+
return cb("InvalidItemKey", undefined);
|
|
60
|
+
}
|
|
61
|
+
return cb(false, { action: "none" });
|
|
62
|
+
}
|
|
63
|
+
load(o, cb) {
|
|
64
|
+
const top = this.stack[this.stack.length - 1] ?? [];
|
|
65
|
+
const offset = o.offset ?? 0;
|
|
66
|
+
const items = top.slice(offset, offset + (o.count ?? 100));
|
|
67
|
+
cb(false, { items, offset, list: { title: "", count: top.length, level: this.stack.length - 1 } });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function buildService(groups, opts, drills) {
|
|
71
|
+
const fake = new FakeBrowse(groups, opts, drills);
|
|
72
|
+
const stub = { waitForCore: async () => undefined, getBrowse: () => fake };
|
|
73
|
+
const browse = new BrowseSessionManager(stub);
|
|
74
|
+
return new SearchService(browse, new GenreService(browse), new TrackExpansionService(browse));
|
|
75
|
+
}
|
|
76
|
+
function item(title, key) {
|
|
77
|
+
return { title, item_key: key, hint: "list" };
|
|
78
|
+
}
|
|
79
|
+
/** A track leaf (no "list" hint, so it's a playable row, not a container). */
|
|
80
|
+
function leaf(title, key) {
|
|
81
|
+
return { title, item_key: key };
|
|
82
|
+
}
|
|
83
|
+
const ARTISTS = {
|
|
84
|
+
title: "Artists",
|
|
85
|
+
key: "g:artists",
|
|
86
|
+
items: [item("Tycho", "a:tycho"), item("Tycho Brahe", "a:brahe")],
|
|
87
|
+
};
|
|
88
|
+
const ALBUMS = {
|
|
89
|
+
title: "Albums",
|
|
90
|
+
key: "g:albums",
|
|
91
|
+
items: [item("Dive", "al:dive")],
|
|
92
|
+
};
|
|
93
|
+
const GENRES = {
|
|
94
|
+
title: "Genres",
|
|
95
|
+
key: "g:genres",
|
|
96
|
+
items: [item("Dark Ambient", "ge:dark")],
|
|
97
|
+
};
|
|
98
|
+
test("searchMusic returns ranked candidates keyed by re-navigable locators", async () => {
|
|
99
|
+
const svc = buildService([ARTISTS]);
|
|
100
|
+
const out = await svc.searchMusic({ query: "Tycho" });
|
|
101
|
+
assert.equal(out.broadened, false);
|
|
102
|
+
assert.equal(out.candidates[0]?.title, "Tycho");
|
|
103
|
+
assert.equal(out.candidates[0]?.type, "artist");
|
|
104
|
+
assert.equal(out.candidates[0]?.sourceGroup, "Artists");
|
|
105
|
+
// The itemKey is a locator carrying the query + group/item indices, not the
|
|
106
|
+
// raw (ephemeral) Roon key.
|
|
107
|
+
const decoded = decodeLocator(out.candidates[0]?.itemKey ?? "");
|
|
108
|
+
assert.ok(decoded && !isGenreLocator(decoded));
|
|
109
|
+
assert.equal(decoded.q, "Tycho");
|
|
110
|
+
assert.equal(typeof decoded.g, "number");
|
|
111
|
+
assert.equal(decoded.i, 0);
|
|
112
|
+
// Exact title match ranks above the prefix match.
|
|
113
|
+
assert.ok((out.candidates[0]?.score ?? 0) > (out.candidates[1]?.score ?? 0));
|
|
114
|
+
});
|
|
115
|
+
test("a type filter collects only the matching category", async () => {
|
|
116
|
+
const svc = buildService([ARTISTS, ALBUMS, GENRES]);
|
|
117
|
+
const out = await svc.searchMusic({ query: "Tycho", type: "artist" });
|
|
118
|
+
assert.equal(out.broadened, false);
|
|
119
|
+
assert.ok(out.candidates.length > 0);
|
|
120
|
+
assert.ok(out.candidates.every((c) => c.type === "artist"));
|
|
121
|
+
});
|
|
122
|
+
test("a typed search with no matching items broadens to all categories", async () => {
|
|
123
|
+
// No Playlists group at all → typed selection is empty → broaden.
|
|
124
|
+
const svc = buildService([ARTISTS, ALBUMS, GENRES]);
|
|
125
|
+
const out = await svc.searchMusic({ query: "Dark Ambient", type: "playlist" });
|
|
126
|
+
assert.equal(out.broadened, true);
|
|
127
|
+
assert.match(out.message ?? "", /broadened/i);
|
|
128
|
+
assert.equal(out.candidates[0]?.title, "Dark Ambient");
|
|
129
|
+
});
|
|
130
|
+
test("type:genre is resolved via the genres tree, not broadened to artists", async () => {
|
|
131
|
+
// The flat-search fake has no genres tree, so resolution yields no genres —
|
|
132
|
+
// crucially it must NOT silently broaden back to the artist/album groups.
|
|
133
|
+
const svc = buildService([ARTISTS, ALBUMS, GENRES]);
|
|
134
|
+
const out = await svc.searchMusic({ query: "Dark Ambient", type: "genre" });
|
|
135
|
+
assert.equal(out.broadened, false);
|
|
136
|
+
assert.deepEqual(out.candidates, []);
|
|
137
|
+
assert.match(out.message ?? "", /No results for/i);
|
|
138
|
+
});
|
|
139
|
+
test("includeStreaming samples a track mix across genre-relevant albums", async () => {
|
|
140
|
+
const albums = {
|
|
141
|
+
title: "Albums",
|
|
142
|
+
key: "g:albums",
|
|
143
|
+
items: [leaf("Selected Ambient Works", "al:saw"), leaf("Ambient Avenue", "al:ave")],
|
|
144
|
+
};
|
|
145
|
+
// Each album drills into its own track list.
|
|
146
|
+
const drills = {
|
|
147
|
+
"al:saw": [leaf("Xtal", "t:xtal"), leaf("Ageispolis", "t:age")],
|
|
148
|
+
"al:ave": [leaf("Fire Ant", "t:fire"), leaf("Jealous", "t:jeal")],
|
|
149
|
+
};
|
|
150
|
+
const svc = buildService([albums], undefined, drills);
|
|
151
|
+
const out = await svc.searchMusic({ query: "Ambient", type: "genre", includeStreaming: true });
|
|
152
|
+
// No library genre tree in the fake, so the candidates are purely streaming.
|
|
153
|
+
assert.ok(out.candidates.length > 0);
|
|
154
|
+
assert.ok(out.candidates.every((c) => c.type === "track"));
|
|
155
|
+
assert.ok(out.candidates.every((c) => c.sourceGroup === "Streaming"));
|
|
156
|
+
// The mix spreads across both albums (locator `i` is the album index), not
|
|
157
|
+
// just the first — that's the whole point versus returning one album.
|
|
158
|
+
const albumIndices = new Set(out.candidates.map((c) => {
|
|
159
|
+
const loc = decodeLocator(c.itemKey);
|
|
160
|
+
assert.ok(loc && !isGenreLocator(loc) && typeof loc.t === "number");
|
|
161
|
+
return loc.i;
|
|
162
|
+
}));
|
|
163
|
+
assert.ok(albumIndices.size >= 2);
|
|
164
|
+
});
|
|
165
|
+
test("includeStreaming is ignored for non-genre searches", async () => {
|
|
166
|
+
const svc = buildService([ARTISTS]);
|
|
167
|
+
const out = await svc.searchMusic({ query: "Tycho", type: "artist", includeStreaming: true });
|
|
168
|
+
assert.ok(out.candidates.every((c) => c.type === "artist"));
|
|
169
|
+
});
|
|
170
|
+
test("limit caps the number of returned candidates", async () => {
|
|
171
|
+
const many = {
|
|
172
|
+
title: "Tracks",
|
|
173
|
+
key: "g:tracks",
|
|
174
|
+
items: Array.from({ length: 20 }, (_, i) => item(`Track ${i}`, `t:${i}`)),
|
|
175
|
+
};
|
|
176
|
+
const svc = buildService([many]);
|
|
177
|
+
const out = await svc.searchMusic({ query: "Track", limit: 5 });
|
|
178
|
+
assert.equal(out.candidates.length, 5);
|
|
179
|
+
});
|
|
180
|
+
test("a message action yields an empty result without throwing", async () => {
|
|
181
|
+
const svc = buildService([ARTISTS], { messageOnInput: "No results found." });
|
|
182
|
+
const out = await svc.searchMusic({ query: "zzz" });
|
|
183
|
+
assert.deepEqual(out.candidates, []);
|
|
184
|
+
assert.equal(out.message, "No results found.");
|
|
185
|
+
});
|
|
186
|
+
test("a single failing group is skipped, others still return", async () => {
|
|
187
|
+
const svc = buildService([{ ...ARTISTS, failDrill: true }, GENRES]);
|
|
188
|
+
const out = await svc.searchMusic({ query: "Dark Ambient" });
|
|
189
|
+
assert.ok(out.candidates.some((c) => c.title === "Dark Ambient"));
|
|
190
|
+
assert.ok(out.candidates.every((c) => c.title !== "Tycho"));
|
|
191
|
+
});
|
|
192
|
+
test("an InvalidItemKey on submit triggers one reset-and-retry", async () => {
|
|
193
|
+
const svc = buildService([ARTISTS], { failInputTimes: 1 });
|
|
194
|
+
const out = await svc.searchMusic({ query: "Tycho" });
|
|
195
|
+
assert.equal(out.candidates[0]?.title, "Tycho");
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=SearchService.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchService.test.js","sourceRoot":"","sources":["../src/SearchService.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { BrowseResultBody } from "node-roon-api-browse";
|
|
2
|
+
import { BrowseSessionManager } from "./BrowseSessionManager.js";
|
|
3
|
+
import { type Locator } from "./locator.js";
|
|
4
|
+
import { type GetTracksForInput, type GetTracksForOutput } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Per-album track budget when spreading a total `limit` across `count` albums,
|
|
7
|
+
* so the result draws from several albums rather than draining the first. At
|
|
8
|
+
* least 1 each.
|
|
9
|
+
*/
|
|
10
|
+
export declare function perAlbumBudget(limit: number, count: number): number;
|
|
11
|
+
/** Expands a search candidate (artist/album/genre/playlist/track) into tracks. */
|
|
12
|
+
export declare class TrackExpansionService {
|
|
13
|
+
private readonly browse;
|
|
14
|
+
private readonly navigator;
|
|
15
|
+
constructor(browse: BrowseSessionManager);
|
|
16
|
+
getTracksFor(input: GetTracksForInput): Promise<GetTracksForOutput>;
|
|
17
|
+
/**
|
|
18
|
+
* Navigate to a single track for playback and browse *into* it, returning its
|
|
19
|
+
* action menu. For a track locator (`t` set) this re-derives the same ordered
|
|
20
|
+
* track list `get_tracks_for` produced and opens entry `t`. Compose inside
|
|
21
|
+
* `runExclusive`. Throws INVALID_ITEM_KEY if the track no longer resolves.
|
|
22
|
+
*/
|
|
23
|
+
openTrackForPlayback(loc: Locator): Promise<BrowseResultBody>;
|
|
24
|
+
/**
|
|
25
|
+
* Classify an opened item and locate its tracks. Leaves the session at the
|
|
26
|
+
* track list for the `list` case. Mirrors the shapes Roon returns:
|
|
27
|
+
* A. a single playable track (children are its action menu)
|
|
28
|
+
* B. tracks directly under the item (album/playlist track list)
|
|
29
|
+
* C. tracks one level down a "Top Tracks"-style container (streaming
|
|
30
|
+
* artist page), or — for a purely local library with no track section —
|
|
31
|
+
* the artist's top album drilled into its own track list.
|
|
32
|
+
*
|
|
33
|
+
* For (C) we drill a single container (the labelled track list if present,
|
|
34
|
+
* else the top album), keeping the resolution a single live list that the
|
|
35
|
+
* {q,g,i,t} locator and `openTrackForPlayback` rely on: `t` indexes that one
|
|
36
|
+
* ordered list, and re-resolving re-derives it with live keys. (Genres, which
|
|
37
|
+
* have no track list of their own, are expanded across albums via
|
|
38
|
+
* `collectGenreAlbumTracks` before this is reached.)
|
|
39
|
+
*/
|
|
40
|
+
private resolveTracks;
|
|
41
|
+
/**
|
|
42
|
+
* Gather a genre's tracks by drilling its "Albums" container and visiting each
|
|
43
|
+
* album in turn, spreading the budget across albums (so the result isn't just
|
|
44
|
+
* the first album's tracks). Each track carries its (album, track) coordinates
|
|
45
|
+
* so the `{ ge, a, t }` locator re-navigates to it. The session must be
|
|
46
|
+
* positioned on the genre page; album-list keys survive drilling+popping a
|
|
47
|
+
* sibling (same pattern as SearchService.collectFromGroups), so one pass
|
|
48
|
+
* collects across albums with live keys.
|
|
49
|
+
*/
|
|
50
|
+
private collectGenreAlbumTracks;
|
|
51
|
+
/** Re-navigate a genre track locator to a live key for playback (album→track). */
|
|
52
|
+
private openGenreTrack;
|
|
53
|
+
/**
|
|
54
|
+
* From a genre page (session already positioned there), drill its "Albums"
|
|
55
|
+
* container and return the album containers, leaving the session on the album
|
|
56
|
+
* list. Returns [] when there's no Albums container.
|
|
57
|
+
*/
|
|
58
|
+
private openGenreAlbums;
|
|
59
|
+
/**
|
|
60
|
+
* Pull ordered track items out of a loaded list. When the list is split into
|
|
61
|
+
* header sections, prefer a "Top Tracks"/"Tracks" section. When there are no
|
|
62
|
+
* headers at all, treat the children as tracks (correct for album/playlist
|
|
63
|
+
* track lists). When several sections exist but none is clearly a track
|
|
64
|
+
* section, return nothing rather than guess albums as tracks — the caller
|
|
65
|
+
* then drills via findTrackContainer.
|
|
66
|
+
*/
|
|
67
|
+
private extractTracks;
|
|
68
|
+
/**
|
|
69
|
+
* Find a sub-list to drill into for tracks: a "Top Tracks"-like container if
|
|
70
|
+
* one is present, else the top album (first navigable container). Only
|
|
71
|
+
* containers (`hint: "list"`) qualify — a bare track leaf would yield its
|
|
72
|
+
* action menu, not a track list.
|
|
73
|
+
*/
|
|
74
|
+
private findTrackContainer;
|
|
75
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { encodeGenreLocator, encodeLocator, hierarchyForLocator, isGenreLocator, withTrackIndex, } from "./locator.js";
|
|
2
|
+
import { SearchNavigator, requireLocator } from "./SearchNavigator.js";
|
|
3
|
+
import { RoonMcpError, } from "./types.js";
|
|
4
|
+
const DEFAULT_LIMIT = 10;
|
|
5
|
+
const MAX_LIMIT = 50;
|
|
6
|
+
// Roon track lists can be long; scan generously so a track section isn't cut.
|
|
7
|
+
const SCAN_COUNT = 100;
|
|
8
|
+
// English section/container titles that hold tracks. Localization is an open
|
|
9
|
+
// plan item (mirrors SearchService's GROUP_TITLE_TO_TYPE / PlaybackService's
|
|
10
|
+
// PLAY_LABELS caveat): non-English Cores need locale-aware matching here.
|
|
11
|
+
const TRACK_SECTION_LABELS = ["top tracks", "tracks", "popular", "songs"];
|
|
12
|
+
// A genre page has no track list of its own — just "Artists"/"Albums"
|
|
13
|
+
// containers and sub-genres. To expand a genre into tracks we drill its
|
|
14
|
+
// "Albums" container and then its first album's track list. English-only, same
|
|
15
|
+
// caveat as TRACK_SECTION_LABELS.
|
|
16
|
+
const ALBUMS_CONTAINER_LABEL = "albums";
|
|
17
|
+
// Leading "Play …"/"Shuffle" shortcut rows that play a whole album/artist/
|
|
18
|
+
// playlist rather than an individual track. They share the `action_list` hint
|
|
19
|
+
// with real track rows (both open an action menu), so they can't be told apart
|
|
20
|
+
// by hint and are excluded by title instead. English-only — same localization
|
|
21
|
+
// caveat as TRACK_SECTION_LABELS and PlaybackService's PLAY_LABELS.
|
|
22
|
+
const PLAY_ACTION_LABELS = [
|
|
23
|
+
"play now",
|
|
24
|
+
"play album",
|
|
25
|
+
"play artist",
|
|
26
|
+
"play genre",
|
|
27
|
+
"play playlist",
|
|
28
|
+
"play track",
|
|
29
|
+
"play",
|
|
30
|
+
"shuffle",
|
|
31
|
+
"start radio",
|
|
32
|
+
];
|
|
33
|
+
function normalize(text) {
|
|
34
|
+
return text.trim().toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Per-album track budget when spreading a total `limit` across `count` albums,
|
|
38
|
+
* so the result draws from several albums rather than draining the first. At
|
|
39
|
+
* least 1 each.
|
|
40
|
+
*/
|
|
41
|
+
export function perAlbumBudget(limit, count) {
|
|
42
|
+
return Math.max(1, Math.ceil(limit / Math.min(count, limit)));
|
|
43
|
+
}
|
|
44
|
+
/** A "Play Album"/"Play Artist"/"Shuffle"-style whole-container action, not a track. */
|
|
45
|
+
function isPlayAction(item) {
|
|
46
|
+
return PLAY_ACTION_LABELS.includes(normalize(item.title));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A single playable track. Roon item hints are
|
|
50
|
+
* `null | action | action_list | list | header`. A track row is either a
|
|
51
|
+
* generic leaf (`null`) or — more commonly in real Cores — an `action_list`
|
|
52
|
+
* row that opens its Play/Queue menu when selected. We therefore *include*
|
|
53
|
+
* `action_list` rows but reject:
|
|
54
|
+
* - `"list"` rows, which are sub-lists (albums, "Top Tracks" containers) to
|
|
55
|
+
* drill via `findTrackContainer` rather than tracks themselves;
|
|
56
|
+
* - `"header"`/`"action"` rows (structural / individual menu actions);
|
|
57
|
+
* - leading "Play …"/"Shuffle" shortcut rows, which share the `action_list`
|
|
58
|
+
* hint with tracks but play the whole container.
|
|
59
|
+
*/
|
|
60
|
+
function isTrackLeaf(item) {
|
|
61
|
+
if (!item.item_key)
|
|
62
|
+
return false;
|
|
63
|
+
if (item.hint === "header" || item.hint === "action" || item.hint === "list")
|
|
64
|
+
return false;
|
|
65
|
+
return !isPlayAction(item);
|
|
66
|
+
}
|
|
67
|
+
/** A navigable sub-list to drill into (an album, or a "Top Tracks" container). */
|
|
68
|
+
function isContainer(item) {
|
|
69
|
+
return Boolean(item.item_key) && item.hint === "list";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* True when the opened item is itself a single playable leaf: drilling into a
|
|
73
|
+
* track yields its action menu (Play Now, Queue, …) rather than a child list.
|
|
74
|
+
*/
|
|
75
|
+
function looksLikeActionList(list, items) {
|
|
76
|
+
if (list?.hint === "action_list")
|
|
77
|
+
return true;
|
|
78
|
+
const selectable = items.filter((i) => i.item_key && i.hint !== "header");
|
|
79
|
+
return selectable.length > 0 && selectable.every((i) => i.hint === "action");
|
|
80
|
+
}
|
|
81
|
+
/** Roon track subtitles are typically the artist (sometimes "Artist / Album"). */
|
|
82
|
+
function toTrackMeta(item) {
|
|
83
|
+
const artist = item.subtitle?.split(" / ")[0]?.trim() || undefined;
|
|
84
|
+
return { title: item.title, artist };
|
|
85
|
+
}
|
|
86
|
+
/** Expands a search candidate (artist/album/genre/playlist/track) into tracks. */
|
|
87
|
+
export class TrackExpansionService {
|
|
88
|
+
browse;
|
|
89
|
+
navigator;
|
|
90
|
+
constructor(browse) {
|
|
91
|
+
this.browse = browse;
|
|
92
|
+
this.navigator = new SearchNavigator(browse);
|
|
93
|
+
}
|
|
94
|
+
async getTracksFor(input) {
|
|
95
|
+
const limit = Math.min(Math.max(input.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
96
|
+
const loc = requireLocator(input.itemKey);
|
|
97
|
+
return this.browse.runExclusive(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const hierarchy = hierarchyForLocator(loc);
|
|
100
|
+
const opened = await this.navigator.openItem(loc);
|
|
101
|
+
if (opened.action === "message")
|
|
102
|
+
return notExpandable(input.itemKey, opened.message);
|
|
103
|
+
// A genre has no track list of its own; gather tracks across its albums,
|
|
104
|
+
// each keyed by an (album, track) genre locator.
|
|
105
|
+
if (isGenreLocator(loc)) {
|
|
106
|
+
const collected = await this.collectGenreAlbumTracks(hierarchy, limit);
|
|
107
|
+
if (collected.length === 0) {
|
|
108
|
+
return notExpandable(input.itemKey, "No albums with tracks were found for this genre.");
|
|
109
|
+
}
|
|
110
|
+
const tracks = collected.map(({ item, a, t }) => ({
|
|
111
|
+
itemKey: encodeGenreLocator(loc.ge, { a, t }),
|
|
112
|
+
...toTrackMeta(item),
|
|
113
|
+
available: true,
|
|
114
|
+
}));
|
|
115
|
+
return { sourceItemKey: input.itemKey, tracks, skipped: [] };
|
|
116
|
+
}
|
|
117
|
+
const resolved = await this.resolveTracks(hierarchy, opened);
|
|
118
|
+
if (resolved.kind === "none")
|
|
119
|
+
return notExpandable(input.itemKey, resolved.reason);
|
|
120
|
+
if (resolved.kind === "self") {
|
|
121
|
+
// The candidate is a single track; key it by its own (t-less) locator.
|
|
122
|
+
return {
|
|
123
|
+
sourceItemKey: input.itemKey,
|
|
124
|
+
tracks: [{ itemKey: input.itemKey, title: resolved.title, available: true }],
|
|
125
|
+
skipped: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const tracks = resolved.items.slice(0, limit).map((item, t) => ({
|
|
129
|
+
itemKey: encodeLocator(withTrackIndex(loc, t)),
|
|
130
|
+
...toTrackMeta(item),
|
|
131
|
+
available: true,
|
|
132
|
+
}));
|
|
133
|
+
return { sourceItemKey: input.itemKey, tracks, skipped: [] };
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
// A stale/invalid locator can't be retried; report it as skipped so the
|
|
137
|
+
// agent can re-search instead of throwing.
|
|
138
|
+
if (err instanceof RoonMcpError && err.code === "INVALID_ITEM_KEY") {
|
|
139
|
+
return {
|
|
140
|
+
sourceItemKey: input.itemKey,
|
|
141
|
+
tracks: [],
|
|
142
|
+
skipped: [{ itemKey: input.itemKey, reason: err.message }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Navigate to a single track for playback and browse *into* it, returning its
|
|
151
|
+
* action menu. For a track locator (`t` set) this re-derives the same ordered
|
|
152
|
+
* track list `get_tracks_for` produced and opens entry `t`. Compose inside
|
|
153
|
+
* `runExclusive`. Throws INVALID_ITEM_KEY if the track no longer resolves.
|
|
154
|
+
*/
|
|
155
|
+
async openTrackForPlayback(loc) {
|
|
156
|
+
const hierarchy = hierarchyForLocator(loc);
|
|
157
|
+
if (loc.t === undefined)
|
|
158
|
+
return this.navigator.openItem(loc); // candidate is itself the track
|
|
159
|
+
// Genre tracks live two levels down (album, track); re-navigate there.
|
|
160
|
+
if (isGenreLocator(loc))
|
|
161
|
+
return this.openGenreTrack(loc, hierarchy);
|
|
162
|
+
const opened = await this.navigator.openItem(loc);
|
|
163
|
+
if (opened.action === "message") {
|
|
164
|
+
throw new RoonMcpError("INVALID_ITEM_KEY", opened.message ?? "Track is no longer available.");
|
|
165
|
+
}
|
|
166
|
+
const resolved = await this.resolveTracks(hierarchy, opened);
|
|
167
|
+
if (resolved.kind !== "list" || !resolved.items[loc.t]?.item_key) {
|
|
168
|
+
throw new RoonMcpError("INVALID_ITEM_KEY", "That track is no longer available; re-run get_tracks_for to refresh it.");
|
|
169
|
+
}
|
|
170
|
+
return this.browse.browse({
|
|
171
|
+
hierarchy,
|
|
172
|
+
item_key: resolved.items[loc.t].item_key,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Classify an opened item and locate its tracks. Leaves the session at the
|
|
177
|
+
* track list for the `list` case. Mirrors the shapes Roon returns:
|
|
178
|
+
* A. a single playable track (children are its action menu)
|
|
179
|
+
* B. tracks directly under the item (album/playlist track list)
|
|
180
|
+
* C. tracks one level down a "Top Tracks"-style container (streaming
|
|
181
|
+
* artist page), or — for a purely local library with no track section —
|
|
182
|
+
* the artist's top album drilled into its own track list.
|
|
183
|
+
*
|
|
184
|
+
* For (C) we drill a single container (the labelled track list if present,
|
|
185
|
+
* else the top album), keeping the resolution a single live list that the
|
|
186
|
+
* {q,g,i,t} locator and `openTrackForPlayback` rely on: `t` indexes that one
|
|
187
|
+
* ordered list, and re-resolving re-derives it with live keys. (Genres, which
|
|
188
|
+
* have no track list of their own, are expanded across albums via
|
|
189
|
+
* `collectGenreAlbumTracks` before this is reached.)
|
|
190
|
+
*/
|
|
191
|
+
async resolveTracks(hierarchy, opened) {
|
|
192
|
+
const loaded = await this.browse.load({
|
|
193
|
+
hierarchy,
|
|
194
|
+
offset: 0,
|
|
195
|
+
count: SCAN_COUNT,
|
|
196
|
+
});
|
|
197
|
+
if (looksLikeActionList(loaded.list, loaded.items)) {
|
|
198
|
+
return { kind: "self", title: opened.item?.title ?? loaded.list?.title ?? "Unknown track" };
|
|
199
|
+
}
|
|
200
|
+
const direct = this.extractTracks(loaded.items);
|
|
201
|
+
if (direct.length > 0)
|
|
202
|
+
return { kind: "list", items: direct };
|
|
203
|
+
const container = this.findTrackContainer(loaded.items);
|
|
204
|
+
if (container?.item_key) {
|
|
205
|
+
await this.browse.browse({ hierarchy, item_key: container.item_key });
|
|
206
|
+
const sub = await this.browse.load({
|
|
207
|
+
hierarchy,
|
|
208
|
+
offset: 0,
|
|
209
|
+
count: SCAN_COUNT,
|
|
210
|
+
});
|
|
211
|
+
const tracks = this.extractTracks(sub.items);
|
|
212
|
+
if (tracks.length > 0)
|
|
213
|
+
return { kind: "list", items: tracks };
|
|
214
|
+
}
|
|
215
|
+
return { kind: "none", reason: "No playable tracks were found for this item." };
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Gather a genre's tracks by drilling its "Albums" container and visiting each
|
|
219
|
+
* album in turn, spreading the budget across albums (so the result isn't just
|
|
220
|
+
* the first album's tracks). Each track carries its (album, track) coordinates
|
|
221
|
+
* so the `{ ge, a, t }` locator re-navigates to it. The session must be
|
|
222
|
+
* positioned on the genre page; album-list keys survive drilling+popping a
|
|
223
|
+
* sibling (same pattern as SearchService.collectFromGroups), so one pass
|
|
224
|
+
* collects across albums with live keys.
|
|
225
|
+
*/
|
|
226
|
+
async collectGenreAlbumTracks(hierarchy, limit) {
|
|
227
|
+
const albums = await this.openGenreAlbums(hierarchy);
|
|
228
|
+
if (albums.length === 0)
|
|
229
|
+
return [];
|
|
230
|
+
const out = [];
|
|
231
|
+
const spread = perAlbumBudget(limit, albums.length);
|
|
232
|
+
for (let a = 0; a < albums.length && out.length < limit; a++) {
|
|
233
|
+
const album = albums[a];
|
|
234
|
+
if (!album.item_key)
|
|
235
|
+
continue;
|
|
236
|
+
await this.browse.browse({ hierarchy, item_key: album.item_key });
|
|
237
|
+
const trackList = await this.browse.load({ hierarchy, offset: 0, count: SCAN_COUNT });
|
|
238
|
+
const tracks = this.extractTracks(trackList.items);
|
|
239
|
+
for (let t = 0; t < tracks.length && t < spread && out.length < limit; t++) {
|
|
240
|
+
out.push({ item: tracks[t], a, t });
|
|
241
|
+
}
|
|
242
|
+
// Level-scoped keys: pop back to the album list before the next album.
|
|
243
|
+
await this.browse.browse({ hierarchy, pop_levels: 1 });
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** Re-navigate a genre track locator to a live key for playback (album→track). */
|
|
248
|
+
async openGenreTrack(loc, hierarchy) {
|
|
249
|
+
const opened = await this.navigator.openItem(loc);
|
|
250
|
+
if (opened.action === "message") {
|
|
251
|
+
throw new RoonMcpError("INVALID_ITEM_KEY", opened.message ?? "Track is no longer available.");
|
|
252
|
+
}
|
|
253
|
+
const albums = await this.openGenreAlbums(hierarchy);
|
|
254
|
+
const album = albums[loc.a ?? 0];
|
|
255
|
+
if (!album?.item_key)
|
|
256
|
+
throw staleGenreTrack();
|
|
257
|
+
await this.browse.browse({ hierarchy, item_key: album.item_key });
|
|
258
|
+
const trackList = await this.browse.load({ hierarchy, offset: 0, count: SCAN_COUNT });
|
|
259
|
+
const track = this.extractTracks(trackList.items)[loc.t];
|
|
260
|
+
if (!track?.item_key)
|
|
261
|
+
throw staleGenreTrack();
|
|
262
|
+
return this.browse.browse({ hierarchy, item_key: track.item_key });
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* From a genre page (session already positioned there), drill its "Albums"
|
|
266
|
+
* container and return the album containers, leaving the session on the album
|
|
267
|
+
* list. Returns [] when there's no Albums container.
|
|
268
|
+
*/
|
|
269
|
+
async openGenreAlbums(hierarchy) {
|
|
270
|
+
const page = await this.browse.load({ hierarchy, offset: 0, count: SCAN_COUNT });
|
|
271
|
+
const albumsContainer = page.items.find((i) => isContainer(i) && normalize(i.title) === ALBUMS_CONTAINER_LABEL);
|
|
272
|
+
if (!albumsContainer?.item_key)
|
|
273
|
+
return [];
|
|
274
|
+
await this.browse.browse({ hierarchy, item_key: albumsContainer.item_key });
|
|
275
|
+
const albumList = await this.browse.load({ hierarchy, offset: 0, count: SCAN_COUNT });
|
|
276
|
+
return albumList.items.filter(isContainer);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Pull ordered track items out of a loaded list. When the list is split into
|
|
280
|
+
* header sections, prefer a "Top Tracks"/"Tracks" section. When there are no
|
|
281
|
+
* headers at all, treat the children as tracks (correct for album/playlist
|
|
282
|
+
* track lists). When several sections exist but none is clearly a track
|
|
283
|
+
* section, return nothing rather than guess albums as tracks — the caller
|
|
284
|
+
* then drills via findTrackContainer.
|
|
285
|
+
*/
|
|
286
|
+
extractTracks(items) {
|
|
287
|
+
const sections = splitByHeaders(items);
|
|
288
|
+
const trackSection = sections.find((s) => s.header && TRACK_SECTION_LABELS.some((l) => normalize(s.header).startsWith(l)));
|
|
289
|
+
const chosen = trackSection ?? (sections.length === 1 ? sections[0] : undefined);
|
|
290
|
+
if (!chosen)
|
|
291
|
+
return [];
|
|
292
|
+
return chosen.items.filter(isTrackLeaf);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Find a sub-list to drill into for tracks: a "Top Tracks"-like container if
|
|
296
|
+
* one is present, else the top album (first navigable container). Only
|
|
297
|
+
* containers (`hint: "list"`) qualify — a bare track leaf would yield its
|
|
298
|
+
* action menu, not a track list.
|
|
299
|
+
*/
|
|
300
|
+
findTrackContainer(items) {
|
|
301
|
+
const containers = items.filter(isContainer);
|
|
302
|
+
for (const label of TRACK_SECTION_LABELS) {
|
|
303
|
+
const hit = containers.find((c) => normalize(c.title).startsWith(label));
|
|
304
|
+
if (hit)
|
|
305
|
+
return hit;
|
|
306
|
+
}
|
|
307
|
+
return containers[0] ?? null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function splitByHeaders(items) {
|
|
311
|
+
const sections = [];
|
|
312
|
+
let current = { items: [] };
|
|
313
|
+
for (const item of items) {
|
|
314
|
+
if (item.hint === "header") {
|
|
315
|
+
if (current.header !== undefined || current.items.length > 0)
|
|
316
|
+
sections.push(current);
|
|
317
|
+
current = { header: item.title, items: [] };
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
current.items.push(item);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (current.header !== undefined || current.items.length > 0)
|
|
324
|
+
sections.push(current);
|
|
325
|
+
return sections;
|
|
326
|
+
}
|
|
327
|
+
function staleGenreTrack() {
|
|
328
|
+
return new RoonMcpError("INVALID_ITEM_KEY", "That genre track is no longer available; re-run get_tracks_for to refresh it.");
|
|
329
|
+
}
|
|
330
|
+
function notExpandable(itemKey, reason) {
|
|
331
|
+
return {
|
|
332
|
+
sourceItemKey: itemKey,
|
|
333
|
+
tracks: [],
|
|
334
|
+
skipped: [{ itemKey, reason: `NOT_EXPANDABLE: ${reason ?? "item cannot be expanded into tracks."}` }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=TrackExpansionService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackExpansionService.js","sourceRoot":"","sources":["../src/TrackExpansionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|