Skip to content

Instantly share code, notes, and snippets.

@RealityRipple
Created September 5, 2022 07:14
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/1bb62ab3ff978e33a833a8c1ab54acbe to your computer and use it in GitHub Desktop.
Save RealityRipple/1bb62ab3ff978e33a833a8c1ab54acbe to your computer and use it in GitHub Desktop.
Titlebot for StreamElements
<!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