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.
Files changed (67) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/dist/BrowseSessionManager.d.ts +34 -0
  5. package/dist/BrowseSessionManager.js +97 -0
  6. package/dist/BrowseSessionManager.js.map +1 -0
  7. package/dist/BrowseSessionManager.test.d.ts +1 -0
  8. package/dist/BrowseSessionManager.test.js +83 -0
  9. package/dist/BrowseSessionManager.test.js.map +1 -0
  10. package/dist/GenreService.d.ts +22 -0
  11. package/dist/GenreService.js +177 -0
  12. package/dist/GenreService.js.map +1 -0
  13. package/dist/GenreService.test.d.ts +1 -0
  14. package/dist/GenreService.test.js +147 -0
  15. package/dist/GenreService.test.js.map +1 -0
  16. package/dist/PlaybackService.d.ts +43 -0
  17. package/dist/PlaybackService.js +291 -0
  18. package/dist/PlaybackService.js.map +1 -0
  19. package/dist/PlaybackService.test.d.ts +1 -0
  20. package/dist/PlaybackService.test.js +281 -0
  21. package/dist/PlaybackService.test.js.map +1 -0
  22. package/dist/RoonClient.d.ts +37 -0
  23. package/dist/RoonClient.js +105 -0
  24. package/dist/RoonClient.js.map +1 -0
  25. package/dist/RoonMcpServer.d.ts +21 -0
  26. package/dist/RoonMcpServer.js +218 -0
  27. package/dist/RoonMcpServer.js.map +1 -0
  28. package/dist/SearchNavigator.d.ts +38 -0
  29. package/dist/SearchNavigator.js +104 -0
  30. package/dist/SearchNavigator.js.map +1 -0
  31. package/dist/SearchService.d.ts +29 -0
  32. package/dist/SearchService.js +236 -0
  33. package/dist/SearchService.js.map +1 -0
  34. package/dist/SearchService.test.d.ts +1 -0
  35. package/dist/SearchService.test.js +197 -0
  36. package/dist/SearchService.test.js.map +1 -0
  37. package/dist/TrackExpansionService.d.ts +75 -0
  38. package/dist/TrackExpansionService.js +337 -0
  39. package/dist/TrackExpansionService.js.map +1 -0
  40. package/dist/TrackExpansionService.test.d.ts +1 -0
  41. package/dist/TrackExpansionService.test.js +382 -0
  42. package/dist/TrackExpansionService.test.js.map +1 -0
  43. package/dist/ZoneService.d.ts +52 -0
  44. package/dist/ZoneService.js +139 -0
  45. package/dist/ZoneService.js.map +1 -0
  46. package/dist/ZoneService.test.d.ts +1 -0
  47. package/dist/ZoneService.test.js +111 -0
  48. package/dist/ZoneService.test.js.map +1 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +44 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/locator.d.ts +58 -0
  53. package/dist/locator.js +88 -0
  54. package/dist/locator.js.map +1 -0
  55. package/dist/locator.test.d.ts +1 -0
  56. package/dist/locator.test.js +37 -0
  57. package/dist/locator.test.js.map +1 -0
  58. package/dist/logger.d.ts +23 -0
  59. package/dist/logger.js +56 -0
  60. package/dist/logger.js.map +1 -0
  61. package/dist/logger.test.d.ts +1 -0
  62. package/dist/logger.test.js +53 -0
  63. package/dist/logger.test.js.map +1 -0
  64. package/dist/types.d.ts +93 -0
  65. package/dist/types.js +13 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +60 -0
