node-red-contrib-tts-ultimate 2.0.6 → 2.0.8

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.
@@ -44,7 +44,7 @@
44
44
  </div>
45
45
  <div class="form-row" id="divSSML">
46
46
  <label></label>
47
- <input type="checkbox" id="node-input-ssml" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Enable SSML (unsupported by Google without authentication)</label>
47
+ <input type="checkbox" id="node-input-ssml" style="margin-left: 0px; vertical-align: top; width: auto !important;"> <label style="width:auto !important;"> Enable SSML (unsupported by Google free TTS)</label>
48
48
  </div>
49
49
  <div class="form-row">
50
50
  <label for="node-input-sonoshailing"><i class="fa fa-bell"></i> Hailing</label>
@@ -95,435 +95,435 @@
95
95
  </script>
96
96
 
97
97
  <script type="text/javascript">
98
- RED.nodes.registerType('ttsultimate',
98
+ RED.nodes.registerType('ttsultimate',
99
+ {
100
+ category: 'TTSUltimate',
101
+ color: '#ffe6ff', // #ffe6ff
102
+ defaults:
103
+ {
104
+ name: { value: "TTS-Ultimate" },
105
+ voice:
99
106
  {
100
- category: 'TTSUltimate',
101
- color: '#ffe6ff', // #ffe6ff
102
- defaults:
103
- {
104
- name: { value: "TTS-Ultimate" },
105
- voice:
106
- {
107
- value: "Ivy",
108
- required: false
109
- },
110
- ssml:
111
- {
112
- value: false,
113
- required: false
114
- },
115
- sonosipaddress:
116
- {
117
- value: "",
118
- required: false
119
- },
120
- sonosvolume:
121
- {
122
- value: "20",
123
- required: false,
124
- type: "text"
125
- },
126
- sonoshailing:
127
- {
128
- value: "Hailing_Hailing.mp3",
129
- required: false,
130
- },
131
- config:
132
- {
133
- type: "ttsultimate-config",
134
- required: false
135
- },
136
- property: { value: "payload", required: false, validate: RED.validators.typedInput("propertyType") },
137
- propertyType: { value: "msg" },
138
- rules: { value: [] },
139
- playertype: { value: "sonos", required: false },
140
- speakingrate: { value: "1", required: false },
141
- speakingpitch: { value: "0", required: false },
142
- unmuteIfMuted: { value: true },
143
- elevenlabsStability: { value: "0.5", required: false },
144
- elevenlabsSimilarity_boost: { value: "0.5", required: false },
145
- },
146
- inputs: 1,
147
- outputs: 2,
148
- outputLabels: function (i) {
149
- var ret = "";
150
- switch (i) {
151
- case 0:
152
- return "Play Status";
153
- break;
154
- case 1:
155
- return "Error";
156
- break;
157
- default:
158
- break;
159
- }
160
- },
161
- icon: "tts-icon.png",
162
- label: function () {
163
- return this.name || "TTS-ultimate";
164
- },
165
- oneditprepare: function () {
166
- var node = this;
167
- var oNodeServer = RED.nodes.node($("#node-input-config").val()); // Store the config-node
168
-
169
- // 19/02/2020 Used to alert the user if the CSV file has not been loaded and to get the server sooner als deploy
170
- // ###########################
171
- $("#node-input-config").change(function () {
172
- try {
173
- oNodeServer = RED.nodes.node($(this).val());
174
- getVoices();
175
- if (oNodeServer.ttsservice === "googletts") {
176
- $("#divGoogleTTSAudioConfig").show();
177
- $("#divElevenLabsOptions").hide();
178
- } else if (oNodeServer.ttsservice === "elevenlabs") {
179
- $("#divGoogleTTSAudioConfig").hide();
180
- $("#divElevenLabsOptions").show();
181
- $("#divSSML").hide();
182
- } else {
183
- $("#divGoogleTTSAudioConfig").hide();
184
- $("#divElevenLabsOptions").hide();
185
- $("#divSSML").show();
107
+ value: "Ivy",
108
+ required: false
109
+ },
110
+ ssml:
111
+ {
112
+ value: false,
113
+ required: false
114
+ },
115
+ sonosipaddress:
116
+ {
117
+ value: "",
118
+ required: false
119
+ },
120
+ sonosvolume:
121
+ {
122
+ value: "20",
123
+ required: false,
124
+ type: "text"
125
+ },
126
+ sonoshailing:
127
+ {
128
+ value: "Hailing_Hailing.mp3",
129
+ required: false,
130
+ },
131
+ config:
132
+ {
133
+ type: "ttsultimate-config",
134
+ required: false
135
+ },
136
+ property: { value: "payload", required: false, validate: RED.validators.typedInput("propertyType") },
137
+ propertyType: { value: "msg" },
138
+ rules: { value: [] },
139
+ playertype: { value: "sonos", required: false },
140
+ speakingrate: { value: "1", required: false },
141
+ speakingpitch: { value: "0", required: false },
142
+ unmuteIfMuted: { value: true },
143
+ elevenlabsStability: { value: "0.5", required: false },
144
+ elevenlabsSimilarity_boost: { value: "0.5", required: false },
145
+ },
146
+ inputs: 1,
147
+ outputs: 2,
148
+ outputLabels: function (i) {
149
+ var ret = "";
150
+ switch (i) {
151
+ case 0:
152
+ return "Play Status";
153
+ break;
154
+ case 1:
155
+ return "Error";
156
+ break;
157
+ default:
158
+ break;
159
+ }
160
+ },
161
+ icon: "tts-icon.png",
162
+ label: function () {
163
+ return this.name || "TTS-ultimate";
164
+ },
165
+ oneditprepare: function () {
166
+ var node = this;
167
+ var oNodeServer = RED.nodes.node($("#node-input-config").val()); // Store the config-node
168
+
169
+ // 19/02/2020 Used to alert the user if the CSV file has not been loaded and to get the server sooner als deploy
170
+ // ###########################
171
+ $("#node-input-config").change(function () {
172
+ try {
173
+ oNodeServer = RED.nodes.node($(this).val());
174
+ getVoices();
175
+ if (oNodeServer.ttsservice === "googletts") {
176
+ $("#divGoogleTTSAudioConfig").show();
177
+ $("#divElevenLabsOptions").hide();
178
+ } else if (oNodeServer.ttsservice.includes("elevenlabs")) {
179
+ $("#divGoogleTTSAudioConfig").hide();
180
+ $("#divElevenLabsOptions").show();
181
+ $("#divSSML").hide();
182
+ } else {
183
+ $("#divGoogleTTSAudioConfig").hide();
184
+ $("#divElevenLabsOptions").hide();
185
+ $("#divSSML").show();
186
+ }
187
+ } catch (error) {
188
+ }
189
+ });
190
+ // ###########################
191
+
192
+
193
+ // 20/09/2021 Player type
194
+ // ###########################
195
+ if (node.playertype === undefined) {
196
+ node.playertype = "sonos";
197
+ $("#node-input-playertype").val("sonos");
198
+ }
199
+ if (node.playertype === "sonos") {
200
+ $("#divSonos").show();
201
+ } else {
202
+ $("#divSonos").hide();
203
+ }
204
+
205
+ $("#node-input-playertype").change(function () {
206
+ if ($("#node-input-playertype").val() === "sonos") {
207
+ $("#divSonos").show();
208
+ } else {
209
+ $("#divSonos").hide();
210
+ }
211
+ });
212
+ // ###########################
213
+
214
+
215
+ // 24/12/2020 Check if the node is the absolute first in the flow. In this case, it has no http server instatiaced
216
+ // !oNodeServer.hasOwnProperty("noderedipaddress") is when the config node exists, but not deployed
217
+ // oNodeServer.noderedipaddress === "" is when the config node has been deployed, but not configured
218
+ if (oNodeServer === null || oNodeServer === undefined || !oNodeServer.hasOwnProperty("noderedipaddress")) {
219
+ $("#pleaseDeploy").show();
220
+ $("#allGUI").hide();
221
+ } else if (oNodeServer.hasOwnProperty("noderedipaddress") && oNodeServer.noderedipaddress === "") {
222
+ // Config Node deployed but not configured
223
+ $("#pleaseDeploy").html("<p align=\"center\"><b>GOOD, YOU'RE ALMOST DONE</b><br/>PLEASE FINISH CONFIGURING<br/>THE CONFIG NODE ABOVE,<br/><b>THEN SAVE AND FULL DEPLOY</b>.</p>");
224
+ $("#pleaseDeploy").show();
225
+ $("#allGUI").hide();
226
+ } else {
227
+ $("#pleaseDeploy").hide();
228
+ $("#allGUI").show();
229
+ };
230
+
231
+
232
+ // 26/10/2020 Retrieve all avaiables voices
233
+ // #####################################
234
+ function getVoices() {
235
+ $('#node-input-voice')
236
+ .find('option')
237
+ .remove()
238
+ .end();
239
+ $.getJSON("ttsgetvoices" + encodeURIComponent(oNodeServer.id) + "?ttsservice=" + oNodeServer.ttsservice, new Date().getTime(), (data) => {
240
+ var aVoices = data.sort(compareVoices);
241
+ for (let index = 0; index < aVoices.length; index++) {
242
+ const oVoice = aVoices[index];
243
+ $("#node-input-voice").append($("<option></option>")
244
+ .attr("value", oVoice.id)
245
+ .text(`${oVoice.name} (ID: ${oVoice.id})`)
246
+ )
247
+ if (oVoice.name.indexOf("ttsultimategooglecredentials") > -1) {
248
+ // Cred file not present or invalid
249
+ // The only way is to wait some time, then refresh
250
+ let myNotification = RED.notify("Either you haven't uploaded the credential file, or the file is invalid. " + oVoice.name,
251
+ {
252
+ modal: true,
253
+ fixed: true,
254
+ type: 'error',
255
+ buttons: [
256
+ {
257
+ "text": "OK",
258
+ "class": "primary",
259
+ "click": function (e) {
260
+ myNotification.close();
186
261
  }
187
- } catch (error) {
188
- }
262
+ }]
263
+ });
264
+ break;
265
+ };
266
+ };
267
+ $("#node-input-voice").val(node.voice);
268
+ });
269
+ function compareVoices(a, b) {
270
+ // Use toUpperCase() to ignore character casing
271
+ const bandA = a.name.toUpperCase();
272
+ const bandB = b.name.toUpperCase();
273
+
274
+ let comparison = 0;
275
+ if (bandA > bandB) {
276
+ comparison = 1;
277
+ } else if (bandA < bandB) {
278
+ comparison = -1;
279
+ }
280
+ return comparison;
281
+ }
282
+ }
283
+ getVoices();
284
+ // #####################################
285
+
286
+ // Refresh the combo
287
+ // #####################################
288
+ node.refreshHailingList = () => {
289
+ return new Promise((resolve, reject) => {
290
+ $('#node-input-sonoshailing')
291
+ .find('option')
292
+ .remove()
293
+ .end();
294
+ try {
295
+ $.getJSON("getHailingFilesList", new Date().getTime(), (data) => {
296
+ $("#node-input-sonoshailing").append($("<option></option>")
297
+ .attr("value", "0")
298
+ .text("Disable")
299
+ )
300
+ data.sort().forEach(oFile => {
301
+ $("#node-input-sonoshailing").append($("<option></option>")
302
+ .attr("value", oFile.filename)
303
+ .text(oFile.name)
304
+ )
189
305
  });
190
- // ###########################
191
-
192
-
193
- // 20/09/2021 Player type
194
- // ###########################
195
- if (node.playertype === undefined) {
196
- node.playertype = "sonos";
197
- $("#node-input-playertype").val("sonos");
198
- }
199
- if (node.playertype === "sonos") {
200
- $("#divSonos").show();
201
- } else {
202
- $("#divSonos").hide();
203
- }
306
+ $("#node-input-sonoshailing").val(typeof node.sonoshailing === "undefined" ? "Hailing_Hailing.mp3" : node.sonoshailing);
307
+ resolve(true);
308
+ });
309
+ } catch (error) {
310
+ return reject(error);
311
+ }
312
+ });
313
+ }
314
+ // #####################################
315
+
316
+
317
+ // 09/03/2020 Upload file or files
318
+ // ##########################################################
319
+ $("#ownFileUpload").change(function (e) {
320
+ var oFiles;
321
+ oFiles = this.files;
322
+ $.each(oFiles, function (i, file) {
323
+ var fdata = new FormData();
324
+ fdata.append("customHailing", file);
325
+
326
+ $.ajax({
327
+ url: "ttsultimateHailing",
328
+ type: "POST",
329
+ data: fdata, //add the FormData object to the data parameter
330
+ processData: false, //tell jquery not to process data
331
+ contentType: false,
332
+ success: function (response, status, jqxhr) {
333
+ if (typeof response === "object" && response.status === 404) {
334
+ let myNotification = RED.notify(response.message,
335
+ {
336
+ modal: true,
337
+ fixed: true,
338
+ type: 'error',
339
+ buttons: [
340
+ {
341
+ text: "Understood",
342
+ click: function (e) {
343
+ myNotification.close();
344
+ }
345
+ }]
204
346
 
205
- $("#node-input-playertype").change(function () {
206
- if ($("#node-input-playertype").val() === "sonos") {
207
- $("#divSonos").show();
208
- } else {
209
- $("#divSonos").hide();
210
- }
211
- });
212
- // ###########################
213
-
214
-
215
- // 24/12/2020 Check if the node is the absolute first in the flow. In this case, it has no http server instatiaced
216
- // !oNodeServer.hasOwnProperty("noderedipaddress") is when the config node exists, but not deployed
217
- // oNodeServer.noderedipaddress === "" is when the config node has been deployed, but not configured
218
- if (oNodeServer === null || oNodeServer === undefined || !oNodeServer.hasOwnProperty("noderedipaddress")) {
219
- $("#pleaseDeploy").show();
220
- $("#allGUI").hide();
221
- } else if (oNodeServer.hasOwnProperty("noderedipaddress") && oNodeServer.noderedipaddress === "") {
222
- // Config Node deployed but not configured
223
- $("#pleaseDeploy").html("<p align=\"center\"><b>GOOD, YOU'RE ALMOST DONE</b><br/>PLEASE FINISH CONFIGURING<br/>THE CONFIG NODE ABOVE,<br/><b>THEN SAVE AND FULL DEPLOY</b>.</p>");
224
- $("#pleaseDeploy").show();
225
- $("#allGUI").hide();
226
- } else {
227
- $("#pleaseDeploy").hide();
228
- $("#allGUI").show();
229
- };
230
-
231
-
232
- // 26/10/2020 Retrieve all avaiables voices
233
- // #####################################
234
- function getVoices() {
235
- $('#node-input-voice')
236
- .find('option')
237
- .remove()
238
- .end();
239
- $.getJSON("ttsgetvoices" + encodeURIComponent(oNodeServer.id) + "?ttsservice=" + oNodeServer.ttsservice, new Date().getTime(), (data) => {
240
- var aVoices = data.sort(compareVoices);
241
- for (let index = 0; index < aVoices.length; index++) {
242
- const oVoice = aVoices[index];
243
- $("#node-input-voice").append($("<option></option>")
244
- .attr("value", oVoice.id)
245
- .text(oVoice.name)
246
- )
247
- if (oVoice.name.indexOf("ttsultimategooglecredentials") > -1) {
248
- // Cred file not present or invalid
249
- // The only way is to wait some time, then refresh
250
- let myNotification = RED.notify("Either you haven't uploaded the credential file, or the file is invalid. " + oVoice.name,
251
- {
252
- modal: true,
253
- fixed: true,
254
- type: 'error',
255
- buttons: [
256
- {
257
- "text": "OK",
258
- "class": "primary",
259
- "click": function (e) {
260
- myNotification.close();
261
- }
262
- }]
263
- });
264
- break;
265
- };
266
- };
267
- $("#node-input-voice").val(node.voice);
268
347
  });
269
- function compareVoices(a, b) {
270
- // Use toUpperCase() to ignore character casing
271
- const bandA = a.name.toUpperCase();
272
- const bandB = b.name.toUpperCase();
273
-
274
- let comparison = 0;
275
- if (bandA > bandB) {
276
- comparison = 1;
277
- } else if (bandA < bandB) {
278
- comparison = -1;
279
- }
280
- return comparison;
281
- }
348
+ return;
282
349
  }
283
- getVoices();
284
- // #####################################
285
-
286
350
  // Refresh the combo
287
- // #####################################
288
- node.refreshHailingList = () => {
289
- return new Promise((resolve, reject) => {
290
- $('#node-input-sonoshailing')
291
- .find('option')
292
- .remove()
293
- .end();
294
- try {
295
- $.getJSON("getHailingFilesList", new Date().getTime(), (data) => {
296
- $("#node-input-sonoshailing").append($("<option></option>")
297
- .attr("value", "0")
298
- .text("Disable")
299
- )
300
- data.sort().forEach(oFile => {
301
- $("#node-input-sonoshailing").append($("<option></option>")
302
- .attr("value", oFile.filename)
303
- .text(oFile.name)
304
- )
305
- });
306
- $("#node-input-sonoshailing").val(typeof node.sonoshailing === "undefined" ? "Hailing_Hailing.mp3" : node.sonoshailing);
307
- resolve(true);
308
- });
309
- } catch (error) {
310
- return reject(error);
351
+ // The only way is to wait some time, then refresh
352
+ let t = setTimeout(function () {
353
+ node.refreshHailingList().then((success, error) => {
354
+ $("#ownFileUpload").val("");// Otherwise will not re-upload a file with the same name
355
+ $("#node-input-sonoshailing").val("Hailing_" + file.name);
356
+ });
357
+ }, 500);
358
+ },
359
+ error: function (jqxhr, status, errorMessage) {
360
+ let myNotification = RED.notify(errorMessage,
361
+ {
362
+ modal: true,
363
+ fixed: true,
364
+ type: 'error',
365
+ buttons: [
366
+ {
367
+ text: "Understood",
368
+ click: function (e) {
369
+ myNotification.close();
311
370
  }
312
- });
313
- }
314
- // #####################################
315
-
316
-
317
- // 09/03/2020 Upload file or files
318
- // ##########################################################
319
- $("#ownFileUpload").change(function (e) {
320
- var oFiles;
321
- oFiles = this.files;
322
- $.each(oFiles, function (i, file) {
323
- var fdata = new FormData();
324
- fdata.append("customHailing", file);
325
-
326
- $.ajax({
327
- url: "ttsultimateHailing",
328
- type: "POST",
329
- data: fdata, //add the FormData object to the data parameter
330
- processData: false, //tell jquery not to process data
331
- contentType: false,
332
- success: function (response, status, jqxhr) {
333
- if (typeof response === "object" && response.status === 404) {
334
- let myNotification = RED.notify(response.message,
335
- {
336
- modal: true,
337
- fixed: true,
338
- type: 'error',
339
- buttons: [
340
- {
341
- text: "Understood",
342
- click: function (e) {
343
- myNotification.close();
344
- }
345
- }]
346
-
347
- });
348
- return;
349
- }
350
- // Refresh the combo
351
- // The only way is to wait some time, then refresh
352
- let t = setTimeout(function () {
353
- node.refreshHailingList().then((success, error) => {
354
- $("#ownFileUpload").val("");// Otherwise will not re-upload a file with the same name
355
- $("#node-input-sonoshailing").val("Hailing_" + file.name);
356
- });
357
- }, 500);
358
- },
359
- error: function (jqxhr, status, errorMessage) {
360
- let myNotification = RED.notify(errorMessage,
361
- {
362
- modal: true,
363
- fixed: true,
364
- type: 'error',
365
- buttons: [
366
- {
367
- text: "Understood",
368
- click: function (e) {
369
- myNotification.close();
370
- }
371
- }]
372
-
373
- });
374
- }
375
- });
376
- });
377
- });
378
- // ##########################################################
379
-
371
+ }]
380
372
 
373
+ });
374
+ }
375
+ });
376
+ });
377
+ });
378
+ // ##########################################################
381
379
 
382
- // Delete selected file
383
- // ##########################################################
384
- $("#deleteSelectedFile").click(function () {
385
- $.getJSON('deleteHailingFile?FileName=' + $("#node-input-sonoshailing").val(), (data) => {
386
- node.refreshHailingList();
387
- });
388
- });
389
- // ##########################################################
390
-
391
-
392
-
393
- // 20/03/2020 Coronavirus issue in Itay. Getting all Sonos Groups
394
- // ##########################################################
395
- $.getJSON('sonosgetAllGroups', (data) => {
396
- if (typeof data === "string" && data == "ERRORDISCOVERY") { // 10/04/2020 if error in discovery, fallback to manual IP input
397
- // Transform the dropdown to a simple input
398
- $("#node-input-sonosipaddress").replaceWith('<input type="text" id="node-input-sonosipaddress" style="width:150px">');
399
- } else {
400
- $("#node-input-sonosipaddress").replaceWith('<select id="node-input-sonosipaddress"></select>');
401
- data.sort().forEach(oGroup => {
402
- $("#node-input-sonosipaddress").append($("<option></option>")
403
- .attr("value", oGroup.host)
404
- .text(oGroup.name + " (" + oGroup.host + ")")
405
- )
406
- });
407
- }
408
- if (typeof node.sonosipaddress === "undefined" || node.sonosipaddress == "") {
409
- $("#node-input-sonosipaddress").val($("#node-input-sonosipaddress option:first").val());
410
- } else { $("#node-input-sonosipaddress").val(node.sonosipaddress) }
411
- });
412
- // ##########################################################
413
-
414
-
415
- //#region ADDITIONAL PLAYERS
416
- // 20/03/2020 ADDITIONAL PLAYERS
417
- // ##########################################################
418
- //var previousValueType = { value: "prev", label: this._("switch.previous"), hasValue: false };
419
-
420
- // Add Selectbox with the volume for the additional players
421
- function addAdditionalPlayerVolumeUI(row, _currentVolume = 0) {
422
- let oAdjustVolume = $('<select/>', { class: "rowRulePlayerHostAdjustVolume", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
423
- for (let index = -100; index < 100; index += 5) {
424
- let sTesto = "";
425
- if (index === 0) sTesto = "Same volume as Main Sonos Player";
426
- if (index < 0) sTesto = "Decrease volume by " + Math.abs(index);
427
- if (index > 0) sTesto = "Increase volume by " + index;
428
- oAdjustVolume.append($("<option></option>")
429
- .attr("value", index)
430
- .text(sTesto)
431
- )
432
- }
433
- oAdjustVolume.val(_currentVolume);
434
- }
435
- function resizeRule(rule) { }
436
380
 
437
- $("#node-input-rule-container").css('min-height', '150px').css('min-width', '450px').editableList({
438
- addItem: function (container, i, opt) { // row, index, data
439
381
 
440
- if (!opt.hasOwnProperty('r')) {
441
- opt.r = {};
442
- }
443
- let rule = opt.r;
444
- if (!opt.hasOwnProperty('i')) {
445
- opt._i = Math.floor((0x99999 - 0x10000) * Math.random()).toString();
446
- }
447
- container.css({
448
- overflow: 'hidden',
449
- whiteSpace: 'nowrap'
450
- });
451
-
452
-
453
- var row = $('<div class="form-row"/>').appendTo(container);
454
- var oPlayer = $('<label>Discovering.... wait...</label>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
455
- oPlayer.on("change", function () {
456
- resizeRule(container);
457
- });
458
-
459
- $.getJSON('sonosgetAllGroups', (data) => {
460
- if (typeof data === "string" && data == "ERRORDISCOVERY") { // 10/04/2020 if error in discovery, fallback to manual IP input
461
- // Transform the dropdown to a simple input
462
- oPlayer.remove();
463
- oPlayer = $('<input/>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
464
- addAdditionalPlayerVolumeUI(row, rule.hostVolumeAdjust);
465
- } else {
466
- oPlayer.remove();
467
- oPlayer = $('<select/>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
468
- data.sort().forEach(oGroup => {
469
- oPlayer.append($("<option></option>")
470
- .attr("value", oGroup.host)
471
- .text(oGroup.name + " (" + oGroup.host + ")")
472
- )
473
- });
474
- addAdditionalPlayerVolumeUI(row, rule.hostVolumeAdjust);
475
- }
476
- oPlayer.val(rule.host);
477
- });
478
-
479
- //oPlayer.change();
480
- },
481
- removeItem: function (opt) {
482
-
483
- },
484
- resizeItem: resizeRule,
485
- sortItems: function (rules) {
486
- },
487
- sortable: true,
488
- removable: true
382
+ // Delete selected file
383
+ // ##########################################################
384
+ $("#deleteSelectedFile").click(function () {
385
+ $.getJSON('deleteHailingFile?FileName=' + $("#node-input-sonoshailing").val(), (data) => {
386
+ node.refreshHailingList();
387
+ });
388
+ });
389
+ // ##########################################################
390
+
391
+
392
+
393
+ // 20/03/2020 Coronavirus issue in Itay. Getting all Sonos Groups
394
+ // ##########################################################
395
+ $.getJSON('sonosgetAllGroups', (data) => {
396
+ if (typeof data === "string" && data == "ERRORDISCOVERY") { // 10/04/2020 if error in discovery, fallback to manual IP input
397
+ // Transform the dropdown to a simple input
398
+ $("#node-input-sonosipaddress").replaceWith('<input type="text" id="node-input-sonosipaddress" style="width:150px">');
399
+ } else {
400
+ $("#node-input-sonosipaddress").replaceWith('<select id="node-input-sonosipaddress"></select>');
401
+ data.sort().forEach(oGroup => {
402
+ $("#node-input-sonosipaddress").append($("<option></option>")
403
+ .attr("value", oGroup.host)
404
+ .text(oGroup.name + " (" + oGroup.host + ")")
405
+ )
406
+ });
407
+ }
408
+ if (typeof node.sonosipaddress === "undefined" || node.sonosipaddress == "") {
409
+ $("#node-input-sonosipaddress").val($("#node-input-sonosipaddress option:first").val());
410
+ } else { $("#node-input-sonosipaddress").val(node.sonosipaddress) }
411
+ });
412
+ // ##########################################################
413
+
414
+
415
+ //#region ADDITIONAL PLAYERS
416
+ // 20/03/2020 ADDITIONAL PLAYERS
417
+ // ##########################################################
418
+ //var previousValueType = { value: "prev", label: this._("switch.previous"), hasValue: false };
419
+
420
+ // Add Selectbox with the volume for the additional players
421
+ function addAdditionalPlayerVolumeUI(row, _currentVolume = 0) {
422
+ let oAdjustVolume = $('<select/>', { class: "rowRulePlayerHostAdjustVolume", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
423
+ for (let index = -100; index < 100; index += 5) {
424
+ let sTesto = "";
425
+ if (index === 0) sTesto = "Same volume as Main Sonos Player";
426
+ if (index < 0) sTesto = "Decrease volume by " + Math.abs(index);
427
+ if (index > 0) sTesto = "Increase volume by " + index;
428
+ oAdjustVolume.append($("<option></option>")
429
+ .attr("value", index)
430
+ .text(sTesto)
431
+ )
432
+ }
433
+ oAdjustVolume.val(_currentVolume);
434
+ }
435
+ function resizeRule(rule) { }
436
+
437
+ $("#node-input-rule-container").css('min-height', '150px').css('min-width', '450px').editableList({
438
+ addItem: function (container, i, opt) { // row, index, data
439
+
440
+ if (!opt.hasOwnProperty('r')) {
441
+ opt.r = {};
442
+ }
443
+ let rule = opt.r;
444
+ if (!opt.hasOwnProperty('i')) {
445
+ opt._i = Math.floor((0x99999 - 0x10000) * Math.random()).toString();
446
+ }
447
+ container.css({
448
+ overflow: 'hidden',
449
+ whiteSpace: 'nowrap'
450
+ });
451
+
452
+
453
+ var row = $('<div class="form-row"/>').appendTo(container);
454
+ var oPlayer = $('<label>Discovering.... wait...</label>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
455
+ oPlayer.on("change", function () {
456
+ resizeRule(container);
457
+ });
458
+
459
+ $.getJSON('sonosgetAllGroups', (data) => {
460
+ if (typeof data === "string" && data == "ERRORDISCOVERY") { // 10/04/2020 if error in discovery, fallback to manual IP input
461
+ // Transform the dropdown to a simple input
462
+ oPlayer.remove();
463
+ oPlayer = $('<input/>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
464
+ addAdditionalPlayerVolumeUI(row, rule.hostVolumeAdjust);
465
+ } else {
466
+ oPlayer.remove();
467
+ oPlayer = $('<select/>', { class: "rowRulePlayerHost", type: "text", style: "width:200px; margin-left: 5px; text-align: left;" }).appendTo(row);
468
+ data.sort().forEach(oGroup => {
469
+ oPlayer.append($("<option></option>")
470
+ .attr("value", oGroup.host)
471
+ .text(oGroup.name + " (" + oGroup.host + ")")
472
+ )
489
473
  });
474
+ addAdditionalPlayerVolumeUI(row, rule.hostVolumeAdjust);
475
+ }
476
+ oPlayer.val(rule.host);
477
+ });
478
+
479
+ //oPlayer.change();
480
+ },
481
+ removeItem: function (opt) {
482
+
483
+ },
484
+ resizeItem: resizeRule,
485
+ sortItems: function (rules) {
486
+ },
487
+ sortable: true,
488
+ removable: true
489
+ });
490
490
 
491
- // 20/03/2020 For each rule, create a row
492
- for (var i = 0; i < this.rules.length; i++) {
493
- let rule = this.rules[i];
494
- $("#node-input-rule-container").editableList('addItem', { r: rule, i: i });
495
- }
496
- // ##########################################################
497
- //#endregion
498
-
499
- node.refreshHailingList();
500
-
501
- }, oneditsave: function () {
502
- let node = this;
503
- let rules = $("#node-input-rule-container").editableList('items');
504
- node.rules = [];
505
- rules.each(function (i) {
506
- let rule = $(this);
507
- let rowRulePlayerHost = rule.find(".rowRulePlayerHost").val();
508
- let rowRulePlayerHostAdjustVolume = rule.find(".rowRulePlayerHostAdjustVolume").val();
509
- node.rules.push({ host: rowRulePlayerHost, hostVolumeAdjust: rowRulePlayerHostAdjustVolume });
510
- });
511
- this.propertyType = $("#node-input-property").typedInput('type');
512
- },
513
- oneditresize: function (size) {
514
- var node = this;
515
-
516
- var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
517
- var height = size.height;
518
- for (var i = 0; i < rows.length; i++) {
519
- height -= $(rows[i]).outerHeight(true);
520
- }
521
- var editorRow = $("#dialog-form>div.node-input-rule-container-row");
522
- height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
523
- height += 16;
524
- $("#node-input-rule-container").editableList('height', height);
525
- }
491
+ // 20/03/2020 For each rule, create a row
492
+ for (var i = 0; i < this.rules.length; i++) {
493
+ let rule = this.rules[i];
494
+ $("#node-input-rule-container").editableList('addItem', { r: rule, i: i });
495
+ }
496
+ // ##########################################################
497
+ //#endregion
498
+
499
+ node.refreshHailingList();
500
+
501
+ }, oneditsave: function () {
502
+ let node = this;
503
+ let rules = $("#node-input-rule-container").editableList('items');
504
+ node.rules = [];
505
+ rules.each(function (i) {
506
+ let rule = $(this);
507
+ let rowRulePlayerHost = rule.find(".rowRulePlayerHost").val();
508
+ let rowRulePlayerHostAdjustVolume = rule.find(".rowRulePlayerHostAdjustVolume").val();
509
+ node.rules.push({ host: rowRulePlayerHost, hostVolumeAdjust: rowRulePlayerHostAdjustVolume });
526
510
  });
511
+ this.propertyType = $("#node-input-property").typedInput('type');
512
+ },
513
+ oneditresize: function (size) {
514
+ var node = this;
515
+
516
+ var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
517
+ var height = size.height;
518
+ for (var i = 0; i < rows.length; i++) {
519
+ height -= $(rows[i]).outerHeight(true);
520
+ }
521
+ var editorRow = $("#dialog-form>div.node-input-rule-container-row");
522
+ height -= (parseInt(editorRow.css("marginTop")) + parseInt(editorRow.css("marginBottom")));
523
+ height += 16;
524
+ $("#node-input-rule-container").editableList('height', height);
525
+ }
526
+ });
527
527
  </script>
528
528
 
529
529
  <script type="text/markdown" data-help-name="ttsultimate">
@@ -556,6 +556,7 @@
556
556
  : sonoshailing (string) : Overrides the selected hailing and plays the filename you passed in. Please double check the spelling of the filename (must be the same as you can see in the dropdown list of your hailing files, in the ttsultimate config window) and do not include the .mp3 extenson. For example node.sonoshailing="ComputerCall".
557
557
  : priority (boolean) : If set to true, the inbound flow message will cancel the current TTS queue, will stop the current phrase being spoken and the node will play this priority message. If there are other priority messages in the queue, they will be retained and the inbound priority flow message is added to the queue. If the inbound priority flow message is the first in the priority queue, the hailing is played first (if the hailing has been enabled or if the hailing has been overridden by node.sonoshailing).
558
558
  : stop (boolean) : If set to true, stops whatever is playing and clears the TTS queue.
559
+ : voiceId (number) : Play a message with custom voice ID.
559
560
  : setConfig (json) : This is the property where you can set all the things. It must be a JSON Object with the below specified properties. The setting is retained until the node receives another msg.setConfig or until node-red is restarted.
560
561
  : setConfig {setMainPlayerIP} (string) : Sets the main player IP. This will also be the coordinator if you have a group of players.
561
562
  : setConfig {setPlayerGroupArray} (array) : Sets the array of players beloging to the group, if any. You can also specify the volume variation from the main volume player, to adapt the additional player's perceived volume to the main sonos player volume. For example, if you have a speaker mounted in celiling, having less perceived volume, you can "push" the volume up, to match the whole perceived volume. Just add # after the IP and a number from -100 to 100 to subtract or add volume compared to the main sonos volume. For example, if the sonos main player volume is 40, you can push this celing speaker's volume to further 10, so it'll have the real volume of 50. See below, the example. Even if you have only one additional player, you need to put it into an array.