updated packages, email subscriptions
This commit is contained in:
parent
fb3d54205e
commit
723b2e4aa5
192
app.js
192
app.js
@ -1,6 +1,9 @@
|
|||||||
const connectSession = require('connect-redis')
|
const connectSession = require('connect-redis')
|
||||||
|
const rateLimit = require("express-rate-limit")
|
||||||
|
const validemail = require('email-validator')
|
||||||
const session = require('express-session')
|
const session = require('express-session')
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
|
const nodemailer = require('nodemailer')
|
||||||
const nunjucks = require('nunjucks')
|
const nunjucks = require('nunjucks')
|
||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
@ -8,6 +11,7 @@ const request = require('request')
|
|||||||
const sqlite3 = require('sqlite3')
|
const sqlite3 = require('sqlite3')
|
||||||
const sqlite = require('sqlite')
|
const sqlite = require('sqlite')
|
||||||
const xml2js = require('xml2js')
|
const xml2js = require('xml2js')
|
||||||
|
const crypto = require('crypto')
|
||||||
const WebSocket = require('ws')
|
const WebSocket = require('ws')
|
||||||
const redis = require('redis')
|
const redis = require('redis')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
@ -52,6 +56,21 @@ config = Object.assign({
|
|||||||
callbackURL: 'http://localhost:5000/auth/_callback/',
|
callbackURL: 'http://localhost:5000/auth/_callback/',
|
||||||
clientID: '1',
|
clientID: '1',
|
||||||
clientSecret: 'changeme'
|
clientSecret: 'changeme'
|
||||||
|
},
|
||||||
|
Email: {
|
||||||
|
enabled: false,
|
||||||
|
from: 'no-reply@icynet.eu',
|
||||||
|
host: '',
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
baseURL: 'http://localhost:9321/',
|
||||||
|
auth: {
|
||||||
|
user: '',
|
||||||
|
pass: '',
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}, config)
|
}, config)
|
||||||
|
|
||||||
@ -72,6 +91,13 @@ const app = express()
|
|||||||
const server = http.createServer(app)
|
const server = http.createServer(app)
|
||||||
const wss = new WebSocket.Server({ clientTracking: false, noServer: true })
|
const wss = new WebSocket.Server({ clientTracking: false, noServer: true })
|
||||||
|
|
||||||
|
// Rate limits
|
||||||
|
const emlLimiter = rateLimit({
|
||||||
|
windowMs: 1000 * 60 * 60 * 24,
|
||||||
|
max: 5,
|
||||||
|
message: 'Too many subscription attempts from this IP address. Try again in 24 hours.'
|
||||||
|
})
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
const Strategy = require(config.Auth.strategy)
|
const Strategy = require(config.Auth.strategy)
|
||||||
const strategyConfig = Object.assign({}, config.Auth)
|
const strategyConfig = Object.assign({}, config.Auth)
|
||||||
@ -82,6 +108,101 @@ passport.use(new Strategy(strategyConfig, function (accessToken, refreshToken, p
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Email
|
||||||
|
let emailTransport;
|
||||||
|
if (config.Email.enabled) {
|
||||||
|
emailTransport = nodemailer.createTransport({
|
||||||
|
...config.Email,
|
||||||
|
pool: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifQueue = []
|
||||||
|
|
||||||
|
function now() {
|
||||||
|
return Math.floor(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function key() {
|
||||||
|
return crypto.randomBytes(32).toString('hex').slice(0, 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendEmailPush(channel) {
|
||||||
|
if (!emailTransport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const db = await dbPromise
|
||||||
|
const { name } = await db.get('SELECT name FROM channels WHERE user_uuid = ?', channel)
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const subs = await db.all('SELECT email,unsubkey FROM emailsub WHERE uuid = ? AND active = 1', channel);
|
||||||
|
for (const sub of subs) {
|
||||||
|
const unsubURL = config.Email.baseURL + 'unsubscribe/' + sub.unsubkey
|
||||||
|
const watchURL = config.Email.baseURL + 'watch/' + name
|
||||||
|
emailTransport.sendMail({
|
||||||
|
from: config.Email.from,
|
||||||
|
to: sub.email,
|
||||||
|
subject: `🔴 ${name} has gone LIVE on IcyTV!`,
|
||||||
|
text: `${name} has gone LIVE on IcyTV!\nWatch now: ${watchURL}`
|
||||||
|
+ `\n\nUnsubscribe from ${name}: ${unsubURL}`,
|
||||||
|
html: `<h1>${name} has gone LIVE on IcyTV!</h1><p>Watch now: `
|
||||||
|
+ `<a href="${watchURL}" target="_blank" rel="nofollow">${watchURL}</a>`
|
||||||
|
+ `</p><br/><p>Unsubscribe from ${name}: `
|
||||||
|
+ `<a href="${unsubURL}" target="_blank" rel="nofollow">${unsubURL}</a></p>`,
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribeToChannel(channel, email) {
|
||||||
|
if (!emailTransport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await dbPromise
|
||||||
|
const { user_uuid } = await db.get('SELECT user_uuid FROM channels WHERE name = ?', channel)
|
||||||
|
if (!user_uuid) {
|
||||||
|
throw new Error('Invalid channel!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await db.get('SELECT * FROM emailsub WHERE email = ? AND uuid = ?', [email, user_uuid])
|
||||||
|
if (exists) {
|
||||||
|
throw new Error('A subscription already exists for this email address.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// New verification email
|
||||||
|
const activateKey = key()
|
||||||
|
const unsubKey = key()
|
||||||
|
const activateURL = config.Email.baseURL + 'email/' + activateKey
|
||||||
|
await db.run('INSERT INTO emailsub (unsubkey, activatekey, email, uuid, active, created_at) VALUES '
|
||||||
|
+ '(?, ?, ?, ?, 0, ?)', [unsubKey, activateKey, email, user_uuid, now()])
|
||||||
|
|
||||||
|
await emailTransport.sendMail({
|
||||||
|
from: config.Email.from,
|
||||||
|
to: email,
|
||||||
|
subject: `Confirm IcyTV subscription to channel ${channel}`,
|
||||||
|
text: `Confirm your subscription\n\nClick here to subscribe to ${channel}: ${activateURL} `
|
||||||
|
+ `\n\nIf you did not subscribe to ${channel} on IcyTV, please ignore this email `
|
||||||
|
+ `\nand no further action is required on your part. If these emails persist, please `
|
||||||
|
+ `\ncontact us via abuse@icynet.eu and we'll be sure to help you.`,
|
||||||
|
html: `<h1>Confirm your subscription</h1><p>Click here to subscribe to ${channel}: `
|
||||||
|
+ `<a href="${activateURL}" target="_blank" rel="nofollow">${activateURL}</a>`
|
||||||
|
+ `</p><br/><p>If you did not subscribe to ${channel} on IcyTV, please ignore this email `
|
||||||
|
+ `and no further action is required on your part. If these emails persist, please contact us via `
|
||||||
|
+ `<a href="mailto:abuse@icynet.eu">abuse@icynet.eu</a> and we'll be sure to help you.</p>`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribe(key) {
|
||||||
|
const db = await dbPromise
|
||||||
|
await db.run('DELETE FROM emailsub WHERE unsubkey = ?', key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateSubscription(key) {
|
||||||
|
const db = await dbPromise
|
||||||
|
await db.run('UPDATE emailsub SET active = 1 WHERE activatekey = ?', key)
|
||||||
|
}
|
||||||
|
|
||||||
passport.serializeUser(function (user, done) {
|
passport.serializeUser(function (user, done) {
|
||||||
done(null, user)
|
done(null, user)
|
||||||
})
|
})
|
||||||
@ -119,6 +240,13 @@ app.use(sessionParser)
|
|||||||
app.use(passport.initialize())
|
app.use(passport.initialize())
|
||||||
app.use(passport.session())
|
app.use(passport.session())
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (!req.session.csrf) {
|
||||||
|
req.session.csrf = key()
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
// Parse stream metrics from the stat.xml file
|
// Parse stream metrics from the stat.xml file
|
||||||
async function pullMetrics (uuid) {
|
async function pullMetrics (uuid) {
|
||||||
const statPath = streamServer + 'stat'
|
const statPath = streamServer + 'stat'
|
||||||
@ -202,6 +330,11 @@ app.post('/publish', async (req, res) => {
|
|||||||
db.run('UPDATE channels SET live_at=? WHERE id=?', Date.now(), streamer.id)
|
db.run('UPDATE channels SET live_at=? WHERE id=?', Date.now(), streamer.id)
|
||||||
cache.live.push(streamer.name)
|
cache.live.push(streamer.name)
|
||||||
|
|
||||||
|
// Send notifications
|
||||||
|
if (!notifQueue.includes(streamer.user_uuid)) {
|
||||||
|
notifQueue.push(streamer.user_uuid)
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect the streaming server to the target
|
// Redirect the streaming server to the target
|
||||||
res.set('Location', publishAddress)
|
res.set('Location', publishAddress)
|
||||||
res.status(302)
|
res.status(302)
|
||||||
@ -388,7 +521,12 @@ app.post('/dashboard/link/delete', authed, async (req, res) => {
|
|||||||
|
|
||||||
// Player
|
// Player
|
||||||
app.get('/watch/:name', (req, res) => {
|
app.get('/watch/:name', (req, res) => {
|
||||||
res.render('player.html', { name: req.params.name, server: streamServer })
|
res.render('player.html', {
|
||||||
|
name: req.params.name,
|
||||||
|
server: streamServer,
|
||||||
|
csrf: req.session.csrf,
|
||||||
|
email: emailTransport != null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/player/:name', (req, res) => {
|
app.get('/player/:name', (req, res) => {
|
||||||
@ -399,7 +537,7 @@ app.get('/player/:name', (req, res) => {
|
|||||||
app.get('/api/channel/:name', async (req, res) => {
|
app.get('/api/channel/:name', async (req, res) => {
|
||||||
const name = req.params.name
|
const name = req.params.name
|
||||||
const db = await dbPromise
|
const db = await dbPromise
|
||||||
const data = await db.get('SELECT user_uuid,name,live_at,last_stream FROM channels WHERE name=?', name)
|
const data = await db.get('SELECT user_uuid,name,live_at,last_stream,chat_channel FROM channels WHERE name=?', name)
|
||||||
if (!data) return res.jsonp({ error: 'No such channel!' })
|
if (!data) return res.jsonp({ error: 'No such channel!' })
|
||||||
const links = await db.all('SELECT name,url FROM link WHERE uuid = ?', data.user_uuid)
|
const links = await db.all('SELECT name,url FROM link WHERE uuid = ?', data.user_uuid)
|
||||||
|
|
||||||
@ -414,6 +552,45 @@ app.get('/api/channel/:name', async (req, res) => {
|
|||||||
res.jsonp(data)
|
res.jsonp(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post('/api/email/:channel', emlLimiter, async (req, res) => {
|
||||||
|
if (!emailTransport) {
|
||||||
|
return res.json({ message: 'Email transport is disabled.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrf = req.body.csrf
|
||||||
|
if (!csrf || !req.session.csrf || req.session.csrf !== csrf) {
|
||||||
|
return res.status(400).json({error: true, message: 'Illegal request!'})
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = req.body.email
|
||||||
|
if (!email || !validemail.validate(email)) {
|
||||||
|
return res.status(400).json({error: true, message: 'Invalid email address!'})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await subscribeToChannel(req.params.channel, email)
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(400).json({error: true, message: e.message})
|
||||||
|
}
|
||||||
|
res.json({ message: 'Confirmation email has been sent!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/email/:key', async (req, res) => {
|
||||||
|
if (!emailTransport) {
|
||||||
|
return res.redirect('/?activated=false')
|
||||||
|
}
|
||||||
|
await activateSubscription(req.params.key)
|
||||||
|
res.redirect('/?activated=true')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/unsubscribe/:key', async (req, res) => {
|
||||||
|
if (!emailTransport) {
|
||||||
|
return res.redirect('/?unsubscribe=false')
|
||||||
|
}
|
||||||
|
await unsubscribe(req.params.key)
|
||||||
|
res.redirect('/?unsubscribe=true')
|
||||||
|
})
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use((error, req, res, next) => {
|
app.use((error, req, res, next) => {
|
||||||
if (dev) console.error(error.stack)
|
if (dev) console.error(error.stack)
|
||||||
@ -487,6 +664,15 @@ server.on('upgrade', (request, socket, head) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Stream start notifications pump
|
||||||
|
function notify() {
|
||||||
|
const channel = notifQueue.pop()
|
||||||
|
if (channel) {
|
||||||
|
sendEmailPush(channel).catch(e => console.error(e))
|
||||||
|
}
|
||||||
|
setTimeout(notify, notifQueue.length ? 1000 : 5000)
|
||||||
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
const host = dev ? '0.0.0.0' : '127.0.0.1'
|
const host = dev ? '0.0.0.0' : '127.0.0.1'
|
||||||
server.listen(port, host, () => {
|
server.listen(port, host, () => {
|
||||||
@ -506,4 +692,6 @@ server.listen(port, host, () => {
|
|||||||
|
|
||||||
console.log('Listening on %s:%d', host, port)
|
console.log('Listening on %s:%d', host, port)
|
||||||
console.log('Authentication module: %s (%s)', strategyConfig.strategy, strategyConfig.provider)
|
console.log('Authentication module: %s (%s)', strategyConfig.strategy, strategyConfig.provider)
|
||||||
|
|
||||||
|
notify()
|
||||||
})
|
})
|
||||||
|
@ -14,3 +14,16 @@
|
|||||||
tokenURL = "http://localhost/oauth2/token"
|
tokenURL = "http://localhost/oauth2/token"
|
||||||
clientID = 1
|
clientID = 1
|
||||||
clientSecret = "hackme"
|
clientSecret = "hackme"
|
||||||
|
|
||||||
|
[Email]
|
||||||
|
enabled = false
|
||||||
|
from = 'no-reply@icynet.eu'
|
||||||
|
host = 'smtp.google.com'
|
||||||
|
port = 587
|
||||||
|
secure = false
|
||||||
|
baseURL = 'http://localhost:9321/'
|
||||||
|
[Email.auth]
|
||||||
|
user = 'my-address@gmail.com'
|
||||||
|
pass = 'hack me lol'
|
||||||
|
[Email.tls]
|
||||||
|
rejectUnauthorized = false
|
||||||
|
4
migrations/003_chat.sql
Normal file
4
migrations/003_chat.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
-- Up
|
||||||
|
ALTER TABLE channels ADD COLUMN chat_channel TEXT;
|
||||||
|
|
||||||
|
-- Down
|
14
migrations/004_email_subscribe.sql
Normal file
14
migrations/004_email_subscribe.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
-- Up
|
||||||
|
CREATE TABLE emailsub (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
unsubkey TEXT NOT NULL,
|
||||||
|
activatekey TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
uuid TEXT NOT NULL,
|
||||||
|
active INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
DROP TABLE emailsub;
|
||||||
|
|
5579
package-lock.json
generated
5579
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@ -7,36 +7,39 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"watch": "webpack -w --mode=development --log-level=debug",
|
"watch": "webpack -w --mode=development --log-level=debug",
|
||||||
"build": "webpack -p",
|
"build": "webpack --mode=production",
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"serve": "NODE_ENV=\"development\" node app.js"
|
"serve": "NODE_ENV=\"development\" node app.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.6.0",
|
||||||
"copy-webpack-plugin": "^6.0.1",
|
"copy-webpack-plugin": "^7.0.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.2.0",
|
||||||
"hls.js": "^0.13.2",
|
"hls.js": "^0.14.17",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^5.21.2",
|
||||||
"webpack-command": "^0.5.0"
|
"webpack-cli": "^4.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"connect-redis": "^4.0.4",
|
"connect-redis": "^5.1.0",
|
||||||
"ejs": "^3.1.3",
|
"ejs": "^3.1.6",
|
||||||
|
"email-validator": "^2.0.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
|
"express-rate-limit": "^5.2.5",
|
||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
"nunjucks": "^3.2.1",
|
"nodemailer": "^6.4.18",
|
||||||
|
"nunjucks": "^3.2.2",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.0.2",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"sqlite": "^4.0.9",
|
"sqlite": "^4.0.19",
|
||||||
"sqlite3": "^4.2.0",
|
"sqlite3": "^5.0.1",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"uuid": "^8.1.0",
|
"uuid": "^8.3.2",
|
||||||
"ws": "^7.3.0",
|
"ws": "^7.4.3",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const overlay = player.querySelector('.overlay')
|
|||||||
const btn = overlay.querySelector('#playbtn')
|
const btn = overlay.querySelector('#playbtn')
|
||||||
const time = overlay.querySelector('#duration')
|
const time = overlay.querySelector('#duration')
|
||||||
const fullscreenbtn = overlay.querySelector('#fullscrbtn')
|
const fullscreenbtn = overlay.querySelector('#fullscrbtn')
|
||||||
|
const subbtn = overlay.querySelector('#subbtn')
|
||||||
const playbtn = overlay.querySelector('#playbtn')
|
const playbtn = overlay.querySelector('#playbtn')
|
||||||
const mutebtn = overlay.querySelector('#mutebtn')
|
const mutebtn = overlay.querySelector('#mutebtn')
|
||||||
const lstat = overlay.querySelector('.live')
|
const lstat = overlay.querySelector('.live')
|
||||||
@ -239,6 +240,24 @@ function handlePause () {
|
|||||||
showBigBtn(true)
|
showBigBtn(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askSubscription () {
|
||||||
|
const email = prompt('Enter your email address to get notifications for when this channel goes live')
|
||||||
|
if (!email) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetch('/api/email/' + STREAM_NAME, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email, csrf: CSRF_TOKEN
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(e => e.json())
|
||||||
|
.then(b => alert(b.message || 'Something went wrong!'))
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('onblur', () => {
|
window.addEventListener('onblur', () => {
|
||||||
shouldHide = true
|
shouldHide = true
|
||||||
}, false)
|
}, false)
|
||||||
@ -436,6 +455,11 @@ playbtn.addEventListener('click', toggleStream)
|
|||||||
mutebtn.addEventListener('click', toggleSound)
|
mutebtn.addEventListener('click', toggleSound)
|
||||||
fullscreenbtn.addEventListener('click', toggleFullscreen)
|
fullscreenbtn.addEventListener('click', toggleFullscreen)
|
||||||
|
|
||||||
|
subbtn.addEventListener('click', askSubscription)
|
||||||
|
if (EMAIL_ENABLED === 'false') {
|
||||||
|
subbtn.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('webkitfullscreenchange', exitHandler, false)
|
document.addEventListener('webkitfullscreenchange', exitHandler, false)
|
||||||
document.addEventListener('mozfullscreenchange', exitHandler, false)
|
document.addEventListener('mozfullscreenchange', exitHandler, false)
|
||||||
document.addEventListener('fullscreenchange', exitHandler, false)
|
document.addEventListener('fullscreenchange', exitHandler, false)
|
||||||
|
@ -6,8 +6,10 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/dist/css/player.css">
|
<link rel="stylesheet" type="text/css" href="/dist/css/player.css">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const STREAM_SERVER = '{{ server }}'
|
const STREAM_SERVER = '{{ server }}';
|
||||||
const STREAM_NAME = '{{ name }}'
|
const STREAM_NAME = '{{ name }}';
|
||||||
|
const CSRF_TOKEN = '{{ csrf }}';
|
||||||
|
const EMAIL_ENABLED = '{{ email }}';
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -29,7 +31,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<div id="duration">0:00</div>
|
<div id="duration">0:00</div>
|
||||||
<div class="flex-divider"></div>
|
<div class="flex-divider"></div>
|
||||||
<div id="fullscrbtn" class="right button"><i class="fa fa-expand fa-fw"></i></div>
|
<div id="subbtn" class="right button" title="Subscribe"><i class="fa fa-envelope-o fa-fw"></i></div>
|
||||||
|
<div id="fullscrbtn" class="right button" title="Fullscreen"><i class="fa fa-expand fa-fw"></i></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,13 +17,11 @@ module.exports = {
|
|||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [{
|
patterns: [{
|
||||||
from: 'src/css/*.css',
|
from: 'src/css/*.css',
|
||||||
to: 'css',
|
to: 'css'
|
||||||
flatten: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
|
from: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
|
||||||
to: 'css',
|
to: 'css'
|
||||||
flatten: true
|
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
Reference in New Issue
Block a user