UNPKG

node-red-contrib-tts-ultimate

Version:

Transforms the text in speech and hear it using Sonos player or generate an audio file to be used with third parties nodes. Works with voices from Amazon, Google (without credentials as well), Microsoft TTS Azure, or your own voice. You can also only crea

963 lines (869 loc) 59.4 kB
module.exports = function (RED) { 'use strict'; var fs = require('fs'); var util = require('util'); var path = require('path'); const sonos = require('sonos'); const crypto = require("crypto"); function slug(_text) { var sRet = _text; sRet = sRet.toString().replace(/\./g, "_stop_"); sRet = sRet.toString().replace(/\?/g, "_qm_"); sRet = sRet.toString().replace(/\!/g, "_em_"); sRet = sRet.toString().replace(/\,/g, "_pause_"); sRet = sRet.toString().replace(/\:/g, "_colon_"); sRet = sRet.toString().replace(/\;/g, "_semicolon_"); sRet = sRet.toString().replace(/\</g, "_less_"); sRet = sRet.toString().replace(/\>/g, "_greater_"); sRet = sRet.toString().replace(/\//g, "_sl_"); sRet = sRet.toString().replace(/\'/g, "_ap_"); sRet = sRet.toString().replace(/\=/g, "_ug_"); sRet = sRet.toString().replace(/\\/g, "_bs_"); sRet = sRet.toString().replace(/\(/g, "_pa_"); sRet = sRet.toString().replace(/\)/g, "_pc_"); sRet = sRet.toString().replace(/\*/g, "_as_"); sRet = sRet.toString().replace(/\[/g, "_qa_"); sRet = sRet.toString().replace(/\]/g, "_qc_"); sRet = sRet.toString().replace(/\^/g, "_fu_"); sRet = sRet.toString().replace(/\|/g, "_pi_"); sRet = sRet.toString().replace(/\"/g, "_dc_"); sRet = sRet.toString().replace(/\#/g, "_"); // slug.charmap['.'] = '_stop_'; // slug.charmap['?'] = '_qm_'; // slug.charmap['!'] = '_em_'; // slug.charmap[','] = '_pause_'; // slug.charmap[':'] = '_colon_'; // slug.charmap[';'] = '_semicolon_'; // slug.charmap['<'] = '_less_'; // slug.charmap['>'] = '_greater_'; return sRet; } // Node Register function PollyNode(config) { RED.nodes.createNode(this, config); var node = this; node.server = RED.nodes.getNode(config.config); if (!node.server) { RED.log.error('Missing Polly config'); return; } node.ssml = config.ssml; node.oTimerSonosConnectionCheck = null; node.sSonosIPAddress = ""; node.sonosCoordinatorGroupName = ""; node.sonoshailing = "0"; // Hailing file node.sSonosIPAddress = config.sonosipaddress.trim(); node.voiceId = config.voice || 0; node.sSonosVolume = config.sonosvolume; node.sonoshailing = config.sonoshailing; node.msg = {}; // 08/05/2019 Node message node.msg.completed = true; node.msg.connectionerror = true; node.userDir = node.server.TTSRootFolderPath === undefined ? path.join(RED.settings.userDir, "sonospollyttsstorage") : node.server.TTSRootFolderPath; node.oAdditionalSonosPlayers = []; // 20/03/2020 Contains other players to be grouped node.rules = config.rules || [{}]; node.sNoderedURL = ""; node.oTimerCacheFlowMSG = null; // 05/12/2020 node.tempMSGStorage = []; // 04/12/2020 Temporary stores the flow messages node.bBusyPlayingQueue = false; // 04/12/2020 is busy during playing of the queue node.currentMSGbeingSpoken = {}; // Stores the current message being spoken node.sonosCoordinatorPreviousVolumeSetByApp = 0; // 05/07/2021 stores the main payer volume set by the sonos app node.playertype = config.playertype === undefined ? "sonos" : config.playertype; // 20/09/2021 Player type node.speakingpitch = config.speakingpitch === undefined ? "0" : config.speakingpitch; // 21/09/2021 AudioConfig speakingpitch node.speakingrate = config.speakingrate === undefined ? "1" : config.speakingrate; // 21/09/2021 AudioConfig speakingrate node.unmuteIfMuted = config.unmuteIfMuted === undefined ? false : config.unmuteIfMuted; // 21/10/2021 Unmute if previiously muted. node.sonosCoordinatorIsPreviouslyMuted = false; node.passThroughMessage = {}; node.bTimeOutPlay = false; if (typeof node.server !== "undefined" && node.server !== null) { node.sNoderedURL = node.server.sNoderedURL || ""; } // 20/11/2019 Used to call the status update node.setNodeStatus = ({ fill, shape, text }) => { try { var dDate = new Date(); node.status({ fill: fill, shape: shape, text: text + " (" + dDate.getDate() + ", " + dDate.toLocaleTimeString() + ")" }); } catch (error) { } } //#region ASYNC DECLARATIONS // 30/12/2020 we are at the end of this crazy 2020 function getMusicQueue(_oPlayer = node.SonosClient) { return new Promise(function (resolve, reject) { var oRet = null; _oPlayer.currentTrack().then(track => { oRet = track;// .queuePosition || 1; // Get the current track in the queue. _oPlayer.getCurrentState().then(state => { // A music queue is playing and no TTS is speaking? oRet.state = state; resolve(oRet); }).catch(err => { //console.log('ttsultimate: getCurrentState: Error occurred %j', err); reject(err); }) }).catch(err => { reject(err); //console.log('ttsultimate: Error currentTrackoccurred %j', err); }); }); }; let iWaitAfterSync = 500; // 24/08/2021 Sync wrapper function PLAYSync(_toPlay, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.play(_toPlay).then(result => { if (iWaitAfterSync > 2000) console.log("PLAYSYNC") let t = setTimeout(() => { resolve(true); }, iWaitAfterSync); }).catch(err => { RED.log.error("ttsultimate: Error PLAYSync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function SEEKSync(_Position, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.seek(_Position).then(result => { if (iWaitAfterSync > 2000) console.log("SEEKSync", _Position) let t = setTimeout(() => { resolve(true); }, iWaitAfterSync); }).catch(err => { //RED.log.error("ttsultimate: Error SEEKSync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function SELECTQUEUESync(_oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.selectQueue().then(result => { if (iWaitAfterSync > 2000) console.log("SELECTQUEUESync") try { STOPSync(); // The SetQueue automatically starts playing, so i need to stop it now! } catch (error) { } let t = setTimeout(() => { resolve(true); }, iWaitAfterSync); }).catch(err => { RED.log.error("ttsultimate: Error SELECTQUEUESync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function SELECTTRACKSync(_queuePosition, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.selectTrack(_queuePosition).then(result => { if (iWaitAfterSync > 2000) console.log("SELECTTRACKSync", _queuePosition) let t = setTimeout(() => { resolve(true); }, iWaitAfterSync); }).catch(err => { RED.log.error("ttsultimate: Error SELECTTRACKSync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function STOPSync(_oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.stop().then(result => { if (iWaitAfterSync > 2000) console.log("STOPSync") let t = setTimeout(() => { resolve(true); }, iWaitAfterSync); }).catch(err => { //RED.log.error("ttsultimate: Error STOPSync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function GETVOLUMESync(_oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.getVolume().then(volume => { if (iWaitAfterSync > 2000) console.log("GETVOLUMESync", volume) resolve(volume); }).catch(err => { RED.log.error("ttsultimate: Error GETVOLUMESync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function SETVOLUMESync(_volume, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.setVolume(_volume).then(result => { if (iWaitAfterSync > 2000) console.log("SETVOLUMESync", _volume) resolve(true); }).catch(err => { RED.log.error("ttsultimate: Error SETVOLUMESync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function setAVTransportURISync(_Uri, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.setAVTransportURI(_Uri).then(volume => { if (iWaitAfterSync > 2000) console.log("setAVTransportURISync", _Uri) resolve(true); }).catch(err => { RED.log.error("ttsultimate: Error setAVTransportURISync: " + err.message); reject(err); }); }); } // 24/08/2021 Sync wrapper function getCurrentStateSync(_oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.getCurrentState().then(state => { resolve(state); }).catch(err => { RED.log.error("ttsultimate: Error getCurrentStateSync: " + err.message); reject(err); }); }); } // 21/10/2021 Sync wrapper function GETMutedSync(_oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.getMuted().then(state => { resolve(state); }).catch(err => { RED.log.error("ttsultimate: Error GETMutedSync: " + err.message); reject(err); }); }); } // 21/10/2021 Sync wrapper function SETMutedSync(_muted, _oPlayer = node.SonosClient) { return new Promise((resolve, reject) => { _oPlayer.setMuted(_muted).then(state => { resolve(state); }).catch(err => { RED.log.error("ttsultimate: Error SETMutedSync: " + err.message); reject(err); }); }); } // 20/03/2020 Join Coordinator queue // ###################################################### async function groupSpeakersSync() { // 05/07/2021 Get the main coordinator player previous volume set by app try { node.sonosCoordinatorPreviousVolumeSetByApp = await GETVOLUMESync(); node.sonosCoordinatorIsPreviouslyMuted = await GETMutedSync(); } catch (error) { node.sonosCoordinatorPreviousVolumeSetByApp = node.sSonosVolume; node.sonosCoordinatorIsPreviouslyMuted = false; } // 30/03/2020 in the middle of coronavirus emergency. Group Speakers for (let index = 0; index < node.oAdditionalSonosPlayers.length; index++) { let element = node.oAdditionalSonosPlayers[index].oPlayer; // 02/07/2021 Get the additional's player's volume set by app and the current track, to be set again in ungroupspealers try { element.additionalPlayerPreviousVolumeSetByApp = await element.getVolume(); element.additionalPlayerCurrentTrack = await getMusicQueue(element); } catch (error) { RED.log.warn("ttsultimate: Error setting volume of joined device " + error.message); } // 21/10/2021 set the previous mute/unmute state try { element.isPreviouslyMuted = await element.getMuted(); } catch (error) { RED.log.warn("ttsultimate: Error getMuted of joined device " + error.message); } try { await element.joinGroup(node.sonosCoordinatorGroupName); } catch (error) { RED.log.warn("ttsultimate: Error joining device " + error.message); } }; } // 20/03/2020 Ungroup Coordinator queue async function ungroupSpeakersSync() { // 05/07/2021, set the previous app volume for main coordinator player try { await SETVOLUMESync(node.sonosCoordinatorPreviousVolumeSetByApp); } catch (error) { RED.log.warn("ttsultimate: Error set preious volume on main coordinator in ungroupSpeakers " + error.message); } // 21/10/2021 Unmute? if (node.unmuteIfMuted && node.sonosCoordinatorIsPreviouslyMuted) { try { await SETMutedSync(true); } catch (error) { RED.log.warn("ttsultimate: Error set preivous mute state on main coordinator in ungroupSpeakers " + error.message); } } for (let index = 0; index < node.oAdditionalSonosPlayers.length; index++) { let element = node.oAdditionalSonosPlayers[index].oPlayer; try { await element.leaveGroup(); } catch (error) { // Dont care RED.log.warn("ttsultimate: Error leaving group device " + error.message); } if (element.additionalPlayerPreviousVolumeSetByApp !== undefined) { try { await element.setVolume(element.additionalPlayerPreviousVolumeSetByApp); } catch (error) { RED.log.warn("ttsultimate: Error set previous volume on group device " + error.message); } } // 21/10/2021 Unmute? if (node.unmuteIfMuted && element.isPreviouslyMuted) { try { await element.setMuted(true); } catch (error) { RED.log.warn("ttsultimate: Error set previous mute state on group device " + error.message); } } } } // ###################################################### async function delay(ms) { return new Promise(function (resolve, reject) { try { node.timerWait = setTimeout(resolve, ms); } catch (error) { reject(); } }); } //#endregion // 27/11/2019 Check Sonos connection health node.CheckSonosConnection = () => { if (node.playertype !== "noplayer") { node.SonosClient.getCurrentState().then(state => { // 11/12/202020 The connection with Sonos is OK. if (node.msg.connectionerror == true) { node.flushQueue(); node.setNodeStatus({ fill: "green", shape: "dot", text: "Sonos is connected." }); node.msg.connectionerror = false; node.send([null, { payload: node.msg.connectionerror }]); } node.oTimerSonosConnectionCheck = setTimeout(function () { node.CheckSonosConnection(); }, 5000); }).catch(err => { node.setNodeStatus({ fill: "red", shape: "dot", text: "Sonos connection is DOWN: " + err.message }); node.flushQueue(); // 11/12/2020 Set node output to signal connectio error if (node.msg.connectionerror == false) { node.msg.connectionerror = true; node.send([null, { payload: node.msg.connectionerror }]); } node.oTimerSonosConnectionCheck = setTimeout(function () { node.CheckSonosConnection(); }, 10000); }); } else { node.setNodeStatus({ fill: "green", shape: "dot", text: "Sonos is connected." }); node.msg.connectionerror = false; node.send([null, { payload: node.msg.connectionerror }]); } } // 20/09/2021 If Sonos, do init if (node.playertype === "sonos") { // Create sonos client & groups node.SonosClient = new sonos.Sonos(node.sSonosIPAddress); // 20/03/2020 Set the coorinator's zone name node.SonosClient.getName().then(info => { node.sonosCoordinatorGroupName = info; RED.log.info("ttsultimate: ZONE COORDINATOR " + JSON.stringify(info)); }).catch(err => { }); // Fill the node.oAdditionalSonosPlayers with all sonos object in the rules for (let index = 0; index < node.rules.length; index++) { let element = node.rules[index]; // Rule row is {host:"192.168.1.12,hostVolumeAdjust:0} // 12/04/2022 Create an object containing the addidtional player and the adapted volume node.oAdditionalSonosPlayers.push({ oPlayer: new sonos.Sonos(element.host), hostVolumeAdjust: Number(element.hostVolumeAdjust) }); RED.log.info("ttsultimate: FOUND ADDITIONAL PLAYER " + element.host + " Adjusted volume: " + element.hostVolumeAdjust); } // 27/11/2019 Start the connection healty check node.oTimerSonosConnectionCheck = setTimeout(function () { node.CheckSonosConnection(); }, 5000); } else if (node.playertype === "noplayer") { node.msg.connectionerror = false; } node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Initialized.' }); // 22/09/2020 Flush Queue and set to stopped node.flushQueue = () => { // 10/04/2018 Remove the TTS message from the queue node.tempMSGStorage = []; // Exit whatever cycle node.bTimeOutPlay = true; node.bBusyPlayingQueue = false; node.currentMSGbeingSpoken = {}; if (node.server.whoIsUsingTheServer === node.id) node.server.whoIsUsingTheServer = ""; } // 30/12/2020 Supergiovane resume queue for radio, queue music, TV in , line in etc. async function resumeMusicQueue(_oTrack, _oPlayer = node.SonosClient) { if (_oTrack !== null) { // Do some checks on the track. if (_oTrack.hasOwnProperty("duration") && _oTrack.duration === 0 || (_oTrack.uri.startsWith("x-sonosprog-http") || _oTrack.uri.startsWith("x-sonosapi-hls-static") || _oTrack.uri.startsWith("x-sonos-spotify"))) { // Stream _oTrack.trackType = "stream"; } else if (_oTrack.hasOwnProperty("duration") && isNaN(_oTrack.duration)) { // Line input _oTrack.trackType = "lineinput"; } else { // Music queue _oTrack.trackType = "musicqueue"; } } else { // Track is null, nothing to resume. return false; } // It's a radio station or a generic stream, not a queue. if (_oTrack.trackType === "stream") { if (_oTrack.state === "playing") { // 03/09/2021 Play if it was playing try { await PLAYSync(_oTrack.uri, _oPlayer); } catch (error) { return error; } try { await delay(1000); await SEEKSync(_oTrack.position, _oPlayer); } catch (error) { // Don't care } } } else { if (_oTrack.trackType === "musicqueue") { // This indicates that is an audio file or stream station try { await SELECTQUEUESync(_oPlayer); } catch (error) { return error; } try { await delay(1000); await SELECTTRACKSync(_oTrack.queuePosition, _oPlayer); } catch (error) { return error; } try { await delay(1000); await SEEKSync(_oTrack.position, _oPlayer); } catch (error) { // Don't care } if (_oTrack.state === "playing") { // 24/08/2021 Play if it was playing try { await PLAYSync(_oPlayer); } catch (error) { return error; } } else { /// 03/09/2021 try { await STOPSync(_oPlayer); } catch (error) { return error; } } } else if (_oTrack.trackType === "lineinput") { // Line in, TV in, etc... if (_oTrack.state === "playing") { try { await setAVTransportURISync(_oTrack.uri, _oPlayer); } catch (error) { return error; } } } } let t = setTimeout(() => { return true; }, 5000); // Wait some seconds }; // Handle the queue async function HandleQueue() { node.bBusyPlayingQueue = true; node.server.whoIsUsingTheServer = node.id; // Signal to other ttsultimate node, that i'm using the Sonos device try { // Get the current music queue, if one var oCurTrack = null; try { oCurTrack = await getMusicQueue(); // 19/04/2022 The current track of additional players is read in the groupSpeakerySync function } catch (error) { oCurTrack = null; } // 05/12/2020 Set "completed" to false and send it node.msg.completed = false; try { await groupSpeakersSync(); // 20/03/2020 Group Speakers toghether and reads each current track } catch (error) { // Don't care. node.setNodeStatus({ fill: "red", shape: "ring", text: "Error grouping speakers: " + error.message }); RED.log.error("ttsultimate: Error grouping speakers: " + error.message); } node.send([{ passThroughMessage: node.passThroughMessage, payload: node.msg.completed }, null]); // 24/08/2021 If something was playing, stop the player https://github.com/Supergiovane/node-red-contrib-tts-ultimate/issues/32 try { //await node.SonosClient.stop(); //.then(result => { await STOPSync(); } catch (error) { //RED.log.error("ttsultimate: Error stopping in HandleSend: " + error.message); } while (node.tempMSGStorage.length > 0) { node.currentMSGbeingSpoken = node.tempMSGStorage[0];// Advise the whole node of the currently spoken MSG const msg = node.currentMSGbeingSpoken.payload.toString(); // Get the text to be spoken node.tempMSGStorage.splice(0, 1); // Remove the first item in the array var sFileToBePlayed = ""; node.setNodeStatus({ fill: "gray", shape: "ring", text: "Read " + msg }); // 04/12/2020 check what really is the file to be played if (msg.toLowerCase().startsWith("http://") || msg.toLowerCase().startsWith("https://")) { RED.log.info('ttsultimate: HTTP filename: ' + msg); sFileToBePlayed = msg; } else if (msg.indexOf("OwnFile_") !== -1) { RED.log.info('ttsultimate: OwnFile .MP3, skip tts, filename: ' + msg); sFileToBePlayed = path.join(node.userDir, "ttspermanentfiles", msg); } else if (msg.indexOf("Hailing_") !== -1) { RED.log.info('ttsultimate: Hailing .MP3, skip tts, filename: ' + msg); sFileToBePlayed = path.join(node.userDir, "hailingpermanentfiles", msg); } else { sFileToBePlayed = getFilename(msg, node.voiceId, node.ssml, "mp3", node.speakingpitch, node.speakingrate); sFileToBePlayed = path.join(node.userDir, "ttsfiles", sFileToBePlayed); // Check if cached if (!fs.existsSync(sFileToBePlayed)) { try { // No file in cache. Download from tts service var data; if (node.server.ttsservice === "polly") { node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Amazon...' }); var params = { OutputFormat: "mp3", SampleRate: '22050', Text: msg, TextType: node.ssml ? 'ssml' : 'text' }; // 02/03/2022 check wether standard or neural engine is POLLY is selected if (node.voiceId.includes("#engineType:")) { params.VoiceId = node.voiceId.split("#engineType:")[0]; params.Engine = node.voiceId.split("#engineType:")[1]; } else { params.VoiceId = node.voiceId; } data = await synthesizeSpeechPolly([node.server.polly, params]); } else if (node.server.ttsservice === "googletts") { node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Google TTS...' }); // VoiceId is: name + "#" + languageCode + "#" + ssmlGender // speakingRate tra 0.25 e 4.0 // pitch tra -20.0 e 20.0 const params = { voice: { name: node.voiceId.split("#")[0], languageCode: node.voiceId.split("#")[1], ssmlGender: node.voiceId.split("#")[2] }, audioConfig: { audioEncoding: "MP3", speakingRate: parseFloat(node.speakingrate), pitch: parseFloat(node.speakingpitch), }, }; params.input = node.ssml === false ? { text: msg } : { ssml: msg }; data = await synthesizeSpeechGoogleTTS([node.server.googleTTS, params]); } else if (node.server.ttsservice === "googletranslate") { node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Google Translate...' }); // VoiceId is: code. SSML is not supported by google translate if (node.voiceId === "cmn-Hant-TW") node.voiceId = "zh-CN"; // 06/08/2022 fix for a wrong voiceid sent by google translate as voice code const params = { text: msg, voice: node.voiceId, slow: false // optional }; data = await synthesizeSpeechGoogleTranslate(node.server.googleTranslateTTS, params); } else if (node.server.ttsservice === "microsoftazuretts") { node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Downloading from Microsoft Azure TTS...' }); // VoiceId is: code const params = { text: msg, voice: node.voiceId }; data = await synthesizeSpeechMicrosoftAzureTTS(node.server.microsoftAzureTTS, params); } // Save the downloaded file into the cache try { fs.writeFileSync(sFileToBePlayed, data); } catch (error) { RED.log.error("ttsultimate: node id: " + node.id + " Unable to save the file " + error.message); node.setNodeStatus({ fill: "red", shape: "ring", text: "Unable to save the file " + sFileToBePlayed + " " + error.message }); throw (error); } } catch (error) { RED.log.error("ttsultimate: node id: " + node.id + " Error Downloading TTS: " + error.message + ". THE TTS SERVICE MAY BE DOWN."); node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'Error Downloading TTS:' + error.message }); sFileToBePlayed = ""; } } else { node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Reading offline from cache' }); } } // Ready to play if (sFileToBePlayed !== "") { //#region Now i am ready to play the file if (node.playertype === "sonos") { // Play with Sonos node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'Play ' + msg }); // Play directly files starting with http:// if (!sFileToBePlayed.toLowerCase().startsWith("http://") && !sFileToBePlayed.toLowerCase().startsWith("https://")) { sFileToBePlayed = node.sNoderedURL + "/tts/tts.mp3?f=" + encodeURIComponent(sFileToBePlayed); } // Set Volume try { let volTemp = 0; if (node.currentMSGbeingSpoken.hasOwnProperty("volume")) { volTemp = Number(node.currentMSGbeingSpoken.volume); } else { volTemp = Number(node.sSonosVolume); } await SETVOLUMESync(volTemp); if (node.unmuteIfMuted) await SETMutedSync(false); // 21/10/2021 Unmute if (node.oAdditionalSonosPlayers.length > 0) { // 05/07/2021 set the volume of additional coordinators for (let index = 0; index < node.oAdditionalSonosPlayers.length; index++) { let element = node.oAdditionalSonosPlayers[index].oPlayer; //node.oAdditionalSonosPlayers.push({ oPlayer: new sonos.Sonos(element.host), hostVolumeAdjust: element.hostVolumeAdjust }); try { // 12/04/20222 Set the adjusted volume, based on the main player volume + the adjusted volume in % let iAdjustedVol = Number(volTemp) + Number((node.oAdditionalSonosPlayers[index].hostVolumeAdjust || 0)); if (iAdjustedVol < 0) iAdjustedVol = 0; if (iAdjustedVol > 100) iAdjustedVol = 100; await element.setVolume(iAdjustedVol); if (node.unmuteIfMuted) await element.setMuted(false); // 21/10/2021 Unmute } catch (error) { RED.log.error("ttsultimate: Handlequeue: Unable to set the volume on additional player " + error.message); } }; }; } catch (error) { RED.log.error("ttsultimate: Unable to set the volume for " + sFileToBePlayed); } try { await setAVTransportURISync(sFileToBePlayed); // Wait for start playing var state = ""; if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); node.bTimeOutPlay = false; node.timerbTimeOutPlay = setTimeout(() => { node.bTimeOutPlay = true; }, 10000); while (state !== "playing" && !node.bTimeOutPlay) { try { //state = await node.SonosClient.getCurrentState(); state = await getCurrentStateSync(); } catch (error) { node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Error getCurrentState of playing ' + msg }); RED.log.error("ttsultimate: Error getCurrentState of playing " + error.message); throw new MessageEvent("Error getCurrentState of playing " + error.message); } } if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); switch (node.bTimeOutPlay) { case false: node.setNodeStatus({ fill: 'green', shape: 'dot', text: 'Playing ' + msg }); break; default: node.setNodeStatus({ fill: 'grey', shape: 'dot', text: 'Timeout waiting start play state: ' + msg }); break; } // Wait for end if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); node.bTimeOutPlay = false; state = ""; node.timerbTimeOutPlay = setTimeout(() => { node.bTimeOutPlay = true; }, 60000*10); // 10 minutes timeout while (state !== "stopped" && !node.bTimeOutPlay) { try { state = await getCurrentStateSync(); } catch (error) { node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'Error getCurrentState of stopped ' + msg }); RED.log.error("ttsultimate: Error getCurrentState of stopped " + error.message); throw new MessageEvent("Error getCurrentState of stopped " + error.message); } } if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); switch (node.bTimeOutPlay) { case false: node.setNodeStatus({ fill: 'green', shape: 'ring', text: 'End playing ' + msg }); break; default: node.setNodeStatus({ fill: 'grey', shape: 'dot', text: 'Timeout waiting end play state: ' + msg }); break; } } catch (error) { if (node.timerbTimeOutPlay !== null) clearTimeout(node.timerbTimeOutPlay); // Clear the player timeout RED.log.error("ttsultimate: Error HandleQueue for " + sFileToBePlayed + " " + error.message); node.setNodeStatus({ fill: 'red', shape: 'dot', text: 'Error ' + msg + " " + error.message }); } } else if (node.playertype === "noplayer") { // Output only the filename if (noPlayerFileArray === undefined || noPlayerFileArray === null) var noPlayerFileArray = []; noPlayerFileArray.push({ file: sFileToBePlayed }); } } //#endregion }; // End Loop // End task if (node.playertype === "sonos") { // Ends the tasks of Sonos // Ungroup speaker try { await ungroupSpeakersSync(); // Ungroup speakers } catch (error) { // Don't care. node.setNodeStatus({ fill: "red", shape: "ring", text: "Error ungrouping speakers: " + error.message }); } await delay(2000); // Resume music try { if (oCurTrack !== null && (!oCurTrack.hasOwnProperty("title") || oCurTrack.title.indexOf(".mp3") === -1)) { node.setNodeStatus({ fill: 'grey', shape: 'ring', text: "Resuming original queue..." }); await resumeMusicQueue(oCurTrack); node.setNodeStatus({ fill: 'green', shape: 'ring', text: "Done resuming queue." }); } else { // 28/08/2021 There was no queue playing. Delete the TTS from the queue node.setNodeStatus({ fill: 'green', shape: 'ring', text: "No queue to resume." }); } } catch (error) { node.setNodeStatus({ fill: 'red', shape: 'ring', text: "Error resuming queue: " + error.message }); } // 19/04/2022 Resume music queue of additional players for (let index = 0; index < node.oAdditionalSonosPlayers.length; index++) { let addPlayer = node.oAdditionalSonosPlayers[index].oPlayer; let trackAddPlayer = addPlayer.additionalPlayerCurrentTrack; if (trackAddPlayer !== null) { try { await resumeMusicQueue(trackAddPlayer, addPlayer); node.setNodeStatus({ fill: 'green', shape: 'ring', text: "Done resuming queue additional player " + addPlayer.host || "" }); } catch (error) { // Dont care RED.log.warn("ttsultimate: Error resuming music queue of additional player " + error.message + " " + addPlayer.host || ""); } } else { node.setNodeStatus({ fill: 'green', shape: 'ring', text: "No queue to resume for " + addPlayer.host || "" }); } } // Signal end playing let t = setTimeout(() => { node.msg.completed = true; node.currentMSGbeingSpoken = {}; node.send([{ passThroughMessage: node.passThroughMessage, payload: node.msg.completed }, null]); node.bBusyPlayingQueue = false node.server.whoIsUsingTheServer = ""; // Signal to other ttsultimate node, that i'm not using the Sonos device anymore }, 1000) } else if (node.playertype === "noplayer") { // End task if no player is selected. // Output the array of files // Signal end playing let t = setTimeout(() => { node.msg.completed = true; node.currentMSGbeingSpoken = {}; node.send([{ passThroughMessage: node.passThroughMessage, payload: node.msg.completed, filesArray: noPlayerFileArray }, null]); node.bBusyPlayingQueue = false node.server.whoIsUsingTheServer = ""; // Signal to other ttsultimate node, that i'm not using the Sonos device anymore }, 1000) } } catch (error) { // Should'nt be there RED.log.error("ttsultimate: BIG Error HandleQueue MAIN " + error.message); node.setNodeStatus({ fill: "grey", shape: "ring", text: "Error Handlequeue " + error.message }); node.flushQueue(); } } node.on('input', function (msg) { // if (msg.hasOwnProperty("banana")) { // node.SonosClient.setMuted(msg.banana); // return; // } // 05/01/2022 Set the passtrough message o come cazzo si scrive node.passThroughMessage = RED.util.cloneMessage(msg); // 09/01/2021 Set the main player and groups IP on request // ********************************* if (msg.hasOwnProperty("setConfig")) { if (msg.setConfig.hasOwnProperty("setMainPlayerIP")) { node.sSonosIPAddress = msg.setConfig.setMainPlayerIP; RED.log.info("ttsultimate: new main player set by msg: " + node.sSonosIPAddress); node.setNodeStatus({ fill: 'grey', shape: 'ring', text: "Main Player changed to " + node.sSonosIPAddress }); // RE-Create sonos client & groups node.SonosClient = new sonos.Sonos(node.sSonosIPAddress); node.SonosClient.getName().then(info => { node.sonosCoordinatorGroupName = info; RED.log.info("ttsultimate: new zone coordinator set by msg: " + JSON.stringify(info)); }).catch(err => { }); }; if (msg.setConfig.hasOwnProperty("setPlayerGroupArray")) { node.setNodeStatus({ fill: 'grey', shape: 'ring', text: "Group players changed" }); // Fill the node.oAdditionalSonosPlayers with all sonos IPs in the setPlayerGroupArray node.oAdditionalSonosPlayers = []; for (let index = 0; index < msg.setConfig.setPlayerGroupArray.length; index++) { const sRow = msg.setConfig.setPlayerGroupArray[index]; let host = ""; let hostVolumeAdjust = 0; if (sRow.includes("#")) { host = sRow.split("#")[0]; hostVolumeAdjust = sRow.split("#")[1]; } else { host = sRow; } //node.oAdditionalSonosPlayers.push({ oPlayer: new sonos.Sonos(element.host), hostVolumeAdjust: element.hostVolumeAdjust }); node.oAdditionalSonosPlayers.push({ oPlayer: new sonos.Sonos(host), hostVolumeAdjust: Number(hostVolumeAdjust) }); RED.log.info("ttsultimate: new group player set by msg: " + host + " adjusted volume: " + Number(hostVolumeAdjust)); } }; }; // ********************************* // In case of connection error, doesn't accept any message if (node.msg.connectionerror && node.playertype !== "noplayer") { RED.log.warn("ttsultimate: Sonos is offline. The new msg coming from the flow will be rejected."); node.setNodeStatus({ fill: 'red', shape: 'ring', text: "Sonos is offline. The msg has been rejected." }); return; } // 27/01/2021 Stop whatever in play. if (msg.hasOwnProperty("stop") && msg.stop === true) { node.flushQueue(); try { STOPSync(); } catch (error) { } node.setNodeStatus({ fill: 'red', shape: 'ring', text: "Forced stop." }); return; } if (!msg.hasOwnProperty("payload")) { notifyError(msg, 'msg.payload must be of type String'); return; } // 21/10/2021 force unmute if (msg.hasOwnProperty("unmute")) { node.unmuteIfMuted = msg.unmute; } // 05/12/2020 handling Hailing var hailingMSG = null; if (msg.hasOwnProperty("nohailing") && (msg.nohailing == "1" || msg.nohailing.toLowerCase() == "true")) { hailingMSG = null; } else { // Backward compatibiliyy, to remove with the next Version // ################ if (config.sonoshailing === "0") { // Remove the hailing.mp3 default file RED.log.info('ttsultimate: Hailing disabled'); } else if (config.sonoshailing == "1") { RED.log.warn("ttsultimate you've an old hailing setting. PLEASE SET AGAIN THE HAILING IN THE CONFIG NODE"); config.sonoshailing = "Hailing_Hailing.mp3"; } else if (config.sonoshailing == "2") { RED.log.warn("ttsultimate you've an old hailing setting. PLEASE SET AGAIN THE HAILING IN THE CONFIG NODE"); config.sonoshailing = "Hailing_ComputerCall.mp3"; } else if (config.sonoshailing == "3") { RED.log.warn("ttsultimate you've an old hailing setting. PLEASE SET AGAIN THE HAILING IN THE CONFIG NODE"); config.sonoshailing = "Hailing_VintageSpace.mp3"; } // ################ if (config.sonoshailing !== "0") { hailingMSG = { payload: config.sonoshailing }; } else { hailingMSG = null; } if (msg.hasOwnProperty("sonoshailing")) hailingMSG = { payload: "Hailing_" + msg.sonoshailing + ".mp3" }; } // 27/01/2021 Handling priority messages // #####################