Browse Source

initial files

master
vampirefrog 2 years ago
parent
commit
1fa4c9bec2
  1. 2
      .gitignore
  2. 9
      config.js.example
  3. 373
      index.js
  4. 18
      package.json
  5. 11
      songprovider.js
  6. 68
      soundcloud.js
  7. 137
      static/SoundManager.js
  8. BIN
      static/frog.ico
  9. BIN
      static/img/cookie.png
  10. BIN
      static/img/diamond.png
  11. BIN
      static/img/icecream1.png
  12. BIN
      static/img/icecream2.png
  13. BIN
      static/img/icecream3.png
  14. BIN
      static/img/icecream4.png
  15. BIN
      static/img/ninjaghini.png
  16. BIN
      static/img/ninjet.png
  17. 16
      static/index.html
  18. BIN
      static/sounds/diamond.mp3
  19. BIN
      static/sounds/follow.mp3
  20. BIN
      static/sounds/host.mp3
  21. BIN
      static/sounds/icecream.mp3
  22. BIN
      static/sounds/lemon.mp3
  23. BIN
      static/sounds/ninjaghini.mp3
  24. BIN
      static/sounds/ninjet.mp3
  25. BIN
      static/sounds/pop.mp3
  26. BIN
      static/sounds/sub.mp3
  27. 42
      static/tts.css
  28. 333
      static/tts.js
  29. 61
      static/util.js
  30. BIN
      template.sqlite
  31. 3321
      voices.json
  32. 63
      youtube.js

2
.gitignore

@ -0,0 +1,2 @@
config.js
node_modules/

9
config.js.example

@ -0,0 +1,9 @@
module.exports = {
streamer: 'vampirefrog',
botUsername: '',
botPassword: '',
port: 3080,
sounds: {
default: 'pop.mp3'
}
};

373
index.js

