Greasy Fork is available in English.
在YouTube的"详细统计信息"中,将连接速度转换为MB/s,并将面板主要术语汉化。
// ==UserScript==
// @name YouTube 网速单位转换器 & 统计信息汉化
// @namespace http://tampermonkey.net/
// @version 5.1
// @description 在YouTube的"详细统计信息"中,将连接速度转换为MB/s,并将面板主要术语汉化。
// @author BlingCc & cxary & Refined for Mobile & Translation
// @match *://www.youtube.com/*
// @match *://m.youtube.com/*
// @match *://youtube.com/*
// @grant none
// @run-at document-start
// @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 配置项 ---
const CONVERTED_VALUE_ID = 'yt-speed-converter-mbps-display';
const CONVERTED_VALUE_COLOR = '#42a5f5'; // Material Design Blue
// --- 汉化字典 ---
const TRANSLATIONS = {
'Video ID / sCPN': '视频 ID / 会话标识',
'Viewport / Frames': '视窗口 / 丢帧统计',
'Current / Optimal Res': '当前 / 最佳分辨率',
'Volume / Normalized': '音量 / 响度标准化',
'Codecs': '视频 / 音频编码格式',
'Color': '色彩特性',
'Connection Speed': '连接速度',
'Network Activity': '网络活动',
'Buffer Health': '缓冲时长',
'Live Latency': '直播延迟',
'Live Mode': '直播模式',
'Mystery Text': '开发调试参数', // YouTube 工程师留下的彩蛋
'Date': '日期',
'Audio / Video': '音频 / 视频',
'Protected': '受保护内容'
};
/**
* 将 Kbps 字符串转换为 MB/s 字符串
*/
function convertKbpsToMBps(kbpsString) {
const kbps = parseInt(kbpsString.replace(/[^0-9]/g, ''), 10);
if (isNaN(kbps)) return null;
const mbps = kbps / 8 / 1024;
return mbps.toFixed(2);
}
/**
* 核心函数:更新 MB/s 显示
*/
function updateSpeedDisplay(speedValueSpan) {
if (!speedValueSpan) return;
const originalText = speedValueSpan.textContent;
if (!/\d/.test(originalText)) return;
const mbpsValue = convertKbpsToMBps(originalText);
if (mbpsValue === null) return;
let displayEl = document.getElementById(CONVERTED_VALUE_ID);
if (!displayEl) {
displayEl = document.createElement('span');
displayEl.id = CONVERTED_VALUE_ID;
displayEl.style.marginLeft = '8px';
displayEl.style.color = CONVERTED_VALUE_COLOR;
displayEl.style.fontWeight = 'bold';
displayEl.style.whiteSpace = 'nowrap';
// 确保其在父级元素中显示
if (speedValueSpan.parentElement && speedValueSpan.parentElement.parentElement) {
speedValueSpan.parentElement.parentElement.appendChild(displayEl);
}
}
displayEl.textContent = `${mbpsValue} MB/s`;
}
/**
* 汉化面板文本
* 遍历面板内的所有文本节点,如果匹配字典则替换
*/
function localizePanel(panelNode) {
// 使用 TreeWalker 高效遍历文本节点
const walker = document.createTreeWalker(
panelNode,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.nodeValue.trim();
// 检查是否有匹配的翻译项
for (const [english, chinese] of Object.entries(TRANSLATIONS)) {
// 这里使用 startsWith 或者完全匹配,防止误伤数值
// 很多时候 Label 和 Value 是分开的节点,所以全等匹配较安全
// 但有时候 Label 包含空格,所以用 include 判断 Label 部分
if (text === english || text.startsWith(english + ' ')) {
node.nodeValue = node.nodeValue.replace(english, chinese);
break;
}
}
}
}
/**
* 设置网速观察者
*/
function setupSpeedObserver(panelNode) {
// 1. 先执行一次汉化
localizePanel(panelNode);
// 2. 定位“连接速度”节点 (支持中英文,因为我们刚才可能已经汉化了)
const labelDivXpath = ".//div[contains(text(), 'Connection Speed') or contains(text(), '连接速度')]";
const labelDiv = document.evaluate(labelDivXpath, panelNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (!labelDiv || !labelDiv.nextElementSibling) return;
const speedValueSpan = labelDiv.nextElementSibling.querySelector('span:nth-child(2)') || labelDiv.nextElementSibling.querySelector('span');
if (speedValueSpan) {
updateSpeedDisplay(speedValueSpan);
const speedObserver = new MutationObserver(() => {
updateSpeedDisplay(speedValueSpan);
// 可选:如果担心YouTube动态刷新导致汉化失效,可以在这里再次调用 localizePanel(panelNode);
// 但通常Label是静态的,只有Value变动。
});
speedObserver.observe(speedValueSpan, {
characterData: true,
childList: true,
subtree: true
});
panelNode.speedObserver = speedObserver;
}
}
/**
* 主观察者:监视面板出现
*/
function setupMainObserver() {
const targetNode = document.getElementById('movie_player') || document.getElementById('player') || document.body;
if (!targetNode) {
setTimeout(setupMainObserver, 500);
return;
}
const mainObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.classList.contains('html5-video-info-panel')) {
setupSpeedObserver(node);
}
else if (node.querySelector && node.querySelector('.html5-video-info-panel')) {
setupSpeedObserver(node.querySelector('.html5-video-info-panel'));
}
}
});
}
if (mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach(node => {
if (node.nodeType === 1) {
let panel = null;
if (node.classList.contains('html5-video-info-panel')) {
panel = node;
} else if (node.querySelector) {
panel = node.querySelector('.html5-video-info-panel');
}
if (panel) {
if (panel.speedObserver) {
panel.speedObserver.disconnect();
}
const displayEl = document.getElementById(CONVERTED_VALUE_ID);
if(displayEl) displayEl.remove();
}
}
});
}
}
});
mainObserver.observe(targetNode, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupMainObserver);
} else {
setupMainObserver();
}
})();