@@ -0,0 +1,147 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { BrowseSessionManager } from "./BrowseSessionManager.js";
4
+ import { GenreService, MIN_GENRE_SCORE, scoreGenre } from "./GenreService.js";
5
+ import { decodeLocator, isGenreLocator } from "./locator.js";
6
+ function genre(title, key, subtitle, children = []) {
7
+ return { title, key, subtitle, hint: "list", children };
8
+ }
9
+ /**
10
+ * Stateful model of Roon's `genres` hierarchy: a tree of nodes navigated by a
11
+ * level stack, mirroring how GenreService drills (browse item_key → push,
12
+ * pop_levels → pop). Counts root resets so we can assert the index is cached.
13
+ */
14
+ class FakeGenres {
15
+ root;
16
+ stack = [];
17
+ rootResets = 0;
18
+ constructor(root) {
19
+ this.root = root;
20
+ }
21
+ browse(o, cb) {
22
+ if (o.pop_levels) {
23
+ for (let i = 0; i < o.pop_levels; i++)
24
+ this.stack.pop();
25
+ return cb(false, { action: "list" });
26
+ }
27
+ if (o.pop_all) {
28
+ this.rootResets++;
29
+ this.stack = [this.root];
30
+ return cb(false, { action: "list" });
31
+ }
32
+ if (o.item_key !== undefined) {
33
+ const top = this.stack[this.stack.length - 1] ?? [];
34
+ const node = top.find((n) => n.key === o.item_key);
35
+ if (!node)
36
+ return cb("InvalidItemKey", undefined);
37
+ this.stack.push(node.children ?? []);
38
+ return cb(false, { action: "list" });
39
+ }
40
+ return cb(false, { action: "none" });
41
+ }
42
+ load(o, cb) {
43
+ const top = this.stack[this.stack.length - 1] ?? [];
44
+ const items = top.map((n) => ({
45
+ title: n.title,
46
+ item_key: n.key,
47
+ subtitle: n.subtitle,
48
+ hint: n.hint ?? "list",
49
+ }));
50
+ cb(false, { items, offset: o.offset ?? 0, list: { title: "", count: items.length, level: 0 } });
51
+ }
52
+ }
53
+ // Electronic → (Artists container, Trance → {Psytrance, Progressive Trance}, House); Jazz.
54
+ function buildTree() {
55
+ return [
56
+ {
57
+ ...genre("Electronic", "g:elec", "229 Artists, 358 Albums", [
58
+ // A within-genre container: list hint but no "N Artists" subtitle → not a genre.
59
+ { title: "Artists", key: "c:elec-artists", hint: "list" },
60
+ genre("Trance", "g:trance", "23 Artists, 22 Albums", [
61
+ genre("Psytrance", "g:psy", "0 Artists, 4 Albums"),
62
+ genre("Progressive Trance", "g:prog", "9 Artists, 4 Albums"),
63
+ ]),
64
+ genre("House", "g:house", "59 Artists, 40 Albums"),
65
+ ]),
66
+ },
67
+ genre("Jazz", "g:jazz", "5 Artists, 3 Albums"),
68
+ ];
69
+ }
70
+ function buildService() {
71
+ const fake = new FakeGenres(buildTree());
72
+ const stub = { waitForCore: async () => undefined, getBrowse: () => fake };
73
+ return { svc: new GenreService(new BrowseSessionManager(stub)), fake };
74
+ }
75
+ test("walks the genre tree and surfaces nested sub-genres with their path", async () => {
76
+ const { svc } = buildService();
77
+ const out = await svc.searchGenres("Psytrance", 10);
78
+ const psy = out.find((c) => c.title === "Psytrance");
79
+ assert.ok(psy, "Psytrance should be found");
80
+ assert.equal(psy.type, "genre");
81
+ assert.equal(psy.subtitle, "Electronic › Trance › Psytrance");
82
+ assert.equal(psy.sourceGroup, "Genres");
83
+ assert.equal(psy.score, 1); // exact title match
84
+ // The itemKey is a genre locator carrying the node path.
85
+ const loc = decodeLocator(psy.itemKey ?? "");
86
+ assert.ok(loc && isGenreLocator(loc));
87
+ assert.deepEqual(loc.ge, ["Electronic", "Trance", "Psytrance"]);
88
+ });
89
+ test("the 'Artists'/'Albums' containers inside a genre page are not indexed", async () => {
90
+ const { svc } = buildService();
91
+ const out = await svc.searchGenres("Artists", 10);
92
+ assert.ok(!out.some((c) => c.title === "Artists"));
93
+ });
94
+ test("a fuzzy query returns ranked near-matches (psychedelic trance → psytrance, trance)", async () => {
95
+ const { svc } = buildService();
96
+ const out = await svc.searchGenres("Psychedelic Trance", 10);
97
+ const titles = out.map((c) => c.title);
98
+ assert.ok(titles.includes("Psytrance"), "Psytrance should be a near-match");
99
+ assert.ok(titles.includes("Trance"), "Trance should be a near-match");
100
+ // Psytrance (shares the "trance" token and a "psy" prefix) outranks the bare
101
+ // parent "Trance" (token-only match).
102
+ const psyRank = titles.indexOf("Psytrance");
103
+ const tranceRank = titles.indexOf("Trance");
104
+ assert.ok(psyRank < tranceRank, "Psytrance should rank above Trance");
105
+ // No exact node named "Psychedelic Trance": all scores are below 1.
106
+ assert.ok(out.every((c) => c.score < 1));
107
+ });
108
+ test("an exact top-level genre matches with score 1", async () => {
109
+ const { svc } = buildService();
110
+ const out = await svc.searchGenres("Jazz", 10);
111
+ assert.equal(out[0]?.title, "Jazz");
112
+ assert.equal(out[0]?.score, 1);
113
+ });
114
+ test("a query that matches nothing returns no candidates", async () => {
115
+ const { svc } = buildService();
116
+ const out = await svc.searchGenres("Polka", 10);
117
+ assert.deepEqual(out, []);
118
+ });
119
+ test("scoreGenre: short title tokens (R&B, Children's) don't spuriously match", () => {
120
+ // "Cumbia" shares a 'b' with R&B and an 's'… with nothing real — must be 0.
121
+ assert.equal(scoreGenre("Cumbia", "R&B"), 0);
122
+ assert.equal(scoreGenre("Shoegaze", "Children's"), 0);
123
+ assert.equal(scoreGenre("Bolero", "R&B"), 0);
124
+ });
125
+ test("scoreGenre: a better sub-genre outranks a partial first-word match", () => {
126
+ // "Psychedelic" covers only the first word of the query; "Psytrance" is the
127
+ // real near-match and must score higher (the old prefix tier inverted this).
128
+ assert.ok(scoreGenre("Psychedelic Trance", "Psytrance") > scoreGenre("Psychedelic Trance", "Psychedelic"));
129
+ });
130
+ test("scoreGenre: stopwords and short tokens don't dilute coverage", () => {
131
+ // "Drum and Bass" → both meaningful words match "Jungle/Drum'n'Bass"; "and"
132
+ // and the title's "n" must not drag the score down.
133
+ assert.ok(scoreGenre("Drum and Bass", "Jungle/Drum'n'Bass") >= MIN_GENRE_SCORE);
134
+ });
135
+ test("absent genres return nothing rather than a noise match", async () => {
136
+ const { svc } = buildService();
137
+ for (const q of ["Cumbia", "Bossa Nova", "Bluegrass", "Zydeco"]) {
138
+ assert.deepEqual(await svc.searchGenres(q, 10), [], `${q} should not match`);
139
+ }
140
+ });
141
+ test("the genre index is built once and reused across searches", async () => {
142
+ const { svc, fake } = buildService();
143
+ await svc.searchGenres("Trance", 10);
144
+ await svc.searchGenres("Jazz", 10);
145
+ assert.equal(fake.rootResets, 1, "the tree should be walked only once (cached)");
146
+ });
147
+ //# sourceMappingURL=GenreService.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GenreService.test.js","sourceRoot":"","sources":["../src/GenreService.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { BrowseSessionManager } from "./BrowseSessionManager.js";
2
+ import { RoonClient } from "./RoonClient.js";
3
+ import { type RoonCallLogger } from "./logger.js";
4
+ import { TrackExpansionService } from "./TrackExpansionService.js";
5
+ import { ZoneService } from "./ZoneService.js";
6
+ import { type EnqueueAndPlayInput, type EnqueueAndPlayOutput, type PlaybackResult, type PlayNowInput } from "./types.js";
7
+ /** Executes Browse actions that build and start queues. */
8
+ export declare class PlaybackService {
9
+ private readonly browse;
10
+ private readonly zones;
11
+ private readonly roon;
12
+ private readonly tracks;
13
+ private readonly logger;
14
+ private readonly navigator;
15
+ constructor(browse: BrowseSessionManager, zones: ZoneService, roon: RoonClient, tracks: TrackExpansionService, logger?: RoonCallLogger);
16
+ /** Start a single search candidate playing immediately in the target zone. */
17
+ playNow(input: PlayNowInput): Promise<PlaybackResult>;
18
+ private performPlayNow;
19
+ /**
20
+ * Build an ad-hoc queue from curated item keys and start it playing. The
21
+ * first playable item starts via "Play Now"; the rest append via "Queue" /
22
+ * "Add to Queue". Items that can't be opened/queued are skipped and reported
23
+ * so the agent can backfill, rather than failing the whole call.
24
+ */
25
+ enqueueAndPlay(input: EnqueueAndPlayInput): Promise<EnqueueAndPlayOutput>;
26
+ /**
27
+ * Re-navigate to one located item/track, find a matching action, and invoke
28
+ * it against the zone. Each call re-navigates from a fresh search (so the keys
29
+ * are live and no pop bookkeeping is needed); searching does not disturb the
30
+ * zone's queue being built. Per-item Roon failures are returned (not thrown)
31
+ * so the enqueue loop can skip and continue.
32
+ */
33
+ private queueOne;
34
+ /**
35
+ * Find an action (by label preference) for the currently-opened item.
36
+ * Inspects the loaded list directly, and drills one level into an
37
+ * `action_list` container if the actions aren't exposed at the top level.
38
+ * Reports how many extra levels it pushed so callers can pop back.
39
+ */
40
+ private findAction;
41
+ /** Best-effort shuffle via Transport; returns false if unsupported/failed. */
42
+ private trySetShuffle;
43
+ }
@@ -0,0 +1,291 @@
1
+ import { silentLogger } from "./logger.js";
2
+ import { hierarchyForLocator } from "./locator.js";
3
+ import { SearchNavigator, requireLocator } from "./SearchNavigator.js";
4
+ import { RoonMcpError, } from "./types.js";
5
+ // Item keys handed to playback are locators; we drive the playback drill in the
6
+ // hierarchy that produced them (flat "search" for most items, "genres" for a
7
+ // genre), re-navigating to a live key via SearchNavigator.
8
+ const LOAD_COUNT = 100;
9
+ // English Roon action labels, ordered by preference (highest first). The plan
10
+ // flags localization as an open item; non-English Cores need locale-aware
11
+ // matching here, mirroring SearchService's GROUP_TITLE_TO_TYPE caveat.
12
+ const PLAY_LABELS = [
13
+ "play now",
14
+ "play album",
15
+ "play artist",
16
+ "play genre",
17
+ "play playlist",
18
+ "play track",
19
+ "play",
20
+ "start radio",
21
+ ];
22
+ const SHUFFLE_LABELS = ["shuffle"];
23
+ // For curated, ordered queues we append to the end ("Add to Queue"/"Queue")
24
+ // rather than "Add Next", which would reverse the order on repeated calls.
25
+ const QUEUE_LABELS = ["add to queue", "queue", "add next"];
26
+ function normalize(text) {
27
+ return text.trim().toLowerCase();
28
+ }
29
+ /** An action candidate has a key and is not a header/list/action-list node. */
30
+ function isActionItem(item) {
31
+ return Boolean(item.item_key) && (item.hint === "action" || item.hint == null);
32
+ }
33
+ /** Pick the best-matching action by label preference (exact, then prefix). */
34
+ function pickAction(items, labels) {
35
+ const actions = items.filter(isActionItem);
36
+ for (const label of labels) {
37
+ const hit = actions.find((a) => normalize(a.title) === label);
38
+ if (hit)
39
+ return hit;
40
+ }
41
+ for (const label of labels) {
42
+ const hit = actions.find((a) => normalize(a.title).startsWith(label));
43
+ if (hit)
44
+ return hit;
45
+ }
46
+ return null;
47
+ }
48
+ /** Executes Browse actions that build and start queues. */
49
+ export class PlaybackService {
50
+ browse;
51
+ zones;
52
+ roon;
53
+ tracks;
54
+ logger;
55
+ navigator;
56
+ constructor(browse, zones, roon, tracks, logger = silentLogger) {
57
+ this.browse = browse;
58
+ this.zones = zones;
59
+ this.roon = roon;
60
+ this.tracks = tracks;
61
+ this.logger = logger;
62
+ this.navigator = new SearchNavigator(browse);
63
+ }
64
+ /** Start a single search candidate playing immediately in the target zone. */
65
+ async playNow(input) {
66
+ const shuffle = input.shuffle ?? false;
67
+ const loc = requireLocator(input.itemKey);
68
+ // Resolve the target up front (explicit id, configured default, or
69
+ // heuristics) so a bad/missing zone fails clearly, not as a confusing
70
+ // browse-action error.
71
+ const { targetId } = await this.zones.resolveTarget(input.zoneId);
72
+ return this.browse.runExclusive(async () => {
73
+ try {
74
+ return await this.performPlayNow(loc, targetId, shuffle);
75
+ }
76
+ catch (err) {
77
+ // Per plan: if action invocation fails, reopen the item and retry once.
78
+ // Re-navigating gives fresh live keys, so the retry is meaningful. (A
79
+ // stale locator surfaces as INVALID_ITEM_KEY and is not retried.)
80
+ if (err instanceof RoonMcpError && err.code === "ACTION_FAILED") {
81
+ return this.performPlayNow(loc, targetId, shuffle);
82
+ }
83
+ throw err;
84
+ }
85
+ });
86
+ }
87
+ async performPlayNow(loc, zoneId, shuffle) {
88
+ const hierarchy = hierarchyForLocator(loc);
89
+ // 1. Re-navigate to a live key for the located item and open it.
90
+ const opened = await this.navigator.openItem(loc);
91
+ if (opened.action === "message") {
92
+ throw new RoonMcpError("NO_PLAY_ACTION", opened.message ?? "Item is not playable.");
93
+ }
94
+ // 2. Discover a play (or shuffle) action among the item's options.
95
+ const labels = shuffle ? [...SHUFFLE_LABELS, ...PLAY_LABELS] : PLAY_LABELS;
96
+ const { action } = await this.findAction(hierarchy, labels);
97
+ if (!action) {
98
+ throw new RoonMcpError("NO_PLAY_ACTION", "No play action is available for this item.");
99
+ }
100
+ const usedShuffleAction = normalize(action.title).includes("shuffle");
101
+ // 3. Invoke the action against the target zone. Starting new content must
102
+ // go through a Browse action carrying zone_or_output_id (not Transport).
103
+ const result = await this.browse.browse({
104
+ hierarchy,
105
+ item_key: action.item_key,
106
+ zone_or_output_id: zoneId,
107
+ });
108
+ if (result.action === "message" && result.is_error) {
109
+ throw new RoonMcpError("ACTION_FAILED", result.message ?? "Play action failed.");
110
+ }
111
+ // 4. If shuffle was requested but the chosen action wasn't a shuffle, try
112
+ // the Transport setting as a best-effort fallback.
113
+ let shuffleNote;
114
+ if (shuffle && !usedShuffleAction) {
115
+ const applied = await this.trySetShuffle(zoneId, true);
116
+ if (!applied) {
117
+ shuffleNote =
118
+ " Shuffle was requested but could not be applied (no shuffle action and Transport setting unavailable).";
119
+ }
120
+ }
121
+ const nowPlaying = await this.zones.nowPlayingFor(zoneId).catch(() => undefined);
122
+ return {
123
+ ok: true,
124
+ zoneId,
125
+ queued: 1,
126
+ skipped: [],
127
+ nowPlaying,
128
+ message: `Started "${action.title}".${shuffleNote ?? ""}`,
129
+ };
130
+ }
131
+ /**
132
+ * Build an ad-hoc queue from curated item keys and start it playing. The
133
+ * first playable item starts via "Play Now"; the rest append via "Queue" /
134
+ * "Add to Queue". Items that can't be opened/queued are skipped and reported
135
+ * so the agent can backfill, rather than failing the whole call.
136
+ */
137
+ async enqueueAndPlay(input) {
138
+ const { targetId } = await this.zones.resolveTarget(input.zoneId);
139
+ const requested = input.itemKeys.length;
140
+ if (requested === 0) {
141
+ return {
142
+ ok: false,
143
+ zoneId: targetId,
144
+ queued: 0,
145
+ requested: 0,
146
+ skipped: [],
147
+ message: "No item keys were provided.",
148
+ };
149
+ }
150
+ return this.browse.runExclusive(async () => {
151
+ const skipped = [];
152
+ let queued = 0;
153
+ let index = 0;
154
+ // 1. Start the queue with the first item that can Play Now. If the first
155
+ // can't play, fall through to the next as the queue starter.
156
+ let started = false;
157
+ for (; index < input.itemKeys.length; index++) {
158
+ const key = input.itemKeys[index];
159
+ const outcome = await this.queueOne(key, targetId, PLAY_LABELS);
160
+ if (outcome.ok) {
161
+ queued++;
162
+ started = true;
163
+ index++;
164
+ break;
165
+ }
166
+ skipped.push({ itemKey: key, reason: outcome.reason });
167
+ }
168
+ if (!started) {
169
+ return {
170
+ ok: false,
171
+ zoneId: targetId,
172
+ queued: 0,
173
+ requested,
174
+ skipped,
175
+ message: "No provided item could start playback.",
176
+ };
177
+ }
178
+ // 2. Append the remaining items in order.
179
+ for (; index < input.itemKeys.length; index++) {
180
+ const key = input.itemKeys[index];
181
+ const outcome = await this.queueOne(key, targetId, QUEUE_LABELS);
182
+ if (outcome.ok)
183
+ queued++;
184
+ else
185
+ skipped.push({ itemKey: key, reason: outcome.reason });
186
+ }
187
+ // 3. Apply shuffle only when explicitly requested (leave the zone's
188
+ // setting untouched otherwise). Best-effort via Transport.
189
+ let shuffleNote;
190
+ if (input.shuffle !== undefined) {
191
+ const applied = await this.trySetShuffle(targetId, input.shuffle);
192
+ if (!applied) {
193
+ shuffleNote = ` Shuffle ${input.shuffle ? "on" : "off"} could not be applied (Transport setting unavailable).`;
194
+ }
195
+ }
196
+ const nowPlaying = await this.zones.nowPlayingFor(targetId).catch(() => undefined);
197
+ const message = `Queued ${queued} of ${requested} item(s)` +
198
+ (skipped.length > 0 ? `, skipped ${skipped.length}.` : ".") +
199
+ (shuffleNote ?? "");
200
+ return { ok: queued > 0, zoneId: targetId, queued, requested, skipped, nowPlaying, message };
201
+ });
202
+ }
203
+ /**
204
+ * Re-navigate to one located item/track, find a matching action, and invoke
205
+ * it against the zone. Each call re-navigates from a fresh search (so the keys
206
+ * are live and no pop bookkeeping is needed); searching does not disturb the
207
+ * zone's queue being built. Per-item Roon failures are returned (not thrown)
208
+ * so the enqueue loop can skip and continue.
209
+ */
210
+ async queueOne(itemKey, zoneId, labels) {
211
+ try {
212
+ const loc = requireLocator(itemKey);
213
+ const hierarchy = hierarchyForLocator(loc);
214
+ // A track locator (t set) resolves through the track list; an item
215
+ // locator opens the candidate directly.
216
+ const opened = loc.t !== undefined
217
+ ? await this.tracks.openTrackForPlayback(loc)
218
+ : await this.navigator.openItem(loc);
219
+ if (opened.action === "message") {
220
+ return { ok: false, reason: opened.message ?? "Item is not playable." };
221
+ }
222
+ const { action } = await this.findAction(hierarchy, labels);
223
+ if (!action)
224
+ return { ok: false, reason: "No matching play/queue action." };
225
+ const result = await this.browse.browse({
226
+ hierarchy,
227
+ item_key: action.item_key,
228
+ zone_or_output_id: zoneId,
229
+ });
230
+ if (result.action === "message" && result.is_error) {
231
+ return { ok: false, reason: result.message ?? "Action failed." };
232
+ }
233
+ return { ok: true };
234
+ }
235
+ catch (err) {
236
+ // A stale locator or a transient per-item browse failure shouldn't abort
237
+ // the whole queue; surface it as a skip reason. Fatal errors (e.g. no
238
+ // Core) still propagate.
239
+ if (err instanceof RoonMcpError &&
240
+ (err.code === "INVALID_ITEM_KEY" || err.code === "BROWSE_FAILED")) {
241
+ return { ok: false, reason: err.message };
242
+ }
243
+ throw err;
244
+ }
245
+ }
246
+ /**
247
+ * Find an action (by label preference) for the currently-opened item.
248
+ * Inspects the loaded list directly, and drills one level into an
249
+ * `action_list` container if the actions aren't exposed at the top level.
250
+ * Reports how many extra levels it pushed so callers can pop back.
251
+ */
252
+ async findAction(hierarchy, labels) {
253
+ const loaded = await this.browse.load({
254
+ hierarchy,
255
+ offset: 0,
256
+ count: LOAD_COUNT,
257
+ });
258
+ const direct = pickAction(loaded.items, labels);
259
+ if (direct)
260
+ return { action: direct, extraLevelsPushed: 0 };
261
+ const container = loaded.items.find((i) => i.hint === "action_list" && i.item_key);
262
+ if (container) {
263
+ await this.browse.browse({ hierarchy, item_key: container.item_key });
264
+ const sub = await this.browse.load({
265
+ hierarchy,
266
+ offset: 0,
267
+ count: LOAD_COUNT,
268
+ });
269
+ return { action: pickAction(sub.items, labels), extraLevelsPushed: 1 };
270
+ }
271
+ return { action: null, extraLevelsPushed: 0 };
272
+ }
273
+ /** Best-effort shuffle via Transport; returns false if unsupported/failed. */
274
+ async trySetShuffle(zoneId, enabled) {
275
+ let transport;
276
+ try {
277
+ transport = this.roon.getTransport();
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ if (typeof transport.change_settings !== "function")
283
+ return false;
284
+ return this.logger.call("change_settings", { zoneId, settings: { shuffle: enabled } }, () => new Promise((resolve) => {
285
+ transport.change_settings(zoneId, { shuffle: enabled }, (error) => {
286
+ resolve(!error);
287
+ });
288
+ }), (applied) => ({ applied }));
289
+ }
290
+ }
291
+ //# sourceMappingURL=PlaybackService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlaybackService.js","sourceRoot":"","sources":["../src/PlaybackService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};