Skip to content

Instantly share code, notes, and snippets.

@lumynou5
Last active March 18, 2024 11:58
Show Gist options
  • Save lumynou5/b0aaa067bd368eae15fbafff1585b1c3 to your computer and use it in GitHub Desktop.
Save lumynou5/b0aaa067bd368eae15fbafff1585b1c3 to your computer and use it in GitHub Desktop.
ytv.user.js
// ==UserScript==
// @name Ytv
// @version 0.1.2
// @description A 7tv workaround for YouTube.
// @author Lumynous
// @license MIT
// @match https://www.youtube.com/live_chat*
// @updateURL https://gist.github.com/lumynou5/b0aaa067bd368eae15fbafff1585b1c3/raw/ytv.user.js
// @downloadURL https://gist.github.com/lumynou5/b0aaa067bd368eae15fbafff1585b1c3/raw/ytv.user.js
// ==/UserScript==
(async function () {
'use strict';
const API_BASE_URL = 'https://7tv.io/v3';
const EMOTE_SET_IDS = [ 'global', '62973fbb0d36ad890711d5a8' ];
const PREFERRED_IMAGE_FORMAT = 'AVIF'; // Or 'WEBP', see https://caniuse.com/avif for browser support information.
const elmGetter = (function () {
const observers = new WeakMap();
// Creates an observer that observes mutations of the target.
function createObserver(target) {
const observer = new MutationObserver((mutations) => {
callback: for (const {callback, selector} of observer.callbacks) {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.target.matches(selector)) {
if (callback(mutation.target)) continue callback;
}
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
if (node.matches(selector)) {
if (callback(node)) continue callback;
}
for (const x of node.querySelectorAll(selector)) {
if (callback(x)) continue callback;
}
}
}
}
}
});
observer.callbacks = new Set();
observer.observe(target, {attributes: true, childList: true, subtree: true});
return observer;
}
// Adds a callback to execute on mutations of target that matches the selector to the observers.
// If there isn't an observer for the target, it's created.
function addCallback(target, callback, selector) {
let observer = observers.get(target);
if (!observer) {
observer = createObserver(target);
observers.set(target, observer);
}
observer.callbacks.add({callback, selector});
}
// Removes the callback that executes on mutations of target from the observers.
// If there's no more callback for the target after the removal, the corresponding observer is removed.
function removeCallback(target, callback) {
const observer = observers.get(target);
if (!observer) return;
observer.callbacks.delete(callback);
if (!observer.callbacks.size) {
observer.disconnect();
observers.delete(target);
}
}
return {
// Promises to find an element matching the selector inside the parent.
// If no parent provided, document is used.
get(selector, parent) {
parent = parent ?? document;
return new Promise((resolve) => {
const elm = parent.querySelector(selector);
if (elm) return resolve(elm);
const callback = (elm) => {
resolve(elm);
removeCallback(parent, callback);
return true;
};
addCallback(parent, callback, selector);
});
},
// Executes the action for each elements matching the selector inside the parent.
// If no parent provided, document is used.
// The callback can return true to cancel, or false to not memorize.
each(selector, ...args) {
const parent = typeof args[0] !== 'function' ? args.shift() : document;
const action = args[0];
let memory = new WeakSet();
for (const elm of parent.querySelectorAll(selector)) {
const ret = action(elm, false);
if (ret === true) return;
if (ret !== false) memory.add(elm);
}
const callback = (elm) => {
if (memory.has(elm)) return;
const ret = action(elm, true);
if (ret === true) {
removeCallback(parent, callback);
return true;
}
if (ret !== false) memory.add(elm);
};
addCallback(parent, callback, selector);
return () => {
memory = new WeakSet();
};
},
};
})();
const emotes = (await Promise.all(
EMOTE_SET_IDS.map(async (id) => {
let response = await fetch(`${API_BASE_URL}/emote-sets/${id}`);
let json = await response.json();
return json.emotes;
})
)).flat();
let style = document.createElement('style');
style.textContent = `
#message.yt-live-chat-text-message-renderer .ytv-emote {
height: var(--yt-live-chat-emoji-size);
margin: -1px 2px 1px;
vertical-align: middle;
}
`;
document.head.append(style);
elmGetter.each('#message.yt-live-chat-text-message-renderer', (elm) => {
elm.replaceChildren(...Array.from(elm.childNodes).flatMap((node) => {
if (node.nodeType !== Node.TEXT_NODE) return node;
return node.textContent
.split(/(?=\s+)/g) // Seperators will be kept at the starts of the substrings.
.map((substr) => {
let token = substr.trimStart();
for (const emote of emotes) {
if (token === emote.name) {
let img = document.createElement('img');
img.classList.add('ytv-emote');
img.alt = emote.name;
img.srcset = emote.data.host.files
.filter((file) => file.format === PREFERRED_IMAGE_FORMAT)
.map((file) => `https:${emote.data.host.url}/${file.name} ${file.name.split('.')[0]}`)
.join(',');
img.loading = 'lazy';
img.decoding = 'async';
return img;
}
}
return document.createTextNode(substr);
});
}));
});
})();
@lumynou5
Copy link
Author

lumynou5 commented Dec 12, 2023

Ytv

Ytv is a 7tv workaround for YouTube since 7tv lacks functionalities for YouTube currently.

screenshot

Install

Ensure Tampermonkey installed on your browser. Click the "Raw" button on this page, and then click the "Install".

Usage

Replace the value of EMOTE_SET_IDS to customize emote sets you want to use. The default ones are the global emote set and Ren's emote set.

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