New feature: featured links
This commit is contained in:
parent
4dda891389
commit
f82214a6f5
82
app.js
82
app.js
@ -11,6 +11,7 @@ const fs = require('fs')
|
|||||||
const uuid = require('uuid/v4')
|
const uuid = require('uuid/v4')
|
||||||
const redis = require('redis')
|
const redis = require('redis')
|
||||||
const connectSession = require('connect-redis')
|
const connectSession = require('connect-redis')
|
||||||
|
const URL = require('url')
|
||||||
|
|
||||||
require('express-async-errors')
|
require('express-async-errors')
|
||||||
|
|
||||||
@ -289,10 +290,17 @@ app.use(async function (req, res, next) {
|
|||||||
|
|
||||||
res.locals.session = { uuid: req.session.login, username: req.session.username }
|
res.locals.session = { uuid: req.session.login, username: req.session.username }
|
||||||
|
|
||||||
|
if (!cache.streamers[req.session.login]) {
|
||||||
|
let db = await dbPromise
|
||||||
|
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.session.login)
|
||||||
|
if (streamer) cache.streamers[req.session.login] = streamer
|
||||||
|
}
|
||||||
|
|
||||||
if (cache.streamers[req.session.login]) {
|
if (cache.streamers[req.session.login]) {
|
||||||
req.isStreamer = true
|
req.isStreamer = true
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -350,6 +358,69 @@ app.get('/dashboard/data', async (req, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get links
|
||||||
|
app.get('/dashboard/link', async (req, res) => {
|
||||||
|
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
|
||||||
|
let user = req.session.login
|
||||||
|
|
||||||
|
let db = await dbPromise
|
||||||
|
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
|
||||||
|
|
||||||
|
res.jsonp(links)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add link URL
|
||||||
|
app.post('/dashboard/link', async (req, res) => {
|
||||||
|
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
|
||||||
|
let user = req.session.login
|
||||||
|
let name = req.body.name
|
||||||
|
let url = req.body.url
|
||||||
|
|
||||||
|
if (name == null || url == null) return res.jsonp({ error: 'Missing parameters!' })
|
||||||
|
if (name.length > 120) return res.jsonp({ error: 'Only 120 characters are allowed in the name.' })
|
||||||
|
if (name.indexOf('<') !== -1 || name.indexOf('>') !== -1) return res.jsonp({ error: 'HTML tags are forbidden!' })
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
try {
|
||||||
|
URL.parse(url)
|
||||||
|
} catch (e) {
|
||||||
|
return res.jsonp({ error: 'Invalid URL!' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks
|
||||||
|
let db = await dbPromise
|
||||||
|
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
|
||||||
|
if (links.length > 10) return res.jsonp({ error: 'You can currently only add up to 10 links!' })
|
||||||
|
|
||||||
|
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', url, user)
|
||||||
|
if (link) return res.jsonp({ error: 'This URL already exists!' })
|
||||||
|
|
||||||
|
// Add
|
||||||
|
await db.run('INSERT INTO link (name,url,uuid) VALUES (?,?,?)', name, url, user)
|
||||||
|
res.jsonp({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove link URL
|
||||||
|
app.post('/dashboard/link/delete', async (req, res) => {
|
||||||
|
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' })
|
||||||
|
let user = req.session.login
|
||||||
|
|
||||||
|
if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' })
|
||||||
|
|
||||||
|
// Check
|
||||||
|
let db = await dbPromise
|
||||||
|
let link = await db.get('SELECT * FROM link WHERE url = ? AND uuid = ?', req.body.url, user)
|
||||||
|
if (!link) {
|
||||||
|
link = await db.get('SELECT * FROM link WHERE name = ? AND uuid = ?', req.body.name, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link) return res.jsonp({ error: 'Invalid link parameter!' })
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await db.run('DELETE FROM link WHERE id = ?', link.id)
|
||||||
|
res.jsonp({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
// 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 })
|
||||||
@ -363,11 +434,20 @@ app.get('/player/:name', (req, res) => {
|
|||||||
app.get('/api/channel/:name', async (req, res) => {
|
app.get('/api/channel/:name', async (req, res) => {
|
||||||
let name = req.params.name
|
let name = req.params.name
|
||||||
let db = await dbPromise
|
let db = await dbPromise
|
||||||
let data = await db.get('SELECT name,live_at,last_stream FROM channels WHERE name=?', name)
|
let data = await db.get('SELECT user_uuid,name,live_at,last_stream FROM channels WHERE name=?', name)
|
||||||
if (!data) return res.jsonp({ error: 'No such channel!' })
|
if (!data) return res.jsonp({ error: 'No such channel!' })
|
||||||
|
let links = await db.all('SELECT * FROM link WHERE uuid = ?', data.user_uuid)
|
||||||
|
|
||||||
data.live = data.live_at != null
|
data.live = data.live_at != null
|
||||||
data.live_at = new Date(parseInt(data.live_at))
|
data.live_at = new Date(parseInt(data.live_at))
|
||||||
data.last_stream = new Date(parseInt(data.last_stream))
|
data.last_stream = new Date(parseInt(data.last_stream))
|
||||||
|
data.links = links || []
|
||||||
|
|
||||||
|
for (let i in data.links) {
|
||||||
|
delete data.links[i].id
|
||||||
|
delete data.links[i].uuid
|
||||||
|
}
|
||||||
|
|
||||||
res.jsonp(data)
|
res.jsonp(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -13,4 +13,3 @@
|
|||||||
[OAuth2]
|
[OAuth2]
|
||||||
ClientID = 1
|
ClientID = 1
|
||||||
ClientSecret = "hackme"
|
ClientSecret = "hackme"
|
||||||
|
|
||||||
|
10
migrations/002_link.sql
Normal file
10
migrations/002_link.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- Up
|
||||||
|
CREATE TABLE link (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
uuid TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
DROP TABLE link;
|
@ -118,3 +118,16 @@ body {
|
|||||||
.seekview:hover > .seeker {
|
.seekview:hover > .seeker {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.overlay-list {
|
||||||
|
background-color: hsla(0, 0%, 12%, 0.75);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.overlay-list.links a {
|
||||||
|
padding: 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.1em;
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
@ -28,6 +28,29 @@ function recursiveStats (table, subtable) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateLinkList (k) {
|
||||||
|
if (k && k.error) return alert(k.error)
|
||||||
|
$.get('/dashboard/link', function (res) {
|
||||||
|
if (res.error) return
|
||||||
|
$('#link-list').html('<tbody></tbody>')
|
||||||
|
$('#link-list tbody').append('<tr><th>Name</th><th>URL</th><th>Action</th></tr>')
|
||||||
|
|
||||||
|
for (let i in res) {
|
||||||
|
let p = res[i]
|
||||||
|
p.name = p.name.replace(/\</g, '<').replace(/\>/g, '>')
|
||||||
|
$('#link-list tbody').append('<tr data-url="' + p.url + '"><td>' + p.name +'</td><td>' +
|
||||||
|
'<a href="' + p.url +'" target="_blank" rel="nofollow">' +
|
||||||
|
p.url + '</a></td><td><a href="#" class="delete-link">Remove</a></td></tr>')
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.delete-link').click(function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
let pr = $(this).parent().parent().attr('data-url')
|
||||||
|
$.post('/dashboard/link/delete', { url: pr }, updateLinkList)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function dashboard (k) {
|
function dashboard (k) {
|
||||||
$.get('/dashboard/data', function (res) {
|
$.get('/dashboard/data', function (res) {
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
@ -46,12 +69,46 @@ function dashboard (k) {
|
|||||||
$('#show_key').html(k)
|
$('#show_key').html(k)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('.go-page').click(function (e) {
|
||||||
|
let el = $(this)
|
||||||
|
$('.go-page').removeClass('active')
|
||||||
|
el.addClass('active')
|
||||||
|
$('.page:visible').fadeOut(function () {
|
||||||
|
$('#page-' + el.attr('data-page')).fadeIn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#add-link').submit(function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
let name = $('input[name="name"]').val()
|
||||||
|
let url = $('input[name="url"]').val()
|
||||||
|
|
||||||
|
if (name.length > 120) return alert('Only 120 characters are allowed in the name.')
|
||||||
|
|
||||||
|
$.post('/dashboard/link', { name, url }, function () {
|
||||||
|
$('input[name="name"]').val('')
|
||||||
|
$('input[name="url"]').val('')
|
||||||
|
updateLinkList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
$.get('/dashboard/stats', function (res) {
|
$.get('/dashboard/stats', function (res) {
|
||||||
if (res.error) return
|
if (res.error) return
|
||||||
recursiveStats(res, '')
|
recursiveStats(res, '')
|
||||||
})
|
})
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
|
updateLinkList()
|
||||||
|
|
||||||
|
let hash = window.location.hash
|
||||||
|
if (hash.indexOf('#') === 0) hash = hash.substr(1)
|
||||||
|
if ($('#page-' + hash).length) {
|
||||||
|
$('.go-page').removeClass('active')
|
||||||
|
$('.go-page[data-page="' + hash + '"').addClass('active')
|
||||||
|
$('.page:visible').hide()
|
||||||
|
$('.page#page-' + hash).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default dashboard
|
export default dashboard
|
||||||
|
103
src/player.js
103
src/player.js
@ -16,6 +16,8 @@ let bigbtn = overlay.querySelector('.bigplaybtn')
|
|||||||
let volumebar = overlay.querySelector('#volume_seek')
|
let volumebar = overlay.querySelector('#volume_seek')
|
||||||
let volumeseek = volumebar.querySelector('.seeker')
|
let volumeseek = volumebar.querySelector('.seeker')
|
||||||
let volumeseekInner = volumeseek.querySelector('.seekbar')
|
let volumeseekInner = volumeseek.querySelector('.seekbar')
|
||||||
|
let links
|
||||||
|
let linksList
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
let hls
|
let hls
|
||||||
@ -251,6 +253,100 @@ if (Hls.isSupported()) {
|
|||||||
alert('Your browser does not support HLS streaming!')
|
alert('Your browser does not support HLS streaming!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helper function to get an element's exact position
|
||||||
|
function getPosition(el) {
|
||||||
|
let xPosition = 0
|
||||||
|
let yPosition = 0
|
||||||
|
|
||||||
|
while (el) {
|
||||||
|
if (el.tagName == 'BODY') {
|
||||||
|
// deal with browser quirks with body/window/document and page scroll
|
||||||
|
let xScrollPos = el.scrollLeft || document.documentElement.scrollLeft
|
||||||
|
let yScrollPos = el.scrollTop || document.documentElement.scrollTop
|
||||||
|
|
||||||
|
xPosition += (el.offsetLeft - xScrollPos + el.clientLeft)
|
||||||
|
yPosition += (el.offsetTop - yScrollPos + el.clientTop)
|
||||||
|
} else {
|
||||||
|
xPosition += (el.offsetLeft - el.scrollLeft + el.clientLeft)
|
||||||
|
yPosition += (el.offsetTop - el.scrollTop + el.clientTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
el = el.offsetParent
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: xPosition,
|
||||||
|
y: yPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOnClickOutside (element) {
|
||||||
|
const isVisible = (element) => {
|
||||||
|
return element.style.display === 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
const outsideClickListener = event => {
|
||||||
|
if ((!element.contains(event.target) && isVisible(element))
|
||||||
|
&& (event.target !== links && !links.contains(event.target))) {
|
||||||
|
element.style.display = 'none'
|
||||||
|
removeClickListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeClickListener = () => {
|
||||||
|
document.removeEventListener('click', outsideClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', outsideClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLinks (srcs) {
|
||||||
|
if (srcs.length === 0) {
|
||||||
|
if (links) links.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!links) {
|
||||||
|
links = document.createElement('div')
|
||||||
|
links.className = 'right button'
|
||||||
|
links.id = 'links'
|
||||||
|
links.innerHTML = '<i class="fa fa-link" aria-hidden="true"></i>'
|
||||||
|
overlay.getElementsByClassName('controls')[0].insertBefore(links, fullscreenbtn)
|
||||||
|
|
||||||
|
linksList = document.createElement('div')
|
||||||
|
linksList.className = 'links overlay-list'
|
||||||
|
linksList.style = 'display: none;'
|
||||||
|
overlay.appendChild(linksList)
|
||||||
|
|
||||||
|
links.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (linksList.style.display === 'block') {
|
||||||
|
linksList.style = 'display: none;'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hideOnClickOutside(linksList)
|
||||||
|
|
||||||
|
let pos = getPosition(links)
|
||||||
|
linksList.style = 'display: block;'
|
||||||
|
pos.x -= linksList.offsetWidth - links.offsetWidth / 2
|
||||||
|
pos.y -= linksList.offsetHeight
|
||||||
|
linksList.style = 'display: block; left: ' + pos.x + 'px; top: ' + pos.y + 'px;'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
links.style.display = 'block'
|
||||||
|
linksList.innerHTML = ''
|
||||||
|
|
||||||
|
for (let i in srcs) {
|
||||||
|
let link = srcs[i]
|
||||||
|
let el = document.createElement('a')
|
||||||
|
el.href = link.url
|
||||||
|
el.innerText = link.name
|
||||||
|
el.target = '_blank'
|
||||||
|
linksList.appendChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStreamStatus () {
|
function getStreamStatus () {
|
||||||
GET('/api/channel/' + STREAM_NAME).then((data) => {
|
GET('/api/channel/' + STREAM_NAME).then((data) => {
|
||||||
let jd = JSON.parse(data)
|
let jd = JSON.parse(data)
|
||||||
@ -259,6 +355,8 @@ function getStreamStatus () {
|
|||||||
return alert(jd.error)
|
return alert(jd.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jd.links) updateLinks(jd.links)
|
||||||
|
|
||||||
if (jd.live && !vidReady) loadSource()
|
if (jd.live && !vidReady) loadSource()
|
||||||
liveStatus(jd.live)
|
liveStatus(jd.live)
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
@ -278,6 +376,11 @@ document.addEventListener('mozfullscreenchange', exitHandler, false)
|
|||||||
document.addEventListener('fullscreenchange', exitHandler, false)
|
document.addEventListener('fullscreenchange', exitHandler, false)
|
||||||
document.addEventListener('MSFullscreenChange', exitHandler, false)
|
document.addEventListener('MSFullscreenChange', exitHandler, false)
|
||||||
|
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
if (!linksList || linksList.style.display !== 'block') return
|
||||||
|
linksList.style = 'display: none;'
|
||||||
|
})
|
||||||
|
|
||||||
vid.addEventListener('timeupdate', updateTime, false)
|
vid.addEventListener('timeupdate', updateTime, false)
|
||||||
|
|
||||||
getStreamStatus()
|
getStreamStatus()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.STREAM_KEY = "{{ stream }}"
|
window.STREAM_KEY = "{{ stream }}"
|
||||||
</script>
|
</script>
|
||||||
|
<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/bootstrap.min.css">
|
<link rel="stylesheet" type="text/css" href="/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/dist/css/dashboard.css">
|
<link rel="stylesheet" type="text/css" href="/dist/css/dashboard.css">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -25,16 +26,22 @@
|
|||||||
<div class="sidebar-sticky">
|
<div class="sidebar-sticky">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="#">
|
<a class="nav-link active go-page" href="#dashboard" data-page="dashboard">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
<i class="fa fa-home" aria-hidden="true"></i>
|
||||||
Dashboard <span class="sr-only">(current)</span>
|
Dashboard <span class="sr-only">(current)</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link go-page" href="#links" data-page="links">
|
||||||
|
<i class="fa fa-link" aria-hidden="true"></i>
|
||||||
|
Manage Links
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-dashboard">
|
<main role="main" class="page col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-dashboard">
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">Dashboard</h1>
|
<h1 class="h2">Dashboard</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -125,6 +132,20 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<main role="main" class="page col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" id="page-links" style="display: none;">
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">My Links</h1>
|
||||||
|
</div>
|
||||||
|
<p>These links will be automatically available from under the player. You can use it to link your social medias, chats or personal website.</p>
|
||||||
|
<table class="list table" id="link-list"></table>
|
||||||
|
<p>Link name can only contain 120 characters and forbids the use of HTML tags.</p>
|
||||||
|
<form id="add-link">
|
||||||
|
<input placeholder="Name" id="add-link-name" name="name" />
|
||||||
|
<input placeholder="URL" id="add-link-url" name="url" />
|
||||||
|
<input type="submit" value="Add URL">
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/dist/main.bundle.js"></script>
|
<script src="/dist/main.bundle.js"></script>
|
||||||
|
Reference in New Issue
Block a user