@ -0,0 +1,373 @@
const
https = require('https'),
http = require('http'),
express = require('express'),
ExpressWS = require('express-ws'),
WebSocket = require('ws'),
child_process = require('child_process'),
fs = require('fs'),
Temp = require('temp'),
sqlite3 = require('sqlite3'),
DLiveChat = require('./streamchat/DLiveChat'),
TwitchChat = require('./streamchat/TwitchChat'),
util = require('./static/util'),
YouTube = require('./youtube'),
SoundCloud = require('./soundcloud');
var config = require('./config');
var voices = JSON.parse(fs.readFileSync('./voices.json'));
const SQLITE_TEMPLATE = 'template.sqlite';
const SQLITE_TEMPLATE_PATH = __dirname+'/'+SQLITE_TEMPLATE;
const SQLITE_FILE = 'tts.sqlite';
const SQLITE_FILE_PATH = __dirname+'/'+SQLITE_FILE;
if(!fs.existsSync(SQLITE_FILE_PATH)) {
console.warn('Could not find', SQLITE_FILE, ', copying it from', SQLITE_TEMPLATE);
if(fs.existsSync(SQLITE_TEMPLATE_PATH)) {
fs.copyFileSync(SQLITE_TEMPLATE_PATH, SQLITE_FILE_PATH);
} else {
console.error('Could not find', SQLITE_TEMPLATE);
process.exit(-1);
}
}
var db = new sqlite3.Database(SQLITE_FILE_PATH, (err) => {
if(err) {
console.error('Error initializing sqlite', err.message);
process.exit(1);
} else {
console.log(new Date(), 'Connected to SQLite database', SQLITE_FILE_PATH);
}
});
var songProviders = [
new YouTube(),
new SoundCloud()
];
Temp.track();
var app = express();
app.use(express.static('static'));
app.get('/speech', (req, res, next) => {
var url = 'https://code.responsivevoice.org/getvoice.php?'+util.buildQueryString(req.query);
var h = https.get(url, {
headers: { referer: 'https://responsivevoice.org/' }
}, (r) => {
if(r.statusCode !== 200) {
res.status(r.statusCode);
res.send('Error '+r.statusCode);
} else {
res.append('Content-type', r.headers['content-type']);
r.on('data', (chunk) => {
res.write(chunk);
});
r.on('end', () => {
res.end();
});
r.on('error', (err) => {
res.status(500);
res.end(err.message);
});
}
});
h.on('error', (err) => {
res.status(500);
res.end(err.errno);
});
});
app.get('/stickerInfo/:id', (req, res, next) => {
var url = 'https://vampi.tech/wp-admin/admin-ajax.php?'+util.buildQueryString({ action: 'sticker_search', sticker_id: req.params.id});
var h = https.get(url, {
headers: { referer: 'https://vampi.tech/' }
}, (r) => {
res.append('Content-type', r.headers['content-type']);
r.on('error', (err) => {
res.status(500);
res.end(err.message);
});
r.on('data', (chunk) => {
res.write(chunk);
});
r.on('end', () => {
res.status(r.statusCode);
res.end();
});
});
h.on('error', (err) => {
res.status(500);
res.end(err.errno);
});
});
app.get('/config.js', (req, res, next) => {
res.append('Content-type', 'application/javascript; charset = utf8');
res.send('var config = '+JSON.stringify({
url: 'http://localhost:'+config.port,
socketUrl: 'ws://localhost:'+config.port+'/socket',
speechUrl: 'http://localhost:'+config.port+'/speech',
stickerInfoUrl: 'http://localhost:'+config.port+'/stickerInfo',
sounds: config.sounds
})+';');
});
var server = http.createServer({}, app);
server.listen(config.port);
var expressWs = ExpressWS(app, server);
app.ws('/socket', function(ws, req) {
ws.on('message', function(msg) {
console.log(msg);
});
console.log('socket', req.testing);
});
const serverWS = expressWs.getWss('/socket');
console.log('Listening on localhost:'+config.port);
function getUser(platform, username, cb) {
let sql = `SELECT * FROM users WHERE platform = ? AND username = ?`;
db.get(sql, [ platform, username ], (err, user) => {
if(err) {
cb(err);
} else if(!user) {
db.run(`
INSERT INTO users (
platform,
username
) VALUES(
?,
?
)
`, [
platform,
username
], function(err) {
if(err) {
cb(`Error inserting user ${platform}/${username}: ${err}`);
} else {
let sql = `SELECT * FROM users WHERE id = ?`;
db.get(sql, [ this.lastID ], (err, user) => {
cb(err, user);
});
}
});
} else {
cb(null, user);
}
});
}
function getResponsiveVoiceData(voice) {
for(let i in voices.responsivevoices) {
var voiceData = voices.responsivevoices[i];
if(voiceData.name.toLowerCase() == voice.toLowerCase()) {
var voiceParams = null;
for(let j in voiceData.voiceIDs) {
var voiceId = voiceData.voiceIDs[j];
var vp = voices.voicecollection[voiceId];
if(vp.fallbackvoice) {
return vp;
}
}
break;
}
}
return null;
}
app.get('/speech/:platform/:username', (req, res, next) => {
getUser(req.params.platform, req.params.username, (err, user) => {
if(err) {
req.end('Error: '+err);
} else {
var rate = user.rate || 0.5;
if(rate < 0) rate = 0;
if(rate > 1) rate = 1;
var pitch = user.pitch || 0.5;
if(pitch < 0) pitch = 0;
if(pitch > 1) pitch = 1;
var voice = user.voice || 'UK English Female';
var urlData = {
t: req.query.text,
tl: 'en-GB',
sv: 'g1',
vn: '',
pitch: pitch,
rate: rate,
vol: '1',
gender: 'female'
};
var voiceParams = getResponsiveVoiceData(voice);
if(voiceParams) {
if(voiceParams.lang) urlData.tl = voiceParams.lang;
if(voiceParams.service) urlData.sv = voiceParams.service || '';
if(voiceParams.gender) urlData.gender = voiceParams.gender;
var url = 'https://code.responsivevoice.org/getvoice.php?' + util.buildQueryString(urlData);
var h = https.get(url, {
headers: { referer: 'https://responsivevoice.org/' }
}, (r) => {
if(r.statusCode !== 200) {
res.send('Error '+r.statusCode);
} else {
res.append('Content-type', r.headers['content-type']);
r.on('data', (chunk) => {
res.write(chunk);
});
r.on('end', () => {
res.end();
});
}
});
h.on('error', (err) => {
res.status(500);
res.end(err.errno);
});
} else {
req.end('Could not find voice');
}
}
});
});
const dLiveChat = new DLiveChat();
dLiveChat.listen(config.streamer);
dLiveChat.listen('manwithmachines');
dLiveChat.addListener('delete', (msg) => {
serverWS.clients.forEach(function (client) {
client.send(JSON.stringify(msg));
});
});
dLiveChat.addListener('chat', (chat) => {
console.log(chat.displayName, chat.text);
getUser('dlive', chat.username, (err, user) => {
if(err) {
console.error(err);
} else {
let m;
if(chat.text) {
if(chat.text == '!help') {
dLiveChat.sendStreamChatMessage('Hello pee pee poo poo. Hope that was helpful.', config.streamer, (msg) => {
console.log(msg);
}, (err) => {
console.error(err);
});
} else if(m = chat.text.match(/^!voice(\s+(.*))?$/)) {
if(m[2]) {
var voiceData = getResponsiveVoiceData(m[2]);
if(voiceData) {
db.run(`UPDATE users SET voice = ? WHERE id = ?`, [m[2], user.id], (err) => {
if(err) {
dLiveChat.sendStreamChatMessage(`Could not change voice for ${chat.displayName}: ${err}`);
} else {
dLiveChat.sendStreamChatMessage(`Voice for ${chat.displayName} changed to ${m[2]}, rate is ${user.rate}, pitch is ${user.pitch}.`, config.streamer);
}
});
} else {
dLiveChat.sendStreamChatMessage(`Invalid voice ${m[2]}`, config.streamer);
}
} else {
dLiveChat.sendStreamChatMessage(`Voice for ${chat.displayName} is ${user.voice}, rate is ${user.rate}, pitch is ${user.pitch}.`, config.streamer);
}
} else if(m = chat.text.match(/^!pitch(\s+(.*))?$/)) {
if(m[2]) {
let pitch = parseFloat(m[2]);
if(typeof pitch === 'NaN' || pitch < 0.0 || pitch > 1.0) {
dLiveChat.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`);
} else {
db.run(`UPDATE users SET pitch = ? WHERE id = ?`, [pitch, user.id], (err) => {
if(err) {
dLiveChat.sendStreamChatMessage(`Could not change pitch for ${chat.displayName}: ${err}`);
} else {
dLiveChat.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${pitch}`, config.streamer);
}
});
}
} else {
dLiveChat.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.pitch}`, config.streamer);
}
} else if(m = chat.text.match(/^!rate(\s+(.*))?$/)) {
if(m[2]) {
let rate = parseFloat(m[2]);
if(typeof rate === 'NaN' || rate < 0.0 || rate > 1.0) {
dLiveChat.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: Invalid number. Specify a value between 0.0 and 1.0.`);
} else {
db.run(`UPDATE users SET rate = ? WHERE id = ?`, [rate, user.id], (err) => {
if(err) {
dLiveChat.sendStreamChatMessage(`Could not change rate for ${chat.displayName}: ${err}`);
} else {
dLiveChat.sendStreamChatMessage(`Pitch for ${chat.displayName} changed to ${rate}`, config.streamer);
}
});
}
} else {
dLiveChat.sendStreamChatMessage(`Pitch for ${chat.displayName} is ${user.rate}`, config.streamer);
}
} else if(m = chat.text.match(/^!sticker(\s+(.*))?$/)) {
var url = 'https://vampi.tech/wp-admin/admin-ajax.php?'+util.buildQueryString({ action: 'sticker_search', q: m[2]||''});
https.get(url, {
headers: { referer: 'https://vampi.tech/' }
}, (r) => {
if(r.statusCode !== 200) {
res.send('Error '+r.statusCode);
} else {
var resultJson = '';
r.on('data', (chunk) => {
resultJson += chunk;
});
r.on('end', () => {
var result;
try {
result = JSON.parse(resultJson);
if(result && result.results && result.results[0]) {
dLiveChat.sendStreamChatMessage(':emote/mine/dlive/'+result.results[0].id+':', config.streamer);
}
} catch(e) {
}
});
}
});
} else {
var provider = null;
for(let p of songProviders) {
if(p.urlMatches(chat.text)) {
provider = p;
break;
}
}
if(provider) {
var url = provider.getCanonicalUrl(chat.text);
provider.getInfo(url, (err, info) => {
if(!err && info && info.title) {
dLiveChat.sendStreamChatMessage(info.title + ' ' + util.secondsToTime(info.duration), config.streamer);
}
});
}
}
}
chat.user = user;
serverWS.clients.forEach(function (client) {
client.send(JSON.stringify(chat));
});
}
});
});
if(config.botUsername && config.botPassword) {
dLiveChat.login(config.botUsername, config.botPassword, (data) => {
console.log('DLive bot logged in successfully.');
}, (err) => {
console.error('DLive bot could not log in:', err);
});
}

