Re-release of Multistream Title Updater

Minor Printer.bot improvements
This commit is contained in:
nuttylmao
2025-09-13 21:12:33 +10:00
parent d95462f65a
commit c5e9890eb2
36 changed files with 1389 additions and 593 deletions
+3 -3
View File
@@ -16,9 +16,9 @@
<img src="../../resources/logo.png" style="height: 40px;" /> <img src="../../resources/logo.png" style="height: 40px;" />
<label id="title" class="title"></label> <label id="title" class="title"></label>
<div id="header-end"> <div id="header-end">
<button id="sb-actions-button" onclick="OpenRequiredActionsDialog()"> <button id="sb-actions-button" class="icon-button" onclick="OpenRequiredActionsDialog()">
</button> </button>
<button id="sb-status-button" onclick="OpenConnectDialog()"> <button id="sb-status-button" class="icon-button" onclick="OpenConnectDialog()">
<img id="sb-status-icon" style="height: 1em;" /> <img id="sb-status-icon" style="height: 1em;" />
</button> </button>
</div> </div>
@@ -29,7 +29,7 @@
</iframe> </iframe>
<!-- Blur Layer --> <!-- Blur Layer -->
<div id="blur-layer"></div> <div id="blur-layer" class="blur"></div>
<!-- Streamer.bot Required Actions Dialog --> <!-- Streamer.bot Required Actions Dialog -->
<div id="sb-required-actions-dialog" class="dialog"> <div id="sb-required-actions-dialog" class="dialog">
+33 -10
View File
@@ -7,6 +7,7 @@ const urlParams = new URLSearchParams(queryString);
const configJson = urlParams.get("config") || ""; const configJson = urlParams.get("config") || "";
/////////////////// ///////////////////
// PAGE ELEMENTS // // PAGE ELEMENTS //
/////////////////// ///////////////////
@@ -22,18 +23,25 @@ const sbRequiredActionsSuccessLabel = document.getElementById('sb-required-actio
const sbRequiredActionsFailureLabel = document.getElementById('sb-required-actions-failure'); const sbRequiredActionsFailureLabel = document.getElementById('sb-required-actions-failure');
const sbRequiredActionsFailureSubtext = document.getElementById('sb-required-actions-failure-subtext'); const sbRequiredActionsFailureSubtext = document.getElementById('sb-required-actions-failure-subtext');
const sbRequiredActionsList = document.getElementById('sb-required-actions-list'); const sbRequiredActionsList = document.getElementById('sb-required-actions-list');
const sbImportCodeLabel = document.getElementById('sb-import-code'); const sbImportCodeLabel = document.getElementById('sb-import-code');
const sbImportCopyButton = document.getElementById('sb-import-copy-button'); const sbImportCopyButton = document.getElementById('sb-import-copy-button');
const sbActionsButton = document.getElementById('sb-actions-button'); const sbActionsButton = document.getElementById('sb-actions-button');
const sbStatusButton = document.getElementById('sb-status-button'); const sbStatusButton = document.getElementById('sb-status-button');
const sbStatusIcon = document.getElementById('sb-status-icon'); const sbStatusIcon = document.getElementById('sb-status-icon');
const blurLayer = document.getElementById('blur-layer'); const blurLayer = document.getElementById('blur-layer');
const contentIFrame = document.getElementById('content');
//////////////////////
// GLOBAL VARIABLES //
//////////////////////
let sbClientListeners;
///////////////////////// /////////////////////////
@@ -54,7 +62,7 @@ const sbServerAddress = sbAddressInput.value;
const sbServerPort = sbPortInput.value; const sbServerPort = sbPortInput.value;
const sbServerPassword = sbPasswordInput.value; const sbServerPassword = sbPasswordInput.value;
window.sbClient = new StreamerbotClient({ sbClient = new StreamerbotClient({
host: sbServerAddress, host: sbServerAddress,
port: sbServerPort, port: sbServerPort,
password: sbServerPassword, password: sbServerPassword,
@@ -65,6 +73,16 @@ window.sbClient = new StreamerbotClient({
console.debug(data); console.debug(data);
SetConnectionState(true); SetConnectionState(true);
// Notify iframe
contentIFrame.addEventListener("load", () => {
NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff(data);
});
NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff();
// Idk why but listeners get cleared when re-connecting, so copy them back in
if (sbClientListeners)
sbClient.listeners = sbClientListeners;
}, },
onDisconnect: () => { onDisconnect: () => {
@@ -89,7 +107,7 @@ function SetConnectionState(isConnected) {
sbErrorLabel.style.display = 'none'; sbErrorLabel.style.display = 'none';
sbStatusIcon.src = 'icons/connected.svg'; sbStatusIcon.src = 'icons/connected.svg';
sbStatusButton.title = `Connected to ${window.sbClient.info.name} (${window.sbClient.info.version})`; sbStatusButton.title = `Connected to ${sbClient.info.name} (${sbClient.info.version})`;
// Check required actions // Check required actions
CheckRequiredActions(); CheckRequiredActions();
@@ -188,8 +206,7 @@ async function CheckRequiredActions() {
sbRequiredActionsList.appendChild(item); sbRequiredActionsList.appendChild(item);
}); });
} }
else else {
{
// There are no required actions, so hide the button // There are no required actions, so hide the button
sbActionsButton.style.display = 'none'; sbActionsButton.style.display = 'none';
} }
@@ -199,8 +216,7 @@ async function CheckRequiredActions() {
} }
function SetRequiredActionState(isSuccess) { function SetRequiredActionState(isSuccess) {
if (isSuccess) if (isSuccess) {
{
sbRequiredActionsSuccessLabel.style.display = 'block'; sbRequiredActionsSuccessLabel.style.display = 'block';
sbRequiredActionsFailureLabel.style.display = 'none'; sbRequiredActionsFailureLabel.style.display = 'none';
sbRequiredActionsFailureSubtext.style.display = 'none'; sbRequiredActionsFailureSubtext.style.display = 'none';
@@ -209,8 +225,7 @@ function SetRequiredActionState(isSuccess) {
sbActionsButton.textContent = '✅'; sbActionsButton.textContent = '✅';
} }
else else {
{
sbRequiredActionsSuccessLabel.style.display = 'none'; sbRequiredActionsSuccessLabel.style.display = 'none';
sbRequiredActionsFailureLabel.style.display = 'block'; sbRequiredActionsFailureLabel.style.display = 'block';
sbRequiredActionsFailureSubtext.style.display = 'block'; sbRequiredActionsFailureSubtext.style.display = 'block';
@@ -229,6 +244,7 @@ function SetRequiredActionState(isSuccess) {
/////////////////////// ///////////////////////
function Connect() { function Connect() {
sbClientListeners = sbClient.listeners;
sbClient.options.host = sbAddressInput.value; sbClient.options.host = sbAddressInput.value;
sbClient.options.port = sbPortInput.value; sbClient.options.port = sbPortInput.value;
sbClient.options.password = sbPasswordInput.value; sbClient.options.password = sbPasswordInput.value;
@@ -274,3 +290,10 @@ function CloseRequiredActionsDialog() {
sbRequiredActionsDialog.style.display = "none"; sbRequiredActionsDialog.style.display = "none";
blurLayer.style.display = "none"; blurLayer.style.display = "none";
} }
function NotifyTheContentIFrameThatSBHasConnectedSuccessfullySoItCanDoStuff(data) {
contentIFrame.contentWindow.postMessage(
{ type: "sbClientConnected", data },
"*" // replace with iframe origin for security if needed
);
}
+2 -21
View File
@@ -17,7 +17,8 @@ body {
align-items: center; align-items: center;
padding: 0em 1em; padding: 0em 1em;
gap: 1em; gap: 1em;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 1); box-shadow: 0 0 10px rgba(0,0,0,1);
z-index: 1;
} }
#header-end { #header-end {
@@ -25,11 +26,6 @@ body {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 1em;
}
#header-end button {
padding: 0.2em;
} }
@@ -41,21 +37,6 @@ body {
#content { #content {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
margin: 1em;
}
/******************/
/*** BLUR LAYER ***/
/******************/
#blur-layer {
position: fixed;
inset: 0; /* top:0; left:0; bottom:0; right:0 */
backdrop-filter: blur(10px); /* blur the content behind */
-webkit-backdrop-filter: blur(10px); /* Safari support */
z-index: 1; /* behind the modal */
} }
+122 -35
View File
@@ -34,20 +34,25 @@ body {
/************/ /************/
.title { .title {
font-weight: 900; font-weight: 900;
font-size: 1.2em; font-size: 1.2em;
text-transform: uppercase; text-transform: uppercase;
} }
.field { .field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5em; gap: 0.5em;
} }
.setting-description { .setting-description {
font-size: 0.9em; font-size: 0.9em;
font-weight: 100; font-weight: 200;
}
.setting-attribute {
font-size: 0.8em;
font-weight: 200;
} }
@@ -80,6 +85,9 @@ button {
padding: 0.5em 1em; padding: 0.5em 1em;
width: 100%; width: 100%;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
} }
button:hover { button:hover {
@@ -92,26 +100,66 @@ button:disabled {
cursor: inherit; cursor: inherit;
} }
.icon-button {
margin-left: auto;
width: 1em;
height: 1em;
background: transparent;
border: none;
font-size: 1.5em;
font-weight: 100;
padding: 1em;
border-radius: 50%;
}
.icon-button:hover {
background: var(--button-color);
}
/************************/ /************************/
/*** INPUT TEXT BOXES ***/ /*** INPUT TEXT BOXES ***/
/************************/ /************************/
textarea,
input { input {
font-family: inherit;
border-radius: 0.5em; border-radius: 0.5em;
width: auto; width: auto;
padding: 0.5em; padding: 0.5em;
background-color: #171717; background-color: #171717;
border: 1px solid #404040; border: none;
outline: 1px solid #404040;
color: white; color: white;
font-size: 0.9em; font-size: 0.9em;
transition: 0.2s;
} }
textarea:disabled,
input:disabled { input:disabled {
opacity: 0.5; opacity: 0.5;
} }
textarea:focus,
input:focus,
select:focus {
outline: 1px solid var(--accent-color);
}
textarea {
resize: vertical;
}
.textarea-description {
min-height: 10em;
}
textarea::-webkit-resizer {
display: none;
}
/***********************/ /***********************/
@@ -226,16 +274,16 @@ iframe {
/***************/ /***************/
.callout { .callout {
font-size: 0.9em; font-size: 0.9em;
font-weight: 100; font-weight: 100;
background-color: var(--callout-background); background-color: var(--callout-background);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1em; gap: 1em;
padding: 1em; padding: 1em;
border-radius: 0.5em; border-radius: 0.5em;
border: 1px solid #40404080; border: 1px solid #40404080;
} }
@@ -245,21 +293,22 @@ iframe {
/**************/ /**************/
.dialog { .dialog {
font-size: 1em; font-size: 1em;
background-color: var(--dialog-background); background-color: var(--dialog-background);
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1em; gap: 1em;
padding: 1em; padding: 1em;
border-radius: 0.5em; border-radius: 0.5em;
border: 1px solid #40404080; border: 1px solid #40404080;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
z-index: 1000; /* above overlay */ z-index: 1000;
/* above overlay */
} }
.dialog-nav-button { .dialog-nav-button {
@@ -271,11 +320,49 @@ iframe {
width: 1em; width: 1em;
height: 1em; height: 1em;
position: absolute; position: absolute;
position: fixed; /* fixed to viewport */ position: fixed;
top: 0.5em; /* small offset from top */ /* fixed to viewport */
right: 0.5em; /* small offset from right */ top: 0.5em;
/* small offset from top */
right: 0.5em;
/* small offset from right */
} }
.dialog-nav-button:hover { .dialog-nav-button:hover {
background: var(--button-color); background: var(--button-color);
} }
/******************/
/*** BLUR LAYER ***/
/******************/
.blur {
position: fixed;
inset: 0;
/* top:0; left:0; bottom:0; right:0 */
backdrop-filter: blur(10px);
/* blur the content behind */
-webkit-backdrop-filter: blur(10px);
/* Safari support */
z-index: 1;
/* behind the modal */
}
/***************/
/*** SELECTS ***/
/***************/
select {
padding: 0.5em;
font-size: 0.9em;
border-radius: 0.5em;
border: none;
outline: 1px solid #404040;
background-color: var(--callout-background);
color: #fff;
cursor: pointer;
min-width: 8em;
}

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" href="#" />
<link rel="stylesheet" href="./style.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" />
</head>
<body>
<div id="refreshContainer">Refreshed</div>
<div id="instructionsContainer">
<div id="streamerbotConnectInstructions" style="display: none;">
<div style="padding: 10px 20px;">
Please connect to Streamer.bot.
<br>
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
</div>
</div>
<div id="missingActionsInstructions" style="display: none; ">
<div style="padding: 10px 20px;">
Streamer.bot is connected, but you are missing actions.
Please import <label style="color: #ffb700;">multistream-title-updater.nut</label> and refresh the page.
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
</div>
</div>
</div>
<div id="mainContainer">
<div class="straightUpIloveCum">
<div id="title" style="font-size: 20px;">Update Titles</div>
<button id="infoIcon" style="padding: 0px 10px;">
<img src="info.svg" height="16px"/>
</button>
<img src="disconnected.svg" id="connectionStatusIcon"/>
</div>
<br/>
<div id="ThisIsWhereAllTheCoolStuffHappens">
<div>
<input type="text" id="titleInput" class="textBox"><br><br>
<div class="straightUpIloveCum">
<input type="submit" id="submitButton" class="button submitButton" value="UPDATE ALL">
<input type="submit" id="refreshButton" class="button" value="REFRESH">
</div>
</div>
<br/>
<div id="fieldsContainer">
</div>
<template id="platformTemplate">
<div class="broadcastBox">
<button id="platformIconButton" style="padding: 0px 10px;">
<img id="icon" height="20px" style="padding: 0px 10px 0px 0px;"/>
</button>
<input type="text" class="textBox platformTitle">
<input type="submit" class="button submitButton platformSubmitButton" value="UPDATE">
</div>
</template>
</div>
<div id="noYouTubeStreamsInstructions" style="display: none; text-align: center; margin: 10px; font-weight: bold; opacity: 0.5;">
<div>
⚠️ YouTube titles can only be updated after the stream has started ⚠️
<br><br>
Please start your stream to update YouTube titles
</div>
</div>
</div>
</body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>
<script src="./script.js"></script>

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

+444
View File
@@ -0,0 +1,444 @@
////////////
// FIELDS //
////////////
let sbDebugMode = true;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const sbServerPort = urlParams.get("port") || 8080;
const sbServerAddress = urlParams.get("server") || "127.0.0.1";
/////////////////
// GLOBAL VARS //
/////////////////
let ws;
///////////////////////////////////
// SRTEAMER.BOT WEBSOCKET SERVER //
///////////////////////////////////
// This is the main function that connects to the Streamer.bot websocke server
function connectws() {
if ("WebSocket" in window) {
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
// Reconnect
ws.onclose = function () {
SetConnectionStatus(false);
setTimeout(connectws, 5000);
};
// Connect
ws.onopen = async function () {
SetConnectionStatus(true);
console.log("Subscribe to events");
ws.send(
JSON.stringify({
request: "Subscribe",
id: "subscribe-events-id",
// This is the list of Streamer.bot websocket events to subscribe to
// See full list of events here:
// https://docs.streamer.bot/api/servers/websocket/requests
events: {
twitch: [
"StreamUpdate"
],
youTube: [
"BroadcastStarted",
"BroadcastEnded",
"BroadcastUpdated"
],
general: [
"Custom"
]
}
})
);
sbGetActions(ws);
ws.onmessage = function (event) {
// Grab message and parse JSON
const msg = event.data;
const wsdata = JSON.parse(msg);
// Check if the user installed all the required Streamer.bot actions
if (wsdata.id == "GetActions") {
// Check if all the required SB action exist
ReadLinesFromFile('requiredActions.txt')
.then(requiredActions => {
if (sbCheckRequiredActions(wsdata.actions, requiredActions)) {
SetElementVisibility("missingActionsInstructions", false);
sbFetchBroadcasts(ws);
}
else
SetElementVisibility("missingActionsInstructions", true);
})
}
if (typeof wsdata.event == "undefined") {
return;
}
// Print data to log for debugging purposes
if (sbDebugMode) {
console.log(wsdata.data);
console.log(wsdata.event.source);
console.log(wsdata.event.type);
}
// Check for events to trigger
// See documentation for all events here:
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
switch (wsdata.event.source) {
// Twitch Events
case 'Twitch':
switch (wsdata.event.type) {
case ('StreamUpdate'):
sbFetchBroadcasts(ws);
break;
}
// Twitch Events
case 'YouTube':
switch (wsdata.event.type) {
case ('BroadcastStarted'):
case ('BroadcastEnded'):
case ('BroadcastUpdated'):
sbFetchBroadcasts(ws);
break;
}
// General Events
case 'General':
switch (wsdata.event.type) {
case ('Custom'):
switch (wsdata.data.action) {
case ('[NUT] Multistream Title Updater | Fetch Broadcasts'):
UpdateBroadcastList(wsdata.data);
break;
}
break;
}
break;
}
};
}
}
}
/////////////////////////
// STREAMER.BOT WIDGET //
/////////////////////////
function sbGetActions(ws) {
let request = JSON.stringify({
request: "GetActions",
id: "GetActions"
});
ws.send(request);
}
// Check if all the entries in targetActionNames exist in actionList
function sbCheckRequiredActions(actionList, targetActionNames) {
let foundActions = 0;
for (targetActionName of targetActionNames) {
if (actionList.some(action => action.name === targetActionName))
foundActions++;
}
return foundActions == targetActionNames.length
}
function sbFetchBroadcasts(ws) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Fetch Broadcasts"
}
});
ws.send(request);
}
function sbUpdateTitles(ws, title) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update All Broadcasts"
},
args: {
title: title
}
});
ws.send(request);
}
function sbUpdateTwitchTitle(ws, title) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update Twitch Title"
},
args: {
title: title
}
});
ws.send(request);
}
function sbUpdateYouTubeTitle(ws, title, broadcastId) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update YouTube Title"
},
args: {
broadcastId: broadcastId,
title: title
}
});
ws.send(request);
}
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
function generateUUID() { // Public Domain/MIT
var d = new Date().getTime();//Timestamp
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16;//random number between 0 and 16
if (d > 0) {//Use timestamp until depleted
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {//Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function IsNullOrWhitespace(str) {
return /^\s*$/.test(str);
}
function SetElementVisibility(elementID, visibility) {
let element = document.getElementById(elementID);
if (visibility)
element.style.display = 'inline';
else
element.style.display = 'none';
}
function ReadLinesFromFile(filePath) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', filePath, true);
request.onload = function () {
if (request.status === 200) {
resolve(request.responseText.split(/\r?\n/));
} else {
reject(new Error(`File loading failed with status: ${request.status}`));
}
};
request.onerror = function () {
reject(new Error('Network error occurred during file loading.'));
};
request.send();
});
}
///////////////////////////////////
// STREAMER.BOT WEBSOCKET STATUS //
///////////////////////////////////
// This function sets the visibility of the Streamer.bot status label on the overlay
function SetConnectionStatus(connected) {
let connectionStatusIcon = document.getElementById("connectionStatusIcon");
let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens");
if (connected) {
connectionStatusIcon.src = "connected.svg";
ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled');
SetElementVisibility("infoIcon", false);
SetElementVisibility("streamerbotConnectInstructions", false);
}
else {
connectionStatusIcon.src = "disconnected.svg";
ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled');
SetElementVisibility("infoIcon", true);
SetElementVisibility("streamerbotConnectInstructions", true);
// (1) Clear list of broadcasts
const fieldsContainer = document.getElementById('fieldsContainer');
fieldsContainer.innerHTML = "";
}
}
// This function sets the visibility of the Streamer.bot status label on the overlay
function RefreshAnimation() {
let refreshContainer = document.getElementById("refreshContainer");
refreshContainer.style.opacity = 1;
var tl = new TimelineMax();
tl
.to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone })
}
// Button handling for UPDATE ALL button only
const infoIcon = document.querySelector("#infoIcon");
infoIcon.addEventListener("click", function () {
window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9");
});
// Button handling for UPDATE ALL button only
const submitButton = document.querySelector("#submitButton");
submitButton.addEventListener("click", function () {
const titleInput = document.querySelector("#titleInput").value;
sbUpdateTitles(ws, titleInput);
});
// Button handling for REFRESH button only
const refreshButton = document.querySelector("#refreshButton");
refreshButton.addEventListener("click", function () {
sbFetchBroadcasts(ws);
});
function UpdateBroadcastList(data) {
// Iterate through broadcast list
// Find div whose ID matches the broadcast
// If it exists, update the title
// else, it's a new broadcast, so add it to the list
const fieldsContainer = document.getElementById('fieldsContainer');
for (const broadcast of data.broadcastList) {
const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`);
if (broadcastDiv == null)
AddBroadcast(broadcast);
else
UpdateBroadcast(broadcastDiv, broadcast);
}
// Check if any broadcasts have gone offline
// If so, delete them from the list
var parentDiv = document.getElementById('fieldsContainer');
const childDivs = parentDiv.querySelectorAll("div");
childDivs.forEach(childDiv => {
var result = data.broadcastList.find(obj => {
return obj.id === childDiv.id
})
if (result == null)
childDiv.innerHTML = "";
});
//// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS
//// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS
// // (1) Clear list of broadcasts
// const fieldsContainer = document.getElementById('fieldsContainer');
// fieldsContainer.innerHTML = "";
// // (2) For each broadcast in list, create new entry
// for (const broadcast of data.broadcastList)
// {
// AddBroadcast(broadcast);
// }
// Count the number of YouTube broadcasts
// If this is 0, put a message to tell the user that they need to go live on YouTube first
// Else, hide that message
const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0);
if (youtubeBroadcastCount <= 0)
SetElementVisibility("noYouTubeStreamsInstructions", true);
else
SetElementVisibility("noYouTubeStreamsInstructions", false);
RefreshAnimation();
}
function AddBroadcast(broadcast) {
// Get a reference to the template
const template = document.getElementById('platformTemplate');
// Create a new instance of the template
const instance = template.content.cloneNode(true);
// Assign ID to instance
const broadcastBox = instance.querySelector('.broadcastBox');
broadcastBox.id = broadcast.id;
// Modify the content of the template instance
const titleElement = instance.querySelector('.platformTitle');
titleElement.value = broadcast.title;
// Modify the content of the template instance
const buttonElement = instance.querySelector('.platformSubmitButton');
buttonElement.classList.add(broadcast.platform);
// Modify the icon of the template instance
const iconElement = instance.querySelector('#icon');
// Add button handling (need separate handling for Twitch/YouTube)
switch (broadcast.platform) {
case 'twitch':
iconElement.src = 'twitch.png';
buttonElement.addEventListener("click", function () {
sbUpdateTwitchTitle(ws, titleElement.value);
});
break;
case 'youtube':
// Modify the icon of the template instance
iconElement.src = 'youtube.png';
buttonElement.addEventListener("click", function () {
const youtubeID = broadcast.id.replace('youtube-', '');
sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID);
});
break;
}
// Add click event
const iconButton = instance.querySelector('#platformIconButton');
iconButton.addEventListener("click", function () {
window.open(broadcast.url);
});
// Insert the modified template instance into the DOM
const fieldsContainer = document.getElementById('fieldsContainer');
fieldsContainer.appendChild(instance);
}
function UpdateBroadcast(div, broadcast) {
// Modify the content of the template instance
const titleElement = div.querySelector('.platformTitle');
titleElement.value = broadcast.title;
}
connectws();

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../../../.common/styles/global.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
</body>
</html>
<script src="script.js"></script>
+45
View File
@@ -0,0 +1,45 @@
//////////////////////
// GLOBAL VARIABLES //
//////////////////////
const sbAction = '';
/////////////////////////
// STREAMER.BOT EVENTS //
/////////////////////////
window.parent.sbClient.on('General.Custom', (response) => {
console.debug(response.data);
CustomEvent(response.data);
})
/////////////////
// PRINTER BOT //
/////////////////
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
///////////////////////
// PAGE INTERACTIONS //
///////////////////////
///////////////////
// PAGE SETTINGS //
///////////////////
+3
View File
@@ -0,0 +1,3 @@
body {
margin: 1em;
}
+20
View File
@@ -0,0 +1,20 @@
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../.common/styles/global.css" />
<link rel="icon" href="../../.resources/logo.png" type="image/png">
<title>nutty</title>
<style>
#dock-wrapper {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<iframe id="dock-wrapper"></iframe>
<script src="script.js"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
// Construct URL
const currentURL = window.location.href;
let baseURL = currentURL;
if (baseURL.endsWith("index.html"))
baseURL = baseURL.replace("index.html", "");
const configJson = "?config=" + baseURL + "config.json";
// Implement widget dock core
window.dockWrapper = document.getElementById('dock-wrapper');
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
dockWrapper.addEventListener('load', () => {
dockWrapper.contentWindow.content.src = baseURL + '/contents';
});
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../../../.common/styles/global.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<!-- Update All Button -->
<button onclick="OpenUpdateAllDialog()">
<img src="https://api.iconify.design/material-symbols:edit-square-outline.svg?color=%23ffffff"> Update All
</button>
<!-- List of all broadcasts -->
<div id="broadcast-list">
</div>
<label id="youtube-warning" class="setting-attribute"><i>YouTube stream info can only be updated while live</i></label>
<!-- Blur Layer -->
<div id="blur-layer" class="blur"></div>
<!-- Update All Popup -->
<div id="update-all-dialog" class="dialog">
<button class="dialog-nav-button" onclick="CloseUpdateAllDialog()">×</button>
<label class="title" style="text-align: center;">Stream Info</label>
<div class="field">
<label>Title</label>
<input type="text" id="update-all-title-input" placeholder="My First Stream #RoadToAffiliate">
</div>
<div class="field">
<label>Category</label>
<input type="text" id="update-all-category-input" placeholder="Just Chatting">
</div>
<button onclick="UpdateAllSubmit()">Update All</button>
</div>
<!-- Update Twitch Popup -->
<div id="update-twitch-dialog" class="dialog">
<button class="dialog-nav-button" onclick="CloseUpdateTwitchDialog()">×</button>
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
<img src="icons/platforms/twitch.png" class="platform-icon">
<label class="title" style="text-align: center;">Stream Info</label>
</div>
<div class="field">
<label>Title</label>
<input type="text" id="update-twitch-title-input" placeholder="My First Stream #RoadToAffiliate">
</div>
<div class="field">
<label>Category</label>
<input type="text" id="update-twitch-category-input" placeholder="Just Chatting">
</div>
<div class="field">
<label>Tags</label>
<input type="text" id="update-twitch-tags-input" placeholder="Retro, Speedrun, CreamTeam">
</div>
<button onclick="UpdateTwitchSubmit()">Update</button>
</div>
<!-- Update Kick Popup -->
<div id="update-kick-dialog" class="dialog">
<button class="dialog-nav-button" onclick="CloseUpdateKickDialog()">×</button>
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
<img src="icons/platforms/kick.png" class="platform-icon">
<label class="title" style="text-align: center;">Stream Info</label>
</div>
<div class="field">
<label>Title</label>
<input type="text" id="update-kick-title-input" placeholder="My First Stream #RoadToAffiliate">
</div>
<div class="field">
<label>Category</label>
<input type="text" id="update-kick-category-input" placeholder="Just Chatting">
</div>
<button onclick="UpdateKickSubmit()">Update</button>
</div>
<!-- Update YouTube Popup -->
<div id="update-youtube-dialog" class="dialog">
<button class="dialog-nav-button" onclick="CloseUpdateYouTubeDialog()">×</button>
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1em;">
<img src="icons/platforms/youtube.png" class="platform-icon">
<label class="title" style="text-align: center;">Stream Info</label>
</div>
<div class="field">
<label>Title</label>
<input type="text" id="update-youtube-title-input" placeholder="My First Stream #RoadToAffiliate">
</div>
<div class="field">
<label>Description</label>
<textarea type="text" id="update-youtube-description-input" placeholder="I'm describing so hard right now..." class="textarea-description"></textarea>
</div>
<div class="field">
<label>Category</label>
<input type="text" id="update-youtube-category-input" placeholder="Gaming">
</div>
<div class="field">
<label>Tags</label>
<input type="text" id="update-youtube-tags-input" placeholder="Retro, Speedrun, CreamTeam">
</div>
<div class="field">
<label>Visibility</label>
<select id="update-youtube-privacy-select">
<option value="private">Private</option>
<option value="unlisted">Unlisted</option>
<option value="public">Public</option>
</select>
</div>
<button onclick="UpdateYouTubeSubmit()">Update</button>
</div>
</body>
</html>
<!-- Broadcast Template -->
<template id="broadcast-template">
<div class="broadcast">
<img class="platform-icon" id="platform-icon" src="icons/platforms/twitch.png"/>
<div class="broadcast-info">
<label id="broadcast-title"></label>
<label id="broadcast-category" class="setting-description"></label>
<label id="kick-warning" class="setting-attribute"><br><i>Current stream info only available when live — Showing last known stream title</i></label>
</div>
<div id="broadcast-buttons">
<button id="broadcast-stream-button" class="icon-button" title="Open Stream"><img class="button-icon" src="https://api.iconify.design/streamline:button-play-solid.svg?color=%23ffffff"></button>
<button id="broadcast-dashboard-button" class="icon-button" title="Dashboard"><img class="button-icon" src="https://api.iconify.design/gridicons:popout.svg?color=%23ffffff"></button>
<button id="broadcast-edit-button" class="icon-button flip-that-shit-homie" title="Edit"></button>
</div>
</div>
</template>
<script src="script.js"></script>
@@ -0,0 +1,308 @@
//////////////////////
// GLOBAL VARIABLES //
//////////////////////
const sbActionFetchBroadcasts = '9486774d-d706-41d8-85b4-7daff5cd1b0d';
const sbActionUpdateStreamInfo = '1c58eff0-e98a-4fab-86f0-6f5cee1d3ab3';
const sbActionOpenUrl = '14da1d44-6e29-4582-92c3-2c59388be57e';
let currentBroadcastId = '';
///////////////////
// PAGE ELEMENTS //
///////////////////
const blurLayer = document.getElementById('blur-layer');
const updateAllDialog = document.getElementById('update-all-dialog');
const updateTwitchDialog = document.getElementById('update-twitch-dialog');
const updateKickDialog = document.getElementById('update-kick-dialog');
const updateYouTubeDialog = document.getElementById('update-youtube-dialog');
const broadcastList = document.getElementById('broadcast-list');
const youtubeWarning = document.getElementById('youtube-warning');
/////////////////////////
// STREAMER.BOT EVENTS //
/////////////////////////
window.addEventListener("message", (event) => {
FetchBroadcasts();
});
window.parent.sbClient.on('General.Custom', (response) => {
GeneralCustom(response.data);
})
///////////////////////////////
// MULTISTREAM TITLE UPDATER //
///////////////////////////////
async function GeneralCustom(data) {
switch(data.actionId) {
case sbActionFetchBroadcasts:
{
// Iterate through list of broadcasts and add it to the list
for (const broadcast of data.broadcastList)
await AddBroadcast(broadcast);
// Check if YouTube account is connected
// If yes, count how many YouTube broadcasts there were
// If 0, show warning
const broadcasterInfo = await window.parent.sbClient.getBroadcaster();
if (broadcasterInfo.platforms.youtube)
{
const ytBroadcastCount = data.broadcastList.filter(b => b.platform === "youtube").length;
if (ytBroadcastCount <= 0)
youtubeWarning.style.display = 'inline';
else
youtubeWarning.style.display = 'none';
}
else
youtubeWarning.style.display = 'none';
// Check if any broadcasts have gone offline
// If so, delete them from the list
const childDivs = broadcastList.querySelectorAll(":scope > div");
childDivs.forEach(childDiv => {
var result = data.broadcastList.find(obj => {
return obj.id === childDiv.id
})
if (result == null)
childDiv.remove();
});
}
break;
}
}
async function FetchBroadcasts() {
// Fetch from Streamer.bot
await window.parent.sbClient.doAction({ id: sbActionFetchBroadcasts});
}
async function AddBroadcast(data) {
// Get a reference to the template
const template = document.getElementById('broadcast-template');
const existingDiv = broadcastList.querySelector(`#${data.id}`);
// Create a new instance of the template
const instance = existingDiv ? existingDiv : template.content.cloneNode(true);
// Get divs
const broadcastEl = instance.querySelector('.broadcast');
const platformIconEl = instance.querySelector('#platform-icon');
const titleEl = instance.querySelector('#broadcast-title');
const categoryEl = instance.querySelector('#broadcast-category');
const kickWarningEl = instance.querySelector('#kick-warning');
const streamButtonEl = instance.querySelector('#broadcast-stream-button');
const dashboardButtonEl = instance.querySelector('#broadcast-dashboard-button');
const editButtonEl = instance.querySelector('#broadcast-edit-button');
// Set properties
if (!existingDiv)
broadcastEl.id = data.id;
// Set the platform icon
platformIconEl.src = `icons/platforms/${data.platform}.png`;
// Set the stream title
if (data.title)
titleEl.textContent = data.title;
// Set the stream category
if (data.category)
categoryEl.textContent = data.category;
// Streamer.bot does not provide title/category for Kick, so pull from API
if (data.platform == 'kick')
{
let response = await fetch('https://kick.com/api/v1/channels/' + data.userLogin);
let response_data = await response.json();
// The current title is only provided if the stream if currently live
if (response_data.livestream)
{
titleEl.textContent = response_data.livestream.session_title;
categoryEl.textContent = response_data.livestream.categories[0].name;
kickWarningEl.style.display = 'none';
}
else if (response_data.previous_livestreams)
{
titleEl.textContent = response_data.previous_livestreams[0].session_title;
categoryEl.textContent = response_data.previous_livestreams[0].categories[0].name;
kickWarningEl.style.display = 'inline';
}
else
{
kickWarningEl.style.display = 'inline';
}
}
streamButtonEl.onclick = function() {
//window.open(data.streamUrl, '_blank');
OpenURL(data.streamUrl);
};
dashboardButtonEl.onclick = function() {
//window.open(data.dashboardUrl, '_blank');
OpenURL(data.dashboardUrl);
};
editButtonEl.onclick = function() {
switch (data.platform) {
case 'twitch':
document.getElementById('update-twitch-title-input').value = titleEl.textContent;
document.getElementById('update-twitch-category-input').value = categoryEl.textContent;
updateTwitchDialog.style.display = "flex";
break;
case 'kick':
document.getElementById('update-kick-title-input').value = titleEl.textContent;
document.getElementById('update-kick-category-input').value = categoryEl.textContent;
updateKickDialog.style.display = "flex";
break;
case 'youtube':
currentBroadcastId = broadcastEl.id;
document.getElementById('update-youtube-title-input').value = data.title;
document.getElementById('update-youtube-description-input').value = data.description;
document.getElementById('update-youtube-category-input').value = data.category;
document.getElementById('update-youtube-privacy-select').value = data.privacy;
updateYouTubeDialog.style.display = "flex";
break;
}
blurLayer.style.display = "block";
};
if (!existingDiv)
broadcastList.appendChild(instance);
}
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
async function OpenURL(url) {
await window.parent.sbClient.doAction(
action = {
id: sbActionOpenUrl
},
args = {
url: url
}
);
}
///////////////////////
// PAGE INTERACTIONS //
///////////////////////
function OpenUpdateAllDialog() {
updateAllDialog.style.display = "flex";
blurLayer.style.display = "block";
}
function CloseUpdateAllDialog() {
updateAllDialog.style.display = "none";
blurLayer.style.display = "none";
}
function CloseUpdateTwitchDialog() {
updateTwitchDialog.style.display = "none";
blurLayer.style.display = "none";
}
function CloseUpdateKickDialog() {
updateKickDialog.style.display = "none";
blurLayer.style.display = "none";
}
function CloseUpdateYouTubeDialog() {
updateYouTubeDialog.style.display = "none";
blurLayer.style.display = "none";
}
async function UpdateAllSubmit() {
await window.parent.sbClient.doAction(
action = {
id: sbActionUpdateStreamInfo
},
args = {
platform: 'all',
title: document.getElementById('update-all-title-input').value,
category: document.getElementById('update-all-category-input').value
}
);
CloseUpdateAllDialog();
}
async function UpdateTwitchSubmit() {
await window.parent.sbClient.doAction(
action = {
id: sbActionUpdateStreamInfo
},
args = {
platform: 'twitch',
title: document.getElementById('update-twitch-title-input').value,
category: document.getElementById('update-twitch-category-input').value,
tags: document.getElementById('update-twitch-tags-input').value
}
);
CloseUpdateTwitchDialog();
}
async function UpdateKickSubmit() {
await window.parent.sbClient.doAction(
action = {
id: sbActionUpdateStreamInfo
},
args = {
platform: 'kick',
title: document.getElementById('update-kick-title-input').value,
category: document.getElementById('update-kick-category-input').value
}
);
CloseUpdateKickDialog();
}
async function UpdateYouTubeSubmit() {
await window.parent.sbClient.doAction(
action = {
id: sbActionUpdateStreamInfo
},
args = {
platform: 'youtube',
title: document.getElementById('update-youtube-title-input').value,
description: document.getElementById('update-youtube-description-input').value,
category: document.getElementById('update-youtube-category-input').value,
tags: document.getElementById('update-youtube-tags-input').value,
privacy: document.getElementById('update-youtube-privacy-select').value,
broadcastId: currentBroadcastId
}
);
CloseUpdateYouTubeDialog();
}
////////////////////////////
// REFRESH BROADCAST LIST //
////////////////////////////
setInterval(FetchBroadcasts, 5000);
@@ -0,0 +1,78 @@
body {
display: flex;
flex-direction: column;
gap: 1em;
margin: 1em;
}
button {
gap: 0.5em;
}
#broadcast-list {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.broadcast {
background-color: var(--dialog-background);
border: 1px solid #40404080;
border-radius: 0.5em;
padding: 0.2em 0.5em;
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
.broadcast-info {
display: flex;
flex-direction: column;
}
#broadcast-buttons {
display: flex;
flex-direction: row;
align-items: center;
/* gap: 1em; */
margin-left: auto;
}
.platform-icon {
height: 2em;
}
.button-icon {
height: 1em;
}
.flip-that-shit-homie {
/* Flip that bad boy */
transform: scaleX(-1);
}
.blur {
display: none;
}
.dialog {
display: none;
width: 90%;
max-height: 80%;
overflow-y: auto;
}
.setting-attribute {
color: yellow;
}
#kick-warning {
display: none;
}
#youtube-warning {
display: none;
text-align: center;
}
+13 -74
View File
@@ -1,81 +1,20 @@
<!DOCTYPE html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="shortcut icon" href="#" /> <link rel="stylesheet" href="../.common/styles/global.css" />
<link rel="stylesheet" href="./style.css" /> <link rel="icon" href="../../.resources/logo.png" type="image/png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css" /> <title>nutty</title>
<style>
#dock-wrapper {
width: 100vw;
height: 100vh;
}
</style>
</head> </head>
<body> <body>
<div id="refreshContainer">Refreshed</div> <iframe id="dock-wrapper"></iframe>
<script src="script.js"></script>
<div id="instructionsContainer">
<div id="streamerbotConnectInstructions" style="display: none;">
<div style="padding: 10px 20px;">
Please connect to Streamer.bot.
<br>
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
</div>
</div>
<div id="missingActionsInstructions" style="display: none; ">
<div style="padding: 10px 20px;">
Streamer.bot is connected, but you are missing actions.
Please import <label style="color: #ffb700;">multistream-title-updater.nut</label> and refresh the page.
Click <a class="hyperlinks" href="https://nuttylmao.notion.site/Multistream-Title-Updater-e2fb7b24c7514f9cbd943729f54be1d9" target="_blank">here</a> for instructions.
</div>
</div>
</div>
<div id="mainContainer">
<div class="straightUpIloveCum">
<div id="title" style="font-size: 20px;">Update Titles</div>
<button id="infoIcon" style="padding: 0px 10px;">
<img src="info.svg" height="16px"/>
</button>
<img src="disconnected.svg" id="connectionStatusIcon"/>
</div>
<br/>
<div id="ThisIsWhereAllTheCoolStuffHappens">
<div>
<input type="text" id="titleInput" class="textBox"><br><br>
<div class="straightUpIloveCum">
<input type="submit" id="submitButton" class="button submitButton" value="UPDATE ALL">
<input type="submit" id="refreshButton" class="button" value="REFRESH">
</div>
</div>
<br/>
<div id="fieldsContainer">
</div>
<template id="platformTemplate">
<div class="broadcastBox">
<button id="platformIconButton" style="padding: 0px 10px;">
<img id="icon" height="20px" style="padding: 0px 10px 0px 0px;"/>
</button>
<input type="text" class="textBox platformTitle">
<input type="submit" class="button submitButton platformSubmitButton" value="UPDATE">
</div>
</template>
</div>
<div id="noYouTubeStreamsInstructions" style="display: none; text-align: center; margin: 10px; font-weight: bold; opacity: 0.5;">
<div>
⚠️ YouTube titles can only be updated after the stream has started ⚠️
<br><br>
Please start your stream to update YouTube titles
</div>
</div>
</div>
</body> </body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script> </html>
<script src="./script.js"></script>
+11 -439
View File
@@ -1,444 +1,16 @@
//////////// // Construct URL
// FIELDS // const currentURL = window.location.href;
//////////// let baseURL = currentURL;
let sbDebugMode = true; if (baseURL.endsWith("index.html"))
baseURL = baseURL.replace("index.html", "");
const queryString = window.location.search; const configJson = "?config=" + baseURL + "config.json";
const urlParams = new URLSearchParams(queryString);
const sbServerPort = urlParams.get("port") || 8080; // Implement widget dock core
const sbServerAddress = urlParams.get("server") || "127.0.0.1"; window.dockWrapper = document.getElementById('dock-wrapper');
dockWrapper.src = `../../.common/core/widget-dock-core${configJson}`;
///////////////// dockWrapper.addEventListener('load', () => {
// GLOBAL VARS // dockWrapper.contentWindow.content.src = baseURL + '/contents';
/////////////////
let ws;
///////////////////////////////////
// SRTEAMER.BOT WEBSOCKET SERVER //
///////////////////////////////////
// This is the main function that connects to the Streamer.bot websocke server
function connectws() {
if ("WebSocket" in window) {
ws = new WebSocket("ws://" + sbServerAddress + ":" + sbServerPort + "/");
// Reconnect
ws.onclose = function () {
SetConnectionStatus(false);
setTimeout(connectws, 5000);
};
// Connect
ws.onopen = async function () {
SetConnectionStatus(true);
console.log("Subscribe to events");
ws.send(
JSON.stringify({
request: "Subscribe",
id: "subscribe-events-id",
// This is the list of Streamer.bot websocket events to subscribe to
// See full list of events here:
// https://docs.streamer.bot/api/servers/websocket/requests
events: {
twitch: [
"StreamUpdate"
],
youTube: [
"BroadcastStarted",
"BroadcastEnded",
"BroadcastUpdated"
],
general: [
"Custom"
]
}
})
);
sbGetActions(ws);
ws.onmessage = function (event) {
// Grab message and parse JSON
const msg = event.data;
const wsdata = JSON.parse(msg);
// Check if the user installed all the required Streamer.bot actions
if (wsdata.id == "GetActions") {
// Check if all the required SB action exist
ReadLinesFromFile('requiredActions.txt')
.then(requiredActions => {
if (sbCheckRequiredActions(wsdata.actions, requiredActions)) {
SetElementVisibility("missingActionsInstructions", false);
sbFetchBroadcasts(ws);
}
else
SetElementVisibility("missingActionsInstructions", true);
})
}
if (typeof wsdata.event == "undefined") {
return;
}
// Print data to log for debugging purposes
if (sbDebugMode) {
console.log(wsdata.data);
console.log(wsdata.event.source);
console.log(wsdata.event.type);
}
// Check for events to trigger
// See documentation for all events here:
// https://wiki.streamer.bot/en/Servers-Clients/WebSocket-Server/Events
switch (wsdata.event.source) {
// Twitch Events
case 'Twitch':
switch (wsdata.event.type) {
case ('StreamUpdate'):
sbFetchBroadcasts(ws);
break;
}
// Twitch Events
case 'YouTube':
switch (wsdata.event.type) {
case ('BroadcastStarted'):
case ('BroadcastEnded'):
case ('BroadcastUpdated'):
sbFetchBroadcasts(ws);
break;
}
// General Events
case 'General':
switch (wsdata.event.type) {
case ('Custom'):
switch (wsdata.data.action) {
case ('[NUT] Multistream Title Updater | Fetch Broadcasts'):
UpdateBroadcastList(wsdata.data);
break;
}
break;
}
break;
}
};
}
}
}
/////////////////////////
// STREAMER.BOT WIDGET //
/////////////////////////
function sbGetActions(ws) {
let request = JSON.stringify({
request: "GetActions",
id: "GetActions"
});
ws.send(request);
}
// Check if all the entries in targetActionNames exist in actionList
function sbCheckRequiredActions(actionList, targetActionNames) {
let foundActions = 0;
for (targetActionName of targetActionNames) {
if (actionList.some(action => action.name === targetActionName))
foundActions++;
}
return foundActions == targetActionNames.length
}
function sbFetchBroadcasts(ws) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Fetch Broadcasts"
}
});
ws.send(request);
}
function sbUpdateTitles(ws, title) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update All Broadcasts"
},
args: {
title: title
}
});
ws.send(request);
}
function sbUpdateTwitchTitle(ws, title) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update Twitch Title"
},
args: {
title: title
}
});
ws.send(request);
}
function sbUpdateYouTubeTitle(ws, title, broadcastId) {
let request = JSON.stringify({
request: "DoAction",
id: generateUUID(),
action: {
name: "[NUT] Multistream Title Updater | Update YouTube Title"
},
args: {
broadcastId: broadcastId,
title: title
}
});
ws.send(request);
}
//////////////////////
// HELPER FUNCTIONS //
//////////////////////
function generateUUID() { // Public Domain/MIT
var d = new Date().getTime();//Timestamp
var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16;//random number between 0 and 16
if (d > 0) {//Use timestamp until depleted
r = (d + r) % 16 | 0;
d = Math.floor(d / 16);
} else {//Use microseconds since page-load if supported
r = (d2 + r) % 16 | 0;
d2 = Math.floor(d2 / 16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
function IsNullOrWhitespace(str) {
return /^\s*$/.test(str);
}
function SetElementVisibility(elementID, visibility) {
let element = document.getElementById(elementID);
if (visibility)
element.style.display = 'inline';
else
element.style.display = 'none';
}
function ReadLinesFromFile(filePath) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', filePath, true);
request.onload = function () {
if (request.status === 200) {
resolve(request.responseText.split(/\r?\n/));
} else {
reject(new Error(`File loading failed with status: ${request.status}`));
}
};
request.onerror = function () {
reject(new Error('Network error occurred during file loading.'));
};
request.send();
});
}
///////////////////////////////////
// STREAMER.BOT WEBSOCKET STATUS //
///////////////////////////////////
// This function sets the visibility of the Streamer.bot status label on the overlay
function SetConnectionStatus(connected) {
let connectionStatusIcon = document.getElementById("connectionStatusIcon");
let ThisIsWhereAllTheCoolStuffHappens = document.getElementById("ThisIsWhereAllTheCoolStuffHappens");
if (connected) {
connectionStatusIcon.src = "connected.svg";
ThisIsWhereAllTheCoolStuffHappens.classList.remove('disabled');
SetElementVisibility("infoIcon", false);
SetElementVisibility("streamerbotConnectInstructions", false);
}
else {
connectionStatusIcon.src = "disconnected.svg";
ThisIsWhereAllTheCoolStuffHappens.classList.add('disabled');
SetElementVisibility("infoIcon", true);
SetElementVisibility("streamerbotConnectInstructions", true);
// (1) Clear list of broadcasts
const fieldsContainer = document.getElementById('fieldsContainer');
fieldsContainer.innerHTML = "";
}
}
// This function sets the visibility of the Streamer.bot status label on the overlay
function RefreshAnimation() {
let refreshContainer = document.getElementById("refreshContainer");
refreshContainer.style.opacity = 1;
var tl = new TimelineMax();
tl
.to(refreshContainer, 2, { opacity: 0, ease: Linear.easeNone })
}
// Button handling for UPDATE ALL button only
const infoIcon = document.querySelector("#infoIcon");
infoIcon.addEventListener("click", function () {
window.open("https://www.notion.so/nutty-s-multistream-title-updater-e2fb7b24c7514f9cbd943729f54be1d9");
}); });
// Button handling for UPDATE ALL button only
const submitButton = document.querySelector("#submitButton");
submitButton.addEventListener("click", function () {
const titleInput = document.querySelector("#titleInput").value;
sbUpdateTitles(ws, titleInput);
});
// Button handling for REFRESH button only
const refreshButton = document.querySelector("#refreshButton");
refreshButton.addEventListener("click", function () {
sbFetchBroadcasts(ws);
});
function UpdateBroadcastList(data) {
// Iterate through broadcast list
// Find div whose ID matches the broadcast
// If it exists, update the title
// else, it's a new broadcast, so add it to the list
const fieldsContainer = document.getElementById('fieldsContainer');
for (const broadcast of data.broadcastList) {
const broadcastDiv = fieldsContainer.querySelector(`#${broadcast.id}`);
if (broadcastDiv == null)
AddBroadcast(broadcast);
else
UpdateBroadcast(broadcastDiv, broadcast);
}
// Check if any broadcasts have gone offline
// If so, delete them from the list
var parentDiv = document.getElementById('fieldsContainer');
const childDivs = parentDiv.querySelectorAll("div");
childDivs.forEach(childDiv => {
var result = data.broadcastList.find(obj => {
return obj.id === childDiv.id
})
if (result == null)
childDiv.innerHTML = "";
});
//// THIS CODE ALSO WORKS AND IS WAY SIMPLER, I JUST MADE THINGS
//// 10 TIMES HARDER FOR MYSELF BECAUSE I LOVE PAIN POGGERS
// // (1) Clear list of broadcasts
// const fieldsContainer = document.getElementById('fieldsContainer');
// fieldsContainer.innerHTML = "";
// // (2) For each broadcast in list, create new entry
// for (const broadcast of data.broadcastList)
// {
// AddBroadcast(broadcast);
// }
// Count the number of YouTube broadcasts
// If this is 0, put a message to tell the user that they need to go live on YouTube first
// Else, hide that message
const youtubeBroadcastCount = data.broadcastList.reduce((count, broadcast) => count + (broadcast.platform === "youtube" ? 1 : 0), 0);
if (youtubeBroadcastCount <= 0)
SetElementVisibility("noYouTubeStreamsInstructions", true);
else
SetElementVisibility("noYouTubeStreamsInstructions", false);
RefreshAnimation();
}
function AddBroadcast(broadcast) {
// Get a reference to the template
const template = document.getElementById('platformTemplate');
// Create a new instance of the template
const instance = template.content.cloneNode(true);
// Assign ID to instance
const broadcastBox = instance.querySelector('.broadcastBox');
broadcastBox.id = broadcast.id;
// Modify the content of the template instance
const titleElement = instance.querySelector('.platformTitle');
titleElement.value = broadcast.title;
// Modify the content of the template instance
const buttonElement = instance.querySelector('.platformSubmitButton');
buttonElement.classList.add(broadcast.platform);
// Modify the icon of the template instance
const iconElement = instance.querySelector('#icon');
// Add button handling (need separate handling for Twitch/YouTube)
switch (broadcast.platform) {
case 'twitch':
iconElement.src = 'twitch.png';
buttonElement.addEventListener("click", function () {
sbUpdateTwitchTitle(ws, titleElement.value);
});
break;
case 'youtube':
// Modify the icon of the template instance
iconElement.src = 'youtube.png';
buttonElement.addEventListener("click", function () {
const youtubeID = broadcast.id.replace('youtube-', '');
sbUpdateYouTubeTitle(ws, titleElement.value, youtubeID);
});
break;
}
// Add click event
const iconButton = instance.querySelector('#platformIconButton');
iconButton.addEventListener("click", function () {
window.open(broadcast.url);
});
// Insert the modified template instance into the DOM
const fieldsContainer = document.getElementById('fieldsContainer');
fieldsContainer.appendChild(instance);
}
function UpdateBroadcast(div, broadcast) {
// Modify the content of the template instance
const titleElement = div.querySelector('.platformTitle');
titleElement.value = broadcast.title;
}
connectws();
+1 -1
View File
@@ -67,7 +67,7 @@
<!-- Footer --> <!-- Footer -->
<div id="receipt-footer"> <div id="receipt-footer">
<img id="receipt-icon" src="icons/platforms/twitch.png"> <img id="receipt-icon" src="">
<div id="receipt-date"></div> <div id="receipt-date"></div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -304,7 +304,7 @@ async function CustomEvent(data) {
titleEl.innerText = `Gifted Sub`; titleEl.innerText = `Gifted Sub`;
const messageEl = document.createElement('div'); const messageEl = document.createElement('div');
messageEl.innerHTML = `<b>${data["recipient.userName"]}</b><br>received a sub from<br><b>${data.user}</b>!`; messageEl.innerHTML = `<b>${data.user}</b><br>gifted a sub to<br><b>${data["recipient.userName"]}</b>!`;
contentEl.appendChild(messageEl); contentEl.appendChild(messageEl);
} }
+6 -1
View File
@@ -1,3 +1,7 @@
body {
margin: 1em;
}
#settings { #settings {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -58,7 +62,8 @@
height: 2em; height: 2em;
} }
#receipt-icon:empty { #receipt-icon:not([src]),
#receipt-icon[src=""] {
display: none; display: none;
} }