您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为YouTube添加双语字幕增强功能。
// ==UserScript== // @name YtDLS: YouTube Dual Language Subtitle (Modified) // @name:zh-CN YtDLS: Youtube 双语字幕(改) // @name:zh-TW YtDLS: Youtube 雙語字幕(改) // @version 2.1.4 // @description Enhances YouTube with dual language subtitles. // @description:zh-CN 为YouTube添加双语字幕增强功能。 // @description:zh-TW 增強YouTube的雙語字幕功能。 // @author CY Fung // @author Coink Wang // @match https://www.youtube.com/* // @match https://m.youtube.com/* // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini|webp|webm)[^\/]*$/ // @exclude /^https?://\S+_live_chat*$/ // @require https://cdn.jsdelivr.net/gh/culefa/xhProxy@eaa2e84b40290fc63af1ca777f3f545008bf79bb/dist/xhProxy.min.js // @grant none // @inject-into page // @allFrames true // @run-at document-start // @namespace Y2BDoubleSubs // @license MIT // @supportURL https://github.com/cyfung1031/Y2BDoubleSubs/tree/translation-api // ==/UserScript== /* global xhProxy */ /* original script: https://greasyfork.runtimutd.eu.org/scripts/397363 based on v1.8.0 + PR#18 ( https://github.com/CoinkWang/Y2BDoubleSubs/pull/18 ) [v2.0.0] added m.youtube.com support based on two scripts (https://greasyfork.runtimutd.eu.org/scripts/457476 & https://greasyfork.runtimutd.eu.org/scripts/464879 ) which are fork from v1.8.0 */ (() => { let localeLangFn = () => document.documentElement.lang || navigator.language || 'en' // follow the language used in YouTube Page // localeLangFn = () => 'zh' // uncomment this line to define the language you wish here function isValidForHook() { try { if (location.pathname === '/live_chat' || location.pathname === '/live_chat_replay') return false; return true; } catch (e) { return false; } } if (!isValidForHook()) return; const Promise = (async () => { })().constructor; const fetch = window.fetch.bind(window) let enableFullWidthSpaceSeparation = true function encodeFullwidthSpace(text) { if (!enableFullWidthSpaceSeparation) return text return text.replace(/\n/g, '\n®\n').replace(/\u3000/g, '\n©\n') } function decodeFullwidthSpace(text) { if (!enableFullWidthSpaceSeparation) return text return text.replace(/\n©\n/g, '\u3000').replace(/\n®\n/g, '\n') } let requestDeferred = Promise.resolve(); const inPlaceArrayPush = (() => { // for details, see userscript-supports/library/misc.js const LIMIT_N = typeof AbortSignal !== 'undefined' && typeof (AbortSignal||0).timeout === 'function' ? 50000 : 10000; return function (dest, source) { let index = 0; const len = source.length; while (index < len) { let chunkSize = len - index; // chunkSize > 0 if (chunkSize > LIMIT_N) { chunkSize = LIMIT_N; dest.push(...source.slice(index, index + chunkSize)); } else if (index > 0) { // to the end dest.push(...source.slice(index)); } else { // normal push.apply dest.push(...source); } index += chunkSize; } } })(); xhProxy.hook({ onConfig(xhr, config) { const originalReqUrl = config.url; if (typeof ytcfg !== 'object' || !originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')){ this.byPassRequest = true; } // config.byPassRequest = true; // console.log(xhr, config) }, onRequest(xhr, config) { // console.log(xhr, config) }, async onResponse(xhr, config) { const o = {} try { const originalReqUrl = config.url; if (!originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')) return; if (typeof ytcfg !== 'object') return; // not a valid youtube page let defaultJson = null const jsonResponse = xhr.xhJson; if (jsonResponse && jsonResponse.events) defaultJson = jsonResponse; if (defaultJson === null) return; const localeLang = localeLangFn() const langIdx = originalReqUrl.indexOf('lang=') if (langIdx > 5) { // &key=yt8&lang=en&fmt=json3&xorb=2&xobt=3&xovt=3 // &key=yt8&lang=ja&fmt=json3&xorb=2&xobt=3&xovt=3 // &key=yt8&lang=ja&name=Romaji&fmt=json3&xorb=2&xobt=3 let ulc = originalReqUrl.charAt(langIdx - 1) if (ulc === '?' || ulc === '&') { let usp = new URLSearchParams(originalReqUrl.substring(langIdx)) let uspLang = usp.get('lang') let uspName = usp.get('name') if (uspName === 'Romaji') return defaultAction() if (typeof uspLang === 'string' && uspLang.toLocaleLowerCase() === localeLang.toLocaleLowerCase()) return; } } const lines = [] for (const event of defaultJson.events) { for (const seg of event.segs) { if (seg && typeof seg.utf8 === 'string') { inPlaceArrayPush(lines, seg.utf8.split('\n')); } } } if (lines.length === 0) return defaultAction() let linesText = lines.join('\n') linesText = encodeFullwidthSpace(linesText) const q = encodeURIComponent(linesText) o.defaultJson = defaultJson o.lines = lines o.requestURL = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${localeLang}&dj=1&dt=t&dt=rm&q=${q}` } catch (e) { console.warn(e) return; } return new Promise(xhrResolve => { function fetchData() { return new Promise(requestDeferredResolve => { fetch(o.requestURL, { method: "GET", headers: { "Accept": "application/json", "Accept-Encoding": "gzip, deflate, br" }, credentials: "omit", referrerPolicy: "no-referrer", redirect: "error", keepalive: false, cache: "default" }) .then(res => { requestDeferredResolve() return res.json() }) .then(result => { let resultText = result.sentences.map((function (s) { return "trans" in s ? s.trans : "" })).join("") resultText = decodeFullwidthSpace(resultText) return resultText.split("\n") }) .then(translatedLines => { const { lines, defaultJson } = o o.lines = null o.defaultJson = null const addTranslation = (line, idx) => { if (line !== lines[i + idx]) return line let translated = translatedLines[i + idx] if (line === translated) return line return `${line}\n${translated}` } let i = 0 for (const event of defaultJson.events) { for (const seg of event.segs) { if (seg && typeof seg.utf8 === 'string') { let s = seg.utf8.split('\n') let st = s.map(addTranslation) seg.utf8 = st.join('\n') i += s.length } } } xhr.xhJson = defaultJson; xhrResolve() }).catch(e => { console.warn(e) xhrResolve() }) }) } requestDeferred = requestDeferred.then(fetchData) }) } }) })();