////////////////////// // 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 = ''; let runningActionId = ''; /////////////////// // 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) => { console.debug(response.data); GeneralCustom(response.data); }) /////////////////////////////// // MULTISTREAM TITLE UPDATER // /////////////////////////////// async function GeneralCustom(data) { // Only run if response matches the ID of the corresponding FetchBroadcasts() call if (runningActionId != data.runningActionId) return; 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) { // Only show the warning if there are 0 monitored broadcasts const ytBroadcastCount = data.broadcastList.filter(b => b.platform === "youtube").length; if (ytBroadcastCount <= 0) youtubeWarning.style.display = 'flex'; else youtubeWarning.style.display = 'none'; // Set the URL to the livestreaming dashboard const broadcastButton = youtubeWarning.querySelector('#broadcast-dashboard-button'); broadcastButton.onclick = function() { //window.open(data.streamUrl, '_blank'); OpenURL(`https://studio.youtube.com/channel/${broadcasterInfo.platforms.youtube.broadcastUserId}/livestreaming`); }; } 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 const response = await window.parent.sbClient.doAction({ id: sbActionFetchBroadcasts}); runningActionId = response.args.runningActionId; } 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 let instance; if (existingDiv) instance = existingDiv; else { instance = template.content.firstElementChild.cloneNode(true); instance.id = data.id; broadcastList.appendChild(instance); } // Get divs 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'); // 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) { data.title = response_data.livestream.session_title; data.category = response_data.livestream.categories[0].name; kickWarningEl.style.display = 'none'; } else if (response_data.previous_livestreams.length > 0) { data.title = response_data.previous_livestreams[0].session_title; data.category = response_data.previous_livestreams[0].categories[0].name; kickWarningEl.style.display = 'inline'; } else { data.title = ''; data.category = ''; kickWarningEl.style.display = 'inline'; } } // Flash green to show that it updated if (titleEl.textContent != data.title || categoryEl.textContent != data.category) { instance.style.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--confirm-flash'); setTimeout(() => { instance.style.backgroundColor = ''; }, 1000); } // Set the platform icon platformIconEl.src = `icons/platforms/${data.platform}.png`; // Special logo for YouTube shorts if (data.platform == 'youtube') { const targets = ["vertical", "shorts"]; const isShort = data.tags.some(item => targets.some(target => item.toLowerCase() === target.toLowerCase()) ); if (isShort) platformIconEl.src = `icons/platforms/youtube-shorts.png`; } // Set the stream title if (data.title) titleEl.textContent = data.title; // Set the stream category if (data.category) categoryEl.textContent = data.category; 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('twitch-title-input').value = data.title; document.getElementById('twitch-category-input').value = data.category; document.getElementById('twitch-tags-input').value = data.tags.join(", "); ValidateTwitchDialog(); updateTwitchDialog.style.display = "flex"; break; case 'kick': document.getElementById('kick-title-input').value = titleEl.textContent; document.getElementById('kick-category-input').value = categoryEl.textContent; ValidateKickDialog(); updateKickDialog.style.display = "flex"; break; case 'youtube': currentBroadcastId = data.id; document.getElementById('youtube-title-input').value = data.title; document.getElementById('youtube-description-input').value = data.description; document.getElementById('youtube-category-input').value = data.category; document.getElementById('youtube-tags-input').value = data.tags.join(", "); document.getElementById('youtube-privacy-select').value = data.privacy; ValidateYouTubeDialog(); updateYouTubeDialog.style.display = "flex"; break; } blurLayer.style.display = "block"; }; } ////////////////////// // 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('all-title-input').value, category: document.getElementById('all-category-input').value } ); CloseUpdateAllDialog(); } async function UpdateTwitchSubmit() { await window.parent.sbClient.doAction( action = { id: sbActionUpdateStreamInfo }, args = { platform: 'twitch', title: document.getElementById('twitch-title-input').value, category: document.getElementById('twitch-category-input').value, tags: document.getElementById('twitch-tags-input').value } ); CloseUpdateTwitchDialog(); } async function UpdateKickSubmit() { await window.parent.sbClient.doAction( action = { id: sbActionUpdateStreamInfo }, args = { platform: 'kick', title: document.getElementById('kick-title-input').value, category: document.getElementById('kick-category-input').value } ); CloseUpdateKickDialog(); } async function UpdateYouTubeSubmit() { await window.parent.sbClient.doAction( action = { id: sbActionUpdateStreamInfo }, args = { platform: 'youtube', title: document.getElementById('youtube-title-input').value, description: document.getElementById('youtube-description-input').value, category: document.getElementById('youtube-category-input').value, tags: document.getElementById('youtube-tags-input').value, privacy: document.getElementById('youtube-privacy-select').value, broadcastId: currentBroadcastId } ); CloseUpdateYouTubeDialog(); } ///////////////////// // DATA VALIDATION // ///////////////////// const GLOBAL_TITLE_MAX = 100; const TWITCH_TITLE_MAX = 140; const TWITCH_TAGS_MAX = 10; const KICK_TITLE_MAX = 100; const YOUTUBE_TITLE_MAX = 100; const YOUTUBE_DESCRIPTION_MAX = 5000; const YOUTUBE_TAGS_MAX = 500; const allTitleInput = document.getElementById("all-title-input"); const twitchTitleInput = document.getElementById("twitch-title-input"); const twitchTagsInput = document.getElementById("twitch-tags-input"); const kickTitleInput = document.getElementById("kick-title-input"); const youtubeTitleInput = document.getElementById("youtube-title-input"); const youtubeDescriptionInput = document.getElementById("youtube-description-input"); const youtubeTagsInput = document.getElementById("youtube-tags-input"); // Function to update char count and handle validation function ValidateUpdateAllDialog() { // Get references to elements const allTitleCharLimit = document.getElementById('all-title-char-limit'); const allCharCount = document.getElementById('all-title-char-count'); const allTitleCharMax = document.getElementById('all-title-char-max'); const allUpdateButton = document.getElementById('all-submit-button'); // Validate title field const currentLength = allTitleInput.value.length; allCharCount.textContent = currentLength; allTitleCharMax.textContent = GLOBAL_TITLE_MAX; if (currentLength > GLOBAL_TITLE_MAX) allTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else allTitleCharLimit.style.color = ""; // reset to default // Set button interactability (yes, that is a word — look it up) if (currentLength > GLOBAL_TITLE_MAX) allUpdateButton.disabled = true; else allUpdateButton.disabled = false; } // Function to update char count and handle validation function ValidateTwitchDialog() { // Get references to elements const twitchTitleCharLimit = document.getElementById('twitch-title-char-limit'); const twitchCharCount = document.getElementById('twitch-title-char-count'); const twitchTitleCharMax = document.getElementById('twitch-title-char-max'); const twitchTagsCharLimit = document.getElementById('twitch-tags-char-limit'); const twitchTagsCharCount = document.getElementById('twitch-tags-char-count'); const twitchTagsCharMax = document.getElementById('twitch-tags-char-max'); const twitchTagsTooLongWarning = document.getElementById('twitch-tags-too-long-warning'); const twitchUpdateButton = document.getElementById('twitch-submit-button'); // Validate title field const currentLength = twitchTitleInput.value.length; twitchCharCount.textContent = currentLength; twitchTitleCharMax.textContent = TWITCH_TITLE_MAX; if (currentLength > TWITCH_TITLE_MAX) twitchTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else twitchTitleCharLimit.style.color = ""; // reset to default // Validate tags field // Split by comma, trim each tag, sum lengths const tagsArray = twitchTagsInput.value .split(',') .map(tag => tag.trim()) .filter(tag => tag.length > 0); const tagsLength = tagsArray.length; twitchTagsCharCount.textContent = tagsLength; twitchTagsCharMax.textContent = TWITCH_TAGS_MAX; if (tagsLength > TWITCH_TAGS_MAX) twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else twitchTagsCharLimit.style.color = ""; // reset to default // Also check that each tag is under 25 characters const hasLongTag = tagsArray.some(tag => tag.length > 25); if (hasLongTag) { twitchTagsTooLongWarning.style.display = 'inline'; twitchTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); } else { twitchTagsTooLongWarning.style.display = 'none'; twitchTagsCharLimit.style.color = ""; // reset to default } // Set button interactability (yes, that is a word — look it up) if (currentLength > TWITCH_TITLE_MAX || tagsLength > TWITCH_TAGS_MAX || hasLongTag) twitchUpdateButton.disabled = true; else twitchUpdateButton.disabled = false; } // Function to update char count and handle validation function ValidateKickDialog() { // Get references to elements const kickTitleCharLimit = document.getElementById('kick-title-char-limit'); const kickCharCount = document.getElementById('kick-title-char-count'); const kickTitleCharMax = document.getElementById('kick-title-char-max'); const kickUpdateButton = document.getElementById('kick-submit-button'); // Validate title field const currentLength = kickTitleInput.value.length; kickCharCount.textContent = currentLength; kickTitleCharMax.textContent = KICK_TITLE_MAX; if (currentLength > KICK_TITLE_MAX) kickTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else kickTitleCharLimit.style.color = ""; // reset to default // Set button interactability (yes, that is a word — look it up) if (currentLength > KICK_TITLE_MAX) kickUpdateButton.disabled = true; else kickUpdateButton.disabled = false; } function ValidateYouTubeDialog() { // Get references to elements const youtubeTitleCharLimit = document.getElementById('youtube-title-char-limit'); const youtubeTitleCharCount = document.getElementById('youtube-title-char-count'); const youtubeTitleCharMax = document.getElementById('youtube-title-char-max'); const youtubeDescriptionCharLimit = document.getElementById('youtube-description-char-limit'); const youtubeDescriptionCharCount = document.getElementById('youtube-description-char-count'); const youtubeDescriptionCharMax = document.getElementById('youtube-description-char-max'); const youtubeTagsCharLimit = document.getElementById('youtube-tags-char-limit'); const youtubeTagsCharCount = document.getElementById('youtube-tags-char-count'); const youtubeTagsCharMax = document.getElementById('youtube-tags-char-max'); const youtubeUpdateButton = document.getElementById('youtube-submit-button'); // Validate title field const titleLength = youtubeTitleInput.value.length; youtubeTitleCharCount.textContent = titleLength; youtubeTitleCharMax.textContent = YOUTUBE_TITLE_MAX; if (titleLength > YOUTUBE_TITLE_MAX) youtubeTitleCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else youtubeTitleCharLimit.style.color = ""; // reset to default // Validate description field const descriptionLength = youtubeDescriptionInput.value.length; youtubeDescriptionCharCount.textContent = descriptionLength; youtubeDescriptionCharMax.textContent = YOUTUBE_DESCRIPTION_MAX; if (descriptionLength > YOUTUBE_DESCRIPTION_MAX) youtubeDescriptionCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else youtubeDescriptionCharLimit.style.color = ""; // reset to default // Validate tags field // Split by comma, trim each tag, sum lengths const tagsArray = youtubeTagsInput.value .split(',') .map(tag => tag.trim()) .filter(tag => tag.length > 0); const tagsLength = tagsArray.reduce((sum, tag) => sum + tag.length, 0) + (tagsArray.length > 0 ? tagsArray.length - 1 : 0); youtubeTagsCharCount.textContent = tagsLength; youtubeTagsCharMax.textContent = YOUTUBE_TAGS_MAX; if (tagsLength > YOUTUBE_TAGS_MAX) youtubeTagsCharLimit.style.color = getComputedStyle(document.documentElement).getPropertyValue('--error-text-color'); else youtubeTagsCharLimit.style.color = ""; // reset to default // Set button interactability (yes, that is a word — look it up) if (titleLength > YOUTUBE_TITLE_MAX || descriptionLength > YOUTUBE_DESCRIPTION_MAX || tagsLength > YOUTUBE_TAGS_MAX) youtubeUpdateButton.disabled = true; else youtubeUpdateButton.disabled = false; } // Attach event listener allTitleInput.addEventListener("input", ValidateUpdateAllDialog); twitchTitleInput.addEventListener("input", ValidateTwitchDialog); twitchTagsInput.addEventListener("input", ValidateTwitchDialog); kickTitleInput.addEventListener("input", ValidateKickDialog); youtubeTitleInput.addEventListener("input", ValidateYouTubeDialog); youtubeDescriptionInput.addEventListener("input", ValidateYouTubeDialog); youtubeTagsInput.addEventListener("input", ValidateYouTubeDialog); // Initial check (in case the input has prefilled text) ValidateUpdateAllDialog(); ValidateTwitchDialog(); ValidateKickDialog(); ValidateYouTubeDialog(); //////////////////////////// // REFRESH BROADCAST LIST // //////////////////////////// setInterval(FetchBroadcasts, 5000);