18
package.json

@ -0,0 +1,18 @@
{
"name": "tts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"express-ws": "^4.0.0",
"sqlite3": "^4.2.0",
"temp": "^0.9.1",
"ws": "^7.3.0"
}
}

11
songprovider.js

@ -0,0 +1,11 @@
class SongProvider {
constructor() {
}
getInfo(url, cb) {
cb(null);
}
}
module.exports = SongProvider;

68
soundcloud.js

@ -0,0 +1,68 @@
const SongProvider = require('./songprovider');
const https = require('https');
class SoundCloud extends SongProvider {
constructor() {
super();
}
urlMatches(url) {
return url.match(/^https?:\/\/(www\.)?soundcloud\.com\/[^/]+\/[^/]+$/);
}
getCanonicalUrl(url) {
var canonicalUrl = url;
var urlInstance = new URL(url);
if(urlInstance.host == 'www.soundcloud.com' || urlInstance.host == 'soundcloud.com' || urlInstance.host == 'm.soundcloud.com')
canonicalUrl = 'https://soundcloud.com/'+urlInstance.pathname.replace(/^\//, '').split('/').slice(0, 2).join('/');
return canonicalUrl;
}
getInfo(url, cb) {
url = this.getCanonicalUrl(url);
https.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
}
}, (r) => {
if(r.statusCode !== 200) {
cb('Could not get SoundCloud info for '+url+'. Status code: ' + r.statusCode);
} else {
var html = '';
r.on('data', chunk => html += chunk);
r.on('end', () => {
let match;
if(match = html.match(/catch\(t\)\{\}\}\)\},(\[(.*?)\])\);/)) {
var scData = JSON.parse(match[1]);
var found = false;
for(let i in scData) {
for(let j in scData[i].data) {
if(scData[i].data[j].title) {
var ret = {
url: url,
title: scData[i].data[j].title,
thumbnail: scData[i].data[j].artwork_url,
duration: scData[i].data[j].duration / 1000.0,
}
cb(null, ret);
found = true;
break;
}
}
if(found)
break;
}
if(!found)
cb('Could not read SoundCloud info from '+url);
} else {
cb('Could not parse SoundCloud data for '+url);
}
});
}
}).on('error', (e) => {
cb('Error reading SoundCloud '+url+'. ');
});
}
}
module.exports = SoundCloud;

137
static/SoundManager.js

@ -0,0 +1,137 @@
function SoundManager(baseUrl) {
this.baseUrl = baseUrl;
try {
this.audioCtx = new AudioContext();
this.mediaElement = new Audio();
this.mediaElementSourceNode = this.audioCtx.createMediaElementSource(this.mediaElement);
this.panNode = this.audioCtx.createStereoPanner();
this.gainNode = this.audioCtx.createGain();
this.gainNode.gain.setValueAtTime(4.0, this.audioCtx.currentTime);
this.compressorNode = this.audioCtx.createDynamicsCompressor();
this.compressorNode.threshold.setValueAtTime(-50, this.audioCtx.currentTime);
this.compressorNode.knee.setValueAtTime(40, this.audioCtx.currentTime);
this.compressorNode.ratio.setValueAtTime(12, this.audioCtx.currentTime);
this.compressorNode.attack.setValueAtTime(0, this.audioCtx.currentTime);
this.compressorNode.release.setValueAtTime(0.25, this.audioCtx.currentTime);
this.panNode.connect(this.gainNode);
this.gainNode.connect(this.compressorNode);
this.compressorNode.connect(this.audioCtx.destination);
} catch(e) {
console.error(e);
}
this.queue = [];
this.playing = false;
}
SoundManager.prototype.speak = function(platform, username, text, options) {
options = options || {};
try {
var url = config.speechUrl + '/' + platform + '/' + username + '?' + util.buildQueryString({ text: text });
this.play(url, options);
} catch(e) {
if(options.onEnd)
options.onEnd.call(null, e, this.playing);
else
console.error(e);
}
};
SoundManager.prototype.getAvailableVoices = function() {
};
SoundManager.prototype.playNext = function() {
if(this.playing) {
if(this.playing.connected)
this.playing.source.disconnect(this.panNode);
if(this.playing.onEnd)
this.playing.onEnd.call(null, null, this.playing);
}
if(this.queue.length > 0) {
var i = this.queue[0];
this.queue.splice(0, 1);
try {
this.connectAndPlay(i);
} catch(e) {
console.log('error in playNext', e);
if(this.playing.onEnd) {
this.playing.onEnd.call(null, e, this.playing);
}
}
} else {
this.playing = false;
}
};
SoundManager.prototype.connectAndPlay = function(item) {
this.panNode.pan.setValueAtTime(item.pan, this.audioCtx.currentTime);
item.source.connect(this.panNode);
item.connected = true;
this.playing = item;
if(item.onStart)
item.onStart.call(null, item);
item.audio.play();
};
SoundManager.prototype.play = function(url, options) {
var a = new Audio(url);
options = options || '';
var item = {
audio: a,
source: this.audioCtx.createMediaElementSource(a),
pan: options.pan || 0,
id: options.id || null,
onEnd: options.onEnd,
onStart: options.onStart,
connected: false
};
if(item.pan < -1) item.pan = -1;
if(item.pan > 1) item.pan = 1;
if(this.playing) {
this.queue.push(item);
var maxLength = 5;
if(this.queue.length > maxLength) {
var s = 1.0 + (this.queue.length - maxLength + 1) / 10.0;
for(let i in this.queue) {
try {
this.queue[i].audio.playbackRate = s;
} catch(e) {
// ignore
}
}
}
} else {
this.playing = item;
this.connectAndPlay(item);
}
item.audio.addEventListener('ended', (ev) => {
this.playNext();
});
item.audio.addEventListener('error', (ev) => {
this.playNext();
if(options.onEnd)
options.onEnd.call(null, ev, this.playing);
});
};
SoundManager.prototype.remove = function(id) {
if(this.playing && this.playing.id == id) {
this.playNext();
}
for(var i = 0; i < this.queue.length; i++) {
if(this.queue[i].id == id) {
this.queue.splice(i, 1);
i--;
}
}
};

BIN
static/frog.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
static/img/cookie.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
static/img/diamond.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
static/img/icecream1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
static/img/icecream2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
static/img/icecream3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
static/img/icecream4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
static/img/ninjaghini.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
static/img/ninjet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

16
static/index.html

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Vampi's TTS</title>
<link rel="icon" href="frog.ico">
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="util.js"></script>
<script type="text/javascript" src="SoundManager.js"></script>
<script type="text/javascript" src="tts.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">
<link rel="stylesheet" href="tts.css">
</head>
<body>
<div id="chatLog" class="chatLog"></div>
</body>
</html>

BIN
static/sounds/diamond.mp3

Binary file not shown.

BIN
static/sounds/follow.mp3

Binary file not shown.

BIN
static/sounds/host.mp3

Binary file not shown.

BIN
static/sounds/icecream.mp3

Binary file not shown.

BIN
static/sounds/lemon.mp3

Binary file not shown.

BIN
static/sounds/ninjaghini.mp3

Binary file not shown.

BIN
static/sounds/ninjet.mp3

Binary file not shown.

BIN
static/sounds/pop.mp3

Binary file not shown.

BIN
static/sounds/sub.mp3

Binary file not shown.

42
static/tts.css

@ -0,0 +1,42 @@
html, body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 18px;
}
.chatLog {
}
.chatLog .chatMessage {
background: green;
color: white;
margin: 4px;
padding: 8px;
}
.chatLog > .chatMessage > .chatImage {
float: right;
max-width: 80px;
max-height: 80px;
}
.chatLog .chatMessage .avatar {
max-width: 16px;
max-height: 16px;
border-radius: 8px;
}
.chatLog .chatMessage.speaking {
text-shadow: 0 0 20px #fff;
filter: drop-shadow(0 0 10px white) brightness(1.7);
}
.chatLog > .chatMessage > .chatSticker {
max-width: 80px;
max-height: 80px;
}
.clearfix {
clear: both;
}

333
static/tts.js

