Skip to content

Instantly share code, notes, and snippets.

@kebien6020
Last active December 12, 2020 20:23
Show Gist options
  • Save kebien6020/e18b489e40cd3767b1badcbb4d8b431c to your computer and use it in GitHub Desktop.
Save kebien6020/e18b489e40cd3767b1badcbb4d8b431c to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Streaming live translate
// @namespace youtube.com
// @version 0.8
// @author u/BakuhatsuK
// @description Get streaming translation comments easily. Based on extension made by u/konokalahola
// @include https://*.youtube.com/watch*
// @run-at document-start
// @updateURL https://gist.github.com/kebien6020/e18b489e40cd3767b1badcbb4d8b431c/raw/live-tranlation.user.js
// ==/UserScript==
(function () {
'use strict';
const MSG_REGEX = /\[eng?\].*/i;
const DEBUG = false;
const sleep = ms => new Promise(res => setTimeout(res, ms));
const whenAvailable = async (selector, wnd = window) => {
while (true) {
const elem = wnd.document.querySelector(selector);
if (!elem) {
if (DEBUG) console.warn('whenAvailable: Could not find selector', selector);
await sleep(1000);
} else {
if (DEBUG) console.log('whenAvailable: Found selector', selector);
return elem;
}
}
};
let ytApp;
async function main() {
if (DEBUG) console.log('main: Running on', window.location.href);
ytApp = await whenAvailable('ytd-app');
const appObserver = new MutationObserver(onPageChange);
appObserver.observe(ytApp, {attributes: true, attributeFilter: ['is-watch-page']});
// Trigger initial setup as if page had just changed
onPageChange();
}
let chatObserver;
async function onPageChange() {
if (DEBUG) console.log('onPageChange: Init');
const changedToWatchPage = ytApp.hasAttribute('is-watch-page');
if (changedToWatchPage) {
const chatFrame = await whenAvailable('#chatframe');
const chat = await whenAvailable('#item-offset', chatFrame.contentWindow);
if (DEBUG) console.log('onPageChange: Setting chat observer');
chatObserver = new MutationObserver(onChatChange);
chatObserver.observe(chat, {childList: true, subtree: true});
if (DEBUG) console.log('onPageChange: Setting up translation container');
resetContainer();
setupContainer();
} else {
if (DEBUG) console.log('onPageChange: Disconnecting chat observer');
if (chatObserver) {
chatObserver.disconnect();
}
if (DEBUG) console.log('onPageChange: Removing translation container');
resetContainer();
}
}
function onChatChange(mutations) {
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
const chatElems = [...mutation.addedNodes]
.filter(node => node.nodeType === Node.ELEMENT_NODE)
.filter(elem => elem.classList.contains('yt-live-chat-item-list-renderer'));
if (chatElems.length === 0) continue;
if (DEBUG) console.log('onChatChange: New messages ', chatElems.length);
for (const chatElem of chatElems) {
onMessage(chatElem);
}
}
}
function onMessage(chatElem) {
const msgElem = chatElem.querySelector('#message');
if (!msgElem) {
if (DEBUG) console.warn('onMessage: Could not find message within chatElem', chatElem);
return;
}
const msgText = msgElem.textContent;
const isAMatch = MSG_REGEX.test(msgText)
if (!isAMatch) return;
const author = chatElem.querySelector('#author-name');
const authorText = author ? author.textContent : '???';
if (DEBUG) console.log('onMessage: Matched text', msgText);
updateContainer(msgText, authorText);
}
function updateContainer(msg, author) {
const container = document.getElementById("translate_container");
if (!container) {
if (DEBUG) console.warn('updateContainer: Wasn\'t able to show message beacuse container is not ready');
return;
}
const position = container.scrollHeight - container.offsetHeight - 5;
container.insertAdjacentHTML('beforeend', `
<div style="margin-top: 15px;">
<b>${author}:</b>&nbsp;&nbsp;${msg}
</div>
`);
if (container.scrollTop >= position) {
container.scrollTo(0, container.scrollHeight);
}
}
function resetContainer() {
const button = document.getElementById("translate_live_button");
if (button) button.remove();
const container = document.getElementById("translate_container");
if (container) container.remove();
}
async function setupContainer() {
const upnext = await whenAvailable("#upnext");
upnext.insertAdjacentHTML('beforeend', '<svg id="translate_live_button" viewBox="0 0 20 20" width="20" height="20" class="adjustments w-6 h-6" style="vertical-align: middle; margin-left: 7px;"><path fill-rule="evenodd" d="M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z" clip-rule="evenodd"></path></svg>');
document.getElementById("translate_live_button").style.fill = "gray";
document.getElementById("upnext").style.display = "flex";
document.getElementById("translate_live_button").style.display = "block";
document.getElementById("translate_live_button").onclick = function() {
if (document.getElementById("translate_live_button").style.fill === "gray") {
let divTemp = document.getElementById("translate_container");
document.getElementById("translate_live_button").style.fill = "#c00";
document.getElementById("info-contents").style.display = "none";
divTemp.style.display = "block";
divTemp.scrollTo(0, divTemp.scrollHeight);
}
else {
document.getElementById("translate_live_button").style.fill = "gray";
document.getElementById("info-contents").style.display = "block";
document.getElementById("translate_container").style.display = "none";
}
};
document.getElementById("info-contents").insertAdjacentHTML('afterend', '<div id="translate_container" style="display: none; font-size: 13px; width: 100%; height: 120px; background-color: white; overflow: hidden; overflow-y: scroll; padding-bottom: 15px; margin-top: 5px; padding-left: 10px; padding-right: 10px;"></div>');
if (DEBUG) console.log("setupContainer: End setup");
}
if (document.readyState == "complete" || document.readyState == "loaded" || document.readyState == "interactive") {
main();
} else {
document.addEventListener("DOMContentLoaded", main);
}
})();
@kebien6020
Copy link
Author

How about putting the new messages at the top of the translate container, instead of the bottom?
Feels like that would decrease the distance you need to look to find the latest translation.

Just have to change
container.insertAdjacentHTML('beforeend, ' at line 115 to container.insertAdjacentHTML('afterbegin, '.

It's supposed to auto-scroll to the bottom, just like the regular chat.

@Fourmisain
Copy link

Hey, wanted to let you know that I forked this script to add a few features and needed fixes here

Changelog (can also be found in the source):

  • fix: script stops working because #chatframe url changes (which in turn changes #item-offset)
  • match 'en:', 'eng:' too
  • option to match generic 'any:', useful for collaborations
  • add 'collaboration' button which switches the generic 'any:' matching
  • option to always show moderators (default: on)
  • option to always show Yagoo (tanigox, default: on)
  • copy the actual comment instead of only the text (e.g. moderators will have blue names and emoji will appear)
    author colors won't work for super chats but they will show the donation amount
  • fix auto-scrolling not always working
  • filter chat mutations via tagName instead of classList (filter out 'engagement messages', slow mode notices and placeholders)
  • make updateContainer async (await the container) to ensure it will add early messages
  • slightly adjusted margins
  • adjust log levels, always log errors
  • add license notice of used icons

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment