Last active
April 17, 2023 03:15
-
-
Save RealityRipple/921e7e02849c280ee6f6adbd92312aff to your computer and use it in GitHub Desktop.
RealityRipple's Homemade Songlist for Streamer Song List
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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