// ==UserScript== // @name YouTube 直播聊天实时翻译 // @version 1.3 // @author lslqtz // @license GPL // @grant GM.xmlHttpRequest // @inject-into content // @run-at document-end // @match *://*.youtube.com/live_chat* // @namespace https://greasyfork.org/users/155581 // @description Same as the name // @downloadURL none // ==/UserScript== window.YTGMFetch = function (url, options = {}) { return new Promise((resolve, reject) => { const method = options.method || 'GET'; const headers = options.headers || {}; const body = options.body || null; GM.xmlHttpRequest({ method: method, url: url, headers: headers, data: body, responseType: options.responseType || 'text', onload: function (response) { const fetchResponse = { ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText, url: response.finalUrl, text: () => Promise.resolve(response.responseText), json: () => Promise.resolve(JSON.parse(response.responseText)), blob: () => Promise.resolve(new Blob([response.response])), arrayBuffer: () => Promise.resolve(response.response), headers: { get: (header) => { const headersArray = response.responseHeaders.split('\r\n'); const headerMap = headersArray.reduce((acc, curr) => { const [key, value] = curr.split(': '); if (key && value) acc[key.toLowerCase()] = value; return acc; }, {}); return headerMap[header.toLowerCase()] || null; }, }, }; resolve(fetchResponse); }, onerror: function () { reject(new TypeError('Network request failed.')); }, ontimeout: function () { reject(new TypeError('Network request timed out.')); }, onabort: function () { reject(new DOMException('The operation was aborted.', 'AbortError')); }, }); }); }; window.ytTextEncoder = new TextEncoder(); window.ytIsSolvingCaptcha = false; window.ytSymbolRegex = /^([\u{2190}-\u{21FF}]|[\u{2300}-\u{23FF}]|[\u{2460}-\u{24FF}]|[\u{2500}-\u{25FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{2B00}-\u{2BFF}]|[\u{1F000}-\u{1F02F}]|[\u{1F0A0}-\u{1F0FF}]|[\u{1F100}-\u{1F6FF}]|[\u{1F900}-\u{1FAFF}]|[\u{1FB00}-\u{1FBFF}]|[ !"#$%&'()*+,-./:;<=>?@\[\\\]^_`\{|\}~。,“”、:;?!〝〞〟~〜])+$/u; // 停止翻译脚本. function StopYouTubeLiveChatTranslator() { console.log("停止 YouTube 直播聊天翻译脚本"); if (window.ytLiveChatInterval) { clearInterval(window.ytLiveChatInterval); console.log("已清除计时器"); } if (window.ytObserver) { window.ytObserver.disconnect(); window.ytObserver = null; console.log("已清除观察器"); } window.chatContainerNotFoundCount = 0; window.ytIsSolvingCaptcha = false; } // 启动翻译脚本. function StartYouTubeLiveChatTranslator() { console.log("启动 YouTube 直播聊天翻译脚本"); // 启动定时器. window.ytLiveChatInterval = setInterval(CheckAndObserveChatContainer, 1000); } function ShowReCaptchaPrompt(onResolved) { if (window.ytIsSolvingCaptcha) { return; } window.ytIsSolvingCaptcha = true; // 创建提示框 HTML. var promptBox = document.createElement("div"); promptBox.style = ` position: fixed; top: 20%; left: 50%; transform: translateX(-50%); padding: 5px; width: 70%; background: white; border: 1px solid gray; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 10000; text-align: center; font-size: 16px; `; promptBox.innerHTML = `

YouTube 直播聊天实时翻译

HTTP 429: 检测到疑似频繁请求触发 reCaptcha 验证码验证.

手动访问 Google Translate API 并完成验证码验证.

完成验证后, 点按"我已完成", 即可继续开始翻译.

