iobroker.yamaha 2.3.2 → 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 +12 -21
- package/admin/i18n/de.json +25 -1
- package/admin/i18n/en.json +25 -1
- package/admin/i18n/es.json +25 -1
- package/admin/i18n/fr.json +25 -1
- package/admin/i18n/it.json +25 -1
- package/admin/i18n/nl.json +25 -1
- package/admin/i18n/pl.json +25 -1
- package/admin/i18n/pt.json +25 -1
- package/admin/i18n/ru.json +25 -1
- package/admin/i18n/uk.json +25 -1
- package/admin/i18n/zh-cn.json +25 -1
- 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 +98 -8
- 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 +24 -6
- package/build/lib/device-controller.js.map +2 -2
- package/build/lib/xml/device-controller.js +14 -3
- package/build/lib/xml/device-controller.js.map +2 -2
- package/build/lib/yxc/device-controller.js +8 -1
- package/build/lib/yxc/device-controller.js.map +2 -2
- package/build/lib/yxc/object-mapper.js +105 -13
- package/build/lib/yxc/object-mapper.js.map +2 -2
- package/build/main.js +1 -1
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- 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,15 @@ 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
|
+
|
|
111
|
+
### 2.3.3 (2026-09-03)
|
|
112
|
+
|
|
113
|
+
- (krobipd) New: the menu browser, the MusicCast group, the clock alarm and the disc drive now explain their datapoints too, in eleven languages
|
|
114
|
+
|
|
106
115
|
### 2.3.2 (2026-09-03)
|
|
107
116
|
|
|
108
117
|
- (krobipd) Fixed: eight tuner and scene datapoints still showed no explanation on MusicCast and older XML receivers
|
|
@@ -116,24 +125,6 @@ On the very first contact the adapter asks the receiver which functions it suppo
|
|
|
116
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
|
|
117
126
|
- (krobipd) Improved: a text that has no translation yet falls back to readable English instead of showing an internal key
|
|
118
127
|
|
|
119
|
-
### 2.2.0 (2026-09-03)
|
|
120
|
-
|
|
121
|
-
- (krobipd) Changed: the playback times are now a number in seconds, so the media player, Alexa and Google can show them on every receiver
|
|
122
|
-
- (krobipd) New: the readable playback times ("1:23") moved to their own datapoints, next to the seconds — bind whichever form your visualization needs
|
|
123
|
-
- (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
|
|
124
|
-
- (krobipd) Fixed: setting a tuner frequency on a MusicCast device never reached it — the command was incomplete
|
|
125
|
-
- (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
|
|
126
|
-
- (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
|
|
127
|
-
- (krobipd) Fixed: after a restart the tuner of an older receiver no longer shows the station of the previous session until the next poll
|
|
128
|
-
- (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
|
|
129
|
-
- (krobipd) Fixed: going one menu level back on an older receiver no longer jumps two levels when the reply gets lost
|
|
130
|
-
- (krobipd) Fixed: a play or pause sent from a script or a visualization now reaches the receiver in every case
|
|
131
|
-
- (krobipd) Improved: each of the 23 input-name datapoints now carries the input it names, instead of all reading the same
|
|
132
|
-
|
|
133
|
-
### 2.1.1 (2026-09-02)
|
|
134
|
-
|
|
135
|
-
- (krobipd) Fixed: on an updated instance the info datapoints now get the translated names too, instead of keeping the ones an older version had written
|
|
136
|
-
|
|
137
128
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
138
129
|
|
|
139
130
|
## History
|
package/admin/i18n/de.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Ob dies der Hauptdienst des Senders ist oder ein Zusatzdienst.",
|
|
392
392
|
"descTotalStations": "Wie viele Digitalsender der letzte Suchlauf gefunden hat.",
|
|
393
393
|
"descInitialScanProgress": "Wie weit der erste Sendersuchlauf ist, in Prozent.",
|
|
394
|
-
"descTuneAidLevel": "Empfangsstärke — als Hilfe beim Ausrichten der Antenne gedacht."
|
|
394
|
+
"descTuneAidLevel": "Empfangsstärke — als Hilfe beim Ausrichten der Antenne gedacht.",
|
|
395
|
+
"descTransports": "Welche der drei Yamaha-Protokolle gerade mit diesem Gerät sprechen.",
|
|
396
|
+
"descPlayingSource": "Die Quelle, aus der wirklich abgespielt wird — die kann vom gewählten Eingang abweichen.",
|
|
397
|
+
"descScenesNumberTitle": "Die Szenen, die das Gerät meldet — mit Nummer und dem dort angezeigten Namen.",
|
|
398
|
+
"descMenuName": "Titel der Menüebene, die die Netzwerkquelle gerade zeigt.",
|
|
399
|
+
"descMenuLevel": "Wie tief Sie gerade im Menü stehen; 0 ist die oberste Ebene.",
|
|
400
|
+
"descTotalEntries": "Wie viele Einträge die aktuelle Menüebene insgesamt hat.",
|
|
401
|
+
"descCurrentLine": "Welcher Eintrag im aktuellen Menüfenster hervorgehoben ist.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Wirkt wie die OK-Taste: ein Ordner öffnet sich, ein Titel startet.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Einen Pfad hineinschreiben, und der Adapter läuft den Weg im Menü selbst ab.",
|
|
404
|
+
"descRowsJSON": "Das ganze aktuelle Menüfenster in einem Datenpunkt, für Skripte.",
|
|
405
|
+
"descBusy": "Wahr, solange das Gerät eine Menü-Anfrage noch beantwortet.",
|
|
406
|
+
"descCursorPad": "Die Richtungstasten des Bildschirmmenüs.",
|
|
407
|
+
"descMenuKey": "Öffnet oder schließt das Bildschirmmenü des Receivers.",
|
|
408
|
+
"descLeaveGroup": "Nimmt dieses Gerät aus seiner MusicCast-Gruppe heraus.",
|
|
409
|
+
"descLinkADeviceItsIP": "Die Adresse eines anderen MusicCast-Geräts hineinschreiben, um es der Gruppe hinzuzufügen.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Eine Position aus der Liste der zuletzt gespielten Titel hineinschreiben, um sie erneut zu starten.",
|
|
411
|
+
"descAutomaticTimeSync": "Lässt das Gerät seine Uhr selbst aus dem Netz stellen.",
|
|
412
|
+
"descClockFormat": "Ob das Gerät die Zeit im 12- oder 24-Stunden-Format zeigt.",
|
|
413
|
+
"descClockAlarm": "Der eingebaute Weckruf des Geräts.",
|
|
414
|
+
"descRepeatSnooze": "Wie lange der Weckruf wartet, bevor er erneut ertönt.",
|
|
415
|
+
"descFadeType": "Wie der Weckruf beim Start lauter wird.",
|
|
416
|
+
"descFadeInTime": "Wie lange der Weckruf braucht, bis er die volle Lautstärke erreicht.",
|
|
417
|
+
"descDriveStatus": "Was das Laufwerk gerade tut.",
|
|
418
|
+
"descToggleTray": "Öffnet oder schließt die Laufwerksschublade."
|
|
395
419
|
}
|
package/admin/i18n/en.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Whether this is the station's main service or a secondary one.",
|
|
392
392
|
"descTotalStations": "How many digital stations the last scan found.",
|
|
393
393
|
"descInitialScanProgress": "How far the first station scan has got, as a percentage.",
|
|
394
|
-
"descTuneAidLevel": "Reception strength, meant as an aid while aiming the aerial."
|
|
394
|
+
"descTuneAidLevel": "Reception strength, meant as an aid while aiming the aerial.",
|
|
395
|
+
"descTransports": "Which of the three Yamaha protocols are talking to this device right now.",
|
|
396
|
+
"descPlayingSource": "The source the receiver is actually playing from, which can differ from the selected input.",
|
|
397
|
+
"descScenesNumberTitle": "The scenes the device declares, with their number and the name shown on it.",
|
|
398
|
+
"descMenuName": "Title of the menu level currently shown by the network source.",
|
|
399
|
+
"descMenuLevel": "How deep in the menu you currently are; 0 is the top.",
|
|
400
|
+
"descTotalEntries": "How many entries the current menu level holds in total.",
|
|
401
|
+
"descCurrentLine": "Which entry of the current menu window is highlighted.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Acts like the OK key: a folder opens, an item starts playing.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Write a path and the adapter walks the menu there by itself.",
|
|
404
|
+
"descRowsJSON": "The whole current menu window in one datapoint, for scripts.",
|
|
405
|
+
"descBusy": "True while the device is still answering a menu request.",
|
|
406
|
+
"descCursorPad": "The direction keys of the on-screen menu.",
|
|
407
|
+
"descMenuKey": "Opens or closes the receiver's on-screen menu.",
|
|
408
|
+
"descLeaveGroup": "Takes this device out of its MusicCast group.",
|
|
409
|
+
"descLinkADeviceItsIP": "Write another MusicCast device's address to add it to this group.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Write a position from the recently played list to start it again.",
|
|
411
|
+
"descAutomaticTimeSync": "Lets the device set its clock from the network by itself.",
|
|
412
|
+
"descClockFormat": "Whether the device shows the time in 12- or 24-hour form.",
|
|
413
|
+
"descClockAlarm": "The device's own wake-up alarm.",
|
|
414
|
+
"descRepeatSnooze": "How long the alarm waits before sounding again.",
|
|
415
|
+
"descFadeType": "How the alarm gets louder when it starts.",
|
|
416
|
+
"descFadeInTime": "How long the alarm takes to reach its full volume.",
|
|
417
|
+
"descDriveStatus": "What the disc drive is doing right now.",
|
|
418
|
+
"descToggleTray": "Opens or closes the disc tray."
|
|
395
419
|
}
|
package/admin/i18n/es.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Si este es el servicio principal de la emisora o uno secundario.",
|
|
392
392
|
"descTotalStations": "Cuántas emisoras digitales encontró la última búsqueda.",
|
|
393
393
|
"descInitialScanProgress": "Hasta dónde llegó la primera búsqueda de emisoras, en porcentaje.",
|
|
394
|
-
"descTuneAidLevel": "Intensidad de recepción, como ayuda al orientar la antena."
|
|
394
|
+
"descTuneAidLevel": "Intensidad de recepción, como ayuda al orientar la antena.",
|
|
395
|
+
"descTransports": "Cuáles de los tres protocolos Yamaha hablan ahora con el aparato.",
|
|
396
|
+
"descPlayingSource": "La fuente de la que realmente se reproduce, que puede diferir de la entrada elegida.",
|
|
397
|
+
"descScenesNumberTitle": "Las escenas que declara el aparato, con número y el nombre mostrado.",
|
|
398
|
+
"descMenuName": "Título del nivel de menú que muestra la fuente de red.",
|
|
399
|
+
"descMenuLevel": "A qué profundidad está en el menú; 0 es el nivel superior.",
|
|
400
|
+
"descTotalEntries": "Cuántas entradas tiene en total el nivel de menú actual.",
|
|
401
|
+
"descCurrentLine": "Qué entrada de la ventana de menú está resaltada.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Actúa como la tecla OK: una carpeta se abre, un tema empieza.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Escriba una ruta y el adaptador recorre el menú por sí mismo.",
|
|
404
|
+
"descRowsJSON": "Toda la ventana de menú actual en un punto de datos, para scripts.",
|
|
405
|
+
"descBusy": "Verdadero mientras el aparato aún responde a una petición de menú.",
|
|
406
|
+
"descCursorPad": "Las teclas de dirección del menú en pantalla.",
|
|
407
|
+
"descMenuKey": "Abre o cierra el menú en pantalla del receptor.",
|
|
408
|
+
"descLeaveGroup": "Saca este aparato de su grupo MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Escriba la dirección de otro aparato MusicCast para añadirlo al grupo.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Escriba una posición de la lista de reproducidos recientemente para reiniciarla.",
|
|
411
|
+
"descAutomaticTimeSync": "Deja que el aparato ponga su reloj desde la red.",
|
|
412
|
+
"descClockFormat": "Si el aparato muestra la hora en formato de 12 o 24 horas.",
|
|
413
|
+
"descClockAlarm": "La alarma de despertador del propio aparato.",
|
|
414
|
+
"descRepeatSnooze": "Cuánto espera la alarma antes de volver a sonar.",
|
|
415
|
+
"descFadeType": "Cómo sube el volumen la alarma al empezar.",
|
|
416
|
+
"descFadeInTime": "Cuánto tarda la alarma en alcanzar el volumen completo.",
|
|
417
|
+
"descDriveStatus": "Qué está haciendo ahora la unidad de disco.",
|
|
418
|
+
"descToggleTray": "Abre o cierra la bandeja del disco."
|
|
395
419
|
}
|
package/admin/i18n/fr.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "S'il s'agit du service principal de la station ou d'un service secondaire.",
|
|
392
392
|
"descTotalStations": "Combien de stations numériques le dernier balayage a trouvées.",
|
|
393
393
|
"descInitialScanProgress": "Où en est le premier balayage des stations, en pourcentage.",
|
|
394
|
-
"descTuneAidLevel": "Force de réception, en aide au pointage de l'antenne."
|
|
394
|
+
"descTuneAidLevel": "Force de réception, en aide au pointage de l'antenne.",
|
|
395
|
+
"descTransports": "Lesquels des trois protocoles Yamaha dialoguent actuellement avec l'appareil.",
|
|
396
|
+
"descPlayingSource": "La source réellement lue, qui peut différer de l'entrée sélectionnée.",
|
|
397
|
+
"descScenesNumberTitle": "Les scènes déclarées par l'appareil, avec leur numéro et le nom affiché.",
|
|
398
|
+
"descMenuName": "Titre du niveau de menu affiché par la source réseau.",
|
|
399
|
+
"descMenuLevel": "À quelle profondeur du menu vous êtes ; 0 est le sommet.",
|
|
400
|
+
"descTotalEntries": "Combien d'entrées compte le niveau de menu actuel.",
|
|
401
|
+
"descCurrentLine": "Quelle entrée de la fenêtre de menu est sélectionnée.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Agit comme la touche OK : un dossier s'ouvre, un titre démarre.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Écrivez un chemin et l'adaptateur parcourt le menu tout seul.",
|
|
404
|
+
"descRowsJSON": "Toute la fenêtre de menu actuelle en un point de données, pour les scripts.",
|
|
405
|
+
"descBusy": "Vrai tant que l'appareil répond encore à une requête de menu.",
|
|
406
|
+
"descCursorPad": "Les touches de direction du menu à l'écran.",
|
|
407
|
+
"descMenuKey": "Ouvre ou ferme le menu à l'écran de l'ampli.",
|
|
408
|
+
"descLeaveGroup": "Retire cet appareil de son groupe MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Écrivez l'adresse d'un autre appareil MusicCast pour l'ajouter au groupe.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Écrivez une position de la liste des lectures récentes pour la relancer.",
|
|
411
|
+
"descAutomaticTimeSync": "Laisse l'appareil régler son horloge depuis le réseau.",
|
|
412
|
+
"descClockFormat": "Si l'appareil affiche l'heure au format 12 ou 24 heures.",
|
|
413
|
+
"descClockAlarm": "Le réveil intégré de l'appareil.",
|
|
414
|
+
"descRepeatSnooze": "Combien de temps le réveil attend avant de resonner.",
|
|
415
|
+
"descFadeType": "Comment le réveil monte en volume au démarrage.",
|
|
416
|
+
"descFadeInTime": "Le temps que met le réveil pour atteindre son volume maximal.",
|
|
417
|
+
"descDriveStatus": "Ce que fait le lecteur de disque en ce moment.",
|
|
418
|
+
"descToggleTray": "Ouvre ou ferme le tiroir de disque."
|
|
395
419
|
}
|
package/admin/i18n/it.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Se questo è il servizio principale della stazione o uno secondario.",
|
|
392
392
|
"descTotalStations": "Quante stazioni digitali ha trovato l'ultima ricerca.",
|
|
393
393
|
"descInitialScanProgress": "A che punto è la prima ricerca delle stazioni, in percentuale.",
|
|
394
|
-
"descTuneAidLevel": "Intensità di ricezione, come aiuto nell'orientare l'antenna."
|
|
394
|
+
"descTuneAidLevel": "Intensità di ricezione, come aiuto nell'orientare l'antenna.",
|
|
395
|
+
"descTransports": "Quali dei tre protocolli Yamaha stanno parlando con questo apparecchio.",
|
|
396
|
+
"descPlayingSource": "La sorgente da cui si riproduce davvero, che può differire dall'ingresso scelto.",
|
|
397
|
+
"descScenesNumberTitle": "Le scene dichiarate dall'apparecchio, con numero e nome mostrato.",
|
|
398
|
+
"descMenuName": "Titolo del livello di menu mostrato dalla sorgente di rete.",
|
|
399
|
+
"descMenuLevel": "Quanto in profondità siete nel menu; 0 è il livello più alto.",
|
|
400
|
+
"descTotalEntries": "Quante voci contiene in tutto il livello di menu attuale.",
|
|
401
|
+
"descCurrentLine": "Quale voce della finestra di menu è evidenziata.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Agisce come il tasto OK: una cartella si apre, un brano parte.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Scrivete un percorso e l'adattatore percorre il menu da solo.",
|
|
404
|
+
"descRowsJSON": "L'intera finestra di menu attuale in un solo datapoint, per gli script.",
|
|
405
|
+
"descBusy": "Vero finché l'apparecchio sta ancora rispondendo a una richiesta di menu.",
|
|
406
|
+
"descCursorPad": "I tasti direzionali del menu a schermo.",
|
|
407
|
+
"descMenuKey": "Apre o chiude il menu a schermo del ricevitore.",
|
|
408
|
+
"descLeaveGroup": "Toglie questo apparecchio dal suo gruppo MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Scrivete l'indirizzo di un altro apparecchio MusicCast per aggiungerlo al gruppo.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Scrivete una posizione dell'elenco dei brani recenti per riavviarla.",
|
|
411
|
+
"descAutomaticTimeSync": "Lascia che l'apparecchio regoli l'orologio dalla rete.",
|
|
412
|
+
"descClockFormat": "Se l'apparecchio mostra l'ora in formato 12 o 24 ore.",
|
|
413
|
+
"descClockAlarm": "La sveglia integrata dell'apparecchio.",
|
|
414
|
+
"descRepeatSnooze": "Quanto attende la sveglia prima di suonare di nuovo.",
|
|
415
|
+
"descFadeType": "Come la sveglia aumenta di volume all'avvio.",
|
|
416
|
+
"descFadeInTime": "Quanto impiega la sveglia a raggiungere il volume pieno.",
|
|
417
|
+
"descDriveStatus": "Cosa sta facendo ora il lettore dischi.",
|
|
418
|
+
"descToggleTray": "Apre o chiude il cassetto del disco."
|
|
395
419
|
}
|
package/admin/i18n/nl.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Of dit de hoofddienst van de zender is of een nevendienst.",
|
|
392
392
|
"descTotalStations": "Hoeveel digitale zenders de laatste zoekactie vond.",
|
|
393
393
|
"descInitialScanProgress": "Hoe ver de eerste zenderzoekactie is, in procent.",
|
|
394
|
-
"descTuneAidLevel": "Ontvangststerkte, als hulp bij het richten van de antenne."
|
|
394
|
+
"descTuneAidLevel": "Ontvangststerkte, als hulp bij het richten van de antenne.",
|
|
395
|
+
"descTransports": "Welke van de drie Yamaha-protocollen nu met dit apparaat praten.",
|
|
396
|
+
"descPlayingSource": "De bron waaruit echt wordt afgespeeld; die kan van de gekozen ingang afwijken.",
|
|
397
|
+
"descScenesNumberTitle": "De scènes die het apparaat meldt, met nummer en de daar getoonde naam.",
|
|
398
|
+
"descMenuName": "Titel van het menuniveau dat de netwerkbron nu toont.",
|
|
399
|
+
"descMenuLevel": "Hoe diep u nu in het menu zit; 0 is bovenaan.",
|
|
400
|
+
"descTotalEntries": "Hoeveel items het huidige menuniveau in totaal heeft.",
|
|
401
|
+
"descCurrentLine": "Welk item in het huidige menuvenster is gemarkeerd.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Werkt als de OK-toets: een map opent, een item start.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Schrijf een pad en de adapter loopt het menu zelf af.",
|
|
404
|
+
"descRowsJSON": "Het hele huidige menuvenster in één datapunt, voor scripts.",
|
|
405
|
+
"descBusy": "Waar zolang het apparaat nog op een menuverzoek antwoordt.",
|
|
406
|
+
"descCursorPad": "De richtingstoetsen van het schermmenu.",
|
|
407
|
+
"descMenuKey": "Opent of sluit het schermmenu van de receiver.",
|
|
408
|
+
"descLeaveGroup": "Haalt dit apparaat uit zijn MusicCast-groep.",
|
|
409
|
+
"descLinkADeviceItsIP": "Schrijf het adres van een ander MusicCast-apparaat om het toe te voegen.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Schrijf een positie uit de lijst van recent afgespeelde om die opnieuw te starten.",
|
|
411
|
+
"descAutomaticTimeSync": "Laat het apparaat zijn klok zelf via het netwerk zetten.",
|
|
412
|
+
"descClockFormat": "Of het apparaat de tijd in 12- of 24-uursvorm toont.",
|
|
413
|
+
"descClockAlarm": "Het eigen wekalarm van het apparaat.",
|
|
414
|
+
"descRepeatSnooze": "Hoe lang het alarm wacht voor het opnieuw afgaat.",
|
|
415
|
+
"descFadeType": "Hoe het alarm bij de start luider wordt.",
|
|
416
|
+
"descFadeInTime": "Hoe lang het alarm doet over het bereiken van vol volume.",
|
|
417
|
+
"descDriveStatus": "Wat de discspeler nu doet.",
|
|
418
|
+
"descToggleTray": "Opent of sluit de disclade."
|
|
395
419
|
}
|
package/admin/i18n/pl.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Czy to główna usługa stacji, czy dodatkowa.",
|
|
392
392
|
"descTotalStations": "Ile stacji cyfrowych znalazło ostatnie wyszukiwanie.",
|
|
393
393
|
"descInitialScanProgress": "Jak daleko zaszło pierwsze wyszukiwanie stacji, w procentach.",
|
|
394
|
-
"descTuneAidLevel": "Siła odbioru, jako pomoc przy ustawianiu anteny."
|
|
394
|
+
"descTuneAidLevel": "Siła odbioru, jako pomoc przy ustawianiu anteny.",
|
|
395
|
+
"descTransports": "Które z trzech protokołów Yamaha rozmawiają teraz z urządzeniem.",
|
|
396
|
+
"descPlayingSource": "Źródło, z którego faktycznie trwa odtwarzanie; może różnić się od wybranego wejścia.",
|
|
397
|
+
"descScenesNumberTitle": "Sceny zgłaszane przez urządzenie, z numerem i wyświetlaną nazwą.",
|
|
398
|
+
"descMenuName": "Tytuł poziomu menu pokazywanego przez źródło sieciowe.",
|
|
399
|
+
"descMenuLevel": "Jak głęboko jesteś w menu; 0 to najwyższy poziom.",
|
|
400
|
+
"descTotalEntries": "Ile pozycji ma łącznie bieżący poziom menu.",
|
|
401
|
+
"descCurrentLine": "Która pozycja bieżącego okna menu jest podświetlona.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Działa jak klawisz OK: folder się otwiera, utwór startuje.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Wpisz ścieżkę, a adapter sam przejdzie przez menu.",
|
|
404
|
+
"descRowsJSON": "Całe bieżące okno menu w jednym punkcie danych, dla skryptów.",
|
|
405
|
+
"descBusy": "Prawda, dopóki urządzenie wciąż odpowiada na żądanie menu.",
|
|
406
|
+
"descCursorPad": "Klawisze kierunkowe menu ekranowego.",
|
|
407
|
+
"descMenuKey": "Otwiera lub zamyka menu ekranowe amplitunera.",
|
|
408
|
+
"descLeaveGroup": "Usuwa to urządzenie z jego grupy MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Wpisz adres innego urządzenia MusicCast, aby dodać je do grupy.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Wpisz pozycję z listy ostatnio odtwarzanych, aby uruchomić ją ponownie.",
|
|
411
|
+
"descAutomaticTimeSync": "Pozwala urządzeniu samodzielnie ustawiać zegar z sieci.",
|
|
412
|
+
"descClockFormat": "Czy urządzenie pokazuje czas w formacie 12- czy 24-godzinnym.",
|
|
413
|
+
"descClockAlarm": "Wbudowany budzik urządzenia.",
|
|
414
|
+
"descRepeatSnooze": "Jak długo budzik czeka przed ponownym sygnałem.",
|
|
415
|
+
"descFadeType": "Jak budzik narasta na głośności przy starcie.",
|
|
416
|
+
"descFadeInTime": "Ile czasu budzik potrzebuje do pełnej głośności.",
|
|
417
|
+
"descDriveStatus": "Co robi teraz napęd płyt.",
|
|
418
|
+
"descToggleTray": "Otwiera lub zamyka szufladę napędu."
|
|
395
419
|
}
|
package/admin/i18n/pt.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Se este é o serviço principal da estação ou um secundário.",
|
|
392
392
|
"descTotalStations": "Quantas estações digitais a última busca encontrou.",
|
|
393
393
|
"descInitialScanProgress": "Até onde chegou a primeira busca de estações, em percentagem.",
|
|
394
|
-
"descTuneAidLevel": "Força de receção, como ajuda ao orientar a antena."
|
|
394
|
+
"descTuneAidLevel": "Força de receção, como ajuda ao orientar a antena.",
|
|
395
|
+
"descTransports": "Quais dos três protocolos Yamaha falam agora com este aparelho.",
|
|
396
|
+
"descPlayingSource": "A fonte de que se está mesmo a reproduzir, que pode diferir da entrada escolhida.",
|
|
397
|
+
"descScenesNumberTitle": "As cenas que o aparelho declara, com número e o nome nele mostrado.",
|
|
398
|
+
"descMenuName": "Título do nível de menu que a fonte de rede mostra agora.",
|
|
399
|
+
"descMenuLevel": "A que profundidade está no menu; 0 é o topo.",
|
|
400
|
+
"descTotalEntries": "Quantas entradas tem no total o nível de menu atual.",
|
|
401
|
+
"descCurrentLine": "Que entrada da janela de menu atual está realçada.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Age como a tecla OK: uma pasta abre, um item começa a tocar.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Escreva um caminho e o adaptador percorre o menu sozinho.",
|
|
404
|
+
"descRowsJSON": "Toda a janela de menu atual num só ponto de dados, para scripts.",
|
|
405
|
+
"descBusy": "Verdadeiro enquanto o aparelho ainda responde a um pedido de menu.",
|
|
406
|
+
"descCursorPad": "As teclas de direção do menu no ecrã.",
|
|
407
|
+
"descMenuKey": "Abre ou fecha o menu no ecrã do recetor.",
|
|
408
|
+
"descLeaveGroup": "Retira este aparelho do seu grupo MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Escreva o endereço de outro aparelho MusicCast para o juntar ao grupo.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Escreva uma posição da lista de reproduções recentes para a iniciar de novo.",
|
|
411
|
+
"descAutomaticTimeSync": "Deixa o aparelho acertar o relógio pela rede sozinho.",
|
|
412
|
+
"descClockFormat": "Se o aparelho mostra a hora em formato de 12 ou 24 horas.",
|
|
413
|
+
"descClockAlarm": "O alarme de despertar do próprio aparelho.",
|
|
414
|
+
"descRepeatSnooze": "Quanto tempo o alarme espera antes de tocar de novo.",
|
|
415
|
+
"descFadeType": "Como o alarme aumenta de volume ao começar.",
|
|
416
|
+
"descFadeInTime": "Quanto tempo o alarme leva a atingir o volume total.",
|
|
417
|
+
"descDriveStatus": "O que a unidade de disco está a fazer agora.",
|
|
418
|
+
"descToggleTray": "Abre ou fecha a gaveta do disco."
|
|
395
419
|
}
|
package/admin/i18n/ru.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Основная это служба станции или дополнительная.",
|
|
392
392
|
"descTotalStations": "Сколько цифровых станций нашёл последний поиск.",
|
|
393
393
|
"descInitialScanProgress": "Насколько продвинулся первый поиск станций, в процентах.",
|
|
394
|
-
"descTuneAidLevel": "Уровень приёма — в помощь при наведении антенны."
|
|
394
|
+
"descTuneAidLevel": "Уровень приёма — в помощь при наведении антенны.",
|
|
395
|
+
"descTransports": "Какие из трёх протоколов Yamaha сейчас общаются с устройством.",
|
|
396
|
+
"descPlayingSource": "Источник, из которого идёт воспроизведение; может отличаться от выбранного входа.",
|
|
397
|
+
"descScenesNumberTitle": "Сцены, о которых сообщает устройство: номер и отображаемое имя.",
|
|
398
|
+
"descMenuName": "Заголовок текущего уровня меню сетевого источника.",
|
|
399
|
+
"descMenuLevel": "Насколько глубоко вы сейчас в меню; 0 — верхний уровень.",
|
|
400
|
+
"descTotalEntries": "Сколько всего записей на текущем уровне меню.",
|
|
401
|
+
"descCurrentLine": "Какая запись текущего окна меню выделена.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Действует как кнопка OK: папка открывается, запись начинает играть.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Впишите путь — адаптер сам пройдёт по меню.",
|
|
404
|
+
"descRowsJSON": "Всё текущее окно меню в одном датапоинте — для скриптов.",
|
|
405
|
+
"descBusy": "Истина, пока устройство ещё отвечает на запрос меню.",
|
|
406
|
+
"descCursorPad": "Клавиши направления экранного меню.",
|
|
407
|
+
"descMenuKey": "Открывает или закрывает экранное меню ресивера.",
|
|
408
|
+
"descLeaveGroup": "Выводит устройство из его группы MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Впишите адрес другого устройства MusicCast, чтобы добавить его в группу.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Впишите позицию из списка недавно игравшего, чтобы запустить снова.",
|
|
411
|
+
"descAutomaticTimeSync": "Позволяет устройству самому выставлять часы из сети.",
|
|
412
|
+
"descClockFormat": "Показывает ли устройство время в 12- или 24-часовом формате.",
|
|
413
|
+
"descClockAlarm": "Собственный будильник устройства.",
|
|
414
|
+
"descRepeatSnooze": "Сколько будильник ждёт перед повторным сигналом.",
|
|
415
|
+
"descFadeType": "Как будильник нарастает по громкости при старте.",
|
|
416
|
+
"descFadeInTime": "За сколько будильник выходит на полную громкость.",
|
|
417
|
+
"descDriveStatus": "Что сейчас делает дисковод.",
|
|
418
|
+
"descToggleTray": "Открывает или закрывает лоток дисковода."
|
|
395
419
|
}
|
package/admin/i18n/uk.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "Чи це основна служба станції, чи додаткова.",
|
|
392
392
|
"descTotalStations": "Скільки цифрових станцій знайшов останній пошук.",
|
|
393
393
|
"descInitialScanProgress": "Наскільки просунувся перший пошук станцій, у відсотках.",
|
|
394
|
-
"descTuneAidLevel": "Сила прийому — на допомогу при наведенні антени."
|
|
394
|
+
"descTuneAidLevel": "Сила прийому — на допомогу при наведенні антени.",
|
|
395
|
+
"descTransports": "Які з трьох протоколів Yamaha зараз спілкуються з пристроєм.",
|
|
396
|
+
"descPlayingSource": "Джерело, з якого справді відтворюється; може відрізнятися від обраного входу.",
|
|
397
|
+
"descScenesNumberTitle": "Сцени, які повідомляє пристрій: номер і показувана назва.",
|
|
398
|
+
"descMenuName": "Заголовок рівня меню, який показує мережеве джерело.",
|
|
399
|
+
"descMenuLevel": "Наскільки глибоко ви в меню; 0 — верхній рівень.",
|
|
400
|
+
"descTotalEntries": "Скільки всього записів на поточному рівні меню.",
|
|
401
|
+
"descCurrentLine": "Який запис поточного вікна меню виділено.",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "Діє як клавіша OK: тека відкривається, запис починає грати.",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "Впишіть шлях — адаптер сам пройде меню.",
|
|
404
|
+
"descRowsJSON": "Усе поточне вікно меню в одному датапоінті — для скриптів.",
|
|
405
|
+
"descBusy": "Істина, доки пристрій ще відповідає на запит меню.",
|
|
406
|
+
"descCursorPad": "Клавіші напрямку екранного меню.",
|
|
407
|
+
"descMenuKey": "Відкриває або закриває екранне меню ресивера.",
|
|
408
|
+
"descLeaveGroup": "Виводить цей пристрій з його групи MusicCast.",
|
|
409
|
+
"descLinkADeviceItsIP": "Впишіть адресу іншого пристрою MusicCast, щоб додати його до групи.",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "Впишіть позицію зі списку нещодавно відтвореного, щоб запустити знову.",
|
|
411
|
+
"descAutomaticTimeSync": "Дозволяє пристрою самому виставляти годинник з мережі.",
|
|
412
|
+
"descClockFormat": "Чи показує пристрій час у 12- чи 24-годинному форматі.",
|
|
413
|
+
"descClockAlarm": "Власний будильник пристрою.",
|
|
414
|
+
"descRepeatSnooze": "Скільки будильник чекає перед повторним сигналом.",
|
|
415
|
+
"descFadeType": "Як будильник наростає за гучністю на старті.",
|
|
416
|
+
"descFadeInTime": "Скільки будильник виходить на повну гучність.",
|
|
417
|
+
"descDriveStatus": "Що зараз робить дисковод.",
|
|
418
|
+
"descToggleTray": "Відкриває або закриває лоток дисковода."
|
|
395
419
|
}
|
package/admin/i18n/zh-cn.json
CHANGED
|
@@ -391,5 +391,29 @@
|
|
|
391
391
|
"descServiceCategory": "这是电台的主服务还是附属服务。",
|
|
392
392
|
"descTotalStations": "上次搜台找到的数字电台数量。",
|
|
393
393
|
"descInitialScanProgress": "首次搜台的进度百分比。",
|
|
394
|
-
"descTuneAidLevel": "接收强度,用于辅助调整天线方向。"
|
|
394
|
+
"descTuneAidLevel": "接收强度,用于辅助调整天线方向。",
|
|
395
|
+
"descTransports": "当前有哪些 Yamaha 协议在与本设备通信。",
|
|
396
|
+
"descPlayingSource": "实际播放所用的音源,可能与所选输入不同。",
|
|
397
|
+
"descScenesNumberTitle": "设备声明的场景,含编号与其上显示的名称。",
|
|
398
|
+
"descMenuName": "网络音源当前显示的菜单层级标题。",
|
|
399
|
+
"descMenuLevel": "当前处于菜单的第几层;0 为最上层。",
|
|
400
|
+
"descTotalEntries": "当前菜单层级共有多少条目。",
|
|
401
|
+
"descCurrentLine": "当前菜单窗口中哪一条被选中。",
|
|
402
|
+
"descSelectLineFolderOpensItemPlays": "等同于 OK 键:文件夹会打开,条目会开始播放。",
|
|
403
|
+
"descNavigatePathEGBookmarksRadioParadise": "写入一个路径,适配器会自行在菜单中导航过去。",
|
|
404
|
+
"descRowsJSON": "把当前整个菜单窗口放在一个数据点里,便于脚本使用。",
|
|
405
|
+
"descBusy": "设备仍在响应菜单请求时为真。",
|
|
406
|
+
"descCursorPad": "屏幕菜单的方向键。",
|
|
407
|
+
"descMenuKey": "打开或关闭功放的屏幕菜单。",
|
|
408
|
+
"descLeaveGroup": "将本设备移出其 MusicCast 组。",
|
|
409
|
+
"descLinkADeviceItsIP": "写入另一台 MusicCast 设备的地址即可将其加入本组。",
|
|
410
|
+
"descRecallRecentlyPlayedNumber": "写入最近播放列表中的位置即可重新播放。",
|
|
411
|
+
"descAutomaticTimeSync": "让设备自行通过网络校准时钟。",
|
|
412
|
+
"descClockFormat": "设备以 12 小时还是 24 小时制显示时间。",
|
|
413
|
+
"descClockAlarm": "设备自带的闹钟。",
|
|
414
|
+
"descRepeatSnooze": "闹钟再次响起前等待多久。",
|
|
415
|
+
"descFadeType": "闹钟开始时如何逐渐增大音量。",
|
|
416
|
+
"descFadeInTime": "闹钟达到最大音量所需的时间。",
|
|
417
|
+
"descDriveStatus": "光驱当前的状态。",
|
|
418
|
+
"descToggleTray": "打开或关闭光驱托盘。"
|
|
395
419
|
}
|
|
@@ -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
|
}
|