Skip to content

Instantly share code, notes, and snippets.

@RealityRipple
Created September 5, 2022 07: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/3818154c11360355dbdff6657cb03872 to your computer and use it in GitHub Desktop.
Save RealityRipple/3818154c11360355dbdff6657cb03872 to your computer and use it in GitHub Desktop.
Titlebot for Streamer Song List
<!doctype html>
<html>
<head>
<script>
/******************************************************************************************************************\
* *
* Titlebot for Streamer Song List v1.0 *
* by RealityRipple *
* *
********************************************************************************************************************
* *
* Titlebot automatically changes the title of your stream based on the current track in Streamer Song List. *
* There are three variables you can enter into your stream title: *
* %TRACK% will become the name of the currently playing media in Streamer Song List. *
* %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 *
* %LIVE_LEARNS% will be replaced with a message depending on the live-learn requests state: *
* If Open, %LIVE_LEARNS% will be replaced with sLiveLearnO *
* If Closed, %LIVE_LEARNS% will be replaced with sLiveLearnC *
* *
* Example: *
* Stream Title of "Let's Jam to %TRACK% - Requests are %QUEUE% - Live Learns are %LIVE_LEARNS%" *
* sTitleE = 'Some Tunes'; *
* sTrack = '%DISP_TITLE%%DISP_ARTIST%'; *
* artistDisp = ' by %ARTIST%'; *
* titleDisp = '%TITLE%'; *
* sQueueO = 'Open'; *
* sQueueF = 'Full'; *
* sQueueC = 'Closed'; *
* sLiveLearnO = 'on'; *
* sLiveLearnC = 'off'; *
* *
* Results: *
* No Track, Open Queue, Live Learns Off: *
* "Let's Jam to Some Tunes - Requests are Open - Live Learns are off" *
* ---------- ---- --- *
* Full Queue, Live Learns On: *
* "Let's Jam to Wish You Were Here by Pink Floyd - Requests are Full - Live Learns are on" *
* -------------------------------- ---- -- *
* *
* 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 *
* *
* 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_ARTIST% and %DISP_TITLE% variables. *
* artistDisp The artist display. This will fill in the %DISP_ARTIST% variable in sTrack. *
* titleDisp The video title display. This will fill in the %DISP_TITLE% variable in sTrack. *
* Example Track Displays: *
* *
* Artist, then Title *
* trackDisp = '%DISP_ARTIST%%DISP_TITLE%' *
* artistDisp = '%ARTIST% - ' *
* titleDisp = '%TITLE%' *
* => Pink Floyd - Wish You Were Here *
* *
* Title, then Artist: *
* trackDisp = '%DISP_TITLE%%DISP_ARTIST%' *
* artistDisp = ' by %ARTIST%' *
* titleDisp = '%TITLE%' *
* => Wish You Were Here by Pink Floyd *
* *
* No Artist, just Title: *
* trackDisp = '%DISP_TITLE%' *
* artistDisp = ''; *
* titleDisp = '%TITLE%'; *
* => Wish You Were Here *
* *
* If there is no listed artist for a track (such as a Live Learn), the variable %DISP_ARTIST% will be replaced *
* with empty text, ''. This can help hide any separators such as " - " or " by " when there's no artist: *
* trackDisp = '%DISP_TITLE%%DISP_ARTIST%' *
* artistDisp = ' by %ARTIST%' *
* 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. *
* *
* sLiveLearnO If live learns are open, %LIVE_LEARNS% will be replaced with this value. *
* sLiveLearnC If live learns are closed, %LIVE_LEARNS% will be replaced with this value. *
* *
********************************************************************************************************************
* *
* Help: Please contact @realityripple for assistance, bug reports, or questions. *
* *
\******************************************************************************************************************/
var userName = 'CHANNEL_NAME';
var clientID = 'eqacdw82ajo14x2y0nbhc0v2zcx8zf';
var oauth = 'OAUTH_ID'; /* channel:manage:broadcast */
var sTitleE = 'Nothing';
var sTrack = '%DISP_ARTIST%%DISP_TITLE%';
var artistDisp = '%ARTIST% - ';
var titleDisp = '%TITLE%';
var sQueueO = ' ~ !sr';
var sQueueF = ' [Queue is Full]';
var sQueueC = '';
var sLiveLearnO = ' ~ !ll';
var sLiveLearnC = '';
</script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Titlebot for StreamerSongList</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 tList = [];
var queueOpen = false;
var llOpen = false;
var queueSize = 0;
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.streamersonglist.com/v1/streamers/' + userName.toLowerCase() +'?platform=twitch', 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, ll: null, size: null};
if (j.hasOwnProperty('requestsActive'))
r.open = (j.requestsActive === true);
if (j.hasOwnProperty('allowLiveLearns'))
r.ll = (j.allowLiveLearns === true);
if (j.hasOwnProperty('concurrentRequests'))
r.size = j.concurrentRequests;
resolve(r);
};
x.send(null);
}
);
return p;
}
function getTracks()
{
let p = new Promise(
(resolve, reject) =>
{
let x = new XMLHttpRequest();
x.open('GET', 'https://api.streamersonglist.com/v1/streamers/' + userName.toLowerCase() +'/queue', 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 ret = [];
if (!j.hasOwnProperty('list') || !Array.isArray(j.list) || j.list.length < 1)
{
resolve(ret);
return;
}
for (let i = 0; i < j.list.length; i++)
{
if (j.list[i].hasOwnProperty('nonlistSong') && j.list[i].nonlistSong !== null && j.list[i].nonlistSong !== '')
{
ret.push({title: j.list[i].nonlistSong, artist: false});
continue;
}
if (!j.list[i].hasOwnProperty('song'))
continue;
if (j.list[i].song === null)
continue;
if (!j.list[i].song.hasOwnProperty('title'))
continue;
if (j.list[i].song.title === '')
continue;
let sTitle = j.list[i].song.title;
let sArtist = false;
if (j.list[i].song.hasOwnProperty('artist') && j.list[i].song.artist !== '')
sArtist = j.list[i].song.artist;
ret.push({title: sTitle, artist: sArtist});
}
resolve(ret);
};
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 (tList.length >= queueSize)
q = sQueueF;
else
q = sQueueO;
}
let l = sLiveLearnC;
if (llOpen)
l = sLiveLearnO;
let tr = '';
let t = '';
if (tList.length === 0 || tList[0].title === false)
tr = sTitleE;
else
{
let dTitle = titleDisp.replace('%TITLE%', tList[0].title);
let dArtist = '';
if (artistDisp !== false && tList[0].artist !== false)
dArtist = artistDisp.replace('%ARTIST%', tList[0].artist);
tr = sTrack.replace('%DISP_TITLE%', dTitle).replace('%DISP_ARTIST%', dArtist);
}
t = sTitleT.replace(/%TRACK%/g, tr).replace(/%QUEUE%/g, q).replace(/%LIVE_LEARNS%/g, l);
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 = tList.length >= queueSize;
let changed = false;
let newT = await getChannelTitle();
if (newT !== false)
{
if (newT !== sTitleT)
{
tList = [];
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.ll !== null)
{
if (reqR.ll !== llOpen)
{
llOpen = reqR.ll;
changed = true;
}
}
if (reqR.size !== null)
{
if (reqR.size !== queueSize)
queueSize = reqR.size;
}
let trR = await getTracks();
let oT = false;
let oA = false;
let nT = false;
let nA = false;
if (tList.length > 0)
{
oT = tList[0].title;
oA = tList[0].artist;
}
if (trR.length > 0)
{
nT = trR[0].title;
nA = trR[0].artist;
}
if (oT !== nT)
changed = true;
if (oA !== nA)
changed = true;
tList = trR;
if ((tList.length >= 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 Streamer Song List';
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