@ -0,0 +1,333 @@
var soundManager = new SoundManager(config.speechUrl);
var socket = null;
function initWebsocket() {
socket = new WebSocket(config.socketUrl);
socket.onopen = (evt) => {
console.log('Connected to websocket', config.socketUrl);
};
socket.onclose = (evt) => {
console.error('websocket closed', evt, socket.readyState);
socket.onopen = socket.onclose = socket.onerror = socket.onmessage = undefined;
socket = null;
setTimeout(() => { this.initWebsocket(); }, 1000);
};
socket.onerror = (evt) => {
console.error('websocket error', evt, socket.readyState);
socket.onopen = socket.onclose = socket.onerror = socket.onmessage = undefined;
socket = null;
setTimeout(() => { this.initWebsocket(); }, 1000);
};
socket.onmessage = (ev) => {
var data;
try {
data = JSON.parse(ev.data);
} catch(e) {
console.error('Could not parse JSON', ev.data, e);
}
if(data)
handleMessage(data);
};
}
initWebsocket();
var lastChat;
function getOffsetTop( elem ) {
var offsetTop = 0;
do {
if (!isNaN(elem.offsetTop)) {
offsetTop += elem.offsetTop;
}
} while( elem = elem.offsetParent );
return offsetTop;
}
function appendChat(chat) {
var div = document.createElement('div');
div.className = 'chatMessage animated zoomIn';
div.id = chat.id;
var soundDone = function(sound) {
div.className = div.className.replace(/\s*speaking\s*/g, '');
setTimeout(function() {
var div = document.getElementById(chat.id);
if(div) {
div.className = div.className.replace('zoomIn', '') + ' zoomOut';
setTimeout(function() {
chatLog.removeChild(div);
}, 500);
} else {
// console.error('Could not find DIV', chat.id, chat, sound);
}
}, 8000);
};
var soundOptions = {
pan: 0,
id: chat.id,
onStart: () => {
div.className += ' speaking';
window.scrollTo(0, getOffsetTop(div));
},
onEnd: (err, sound) => {
if(err)
console.error('error speaking', err);
soundDone(sound);
}
};
var speak = (chat.speak || '')
.replace(/^!.*/g, '')
.replace(/(6,?000,?000|(6|six)\s+mill?ion)/g, 'two hundred and seventy one thousand three hundred and one')
.replace(/(((https?\:\/\/)|(www\.))(\S+))/gi, '')
.replace(/((.)\2)\2+/gu, '$1')
;
if(speak) {
if(chat.sound) {
soundManager.speak(chat.platform, chat.username, speak, soundOptions);
soundManager.play(chat.sound, soundOptions);
} else {
soundManager.speak(chat.platform, chat.username, speak, soundOptions);
}
} else {
soundManager.play(chat.sound || 'sounds/pop.mp3', soundOptions);
}
var html = '';
var con = [ '' ];
if(chat.image) {
if(Array.isArray(chat.image)) {
for(var m in chat.image) {
html += '<img src="'+chat.image[m]+'" class="chatImage">';
}
} else {
html += '<img src="'+chat.image+'" class="chatImage">';
}
}
if(chat.avatar) {
html += '<img alt="" src="'+util.escapeHtml(chat.avatar)+'" class="avatar"> ';
con[0] += '%c ';
con.push('background-image: url('+chat.avatar+'); background-size: auto 1.2em; background-repeat: no-repeat');
}
if(chat.displayName) {
html += '<b>' + util.escapeHtml(chat.displayName) + '</b>';
con[0] += '%c'+chat.displayName+'%c ';
con.push('font-weight: bold');
con.push('font-weight: normal');
}
for(var i = 0; i < chat.content.length; i++) {
var c = chat.content[i];
if(!c)
continue;
html += ' ';
if(typeof c == 'string') {
con[0] += c;
html += util.escapeHtml(c);
} else if(c.type == 'image') {
con[0] += '%c '+c.alt;
con.push('background-image: url('+c.src+'); background-size: auto 1.2em; background-repeat: no-repeat');
html += '<img';
if(c.alt) html += ' alt="'+util.escapeHtml(c.alt)+'"';
if(c.src) html += ' src="'+util.escapeHtml(c.src)+'"';
if(c.class) html += ' class="'+util.escapeHtml(c.class)+'"';
html += '>';
} else if(c.type == 'br') {
html += '<br />';
con[0] += ' ';
}
}
html += '<div class="clearfix"></div>';
div.innerHTML = html;
var chatLog = document.getElementById('chatLog');
chatLog.appendChild(div);
console.log.apply(null, con);
}
function handleMessage(msg) {
var testMessages = {
Lemon: {"__typename":"ChatGift","amount":"5","createdAt":"1593804669046558744","expireDuration":0,"gift":"LEMON","id":"5ac8bdb9-8e72-4418-8b37-d0e616e91098","recentCount":1,"role":"None","roomRole":"Moderator","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":true,"type":"Gift"},
IceCream: {"__typename":"ChatGift","amount":"3","createdAt":"1593804669046558744","expireDuration":0,"gift":"ICE_CREAM","id":"5ac8bdb9-8e72-4418-8b37-d0e616e91098","recentCount":1,"role":"None","roomRole":"Moderator","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":true,"type":"Gift"},
Diamond: {"__typename":"ChatGift","amount":"1","createdAt":"1593804669046558744","expireDuration":0,"gift":"DIAMOND","id":"5ac8bdb9-8e72-4418-8b37-d0e616e91098","message":"Hello Frens","recentCount":1,"role":"None","roomRole":"Moderator","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":true,"type":"Gift"},
Ninjaghini: {"__typename":"ChatGift","amount":"1","createdAt":"1593804669046558744","expireDuration":0,"gift":"NINJAGHINI","id":"5ac8bdb9-8e72-4418-8b37-d0e616e91098","message":"Pee Pee poo Poo","recentCount":1,"role":"None","roomRole":"Moderator","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":true,"type":"Gift"},
Ninjet: {"__typename":"ChatGift","amount":"1","createdAt":"1593804669046558744","expireDuration":0,"gift":"NINJET","id":"5ac8bdb9-8e72-4418-8b37-d0e616e91098","message":"Dee Dee Get Out Of My laboratory","recentCount":1,"role":"None","roomRole":"Moderator","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":true,"type":"Gift"},
Host: {"__typename":"ChatHost","id":"8ecb00e0-1061-4d52-804c-b5e7b5ef7ff4","role":"None","roomRole":"Member","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/799c0ae0-a4e2-11ea-b737-e2443572cd01","badges":[],"displayname":"FerociousChihuahua","effect":null,"id":"streamchatuser:ferociouschihuahua","partnerStatus":"AFFILIATE","username":"ferociouschihuahua"},"subscribing":false,"type":"Host","viewer":2},
Follow: {"__typename":"ChatFollow","id":"f936fcb8-ebd5-4f88-9db6-6d4aa18e74d9","role":"None","roomRole":"Member","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/f7fda7f0-bc94-11ea-b737-e2443572cd01","badges":[],"displayname":"r-eVan","effect":null,"id":"streamchatuser:r-evan","partnerStatus":"NONE","username":"r-evan"},"subscribing":false,"type":"Follow"},
ExtendSub: {"__typename":"ChatExtendSub","id":"b9e5e15b-d754-46a4-b71d-63571975b09f","length":3,"month":1,"role":"None","roomRole":"Member","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/f85df577-6e2f-11ea-8119-a272e850df75","badges":[],"displayname":"PROPRBOY","effect":null,"id":"streamchatuser:proprboy","partnerStatus":"NONE","username":"proprboy"},"subscribing":true,"type":"ExtendSub"},
SubStreak: {"__typename":"ChatSubStreak","id":"3bd0a19e-8791-410d-b3b2-9f044ba33881","length":2,"role":"None","roomRole":"Member","sender":{"__typename":"StreamchatUser","avatar":"https://images.prd.dlivecdn.com/avatar/f85df577-6e2f-11ea-8119-a272e850df75","badges":[],"displayname":"PROPRBOY","effect":null,"id":"streamchatuser:proprboy","partnerStatus":"NONE","username":"proprboy"},"subscribing":true,"type":"SubStreak"},
GiftSubReceive: {"__typename":"ChatGiftSubReceive","type":"GiftSubReceive","id":"6253dfab-10e0-47fd-889e-c8ada929ac02","sender":{"__typename":"StreamchatUser","id":"streamchatuser:greenbaywacky","username":"greenbaywacky","displayname":"greenbaywacky","avatar":"https://images.prd.dlivecdn.com/avatar/f7672f4e-63f2-11e9-bc94-460601ac0a66","partnerStatus":"VERIFIED_PARTNER","badges":[],"effect":null},"role":"None","roomRole":"Moderator","subscribing":true,"gifter":"thomasthefam"},
GiftSub: {"__typename":"ChatGiftSub","type":"GiftSub","id":"7c5741ce-4d68-4904-9a22-15c684bb936b","sender":{"__typename":"StreamchatUser","id":"streamchatuser:thomas42","username":"thomas42","displayname":"thomasthefam","avatar":"https://images.prd.dlivecdn.com/avatar/69b8b18d-4432-11ea-9529-e2443572cd01","partnerStatus":"NONE","badges":[],"effect":"https://images.prd.dlivecdn.com/effect/trxsmall"},"role":"None","roomRole":"Moderator","subscribing":true,"receiver":"greenbaywacky","count":null},
Mod: {"__typename":"ChatModerator","type":"Mod","id":"4ba35866-7b3d-4922-b1e0-a25907f9555e","sender":{"__typename":"StreamchatUser","id":"streamchatuser:thestoryofdori","username":"thestoryofdori","displayname":"TheStoryOfDori13","avatar":"https://images.prd.dlivecdn.com/avatar/1b0c639f-208d-11ea-bd1e-563a837bad22","partnerStatus":"AFFILIATE","badges":[],"effect":"https://images.prd.dlivecdn.com/effect/trx"},"role":"None","roomRole":"Moderator","subscribing":true,"add":true},
};
if(msg.original.type == 'Message' && msg.original.sender.username == 'vampirefrog' && testMessages[msg.original.content]) {
msg.original = testMessages[msg.original.content];
}
var id = msg.original.id;
var type = msg.original.type;
var banned = [ 'tidylabs' ];
if(msg.original.sender && msg.original.sender.username && banned.indexOf(msg.original.sender.username) > -1) {
return;
}
if(type == 'Message') {
var avatar = msg.original.sender.avatar;
var displayName = msg.displayName;
var text = msg.text.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' ');
var m = text.match(/:emote\/([^/]+)\/([^/]+)\/([^:]+):/);
if(m && m[3]) {
util.loadJSON(config.stickerInfoUrl+'/'+m[3], function(response) {
var chat = {
id: id,
avatar: avatar,
displayName: displayName,
content: [
{
type: 'image',
src: 'https://images.prd.dlivecdn.com/emote/'+m[3],
alt: m[3],
class: 'chatSticker'
}
]
};
if(response && response.results && response.results[0] && response.results[0].name) {
chat.platform = msg.user.platform;
chat.username = msg.user.username;
chat.speak = chat.content[0].alt = response.results[0].name;
} else {
chat.sound = 'sounds/pop.mp3';
}
appendChat(chat);
});
} else {
if(text) {
appendChat({
id: id,
avatar: avatar,
displayName: displayName,
platform: msg.user.platform,
username: msg.user.username,
speak: text,
content: [
text
]
});
}
}
} else if(type == 'Gift') {
var avatar = msg.original.sender.avatar;
var displayName = msg.displayName;
var gifts = {
'LEMON': {
sound: 'sounds/lemon.mp3',
string: 'donated a lemon',
stringMulti: 'donated # lemons',
img: 'img/cookie.png',
},
'ICE_CREAM': {
sound: 'sounds/icecream.mp3',
string: 'donated an ice cream',
stringMulti: 'donated # ice creams',
img: [ 'img/icecream1.png', 'img/icecream2.png', 'img/icecream3.png', 'img/icecream4.png' ],
},
'DIAMOND': {
sound: 'sounds/diamond.mp3',
string: 'donated a diamond',
stringMulti: 'donated # diamonds',
img: 'img/diamond.png',
},
'NINJAGHINI': {
sound: 'sounds/ninjaghini.mp3',
string: 'donated a ninjaghini',
stringMulti: 'donated # ninjaghinis',
img: 'img/ninjaghini.png',
},
'NINJET': {
sound: 'sounds/ninjet.mp3',
string: 'donated a ninjet',
stringMulti: 'donated # ninjets',
img: 'img/ninjet.png',
}
};
if(gifts[msg.original.gift]) {
var gift = gifts[msg.original.gift];
var amount = parseInt(msg.original.amount);
var string = gift.string;
if(amount > 1) {
string = gift.stringMulti.replace('#', msg.original.amount);
}
var image = [];
for(var m = 0; m < amount; m++) {
var img = gift.img;
if(Array.isArray(img)) // pick a random image
img = img[Math.floor(Math.random() * img.length)];
image.push(img);
}
appendChat({
id: id,
avatar: avatar,
displayName: displayName,
sound: gift.sound,
platform: msg.user.platform,
username: msg.user.username,
speak: msg.original.message,
image: image,
content: [
string,
{ type: 'br' },
msg.original.message
]
});
}
} else if(type == 'Delete') {
for(var i in msg.original.ids) {
soundManager.remove(msg.original.ids[i]);
}
console.log('%cDelete%c '+msg.original.ids.join(', '), 'color: red; font-weight: bold', 'color: inherit; font-weight: normal');
} else if(type == 'Host') {
appendChat({
id: id,
avatar: msg.original.sender.avatar,
displayName: msg.original.sender.displayname,
sound: 'sounds/host.mp3',
content: [
'hosted with ' + msg.original.viewer + ' viewers!'
]
});
} else if(type == 'Follow') {
appendChat({
id: id,
avatar: msg.original.sender.avatar,
displayName: msg.original.sender.displayname,
sound: 'sounds/follow.mp3',
content: [
'just followed!'
]
});
// } else if(type == 'ExtendSub') {
// } else if(type == 'SubStreak') {
// } else if(type == 'GiftSubReceive') {
// } else if(type == 'GiftSub') {
} else {
appendChat({
id: id,
avatar: msg.original.sender && msg.original.sender.avatar,
displayName: msg.original.sender && msg.original.sender.displayname,
sound: 'sounds/pop.mp3',
content: [
type,
JSON.stringify(msg.original)
]
});
}
}

