iobroker.sayit
Version:
Text to speech interface for ioBroker.
997 lines • 42 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SayItAdapter = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_crypto_1 = require("node:crypto");
const engines_1 = require("./lib/engines");
const text2speech_1 = __importDefault(require("./lib/text2speech"));
const speech2device_1 = __importDefault(require("./lib/speech2device"));
const adapter_core_1 = require("@iobroker/adapter-core");
class SayItAdapter extends adapter_core_1.Adapter {
dataDir = (0, node_path_1.join)((0, adapter_core_1.getAbsoluteDefaultDataDir)(), 'sayit');
processMessageTimeout = null;
timeoutRunning = null;
lang;
sayLastGeneratedText = '';
lastSay = null;
fileExt = 'mp3';
text2speech = null;
speech2device = null;
MP3FILE;
tasks = [];
processing = false;
helloCounter = 1;
cacheDir = '';
outFileExt = 'mp3';
webLink = '';
options;
constructor(options = {}) {
super({
...options,
name: 'sayit',
ready: () => this.main(),
message: (obj) => obj && this.processMessage(obj),
stateChange: (id, state) => {
if (state && !state.ack) {
if (id === `${this.namespace}.tts.clearQueue`) {
if (this.tasks.length > 1) {
this.tasks.splice(1);
void this.setState('tts.clearQueue', false, true);
}
}
else if (id === `${this.namespace}.tts.volume`) {
if (this.config.type === 'system') {
void this.speech2device
?.sayItSystemVolume(state.val)
.catch((err) => this.log.error(`Cannot set volume: ${err}`));
}
else {
this.options.sayLastVolume = parseInt(state.val, 10);
}
}
else if (id === `${this.namespace}.tts.text`) {
if (typeof state.val !== 'string') {
if (state.val === null || state.val === undefined) {
return this.log.warn('Cannot cache empty text');
}
state.val = state.val.toString();
}
this.addToQueue({ text: state.val }).catch(e => this.log.error(`Cannot add to queue ${e.toString()}`));
}
else if (id === `${this.namespace}.tts.cachetext`) {
if (typeof state.val !== 'string') {
if (state.val === null || state.val === undefined) {
return this.log.warn('Cannot cache empty text');
}
state.val = state.val.toString();
}
this.addToQueue({ text: state.val }, true).catch(e => this.log.error(`Cannot add to queue ${e.toString()}`));
}
}
},
objectChange: (id, obj) => {
if (id === `system.adapter.${this.config.webInstance}`) {
this.webLink = this.getWebLink(obj, this.config.webServer, this.config.webInstance);
}
},
unload: (callback) => this.stopInstance(true, callback),
});
process.on('SIGINT', this.stopInstance);
}
async browseMdns(obj) {
try {
const mdns = await import('mdns');
let browser = mdns.default.createBrowser(mdns.tcp('googlecast'));
const result = [];
browser.on('serviceUp', service => result.push({ name: service.name || service.fullname, ip: service.addresses[0] }));
browser.on('error', (err) => this.log.error(`Error on MDNS discovery: ${err}`));
this.processMessageTimeout = setTimeout(() => {
this.processMessageTimeout = null;
if (browser) {
browser.stop();
browser = null;
}
if (obj.command === 'browseGoogleHome') {
this.sendTo(obj.from, obj.command, result.map(s => ({ label: `${s.name}[${s.ip}]`, value: s.ip })), obj.callback);
}
else {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
}, 2000);
browser.start();
}
catch (e) {
this.log.debug(`Cannot browse mdns: ${e}`);
this.sendTo(obj.from, obj.command, null, obj.callback);
}
}
processMessage(obj) {
if (obj.command === 'say') {
const text = obj.message?.text;
const language = obj.message?.language;
const volume = obj.message?.volume ? parseInt(obj.message.volume, 10) : undefined;
const browserVis = obj.message?.browserVis;
const browserInstance = obj.message?.browserInstance;
const sonosDevice = obj.message?.sonosDevice;
const heosDevice = obj.message?.heosDevice;
const mpdInstance = obj.message?.mpdInstance;
const chromecastDevice = obj.message?.chromecastDevice;
if (text) {
if (obj.callback) {
const testOptions = { ...obj.message };
testOptions.callback = (error) => {
this.sendTo(obj.from, obj.command, { error, result: error ? undefined : 'Ok' }, obj.callback);
};
this.addToQueue({ text, language, volume, testOptions }).catch(e => this.log.error(`Cannot add to queue ${e}`));
}
else {
this.addToQueue({
text,
language,
volume,
testOptions: {
engine: language,
type: this.config.type,
browserVis,
sonosDevice,
heosDevice,
mpdInstance,
browserInstance,
chromecastDevice,
},
}).catch(e => this.log.error(`Cannot add to queue ${e.toString()}`));
}
}
else {
this.sendTo(obj.from, obj.command, { error: 'No text' }, obj.callback);
}
}
else if (obj.command === 'stopInstance') {
this.stopInstance(false, () => {
if (obj.callback) {
this.sendTo(obj.from, obj.command, null, obj.callback);
}
});
}
else if (obj.callback && obj.command === 'browseGoogleHome') {
this.browseMdns(obj).catch(e => {
this.log.debug(`Cannot browse mdns: ${e}`);
if (obj.callback) {
this.sendTo(obj.from, obj.command, null, obj.callback);
}
});
}
else if (obj.callback && obj.command === 'browseChromecast') {
this.getObjectView('system', 'device', { startkey: 'chromecast.', endkey: 'chromecast.\u9999' }, (err, res) => {
const list = [];
if (!err && res) {
res.rows.forEach(row => {
let name = row.value?.common?.name;
if (typeof name === 'object') {
name = name[this.lang] || name.en;
}
list.push({ value: row.id, label: `${name} [${row.id}]` });
});
}
this.sendTo(obj.from, obj.command, list, obj.callback);
});
}
else if (obj.callback && obj.command === 'browseHeos') {
this.getObjectView('system', 'device', { startkey: 'heos.', endkey: 'heos.\u9999' }, (err, res) => {
const list = [];
res?.rows.forEach(row => {
let name = row.value?.common?.name;
if (typeof name === 'object') {
name = name[this.lang] || name.en;
}
if (row.id.includes('.players.')) {
list.push({
value: row.id,
label: `${row.id.replace(/^heos\.\d+\.players\./, '')} [${name}]`,
});
}
});
this.sendTo(obj.from, obj.command, list, obj.callback);
});
}
else if (obj.callback && obj.command === 'browseSonos') {
this.getObjectView('system', 'device', { startkey: 'sonos.', endkey: 'heos.\u9999' }, (err, res) => {
const list = [];
res?.rows.forEach(row => {
let name = row.value?.common?.name;
if (typeof name === 'object') {
name = name[this.lang] || name.en;
}
if (row.id.includes('.players.')) {
list.push({
value: row.id,
label: `${row.id.replace(/^sonos\.\d+\.root\./, '')} [${name}]`,
});
}
});
this.sendTo(obj.from, obj.command, list, obj.callback);
});
}
else if (obj.callback && obj.command === 'test') {
const language = (obj.message?.engine || this.config.engine).substring(0, 2);
let text = 'Hello';
if (language === 'de') {
text = 'Hallo';
}
else if (language === 'pl') {
text = 'Cześć';
}
else if (language === 'uk') {
text = 'Привіт';
}
else if (language === 'ru') {
text = 'Привет';
}
else if (language === 'it') {
text = 'Ciao';
}
else if (language === 'pt') {
text = 'Olá';
}
else if (language === 'es') {
text = 'Hola';
}
else if (language === 'fr') {
text = 'Bonjour';
}
else if (language === 'nl') {
text = 'Hallo';
}
else if (language === 'zh') {
text = '你好';
}
text += ` ${this.helloCounter++}`;
const testOptions = { ...obj.message };
if (obj.callback) {
testOptions.callback = (error) => {
this.sendTo(obj.from, obj.command, { error, result: error ? undefined : 'Ok' }, obj.callback);
};
}
this.addToQueue({ text, testOptions }).catch(e => this.log.error(`Cannot add to queue ${e}`));
}
}
stopInstance = (unload, callback) => {
if (this.processMessageTimeout) {
clearTimeout(this.processMessageTimeout);
this.processMessageTimeout = null;
}
if (this.timeoutRunning) {
clearTimeout(this.timeoutRunning);
this.timeoutRunning = null;
}
try {
this?.log?.info?.('stopping...');
}
catch {
// ignore
}
if (typeof callback === 'function') {
callback();
}
if (!unload) {
setTimeout(() => (this.terminate ? this.terminate() : process.exit()), 500);
}
};
static mkpathSync(rootPath, dirPath) {
// Remove filename
const dirPathArr = dirPath.split('/');
dirPathArr.pop();
if (!dirPathArr.length) {
return;
}
for (let i = 0; i < dirPathArr.length; i++) {
rootPath += `${dirPathArr[i]}/`;
if (!(0, node_fs_1.existsSync)(rootPath)) {
if (dirPathArr[i] !== '..') {
(0, node_fs_1.mkdirSync)(rootPath);
}
else {
throw new Error(`Cannot create ${rootPath}${dirPathArr.join('/')}`);
}
}
}
}
addToQueue = async (props, onlyCache) => {
// Extract language from "en;volume;Text to say"
if (props.text.includes(';')) {
const arr = props.text.split(';', 3);
// If "language;text" or "volume;text"
if (arr.length === 2) {
// If number
if (parseInt(arr[0]).toString() === arr[0].toString()) {
props.volume = parseInt(arr[0].trim(), 10);
}
else {
props.language = arr[0].trim();
}
props.text = arr[1].trim();
}
else if (arr.length === 3) {
// If language;volume;text or volume;language;text
// If number
if (parseInt(arr[0]).toString() === arr[0].toString()) {
props.volume = parseInt(arr[0].trim(), 10);
props.language = arr[1].trim();
}
else {
props.volume = parseInt(arr[1].trim(), 10);
props.language = arr[0].trim();
}
props.text = arr[2].trim();
}
}
// Workaround for double text
// find all similar texts with interval less han 500 ms
const combined = [props.text, props.language || '', props.volume || ''].filter(t => t).join(';');
if (this.tasks.find(task => task.combined === combined && Date.now() - task.ts < 500)) {
// ignore it
return;
}
const highPriority = props.text.startsWith('!');
props.volume ||= parseInt(this.config.volume, 10);
if (Number.isNaN(props.volume)) {
props.volume = undefined;
}
if (props.volume === undefined || props.volume === null) {
try {
const state = await this.getStateAsync('tts.volume');
if (state?.val) {
props.volume = parseInt(state.val, 10);
}
}
catch {
// ignore
}
}
let announce = props.testOptions?.announce !== undefined ? props.testOptions.announce : this.config.announce;
const annoTimeout = parseInt(props.testOptions?.annoTimeout !== undefined
? props.testOptions.annoTimeout
: this.config.annoTimeout, 10);
const task = {
text: props.text,
language: props.language || (props.testOptions && props.testOptions.engine) || this.config.engine,
volume: props.volume,
onlyCache,
ts: Date.now(),
combined,
testOptions: props.testOptions,
};
// If more time than 15 seconds till last text, add announcement
if (!onlyCache &&
announce &&
!this.tasks.length &&
(!this.lastSay || Date.now() - this.lastSay > annoTimeout * 1000)) {
if (props.testOptions) {
await this.prepareAnnounceFiles(props.testOptions);
}
const annoVolumeInPercent = parseInt(props.testOptions?.annoVolume !== undefined
? props.testOptions.annoVolume
: this.config.annoVolume, 10);
announce = props.testOptions?.announce !== undefined ? props.testOptions.announce : this.config.announce;
// We take the percent from actual volume
const annoVolume = Math.round(((props.volume || 70) / 100) * (annoVolumeInPercent || 50));
// place as first the announcement mp3
this.tasks.push({
combined: [announce, task.language, annoVolume].filter(t => t).join(';'),
text: announce,
language: task.language,
volume: annoVolume,
ts: task.ts,
testOptions: props.testOptions,
});
// and then text
this.tasks.push(task);
}
else if (!onlyCache && highPriority) {
this.tasks.unshift(task);
}
else {
this.tasks.push(task);
}
this.processTasks().catch(() => { });
};
getCachedFileName = (text) => {
return (0, node_path_1.normalize)((0, node_path_1.join)(this.cacheDir, `${(0, node_crypto_1.createHash)('md5').update(text).digest('hex')}.${this.fileExt}`));
};
isCached = (text) => {
const md5filename = this.getCachedFileName(text);
if ((0, node_fs_1.existsSync)(md5filename)) {
if (this.config.cacheExpiryDays) {
const fileStat = (0, node_fs_1.statSync)(md5filename);
if (fileStat.ctime &&
Date.now() - new Date(fileStat.ctime).getTime() > this.config.cacheExpiryDays * 1000 * 60 * 60 * 24) {
this.log.info('Cached File expired, remove and re-generate');
(0, node_fs_1.unlinkSync)(md5filename);
return false;
}
}
return md5filename;
}
return false;
};
async processTasks() {
if (this.processing) {
return;
}
this.processing = true;
const { onlyCache, testOptions } = this.tasks[0];
let { text, language, volume } = this.tasks[0];
let error;
if (text[0] === '!') {
text = text.substring(1);
}
const type = testOptions?.type || this.config.type;
if (volume === undefined || volume === null) {
try {
const state = await this.getStateAsync('tts.volume');
if (state?.val) {
volume = parseInt(state.val, 10);
}
}
catch {
// ignore
}
}
volume ||= parseInt(testOptions?.volume || this.config.volume, 10);
if (Number.isNaN(volume)) {
volume = undefined;
}
let fileName;
// find out if say.mp3 must be generated
const isGenerate = !speech2device_1.default.isPlayFile(text) && engines_1.sayitOptions[type].mp3Required;
language ||= (testOptions && testOptions.engine) || this.config.engine;
// if no text => does not process
if (isGenerate && text.length && this.text2speech && this.speech2device) {
// Check: may be it is a file from DB filesystem, like /vis.0/main/img/door-bell.mp3
if (text[0] === '/') {
let fileNameTemp;
if (!testOptions && (this.config.cache || onlyCache)) {
fileNameTemp = this.isCached(text);
}
if (!fileNameTemp) {
const parts = text.split('/');
const _adapter = parts[0];
parts.shift();
const _path = parts.join('/');
let data;
try {
data = await this.readFileAsync(_adapter, _path);
}
catch {
// this.log.error(`Cache file does not exist "${text}": ${e.toString()}`);
}
if (!data) {
// maybe the file is from real FS
if ((0, node_fs_1.existsSync)(text)) {
try {
data = { file: (0, node_fs_1.readFileSync)(text) };
}
catch (e) {
this.log.error(`Cannot read file "${text}": ${e.toString()}`);
}
}
else {
this.log.warn(`File "${text}" not found`);
}
}
if (data?.file) {
try {
// Cache the file
if (this.config.cache || onlyCache) {
// get file name for cache
fileName = this.getCachedFileName(text);
}
else {
fileName = this.MP3FILE;
}
(0, node_fs_1.writeFileSync)(fileName, data.file);
}
catch (e) {
this.log.error(`Cannot write file "${this.MP3FILE}": ${e.toString()}`);
}
}
}
else {
fileName = fileNameTemp;
}
}
this.log.info(`saying: ${text}`);
// If a text first must be generated, and it is different from the last one
if (!fileName && isGenerate) {
// do not cache if test options active, to test the voice generation too
if (this.sayLastGeneratedText !== `[${language}]${text}` || testOptions) {
if (this.config.cache && !testOptions) {
const md5filename = this.isCached(`${language};${text}`);
if (md5filename) {
fileName = md5filename;
}
}
if (!fileName) {
try {
fileName = await this.text2speech.sayItGetSpeech({
type,
text,
language,
volume,
testOptions,
});
this.sayLastGeneratedText = `[${language}]${text}`;
}
catch (e) {
fileName = '';
error = `Cannot generate speech file: ${e}`;
this.log.error(error);
}
}
}
else {
fileName = this.MP3FILE;
}
}
}
const props = { type, text, language, volume, testOptions, duration: 0 };
if (!onlyCache && text.length) {
await this.setStateAsync('tts.playing', true, true);
try {
// play file
if (fileName && this.text2speech && this.speech2device) {
props.duration = (await this.text2speech.getDuration(fileName)) || 0;
props.fileName = fileName;
await this.speech2device.playFile(props);
}
else if (!isGenerate) {
if (speech2device_1.default.isPlayFile(text) && this.text2speech) {
props.duration = (await this.text2speech.getDuration(text)) || 0;
}
await this.speech2device?.playFile(props);
}
this.lastSay = Date.now();
}
catch (e) {
error = `Cannot play file: ${e}`;
this.log.error(error);
}
await this.setStateAsync('tts.playing', false, true);
}
if (this.tasks[0]?.testOptions?.callback) {
this.tasks[0].testOptions.callback(error);
this.tasks[0].testOptions.callback = undefined;
}
this.tasks.shift();
if (this.tasks.length) {
this.timeoutRunning = setTimeout(() => {
this.timeoutRunning = null;
this.processing = false;
this.processTasks().catch(e => this.log.error(`Cannot process tasks: ${e.toString()}`));
}, 100 + props.duration * 1000);
}
else {
this.processing = false;
}
}
async uploadFile(file) {
try {
const stat = (0, node_fs_1.statSync)((0, node_path_1.join)(`${__dirname}/mp3/`, file));
if (!stat.isFile()) {
// ignore not a file
return;
}
}
catch {
// ignore not a file
return;
}
let data;
try {
data = await this.readFileAsync(this.namespace, `tts.userfiles/${file}`);
}
catch {
// ignore error
}
if (!data) {
try {
data = (0, node_fs_1.readFileSync)((0, node_path_1.join)(`${__dirname}/mp3/`, file));
this.log.debug(`Upload file: ${(0, node_path_1.join)(`${__dirname}/mp3/`, file)} (${data.length} bytes`);
await this.writeFileAsync(this.namespace, `tts.userfiles/${file}`, data);
}
catch (e) {
this.log.error(`Cannot write file "${__dirname}/mp3/${file}": ${e.toString()}`);
}
}
}
async uploadFiles() {
if ((0, node_fs_1.existsSync)(`${__dirname}/mp3`)) {
this.log.info('Upload announce mp3 files');
let obj;
try {
obj = await this.getForeignObjectAsync(this.namespace);
}
catch {
// ignore
}
if (!obj) {
await this.setForeignObjectAsync(this.namespace, {
type: 'meta',
common: {
name: 'User files for SayIt',
type: 'meta.user',
},
native: {},
});
}
const files = (0, node_fs_1.readdirSync)(`${__dirname}/mp3`);
for (let f = 0; f < files.length; f++) {
await this.uploadFile(files[f]);
}
}
}
async prepareAnnounceFiles(config) {
if (config.announce) {
config.annoDuration = parseInt(config.annoDuration) || 0;
config.annoTimeout = parseInt(config.annoTimeout) || 15;
config.annoVolume = parseInt(config.annoVolume) || 70; // percent from actual volume
// remove "tts.userfiles/" from file name
const fileName = config.announce.split('/').pop();
if (fileName && !(0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, fileName))) {
try {
const data = await this.readFileAsync(this.namespace, `tts.userfiles/${fileName}`);
if (data?.file) {
try {
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(__dirname, fileName), data.file);
config.announce = (0, node_path_1.join)(__dirname, fileName);
}
catch (e) {
this.log.error(`Cannot write file: ${e.toString()}`);
config.announce = '';
}
}
}
catch (e) {
this.log.error(`Cannot read file: ${e.toString()}`);
config.announce = '';
}
}
else if (fileName) {
config.announce = (0, node_path_1.join)(__dirname, fileName);
}
}
}
async start() {
if (!this.config.convertedV1toV2) {
const newConfig = JSON.parse(JSON.stringify(this.config));
if (newConfig.type === 'system') {
newConfig.systemCommand = newConfig.command;
newConfig.systemPlayer = newConfig.player;
}
else if (newConfig.type === 'mp24ftp') {
newConfig.mp24Server = newConfig.server;
newConfig.ftpUser = newConfig.user;
newConfig.ftpPort = newConfig.port;
newConfig.ftpPassword = newConfig.pass;
}
else if (newConfig.type === 'mp24') {
newConfig.mp24Server = newConfig.server;
}
else if (newConfig.type === 'chromecast') {
newConfig.chromecastDevice = newConfig.cDevice;
}
else if (newConfig.type === 'googleHome') {
newConfig.googleHomeServer = newConfig.server;
}
else if (newConfig.type === 'sonos') {
newConfig.sonosDevice = newConfig.device;
}
else if (newConfig.type === 'browser') {
newConfig.browserInstance = newConfig.instance;
}
else if (newConfig.type === 'mpd') {
newConfig.mpdInstance = newConfig.mpd_device;
}
else if (newConfig.type === 'heos') {
newConfig.heosDevice = newConfig.heos_device;
}
newConfig.webInstance = newConfig.web;
delete newConfig.server;
delete newConfig.mpd_device;
delete newConfig.heos_device;
delete newConfig.web;
delete newConfig.command;
delete newConfig.player;
delete newConfig.user;
delete newConfig.port;
delete newConfig.pass;
delete newConfig.cDevice;
delete newConfig.instance;
delete newConfig.sonos;
delete newConfig.googleHome;
delete newConfig.device;
if (newConfig.engine === 'ru_YA_CLOUD') {
newConfig.yandexKey = newConfig.key;
newConfig.yandexCloudVoice = newConfig.voice;
newConfig.yandexFolderID = newConfig.folderID;
newConfig.yandexEmotion = newConfig.emotion;
}
else if (newConfig.engine === 'ru_YA') {
newConfig.yandexKey = newConfig.key;
newConfig.yandexVoice = newConfig.voice;
newConfig.yandexEmotion = newConfig.emotion;
newConfig.yandexDrunk = newConfig.drunk;
newConfig.yandexIll = newConfig.ill;
newConfig.yandexRobot = newConfig.robot;
}
else if (newConfig.engine.includes('_CLOUD_')) {
newConfig.cloudInstance = newConfig.cloud;
}
else if (newConfig.engine.includes('_AP_')) {
newConfig.awsAccessKey = newConfig.accessKey;
newConfig.awsSecretKey = newConfig.secretKey;
newConfig.awsRegion = newConfig.region;
}
delete newConfig.accessKey;
delete newConfig.secretKey;
delete newConfig.region;
delete newConfig.robot;
delete newConfig.ill;
delete newConfig.drunk;
delete newConfig.emotion;
delete newConfig.voice;
delete newConfig.key;
delete newConfig.folderID;
delete newConfig.cloud;
newConfig.convertedV1toV2 = true;
const configObj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
if (configObj) {
configObj.native = newConfig;
await this.setForeignObjectAsync(configObj._id, configObj);
// wait for restart
return;
}
throw new Error('Cannot get instance config object');
}
if (this.config.browserVis === undefined) {
const configObj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`);
if (configObj) {
configObj.native.browserVis = '';
await this.setForeignObjectAsync(configObj._id, configObj);
// wait for restart
return;
}
throw new Error('Cannot get instance config object');
}
this.config.browserVis = this.config.browserVis.toString();
const systemConfig = await this.getForeignObjectAsync('system.config');
this.lang = systemConfig?.common?.language || 'de';
this.config.engine ||= this.lang || 'de';
if (this.config.engine === 'ru_YA_CLOUD') {
this.fileExt = 'ogg';
}
else {
this.fileExt = 'mp3';
}
this.MP3FILE = (0, node_path_1.normalize)((0, node_path_1.join)(this.dataDir, `${this.namespace}.say.${this.fileExt}`));
this.outFileExt = this.fileExt;
await this.prepareAnnounceFiles(this.config);
// If cache enabled
if (this.config.cache) {
if (this.config.cacheDir && (this.config.cacheDir[0] === '/' || this.config.cacheDir[0] === '\\')) {
this.config.cacheDir = this.config.cacheDir.substring(1);
}
this.cacheDir = (0, node_path_1.join)(__dirname, this.config.cacheDir);
if (this.cacheDir) {
this.cacheDir = this.cacheDir.replace(/\\/g, '/');
if (this.cacheDir[this.cacheDir.length - 1] === '/') {
this.cacheDir = this.cacheDir.substring(0, this.cacheDir.length - 1);
}
}
else {
this.cacheDir = '';
}
const parts = this.cacheDir.split('/');
let i = 0;
while (i < parts.length) {
if (parts[i] === '..') {
parts.splice(i - 1, 2);
i--;
}
else {
i++;
}
}
this.cacheDir = parts.join('/');
// Create cache directory, if it does not exist
if (!(0, node_fs_1.existsSync)(this.cacheDir)) {
try {
(0, node_fs_1.mkdirSync)(this.cacheDir, { recursive: true });
this.log.info(`Cache directory "${this.cacheDir}" created`);
}
catch (e) {
this.log.error(`Cannot create "${this.cacheDir}": ${e.message}`);
}
}
else {
let engine = '';
// Read the old engine
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(this.cacheDir, 'engine.txt'))) {
try {
engine = (0, node_fs_1.readFileSync)((0, node_path_1.join)(this.cacheDir, 'engine.txt')).toString();
}
catch (e) {
this.log.error(`Cannot read file "${(0, node_path_1.join)(this.cacheDir, 'engine.txt')}: ${e.toString()}`);
}
}
// If engine changed
if (engine !== this.config.engine) {
// Delete all files in this directory
const files = (0, node_fs_1.readdirSync)(this.cacheDir);
for (let f = 0; f < files.length; f++) {
if (files[f] === 'engine.txt') {
continue;
}
try {
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(this.cacheDir, files[f])) &&
(0, node_fs_1.lstatSync)((0, node_path_1.join)(this.cacheDir, files[f])).isDirectory()) {
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(this.cacheDir, files[f]));
}
}
catch (e) {
this.log.error(`Cannot remove cache file "${(0, node_path_1.join)(this.cacheDir, files[f])}: ${e.toString()}`);
}
}
try {
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(this.cacheDir, 'engine.txt'), this.config.engine);
}
catch (e) {
this.log.error(`Cannot write file "${(0, node_path_1.join)(this.cacheDir, 'engine.txt')}: ${e.toString()}`);
}
}
}
}
// initialize tts.text
await this.setStateAsync('tts.playing', false, true);
// calculate weblink for devices that require it
if (this.config.type === 'sonos' ||
this.config.type === 'heos' ||
this.config.type === 'chromecast' ||
this.config.type === 'mpd' ||
this.config.type === 'googleHome') {
const obj = await this.getForeignObjectAsync(`system.adapter.${this.config.webInstance}`);
this.webLink = this.getWebLink(obj, this.config.webServer, this.config.webInstance);
// update web link on changes
await this.subscribeForeignObjectsAsync(`system.adapter.${this.config.webInstance}`);
}
// initialize tts.text
let textState;
try {
textState = await this.getStateAsync('tts.text');
}
catch {
// ignore
}
if (!textState) {
await this.setStateAsync('tts.text', '', true);
}
// create Text2Speech and Speech2Device
this.options = {
outFileExt: this.outFileExt,
addToQueue: this.addToQueue,
getCachedFileName: this.getCachedFileName,
isCached: this.isCached,
MP3FILE: this.MP3FILE,
sayLastVolume: 70,
webLink: this.webLink,
getWebLink: this.getWebLink,
};
try {
this.text2speech = new text2speech_1.default(this, this.options);
this.speech2device = new speech2device_1.default(this, this.options);
}
catch (e) {
this.log.error(`Cannot initialize engines: ${e.toString()}`);
return;
}
// initialize tts.volume
let volumeState;
try {
volumeState = await this.getStateAsync('tts.volume');
}
catch {
// ignore
}
if (!volumeState) {
await this.setStateAsync('tts.volume', 70, true);
if (this.config.type !== 'system') {
this.options.sayLastVolume = 70;
}
else {
await this.speech2device?.sayItSystemVolume(70);
}
}
else {
if (this.config.type !== 'system') {
this.options.sayLastVolume = parseInt(volumeState.val, 10);
}
else {
await this.speech2device?.sayItSystemVolume(parseInt(volumeState.val, 10));
}
}
this.subscribeStates('*');
}
getWebLink = (obj, webServer, webInstance) => {
let webLink = '';
if (obj?.native) {
webLink = 'http';
if (obj.native.auth) {
this.log.error(`Cannot use server "${obj._id}" with authentication for sonos/heos/chromecast. Select other or create another one.`);
}
else {
if (obj.native.secure) {
webLink += 's';
}
webLink += '://';
if (obj.native.bind === 'localhost' || obj.native.bind === '127.0.0.1') {
this.log.error(`Selected web server "${obj._id}" is only on local device available. Select other or create another one.`);
}
else {
if (obj.native.bind === '0.0.0.0') {
webLink += webServer || this.config.webServer;
}
else {
webLink += obj.native.bind;
}
}
webLink += `:${obj.native.port}`;
}
}
else {
this.log.error(`Cannot read information about "${webInstance || this.config.webInstance}". No web server is active`);
}
return webLink;
};
async main() {
try {
// create directory
if (!(0, node_fs_1.existsSync)(this.dataDir)) {
(0, node_fs_1.mkdirSync)(this.dataDir, { recursive: true });
}
}
catch (err) {
this.log.error(`Could not create Storage directory: ${err}`);
this.dataDir = __dirname;
}
if (process.argv?.includes('--install') ||
(!process.argv?.includes('--force') && // If no arguments or no --force
!this.common?.enabled && // And adapter is not enabled
!process.argv?.includes('--debug')) // and not debug
) {
this.log.info('Install process. Upload files and stop.');
// Check if files exists in data storage
await this.uploadFiles();
this.stopInstance();
}
else {
// Check if files exists in data storage
await this.uploadFiles();
await this.start();
}
}
}
exports.SayItAdapter = SayItAdapter;
// If started as allInOne mode => return function to create instance
if (require.main !== module) {
// Export the constructor in compact mode
module.exports = (options) => new SayItAdapter(options);
}
else {
// otherwise start the instance directly
(() => new SayItAdapter())();
}
//# sourceMappingURL=main.js.map