Skip to content

Instantly share code, notes, and snippets.

@RealityRipple
Last active April 17, 2023 03:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RealityRipple/921e7e02849c280ee6f6adbd92312aff to your computer and use it in GitHub Desktop.
Save RealityRipple/921e7e02849c280ee6f6adbd92312aff to your computer and use it in GitHub Desktop.
RealityRipple's Homemade Songlist for Streamer Song List
<!doctype html>
<html>
<head>
<script>
/* RealityRipple's Homemade Songlist (Version 1.3) */
var userID = 'CHANNEL_NAME';
/* Twitch channel */
var oauthR = 'OAUTH_REFRESH';
/*
* oAuth Refresh Token (moderator:read:followers)
* You can get one on the Songlist website at:
* <https://realityripple.com/Tools/Twitch/Songlist/>
*/
var clientID = 'qkqa7k3ekuqbm5wxpekawtwv3xkmep';
/* Client ID (must match above oauth token) */
var displayTime = 10;
/* Number of seconds the queue will display for */
var displayCmd = [
'!tracks',
'!songlist',
'!sl',
'!list',
'!songqueue',
'!sq',
'!queue',
'!queuelist',
'!ql',
'!listqueue',
'!queuedsongs',
'!myqueue',
'!mq'
];
/* A list of command aliases which can be used to show the queue */
var displayAccess = 0x800 | 0x400 | 0x200 | 0x100 | 0x080 | 0x040 | 0x020 | 0x010 | 0x004 | 0x002 | 0x001;
/*
* A bitwise flag representing which users have access to the display command. Account types are
* represented by the following values:
*
* 0x800 = broadcaster
* 0x400 = moderator badge
* 0x200 = founder badge
* 0x100 = vip badge
* 0x080 = artist badge
* 0x040 = tier 3 subscriber badge
* 0x020 = tier 2 subscriber badge
* 0x010 = tier 1 subscriber badge
* 0x004 = cheer badge
* 0x002 = follower
* 0x001 = stranger
*
* Just put a vertical pipe " | " in between each of the values representing levels of access:
*
* ACCESS MEANING
* 0x800 | 0x400 broadcaster and moderator only
* 0x800 | 0x400 | 0x100 | 0x040 | 0x020 broadcasters, mods, VIPs, and tier 2 and 3 subscribers
* 0x800 | 0x010 | 0x002 boradcaster, tier 1 subscribers, and followers
*
* If you know how bitwise flags work, you can also use them in more complicated ways:
* ACCESS MEANING
* 0xFF7 all users from the broadcaster to strangers
* 0xFF7 ^ 0x003 all users except followers and strangers
* 0x0F0 all subscribers
*/
var sTitle = 'Songlist';
/* Title of queue */
var sMessageO = 'Played %SESSION_COUNT% of %SESSION_LIMIT% songs, %REMAIN% of %LIMIT% slots open - type !sr [NAME] to request a song now! Live Learns are %LIVE_LEARNS%.';
/* Message when queue is OPEN */
var sQueueOpen = 'Queue is open';
/* Queue status when OPEN */
var sMessageF = 'Played %SESSION_COUNT% of %SESSION_LIMIT% songs, %REMAIN% of %LIMIT% slots open - wait until the current song is over before requesting! Live Learns are %LIVE_LEARNS%.';
/* Message when queue is FULL */
var sQueueFull = 'Queue is full';
/* Queue status when FULL */
var sMessageC = 'Played %SESSION_COUNT% of %SESSION_LIMIT% songs, no slots open - that\'s all for today!';
/* Message when queue is CLOSED */
var sQueueClosed = 'Queue is closed';
/* Queue status when CLOSED */
var sLLOpen = 'ON';
/* Value of %LIVE_LEARNS% in sMessage(O/F/C) when Live Learns are OPEN */
var sLLClosed = 'OFF';
/* Value of %LIVE_LEARNS% in sMessage(O/F/C) when Live Learns are CLOSED */
var padding = 10;
/* Padding around each cell in the queue's table */
var interval = 2;
/*
* Website synchronization time in seconds -
* while the queue is visible, the system will check for changes regularly
*/
var maxLines = 6;
/* Maximum number of rows to show in the queue */
var displayRate = 10;
/* MS pause per letter while showing the queue */
var hideRate = displayRate;
/* MS pause per letter while hiding the queue */
var noShowFor = 5;
/* Seconds pause after hiding the list before the display command works again */
</script>
<style>
body
{
}
.header
{
font-size: 48px;
font-weight: bold;
text-align: center;
}
.track
{
font-size: 32px;
}
.message
{
margin-left: 10px;
margin-right: 10px;
font-size: 36px;
}
</style>
<!-- DO NOT EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU'RE DOING -->
<style>
:root
{
--number: 0px;
--title: 0px;
--artist: 0px;
--user: 0px;
}
body
{
text-align: left;
margin: 0;
}
table
{
width: 100%;
table-layout:fixed;
margin-top: 1em;
margin-bottom: 1em;
}
td
{
vertical-align: top;
overflow: hidden;
white-space: nowrap;
}
td.number
{
text-align: right;
width: var(--number);
}
td.title
{
width: var(--title);
}
td.artist
{
width: var(--artist);
}
td.user
{
width: var(--user);
}
#measure
{
opacity: 0;
}
.word
{
white-space: nowrap;
}
.letter
{
opacity: 0;
display: inline-block;
position: relative;
animation: 0.5s ease-in 0s 1 forwards paused show;
white-space: pre;
}
@keyframes show
{
0%
{
opacity: 0;
top: 0;
}
50%
{
opacity: 0.5;
top: -0.17em;
}
70%
{
opacity: 0.70;
top: 0em;
}
85%
{
opacity: 0.85;
top: -0.1em;
}
100%
{
opacity: 1;
top: 0;
}
}
button
{
background-color: #7D5BBE;
transition: background 0.12s ease-in, color 0.12s ease-in;
white-space: nowrap;
cursor: pointer;
color: #FFFFFF;
border-radius: 4px;
border: none;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
button:focus, button:hover
{
background-color: #772CE8;
}
button:focus
{
outline: none;
}
button:active
{
background-color: #5C16C5;
}
</style>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Songlist for StreamerSongList</title>
<script>
/* constants */
const gcTime = 500;
const resetTime = 600;
const cURLs = {
twitch: {
ws: 'wss://irc-ws.chat.twitch.tv',
oauth: 'https://id.twitch.tv/oauth2/authorize?client_id=%CLIENT_ID%&redirect_uri=%URL%&response_type=code&scope=%SCOPE%&state=redirto_%ORIGIN%&force_verify=true',
followers: 'https://api.twitch.tv/helix/channels/followers?user_id=%USER_ID%&broadcaster_id=%CHANNEL_ID%'
},
ssl: {
status: 'https://api.streamersonglist.com/v1/streamers/%USER%?platform=twitch',
queue: 'https://api.streamersonglist.com/v1/streamers/%USER%/queue'
},
rr: {
oauth: 'https://realityripple.com/Tools/Twitch/Songlist/oauth.php',
refresh: 'https://realityripple.com/Tools/Twitch/Songlist/oauth.php?refresh=%REFRESH_TOKEN%'
}
};
/* global variables */
var oauth = false;
var ortime = 0;
var tList = [];
var fList = [];
var queueOpen = false;
var llOpen = false;
var queueSize = 0;
var sessionSize = 0;
var sessionCount = 0;
var sessionTime = 0;
var showing = 0;
var lastShown = 0;
var tShowing = false;
var tChecking = false;
var dead = false;
var tIRC = false;
var tL = false;
var expires = 0;
function sleep(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}
function _httpRequest_RSC(x)
{
if (x.readyState < 2)
return null;
if (Math.floor(x.status / 100) !== 2)
{
x.onreadystatechange = null;
return false;
}
if (x.readyState !== 4)
return null;
if (x.responseText === '')
return null;
x.onreadystatechange = null;
if (x.responseText === null)
return false;
return x.responseText;
}
function httpRequest(url, hdrs = {}, nullOn401 = false, nocache = true)
{
const p = new Promise(
function(resolve)
{
const x = new XMLHttpRequest();
if (nocache)
{
if (url.includes('?'))
url+= '&';
else
url+= '?';
url+= 'nocache=' + _rnd(0xFFFFFFFF);
}
x.open('GET', url);
for (const hK in hdrs)
{
if (!hdrs.hasOwnProperty(hK))
continue;
x.setRequestHeader(hK, hdrs[hK]);
}
x.onreadystatechange = function()
{
const r = _httpRequest_RSC(x);
if (r === null)
return;
if (r === false && nullOn401 && x.status === 401)
{
resolve(null);
return;
}
resolve(r);
};
x.send();
}
);
return p;
}
function jSplit(s, sep, limit)
{
let arr = [];
let v = '';
for (let i = 0; i < s.length; i++)
{
if (arr.length < limit - 1)
{
if (s[i] === sep)
{
arr.push(v);
v = '';
continue;
}
}
v += s[i];
}
if (v.length > 0)
arr.push(v);
return arr;
}
function _rnd(m)
{
let r = new Uint32Array(1);
window.crypto.getRandomValues(r);
const f = r[0] / 4294967295;
if (m === undefined)
return f;
if (m < 1)
return f * m;
return Math.floor(f * m);
}
function makeSegments(s)
{
let ret = '<span class="word">';
let a = Array.from(s);
for (let i = 0; i < a.length; i++)
{
if (a[i] === ' ')
ret += '</span>';
let enc = document.createElement('div');
enc.textContent = a[i];
ret += '<span class="letter">' + enc.innerHTML + '</span>';
enc = null;
if (a[i] === ' ')
ret += '<span class="word">';
}
ret += '</span>';
return ret;
}
async function updateUI()
{
if (showing === 1)
return false;
if (showing === 3)
return false;
let tblQueue = document.getElementById('tblQueue');
if (tblQueue === null)
return false;
let txtMessage = document.getElementById('txtMessage');
if (txtMessage === null)
return false;
let txtStatus = document.getElementById('txtStatus');
if (txtStatus === null)
return false;
let didChange = false;
let t = '<tbody>';
let wN = 0;
let wT = 0;
let wA = 0;
let wU = 0;
let qLen = queueSize;
if (qLen === 0)
qLen = tList.length;
if (qLen > maxLines)
qLen = maxLines;
for (let i = 0; i < qLen; i++)
{
const cN = measureText((i + 1));
let cT = 0;
let cA = 0;
let cU = 0;
if (tList.length > i)
{
if (tList[i].title !== false)
cT = Math.ceil(measureText(tList[i].title));
if (tList[i].artist !== false)
cA = Math.ceil(measureText(tList[i].artist));
if (tList[i].user !== false)
cU = Math.ceil(measureText(tList[i].user));
if (wN < cN)
wN = cN;
if (wT < cT)
wT = cT;
if (wA < cA)
wA = cA;
if (wU < cU)
wU = cU;
}
}
const maxW = window.innerWidth;
if (wN > Math.floor(maxW * 0.1))
wN = Math.floor(maxW * 0.1);
if (wA > Math.floor(maxW * 0.2))
wA = Math.floor(maxW * 0.2);
if (wU > Math.floor(maxW * 0.2))
wU = Math.floor(maxW * 0.2);
const x = wN + wA + wU + (padding * 7);
if (wT > (maxW - x))
wT = Math.floor(maxW - x);
document.documentElement.style.setProperty('--number', wN + 'px');
document.documentElement.style.setProperty('--title', wT + 'px');
document.documentElement.style.setProperty('--artist', wA + 'px');
document.documentElement.style.setProperty('--user', wU + 'px');
for (let i = 0; i < qLen; i++)
{
t+= '<tr>';
t+= '<td class="track number">' + makeSegments('' + (i + 1)) + '</td>';
if (tList.length > i)
{
if (tList[i].title === false)
t+= '<td class="track title empty"></td>';
else
t+= '<td class="track title">' + makeSegments(trimText(tList[i].title, wT)) + '</td>';
if (tList[i].artist === false)
t+= '<td class="track artist empty"></td>';
else
t+= '<td class="track artist">' + makeSegments(trimText(tList[i].artist, wA)) + '</td>';
if (tList[i].user === false)
t+= '<td class="track user empty"></td>';
else
t+= '<td class="track user">' + makeSegments(trimText(tList[i].user, wU)) + '</td>';
}
else
t+= '<td class="track empty" colspan="3"></td>';
t+= '</tr>';
}
t+='</tbody>';
if (tblQueue.innerHTML !== t)
didChange = true;
let m = sMessageC;
if (queueOpen)
{
m = sMessageO;
if (queueSize > 0 && tList.length >= queueSize)
m = sMessageF;
}
m = m.replace(/%COUNT%/g, tList.length);
if (queueSize === 0)
{
m = m.replace(/%LIMIT%/g, 'infinite');
m = m.replace(/%REMAIN%/g, 'infinite');
}
else
{
m = m.replace(/%LIMIT%/g, queueSize);
let qR = (queueSize - tList.length);
if (qR < 0)
qR = 0;
m = m.replace(/%REMAIN%/g, qR);
}
m = m.replace(/%SESSION_COUNT%/g, sessionCount);
if (sessionSize === 0)
{
m = m.replace(/%SESSION_LIMIT%/g, 'infinite');
m = m.replace(/%SESSION_REMAIN%/g, 'infinite');
}
else
{
m = m.replace(/%SESSION_LIMIT%/g, sessionSize);
let sR = (sessionSize - sessionCount);
if (sR < 0)
sR = 0;
m = m.replace(/%SESSION_REMAIN%/g, sR);
}
if (llOpen)
m = m.replace(/%LIVE_LEARNS%/g, sLLOpen);
else
m = m.replace(/%LIVE_LEARNS%/g, sLLClosed);
const sM = makeSegments(m);
if (txtMessage.innerHTML !== sM)
didChange = true;
let s = sQueueClosed;
if (queueOpen)
{
s = sQueueOpen;
if (queueSize > 0 && tList.length >= queueSize)
s = sQueueFull;
}
const sS = makeSegments(s);
if (txtStatus.innerHTML !== sS)
didChange = true;
if (didChange)
{
let doShowing = false;
if (showing === 2)
{
doShowing = true;
if (tShowing !== false)
window.clearTimeout(tShowing);
tShowing = false;
if (tChecking !== false)
window.clearInterval(tChecking);
tChecking = false;
await hideTable();
await sleep(gcTime);
}
if (tblQueue.innerHTML !== t)
tblQueue.innerHTML = t;
if (txtMessage.innerHTML !== sM)
txtMessage.innerHTML = sM;
if (txtStatus.innerHTML !== sS)
txtStatus.innerHTML = sS;
if (doShowing)
window.setTimeout(showTable, resetTime);
}
}
async function updateStatus(showStatus)
{
queueOpen = false;
llOpen = false;
queueSize = 0;
sessionSize = 0;
sessionTime = 0;
const r = await httpRequest(cURLs.ssl.status.replaceAll('%USER%', userID.toLowerCase()));
if (r === null || r === false)
return;
const j = JSON.parse(r);
if (j.hasOwnProperty('requestsActive'))
queueOpen = (j.requestsActive === true);
if (j.hasOwnProperty('allowLiveLearns'))
llOpen = (j.allowLiveLearns === true);
if (j.hasOwnProperty('maxRequests'))
sessionSize = j.maxRequests;
if (j.hasOwnProperty('sessionLength'))
sessionTime = j.sessionLength;
if (j.hasOwnProperty('concurrentRequests'))
queueSize = j.concurrentRequests;
await updateTrack(showStatus);
}
async function updateTrack(showStatus)
{
tList = [];
const r = await httpRequest(cURLs.ssl.queue.replaceAll('%USER%', userID.toLowerCase()));
if (r === null || r === false)
return;
parseTrack(r);
updateUI();
if (showStatus)
showTable();
}
function parseTrack(sJSON)
{
const j = JSON.parse(sJSON);
if (j.hasOwnProperty('status') && j.status.hasOwnProperty('songsPlayedToday'))
sessionCount = j.status.songsPlayedToday;
if (!j.hasOwnProperty('list') || !Array.isArray(j.list) || j.list.length < 1)
{
tList = [];
return;
}
for (let i = 0; i < j.list.length; i++)
{
let sTitle = false;
let sArtist = false;
let sUser = false;
if (j.list[i].hasOwnProperty('requests') && Array.isArray(j.list[i].requests) && j.list[i].requests.length > 0)
{
let sUsers = [];
for (let l = 0; l < j.list[i].requests.length; l++)
{
if (!j.list[i].requests[l].hasOwnProperty('name') || j.list[i].requests[l].name === '')
continue;
sUsers.push(j.list[i].requests[l].name);
}
if (sUsers.length === 0)
sUser = false;
else
sUser = sUsers.join(', ');
}
if (j.list[i].hasOwnProperty('nonlistSong') && j.list[i].nonlistSong !== null && j.list[i].nonlistSong !== '')
{
sTitle = j.list[i].nonlistSong;
sArtist = false;
}
else if (j.list[i].hasOwnProperty('song') && j.list[i].song !== null)
{
if (j.list[i].song.hasOwnProperty('title') && j.list[i].song.title !== '')
sTitle = j.list[i].song.title;
else
sTitle = false;
if (j.list[i].song.hasOwnProperty('artist') && j.list[i].song.artist !== '')
sArtist = j.list[i].song.artist;
else
sArtist = false;
}
tList.push({title: sTitle, artist: sArtist, user: sUser});
}
}
async function showTable()
{
if (showing !== 0)
return;
showing = 1;
let letters = document.getElementsByClassName('letter');
let r = false;
for (let i = 0; i < letters.length; i++)
{
let a = letters[i].getAnimations();
if (a.length < 1)
{
r = true;
letters[i].setAttribute('style', 'animation: none;');
letters[i].offsetHeight; /* trigger reflow */
letters[i].removeAttribute('style');
a = letters[i].getAnimations();
if (a.length < 1)
return;
}
}
if (r)
await sleep(resetTime);
for (let i = 0; i < letters.length; i++)
{
let a = letters[i].getAnimations();
if (a.length < 1)
return;
a[0].onremove = null;
a[0].playbackRate = 1;
a[0].play();
await sleep(displayRate);
}
tShowing = window.setTimeout(hideTable, displayTime * 1000);
tChecking = window.setInterval(function() {updateStatus(false); }, interval * 1000);
showing = 2;
}
async function hideTable()
{
showing = 3;
if (tShowing !== false)
window.clearTimeout(tShowing);
tShowing = false;
if (tChecking !== false)
window.clearInterval(tChecking);
tChecking = false;
let letters = document.getElementsByClassName('letter');
for (let i = 0; i < letters.length; i++)
{
let a = letters[i].getAnimations();
a[0].playbackRate = -1;
a[0].play();
await sleep(hideRate);
}
window.setTimeout(resetTable, gcTime);
}
async function resetTable()
{
let letters = document.getElementsByClassName('letter');
for (let i = 0; i < letters.length; i++)
{
let a = letters[i].getAnimations();
if (a.length < 1)
{
letters[i].setAttribute('style', 'animation: none;');
letters[i].offsetHeight; /* trigger reflow */
letters[i].removeAttribute('style');
}
}
await sleep(resetTime);
lastShown = new Date().getTime();
showing = 0;
}
function measureText(s)
{
let t = document.getElementById('measure');
if (t === null)
return 0;
t.style.display = 'inline-block';
t.innerHTML = s;
const w = t.getBoundingClientRect()['width'];
t.style.display = '';
t.innerHTML = '';
return w;
}
function trimText(s, w)
{
if (measureText(s) <= w)
return s;
for (let i = s.length; i >= 0; i--)
{
const x = s.slice(0, i);
if (measureText(x) < w)
return x.slice(0, -3) + '...';
}
return '...';
}
function parseMsg(line)
{
let cmd = {};
if (line.slice(0, 1) === '@')
{
line = line.slice(1);
if (!line.includes(' '))
return false;
cmd.tags = {};
const t = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
const a = t.split(';');
for (let i = 0; i < a.length; i++)
{
const k = a[i].slice(0, a[i].indexOf('='));
const v = a[i].slice(a[i].indexOf('=') + 1).replace(/\\s/g, ' ');
cmd.tags[k] = v;
}
}
if (line.slice(0, 1) === ':')
{
line = line.slice(1);
if (!line.includes(' '))
return false;
cmd.prefix = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
}
if (!line.includes(' '))
{
cmd.command = line;
return cmd;
}
cmd.command = line.slice(0, line.indexOf(' '));
line = line.slice(line.indexOf(' ') + 1);
cmd.params = [];
if (!line.includes(' '))
{
cmd.params.push(line);
return cmd;
}
while (line.includes(' '))
{
if (line.slice(0, 1) === ':')
{
cmd.params.push(line.slice(1));
return cmd;
}
cmd.params.push(line.slice(0, line.indexOf(' ')));
line = line.slice(line.indexOf(' ') + 1);
}
if (line.slice(0, 1) === ':')
line = line.slice(1);
cmd.params.push(line);
return cmd;
}
async function checkFollower(cmd)
{
if (fList.length > 0)
{
for (let i = 0; i < fList.length; i++)
{
if (fList[i].id === cmd.tags['user-id'])
{
const tDif = Math.floor((new Date().getTime() - fList[i].t) / 1000);
if (tDif > 3600)
{
fList.splice(i, 1);
break;
}
return fList[i].value;
}
}
}
const uID = cmd.tags['user-id'];
const chID = cmd.tags['room-id'];
const h = {
'Authorization': 'Bearer ' + oauth,
'Client-Id': clientID
};
const r = await httpRequest(cURLs.twitch.followers.replaceAll('%USER_ID%', uID).replaceAll('%CHANNEL_ID%', chID), h);
if (r === false || r === null)
return false;
const j = JSON.parse(r);
if (j.total > 0)
{
fList.push({id: uID, value: true, t: new Date().getTime()});
return true;
}
fList.push({id: uID, value: false, t: new Date().getTime()});
return false;
}
async function parseLevel(cmd)
{
let level = 0x001;
if (cmd.tags.hasOwnProperty('badges'))
{
const badges = cmd.tags.badges.split(',');
for (let i = 0; i < badges.length; i++)
{
const bData = jSplit(badges[i], '/', 2);
switch (bData[0])
{
case 'broadcaster':
level |= 0x800;
break;
case 'moderator':
level |= 0x400;
break;
case 'vip':
level |= 0x100;
break;
case 'artist-badge':
level |= 0x080;
break;
case 'founder':
level |= 0x200;
break;
case 'bits':
level |= 0x004;
break;
case 'subscriber':
const badge = parseInt(bData[1], 10);
if (badge < 2000)
level |= 0x010;
else if (badge < 3000)
level |= 0x020;
else
level |= 0x040;
break;
}
}
}
/* api-heavy, only check if follower access is allowed and there's a chance it matters */
let needF = false;
if ((displayAccess & 0x002) === 0x002 && (displayAccess & 0x001) !== 0x001)
needF = true;
if (needF)
{
const f = await checkFollower(cmd);
if (f)
level |= 0x002;
}
return level;
}
function irc()
{
const ws = new WebSocket(cURLs.twitch.ws);
ws.onopen = function(event)
{
tIRC = setTimeout(
function()
{
if (dead === true)
return;
dead = true;
if (tIRC !== false)
{
clearTimeout(tIRC);
tIRC = false;
}
ws.close();
blargIAmDead(4);
},
5000);
ws.send('CAP REQ :twitch.tv/commands twitch.tv/tags');
ws.send('PASS 1234');
ws.send('NICK justinfan' + _rnd(0xFFFFFFFF));
ws.send('JOIN #' + userID.toLowerCase());
};
ws.onclose = function()
{
if (tIRC !== false)
{
clearTimeout(tIRC);
tIRC = false;
}
if (!dead)
window.setTimeout(irc, 5000);
};
ws.onmessage = async function(event)
{
const data = event.data.split('\r\n');
for (let i = 0; i < data.length; i++)
{
if (data[i].length === 0)
continue;
const cmd = parseMsg(data[i]);
if (cmd === false)
{
console.log('Unparsed IRC Command: ', data[i]);
continue;
}
switch(cmd.command)
{
case 'PING':
ws.send('PONG ' + cmd.params[0]);
break;
case 'ROOMSTATE':
if (tIRC !== false)
{
clearTimeout(tIRC);
tIRC = false;
}
break;
case 'PRIVMSG':
if (showing !== 0)
break;
if (!displayCmd.includes(cmd.params[1].toLowerCase()))
break;
if (lastShown !== 0 && new Date().getTime() - lastShown < noShowFor * 1000)
break;
const level = await parseLevel(cmd);
if ((level & displayAccess) !== 0)
await updateStatus(true);
break;
case 'NOTICE':
if (cmd.params[1] === 'Login authentication failed')
{
dead = true;
blargIAmDead(1);
}
else
console.log('Unhandled IRC NOTICE: ', cmd);
break;
}
}
};
}
async function _getOAuthToken(t)
{
const url = cURLs.rr.refresh.replaceAll('%REFRESH_TOKEN%', t);
const r = await httpRequest(url, {}, true, false);
if (r === false)
return false;
if (r === null)
{
blargIAmDead(1);
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
return false;
}
const j = JSON.parse(r);
if (!j.hasOwnProperty('access_token'))
return false;
if (!j.hasOwnProperty('refresh_token'))
return false;
return j;
}
async function updateOAuth()
{
const lsOAuth = window.localStorage.getItem(login.path() + '.oauth');
const lsRefresh = window.localStorage.getItem(login.path() + '.refresh');
const lsRefreshed = window.localStorage.getItem(login.path() + '.refreshed');
if (lsOAuth !== null)
oauth = lsOAuth;
if (lsRefresh !== null)
oauthR = lsRefresh;
if (lsRefreshed !== null)
ortime = lsRefreshed;
if (ortime > 0)
{
const tokAge = Math.floor(new Date().getTime() / 1000) - ortime;
if (tokAge < 60 * 60)
return;
}
const ret = await _getOAuthToken(oauthR);
if (ret === false)
return;
oauth = ret.access_token;
oauthR = ret.refresh_token;
ortime = Math.floor(new Date().getTime() / 1000);
window.localStorage.setItem(login.path() + '.oauth', oauth);
window.localStorage.setItem(login.path() + '.refresh', oauthR);
window.localStorage.setItem(login.path() + '.refreshed', ortime);
}
const login = function()
{
let _tL = false;
const _visTime = 5000;
function _activeScope()
{
let r = [];
r.push('moderator:read:followers');
return r;
}
function _uScope()
{
return encodeURIComponent(_activeScope().join(' '));
}
function lsPath()
{
return 'twitch.' + _activeScope().join('+');
}
function shouldUseLogin()
{
login.inUse = false;
if (userID !== false && userID !== 'CHANNEL_NAME' && oauthR !== false && oauthR !== 'OAUTH_REFRESH')
return false;
login.inUse = true;
let lsChannel = window.localStorage.getItem(login.path() + '.channel');
let lsClient = window.localStorage.getItem(login.path() + '.client');
let lsRefresh = window.localStorage.getItem(login.path() + '.refresh');
if (lsChannel === null || lsRefresh === null)
{
const h = _getHashParams();
if (!h.hasOwnProperty('channel') || !h.hasOwnProperty('client') || !h.hasOwnProperty('oauth_refresh'))
{
login.showIn();
return true;
}
lsChannel = h.channel;
lsClient = h.client;
lsRefresh = h.oauth_refresh;
window.localStorage.setItem(login.path() + '.channel', lsChannel);
window.localStorage.setItem(login.path() + '.refresh', lsRefresh);
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.setItem(login.path() + '.client', lsClient);
}
userID = lsChannel;
oauthR = lsRefresh;
clientID = lsClient;
document.title = userID + ' Songlist for StreamerSongList';
showLogoutButton();
return false;
}
function _getHashParams()
{
const d = function(s) {
const a = /\+/g;
return decodeURIComponent(s.replace(a, ' '));
};
let hashParams = {};
const r = /([^&;=]+)=?([^&;]*)/g;
const q = window.location.hash.substring(1);
let e;
while ((e = r.exec(q)) !== null)
{
hashParams[d(e[1])] = d(e[2]);
}
return hashParams;
}
function doLogin()
{
window.localStorage.removeItem(login.path() + '.channel');
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.removeItem(login.path() + '.client');
const o = encodeURIComponent(btoa(window.location));
const c = encodeURIComponent('qkqa7k3ekuqbm5wxpekawtwv3xkmep');
const r = encodeURIComponent(cURLs.rr.oauth);
const u = cURLs.twitch.oauth.replaceAll('%CLIENT_ID%', c).replaceAll('%URL%', r).replaceAll('%SCOPE%', _uScope()).replaceAll('%ORIGIN%', o);
window.location = u;
}
function showLoginButton()
{
document.title = 'Log In to Access Songlist for StreamerSongList';
if (document.getElementById('cmdLogout'))
document.body.removeChild(document.getElementById('cmdLogout'));
window.localStorage.removeItem(login.path() + '.channel');
window.localStorage.removeItem(login.path() + '.oauth');
window.localStorage.removeItem(login.path() + '.refresh');
window.localStorage.removeItem(login.path() + '.refreshed');
window.localStorage.removeItem(login.path() + '.client');
let cmdLogin = document.createElement('button');
cmdLogin.setAttribute('id', 'cmdLogin');
cmdLogin.setAttribute('type', 'button');
cmdLogin.setAttribute('onclick', 'login.begin();');
let sStyle = 'z-index: 1000;';
sStyle += ' position: absolute;';
sStyle += ' top: 45%;';
sStyle += ' left: calc(50% - 6.5em);';
sStyle += ' width: 13em;';
sStyle += ' font-size: 3vw;';
sStyle += ' padding: 0.5em;';
cmdLogin.setAttribute('style', sStyle);
cmdLogin.innerHTML = 'Authenticate Songlist';
document.body.appendChild(cmdLogin);
}
function showLogoutButton(v = false)
{
if (document.getElementById('cmdLogout'))
document.body.removeChild(document.getElementById('cmdLogout'));
let cmdLogout = document.createElement('button');
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
cmdLogout.setAttribute('id', 'cmdLogout');
cmdLogout.setAttribute('type', 'button');
let sStyle = 'z-index: 1000;';
sStyle += ' transition: opacity 0.5s;';
sStyle += ' position: absolute;';
sStyle += ' top: 1em;';
sStyle += ' right: 1em;';
sStyle += ' width: 5em;';
sStyle += ' font-size: 1vw;';
sStyle += ' padding: 0.5em;';
document.addEventListener('mouseover', _fadeInLogout);
if (v)
{
cmdLogout.setAttribute('onclick', 'login.begin();');
cmdLogout.innerHTML = 'Re-Auth';
sStyle += ' opacity: 1;';
_tL = window.setTimeout(_fadeOutLogout, _visTime);
}
else
{
cmdLogout.setAttribute('onclick', 'login.showIn();');
cmdLogout.innerHTML = 'Log Out';
sStyle += ' opacity: 0;';
}
cmdLogout.setAttribute('style', sStyle);
document.body.appendChild(cmdLogout);
}
function _fadeInLogout()
{
if (document.getElementById('cmdLogout'))
document.getElementById('cmdLogout').style.opacity = '1';
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
_tL = window.setTimeout(_fadeOutLogout, _visTime);
}
function _fadeOutLogout()
{
if (_tL !== false)
{
window.clearTimeout(_tL);
_tL = false;
}
if (document.getElementById('cmdLogout'))
document.getElementById('cmdLogout').style.opacity = '0';
}
return {
inUse: false,
use: shouldUseLogin,
begin: doLogin,
showIn: showLoginButton,
showOut: showLogoutButton,
path: lsPath
};
}();
function blargIAmDead(e)
{
let showButton = false;
switch (e)
{
case 1:
if (login.inUse)
{
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">Songlist Error:<br><br>Unable to Connect to Twitch<br><br>Please Log In Again</div>';
showButton = true;
login.showOut(true);
}
else
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">Songlist Error:<br><br>Unable to Connect to Twitch<br><br>Please Update Your OAuth Refresh Token</div>';
break;
case 4:
document.body.innerHTML = '<div style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(255, 0, 0, 0.75); color: #FFFF00; text-shadow: 2px 2px 4px #000000; font-size: 300%; font-weight: bold; font-family: sans-serif; text-align: center; padding-top: 3em;">Songlist Error:<br><br>The connection to the IRC channel was incomplete.<br><br>Please check your Channel</div>';
break;
}
window.setTimeout(function(){document.body.innerHTML = ''; if (showButton) showLoginButton();}, 15000);
}
function initUI()
{
let txtTitle = document.getElementById('txtTitle');
let tblQueue = document.getElementById('tblQueue');
const s = makeSegments(sTitle);
txtTitle.innerHTML = s;
tblQueue.setAttribute('style', 'border-spacing: ' + padding + 'px;');
let m = document.getElementsByClassName('message');
for (let i = 0; i < m.length; i++)
{
m[i].setAttribute('style', 'margin-left: ' + padding + 'px; margin-right: ' + padding + 'px;');
}
}
async function init()
{
initUI();
if (login.use() === true)
return;
if (oauthR !== false && oauthR !== 'OAUTH_REFRESH')
await updateOAuth();
await updateStatus(false);
irc();
}
window.addEventListener('load', init);
</script>
</head>
<body>
<div class="header" id="txtTitle">QUEUE</div>
<table id="tblQueue"></table>
<div class="message" id="txtMessage"></div>
<div class="message" id="txtStatus"></div>
<div id="measure" class="track"></div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment