Created
September 5, 2022 07:15
-
-
Save RealityRipple/3818154c11360355dbdff6657cb03872 to your computer and use it in GitHub Desktop.
Titlebot 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> | |
/******************************************************************************************************************\ | |
* * | |
* 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