Created
September 5, 2022 07:14
-
-
Save RealityRipple/1bb62ab3ff978e33a833a8c1ab54acbe to your computer and use it in GitHub Desktop.
Titlebot for StreamElements
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> | |
/******************************************************************************************************************\ | |
* * | |
* Titlebot for StreamElements v1.0 * | |
* by RealityRipple * | |
* * | |
******************************************************************************************************************** | |
* * | |
* Titlebot automatically changes the title of your stream based on the current track in StreamElements. * | |
* There are two variables you can enter into your stream title: * | |
* %TRACK% will become the name of the currently playing media in StreamElements. * | |
* %QUEUE% will be replaced with a message depending on the queue state: * | |
* If Open, %QUEUE% will be replaced with sQueueO * | |
* If Full, %QUEUE% will be replaced with sQueueF * | |
* If Closed, %QUEUE% will be replaced with sQueueC * | |
* * | |
* Example: * | |
* Stream Title of "Let's Jam to %TRACK% - Requests are %QUEUE%" * | |
* sTitleE = 'Some Tunes'; * | |
* sTrack = '%DISP_TITLE%%DISP_CHANNEL%'; * | |
* channelDisp = ' by %CHANNEL%'; * | |
* titleDisp = '%TITLE%'; * | |
* sQueueO = 'Open'; * | |
* sQueueF = 'Full'; * | |
* sQueueC = 'Closed'; * | |
* * | |
* Results: * | |
* No Track, Open Queue: * | |
* "Let's Jam to Some Tunes - Requests are Open" * | |
* ---------- ---- * | |
* Full Queue: * | |
* "Let's Jam to Wish You Were Here by Pink Floyd - Requests are Full" * | |
* -------------------------------- ---- * | |
* * | |
* If you set a variable equal to '', it will not show up. You can use this to show commands only sometimes: * | |
* Example: * | |
* Stream Title of "%TRACK% Vibes%QUEUE%" * | |
* sQueueO = ' ~ !sr'; * | |
* sQueueF = ' [Queue is Full]'; * | |
* sQueueC = ''; * | |
* * | |
* Results: * | |
* Open Queue: "Wish You Were Here by Pink Floyd Vibes ~ !sr" * | |
* -------------------------------- ------ * | |
* Full Queue: "Wish You Were Here by Pink Floyd Vibes [Queue is Full]" * | |
* -------------------------------- ---------------- * | |
* Closed Queue: "Wish You Were Here by Pink Floyd Vibes" * | |
* -------------------------------- (empty) * | |
* Note the space in sQueueO and sQueueF makes sure a space is added only in those cases, where sQueueC is empty. * | |
* This is to ensure a trailing space is not added to the title after the word "Vibes" if it's not needed. * | |
* * | |
******************************************************************************************************************** | |
* * | |
* VARIABLES * | |
* * | |
* userID Your StreamElements Account ID, found at <https://streamelements.com/dashboard/account/channels>. * | |
* userName Your Twitch Channel Name, optional. Leave as 'CHANNEL_NAME' or set to false to use interact login. * | |
* clientID Titlebot's Client ID. Do not change unless you're using your own oauth generator. * | |
* oauth Your Twitch OAuth2 ID. Leave as 'OAUTH_ID' or set to false to use interact login. The required * | |
* access for Titlebot is "channel:manage:broadcast". * | |
* * | |
* sTitleE If there is no current track, %TRACK% will be replaced with this value. * | |
* sTrack The display of the current song. Use %DISP_CHANNEL% and %DISP_TITLE% variables. * | |
* channelDisp The channel name display. This will fill in the %DISP_CHANNEL% variable in sTrack. * | |
* titleDisp The video title display. This will fill in the %DISP_TITLE% variable in sTrack. * | |
* Example Track Displays: * | |
* * | |
* YouTube Channel, then Title * | |
* trackDisp = '%DISP_CHANNEL%%DISP_TITLE%' * | |
* channelDisp = '%CHANNEL% - ' * | |
* titleDisp = '%TITLE%' * | |
* => Pink Floyd - Wish You Were Here * | |
* * | |
* Title, then YouTube Channel: * | |
* trackDisp = '%DISP_TITLE%%DISP_CHANNEL%' * | |
* channelDisp = ' by %CHANNEL%' * | |
* titleDisp = '%TITLE%' * | |
* => Wish You Were Here by Pink Floyd * | |
* * | |
* No YouTube Channel, just Title: * | |
* trackDisp = '%DISP_TITLE%' * | |
* channelDisp = ''; * | |
* titleDisp = '%TITLE%'; * | |
* => Wish You Were Here * | |
* * | |
* sQueueO If the queue is open, %QUEUE% will be replaced with this value. * | |
* sQueueF If the queue is full, %QUEUE% will be replaced with this value. * | |
* sQueueC If the queue is closed, %QUEUE% will be replaced with this value. * | |
* * | |
******************************************************************************************************************** | |
* * | |
* Help: Please contact @realityripple for assistance, bug reports, or questions. * | |
* * | |
\******************************************************************************************************************/ | |
var userID = 'your_account'; | |
var userName = 'CHANNEL_NAME'; | |
var clientID = 'eqacdw82ajo14x2y0nbhc0v2zcx8zf'; | |
var oauth = 'OAUTH_ID'; /* channel:manage:broadcast */ | |
var sTitleE = 'Nothing'; | |
var sTrack = '%DISP_CHANNEL%%DISP_TITLE%'; | |
var channelDisp = '%CHANNEL% - '; | |
var titleDisp = '%TITLE%'; | |
var sQueueO = ' ~ !sr'; | |
var sQueueF = ' [Queue is Full]'; | |
var sQueueC = ''; | |
</script> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<title>Titlebot for StreamElements</title> | |
<style> | |
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> | |
<script> | |
/* global variables */ | |
var interval = 5; | |
var sTitleT = ''; | |
var tCount = 0; | |
var queueOpen = false; | |
var queueSize = 0; | |
var sTitle = false; | |
var sChannel = false; | |
var uID = -1; | |
var useL = false; | |
var tL = false; | |
function getRequestInfo() | |
{ | |
let p = new Promise( | |
(resolve, reject) => | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('GET', 'https://api.streamelements.com/kappa/v2/songrequest/' + userID +'/settings/public', true); | |
x.onreadystatechange = function() | |
{ | |
if (x.readyState !== 4) | |
return; | |
if (x.status !== 200 && x.status !== 0) | |
return; | |
if (x.responseText === '') | |
return; | |
const j = JSON.parse(x.responseText); | |
let r = {open: null, size: null}; | |
if (j.hasOwnProperty('enabled')) | |
r.open = (j.enabled === true); | |
if (j.hasOwnProperty('limits') && j.limits.hasOwnProperty('queueLimit')) | |
r.size = j.limits.queueLimit; | |
resolve(r); | |
}; | |
x.send(null); | |
} | |
); | |
return p; | |
} | |
function cleanTitle(s) | |
{ | |
let enclosedEnd = /[([][^()[\]]+ (video|lyrics|remaster)[)\]]$/gi; | |
let enclosedMatch = /[([](video|lyrics|hd|full|720p|1080p|remastered|high quality)[)\]]$/gi; | |
let paddedEnd = /[~-]+[^-~]+(lyrics|video)$/gi; | |
let paddedMatch = /[~-]+ *(lyrics|video|hd|full|720p|1080p|high quality)$/gi; | |
while (s.match(enclosedEnd) || | |
s.match(enclosedMatch) || | |
s.match(paddedEnd) || | |
s.match(paddedMatch)) | |
{ | |
s = s.replace(enclosedEnd, '').trimEnd(); | |
s = s.replace(enclosedMatch, '').trimEnd(); | |
s = s.replace(paddedEnd, '').trimEnd(); | |
s = s.replace(paddedMatch, '').trimEnd(); | |
} | |
let rawEnd = / (lyrics|1080p|720p)$/gi; | |
while (s.match(rawEnd)) | |
s = s.replace(rawEnd, '').trimEnd(); | |
return s; | |
} | |
function cleanChannel(s) | |
{ | |
while (s.slice(-8) === ' - Topic') | |
s = s.slice(0, -8); | |
return s; | |
} | |
function getTrack() | |
{ | |
let p = new Promise( | |
(resolve, reject) => | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('GET', 'https://api.streamelements.com/kappa/v2/songrequest/' + userID +'/playing', true); | |
x.onreadystatechange = function() | |
{ | |
if(x.readyState !== 4) | |
return; | |
if (x.status !== 200 && x.status !== 0) | |
return; | |
if (x.responseText === '') | |
return; | |
let r = {title: false, channel: false}; | |
if (x.responseText === 'null') | |
{ | |
resolve(r); | |
return; | |
} | |
const j = JSON.parse(x.responseText); | |
if (j.hasOwnProperty('title')) | |
r.title = cleanTitle(j.title); | |
if (j.hasOwnProperty('channel')) | |
r.channel = cleanChannel(j.channel); | |
resolve(r); | |
}; | |
x.send(null); | |
} | |
); | |
return p; | |
} | |
function getQueueSize() | |
{ | |
let p = new Promise( | |
(resolve, reject) => | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('GET', 'https://api.streamelements.com/kappa/v2/songrequest/' + userID +'/queue/public', true); | |
x.onreadystatechange = function() | |
{ | |
if(x.readyState !== 4) | |
return; | |
if (x.status !== 200 && x.status !== 0) | |
return; | |
if (x.responseText === '') | |
return; | |
const j = JSON.parse(x.responseText); | |
if (!Array.isArray(j)) | |
{ | |
resolve(0); | |
return; | |
} | |
resolve(j.length); | |
}; | |
x.send(null); | |
} | |
); | |
return p; | |
} | |
function getUID() | |
{ | |
let p = new Promise( | |
(resolve, reject) => | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('GET', 'https://api.twitch.tv/helix/users?login=' + userName.toLowerCase()); | |
x.setRequestHeader('Authorization', 'Bearer ' + oauth); | |
x.setRequestHeader('Client-Id', clientID); | |
x.onreadystatechange = async function() | |
{ | |
if (x.readyState !== 4) | |
return; | |
if (x.status !== 200 && x.status !== 0) | |
return; | |
if (x.responseText === '') | |
return; | |
const j = JSON.parse(x.responseText); | |
if (!j.hasOwnProperty('data')) | |
{ | |
resolve(false); | |
return; | |
} | |
if (!j.data[0].hasOwnProperty('id')) | |
{ | |
resolve(false); | |
return; | |
} | |
uID = j.data[0].id; | |
resolve(true); | |
}; | |
x.send(); | |
} | |
); | |
return p; | |
} | |
async function updateInfo() | |
{ | |
let q = sQueueC; | |
if (queueOpen) | |
{ | |
if (tCount >= queueSize) | |
q = sQueueF; | |
else | |
q = sQueueO; | |
} | |
let tr = ''; | |
let t = ''; | |
if (sTitle === false) | |
tr = sTitleE; | |
else | |
{ | |
let dTitle = titleDisp.replace('%TITLE%', sTitle); | |
let dChannel = ''; | |
if (channelDisp !== false && sChannel !== false) | |
dChannel = channelDisp.replace('%CHANNEL%', sChannel); | |
tr = sTrack.replace('%DISP_TITLE%', dTitle).replace('%DISP_CHANNEL%', dChannel); | |
} | |
t = sTitleT.replace(/%TRACK%/g, tr).replace(/%QUEUE%/g, q); | |
setTitle(t); | |
} | |
function getChannelTitle() | |
{ | |
let p = new Promise( | |
(resolve, reject) => | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('GET', 'https://api.twitch.tv/helix/channels?broadcaster_id=' + uID); | |
x.setRequestHeader('Authorization', 'Bearer ' + oauth); | |
x.setRequestHeader('Client-Id', clientID); | |
x.onreadystatechange = function() | |
{ | |
if(x.readyState !== 4) | |
return; | |
if (x.status !== 200 && x.status !== 0) | |
return; | |
if (x.responseText === '') | |
return; | |
const j = JSON.parse(x.responseText); | |
if (!j.hasOwnProperty('data')) | |
{ | |
resolve(false); | |
return; | |
} | |
if (j.data.length !== 1) | |
{ | |
resolve(false); | |
return; | |
} | |
if (!j.data[0].hasOwnProperty('title')) | |
{ | |
resolve(false); | |
return; | |
} | |
if (!j.data[0].title.includes('%TRACK%')) | |
{ | |
resolve(false); | |
return; | |
} | |
resolve(j.data[0].title); | |
}; | |
x.send(null); | |
} | |
); | |
return p; | |
} | |
function setTitle(title) | |
{ | |
let x = new XMLHttpRequest(); | |
x.open('PATCH', 'https://api.twitch.tv/helix/channels?broadcaster_id=' + uID); | |
x.setRequestHeader('Authorization', 'Bearer ' + oauth); | |
x.setRequestHeader('Client-Id', clientID); | |
x.setRequestHeader('Content-Type', 'application/json'); | |
let d = {}; | |
d.title = title; | |
let s = JSON.stringify(d); | |
x.send(s); | |
} | |
async function doUpdate() | |
{ | |
if (uID === -1) | |
return; | |
let wasFull = tCount >= queueSize; | |
let changed = false; | |
let newT = await getChannelTitle(); | |
if (newT !== false) | |
{ | |
if (newT !== sTitleT) | |
{ | |
sTitle = false; | |
sChannel = false; | |
sTitleT = newT; | |
changed = true; | |
} | |
} | |
if (newT === false && sTitleT === '') | |
return; | |
let reqR = await getRequestInfo(); | |
if (reqR.open !== null) | |
{ | |
if (reqR.open !== queueOpen) | |
{ | |
queueOpen = reqR.open; | |
changed = true; | |
} | |
} | |
if (reqR.size !== null) | |
{ | |
if (reqR.size !== queueSize) | |
queueSize = reqR.size; | |
} | |
let trR = await getTrack(); | |
if (trR.title !== sTitle) | |
{ | |
sTitle = trR.title; | |
changed = true; | |
} | |
if (trR.channel !== sChannel) | |
{ | |
sChannel = trR.channel; | |
changed = true; | |
} | |
let qR = await getQueueSize(); | |
if (qR !== tCount) | |
tCount = qR; | |
if ((tCount >= queueSize) !== wasFull) | |
changed = true; | |
if (!changed) | |
return; | |
if (sTitleT === '') | |
return; | |
updateInfo(); | |
} | |
function shouldUseLogin() | |
{ | |
useL = false; | |
if (userName !== false && userName !== 'CHANNEL_NAME' && oauth !== false && oauth !== 'OAUTH_ID') | |
return false; | |
useL = true; | |
let lsChannel = window.localStorage.getItem('twitch.channel:manage:broadcast.channel'); | |
let lsClient = window.localStorage.getItem('twitch.channel:manage:broadcast.client'); | |
let lsOAuth = window.localStorage.getItem('twitch.channel:manage:broadcast.oauth'); | |
if (lsChannel === null || lsOAuth === null) | |
{ | |
let h = getHashParams(); | |
if (!h.hasOwnProperty('channel') || !h.hasOwnProperty('client') || !h.hasOwnProperty('oauth')) | |
{ | |
showLoginButton(); | |
return true; | |
} | |
lsChannel = h.channel; | |
lsClient = h.client; | |
lsOAuth = h.oauth; | |
window.localStorage.setItem('twitch.channel:manage:broadcast.channel', lsChannel); | |
window.localStorage.setItem('twitch.channel:manage:broadcast.oauth', lsOAuth); | |
window.localStorage.setItem('twitch.channel:manage:broadcast.client', lsClient); | |
} | |
userName = lsChannel; | |
oauth = lsOAuth; | |
clientID = lsClient; | |
document.title = userName + ' Titlebot for StreamElements'; | |
showLogoutButton(); | |
return false; | |
} | |
function getHashParams() | |
{ | |
let d = function(s) { | |
let a = /\+/g; | |
return decodeURIComponent(s.replace(a, " ")); | |
}; | |
let hashParams = {}; | |
let r = /([^&;=]+)=?([^&;]*)/g; | |
let q = window.location.hash.substring(1); | |
let e; | |
while ((e = r.exec(q))) | |
{ | |
hashParams[d(e[1])] = d(e[2]); | |
} | |
return hashParams; | |
} | |
function doLogin() | |
{ | |
let o = encodeURIComponent(btoa(window.location)); | |
let u = 'https://id.twitch.tv/oauth2/authorize?client_id=' + encodeURIComponent('eqacdw82ajo14x2y0nbhc0v2zcx8zf') + '&redirect_uri=' + encodeURIComponent('https://realityripple.com/Tools/Twitch/Titlebot/oauth.php') + '&response_type=token&scope=' + encodeURIComponent('channel:manage:broadcast') + '&state=redirto_' + o + '&force_verify=true'; | |
window.location = u; | |
} | |
function showLoginButton() | |
{ | |
document.title = 'Log In to Access Titlebot'; | |
if (document.getElementById('cmdLogout')) | |
document.body.removeChild(document.getElementById('cmdLogout')); | |
uID = -1; | |
window.localStorage.removeItem('twitch.channel:manage:broadcast.channel'); | |
window.localStorage.removeItem('twitch.channel:manage:broadcast.oauth'); | |
window.localStorage.removeItem('twitch.channel:manage:broadcast.client'); | |
let cmdLogin = document.createElement('button'); | |
cmdLogin.setAttribute('id', 'cmdLogin'); | |
cmdLogin.setAttribute('type', 'button'); | |
cmdLogin.setAttribute('onclick', 'doLogin();'); | |
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 Titlebot'; | |
document.body.appendChild(cmdLogin); | |
} | |
function showLogoutButton(v = false) | |
{ | |
let visTime = 5000; | |
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', | |
function() | |
{ | |
cmdLogout.style.opacity = '1'; | |
if (tL !== false) | |
{ | |
window.clearTimeout(tL); | |
tL = false; | |
} | |
tL = window.setTimeout( | |
function() | |
{ | |
if (tL !== false) | |
{ | |
window.clearTimeout(tL); | |
tL = false; | |
} | |
cmdLogout.style.opacity = '0'; | |
}, | |
visTime | |
); | |
} | |
); | |
if (v) | |
{ | |
cmdLogout.setAttribute('onclick', 'doLogin();'); | |
cmdLogout.innerHTML = 'Re-Auth'; | |
sStyle += ' opacity: 1;'; | |
tL = window.setTimeout( | |
function() | |
{ | |
if (tL !== false) | |
{ | |
window.clearTimeout(tL); | |
tL = false; | |
} | |
cmdLogout.style.opacity = '0'; | |
}, | |
visTime | |
); | |
} | |
else | |
{ | |
cmdLogout.setAttribute('onclick', 'showLoginButton();'); | |
cmdLogout.innerHTML = 'Log Out'; | |
sStyle += ' opacity: 0;'; | |
} | |
cmdLogout.setAttribute('style', sStyle); | |
document.body.appendChild(cmdLogout); | |
} | |
async function init() | |
{ | |
if (shouldUseLogin() === true) | |
return; | |
await getUID(); | |
window.setInterval(function() {doUpdate(); }, interval * 1000); | |
} | |
window.addEventListener('load', init); | |
</script> | |
</head> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment