From 85f132babd6cf00e60d50acda4fcf3a39e21d13e Mon Sep 17 00:00:00 2001 From: nuttylmao Date: Thu, 10 Jul 2025 10:21:20 +1000 Subject: [PATCH] Add support for YouTube Link Previews --- multichat-overlay/script.js | 131 +++++++++++++++++++++-- multichat-overlay/settings/settings.json | 8 ++ multichat-overlay/style.css | 15 +++ 3 files changed, 145 insertions(+), 9 deletions(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 859c983..139ba9f 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -10,6 +10,12 @@ const sbServerPort = urlParams.get("port") || "8080"; const avatarMap = new Map(); const pronounMap = new Map(); +///////////////// +// GLOBAL VARS // +///////////////// + +const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})(?:[&?#].*)?$/; + ///////////// // OPTIONS // ///////////// @@ -37,6 +43,7 @@ const scrollDirection = GetIntParam("scrollDirection", 1); const groupConsecutiveMessages = GetBooleanParam("groupConsecutiveMessages", false); const inlineChat = GetBooleanParam("inlineChat", false); const imageEmbedPermissionLevel = GetIntParam("imageEmbedPermissionLevel", 20); +const showYouTubeLinkPreviews = GetBooleanParam("showYouTubeLinkPreviews", true); const showTwitchMessages = GetBooleanParam("showTwitchMessages", true); const showTwitchAnnouncements = GetBooleanParam("showTwitchAnnouncements", true); @@ -537,7 +544,7 @@ async function TwitchChatMessage(data) { // Render emotes for (i in data.emotes) { - const emoteElement = ``; + const emoteElement = ``; const emoteName = EscapeRegExp(data.emotes[i].name); let regexPattern = emoteName; @@ -560,7 +567,7 @@ async function TwitchChatMessage(data) { const bits = data.cheerEmotes[i].bits; const imageUrl = data.cheerEmotes[i].imageUrl; const name = data.cheerEmotes[i].name; - const cheerEmoteElement = ``; + const cheerEmoteElement = ``; const bitsElements = `${bits}` messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${name}${bits}\\b`, 'i'), cheerEmoteElement + bitsElements); } @@ -609,6 +616,15 @@ async function TwitchChatMessage(data) { else { AddMessageItem(instance, data.message.msgId, 'twitch', data.user.id); } + + // Render YouTube links + if (youtubeRegex.test(message)) + { + const videoId = ExtractYouTubeVideoId(message); + const videoData = await GetYouTubeVideoData(videoId); + + YouTubeThumbnailPreview(videoData); + } } async function TwitchAutomaticRewardRedemption(data) { @@ -738,7 +754,7 @@ async function TwitchAnnouncement(data) { // Render emotes for (i in data.parts) { if (data.parts[i].type == `emote`) { - const emoteElement = ``; + const emoteElement = ``; const emoteName = EscapeRegExp(data.parts[i].text); let regexPattern = emoteName; @@ -1081,7 +1097,7 @@ function TwitchChatCleared(data) { } } -function YouTubeMessage(data) { +async function YouTubeMessage(data) { if (!showYouTubeMessages) return; @@ -1193,7 +1209,7 @@ function YouTubeMessage(data) { // Render emotes for (i in data.emotes) { - const emoteElement = ``; + const emoteElement = ``; // messageDiv.innerHTML = messageDiv.innerHTML.replace(new RegExp(`\\b${data.emotes[i].name}\\b`), emoteElement); messageDiv.innerHTML = messageDiv.innerHTML.replace(data.emotes[i].name, emoteElement); } @@ -1240,6 +1256,15 @@ function YouTubeMessage(data) { else { AddMessageItem(instance, data.eventId, 'youtube', data.user.id); } + + // Render YouTube links + if (youtubeRegex.test(data.message)) + { + const videoId = ExtractYouTubeVideoId(data.message); + const videoData = await GetYouTubeVideoData(videoId); + + YouTubeThumbnailPreview(videoData); + } } function YouTubeSuperChat(data) { @@ -1670,7 +1695,6 @@ function FourthwallOrderPlaced(data) { const iconDiv = instance.querySelector("#icon"); const titleDiv = instance.querySelector("#title"); const contentDiv = instance.querySelector("#content"); - const messageContentsDiv = instance.querySelector(".message-contents"); // Set the card background colors cardDiv.classList.add('fourthwall'); @@ -1680,7 +1704,7 @@ function FourthwallOrderPlaced(data) { // titleDiv.classList.add('centerThatShitHomie'); // contentDiv.classList.add('centerThatShitHomie'); - messageContentsDiv.style.gap = '1em'; + avatarDiv.style.width = 'auto'; // Set the text let user = data.username; @@ -2171,6 +2195,15 @@ async function KickChatMessage(data) { else { AddMessageItem(instance, data.msgId, 'kick', data.userId); } + + // Render YouTube links + if (youtubeRegex.test(message)) + { + const videoId = ExtractYouTubeVideoId(message); + const videoData = await GetYouTubeVideoData(videoId); + + YouTubeThumbnailPreview(videoData); + } } async function KickFollow(data) { @@ -2446,7 +2479,7 @@ function KickBan(data) { -function TikTokChat(data) { +async function TikTokChat(data) { // Don't post messages starting with "!" if (data.comment.startsWith("!") && excludeCommands) return; @@ -2571,6 +2604,15 @@ function TikTokChat(data) { } AddMessageItem(instance, data.msgId, 'tiktok', data.userId); + + // Render YouTube links + if (youtubeRegex.test(message)) + { + const videoId = ExtractYouTubeVideoId(message); + const videoData = await GetYouTubeVideoData(videoId); + + YouTubeThumbnailPreview(videoData); + } } function TikTokGift(data) { @@ -2631,6 +2673,41 @@ function TikTokSubscribe(data) { AddMessageItem(instance, data.msgId); } +function YouTubeThumbnailPreview(data) { + if (!showYouTubeLinkPreviews) + return; + + // Get a reference to the template + const template = document.getElementById('cardTemplate'); + + // Create a new instance of the template + const instance = template.content.cloneNode(true); + + // Get divs + const cardDiv = instance.querySelector("#card"); + const headerDiv = instance.querySelector("#header"); + const avatarDiv = instance.querySelector("#avatar"); + const iconDiv = instance.querySelector("#icon"); + const titleDiv = instance.querySelector("#title"); + const contentDiv = instance.querySelector("#content"); + + // Set the card background colors + cardDiv.classList.add('thumbnail'); + + avatarDiv.style.width = 'auto'; + + // Set the text + const title = data.title; + const author = data.author; + const thumbnail = ``; + + avatarDiv.innerHTML = thumbnail; + titleDiv.innerHTML = `${title}`; + contentDiv.innerHTML = `by ${author}`; + + AddMessageItem(instance); +} + ////////////////////// @@ -2753,6 +2830,11 @@ function IsImageUrl(url) { } } +function ExtractYouTubeVideoId(url) { + const match = url.match(youtubeRegex); + return match ? match[1] : null; +} + function AddMessageItem(element, elementID, platform, userId) { // Calculate the height of the div before inserting const tempDiv = document.getElementById('IPutThisHereSoICanCalculateHowBigEachMessageIsSupposedToBeBeforeIAddItToTheMessageList'); @@ -2972,4 +3054,35 @@ function SetConnectionStatus(connected) { statusContainer.style.transition = ""; statusContainer.style.opacity = 1; } -} \ No newline at end of file +} + +async function GetYouTubeVideoData(videoId) { + const url = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + return { + title: data.title, + author: data.author_name, + thumbnail: data.thumbnail_url, + }; + } catch (error) { + console.error('Error fetching YouTube video data:', error); + return null; + } +} + +async function SemenLover() +{ + let data = await GetYouTubeVideoData('dQw4w9WgXcQ'); + + console.log(data); +} + +SemenLover() \ No newline at end of file diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 31f8a69..e4f8777 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -226,6 +226,14 @@ "defaultValue": 20, "group": "General" }, + { + "id": "showYouTubeLinkPreviews", + "label": "Show YouTube Link Previews", + "description": "", + "type": "checkbox", + "defaultValue": true, + "group": "General" + }, { "id": "showTwitchMessages", "label": "Chat Messages", diff --git a/multichat-overlay/style.css b/multichat-overlay/style.css index fc7fc7f..9f36798 100644 --- a/multichat-overlay/style.css +++ b/multichat-overlay/style.css @@ -168,6 +168,12 @@ li { transform: translate(0px, 0.25em); } +.youtubeThumbnail { + width: 5em; + border-radius: 0.25em; + transform: translate(0px, 0.25em); +} + /* .centerThatShitHomie { display: inline-block; width: 100%; @@ -319,6 +325,15 @@ li { background: linear-gradient(#262626BF, #000000BF) !important; } +.fourthwall { + background: linear-gradient(#466edbbf, #1c56f5bf) !important; +} + +.thumbnail { + background: linear-gradient(#494949bf, #202020bf) !important; +} + + .blank { background: #adadad00 !important; }