@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
module.exports = { |
||||
streamer: 'vampirefrog', |
||||
botUsername: '', |
||||
botPassword: '', |
||||
port: 3080, |
||||
sounds: { |
||||
default: 'pop.mp3' |
||||
} |
||||
}; |
@ -0,0 +1,373 @@
@@ -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); |
||||
}); |
||||
} |
@ -0,0 +1,18 @@
@@ -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" |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
class SongProvider { |
||||
constructor() { |
||||
|
||||
} |
||||
|
||||
getInfo(url, cb) { |
||||
cb(null); |
||||
} |
||||
} |
||||
|
||||
module.exports = SongProvider; |
@ -0,0 +1,68 @@
@@ -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; |
@ -0,0 +1,137 @@
@@ -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--; |
||||
} |
||||
} |
||||
}; |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,16 @@
@@ -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> |
@ -0,0 +1,42 @@
@@ -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; |
||||
} |
@ -0,0 +1,333 @@
@@ -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) |
||||
] |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,61 @@
@@ -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, "&") |
||||
.replace(/</g, "<") |
||||
.replace(/>/g, ">") |
||||
.replace(/"/g, """) |
||||
.replace(/'/g, "'"); |
||||
}, |
||||
|
||||
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; |
||||
} |
@ -0,0 +1,63 @@
@@ -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; |