您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
To restore YouTube Username to the traditional custom name
当前为
- /*
- MIT License
- Copyright 2023 CY Fung
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
- // ==UserScript==
- // @name Restore YouTube Username from Handle to Custom
- // @namespace http://tampermonkey.net/
- // @version 0.3.1
- // @license MIT License
- // @description To restore YouTube Username to the traditional custom name
- // @description:ja YouTubeのユーザー名を伝統的なカスタム名に復元するために。
- // @author CY Fung
- // @match https://www.youtube.com/*
- // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
- // @icon https://github.com/cyfung1031/userscript-supports/raw/main/icons/general-icon.png
- // @supportURL https://github.com/cyfung1031/userscript-supports
- // @run-at document-start
- // @grant none
- // @unwrap
- // @allFrames
- // @inject-into page
- // ==/UserScript==
- /* jshint esversion:8 */
- (function () {
- 'use strict';
- const cfg = {};
- class Mutex {
- constructor() {
- this.p = Promise.resolve()
- }
- lockWith(f) {
- this.p = this.p.then(() => new Promise(f)).catch(console.warn)
- }
- }
- const mutex = new Mutex();
- const displayNameCacheStore = new Map();
- const promisesStore = {};
- function createNetworkPromise(channelId) {
- return new Promise(networkResolve => {
- mutex.lockWith(lockResolve => {
- if (!document.querySelector(`[jkrgy="${channelId}"]`)) {
- // element has already been removed
- lockResolve(null);
- }
- //INNERTUBE_API_KEY = ytcfg.data_.INNERTUBE_API_KEY
- fetch(new window.Request(`/youtubei/v1/browse?key=${cfg.INNERTUBE_API_KEY}&prettyPrint=false`, {
- "method": "POST",
- "mode": "same-origin",
- "credentials": "same-origin",
- // (-- reference: https://javascript.info/fetch-api
- referrerPolicy: "no-referrer",
- cache: "default",
- redirect: "error",
- integrity: "",
- keepalive: false,
- signal: undefined,
- window: window,
- // --)
- "headers": {
- "Content-Type": "application/json",
- "Accept-Encoding": "gzip, deflate, br"
- },
- "body": JSON.stringify({
- "context": {
- "client": {
- "clientName": "MWEB",
- "clientVersion": `${cfg.INNERTUBE_CLIENT_VERSION || '2.20230614.01.00'}`,
- "originalUrl": `https://m.youtube.com/channel/${channelId}`,
- "playerType": "UNIPLAYER",
- "platform": "MOBILE",
- "clientFormFactor": "SMALL_FORM_FACTOR",
- "acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
- "mainAppWebInfo": {
- "graftUrl": `/channel/${channelId}`,
- "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
- "isWebNativeShareAvailable": true
- }
- },
- "user": {
- "lockedSafetyMode": false
- },
- "request": {
- "useSsl": true,
- "internalExperimentFlags": [],
- "consistencyTokenJars": []
- }
- },
- "browseId": `${channelId}`
- })
- })).then(res => {
- lockResolve();
- return res.json();
- }).then(res => {
- networkResolve(res);
- }).catch(e => {
- lockResolve();
- console.warn(e);
- networkResolve(null);
- })
- });
- })
- }
- const queueMicrotask_ = typeof queueMicrotask === 'function' ? queueMicrotask : requestAnimationFrame;
- function getDisplayName(channelId) {
- return new Promise(resolve => {
- let cachedResult = displayNameCacheStore.get(channelId);
- if (cachedResult) {
- resolve(cachedResult);
- return;
- }
- if (!promisesStore[channelId]) promisesStore[channelId] = createNetworkPromise(channelId);
- promisesStore[channelId].then(res => {
- // res might be null
- queueMicrotask_(() => {
- promisesStore[channelId] = null;
- delete promisesStore[channelId];
- });
- let resultInfo = ((res || 0).metadata || 0).channelMetadataRenderer;
- if (!resultInfo) {
- resolve(null);
- } else {
- const { title, externalId, ownerUrls, channelUrl, vanityChannelUrl } = res.metadata.channelMetadataRenderer;
- const displayNameRes = { title, externalId, ownerUrls, channelUrl, vanityChannelUrl };
- displayNameCacheStore.set(channelId, displayNameRes);
- resolve(displayNameRes);
- }
- }).catch(console.warn);
- }).catch(console.warn);
- }
- const dataChangedFuncStore = new WeakMap();
- const obtainChannelId = (href) => {
- let m = /\/channel\/([^/?#\s]+)/.exec(`/${href}`);
- return !m ? '' : (m[1] || '');
- }
- const dataChangeFuncProducer = (dataChanged) => {
- return function () {
- let anchors = null;
- try {
- anchors = HTMLElement.prototype.querySelectorAll.call(this, 'a[id][href*="channel/"][jkrgy]');
- } catch (e) { }
- if ((anchors || 0).length >= 1 && (this.data || 0).jkrgx !== 1) {
- for (const anchor of anchors) {
- anchor.removeAttribute('jkrgy');
- }
- }
- return dataChanged.apply(this, arguments)
- }
- }
- const anchorIntegrityCheck = (anchor, channelHref, channelId) => {
- // https://www.youtube.com/channel/UCRmLncxsQFcOOC8OhzUIfxQ/videos /channel/UCRmLncxsQFcOOC8OhzUIfxQ UCRmLncxsQFcOOC8OhzUIfxQ
- let currentHref = anchor.getAttribute('href');
- if (currentHref === channelHref) return true; // /channel/UCRmLncxsQFcOOC8OhzUIfxQ // /channel/UCRmLncxsQFcOOC8OhzUIfxQ
- return (currentHref + '/').indexOf(channelHref + '/') >= 0;
- }
- const verifyAndConvertHandle = (currentDisplayed, fetchResult) => {
- const { title, externalId, ownerUrls, channelUrl, vanityChannelUrl } = fetchResult;
- const currentDisplayTrimmed = currentDisplayed.trim();
- let match = false;
- if ((vanityChannelUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
- match = true;
- } else if ((ownerUrls || 0).length >= 1) {
- for (const ownerUrl of ownerUrls) {
- if ((ownerUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
- match = true;
- break;
- }
- }
- }
- if (match) {
- return currentDisplayTrimmed;
- }
- return '';
- }
- const isDisplayAsHandle = (text) => {
- if (typeof text !== 'string') return false;
- if (text.length < 4) return false;
- if (text.indexOf('@') < 0) return false;
- return /^\s*@[a-zA-Z0-9_\-.]{3,30}\s*$/.test(text);
- /* https://support.google.com/youtube/answer/11585688?hl=en&co=GENIE.Platform%3DAndroid
- Handle naming guidelines
- Is between 3-30 characters
- Is made up of alphanumeric characters (A–Z, a–z, 0–9)
- Your handle can also include: underscores (_), hyphens (-), dots (.)
- Is not URL-like or phone number-like
- Is not already being used
- Follows YouTube's Community Guidelines
- // auto handle - without dot (.)
- */
- }
- const contentTextProcess = (contentTexts, idx) => {
- const contentText = contentTexts[idx];
- const text = (contentText || 0).text;
- const url = (((contentText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url;
- if (typeof url === 'string' && typeof text === 'string') {
- if (!isDisplayAsHandle(text)) return null;
- const channelId = obtainChannelId(url);
- return getDisplayName(channelId).then(fetchResult => {
- let resolveResult = null;
- if (fetchResult) {
- // note: if that user shown is not found in `a[id]`, the hyperlink would not change
- const textTrimmed = verifyAndConvertHandle(text, fetchResult);
- if (textTrimmed) {
- resolveResult = (md) => {
- let runs = ((md || 0).contentText || 0).runs;
- if (!runs || !runs[idx]) return;
- if (runs[idx].text !== text) return;
- runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
- md.contentText = Object.assign({}, md.contentText);
- };
- }
- }
- return (resolveResult); // function as a Promise
- });
- }
- return null;
- }
- const domCheck = async (anchor, channelHref, mt) => {
- try {
- if (!channelHref || !mt) return;
- let parentNode = anchor.parentNode;
- while (parentNode instanceof Node) {
- if (typeof parentNode.is === 'string' && typeof parentNode.dataChanged === 'function') break;
- parentNode = parentNode.parentNode
- }
- if (parentNode instanceof Node && typeof parentNode.is === 'string' && typeof parentNode.dataChanged === 'function') { } else return;
- const authorText = (parentNode.data || 0).authorText;
- const currentDisplayed = (authorText || 0).simpleText;
- if (typeof currentDisplayed !== 'string') return;
- if (!isDisplayAsHandle(currentDisplayed)) return;
- const oldDataChanged = parentNode.dataChanged;
- if (typeof oldDataChanged === 'function' && !oldDataChanged.jkrgx) {
- let newDataChanged = dataChangedFuncStore.get(oldDataChanged)
- if (!newDataChanged) {
- newDataChanged = dataChangeFuncProducer(oldDataChanged);
- newDataChanged.jkrgx = 1;
- dataChangedFuncStore.set(oldDataChanged, newDataChanged);
- }
- parentNode.dataChanged = newDataChanged;
- }
- const fetchResult = await getDisplayName(mt);
- if (fetchResult === null) return;
- const { title, externalId, ownerUrls, channelUrl, vanityChannelUrl } = fetchResult;
- if (externalId !== mt) return; // channel id must be the same
- // anchor href might be changed by external
- if (!anchorIntegrityCheck(anchor, channelHref, externalId)) return;
- const parentNodeData = parentNode.data
- const funcPromises = [];
- if (parentNode.isAttached === true && parentNode.isConnected === true && typeof parentNodeData === 'object' && parentNodeData && parentNodeData.authorText === authorText) {
- if (authorText.simpleText !== currentDisplayed) return;
- const currentDisplayTrimmed = verifyAndConvertHandle(currentDisplayed, fetchResult);
- const cSimpleText = ((parentNodeData.authorText || 0).simpleText || '');
- if (currentDisplayTrimmed && currentDisplayed !== title && cSimpleText === currentDisplayed) {
- // the inside hyperlinks will be only converted if its parent author name is handle
- const contentTexts = (parentNodeData.contentText || 0).runs;
- if (contentTexts && contentTexts.length >= 1) {
- for (let aidx = 0; aidx < contentTexts.length; aidx++) {
- const r = contentTextProcess(contentTexts, aidx);
- if (r instanceof Promise) funcPromises.push(r);
- }
- }
- const md = Object.assign({}, parentNodeData);
- let setBadge = false;
- if (((((md.authorCommentBadge || 0).authorCommentBadgeRenderer || 0).authorText || 0).simpleText || '').trim() === cSimpleText.trim()) {
- setBadge = true;
- }
- // parentNode.data = Object.assign({}, { jkrgx: 1 });
- md.authorText = Object.assign({}, md.authorText, { simpleText: currentDisplayed.replace(currentDisplayTrimmed, title) });
- if (setBadge) {
- md.authorCommentBadge = Object.assign({}, md.authorCommentBadge);
- md.authorCommentBadge.authorCommentBadgeRenderer = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer);
- md.authorCommentBadge.authorCommentBadgeRenderer.authorText = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer.authorText, { simpleText: title });
- }
- if (funcPromises.length >= 1) {
- let funcs = await Promise.all(funcPromises);
- for (const func of funcs) {
- if (typeof func === 'function') {
- func(md);
- }
- }
- }
- parentNode.data = Object.assign({}, md, { jkrgx: 1 });
- }
- }
- } catch (e) {
- console.warn(e);
- }
- }
- const domChecker = () => {
- const newAnchors = document.querySelectorAll('a[id][href*="channel/"]:not([jkrgy])');
- for (const anchor of newAnchors) {
- // author-text or name
- // normal url: /channel/xxxxxxx
- // Improve YouTube! - https://www.youtube.com/channel/xxxxxxx/videos
- const href = anchor.getAttribute('href');
- const channelId = obtainChannelId(href); // string, can be empty
- anchor.setAttribute('jkrgy', channelId);
- domCheck(anchor, href, channelId);
- }
- };
- /** @type {MutationObserver | null} */
- let domObserver = null;
- document.addEventListener('yt-page-data-fetched', function (evt) {
- const cfgData = (((window || 0).ytcfg || 0).data_ || 0);
- for (const key of ['INNERTUBE_API_KEY', 'INNERTUBE_CLIENT_VERSION']) {
- cfg[key] = cfgData[key];
- }
- if (!cfg['INNERTUBE_API_KEY']) {
- console.warn("Userscript Error: INNERTUBE_API_KEY is not found.");
- return;
- }
- if (!domObserver) {
- domObserver = new MutationObserver(domChecker);
- } else {
- domObserver.takeRecords();
- domObserver.disconnect();
- }
- domObserver.observe(evt.target || document.body, { childList: true, subtree: true });
- domChecker();
- });
- })();