Skip to content

Instantly share code, notes, and snippets.

@lumynou5
Last active May 3, 2024 08:21
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lumynou5/74bcbab54cd9d8fcd3c873fffbac5d3d to your computer and use it in GitHub Desktop.
Save lumynou5/74bcbab54cd9d8fcd3c873fffbac5d3d to your computer and use it in GitHub Desktop.
Make YouTube display the names of commenters instead of their handles.
// ==UserScript==
// @name YouTube Commenter Names
// @version 1.5.17
// @description Make YouTube display the names of commenters instead of their handles.
// @author Lumynous
// @license MIT
// @match https://www.youtube.com/*
// @match https://studio.youtube.com/*
// @noframes
// @downloadURL https://gist.github.com/lumynou5/74bcbab54cd9d8fcd3c873fffbac5d3d/raw/youtube-commenter-names.user.js
// ==/UserScript==
'use strict';
const elmGetter = (function () {
const observers = new WeakMap();
function observerCallback(mutations, observer) {
callback: for (const {callback, selector} of observer.callbacks) {
for (const mutation of mutations) {
let tmp;
if (mutation.type === 'characterData' && (tmp = mutation.target.parentElement?.closest(selector))) {
if (callback(tmp)) continue callback;
}
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.TEXT_NODE && (tmp = node.parentElement?.closest(selector))) {
if (callback(tmp)) continue callback;
continue;
}
if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.matches(selector)) {
if (callback(node)) continue callback;
}
for (tmp of node.querySelectorAll(selector)) {
if (callback(tmp)) continue callback;
}
}
}
}
}
// Creates a mutation observer to the target.
function createObserver(target) {
const observer = new MutationObserver(observerCallback);
observer.callbacks = new Set();
observer.observe(target, {characterData: true, childList: true, subtree: true});
return observer;
}
// Adds a callback that is involved on mutations to elements matching the selector in the
// target. If there isn't an observer to the target, a new one is 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 is involved on mutations in the target from the observers. If
// there would be no callback attached to the observer to the target, the 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 action callback can return true to remove itself.
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)) {
if (action(elm, false) === true) return;
memory.add(elm);
}
const callback = (elm) => {
if (memory.has(elm)) return;
if (action(elm, true) === true) {
removeCallback(parent, callback);
return true;
}
memory.add(elm);
};
addCallback(parent, callback, selector);
return () => {
memory = new WeakSet();
};
},
};
})();
function fetchInternalApi(endpoint, body) {
return fetch(
`https://www.youtube.com/youtubei/v1/${endpoint}?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8`,
{
method: 'POST',
body: JSON.stringify({
context: {client: {clientName: 'WEB', clientVersion: '2.20240411.01.00'}},
...body,
}),
}
)
.then((response) => response.json());
}
const getChannelId = (function () {
const channelIds = new Map();
return (url) => {
if (!channelIds.has(url)) {
// Fetching `/navigation/resolve_url` endpoint for channel IDs was introduced in 1.5.10.
// Testing shows this approach has a better performance (both time cost and data used) than
// requesting channel pages, even though it requests twice for each channel, while regex is
// slow and channel pages are huge.
channelIds.set(
url,
fetchInternalApi('navigation/resolve_url', {url})
.then((json) => {
if (json.endpoint.browseEndpoint) {
return json.endpoint.browseEndpoint.browseId;
} else {
// Workaround: Some channels such as @rayduenglish behave strange. Normally GETing
// channel pages result 303 and redirect to `/rayduenglish` for example; the internal
// API responses similarly, the workaround is to resolve twice. However, some are
// impossible to resolve correctly; for example, requesting `/@Konata` redirected to
// `/user/Konata`, and `/user/Konata` leads 404. This is probably a bug of YouTube.
return fetchInternalApi('navigation/resolve_url', json.endpoint.urlEndpoint)
.then((json) => json.endpoint.browseEndpoint.browseId);
}
})
);
}
return channelIds.get(url);
};
})();
const getChannelName = (function () {
const channelNames = new Map();
return (id) => {
if (!channelNames.has(id)) {
channelNames.set(
id,
fetchInternalApi('browse', {browseId: id})
.then((json) => json.metadata.channelMetadataRenderer.title)
);
}
return channelNames.get(id);
};
})();
const forgetters = [];
///// YouTube /////
// Mentions in titles.
const forgetTitleMentions = elmGetter.each('#title.ytd-watch-metadata a.yt-simple-endpoint', (elm) => {
if (elm.textContent.trim()[0] !== '@') return; // Skip hashtags.
getChannelName(elm.href.slice(elm.href.lastIndexOf('/') + 1))
.then((name) => void (elm.textContent = name));
});
document.addEventListener('yt-navigate-finish', forgetTitleMentions);
forgetters.push(
// Commenters.
elmGetter.each('#author-text:is(.ytd-comment-renderer, .ytd-comment-view-model):not([hidden])', (elm) => {
getChannelName(elm.data.browseEndpoint.browseId)
.then((name) => void (elm.firstElementChild.firstChild.textContent = name));
}),
elmGetter.each('#name.ytd-author-comment-badge-renderer', (elm) => {
getChannelName(elm.data.browseEndpoint.browseId)
.then((name) => void (elm.querySelector('#text').firstChild.textContent = name));
}),
// Mentions in comments.
elmGetter.each('#content-text:is(.ytd-comment-renderer, .ytd-comment-view-model) a', (elm) => {
if (elm.textContent.trim()[0] !== '@') return; // Skip anchors such as timestamps.
getChannelName(elm.href.slice(elm.href.lastIndexOf('/') + 1))
.then((name) => void (elm.textContent = `\xA0${name}\xA0`));
})
);
elmGetter.each('#primary #sections.ytd-comments', (elm) => {
elm.addEventListener('yt-service-request-sent', () => {
forgetters.forEach((f) => f());
});
});
elmGetter.each('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-comments-section"]', (elm) => {
elm.addEventListener('yt-service-request-sent', () => {
forgetters.forEach((f) => f());
});
});
///// YouTube Studio /////
forgetters.push(
elmGetter.each('#name.ytcp-comment:not([hidden]), #badge-name.ytcp-author-comment-badge', (elm) => {
getChannelId(elm.href)
.then(getChannelName)
.then((name) => void (elm.firstElementChild.firstChild.textContent = name));
})
);
elmGetter.each('#filter-bar.ytcp-comments-filter', (elm) => {
elm.addEventListener('ytcp-filter-bar-updated', () => {
forgetters.forEach((f) => f());
});
});
@lumynou5
Copy link
Author

lumynou5 commented Aug 3, 2023

安裝

在瀏覽器安裝 Tampermonkey 擴充套件後,點擊此頁面上的「Raw」按鈕,然後點擊「安裝」即可。


Install

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

Compatibility

Browser minimum requirements:

  • Chrome 80
  • Edge 80
  • Firefox 72
  • Safari 13.1

Userscript managers:

  • Greasemonkey
  • Tampermonkey
  • Violentmonkey

YouTube client versions:

  • 2.20240411.01.00
  • 2.20240416.01.00-canary_experiment_2.20240415.01.00
  • 2.20240416.01.00
  • 2.20240419.01.00
  • 2.20240430.06.00

YouTube Studio client versions:

  • 1.20240415.03.00

@lumynou5
Copy link
Author

lumynou5 commented Aug 6, 2023

Todos

  • Mentioned channels in video descriptions.
  • Use element.data.browseEndpoint.browseId to get channel IDs instead.
    Note: it doesn't work for YouTube Studio.
  • Names of commenters aren't replaced after editing.
  • Not all names of commenters are replaced after re-sorting.
    Because YouTube somehow falsely changes href's of some comments twice after re-sorting, thus those comments are memorized before YouTube changes the texts (even before YouTube really updates the href's.) Fixed in 1.5.14.
  • Not all names are replaced in the YouTube Studio Comment View when filters changed.
    Because commenters' href's won't be changed if the comments are the same one before filters changed. Fixed in 1.5.14.
  • Mentioned channels in video titles.

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