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('NameURLAction') + + 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.

+ +