iobroker.yamaha 2.3.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -17
- package/build/lib/browse/browse-engine.js +35 -0
- package/build/lib/browse/browse-engine.js.map +3 -3
- package/build/lib/browse/objects.js +48 -2
- package/build/lib/browse/objects.js.map +2 -2
- package/build/lib/browse/surface.js +2 -1
- package/build/lib/browse/surface.js.map +2 -2
- package/build/lib/browse/types.js +6 -0
- package/build/lib/browse/types.js.map +2 -2
- package/build/lib/browse/xml-browse-driver.js +37 -18
- package/build/lib/browse/xml-browse-driver.js.map +2 -2
- package/build/lib/browse/ynca-browse-driver.js +64 -1
- package/build/lib/browse/ynca-browse-driver.js.map +2 -2
- package/build/lib/catalog/groups.js +1 -1
- package/build/lib/catalog/groups.js.map +2 -2
- package/build/lib/device-controller.js +8 -4
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/xml/device-controller.js +6 -2
- package/build/lib/xml/device-controller.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ legacy XML protocol of the oldest pre-2010 models — behind one object tree.
|
|
|
23
23
|
- **Presets and favourites** — recall tuner presets and stored network/USB favourites by number, step through presets, save the current station to a preset slot or bookmark it, and read the stored lists with their names (MusicCast); recently-played recall on MusicCast devices
|
|
24
24
|
- **Menu browsing** — page through the Net Radio, media-server and USB menus like with the remote: the visible menu lines as datapoints, select-by-line, and a path datapoint that navigates to a favourite in one write
|
|
25
25
|
- **Scenes with their names** — recall a scene by number or by its title from a dropdown that shows the names the receiver reports, per zone — plus a scene list for visualizations
|
|
26
|
-
- **On-screen remote** — cursor pad and menu keys as datapoints on MusicCast
|
|
26
|
+
- **On-screen remote** — cursor pad and menu keys as datapoints, on **all three protocols**: the same words drive a 2024 MusicCast device, a YNCA receiver and a pre-2010 XML model
|
|
27
27
|
- **Clock & alarm view** — MusicCast desk-audio devices show their clock and alarm settings
|
|
28
28
|
- **Capability-driven** — states are generated from what each device reports, no hardcoded model list
|
|
29
29
|
- **Automatic discovery** — an empty device list finds and sets up MusicCast devices at startup
|
|
@@ -61,8 +61,8 @@ The **Data points** section switches whole groups of datapoints on or off — **
|
|
|
61
61
|
Each receiver becomes one device node with themed groups — the same groups the
|
|
62
62
|
**Data points** switches control. Only what your device reports is created.
|
|
63
63
|
|
|
64
|
-
- **Amplifier core** (always on) — power, volume, mute, input, sound program, sleep, plus the device info with model, firmware, IP address and connection
|
|
65
|
-
- **`player`** — ONE "now playing" block per zone: `player.source` says what the zone is listening to, and playback state, artist, album, track, cover art, times and the transport buttons always describe exactly that — whatever source is playing. Zones 2–4 get their own block under `multiroom.zoneN.player`. The source folders keep only what is genuinely their own: preset recall & save for net radio/server/USB, the MusicCast favourite/recent/playlist/queue lists under `player.netPlayer`, the CD drive states, Bluetooth pairing and the AirPlay volume interlock. The `player.browse` folder mirrors the device's media menu: the eight visible lines (folders and titles marked by symbol), `selectLine` acts like OK on the remote, page/back/root buttons, a `rows` JSON for widgets and a `path` datapoint that walks e.g. `Bookmarks>Radio Paradise` on one write.
|
|
64
|
+
- **Amplifier core** (always on) — power, volume, mute, input, sound program, sleep, plus the device info with model, firmware, IP address and connection.
|
|
65
|
+
- **`player`** — ONE "now playing" block per zone: `player.source` says what the zone is listening to, and playback state, artist, album, track, cover art, times and the transport buttons always describe exactly that — whatever source is playing. Zones 2–4 get their own block under `multiroom.zoneN.player`. The source folders keep only what is genuinely their own: preset recall & save for net radio/server/USB, the MusicCast favourite/recent/playlist/queue lists under `player.netPlayer`, the CD drive states, Bluetooth pairing and the AirPlay volume interlock. The `player.browse` folder mirrors the device's media menu: the eight visible lines (folders and titles marked by symbol), `selectLine` acts like OK on the remote, page/back/root buttons, a `rows` JSON for widgets and a `path` datapoint that walks e.g. `Bookmarks>Radio Paradise` on one write. Beside it, `remote.cursor` (and `remote.menu` where the protocol has it) is the on-screen remote itself — `up`, `down`, `left`, `right`, `select`, `return`, `home` mean the same on every receiver, whichever protocol it speaks.
|
|
66
66
|
- **`tuner`** — one band, one frequency (kHz on every generation) and one preset for AM, FM and DAB, plus RDS texts and reception flags; only genuinely DAB-specific detail (service, ensemble, DLS, …) sits under `tuner.dab`.
|
|
67
67
|
- **`multiroom`** — zones 2–4 (each with its own player and scene block), Zone B, the all-zones switches (master power, party mode) and the MusicCast device group in its own `multiroom.group` folder.
|
|
68
68
|
- **`hdmi`** — the HDMI outputs and the two lip-sync offsets.
|
|
@@ -103,6 +103,11 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
103
103
|
Placeholder for the next version (at the beginning of the line):
|
|
104
104
|
### **WORK IN PROGRESS**
|
|
105
105
|
-->
|
|
106
|
+
### 2.4.0 (2026-09-03)
|
|
107
|
+
|
|
108
|
+
- (krobipd) New: the on-screen remote reaches every protocol now — the cursor pad and the menu keys work on YNCA and pre-2010 XML receivers, not just on MusicCast
|
|
109
|
+
- (krobipd) Changed: stepping one menu level back no longer switches to a substitute key on a receiver that rejects it — on those models the new cursor pad does it
|
|
110
|
+
|
|
106
111
|
### 2.3.3 (2026-09-03)
|
|
107
112
|
|
|
108
113
|
- (krobipd) New: the menu browser, the MusicCast group, the clock alarm and the disc drive now explain their datapoints too, in eleven languages
|
|
@@ -120,20 +125,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
120
125
|
- (krobipd) New: every datapoint and every folder now carries a short explanation in eleven languages, so the object tree tells you what a value actually means
|
|
121
126
|
- (krobipd) Improved: a text that has no translation yet falls back to readable English instead of showing an internal key
|
|
122
127
|
|
|
123
|
-
### 2.2.0 (2026-09-03)
|
|
124
|
-
|
|
125
|
-
- (krobipd) Changed: the playback times are now a number in seconds, so the media player, Alexa and Google can show them on every receiver
|
|
126
|
-
- (krobipd) New: the readable playback times ("1:23") moved to their own datapoints, next to the seconds — bind whichever form your visualization needs
|
|
127
|
-
- (krobipd) Fixed: after a restart the adapter asks the receiver again instead of trusting what it remembered, so the menu, the tuner band and each zone's source are the current ones
|
|
128
|
-
- (krobipd) Fixed: setting a tuner frequency on a MusicCast device never reached it — the command was incomplete
|
|
129
|
-
- (krobipd) Fixed: commands to a receiver from before 2010 are now sent the way every comparable program sends them; older models can reject the previous form outright
|
|
130
|
-
- (krobipd) Fixed: a scene you renamed at the receiver now appears while the adapter runs, and recalling a scene by an unknown name says so in the log instead of doing nothing
|
|
131
|
-
- (krobipd) Fixed: after a restart the tuner of an older receiver no longer shows the station of the previous session until the next poll
|
|
132
|
-
- (krobipd) Fixed: an incomplete answer from a MusicCast device is no longer kept as that device's capabilities, and a device that answers nothing is no longer shown as connected
|
|
133
|
-
- (krobipd) Fixed: going one menu level back on an older receiver no longer jumps two levels when the reply gets lost
|
|
134
|
-
- (krobipd) Fixed: a play or pause sent from a script or a visualization now reaches the receiver in every case
|
|
135
|
-
- (krobipd) Improved: each of the 23 input-name datapoints now carries the input it names, instead of all reading the same
|
|
136
|
-
|
|
137
128
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
138
129
|
|
|
139
130
|
## History
|
|
@@ -99,6 +99,41 @@ class BrowseEngine {
|
|
|
99
99
|
}
|
|
100
100
|
this.deps.emit("player.browse.rows", JSON.stringify(window.rows));
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Handle a user write to the on-screen remote (`remote.cursor`, `remote.menu`).
|
|
104
|
+
*
|
|
105
|
+
* Deliberately NOT through {@link run}: a remote key is a single press, not a menu
|
|
106
|
+
* operation. `run` serialises and DROPS a second write while one is in flight, which on a
|
|
107
|
+
* pad — where the user holds a direction — would swallow presses; and it would raise `busy`
|
|
108
|
+
* on a surface nobody is browsing. Ordering and pacing on the wire are the command gate's
|
|
109
|
+
* job, and it does that for every transport already.
|
|
110
|
+
*
|
|
111
|
+
* @param stateId the state id relative to the device (`remote.…`)
|
|
112
|
+
* @param value the written value
|
|
113
|
+
*/
|
|
114
|
+
handleRemoteWrite(stateId, value) {
|
|
115
|
+
var _a;
|
|
116
|
+
if (this.closed || typeof value !== "string") {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const press = stateId === "remote.cursor" ? { what: `cursor ${value}`, key: this.driver.cursorValues, run: () => {
|
|
120
|
+
var _a2, _b;
|
|
121
|
+
return (_b = (_a2 = this.driver).cursor) == null ? void 0 : _b.call(_a2, value);
|
|
122
|
+
} } : stateId === "remote.menu" ? { what: `menu ${value}`, key: this.driver.menuValues, run: () => {
|
|
123
|
+
var _a2, _b;
|
|
124
|
+
return (_b = (_a2 = this.driver).menu) == null ? void 0 : _b.call(_a2, value);
|
|
125
|
+
} } : void 0;
|
|
126
|
+
if (!((_a = press == null ? void 0 : press.key) == null ? void 0 : _a.includes(value))) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
void (async () => {
|
|
130
|
+
try {
|
|
131
|
+
await press.run();
|
|
132
|
+
} catch (e) {
|
|
133
|
+
this.deps.log.warn(`browse: ${press.what} failed: ${(0, import_util.errorMessage)(e)}`);
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
102
137
|
/**
|
|
103
138
|
* Handle a user write to one of the browse states.
|
|
104
139
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/browse-engine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { BrowseDriver, BrowseRow, BrowseWindow } from \"./types\";\nimport type { ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\n\n/** Poll interval while waiting for the device to deliver a fresh window. */\nconst WAIT_POLL_MS = 250;\n/** How long one navigation step (select/page/back/home) may take before the walk aborts. */\nconst STEP_TIMEOUT_MS = 15000;\n/** Hard page cap while searching one path segment, beyond the totalItems-derived bound. */\nconst MAX_SEARCH_PAGES = 32;\n\n/** The adapter-bound callbacks the engine drives. */\nexport interface BrowseEngineDeps {\n /** Write a browse state (id relative to the device, e.g. `player.browse.line1`) with ack. */\n emit(id: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /** Adapter-managed delay (so no native timer outlives onUnload). */\n delay(ms: number): Promise<void>;\n}\n\n/**\n * Prefix a row's text with its kind symbol, so a plain VIS button caption already\n * shows whether the line is a folder or a playable title.\n *\n * @param row the row to label\n * @returns the display text\n */\nexport function rowLabel(row: BrowseRow): string {\n if (row.kind === \"folder\") {\n return `\\u{1F4C1} ${row.text}`;\n }\n if (row.kind === \"item\") {\n return `\u266A ${row.text}`;\n }\n return row.text;\n}\n\n/**\n * The transport-neutral browsing engine: it owns the `player.browse.*` states, turns\n * user writes into driver operations, and renders every window the driver reports.\n * Exactly one engine is active per device \u2014 the one inside the owning transport's\n * controller (writes only ever reach the owner, and a non-owner's state writes are\n * filtered by the transport adapter).\n */\nexport class BrowseEngine {\n private window: BrowseWindow | undefined;\n /** Bumped on every window report \u2014 the path walk waits on it. */\n private windowVersion = 0;\n /** Serializes operations: a write while an operation runs is dropped with a log line. */\n private running = false;\n private closed = false;\n\n /**\n * @param driver the transport's list operations\n * @param deps the adapter-bound callbacks\n */\n public constructor(\n private readonly driver: BrowseDriver,\n private readonly deps: BrowseEngineDeps,\n ) {}\n\n /**\n * Seed the surface into its resting form on every connect: no menu is open yet, so every\n * window state has to say so.\n *\n * Only `busy` and `path` used to be seeded, and the window states are written nowhere but in\n * {@link onWindow} \u2014 so the eight lines, the menu name and the rows kept whatever the last\n * browsing session had painted, across restarts and for as long as nobody browsed again\n * (measured on a live receiver: rows from six days earlier next to a connection minutes old).\n * A visualisation or script reading them saw a menu that is not open. Same rule as the player\n * block's resting seeds in v2.0.1 \u2014 the browse block was simply missed then.\n */\n public seed(): void {\n this.deps.emit(\"player.browse.busy\", false);\n this.deps.emit(\"player.browse.path\", \"\");\n this.deps.emit(\"player.browse.menuName\", \"\");\n this.deps.emit(\"player.browse.layer\", 0);\n this.deps.emit(\"player.browse.totalItems\", 0);\n this.deps.emit(\"player.browse.currentLine\", 0);\n for (let line = 1; line <= 8; line++) {\n this.deps.emit(`player.browse.line${line}`, \"\");\n }\n this.deps.emit(\"player.browse.rows\", \"[]\");\n }\n\n /** Stop accepting operations (the controller is closing). */\n public close(): void {\n this.closed = true;\n }\n\n /**\n * Render a fresh window the driver reports (a fetch result or, on YNCA, an\n * unsolicited auto-feedback push).\n *\n * @param window the window snapshot\n */\n public onWindow(window: BrowseWindow): void {\n // A fetch that was already in flight when the connection closed must not paint stale\n // rows into a tree that is being torn down (the XML driver polls a busy menu for up\n // to five seconds).\n if (this.closed) {\n return;\n }\n this.window = window;\n this.windowVersion++;\n this.deps.emit(\"player.browse.menuName\", window.menuName);\n this.deps.emit(\"player.browse.layer\", window.layer);\n this.deps.emit(\"player.browse.totalItems\", window.totalItems);\n this.deps.emit(\"player.browse.currentLine\", window.currentLine);\n for (let line = 1; line <= 8; line++) {\n const row = window.rows.find(r => r.line === line);\n this.deps.emit(`player.browse.line${line}`, row ? rowLabel(row) : \"\");\n }\n this.deps.emit(\"player.browse.rows\", JSON.stringify(window.rows));\n }\n\n /**\n * Handle a user write to one of the browse states.\n *\n * @param stateId the state id relative to the device (`player.browse.\u2026`)\n * @param value the written value\n */\n public handleWrite(stateId: string, value: unknown): void {\n const sub = stateId.slice(\"player.browse.\".length);\n switch (sub) {\n case \"source\":\n if (typeof value === \"string\" && value in this.driver.sources()) {\n void this.run(`open ${value}`, async () => {\n await this.driver.open(value);\n this.deps.emit(\"player.browse.source\", value);\n });\n }\n return;\n case \"selectLine\": {\n const line = Number(value);\n if (Number.isInteger(line) && line >= 1 && line <= 8) {\n void this.run(`select line ${line}`, () => this.driver.select(line));\n }\n return;\n }\n case \"pageUp\":\n void this.run(\"page up\", () => this.driver.pageUp());\n return;\n case \"pageDown\":\n void this.run(\"page down\", () => this.driver.pageDown());\n return;\n case \"back\":\n void this.run(\"back\", () => this.driver.back());\n return;\n case \"home\":\n void this.run(\"home\", () => this.driver.home());\n return;\n case \"path\":\n if (typeof value === \"string\" && value.trim().length > 0) {\n void this.run(`path ${value}`, () => this.walkPath(value));\n }\n return;\n default:\n return;\n }\n }\n\n /**\n * Run one operation, serialized: while one runs, further writes are dropped (the\n * busy state shows why). Failures land in the log as warnings \u2014 a user action\n * failing must be visible.\n *\n * @param what the operation, for the log line\n * @param op the operation to run\n */\n private async run(what: string, op: () => Promise<void> | void): Promise<void> {\n if (this.closed) {\n return;\n }\n if (this.running) {\n this.deps.log.debug(`browse: \"${what}\" dropped \u2014 another browse operation is still running`);\n return;\n }\n this.running = true;\n this.deps.emit(\"player.browse.busy\", true);\n try {\n await op();\n } catch (e) {\n this.deps.log.warn(`browse: ${what} failed: ${errorMessage(e)}`);\n } finally {\n this.running = false;\n if (!this.closed) {\n this.deps.emit(\"player.browse.busy\", false);\n }\n }\n }\n\n /**\n * Wait until the driver reports a window fresher than `sinceVersion` (or the step\n * timeout passes \u2014 menu levels are served by the device/catalog service and can be\n * slow, so this polls rather than racing).\n *\n * @param sinceVersion the window version before the operation\n * @returns true if a fresh window arrived, false on timeout\n */\n private async waitForWindow(sinceVersion: number): Promise<boolean> {\n const deadline = STEP_TIMEOUT_MS / WAIT_POLL_MS;\n for (let i = 0; i < deadline; i++) {\n if (this.closed) {\n return false;\n }\n if (this.windowVersion > sinceVersion) {\n return true;\n }\n await this.deps.delay(WAIT_POLL_MS);\n }\n return this.windowVersion > sinceVersion;\n }\n\n /**\n * Walk a `>`-separated path from the menu root, selecting each segment by its text\n * (paging forward when it is not in the visible window) \u2014 the one-write navigation\n * for scripts and scenes. The final segment behaves like any selection: a folder\n * opens, a playable item starts.\n *\n * @param path the path, e.g. `Bookmarks>Radio Paradise`\n */\n private async walkPath(path: string): Promise<void> {\n const segments = path\n .split(\">\")\n .map(segment => segment.trim())\n .filter(segment => segment.length > 0);\n if (segments.length === 0) {\n return;\n }\n let version = this.windowVersion;\n await this.driver.home();\n if (!(await this.waitForWindow(version))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 the menu root did not load`);\n return;\n }\n for (const segment of segments) {\n if (!(await this.findAndSelect(segment))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 \"${segment}\" not found in this menu`);\n return;\n }\n version = this.windowVersion;\n // Give the device a chance to deliver the next level; a playable final segment\n // may not change the window at all, so a timeout here is not an error.\n await this.waitForWindow(version);\n }\n }\n\n /**\n * Find a row by its text in the current menu, paging forward as needed, and select it.\n *\n * @param text the row text to find (without the symbol prefix)\n * @returns true when the row was found and selected\n */\n private async findAndSelect(text: string): Promise<boolean> {\n for (let page = 0; page < MAX_SEARCH_PAGES; page++) {\n const window = this.window;\n if (!window) {\n return false;\n }\n const row = window.rows.find(r => r.text === text && r.kind !== \"unselectable\");\n if (row) {\n await this.driver.select(row.line);\n return true;\n }\n // Stop at the last page: the window already shows the menu's tail.\n if (window.currentLine - 1 + window.rows.length >= window.totalItems) {\n return false;\n }\n const version = this.windowVersion;\n await this.driver.pageDown();\n if (!(await this.waitForWindow(version))) {\n return false;\n }\n }\n return false;\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type { BrowseDriver, BrowseRow, BrowseWindow } from \"./types\";\nimport type { ControllerLog } from \"../controller\";\nimport { errorMessage } from \"../util\";\n\n/** Poll interval while waiting for the device to deliver a fresh window. */\nconst WAIT_POLL_MS = 250;\n/** How long one navigation step (select/page/back/home) may take before the walk aborts. */\nconst STEP_TIMEOUT_MS = 15000;\n/** Hard page cap while searching one path segment, beyond the totalItems-derived bound. */\nconst MAX_SEARCH_PAGES = 32;\n\n/** The adapter-bound callbacks the engine drives. */\nexport interface BrowseEngineDeps {\n /** Write a browse state (id relative to the device, e.g. `player.browse.line1`) with ack. */\n emit(id: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /** Adapter-managed delay (so no native timer outlives onUnload). */\n delay(ms: number): Promise<void>;\n}\n\n/**\n * Prefix a row's text with its kind symbol, so a plain VIS button caption already\n * shows whether the line is a folder or a playable title.\n *\n * @param row the row to label\n * @returns the display text\n */\nexport function rowLabel(row: BrowseRow): string {\n if (row.kind === \"folder\") {\n return `\\u{1F4C1} ${row.text}`;\n }\n if (row.kind === \"item\") {\n return `\u266A ${row.text}`;\n }\n return row.text;\n}\n\n/**\n * The transport-neutral browsing engine: it owns the `player.browse.*` states, turns\n * user writes into driver operations, and renders every window the driver reports.\n * Exactly one engine is active per device \u2014 the one inside the owning transport's\n * controller (writes only ever reach the owner, and a non-owner's state writes are\n * filtered by the transport adapter).\n */\nexport class BrowseEngine {\n private window: BrowseWindow | undefined;\n /** Bumped on every window report \u2014 the path walk waits on it. */\n private windowVersion = 0;\n /** Serializes operations: a write while an operation runs is dropped with a log line. */\n private running = false;\n private closed = false;\n\n /**\n * @param driver the transport's list operations\n * @param deps the adapter-bound callbacks\n */\n public constructor(\n private readonly driver: BrowseDriver,\n private readonly deps: BrowseEngineDeps,\n ) {}\n\n /**\n * Seed the surface into its resting form on every connect: no menu is open yet, so every\n * window state has to say so.\n *\n * Only `busy` and `path` used to be seeded, and the window states are written nowhere but in\n * {@link onWindow} \u2014 so the eight lines, the menu name and the rows kept whatever the last\n * browsing session had painted, across restarts and for as long as nobody browsed again\n * (measured on a live receiver: rows from six days earlier next to a connection minutes old).\n * A visualisation or script reading them saw a menu that is not open. Same rule as the player\n * block's resting seeds in v2.0.1 \u2014 the browse block was simply missed then.\n */\n public seed(): void {\n this.deps.emit(\"player.browse.busy\", false);\n this.deps.emit(\"player.browse.path\", \"\");\n this.deps.emit(\"player.browse.menuName\", \"\");\n this.deps.emit(\"player.browse.layer\", 0);\n this.deps.emit(\"player.browse.totalItems\", 0);\n this.deps.emit(\"player.browse.currentLine\", 0);\n for (let line = 1; line <= 8; line++) {\n this.deps.emit(`player.browse.line${line}`, \"\");\n }\n this.deps.emit(\"player.browse.rows\", \"[]\");\n }\n\n /** Stop accepting operations (the controller is closing). */\n public close(): void {\n this.closed = true;\n }\n\n /**\n * Render a fresh window the driver reports (a fetch result or, on YNCA, an\n * unsolicited auto-feedback push).\n *\n * @param window the window snapshot\n */\n public onWindow(window: BrowseWindow): void {\n // A fetch that was already in flight when the connection closed must not paint stale\n // rows into a tree that is being torn down (the XML driver polls a busy menu for up\n // to five seconds).\n if (this.closed) {\n return;\n }\n this.window = window;\n this.windowVersion++;\n this.deps.emit(\"player.browse.menuName\", window.menuName);\n this.deps.emit(\"player.browse.layer\", window.layer);\n this.deps.emit(\"player.browse.totalItems\", window.totalItems);\n this.deps.emit(\"player.browse.currentLine\", window.currentLine);\n for (let line = 1; line <= 8; line++) {\n const row = window.rows.find(r => r.line === line);\n this.deps.emit(`player.browse.line${line}`, row ? rowLabel(row) : \"\");\n }\n this.deps.emit(\"player.browse.rows\", JSON.stringify(window.rows));\n }\n\n /**\n * Handle a user write to the on-screen remote (`remote.cursor`, `remote.menu`).\n *\n * Deliberately NOT through {@link run}: a remote key is a single press, not a menu\n * operation. `run` serialises and DROPS a second write while one is in flight, which on a\n * pad \u2014 where the user holds a direction \u2014 would swallow presses; and it would raise `busy`\n * on a surface nobody is browsing. Ordering and pacing on the wire are the command gate's\n * job, and it does that for every transport already.\n *\n * @param stateId the state id relative to the device (`remote.\u2026`)\n * @param value the written value\n */\n public handleRemoteWrite(stateId: string, value: unknown): void {\n if (this.closed || typeof value !== \"string\") {\n return;\n }\n const press =\n stateId === \"remote.cursor\"\n ? { what: `cursor ${value}`, key: this.driver.cursorValues, run: () => this.driver.cursor?.(value) }\n : stateId === \"remote.menu\"\n ? { what: `menu ${value}`, key: this.driver.menuValues, run: () => this.driver.menu?.(value) }\n : undefined;\n if (!press?.key?.includes(value)) {\n return;\n }\n void (async (): Promise<void> => {\n try {\n await press.run();\n } catch (e) {\n this.deps.log.warn(`browse: ${press.what} failed: ${errorMessage(e)}`);\n }\n })();\n }\n\n /**\n * Handle a user write to one of the browse states.\n *\n * @param stateId the state id relative to the device (`player.browse.\u2026`)\n * @param value the written value\n */\n public handleWrite(stateId: string, value: unknown): void {\n const sub = stateId.slice(\"player.browse.\".length);\n switch (sub) {\n case \"source\":\n if (typeof value === \"string\" && value in this.driver.sources()) {\n void this.run(`open ${value}`, async () => {\n await this.driver.open(value);\n this.deps.emit(\"player.browse.source\", value);\n });\n }\n return;\n case \"selectLine\": {\n const line = Number(value);\n if (Number.isInteger(line) && line >= 1 && line <= 8) {\n void this.run(`select line ${line}`, () => this.driver.select(line));\n }\n return;\n }\n case \"pageUp\":\n void this.run(\"page up\", () => this.driver.pageUp());\n return;\n case \"pageDown\":\n void this.run(\"page down\", () => this.driver.pageDown());\n return;\n case \"back\":\n void this.run(\"back\", () => this.driver.back());\n return;\n case \"home\":\n void this.run(\"home\", () => this.driver.home());\n return;\n case \"path\":\n if (typeof value === \"string\" && value.trim().length > 0) {\n void this.run(`path ${value}`, () => this.walkPath(value));\n }\n return;\n default:\n return;\n }\n }\n\n /**\n * Run one operation, serialized: while one runs, further writes are dropped (the\n * busy state shows why). Failures land in the log as warnings \u2014 a user action\n * failing must be visible.\n *\n * @param what the operation, for the log line\n * @param op the operation to run\n */\n private async run(what: string, op: () => Promise<void> | void): Promise<void> {\n if (this.closed) {\n return;\n }\n if (this.running) {\n this.deps.log.debug(`browse: \"${what}\" dropped \u2014 another browse operation is still running`);\n return;\n }\n this.running = true;\n this.deps.emit(\"player.browse.busy\", true);\n try {\n await op();\n } catch (e) {\n this.deps.log.warn(`browse: ${what} failed: ${errorMessage(e)}`);\n } finally {\n this.running = false;\n if (!this.closed) {\n this.deps.emit(\"player.browse.busy\", false);\n }\n }\n }\n\n /**\n * Wait until the driver reports a window fresher than `sinceVersion` (or the step\n * timeout passes \u2014 menu levels are served by the device/catalog service and can be\n * slow, so this polls rather than racing).\n *\n * @param sinceVersion the window version before the operation\n * @returns true if a fresh window arrived, false on timeout\n */\n private async waitForWindow(sinceVersion: number): Promise<boolean> {\n const deadline = STEP_TIMEOUT_MS / WAIT_POLL_MS;\n for (let i = 0; i < deadline; i++) {\n if (this.closed) {\n return false;\n }\n if (this.windowVersion > sinceVersion) {\n return true;\n }\n await this.deps.delay(WAIT_POLL_MS);\n }\n return this.windowVersion > sinceVersion;\n }\n\n /**\n * Walk a `>`-separated path from the menu root, selecting each segment by its text\n * (paging forward when it is not in the visible window) \u2014 the one-write navigation\n * for scripts and scenes. The final segment behaves like any selection: a folder\n * opens, a playable item starts.\n *\n * @param path the path, e.g. `Bookmarks>Radio Paradise`\n */\n private async walkPath(path: string): Promise<void> {\n const segments = path\n .split(\">\")\n .map(segment => segment.trim())\n .filter(segment => segment.length > 0);\n if (segments.length === 0) {\n return;\n }\n let version = this.windowVersion;\n await this.driver.home();\n if (!(await this.waitForWindow(version))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 the menu root did not load`);\n return;\n }\n for (const segment of segments) {\n if (!(await this.findAndSelect(segment))) {\n this.deps.log.warn(`browse: path \"${path}\" aborted \u2014 \"${segment}\" not found in this menu`);\n return;\n }\n version = this.windowVersion;\n // Give the device a chance to deliver the next level; a playable final segment\n // may not change the window at all, so a timeout here is not an error.\n await this.waitForWindow(version);\n }\n }\n\n /**\n * Find a row by its text in the current menu, paging forward as needed, and select it.\n *\n * @param text the row text to find (without the symbol prefix)\n * @returns true when the row was found and selected\n */\n private async findAndSelect(text: string): Promise<boolean> {\n for (let page = 0; page < MAX_SEARCH_PAGES; page++) {\n const window = this.window;\n if (!window) {\n return false;\n }\n const row = window.rows.find(r => r.text === text && r.kind !== \"unselectable\");\n if (row) {\n await this.driver.select(row.line);\n return true;\n }\n // Stop at the last page: the window already shows the menu's tail.\n if (window.currentLine - 1 + window.rows.length >= window.totalItems) {\n return false;\n }\n const version = this.windowVersion;\n await this.driver.pageDown();\n if (!(await this.waitForWindow(version))) {\n return false;\n }\n }\n return false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a"]
|
|
7
7
|
}
|
|
@@ -18,9 +18,11 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var objects_exports = {};
|
|
20
20
|
__export(objects_exports, {
|
|
21
|
-
browseObjectDefs: () => browseObjectDefs
|
|
21
|
+
browseObjectDefs: () => browseObjectDefs,
|
|
22
|
+
remoteObjectDefs: () => remoteObjectDefs
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(objects_exports);
|
|
25
|
+
var import_types = require("../catalog/types");
|
|
24
26
|
var import_i18n = require("../i18n");
|
|
25
27
|
function browseObjectDefs(sources) {
|
|
26
28
|
const line = (n) => ({
|
|
@@ -147,8 +149,52 @@ function browseObjectDefs(sources) {
|
|
|
147
149
|
}
|
|
148
150
|
];
|
|
149
151
|
}
|
|
152
|
+
function remoteObjectDefs(cursorValues, menuValues) {
|
|
153
|
+
const states = (values) => Object.fromEntries(values.map((value) => [value, value]));
|
|
154
|
+
const defs = [];
|
|
155
|
+
if (!(cursorValues == null ? void 0 : cursorValues.length) && !(menuValues == null ? void 0 : menuValues.length)) {
|
|
156
|
+
return defs;
|
|
157
|
+
}
|
|
158
|
+
defs.push({
|
|
159
|
+
id: "remote",
|
|
160
|
+
type: "channel",
|
|
161
|
+
common: { name: (0, import_i18n.tName)(import_types.CHANNEL_NAME_KEYS.remote), desc: (0, import_i18n.tName)(import_types.CHANNEL_DESC_KEYS.remote) }
|
|
162
|
+
});
|
|
163
|
+
if (cursorValues == null ? void 0 : cursorValues.length) {
|
|
164
|
+
defs.push({
|
|
165
|
+
id: "remote.cursor",
|
|
166
|
+
type: "state",
|
|
167
|
+
common: {
|
|
168
|
+
name: (0, import_i18n.tName)("cursorPad"),
|
|
169
|
+
desc: (0, import_i18n.tName)("descCursorPad"),
|
|
170
|
+
type: "string",
|
|
171
|
+
role: "state",
|
|
172
|
+
read: false,
|
|
173
|
+
write: true,
|
|
174
|
+
states: states(cursorValues)
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (menuValues == null ? void 0 : menuValues.length) {
|
|
179
|
+
defs.push({
|
|
180
|
+
id: "remote.menu",
|
|
181
|
+
type: "state",
|
|
182
|
+
common: {
|
|
183
|
+
name: (0, import_i18n.tName)("menuKey"),
|
|
184
|
+
desc: (0, import_i18n.tName)("descMenuKey"),
|
|
185
|
+
type: "string",
|
|
186
|
+
role: "state",
|
|
187
|
+
read: false,
|
|
188
|
+
write: true,
|
|
189
|
+
states: states(menuValues)
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return defs;
|
|
194
|
+
}
|
|
150
195
|
// Annotate the CommonJS export names for ESM import in node:
|
|
151
196
|
0 && (module.exports = {
|
|
152
|
-
browseObjectDefs
|
|
197
|
+
browseObjectDefs,
|
|
198
|
+
remoteObjectDefs
|
|
153
199
|
});
|
|
154
200
|
//# sourceMappingURL=objects.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/objects.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\n\n/**\n * The object tree of the browsing surface. Every capable transport contributes the\n * SAME ids, so the object-tree coordinator dedups them and modernity picks the one\n * owner (YXC > YNCA > XML) \u2014 no owner override needed. The ids live under\n * `player.browse.*`, so the existing \"Playback & browsing\" admin group switch and\n * the `player` folder gate them like every other playback datapoint.\n *\n * @param sources the selectable sources (state value \u2192 display label)\n * @returns the channel and state definitions, parents first\n */\nexport function browseObjectDefs(sources: Record<string, string>): ObjectDef[] {\n const line = (n: number): ObjectDef => ({\n id: `player.browse.line${n}`,\n type: \"state\",\n common: { name: tName(\"line\", n), type: \"string\", role: \"text\", read: true, write: false },\n });\n const button = (id: string, name: ioBroker.StringOrTranslated): ObjectDef => ({\n id: `player.browse.${id}`,\n type: \"state\",\n common: { name, type: \"boolean\", role: \"button\", read: false, write: true },\n });\n return [\n { id: \"player\", type: \"channel\", common: { name: tName(\"mediaPlayer\") } },\n { id: \"player.browse\", type: \"channel\", common: { name: tName(\"browse\") } },\n {\n id: \"player.browse.source\",\n type: \"state\",\n common: { name: tName(\"source\"), type: \"string\", role: \"state\", read: true, write: true, states: sources },\n },\n {\n id: \"player.browse.menuName\",\n type: \"state\",\n common: {\n name: tName(\"menuName\"),\n desc: tName(\"descMenuName\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.layer\",\n type: \"state\",\n common: {\n name: tName(\"menuLevel\"),\n desc: tName(\"descMenuLevel\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.totalItems\",\n type: \"state\",\n common: {\n name: tName(\"totalEntries\"),\n desc: tName(\"descTotalEntries\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.currentLine\",\n type: \"state\",\n common: {\n name: tName(\"currentLine\"),\n desc: tName(\"descCurrentLine\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n ...[1, 2, 3, 4, 5, 6, 7, 8].map(line),\n {\n id: \"player.browse.selectLine\",\n type: \"state\",\n common: {\n name: tName(\"selectLineFolderOpensItemPlays\"),\n desc: tName(\"descSelectLineFolderOpensItemPlays\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n min: 1,\n max: 8,\n step: 1,\n },\n },\n button(\"pageUp\", tName(\"pageUp\")),\n button(\"pageDown\", tName(\"pageDown\")),\n button(\"back\", tName(\"back\")),\n button(\"home\", tName(\"menuRoot\")),\n {\n id: \"player.browse.path\",\n type: \"state\",\n common: {\n name: tName(\"navigatePathEGBookmarksRadioParadise\"),\n desc: tName(\"descNavigatePathEGBookmarksRadioParadise\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n },\n },\n {\n id: \"player.browse.rows\",\n type: \"state\",\n common: {\n name: tName(\"rowsJSON\"),\n desc: tName(\"descRowsJSON\"),\n type: \"string\",\n role: \"json\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.busy\",\n type: \"state\",\n common: {\n name: tName(\"busy\"),\n desc: tName(\"descBusy\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { CHANNEL_DESC_KEYS, CHANNEL_NAME_KEYS } from \"../catalog/types\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport { tName } from \"../i18n\";\n\n/**\n * The object tree of the browsing surface. Every capable transport contributes the\n * SAME ids, so the object-tree coordinator dedups them and modernity picks the one\n * owner (YXC > YNCA > XML) \u2014 no owner override needed. The ids live under\n * `player.browse.*`, so the existing \"Playback & browsing\" admin group switch and\n * the `player` folder gate them like every other playback datapoint.\n *\n * @param sources the selectable sources (state value \u2192 display label)\n * @returns the channel and state definitions, parents first\n */\nexport function browseObjectDefs(sources: Record<string, string>): ObjectDef[] {\n const line = (n: number): ObjectDef => ({\n id: `player.browse.line${n}`,\n type: \"state\",\n common: { name: tName(\"line\", n), type: \"string\", role: \"text\", read: true, write: false },\n });\n const button = (id: string, name: ioBroker.StringOrTranslated): ObjectDef => ({\n id: `player.browse.${id}`,\n type: \"state\",\n common: { name, type: \"boolean\", role: \"button\", read: false, write: true },\n });\n return [\n { id: \"player\", type: \"channel\", common: { name: tName(\"mediaPlayer\") } },\n { id: \"player.browse\", type: \"channel\", common: { name: tName(\"browse\") } },\n {\n id: \"player.browse.source\",\n type: \"state\",\n common: { name: tName(\"source\"), type: \"string\", role: \"state\", read: true, write: true, states: sources },\n },\n {\n id: \"player.browse.menuName\",\n type: \"state\",\n common: {\n name: tName(\"menuName\"),\n desc: tName(\"descMenuName\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.layer\",\n type: \"state\",\n common: {\n name: tName(\"menuLevel\"),\n desc: tName(\"descMenuLevel\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.totalItems\",\n type: \"state\",\n common: {\n name: tName(\"totalEntries\"),\n desc: tName(\"descTotalEntries\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.currentLine\",\n type: \"state\",\n common: {\n name: tName(\"currentLine\"),\n desc: tName(\"descCurrentLine\"),\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n ...[1, 2, 3, 4, 5, 6, 7, 8].map(line),\n {\n id: \"player.browse.selectLine\",\n type: \"state\",\n common: {\n name: tName(\"selectLineFolderOpensItemPlays\"),\n desc: tName(\"descSelectLineFolderOpensItemPlays\"),\n type: \"number\",\n role: \"level\",\n read: true,\n write: true,\n min: 1,\n max: 8,\n step: 1,\n },\n },\n button(\"pageUp\", tName(\"pageUp\")),\n button(\"pageDown\", tName(\"pageDown\")),\n button(\"back\", tName(\"back\")),\n button(\"home\", tName(\"menuRoot\")),\n {\n id: \"player.browse.path\",\n type: \"state\",\n common: {\n name: tName(\"navigatePathEGBookmarksRadioParadise\"),\n desc: tName(\"descNavigatePathEGBookmarksRadioParadise\"),\n type: \"string\",\n role: \"text\",\n read: true,\n write: true,\n },\n },\n {\n id: \"player.browse.rows\",\n type: \"state\",\n common: {\n name: tName(\"rowsJSON\"),\n desc: tName(\"descRowsJSON\"),\n type: \"string\",\n role: \"json\",\n read: true,\n write: false,\n },\n },\n {\n id: \"player.browse.busy\",\n type: \"state\",\n common: {\n name: tName(\"busy\"),\n desc: tName(\"descBusy\"),\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n },\n },\n ];\n}\n\n/**\n * The on-screen remote of one transport: the cursor pad, and the menu keys where the\n * protocol has them.\n *\n * Lives beside the browsing surface because that is where the proof is. A cursor key is a\n * LIST command on both text protocols (`LISTCURSOR`, `<List_Control><Cursor>`), so a device\n * that answered no list probe has no pad either \u2014 the same rule that keeps the menu folder\n * off a receiver that cannot browse (#613). Ids are unprefixed, i.e. the main zone: neither\n * YNCA nor XML declares a cursor for zones 2\u20134.\n *\n * @param cursorValues the cursor words this transport supports (empty/absent = no pad)\n * @param menuValues the menu keys this transport supports (empty/absent = none)\n * @returns the channel and state definitions, parents first\n */\nexport function remoteObjectDefs(cursorValues?: readonly string[], menuValues?: readonly string[]): ObjectDef[] {\n const states = (values: readonly string[]): Record<string, string> =>\n Object.fromEntries(values.map(value => [value, value]));\n const defs: ObjectDef[] = [];\n if (!cursorValues?.length && !menuValues?.length) {\n return defs;\n }\n defs.push({\n id: \"remote\",\n type: \"channel\",\n common: { name: tName(CHANNEL_NAME_KEYS.remote), desc: tName(CHANNEL_DESC_KEYS.remote) },\n });\n if (cursorValues?.length) {\n defs.push({\n id: \"remote.cursor\",\n type: \"state\",\n common: {\n name: tName(\"cursorPad\"),\n desc: tName(\"descCursorPad\"),\n type: \"string\",\n role: \"state\",\n read: false,\n write: true,\n states: states(cursorValues),\n },\n });\n }\n if (menuValues?.length) {\n defs.push({\n id: \"remote.menu\",\n type: \"state\",\n common: {\n name: tName(\"menuKey\"),\n desc: tName(\"descMenuKey\"),\n type: \"string\",\n role: \"state\",\n read: false,\n write: true,\n states: states(menuValues),\n },\n });\n }\n return defs;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAqD;AAErD,kBAAsB;AAYf,SAAS,iBAAiB,SAA8C;AAC7E,QAAM,OAAO,CAAC,OAA0B;AAAA,IACtC,IAAI,qBAAqB,CAAC;AAAA,IAC1B,MAAM;AAAA,IACN,QAAQ,EAAE,UAAM,mBAAM,QAAQ,CAAC,GAAG,MAAM,UAAU,MAAM,QAAQ,MAAM,MAAM,OAAO,MAAM;AAAA,EAC3F;AACA,QAAM,SAAS,CAAC,IAAY,UAAkD;AAAA,IAC5E,IAAI,iBAAiB,EAAE;AAAA,IACvB,MAAM;AAAA,IACN,QAAQ,EAAE,MAAM,MAAM,WAAW,MAAM,UAAU,MAAM,OAAO,OAAO,KAAK;AAAA,EAC5E;AACA,SAAO;AAAA,IACL,EAAE,IAAI,UAAU,MAAM,WAAW,QAAQ,EAAE,UAAM,mBAAM,aAAa,EAAE,EAAE;AAAA,IACxE,EAAE,IAAI,iBAAiB,MAAM,WAAW,QAAQ,EAAE,UAAM,mBAAM,QAAQ,EAAE,EAAE;AAAA,IAC1E;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ,EAAE,UAAM,mBAAM,QAAQ,GAAG,MAAM,UAAU,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM,QAAQ,QAAQ;AAAA,IAC3G;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,UAAU;AAAA,QACtB,UAAM,mBAAM,cAAc;AAAA,QAC1B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,WAAW;AAAA,QACvB,UAAM,mBAAM,eAAe;AAAA,QAC3B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,cAAc;AAAA,QAC1B,UAAM,mBAAM,kBAAkB;AAAA,QAC9B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,aAAa;AAAA,QACzB,UAAM,mBAAM,iBAAiB;AAAA,QAC7B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAAE,IAAI,IAAI;AAAA,IACpC;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,gCAAgC;AAAA,QAC5C,UAAM,mBAAM,oCAAoC;AAAA,QAChD,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,KAAK;AAAA,QACL,KAAK;AAAA,QACL,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,OAAO,cAAU,mBAAM,QAAQ,CAAC;AAAA,IAChC,OAAO,gBAAY,mBAAM,UAAU,CAAC;AAAA,IACpC,OAAO,YAAQ,mBAAM,MAAM,CAAC;AAAA,IAC5B,OAAO,YAAQ,mBAAM,UAAU,CAAC;AAAA,IAChC;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,sCAAsC;AAAA,QAClD,UAAM,mBAAM,0CAA0C;AAAA,QACtD,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,UAAU;AAAA,QACtB,UAAM,mBAAM,cAAc;AAAA,QAC1B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,MAAM;AAAA,QAClB,UAAM,mBAAM,UAAU;AAAA,QACtB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAgBO,SAAS,iBAAiB,cAAkC,YAA6C;AAC9G,QAAM,SAAS,CAAC,WACd,OAAO,YAAY,OAAO,IAAI,WAAS,CAAC,OAAO,KAAK,CAAC,CAAC;AACxD,QAAM,OAAoB,CAAC;AAC3B,MAAI,EAAC,6CAAc,WAAU,EAAC,yCAAY,SAAQ;AAChD,WAAO;AAAA,EACT;AACA,OAAK,KAAK;AAAA,IACR,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ,EAAE,UAAM,mBAAM,+BAAkB,MAAM,GAAG,UAAM,mBAAM,+BAAkB,MAAM,EAAE;AAAA,EACzF,CAAC;AACD,MAAI,6CAAc,QAAQ;AACxB,SAAK,KAAK;AAAA,MACR,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,WAAW;AAAA,QACvB,UAAM,mBAAM,eAAe;AAAA,QAC3B,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QAAQ,OAAO,YAAY;AAAA,MAC7B;AAAA,IACF,CAAC;AAAA,EACH;AACA,MAAI,yCAAY,QAAQ;AACtB,SAAK,KAAK;AAAA,MACR,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ;AAAA,QACN,UAAM,mBAAM,SAAS;AAAA,QACrB,UAAM,mBAAM,aAAa;AAAA,QACzB,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,QACP,QAAQ,OAAO,UAAU;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,7 +28,8 @@ async function createBrowseSurface(driver, deviceId, deps) {
|
|
|
28
28
|
if (Object.keys(sources).length === 0) {
|
|
29
29
|
return void 0;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const defs = [...(0, import_objects.browseObjectDefs)(sources), ...(0, import_objects.remoteObjectDefs)(driver.cursorValues, driver.menuValues)];
|
|
32
|
+
for (const def of defs) {
|
|
32
33
|
await deps.upsertObject(`${deviceId}.${def.id}`, def);
|
|
33
34
|
}
|
|
34
35
|
const engine = new import_browse_engine.BrowseEngine(driver, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/surface.ts"],
|
|
4
|
-
"sourcesContent": ["import { BrowseEngine } from \"./browse-engine\";\nimport { browseObjectDefs } from \"./objects\";\nimport type { BrowseDriver } from \"./types\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport type { ControllerLog } from \"../controller\";\n\n/** What creating a browsing surface needs from its controller. */\nexport interface BrowseSurfaceDeps {\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a browse state with ack (already guarded against a closed connection). */\n emit(relativeId: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /** Adapter-managed delay that ends when the connection closes. */\n delay(ms: number): Promise<void>;\n}\n\n/**\n * Create the `player.browse.*` surface for one transport: its objects, the engine that\n * owns the states, and the wiring between them. All three transports need exactly this\n * sequence, so it lives here once instead of being copied into each controller.\n *\n * Returns undefined when the device offers nothing browsable \u2014 then no objects are created\n * at all, which is what keeps the folder off devices that cannot browse.\n *\n * @param driver the transport's list driver\n * @param deviceId the id-safe device id\n * @param deps the controller callbacks\n * @returns the engine driving the surface, or undefined when there is nothing to browse\n */\nexport async function createBrowseSurface(\n driver: BrowseDriver & { attach(engine: BrowseEngine): void },\n deviceId: string,\n deps: BrowseSurfaceDeps,\n): Promise<BrowseEngine | undefined> {\n const sources = driver.sources();\n if (Object.keys(sources).length === 0) {\n return undefined;\n }\n for (const def of
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA6B;AAC7B,
|
|
4
|
+
"sourcesContent": ["import { BrowseEngine } from \"./browse-engine\";\nimport { browseObjectDefs, remoteObjectDefs } from \"./objects\";\nimport type { BrowseDriver } from \"./types\";\nimport type { ObjectDef } from \"../catalog/types\";\nimport type { ControllerLog } from \"../controller\";\n\n/** What creating a browsing surface needs from its controller. */\nexport interface BrowseSurfaceDeps {\n /** Create or update an object in the device tree. */\n upsertObject(id: string, def: ObjectDef): Promise<void>;\n /** Write a browse state with ack (already guarded against a closed connection). */\n emit(relativeId: string, value: boolean | number | string): void;\n /** Adapter log. */\n log: ControllerLog;\n /** Adapter-managed delay that ends when the connection closes. */\n delay(ms: number): Promise<void>;\n}\n\n/**\n * Create the `player.browse.*` surface for one transport: its objects, the engine that\n * owns the states, and the wiring between them. All three transports need exactly this\n * sequence, so it lives here once instead of being copied into each controller.\n *\n * Returns undefined when the device offers nothing browsable \u2014 then no objects are created\n * at all, which is what keeps the folder off devices that cannot browse.\n *\n * @param driver the transport's list driver\n * @param deviceId the id-safe device id\n * @param deps the controller callbacks\n * @returns the engine driving the surface, or undefined when there is nothing to browse\n */\nexport async function createBrowseSurface(\n driver: BrowseDriver & { attach(engine: BrowseEngine): void },\n deviceId: string,\n deps: BrowseSurfaceDeps,\n): Promise<BrowseEngine | undefined> {\n const sources = driver.sources();\n if (Object.keys(sources).length === 0) {\n return undefined;\n }\n const defs = [...browseObjectDefs(sources), ...remoteObjectDefs(driver.cursorValues, driver.menuValues)];\n for (const def of defs) {\n await deps.upsertObject(`${deviceId}.${def.id}`, def);\n }\n const engine = new BrowseEngine(driver, {\n emit: (id, value) => deps.emit(id, value),\n log: deps.log,\n delay: deps.delay,\n });\n driver.attach(engine);\n engine.seed();\n return engine;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA6B;AAC7B,qBAAmD;AA8BnD,eAAsB,oBACpB,QACA,UACA,MACmC;AACnC,QAAM,UAAU,OAAO,QAAQ;AAC/B,MAAI,OAAO,KAAK,OAAO,EAAE,WAAW,GAAG;AACrC,WAAO;AAAA,EACT;AACA,QAAM,OAAO,CAAC,OAAG,iCAAiB,OAAO,GAAG,OAAG,iCAAiB,OAAO,cAAc,OAAO,UAAU,CAAC;AACvG,aAAW,OAAO,MAAM;AACtB,UAAM,KAAK,aAAa,GAAG,QAAQ,IAAI,IAAI,EAAE,IAAI,GAAG;AAAA,EACtD;AACA,QAAM,SAAS,IAAI,kCAAa,QAAQ;AAAA,IACtC,MAAM,CAAC,IAAI,UAAU,KAAK,KAAK,IAAI,KAAK;AAAA,IACxC,KAAK,KAAK;AAAA,IACV,OAAO,KAAK;AAAA,EACd,CAAC;AACD,SAAO,OAAO,MAAM;AACpB,SAAO,KAAK;AACZ,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,9 +18,13 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var types_exports = {};
|
|
20
20
|
__export(types_exports, {
|
|
21
|
+
CURSOR_VALUES: () => CURSOR_VALUES,
|
|
22
|
+
MENU_VALUES: () => MENU_VALUES,
|
|
21
23
|
ROW_KIND_BY_ATTRIBUTE: () => ROW_KIND_BY_ATTRIBUTE
|
|
22
24
|
});
|
|
23
25
|
module.exports = __toCommonJS(types_exports);
|
|
26
|
+
const CURSOR_VALUES = ["up", "down", "left", "right", "select", "return", "home"];
|
|
27
|
+
const MENU_VALUES = ["on_screen", "top_menu", "menu", "option", "display", "home"];
|
|
24
28
|
const ROW_KIND_BY_ATTRIBUTE = {
|
|
25
29
|
Container: "folder",
|
|
26
30
|
Item: "item",
|
|
@@ -29,6 +33,8 @@ const ROW_KIND_BY_ATTRIBUTE = {
|
|
|
29
33
|
};
|
|
30
34
|
// Annotate the CommonJS export names for ESM import in node:
|
|
31
35
|
0 && (module.exports = {
|
|
36
|
+
CURSOR_VALUES,
|
|
37
|
+
MENU_VALUES,
|
|
32
38
|
ROW_KIND_BY_ATTRIBUTE
|
|
33
39
|
});
|
|
34
40
|
//# sourceMappingURL=types.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Shared contracts for the media-menu browsing surface (`player.browse.*`, issue #613).\n * One transport-neutral engine drives the states; each transport contributes a thin\n * {@link BrowseDriver} over its own list protocol (YNCA LISTINFO/LISTSEL, YXC\n * netusb getListInfo/setListControl, XML List_Info/List_Control). The surface mirrors\n * the device's own menu: an 8-line window, select-by-line, page/back/home, plus a\n * path auto-walk for scripts \u2014 the predecessor adapter's proven VIS workflow.\n */\n\n/** The kind of a menu row, as the device reports it. */\nexport type BrowseRowKind = \"folder\" | \"item\" | \"unplayable\" | \"unselectable\";\n\n/** One visible menu row. */\nexport interface BrowseRow {\n /** The row's line number within the window (1\u20138). */\n line: number;\n /** The row's raw text (no symbol prefix). */\n text: string;\n /** Whether the row is a folder, a playable item, or not selectable. */\n kind: BrowseRowKind;\n /** Cover/thumbnail URL, where the protocol carries one (YXC only). */\n thumbnail?: string;\n}\n\n/** A snapshot of the device's current menu window. */\nexport interface BrowseWindow {\n /** The menu title (the current folder's name). */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** Total entries in the current menu. */\n totalItems: number;\n /**\n * The device's cursor position within the whole list (1-based) \u2014 that is what the\n * official YNCA command list defines CURRLINE as (\"absolute position number of current\n * cursor position\"), and the XML `Current_Line` matches it. It is NOT necessarily the\n * window's first line: after stepping back out of a submenu the cursor sits on the row\n * that was entered. The YXC driver has a real window index and reports that.\n */\n currentLine: number;\n /** The visible rows (up to 8). */\n rows: BrowseRow[];\n}\n\n/**\n * The per-transport list operations behind the engine. Every method may be async;\n * the driver reports fresh windows through the engine's `onWindow` (pull transports\n * fetch after each operation, YNCA also receives unsolicited auto-feedback).\n */\nexport interface BrowseDriver {\n /** The selectable sources as state-value \u2192 display-label entries. */\n sources(): Record<string, string>;\n /** Open a source's menu (the device keeps the session position). */\n open(source: string): Promise<void> | void;\n /** Select a visible line (1\u20138) \u2014 a folder opens, a playable item starts. */\n select(line: number): Promise<void> | void;\n /** Show the previous 8 lines. */\n pageUp(): Promise<void> | void;\n /** Show the next 8 lines. */\n pageDown(): Promise<void> | void;\n /** Go one menu level back. */\n back(): Promise<void> | void;\n /** Return to the menu root. */\n home(): Promise<void> | void;\n}\n\n/**\n * How the two text protocols name a row's kind. YNCA (`LINE1ATRIB`) and XML\n * (`Current_List` attribute) use the SAME four words \u2014 one table for both, so a new\n * attribute value is added once instead of in two drivers that then drift apart.\n */\nexport const ROW_KIND_BY_ATTRIBUTE: Readonly<Record<string, BrowseRowKind>> = {\n Container: \"folder\",\n Item: \"item\",\n \"Unplayable Item\": \"unplayable\",\n Unselectable: \"unselectable\",\n};\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/**\n * Shared contracts for the media-menu browsing surface (`player.browse.*`, issue #613).\n * One transport-neutral engine drives the states; each transport contributes a thin\n * {@link BrowseDriver} over its own list protocol (YNCA LISTINFO/LISTSEL, YXC\n * netusb getListInfo/setListControl, XML List_Info/List_Control). The surface mirrors\n * the device's own menu: an 8-line window, select-by-line, page/back/home, plus a\n * path auto-walk for scripts \u2014 the predecessor adapter's proven VIS workflow.\n */\n\n/** The kind of a menu row, as the device reports it. */\nexport type BrowseRowKind = \"folder\" | \"item\" | \"unplayable\" | \"unselectable\";\n\n/** One visible menu row. */\nexport interface BrowseRow {\n /** The row's line number within the window (1\u20138). */\n line: number;\n /** The row's raw text (no symbol prefix). */\n text: string;\n /** Whether the row is a folder, a playable item, or not selectable. */\n kind: BrowseRowKind;\n /** Cover/thumbnail URL, where the protocol carries one (YXC only). */\n thumbnail?: string;\n}\n\n/** A snapshot of the device's current menu window. */\nexport interface BrowseWindow {\n /** The menu title (the current folder's name). */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** Total entries in the current menu. */\n totalItems: number;\n /**\n * The device's cursor position within the whole list (1-based) \u2014 that is what the\n * official YNCA command list defines CURRLINE as (\"absolute position number of current\n * cursor position\"), and the XML `Current_Line` matches it. It is NOT necessarily the\n * window's first line: after stepping back out of a submenu the cursor sits on the row\n * that was entered. The YXC driver has a real window index and reports that.\n */\n currentLine: number;\n /** The visible rows (up to 8). */\n rows: BrowseRow[];\n}\n\n/**\n * The per-transport list operations behind the engine. Every method may be async;\n * the driver reports fresh windows through the engine's `onWindow` (pull transports\n * fetch after each operation, YNCA also receives unsolicited auto-feedback).\n */\nexport interface BrowseDriver {\n /** The selectable sources as state-value \u2192 display-label entries. */\n sources(): Record<string, string>;\n /** Open a source's menu (the device keeps the session position). */\n open(source: string): Promise<void> | void;\n /** Select a visible line (1\u20138) \u2014 a folder opens, a playable item starts. */\n select(line: number): Promise<void> | void;\n /** Show the previous 8 lines. */\n pageUp(): Promise<void> | void;\n /** Show the next 8 lines. */\n pageDown(): Promise<void> | void;\n /** Go one menu level back. */\n back(): Promise<void> | void;\n /** Return to the menu root. */\n home(): Promise<void> | void;\n /**\n * The cursor-pad values this transport supports, from {@link CURSOR_VALUES} \u2014 absent\n * where the protocol has no cursor pad. The device tree offers exactly this subset, so a\n * word that appears in the dropdown always works on that device.\n */\n cursorValues?: readonly string[];\n /**\n * Press a cursor key.\n *\n * @param value one of {@link cursorValues}\n */\n cursor?(value: string): Promise<void> | void;\n /** The menu keys this transport supports, from {@link MENU_VALUES} \u2014 absent where it has none. */\n menuValues?: readonly string[];\n /**\n * Press a menu key.\n *\n * @param value one of {@link menuValues}\n */\n menu?(value: string): Promise<void> | void;\n}\n\n/**\n * The on-screen remote, in ONE vocabulary for all three protocols.\n *\n * The words are MusicCast's, because that is the set `remote.cursor`/`remote.menu` have carried\n * since v1.x \u2014 a script that presses `select` today must keep working. YNCA and XML spell the\n * same keys differently on the wire (`Sel`, `Back to Home`, `Return to Home`); each driver\n * translates, so the datapoint means the same thing on a 2009 receiver and on a 2024 one.\n *\n * A transport publishes only the words it really has: the YNCA source subunits know no\n * Left/Right, and no XML menu key is documented at all. A missing word is left out of that\n * device's dropdown rather than mapped onto something that merely looks similar.\n */\nexport const CURSOR_VALUES = [\"up\", \"down\", \"left\", \"right\", \"select\", \"return\", \"home\"] as const;\n\n/** The menu keys, same vocabulary rule as {@link CURSOR_VALUES}. */\nexport const MENU_VALUES = [\"on_screen\", \"top_menu\", \"menu\", \"option\", \"display\", \"home\"] as const;\n\n/**\n * How the two text protocols name a row's kind. YNCA (`LINE1ATRIB`) and XML\n * (`Current_List` attribute) use the SAME four words \u2014 one table for both, so a new\n * attribute value is added once instead of in two drivers that then drift apart.\n */\nexport const ROW_KIND_BY_ATTRIBUTE: Readonly<Record<string, BrowseRowKind>> = {\n Container: \"folder\",\n Item: \"item\",\n \"Unplayable Item\": \"unplayable\",\n Unselectable: \"unselectable\",\n};\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkGO,MAAM,gBAAgB,CAAC,MAAM,QAAQ,QAAQ,SAAS,UAAU,UAAU,MAAM;AAGhF,MAAM,cAAc,CAAC,aAAa,YAAY,QAAQ,UAAU,WAAW,MAAM;AAOjF,MAAM,wBAAiE;AAAA,EAC5E,WAAW;AAAA,EACX,MAAM;AAAA,EACN,mBAAmB;AAAA,EACnB,cAAc;AAChB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -54,6 +54,15 @@ function parseXmlListInfo(xml) {
|
|
|
54
54
|
rows
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
const XML_CURSOR_WIRE = {
|
|
58
|
+
up: "Up",
|
|
59
|
+
down: "Down",
|
|
60
|
+
left: "Left",
|
|
61
|
+
right: "Right",
|
|
62
|
+
select: "Sel",
|
|
63
|
+
return: "Return",
|
|
64
|
+
home: "Return to Home"
|
|
65
|
+
};
|
|
57
66
|
class XmlBrowseDriver {
|
|
58
67
|
/**
|
|
59
68
|
* @param client the XML client slice (send + getXml)
|
|
@@ -68,13 +77,6 @@ class XmlBrowseDriver {
|
|
|
68
77
|
engine;
|
|
69
78
|
active;
|
|
70
79
|
lastTotal = 0;
|
|
71
|
-
/**
|
|
72
|
-
* The cursor value this device accepts for "one level back". The spec vocabulary is
|
|
73
|
-
* `Return`, but the 2012 generation (RX-V473 class) refuses it and accepts `Left`
|
|
74
|
-
* instead (user-measured on the predecessor adapter, #613). First refusal switches
|
|
75
|
-
* permanently for this device.
|
|
76
|
-
*/
|
|
77
|
-
backCursor = "Return";
|
|
78
80
|
/**
|
|
79
81
|
* Attach the engine that renders the windows (set after both are constructed).
|
|
80
82
|
*
|
|
@@ -118,23 +120,40 @@ class XmlBrowseDriver {
|
|
|
118
120
|
async pageDown() {
|
|
119
121
|
await this.jumpBy(8);
|
|
120
122
|
}
|
|
121
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Go one menu level back.
|
|
125
|
+
*
|
|
126
|
+
* Always `Return`, never a learned substitute (krobi 2026-09-03): a refusal on ONE model is
|
|
127
|
+
* no reason to change the word for every other model, and a transport error is not a refusal
|
|
128
|
+
* at all. Where a receiver of the 2012 generation rejects it (#613), the user reaches the same
|
|
129
|
+
* step through `remote.cursor` = `left`, which that generation accepts.
|
|
130
|
+
*/
|
|
122
131
|
async back() {
|
|
123
|
-
|
|
124
|
-
await this.send(`<Cursor>${this.backCursor}</Cursor>`);
|
|
125
|
-
} catch (e) {
|
|
126
|
-
if (this.backCursor !== "Return") {
|
|
127
|
-
throw e;
|
|
128
|
-
}
|
|
129
|
-
this.backCursor = "Left";
|
|
130
|
-
await this.send("<Cursor>Left</Cursor>");
|
|
131
|
-
}
|
|
132
|
-
await this.fetch();
|
|
132
|
+
await this.control("<Cursor>Return</Cursor>");
|
|
133
133
|
}
|
|
134
134
|
/** Return to the menu root. */
|
|
135
135
|
async home() {
|
|
136
136
|
await this.control("<Cursor>Return to Home</Cursor>");
|
|
137
137
|
}
|
|
138
|
+
/** The cursor keys this protocol declares — the full pad, `<List_Control><Cursor>`. */
|
|
139
|
+
cursorValues = Object.keys(XML_CURSOR_WIRE);
|
|
140
|
+
/**
|
|
141
|
+
* Press a cursor key on the open list.
|
|
142
|
+
*
|
|
143
|
+
* XML/YNC knows the cursor ONLY inside `List_Control`, addressed to the source whose menu
|
|
144
|
+
* is open (`PUT <NET_RADIO><List_Control>…`) — this generation has no zone-wide remote
|
|
145
|
+
* endpoint, and inventing one would put a blind command on the wire. With no menu open the
|
|
146
|
+
* press therefore goes nowhere, which `send()` already handles.
|
|
147
|
+
*
|
|
148
|
+
* @param value one of {@link cursorValues}
|
|
149
|
+
*/
|
|
150
|
+
async cursor(value) {
|
|
151
|
+
const wire = XML_CURSOR_WIRE[value];
|
|
152
|
+
if (wire === void 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await this.control(`<Cursor>${wire}</Cursor>`);
|
|
156
|
+
}
|
|
138
157
|
/**
|
|
139
158
|
* Send a List_Control command to the active source — WITHOUT reading the window back,
|
|
140
159
|
* so a caller can tell a device refusal from a failed follow-up read (see {@link back}).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/browse/xml-browse-driver.ts"],
|
|
4
|
-
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\nimport { decodeXmlText } from \"../xml/entities\";\n\n/** How often to re-read while the device reports Menu_Status Busy. */\nconst BUSY_POLL_MS = 500;\n/** How many busy polls before giving up on one read. */\nconst MAX_BUSY_POLLS = 10;\n\n/**\n * The XML/YNC sources with a List_Info menu (the ones the predecessor adapter's\n * users browsed; rxv drives the same three), with the transport-neutral source key\n * and the input name that activates the source.\n */\nexport const XML_BROWSE_SOURCES: ReadonlyArray<{ element: string; key: string; label: string; input: string }> = [\n { element: \"NET_RADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { element: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { element: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n];\n\n/** The client surface the driver needs (a slice of the XML client). */\nexport interface XmlBrowseClient {\n /** Send an inner PUT command to an element (a zone or a source). */\n send(element: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** A parsed List_Info response. */\nexport interface XmlListInfo {\n /** Whether the menu is ready (false = the device is still fetching it). */\n ready: boolean;\n /** The menu title. */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** The cursor's absolute line number. */\n currentLine: number;\n /** Total entries in the menu. */\n totalItems: number;\n /** The visible rows. */\n rows: BrowseRow[];\n}\n\n/**\n * Parse a `<List_Info>` response (menu status, layer, name, cursor, and the eight\n * `Current_List` lines with text + attribute \u2014 the shape rxv reads).\n *\n * @param xml the List_Info response body\n * @returns the parsed window\n */\nexport function parseXmlListInfo(xml: string): XmlListInfo {\n const rows: BrowseRow[] = [];\n const linePattern = /<Line_([1-8])>\\s*<Txt>([^<]*)<\\/Txt>\\s*<Attribute>([^<]*)<\\/Attribute>/g;\n for (let match = linePattern.exec(xml); match; match = linePattern.exec(xml)) {\n if (match[2].length > 0) {\n rows.push({\n line: Number(match[1]),\n text: decodeXmlText(match[2]),\n kind: ROW_KIND_BY_ATTRIBUTE[match[3]] ?? \"item\",\n });\n }\n }\n return {\n ready: !/<Menu_Status>Busy<\\/Menu_Status>/.test(xml),\n menuName: decodeXmlText(/<Menu_Name>([^<]*)<\\/Menu_Name>/.exec(xml)?.[1] ?? \"\"),\n layer: Number(/<Menu_Layer>(\\d+)<\\/Menu_Layer>/.exec(xml)?.[1] ?? 0),\n currentLine: Number(/<Current_Line>(\\d+)<\\/Current_Line>/.exec(xml)?.[1] ?? 1),\n totalItems: Number(/<Max_Line>(\\d+)<\\/Max_Line>/.exec(xml)?.[1] ?? 0),\n rows,\n };\n}\n\n/**\n * The XML/YNC list driver over `<List_Info>` + `<List_Control>` (the predecessor\n * adapter's browsing path). Pull-based: every operation re-reads the window, polling\n * while the device reports the menu as busy (levels come from the catalog service).\n * The cursor line stands in for the window start \u2014 the same approximation rxv makes.\n */\nexport class XmlBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { element: string; key: string; input: string } | undefined;\n private lastTotal = 0;\n /**\n *
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyE;AAEzE,sBAA8B;AAG9B,MAAM,eAAe;AAErB,MAAM,iBAAiB;AAOhB,MAAM,qBAAoG;AAAA,EAC/G,EAAE,SAAS,aAAa,KAAK,YAAY,OAAO,aAAa,OAAO,YAAY;AAAA,EAChF,EAAE,SAAS,UAAU,KAAK,UAAU,OAAO,gBAAgB,OAAO,SAAS;AAAA,EAC3E,EAAE,SAAS,OAAO,KAAK,OAAO,OAAO,OAAO,OAAO,MAAM;AAC3D;AAiCO,SAAS,iBAAiB,KAA0B;AAnD3D;AAoDE,QAAM,OAAoB,CAAC;AAC3B,QAAM,cAAc;AACpB,WAAS,QAAQ,YAAY,KAAK,GAAG,GAAG,OAAO,QAAQ,YAAY,KAAK,GAAG,GAAG;AAC5E,QAAI,MAAM,CAAC,EAAE,SAAS,GAAG;AACvB,WAAK,KAAK;AAAA,QACR,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA,QACrB,UAAM,+BAAc,MAAM,CAAC,CAAC;AAAA,QAC5B,OAAM,wCAAsB,MAAM,CAAC,CAAC,MAA9B,YAAmC;AAAA,MAC3C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO,CAAC,mCAAmC,KAAK,GAAG;AAAA,IACnD,cAAU,gCAAc,6CAAkC,KAAK,GAAG,MAA1C,mBAA8C,OAA9C,YAAoD,EAAE;AAAA,IAC9E,OAAO,QAAO,6CAAkC,KAAK,GAAG,MAA1C,mBAA8C,OAA9C,YAAoD,CAAC;AAAA,IACnE,aAAa,QAAO,iDAAsC,KAAK,GAAG,MAA9C,mBAAkD,OAAlD,YAAwD,CAAC;AAAA,IAC7E,YAAY,QAAO,yCAA8B,KAAK,GAAG,MAAtC,mBAA0C,OAA1C,YAAgD,CAAC;AAAA,IACpE;AAAA,EACF;AACF;AAQO,MAAM,gBAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { ROW_KIND_BY_ATTRIBUTE, type BrowseDriver, type BrowseRow } from \"./types\";\nimport type { BrowseEngine } from \"./browse-engine\";\nimport { decodeXmlText } from \"../xml/entities\";\n\n/** How often to re-read while the device reports Menu_Status Busy. */\nconst BUSY_POLL_MS = 500;\n/** How many busy polls before giving up on one read. */\nconst MAX_BUSY_POLLS = 10;\n\n/**\n * The XML/YNC sources with a List_Info menu (the ones the predecessor adapter's\n * users browsed; rxv drives the same three), with the transport-neutral source key\n * and the input name that activates the source.\n */\nexport const XML_BROWSE_SOURCES: ReadonlyArray<{ element: string; key: string; label: string; input: string }> = [\n { element: \"NET_RADIO\", key: \"netRadio\", label: \"Net Radio\", input: \"NET RADIO\" },\n { element: \"SERVER\", key: \"server\", label: \"Media server\", input: \"SERVER\" },\n { element: \"USB\", key: \"usb\", label: \"USB\", input: \"USB\" },\n];\n\n/** The client surface the driver needs (a slice of the XML client). */\nexport interface XmlBrowseClient {\n /** Send an inner PUT command to an element (a zone or a source). */\n send(element: string, inner: string): Promise<void>;\n /** Read an element's inner GET request and return the raw response body. */\n getXml(element: string, inner: string): Promise<string>;\n}\n\n/** A parsed List_Info response. */\nexport interface XmlListInfo {\n /** Whether the menu is ready (false = the device is still fetching it). */\n ready: boolean;\n /** The menu title. */\n menuName: string;\n /** The menu depth (1 = root). */\n layer: number;\n /** The cursor's absolute line number. */\n currentLine: number;\n /** Total entries in the menu. */\n totalItems: number;\n /** The visible rows. */\n rows: BrowseRow[];\n}\n\n/**\n * Parse a `<List_Info>` response (menu status, layer, name, cursor, and the eight\n * `Current_List` lines with text + attribute \u2014 the shape rxv reads).\n *\n * @param xml the List_Info response body\n * @returns the parsed window\n */\nexport function parseXmlListInfo(xml: string): XmlListInfo {\n const rows: BrowseRow[] = [];\n const linePattern = /<Line_([1-8])>\\s*<Txt>([^<]*)<\\/Txt>\\s*<Attribute>([^<]*)<\\/Attribute>/g;\n for (let match = linePattern.exec(xml); match; match = linePattern.exec(xml)) {\n if (match[2].length > 0) {\n rows.push({\n line: Number(match[1]),\n text: decodeXmlText(match[2]),\n kind: ROW_KIND_BY_ATTRIBUTE[match[3]] ?? \"item\",\n });\n }\n }\n return {\n ready: !/<Menu_Status>Busy<\\/Menu_Status>/.test(xml),\n menuName: decodeXmlText(/<Menu_Name>([^<]*)<\\/Menu_Name>/.exec(xml)?.[1] ?? \"\"),\n layer: Number(/<Menu_Layer>(\\d+)<\\/Menu_Layer>/.exec(xml)?.[1] ?? 0),\n currentLine: Number(/<Current_Line>(\\d+)<\\/Current_Line>/.exec(xml)?.[1] ?? 1),\n totalItems: Number(/<Max_Line>(\\d+)<\\/Max_Line>/.exec(xml)?.[1] ?? 0),\n rows,\n };\n}\n\n/**\n * The pad in XML wire words. Same seven keys as YNCA's main zone, two of them spelled\n * differently (`Return`, `Return to Home`). There is no documented menu-key command on this\n * generation, so the driver offers no `remote.menu`.\n */\nconst XML_CURSOR_WIRE: Record<string, string> = {\n up: \"Up\",\n down: \"Down\",\n left: \"Left\",\n right: \"Right\",\n select: \"Sel\",\n return: \"Return\",\n home: \"Return to Home\",\n};\n\n/**\n * The XML/YNC list driver over `<List_Info>` + `<List_Control>` (the predecessor\n * adapter's browsing path). Pull-based: every operation re-reads the window, polling\n * while the device reports the menu as busy (levels come from the catalog service).\n * The cursor line stands in for the window start \u2014 the same approximation rxv makes.\n */\nexport class XmlBrowseDriver implements BrowseDriver {\n private engine: BrowseEngine | undefined;\n private active: { element: string; key: string; input: string } | undefined;\n private lastTotal = 0;\n /**\n * @param client the XML client slice (send + getXml)\n * @param available the source keys whose List_Info the start-up probe answered\n * @param delay adapter-managed delay\n */\n public constructor(\n private readonly client: XmlBrowseClient,\n private readonly available: ReadonlySet<string>,\n private readonly delay: (ms: number) => Promise<void>,\n ) {}\n\n /**\n * Attach the engine that renders the windows (set after both are constructed).\n *\n * @param engine the browse engine\n */\n public attach(engine: BrowseEngine): void {\n this.engine = engine;\n }\n\n /** @returns the selectable sources this device offers (state value \u2192 label) */\n public sources(): Record<string, string> {\n const entries = XML_BROWSE_SOURCES.filter(source => this.available.has(source.key));\n return Object.fromEntries(entries.map(source => [source.key, source.label]));\n }\n\n /**\n * Open a source's menu: switch the main-zone input to it and read the window.\n *\n * @param source the source key (from {@link sources})\n */\n public async open(source: string): Promise<void> {\n const entry = XML_BROWSE_SOURCES.find(s => s.key === source && this.available.has(s.key));\n if (!entry) {\n return;\n }\n this.active = entry;\n await this.client.send(\"Main_Zone\", `<Input><Input_Sel>${entry.input}</Input_Sel></Input>`);\n await this.fetch();\n }\n\n /**\n * Select a visible line via Direct_Sel \u2014 a folder opens, a playable item starts.\n *\n * @param line the line number (1\u20138)\n */\n public async select(line: number): Promise<void> {\n await this.control(`<Direct_Sel>Line_${line}</Direct_Sel>`);\n }\n\n /** Show the previous 8 lines (jump the cursor back a page). */\n public async pageUp(): Promise<void> {\n await this.jumpBy(-8);\n }\n\n /** Show the next 8 lines (jump the cursor forward a page). */\n public async pageDown(): Promise<void> {\n await this.jumpBy(8);\n }\n\n /**\n * Go one menu level back.\n *\n * Always `Return`, never a learned substitute (krobi 2026-09-03): a refusal on ONE model is\n * no reason to change the word for every other model, and a transport error is not a refusal\n * at all. Where a receiver of the 2012 generation rejects it (#613), the user reaches the same\n * step through `remote.cursor` = `left`, which that generation accepts.\n */\n public async back(): Promise<void> {\n await this.control(\"<Cursor>Return</Cursor>\");\n }\n\n /** Return to the menu root. */\n public async home(): Promise<void> {\n await this.control(\"<Cursor>Return to Home</Cursor>\");\n }\n\n /** The cursor keys this protocol declares \u2014 the full pad, `<List_Control><Cursor>`. */\n public readonly cursorValues = Object.keys(XML_CURSOR_WIRE);\n\n /**\n * Press a cursor key on the open list.\n *\n * XML/YNC knows the cursor ONLY inside `List_Control`, addressed to the source whose menu\n * is open (`PUT <NET_RADIO><List_Control>\u2026`) \u2014 this generation has no zone-wide remote\n * endpoint, and inventing one would put a blind command on the wire. With no menu open the\n * press therefore goes nowhere, which `send()` already handles.\n *\n * @param value one of {@link cursorValues}\n */\n public async cursor(value: string): Promise<void> {\n const wire = XML_CURSOR_WIRE[value];\n if (wire === undefined) {\n return;\n }\n await this.control(`<Cursor>${wire}</Cursor>`);\n }\n\n /**\n * Send a List_Control command to the active source \u2014 WITHOUT reading the window back,\n * so a caller can tell a device refusal from a failed follow-up read (see {@link back}).\n *\n * @param inner the List_Control payload (Direct_Sel, Cursor, Jump_Line)\n */\n private async send(inner: string): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.client.send(this.active.element, `<List_Control>${inner}</List_Control>`);\n }\n\n /**\n * Send a List_Control command to the active source and read the window back.\n *\n * @param inner the List_Control payload (Direct_Sel, Cursor, Jump_Line)\n */\n private async control(inner: string): Promise<void> {\n if (!this.active) {\n return;\n }\n await this.send(inner);\n await this.fetch();\n }\n\n /**\n * Jump the cursor by a page, clamped to the menu bounds.\n *\n * @param delta the line offset (\u00B18)\n */\n private async jumpBy(delta: number): Promise<void> {\n if (!this.active) {\n return;\n }\n const current = await this.readWindow();\n if (!current) {\n return;\n }\n const target = Math.min(Math.max(1, current.currentLine + delta), Math.max(1, this.lastTotal));\n await this.control(`<Jump_Line>${target}</Jump_Line>`);\n }\n\n /** Read the window (polling while busy) and render it to the engine. */\n private async fetch(): Promise<void> {\n const window = await this.readWindow();\n if (window) {\n this.lastTotal = window.totalItems;\n this.engine?.onWindow({\n menuName: window.menuName,\n layer: window.layer,\n totalItems: window.totalItems,\n currentLine: window.currentLine,\n rows: window.rows,\n });\n }\n }\n\n /**\n * Read the active source's List_Info, polling while the device reports Busy.\n *\n * @returns the parsed window, or undefined without an active source / when busy persists\n */\n private async readWindow(): Promise<XmlListInfo | undefined> {\n if (!this.active) {\n return undefined;\n }\n for (let attempt = 0; attempt < MAX_BUSY_POLLS; attempt++) {\n const info = parseXmlListInfo(await this.client.getXml(this.active.element, \"<List_Info>GetParam</List_Info>\"));\n if (info.ready) {\n return info;\n }\n await this.delay(BUSY_POLL_MS);\n }\n return undefined;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|