commit 732ede89864c1b365ac464fc1990dc37c69ab8e7 Author: Evert Prants Date: Tue Dec 18 19:38:18 2018 +0200 initial diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a0a4de8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.js] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c01727e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/config.json diff --git a/control/config.js b/control/config.js new file mode 100644 index 0000000..d69deec --- /dev/null +++ b/control/config.js @@ -0,0 +1,89 @@ +const fs = require('fs').promises +const path = require('path') +const configFile = path.join(__dirname, '..', 'config.json') + +const defvals = { + api: { + host: '0.0.0.0', + port: 7005, + mount: '/api' + }, + rtmp: { + host: 'icynet.eu:1935', + mount: '/live', + key: '' + }, + control: { + host: '127.0.0.1', + port: 7004 + }, + liquidsoap: { + fallback: '', + bitrate: 2000, + entry: 'liq/view.liq' + }, + calendar: { + googleKey: '', + calendar: '', + interval: 60, + timeFrame: 120000, + } +} + +function getKeys (obj, vals) { + let keys = {} + + for (let key in obj) { + if (key === 'rtmp') { + keys['rtmp.url'] = 'rtmp://' + vals.rtmp.host + vals.rtmp.mount + keys['rtmp.key'] = vals.rtmp.key + } else if (typeof vals[key] == 'object') { + let ks = getKeys(defvals[key], vals[key]) + for (let i in ks) { + keys[key + '.' + i] = ks[i] + } + } else { + keys[key] = vals[key] + } + } + + return keys +} + +class Config { + constructor () { + for (let k in defvals) { + this[k] = defvals[k] + } + } + + writeDefault () { + fs.writeFile(configFile, JSON.stringify(defvals, null, 2)).catch((err) => { + console.error(err.stack) + process.exit(1) + }) + } + + async read () { + try { + await fs.stat(configFile) + } catch (e) { + return this.writeDefault() + } + + let conf = await fs.readFile(configFile, 'utf-8') + let json = JSON.parse(conf) + let obj = Object.assign({}, defvals, json) + + for (let k in obj) { + this[k] = obj[k] + } + } + + async liq () { + await this.read() + return JSON.stringify(getKeys(defvals, this)) + } +} + +module.exports = new Config() diff --git a/control/console.js b/control/console.js new file mode 100644 index 0000000..54a1143 --- /dev/null +++ b/control/console.js @@ -0,0 +1,45 @@ +// Readline +const readline = require('readline') +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +// Logging +function pz (z) { + if (z < 10) { + return '0' + z + } + return z +} + +function dateFormat (date) { + return date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() + ' ' + + pz(date.getHours()) + ':' + pz(date.getMinutes()) + ':' + pz(date.getSeconds()) +} + +const realConsoleLog = console.log +console.log = function () { + rl.output.write('\x1b[2K\r') + rl.output.write('[info] [' + dateFormat(new Date()) + '] ') + realConsoleLog.apply(this, arguments) + rl && rl.prompt(true) +} + +const realConsoleWarn = console.warn +console.warn = function () { + rl.output.write('\x1b[2K\r') + rl.output.write('[warn] [' + dateFormat(new Date()) + '] ') + realConsoleWarn.apply(this, arguments) + rl && rl.prompt(true) +} + +const realConsoleError = console.error +console.error = function () { + rl.output.write('\x1b[2K\r') + rl.output.write('[ err] [' + dateFormat(new Date()) + '] ') + realConsoleError.apply(this, arguments) + rl && rl.prompt(true) +} + +module.exports = { realConsoleLog, realConsoleWarn, realConsoleError, rl } diff --git a/control/index.js b/control/index.js new file mode 100644 index 0000000..2b6a8ff --- /dev/null +++ b/control/index.js @@ -0,0 +1,77 @@ +const path = require('path') +const config = require(path.join(__dirname, 'config.js')) +const Liquidsoap = require(path.join(__dirname, 'liquidsoap.js')) +const Scheduler = require(path.join(__dirname, 'schedule.js')) +const rl = require(path.join(__dirname, 'console.js')).rl + +let liq = new Liquidsoap() +liq.start() + +let scheduler = new Scheduler(config.calendar) +scheduler.startTimers() +scheduler.runner = function (event) { + liq.queue(event.descriptor) +} + +// User input handler +rl.on('line', function (line) { + let argv = line.split(' ') + + switch (argv[0]) { + case 'stop': + console.log('Stopping liquidsoap..') + liq.stop() + break + case 'start': + if (liq.running) return + console.log('Starting liquidsoap..') + liq.start() + break + case 'restart': + console.log('Restarting liquidsoap..') + liq.stop() + setTimeout(() => liq.start(), 4000) + break + case 'queue': + let qline = line.substring(6) + liq.queue(qline) + break + case 'skip': + liq.skip() + break + case 'status': + console.log(liq.running ? 'Liquidsoap is running' : 'Liquidsoap is stopped') + break + case 'events': + console.log('Refreshing scheduler..') + scheduler.calendarFetch().catch((e) => console.error('Calendar fetch failed!', e.stack)) + break + default: + console.log('Unknown command.') + } + + rl.prompt(true) +}) + +rl.on('close', function () { + scheduler.stopTimers() + + if (liq.running) { + liq.stop() + console.log('Waiting for liquidsoap to exit') + setTimeout(function () { + if (liq.running) { + console.warn('Liquidsoap did not exit in time, forcing..') + liq.proc.kill(9) + } + + process.stdout.write('\n') + process.exit(0) + }, 1000) + return + } + + console.log('Bye bye!') + process.stdout.write('\n') + process.exit(0) +}) diff --git a/control/liquidsoap.js b/control/liquidsoap.js new file mode 100644 index 0000000..0b6ca00 --- /dev/null +++ b/control/liquidsoap.js @@ -0,0 +1,79 @@ +// Liquidsoap class +const path = require('path') +const net = require('net') + +const spawn = require('child_process').spawn + +const config = require(path.join(__dirname, 'config.js')) +const rootpath = path.join(__dirname, '..') +const csl = require(path.join(__dirname, 'console.js')) + +function printProcessOutput (fn) { + return function (data) { + let str = data.toString().trim() + csl.rl.output.write('\x1b[2K\r') + fn.call(null, str) + csl.rl && csl.rl.prompt(true) + } +} + +class Liquidsoap { + constructor () { + this.running = false + } + + start () { + let pd = path.parse(config.liquidsoap.entry) + let proc = this.proc = spawn('liquidsoap', [pd.base], {cwd: path.join(rootpath, pd.dir)}) + this.running = true + + proc.stdout.on('data', printProcessOutput(csl.realConsoleLog)) + proc.stderr.on('data', printProcessOutput(csl.realConsoleError)) + + proc.on('close', () => { + this.running = false + }) + } + + stop () { + if (this.running && this.proc) { + this.proc.kill() + } + } + + async sendCommand (msg) { + if (!this.running) throw new Error('Process is not running.') + let client = net.connect(config.control.port, config.control.host) + + return new Promise((resolve, reject) => { + client.once('connect', function () { + let split = msg.split('\n') + for (let i in split) { + client.write(split[i] + '\r\n') + } + + client.write('quit\r\n') + client.end() + + resolve() + }) + }) + } + + queue (item) { + if (typeof item !== 'object') item = [item] + + let q = [] + for (let i in item) { + q.push('queue.push smart:' + item[i]) + } + + this.sendCommand(q.join('\n')).catch((e) => console.error('Failed to queue:', e.message)) + } + + skip () { + this.sendCommand('skip').catch((e) => console.error('Failed to skip:', e.message)) + } +} + +module.exports = Liquidsoap diff --git a/control/request.js b/control/request.js new file mode 100644 index 0000000..d554117 --- /dev/null +++ b/control/request.js @@ -0,0 +1,108 @@ +const url = require('url') +const qs = require('querystring') + +function HTTP_GET (link, headers = {}, lback) { + if (lback && lback >= 4) throw new Error('infinite loop!') // Prevent infinite loop requests + let parsed = url.parse(link) + let opts = { + host: parsed.hostname, + port: parsed.port, + path: parsed.path, + headers: { + 'User-Agent': 'icytv/1.0.0', + 'Accept': '*/*', + 'Accept-Language': 'en-GB,en;q=0.5' + } + } + + if (headers) { + opts.headers = Object.assign(opts.headers, headers) + } + + let reqTimeOut + + let httpModule = parsed.protocol === 'https:' ? require('https') : require('http') + return new Promise((resolve, reject) => { + let req = httpModule.get(opts, (res) => { + if (res.statusCode === 302 || res.statusCode === 301) { + if (!lback) { + lback = 1 + } else { + lback += 1 + } + + return HTTP_GET(res.headers.location, headers, lback).then(resolve, reject) + } + + let data = '' + + reqTimeOut = setTimeout(() => { + req.abort() + data = null + reject(new Error('Request took too long!')) + }, 5000) + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + clearTimeout(reqTimeOut) + + resolve(data) + }) + }).on('error', (e) => { + reject(new Error(e.message)) + }) + + req.setTimeout(10000) + }) +} + +function HTTP_POST (link, headers = {}, data) { + let parsed = url.parse(link) + let postData = qs.stringify(data) + + let opts = { + host: parsed.host, + port: parsed.port, + path: parsed.path, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData), + 'User-Agent': 'Squeebot/Commons-2.0.0' + } + } + + if (headers) { + opts.headers = Object.assign(opts.headers, headers) + } + + if (opts.headers['Content-Type'] === 'application/json') { + postData = JSON.stringify(data) + } + + return new Promise((resolve, reject) => { + let httpModule = parsed.protocol === 'https:' ? require('https') : require('http') + let req = httpModule.request(opts, (res) => { + res.setEncoding('utf8') + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + resolve(data) + }) + }).on('error', (e) => { + reject(new Error(e)) + }) + + req.write(postData) + req.end() + }) +} + +module.exports = { GET: HTTP_GET, POST: HTTP_POST } diff --git a/control/schedule.js b/control/schedule.js new file mode 100644 index 0000000..eed4965 --- /dev/null +++ b/control/schedule.js @@ -0,0 +1,217 @@ +const fs = require('fs') +const path = require('path') +const c = require('canvas') + +const config = require(path.join(__dirname, 'config.js')) +const request = require(path.join(__dirname, 'request.js')) + +function pz (z) { + if (z < 10) { + return '0' + z + } + return z +} + +function dateFormat (date, yr) { + return (yr ? (date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() + ' ') : '') + + pz(date.getHours()) + ':' + pz(date.getMinutes()) +} + +class Drawer { + constructor (w, h) { + this.canvas = c.createCanvas(w, h) + this.ctx = this.canvas.getContext('2d') + } + + draw (cal) { + let ctx = this.ctx + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) + + ctx.fillStyle = 'rgba(0,126,255,0.8)' + ctx.fillRect(5, 5, this.canvas.width - 10, this.canvas.height - 10) + + let added = 0 + + for (let i in cal.events) { + let ev = cal.events[i] + + if (i > 5) break // Can't fit so many on here :P + if (ev.eventStartTime < Date.now() || ev.eventEndTime < Date.now()) continue + + ctx.fillStyle = '#fff' + let y = (i * 60) + 30 + + // Write event name + let evn = ev.eventName + if (evn.length > 14) { + evn = evn.substring(0, 14) + '…' + } + + ctx.font = '26px sans-serif' + ctx.fillText(evn, 10, y) + + // Write event description + ctx.font = '16px sans-serif' + ctx.fillText(ev.description, 10, y + 20) + + // Write event time + let time = dateFormat(ev.eventStartTime) + ctx.font = '26px sans-serif' + let tlen = ctx.measureText(time).width + ctx.fillText(time, (this.canvas.width - 10) - tlen, y) + + // Draw line under text + ctx.strokeStyle = 'rgba(0,0,0,0.5)' + ctx.beginPath() + ctx.lineTo(10, y + 20 + 12) + ctx.lineTo(this.canvas.width - 10, y + 20 + 12) + ctx.stroke() + added++ + } + + if (added === 0) { + ctx.fillStyle = '#fff' + ctx.font = '26px sans-serif' + ctx.fillText('No events scheduled.', 10, 35) + } + + this.toFile() + } + + toFile () { + const out = fs.createWriteStream(path.join(process.env.PWD, 'liq', 'schedule.png')) + const stream = this.canvas.createPNGStream() + stream.pipe(out) + + return new Promise((resolve, reject) => { + out.on('finish', resolve) + out.on('error', reject) + }) + } +} + +class Calendar { + constructor (conf) { + this.events = [] + + this.config = conf + + this.drawer = new Drawer(300, 400) + this.schedRunner = null + + this.started = [] + this.timers = { a: null, b: null, c: null } + } + + startTimers () { + // Initial run + this.calendarFetch().then( + (e) => this.drawer.draw(this), + (e) => console.error('Calendar fetch failed!', e.stack) + ) + + // Set timers + this.timers = { + a: setInterval(() => this.calendarFetch().catch((e) => console.error('Calendar fetch failed!', e.stack)), this.config.interval * 1000), + b: setInterval(() => this.runners(), 60 * 1000), + c: setInterval(() => this.drawer.draw(this), 120 * 1000) + } + } + + stopTimers () { + clearInterval(this.timers.a) + clearInterval(this.timers.b) + clearInterval(this.timers.c) + } + + static sortStartTime (a, b) { + return a.eventStartTime - b.eventEndTime + } + + static prettifyEvent (item) { + let ev = { + id: item.id, + htmlLink: item.htmlLink, + created: new Date(item.created), + updated: new Date(item.updated), + + title: item.summary.replace(/\n/g, ' '), + location: item.location, + description: (item.description || '').replace(/\n/g, ' '), + + start: new Date(item.start.dateTime || item.start.date), + end: new Date(item.end.dateTime || item.end.date), + sequence: item.sequence + } + + ev.length = (ev.end.getTime() - ev.start.getTime()) / 1000 + + return ev + } + + async calendarFetch () { + const timeFrame = this.config.timeFrame + const cUrl = this.config.calendar + const apiKey = this.config.googleKey + + let now = Date.now() + let url = 'https://www.googleapis.com/calendar/v3/calendars/' + encodeURIComponent(cUrl) + '/events?key=' + apiKey + + '&timeMin=' + (new Date(now - 10 * 60 * 1000).toISOString()) + + '&timeMax=' + (new Date(now + timeFrame * 1000).toISOString()) + '&singleEvents=true' + + let data = await request.GET(url) + data = JSON.parse(data) + now = Date.now() + + if (!data.items) return + let results = [] + + for (let i = 0; i < data.items.length; i++) { + let item = Calendar.prettifyEvent(data.items[i]) + + if (now < item.end.getTime()) { + let desc = item.description + let scr = null + if (desc.indexOf('$:') !== -1) { + let a = desc.split('$:') + desc = a[0].trim() + scr = a[1].trim() + } + + results.push({ + id: item.id, + eventName: item.title, + eventStartTime: item.start, + eventEndTime: item.end, + description: desc, + descriptor: scr + }) + } + } + + console.log(`[schedule] Calendar fetched successfully!`) + + this.events = results.sort(Calendar.sortStartTime) + } + + set runner (runner) { + this.schedRunner = runner + } + + runners () { + if (!this.schedRunner) return + for (let i in this.events) { + let ev = this.events[i] + if (!ev.descriptor) continue + if (ev.eventStartTime < Date.now() && ev.eventEndTime > Date.now()) { + if (this.started.indexOf(ev.id) !== -1) continue + console.log(`[schedule] Starting event ${ev.eventName}..`) + + this.started.push(ev.id) + this.schedRunner.call(this, ev) + } + } + } +} + +module.exports = Calendar diff --git a/index.js b/index.js new file mode 100644 index 0000000..32391e3 --- /dev/null +++ b/index.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +const path = require('path') +const config = require(path.join(__dirname, 'control', 'config.js')) +const env = 'development' + +// Config grabber +for (let i in process.argv) { + let arg = process.argv[i] + if (arg === '-c') { + config.liq().then( + (dat) => console.log(dat), + (err) => { + console.error(err.stack) + process.exit(1) + } + ) + return + } else if (arg === '-p') { + env = 'production' + break + } +} + +// Application starter +process.env.NODE_ENV = env + +config.read().then( + (c) => require(path.join(__dirname, 'control')), + (e) => console.error(e.stack) +) diff --git a/liq/.gitignore b/liq/.gitignore new file mode 100644 index 0000000..58e6eb0 --- /dev/null +++ b/liq/.gitignore @@ -0,0 +1 @@ +/schedule.png diff --git a/liq/config.liq b/liq/config.liq new file mode 100644 index 0000000..78a772b --- /dev/null +++ b/liq/config.liq @@ -0,0 +1,20 @@ +confsrc = get_process_output("node --no-warnings ../index -c") +config = of_json(default=[("error", "json")], confsrc) + +if config["error"] == "json" then + print("Failed to parse config JSON:") + print(confsrc) + shutdown() +end + +def confstr(key,default="") = + c = config[key] + if c == "" then + default + else + c + end +end +def confint(key,default=0) = int_of_string(config[key], default=default) end +def conffloat(key,default=0.0) = float_of_string(config[key], default=default) end +def confbool(key,default=false) = bool_of_string(config[key], default=default) end diff --git a/liq/protocols.liq b/liq/protocols.liq new file mode 100644 index 0000000..1bc5433 --- /dev/null +++ b/liq/protocols.liq @@ -0,0 +1,7 @@ +# uses an external script to handle anything thrown at it! +def smart_protocol(~rlog,~maxtime,arg) + res = get_process_lines("node scripts/smart "^quote(arg)) + print(res) + res +end +add_protocol("smart", smart_protocol) diff --git a/liq/rtmp.liq b/liq/rtmp.liq new file mode 100644 index 0000000..c72c698 --- /dev/null +++ b/liq/rtmp.liq @@ -0,0 +1,25 @@ +# Stream live via RTMP. You need the following Gstreamer plugins: flvmux, rtmpsink, x264enc and a suitable AAC encoder (see @audio_encoder@ params). +# @param ~id Source ID +# @param ~video_bitrate Video bitrate +# @param ~audio_encoder Audio encoder. Can be one of: "fdkaacenc", "voaacenc" +# @param ~audio_bitrate Audio bitrate +# @param ~url Server URL +# @param ~key Secret key +# @param source Source to stream +# @category Source / Output +def output.rtmp.live(~id="", + ~video_bitrate=2000, + ~audio_encoder="fdkaacenc", + ~audio_bitrate=128000, + ~url="rtmp://icynet.eu:1935/live", + ~key, + source) = + video_pipeline = "videoconvert ! \ + x264enc bitrate=#{video_bitrate} byte-stream=false key-int-max=60 bframes=0 aud=true tune=zerolatency ! \ + video/x-h264,profile=main ! queue ! mux." + audio_pipeline = "audioconvert ! #{audio_encoder} bitrate=#{audio_bitrate} ! queue ! mux." + pipeline = "flvmux streamable=true name=mux ! rtmpsink location=\"#{url}/#{key}\"" + + output.gstreamer.audio_video(id=id,video_pipeline=video_pipeline, + audio_pipeline=audio_pipeline,pipeline=pipeline,source) +end diff --git a/liq/scripts/queue b/liq/scripts/queue new file mode 100755 index 0000000..de5e9c2 --- /dev/null +++ b/liq/scripts/queue @@ -0,0 +1,17 @@ +#!/usr/bin/env node +'use strict' +const net = require('net') +const path = require('path') + +let client = net.connect('7004', '127.0.0.1') + +client.pipe(process.stdout) + +client.once('connect', function () { + if (process.argv[2]) { + client.write('queue.push smart:' + process.argv[2] + '\r\n') + } + client.write('quit\r\n') + client.end() +}) + diff --git a/liq/scripts/smart b/liq/scripts/smart new file mode 100755 index 0000000..28500ce --- /dev/null +++ b/liq/scripts/smart @@ -0,0 +1,77 @@ +#!/usr/bin/env node +'use strict' + +const url = require('url') +const spawn = require('child_process').spawn +const os = require('os') + +let arg = (process.argv[2] || '').trim() + +function protocol (arg, handleCb) { + let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'best', arg]) + + let output = '' + + yt.stdout.on('data', function (chunk) { + output += chunk.toString('utf8') + }) + yt.on('close', function () { + let data = JSON.parse(output) + delete data.formats + fetchVideo(data, handleCb) + }) +} + +function fetchVideo (data, cb) { + let tempName = os.tmpdir() + '/tmp.yt.' + data.id + '.mkv' + + let ffmpeg = spawn('ffmpeg', ['-hide_banner', '-i', data.url, '-codec:a', 'copy', '-codec:v', 'copy', '-y', tempName]) + + ffmpeg.stdout.pipe(process.stderr) + ffmpeg.stderr.pipe(process.stderr) + + data.filename = data.url + data.filename = tempName + + outputVideo(data, cb) +} + +function formatter (o) { + if (Array.isArray(o)) { + o.forEach(formatter) + return + } + + let list = [] + for (let key in o) { + if (o.hasOwnProperty(key) && key !== 'source' && o[key] !== null && o[key] !== undefined) { + list.push(key + '=' + JSON.stringify(o[key])) + } + } + + let out = '' + if (list.length > 0) { + out += 'annotate:' + list.join(',') + ':' + } + out += o.source + + console.log(out) +} + +function outputVideo (video, cb) { + cb({ + title: video.title, + artist: video.uploader, + url: video.webpage_url, + art: video.thumbnail, + temporary: true, + + source: video.filename + }) +} + +if (arg.indexOf('youtube.com') !== -1 || arg.indexOf('youtu.be') !== -1) { + protocol(arg, formatter) +} else { + console.log(arg) +} diff --git a/liq/utils.liq b/liq/utils.liq new file mode 100644 index 0000000..ea26853 --- /dev/null +++ b/liq/utils.liq @@ -0,0 +1,24 @@ +# Add a skip function to a source +# when it does not have one +# by default +def add_skip_command(~command,s) + # Register the command: + server.register( + usage=command, + description="Skip the current video in source.", + command, + fun(_) -> begin + print("Skipping...") + source.skip(s) + "OK!" + end + ) +end + +# Clean-up after video ended +def file_end_cleanup(time, m) + if m["temporary"] == "true" and m["filename"] != "" then + print("rm "^quote(m["filename"])) + system("rm "^quote(m["filename"])) + end +end diff --git a/liq/view.liq b/liq/view.liq new file mode 100644 index 0000000..3c35901 --- /dev/null +++ b/liq/view.liq @@ -0,0 +1,34 @@ + +%include "config.liq" +%include "protocols.liq" +%include "rtmp.liq" +%include "utils.liq" + +set("server.telnet", true) +set("server.telnet.bind_addr", confstr("control.host", "0.0.0.0")) +set("server.telnet.port", confint("control.port", 7004)) + +set("frame.video.width",1280) +set("frame.video.height",720) +set("frame.video.samplerate",25) + +video_no_queue = single(confstr("liquidsoap.fallback",""), conservative=true) +video_base = video.add_image( + width=300,height=400, + x=900,y=80, + file="schedule.png", + video_no_queue) + +queue1 = request.equeue(id="queue", conservative=true, length=60., timeout=1000.) +add_skip_command(command="queue.skip", queue1) + +source = fallback([queue1, video_base],track_sensitive=false) +add_skip_command(command="skip", source) + +source = on_end(delay=0., file_end_cleanup, source) + +output.rtmp.live( + key=confstr("rtmp.key",""), + url=confstr("rtmp.url","rtmp://localhost/live"), + video_bitrate=confint("liquidsoap.bitrate", 2000), + source) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f680b2c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,491 @@ +{ + "name": "icytv", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "canvas": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.2.0.tgz", + "integrity": "sha512-4blMi2I2DuHh9UZNrmvU0hyY4dZJFOjNuqaZpI/66pKCyX1HPstvK+f2fIdc+NaF8b6wiuhvwXEFNkm7jIKYSA==", + "requires": { + "nan": "^2.11.1", + "node-pre-gyp": "^0.11.0" + } + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.0.tgz", + "integrity": "sha512-zT5nC0JhbljmyEf+Z456nvm7iO7XgRV2hYxoBtPpnyp+0Q4aCoP6uWNn76v/I6k2kCYNLWqWbwBWQcjsNI/bjw==" + }, + "needle": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + }, + "npm-packlist": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..568d182 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "icytv", + "version": "1.0.0", + "description": "Liquidsoap WebTV Channel", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Evert \"Diamond\" Prants ", + "license": "MIT", + "dependencies": { + "canvas": "^2.2.0" + } +}