diff --git a/app.js b/app.js
index 0983801..d0384c5 100644
--- a/app.js
+++ b/app.js
@@ -11,6 +11,7 @@ const fs = require('fs')
const uuid = require('uuid/v4')
const redis = require('redis')
const connectSession = require('connect-redis')
+const URL = require('url')
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 }
+ 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]) {
req.isStreamer = true
return 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
app.get('/watch/:name', (req, res) => {
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) => {
let name = req.params.name
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!' })
+ let links = await db.all('SELECT * FROM link WHERE uuid = ?', data.user_uuid)
+
data.live = data.live_at != null
data.live_at = new Date(parseInt(data.live_at))
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)
})
diff --git a/config.example.toml b/config.example.toml
index 268d7fa..1a50c33 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -13,4 +13,3 @@
[OAuth2]
ClientID = 1
ClientSecret = "hackme"
-
diff --git a/migrations/002_link.sql b/migrations/002_link.sql
new file mode 100644
index 0000000..bdb4708
--- /dev/null
+++ b/migrations/002_link.sql
@@ -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;
diff --git a/src/css/player.css b/src/css/player.css
index 533cf41..51cf433 100644
--- a/src/css/player.css
+++ b/src/css/player.css
@@ -118,3 +118,16 @@ body {
.seekview:hover > .seeker {
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;
+}
diff --git a/src/dashboard.js b/src/dashboard.js
index e5c0f00..10ea872 100644
--- a/src/dashboard.js
+++ b/src/dashboard.js
@@ -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('
')
+ $('#link-list tbody').append('Name | URL | Action |
')
+
+ for (let i in res) {
+ let p = res[i]
+ p.name = p.name.replace(/\/g, '>')
+ $('#link-list tbody').append('' + p.name +' | ' +
+ '' +
+ p.url + ' | Remove |
')
+ }
+
+ $('.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) {
$.get('/dashboard/data', function (res) {
if (res.error) {
@@ -46,12 +69,46 @@ function dashboard (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 () {
$.get('/dashboard/stats', function (res) {
if (res.error) return
recursiveStats(res, '')
})
}, 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
diff --git a/src/player.js b/src/player.js
index 6f98550..37eed5c 100644
--- a/src/player.js
+++ b/src/player.js
@@ -16,6 +16,8 @@ let bigbtn = overlay.querySelector('.bigplaybtn')
let volumebar = overlay.querySelector('#volume_seek')
let volumeseek = volumebar.querySelector('.seeker')
let volumeseekInner = volumeseek.querySelector('.seekbar')
+let links
+let linksList
// Variables
let hls
@@ -251,6 +253,100 @@ if (Hls.isSupported()) {
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 = ''
+ 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 () {
GET('/api/channel/' + STREAM_NAME).then((data) => {
let jd = JSON.parse(data)
@@ -259,6 +355,8 @@ function getStreamStatus () {
return alert(jd.error)
}
+ if (jd.links) updateLinks(jd.links)
+
if (jd.live && !vidReady) loadSource()
liveStatus(jd.live)
}, (e) => {
@@ -278,6 +376,11 @@ document.addEventListener('mozfullscreenchange', exitHandler, false)
document.addEventListener('fullscreenchange', 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)
getStreamStatus()
diff --git a/templates/dashboard.html b/templates/dashboard.html
index 0041339..69281b1 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -4,6 +4,7 @@
+
@@ -25,16 +26,22 @@
-
+
Dashboard
@@ -125,6 +132,20 @@
+
+
+
+
My Links
+
+ These links will be automatically available from under the player. You can use it to link your social medias, chats or personal website.
+
+ Link name can only contain 120 characters and forbids the use of HTML tags.
+
+