`; document.body.appendChild(promptBox); document.getElementById("ytRecaptchaResolvedButton").addEventListener("click", () => { promptBox.remove(); window.ytIsSolvingCaptcha = false; }); document.getElementById("ytDisableTranslation").addEventListener("click", () => { promptBox.remove(); StopYouTubeLiveChatTranslator(); }); } // Google Translate API 调用. async function TranslateText(text, targetLang = 'zh-CN') { console.log("翻译消息: " + text); if (window.ytIsSolvingCaptcha) { return "[等待解决验证码]"; } try { var response = await YTGMFetch('https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=' + targetLang + '&dt=t&q=' + encodeURIComponent(text)); if (!response.ok) { console.error("翻译 API 请求失败", response.status, response.statusText); if (response.status === 429) { ShowReCaptchaPrompt(); return "[翻译失败, 疑似频繁请求触发 reCaptcha 验证码]"; } return "[翻译失败]"; } var result = await response.json(); return result[0][0][0]; } catch (error) { console.error("翻译出错", error); return "[翻译错误]"; } } // 解析消息内容, 过滤表情和图片, 并将它们替换为占位符. function ExtractTextContent(element) { var text = ''; var elements = element.childNodes; var placeholders = []; // 存储占位符与原始内容的对应关系. var hasText = false; elements.forEach(function (node, index) { if (node.nodeType === Node.TEXT_NODE) { if (node.nodeValue.trim().split(/\r?\n/).every(line => window.ytSymbolRegex.test(line))) { return; } if (!hasText) { hasText = true; } text += node.nodeValue.trim(); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName.toLowerCase() === 'img' || (node.tagName.toLowerCase() === 'span' && node.classList.contains('emoji'))) { var placeholder = `{{ytPH${index}}}`; // 使用占位符. placeholders.push({ placeholder: placeholder, html: node.outerHTML }); text += placeholder; // 将表情或图片替换为占位符. } } }); if (!hasText || (text.trim().length <= 2 && window.ytTextEncoder.encode(text.trim()).length <= 2)) { return { text: "", placeholders: [] }; } return { text: text.trim(), placeholders: placeholders }; } // 重新将占位符替换为表情和图片. function InsertPlaceholdersIntoTranslation(translatedMessage, placeholders) { translatedMessage = translatedMessage.replace('{ {', '{{').replace('} }', '}}'); placeholders.forEach(function (placeholder) { translatedMessage = translatedMessage.replace(placeholder.placeholder, placeholder.html); }); return translatedMessage; } // 插入翻译后的消息. function InsertTranslatedMessage(messageElement, translatedMessage) { var translationElement = document.createElement('div'); translationElement.style.color = 'gray'; translationElement.style.fontSize = 'small'; translationElement.className = 'translated-message'; translationElement.innerHTML = "[翻译]: " + translatedMessage; // 在原消息下方插入翻译内容. messageElement.appendChild(translationElement); } // 检查并观察聊天容器. function CheckAndObserveChatContainer() { var chatContainer = document.querySelector('#live-chat-item-list-panel'); if (chatContainer) { if (!window.ytObserver) { console.log("聊天容器找到: ", chatContainer); ObserveChatUpdates(chatContainer); TranslateInitialMessages(chatContainer); } } else if (window.ytObserver) { if (window.chatContainerNotFoundCount++ >= 3) { window.chatContainerNotFoundCount = 0; window.ytObserver.disconnect(); window.ytObserver = null; console.log("聊天容器已丢失, 停止以前监听的聊天更新"); } } } // 翻译已有的最新 20 条消息. async function TranslateInitialMessages(chatContainer) { console.log("开始翻译已有消息..."); // 获取所有聊天消息节点. var messages = chatContainer.querySelectorAll('yt-live-chat-text-message-renderer, yt-live-chat-paid-message-renderer'); var totalMessages = messages.length; // 只处理最后的 20 条消息. for (var i = Math.max(0, totalMessages - 20); i < totalMessages; i++) { var messageNode = messages[i]; var messageElement = messageNode.querySelector('#message'); if (messageElement) { // 跳过已翻译消息. if (messageElement.querySelector('.translated-message')) { console.log("消息已翻译,跳过: ", messageElement.textContent); continue; } var { text, placeholders } = ExtractTextContent(messageElement); if (text.length === 0) { console.log("已有消息内容为空,跳过翻译"); continue; } console.log("已有消息: " + text); var translatedMessage = await TranslateText(text); var finalMessage = InsertPlaceholdersIntoTranslation(translatedMessage, placeholders); InsertTranslatedMessage(messageElement, finalMessage); } } } // 监听聊天消息更新. function ObserveChatUpdates(chatContainer) { window.ytObserver = new MutationObserver(async function (mutations) { for (var i = 0; i < mutations.length; i++) { var mutation = mutations[i]; mutation.addedNodes.forEach(async function (node) { // 检查是否为聊天消息. if (node.nodeType === 1 && (node.tagName.toLowerCase() === 'yt-live-chat-text-message-renderer' || node.tagName.toLowerCase() === 'yt-live-chat-paid-message-renderer')) { var messageElement = node.querySelector('#message'); if (messageElement) { // 跳过已翻译消息. if (messageElement.querySelector('.translated-message')) { console.log("消息已翻译,跳过: ", messageElement.textContent); return; } // 提取文本并替换表情或图片为占位符. var { text, placeholders } = ExtractTextContent(messageElement); if (text.length === 0) { console.log("消息内容为空, 跳过翻译"); return; } console.log("检测到新消息: " + text); var translatedMessage = await TranslateText(text); // 将翻译后的文本和表情或图片组合. var finalMessage = InsertPlaceholdersIntoTranslation(translatedMessage, placeholders); InsertTranslatedMessage(messageElement, finalMessage); } else { console.warn("未找到消息内容元素"); } } }); } }); console.log("开始监听聊天更新..."); window.ytObserver.observe(chatContainer, { childList: true, subtree: true }); } StopYouTubeLiveChatTranslator(); StartYouTubeLiveChatTranslator();