61
static/util.js

@ -0,0 +1,61 @@
var util = {
loadJSON: function(url, cb) {
var x = new XMLHttpRequest();
x.timeout = 2000;
x.addEventListener('load', function() {
if(x.status == 200) {
try {
var response = JSON.parse(this.responseText);
cb(response);
} catch(e) {
console.error(e);
}
} else console.error(x.status);
});
x.open('GET', url);
x.send();
},
postJSON: function(url, data, cb) {
var x = new XMLHttpRequest();
x.open('POST', url, true);
x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8');
x.onreadystatechange = function() { //Call a function when the state changes.
if(x.readyState == 4 && x.status == 200 && cb) {
cb(JSON.parse(x.responseText));
}
}
x.send(buildQueryString(data));
},
escapeHtml: function(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
buildQueryString: function(params) {
return Object.keys(params)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
},
padInt: function(i) {
return ('00'+i).substr(-2);
},
secondsToTime: function(sec) {
sec = Math.floor(sec);
var s = sec % 60;
var m = Math.floor(sec / 60) % 60;
var h = Math.floor(sec / 60 / 60);
return (h ? h + ':' : '') + (h ? util.padInt(m) + ':' : (m ? m + ':' : '')) + (h || m ? util.padInt(s) : s);
}
};
if(typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = util;
}

BIN
template.sqlite

Binary file not shown.

3321
voices.json

File diff suppressed because it is too large

63
youtube.js

@ -0,0 +1,63 @@
const SongProvider = require('./songprovider');
const https = require('https');
class YouTube extends SongProvider {
constructor() {
super();
}
urlMatches(url) {
try {
var urlInstance = new URL(url);
return urlInstance.host == 'www.youtube.com' || urlInstance.host == 'm.youtube.com' || urlInstance.host == 'youtu.be';
} catch(e) {
return false;
}
}
getCanonicalUrl(url) {
var canonicalUrl = url;
var urlInstance = new URL(url);
if(urlInstance.host == 'www.youtube.com' || urlInstance.host == 'youtube.com' || urlInstance.host == 'm.youtube.com')
canonicalUrl = 'https://www.youtube.com/watch?v='+urlInstance.searchParams.get('v');
else if(urlInstance.host == 'youtu.be')
canonicalUrl = 'https://www.youtube.com/watch?v='+urlInstance.pathname.replace(/^\//, '');
return canonicalUrl;
}
getInfo(url, cb) {
url = this.getCanonicalUrl(url);
https.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
}
}, (r) => {
if(r.statusCode !== 200) {
cb('Could not get YouTube info for '+url+'. Status code: ' + r.statusCode);
} else {
var html = '';
r.on('data', chunk => html += chunk);
r.on('end', () => {
let match;
if(match = html.match(/ytplayer\.config = (\{(.*?)\});/)) {
var ytPlayerConfig = JSON.parse(match[1]);
var ytPlayerResponse = JSON.parse(ytPlayerConfig.args.player_response);
var ret = {
url: url,
title: ytPlayerResponse.videoDetails.title,
thumbnail: ytPlayerResponse.videoDetails.thumbnail && ytPlayerResponse.videoDetails.thumbnail.thumbnails && ytPlayerResponse.videoDetails.thumbnail.thumbnails[0].url || null,
duration: ytPlayerResponse.videoDetails.lengthSeconds
};
cb(null, ret);
} else {
cb('Could not parse YouTube data for '+url);
}
});
}
}).on('error', (e) => {
cb('Error reading YouTube '+url+'. ')
});
}
}
module.exports = YouTube;
Loading…
Cancel
Save