/* jshint -W097 */ /* jshint strict: false */ /* jslint node: true */ 'use strict'; const utils = require(__dirname + '/lib/utils'); // Get common adapter utils const engines = require(__dirname + '/admin/engines.js'); const Text2Speech = require(__dirname + '/lib/text2speech'); const Speech2Device = require(__dirname + '/lib/speech2device'); const sayitOptions = engines.sayitOptions; const libs = {}; const adapter = new utils.Adapter({ name: 'sayit', unload: stop }); process.on('SIGINT', stop); adapter.on('stateChange', (id, state) => { if (state && !state.ack) { if (id === adapter.namespace + '.tts.volume') { if (adapter.config.type === 'system') { speech2device.sayItSystemVolume(state.val); } else { options.sayLastVolume = state.val; } } else if (id === adapter.namespace + '.tts.text') { if (typeof state.val !== 'string') { if (state.val === null || state.val === undefined || state.val === '') { adapter.log.warn('Cannot cache empty text'); return; } state.val = state.val.toString(); } sayIt(state.val); } else if (id === adapter.namespace + '.tts.cachetext') { if (typeof state.val !== 'string') { if (state.val === null || state.val === undefined || state.val === '') { adapter.log.warn('Cannot cache empty text'); return; } state.val = state.val.toString(); } cacheIt(state.val); } } }); adapter.on('ready', main); adapter.on('message', obj => { if (obj) processMessage(obj); processMessages(); }); function processMessage(obj) { if (obj) { if (obj.command === 'stopInstance') { stop(() => { if (obj.callback) { adapter.sendTo(obj.from, obj.command, null, obj.callback); } }); } else if (obj.command === 'browseChromecast') { try { const mdns = require('mdns'); let browser = mdns.createBrowser(mdns.tcp('googlecast')); const result = []; browser.on('serviceUp', service => result.push({name: service.name, ip: service.addresses[0]})); setTimeout(() => { browser.stop(); browser = null; if (obj.callback) { adapter.sendTo(obj.from, obj.command, result, obj.callback); } }, 2000); browser.start(); } catch (e) { adapter.log.error(e); if (obj.callback) adapter.sendTo(obj.from, obj.command, null, obj.callback); } } } } function processMessages() { adapter.getMessage((err, obj) => { if (obj) setTimeout(processMessages, 0); }); } function stop(callback) { try { if (adapter && adapter.log && adapter.log.info) { adapter.log.info('stopping...'); } setTimeout(() => process.exit(), 1000); if (typeof callback === 'function') callback(); } catch (e) { process.exit(); } } const options = { sayLastVolume: null, webLink: '', cacheDir: '' }; let sayLastGeneratedText = ''; let list = []; let lastSay = null; const text2speech = new Text2Speech(adapter, libs, options, sayIt); const speech2device = new Speech2Device(adapter, libs, options); const MP3FILE = __dirname + '/' + adapter.namespace + '.say.mp3'; function mkpathSync(rootpath, dirpath) { libs.fs = libs.fs || require('fs'); // Remove filename dirpath = dirpath.split('/'); dirpath.pop(); if (!dirpath.length) return; for (let i = 0; i < dirpath.length; i++) { rootpath += dirpath[i] + '/'; if (!libs.fs.existsSync(rootpath)) { if (dirpath[i] !== '..') { libs.fs.mkdirSync(rootpath); } else { throw 'Cannot create ' + rootpath + dirpath.join('/'); } } } } function sayFinished(error, duration) { if (error) { adapter.log.error(error); } duration = duration || 0; if (list.length) { adapter.log.debug('Duration "' + list[0].text + '": ' + duration); } setTimeout(() => { // Remember when last text finished lastSay = Date.now(); if (list.length) list.shift(); if (list.length) { sayIt(list[0].text, list[0].language, list[0].volume, true); } }, duration * 1000); } let cacheRunning = false; let cacheFiles = []; function cacheIt(text, language) { // process queue if (text === true) { if (!cacheFiles.length) { cacheRunning = false; return; } // get next queued text const toCache = cacheFiles.shift(); text = toCache.text; language = toCache.language; } else { // new text to cache if (!adapter.config.cache) { adapter.log.warn('Cache is not enabled. Unable to cache: ' + text); return; } // Extract language from "en;volume;Text to say" if (text.indexOf(';') !== -1) { const arr = text.split(';', 3); // If language;text or volume;text if (arr.length === 2) { // If number if (parseInt(arr[0]).toString() !== arr[0]) { language = arr[0]; } text = arr[1]; } else if (arr.length === 3) { // If language;volume;text or volume;language;text // If number if (parseInt(arr[0]).toString() === arr[0]) { language = arr[1]; } else { language = arr[0]; } text = arr[2]; } } // if no text => do not process if (!text.length) { return; } // Check: may be it is file from DB filesystem, like /vis.0/main/img/door-bell.mp3 if (text[0] === '/') { adapter.log.warn('mp3 file must not be cached: ' + text); return; } let isGenerate = false; if (!language) language = adapter.config.engine; // find out if say.mp3 must be generated if (!speech2device.sayItIsPlayFile(text)) isGenerate = sayitOptions[adapter.config.type].mp3Required; if (!isGenerate) { if (speech2device.sayItIsPlayFile(text)) { adapter.log.warn('mp3 file must not be cached: ' + text); } else { adapter.log.warn('Cache does not required for this engine: ' + adapter.config.engine); } return; } const md5filename = libs.path.join(options.cacheDir, libs.crypto.createHash('md5').update(language + ';' + text).digest('hex') + '.mp3'); libs.fs = libs.fs || require('fs'); if (libs.fs.existsSync(md5filename)) { adapter.log.debug('Text is yet cached: ' + text); return; } if (cacheRunning) { cacheFiles.push({text: text, language: language}); return; } } cacheRunning = true; text2speech.sayItGetSpeech(text, language, false, (error, md5filename, _language, volume, seconds) => { if (error) { adapter.log.error('Cannot cache text: "' + error); } else { adapter.log.debug('Text is cached: "' + text + '" under ' + md5filename); } setTimeout(function () { cacheIt(true); }, 2000); }); } function sayIt(text, language, volume, process) { let md5filename; // Extract language from "en;volume;Text to say" if (text.indexOf(';') !== -1) { const arr = text.split(';', 3); // If language;text or volume;text if (arr.length === 2) { // If number if (parseInt(arr[0]).toString() === arr[0].toString()) { volume = arr[0]; } else { language = arr[0]; } text = arr[1]; } else if (arr.length === 3) { // If language;volume;text or volume;language;text // If number if (parseInt(arr[0]).toString() === arr[0].toString()) { volume = arr[0]; language = arr[1]; } else { volume = arr[1]; language = arr[0]; } text = arr[2]; } } // if no text => do not process if (!text.length) { sayFinished(0); return; } // Check: may be it is file from DB filesystem, like /vis.0/main/img/door-bell.mp3 if (text[0] === '/') { let cached = false; if (adapter.config.cache) { md5filename = libs.path.join(options.cacheDir, libs.crypto.createHash('md5').update(text).digest('hex') + '.mp3'); if (libs.fs.existsSync(md5filename)) { cached = true; text = md5filename; } } if (!cached) { const parts = text.split('/'); const adap = parts[0]; parts.splice(0, 1); const _path = parts.join('/'); adapter.readFile(adap, _path, (err, data) => { if (data) { try { // Cache the file if (md5filename) libs.fs.writeFileSync(md5filename, data); libs.fs.writeFileSync(MP3FILE, data); sayIt(MP3FILE, language, volume, process); } catch (e) { adapter.log.error('Cannot write file "' + MP3FILE + '": ' + e.toString()); sayFinished(0); } } else { // may be file from real FS if (libs.fs.existsSync(text)) { try { data = libs.fs.readFileSync(text); } catch (e) { adapter.log.error('Cannot read file "' + text + '": ' + e.toString()); sayFinished(0); } // Cache the file if (md5filename) libs.fs.writeFileSync(md5filename, data); libs.fs.writeFileSync(MP3FILE, data); sayIt(MP3FILE, language, volume, process); } else { adapter.log.warn('File "' + text + '" not found'); sayFinished(0); } } }); return; } } if (!process) { const time = Date.now(); // Workaround for double text if (list.length > 1 && (list[list.length - 1].text === text) && (time - list[list.length - 1].time < 500)) { adapter.log.warn('Same text in less than half a second.. Strange. Ignore it.'); return; } // If more time than 15 seconds if (adapter.config.announce && !list.length && (!lastSay || (time - lastSay > adapter.config.annoTimeout * 1000))) { // place as first the announce mp3 list.push({text: adapter.config.announce, language: language, volume: (volume || adapter.config.volume) / 2, time: time}); // and then text list.push({text: text, language: language, volume: (volume || adapter.config.volume), time: time}); text = adapter.config.announce; volume = Math.round((volume || adapter.config.volume) / 100 * adapter.config.annoVolume); } else { list.push({text: text, language: language, volume: (volume || adapter.config.volume), time: time}); if (list.length > 1) return; } } adapter.log.info('saying: ' + text); let isGenerate = false; if (!language) { language = adapter.config.engine; } if (!volume && adapter.config.volume) volume = adapter.config.volume; // find out if say.mp3 must be generated if (!speech2device.sayItIsPlayFile(text)) { isGenerate = sayitOptions[adapter.config.type].mp3Required; } const speechFunction = speech2device.getFunction(adapter.config.type); // If text first must be generated if (isGenerate && sayLastGeneratedText !== '[' + language + ']' + text) { sayLastGeneratedText = '[' + language + ']' + text; text2speech.sayItGetSpeech(text, language, volume, (error, text, language, volume, duration) => { speechFunction(error, text, language, volume, duration, sayFinished); }); } else { if (speech2device.sayItIsPlayFile(text)) { text2speech.getLength(text, (error, duration) => { speechFunction(error, text, language, volume, duration, sayFinished); }); } else { if (!isGenerate) { speechFunction(null, text, language, volume, 0, sayFinished); } else if (adapter.config.cache) { md5filename = libs.path.join(options.cacheDir, libs.crypto.createHash('md5').update(language + ';' + text).digest('hex') + '.mp3'); if (libs.fs.existsSync(md5filename)) { text2speech.getLength(md5filename, (error, duration) => { speechFunction(error, md5filename, language, volume, duration, sayFinished); }); } else { sayLastGeneratedText = '[' + language + ']' + text; text2speech.sayItGetSpeech(text, language, volume, (error, text, language, volume, duration) => { speechFunction(error, text, language, volume, duration, sayFinished); }); } } else { text2speech.getLength(MP3FILE, (error, duration) => { speechFunction(error, text, language, volume, duration, sayFinished); }); } } } } function uploadFile(file, callback) { try { const stat = libs.fs.statSync(libs.path.join(__dirname + '/mp3/', file)); if (!stat.isFile()) { // ignore not a file if (callback) callback(); return; } } catch (e) { // ignore not a file if (callback) callback(); return; } adapter.readFile(adapter.namespace, 'tts.userfiles/' + file, (err, data) => { if (err || !data) { try { adapter.writeFile(adapter.namespace, 'tts.userfiles/' + file, libs.fs.readFileSync(libs.path.join(__dirname + '/mp3/', file)), () => { if (callback) callback(); }); } catch (e) { adapter.log.error('Cannot read file "' + __dirname + '/mp3/' + file + '": ' + e.toString()); if (callback) callback(); } } else { if (callback) callback(); } }); } function _uploadFiles(files, callback) { if (!files || !files.length) { adapter.log.info('All files uploaded'); if (callback) callback(); return; } uploadFile(files.pop(), () => setTimeout(_uploadFiles, 0, files, callback)); } function uploadFiles(callback) { if (libs.fs.existsSync(__dirname + '/mp3')) { adapter.log.info('Upload announce mp3 files'); _uploadFiles(libs.fs.readdirSync(__dirname + '/mp3'), callback); } else if (callback) { callback(); } } function start() { if (adapter.config.announce) { adapter.config.annoDuration = parseInt(adapter.config.annoDuration) || 0; adapter.config.annoTimeout = parseInt(adapter.config.annoTimeout) || 15; adapter.config.annoVolume = parseInt(adapter.config.annoVolume) || 70; // percent from actual volume if (!libs.fs.existsSync(libs.path.join(__dirname, adapter.config.announce))) { adapter.readFile(adapter.namespace, 'tts.userfiles/' + adapter.config.announce, (err, data) => { if (data) { try { libs.fs.writeFileSync(libs.path.join(__dirname, adapter.config.announce), data); adapter.config.announce = libs.path.join(__dirname, adapter.config.announce); } catch (e) { adapter.log.error('Cannot write file: ' + e.toString()); adapter.config.announce = ''; } } }); } else { adapter.config.announce = __dirname + '/' + adapter.config.announce; } } // If cache enabled if (adapter.config.cache) { if (adapter.config.cacheDir && (adapter.config.cacheDir[0] === '/' || adapter.config.cacheDir[0] === '\\')) { adapter.config.cacheDir = adapter.config.cacheDir.substring(1); } options.cacheDir = libs.path.join(__dirname, adapter.config.cacheDir); if (options.cacheDir) { options.cacheDir = options.cacheDir.replace(/\\/g, '/'); if (options.cacheDir[options.cacheDir.length - 1] === '/') { options.cacheDir = options.cacheDir.substring(0, options.cacheDir.length - 1); } } else { options.cacheDir = ''; } const parts = options.cacheDir.split('/'); let i = 0; while (i < parts.length) { if (parts[i] === '..') { parts.splice(i - 1, 2); i--; } else { i++; } } options.cacheDir = parts.join('/'); // Create cache dir if does not exist if (!libs.fs.existsSync(options.cacheDir)) { try { mkpathSync(__dirname + '/', adapter.config.cacheDir); } catch (e) { adapter.log.error('Cannot create "' + options.cacheDir + '": ' + e.message); } } else { let engine = ''; // Read the old engine if (libs.fs.existsSync(libs.path.join(options.cacheDir, 'engine.txt'))) { try { engine = libs.fs.readFileSync(libs.path.join(options.cacheDir, 'engine.txt')).toString(); } catch (e) { adapter.log.error('Cannot read file "' + libs.path.join(options.cacheDir, 'engine.txt') + ': ' + e.toString()); } } // If engine changed if (engine !== adapter.config.engine) { // Delete all files in this directory const files = libs.fs.readdirSync(options.cacheDir); for (let f = 0; f < files.length; f++) { if (files[f] === 'engine.txt') continue; if (libs.fs.existsSync(libs.path.join(options.cacheDir, files[f])) && libs.fs.lstatSync(libs.path.join(options.cacheDir, files[f])).isDirectory()) { libs.fs.unlinkSync(libs.path.join(options.cacheDir, files[f])); } } try { libs.fs.writeFileSync(libs.path.join(options.cacheDir, 'engine.txt'), adapter.config.engine); } catch (e) { adapter.log.error('Cannot write file "' + libs.path.join(options.cacheDir, 'engine.txt') + ': ' + e.toString()); } } } } // Load libs for (let j = 0; j < sayitOptions[adapter.config.type].libs.length; j++) { libs[sayitOptions[adapter.config.type].libs[j]] = require(sayitOptions[adapter.config.type].libs[j]); } adapter.getState('tts.text', (err, state) => { if (err || !state) { adapter.setState('tts.text', '', true); } }); adapter.getState('tts.volume', (err, state) => { if (err || !state) { adapter.setState('tts.volume', 70, true); if (adapter.config.type !== 'system') options.sayLastVolume = 70; } else { if (adapter.config.type !== 'system') options.sayLastVolume = state.val; } }); adapter.getState('tts.playing', (err, state) => { if (err || !state) { adapter.setState('tts.playing', false, true); } }); if (adapter.config.type === 'system') { // Read volume adapter.getState('tts.volume', (err, state) => { if (!err && state) { speech2device.sayItSystemVolume(state.val); } else { speech2device.sayItSystemVolume(70); } }); } // calculate weblink for devices that require it if ((adapter.config.type === 'sonos') || (adapter.config.type === 'chromecast') || (adapter.config.type === 'mpd') || (adapter.config.type === 'googleHome')) { adapter.getForeignObject('system.adapter.' + adapter.config.web, (err, obj) => { if (!err && obj && obj.native) { options.webLink = 'http'; if (obj.native.auth) { adapter.log.error('Cannot use server "' + adapter.config.web + '" with authentication for sonos/chromecast. Select other or create another one.'); } else { if (obj.native.secure) options.webLink += 's'; options.webLink += '://'; if (obj.native.bind === 'localhost' || obj.native.bind === '127.0.0.1') { adapter.log.error('Selected web server "' + adapter.config.web + '" is only on local device available. Select other or create another one.'); } else { if (obj.native.bind === '0.0.0.0') { options.webLink += adapter.config.webServer; } else { options.webLink += obj.native.bind; } } options.webLink += ':' + obj.native.port; } } else { adapter.log.error('Cannot read information about "' + adapter.config.web + '". No web server is active'); } }); } adapter.subscribeStates('*'); } function main() { libs.fs = require('fs'); libs.path = require('path'); if ((process.argv && process.argv.indexOf('--install') !== -1) || ((!process.argv || process.argv.indexOf('--force') === -1) && (!adapter.common || !adapter.common.enabled))) { adapter.log.info('Install process. Upload files and stop.'); // Check if files exists in datastorage uploadFiles(() => adapter.stop ? adapter.stop() : process.exit()); } else { // Check if files exists in datastorage uploadFiles(start); } }