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
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;
}