YtDLS: Youtube 双语字幕(改)

为YouTube添加双语字幕增强功能。

  1. // ==UserScript==
  2. // @name YtDLS: YouTube Dual Language Subtitle (Modified)
  3. // @name:zh-CN YtDLS: Youtube 双语字幕(改)
  4. // @name:zh-TW YtDLS: Youtube 雙語字幕(改)
  5. // @version 2.1.4
  6. // @description Enhances YouTube with dual language subtitles.
  7. // @description:zh-CN 为YouTube添加双语字幕增强功能。
  8. // @description:zh-TW 增強YouTube的雙語字幕功能。
  9. // @author CY Fung
  10. // @author Coink Wang
  11. // @match https://www.youtube.com/*
  12. // @match https://m.youtube.com/*
  13. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini|webp|webm)[^\/]*$/
  14. // @exclude /^https?://\S+_live_chat*$/
  15. // @require https://cdn.jsdelivr.net/gh/culefa/xhProxy@eaa2e84b40290fc63af1ca777f3f545008bf79bb/dist/xhProxy.min.js
  16. // @grant none
  17. // @inject-into page
  18. // @allFrames true
  19. // @run-at document-start
  20. // @namespace Y2BDoubleSubs
  21. // @license MIT
  22. // @supportURL https://github.com/cyfung1031/Y2BDoubleSubs/tree/translation-api
  23. // ==/UserScript==
  24.  
  25. /* global xhProxy */
  26.  
  27. /*
  28.  
  29. original script: https://gf.qytechs.cn/scripts/397363
  30. based on v1.8.0 + PR#18 ( https://github.com/CoinkWang/Y2BDoubleSubs/pull/18 ) [v2.0.0]
  31. added m.youtube.com support based on two scripts (https://gf.qytechs.cn/scripts/457476 & https://gf.qytechs.cn/scripts/464879 ) which are fork from v1.8.0
  32.  
  33. */
  34.  
  35.  
  36.  
  37.  
  38. (() => {
  39.  
  40. let localeLangFn = () => document.documentElement.lang || navigator.language || 'en' // follow the language used in YouTube Page
  41. // localeLangFn = () => 'zh' // uncomment this line to define the language you wish here
  42. function isValidForHook() {
  43. try {
  44. if (location.pathname === '/live_chat' || location.pathname === '/live_chat_replay') return false;
  45. return true;
  46. } catch (e) {
  47. return false;
  48. }
  49. }
  50. if (!isValidForHook()) return;
  51. const Promise = (async () => { })().constructor;
  52. const fetch = window.fetch.bind(window)
  53. let enableFullWidthSpaceSeparation = true
  54. function encodeFullwidthSpace(text) {
  55. if (!enableFullWidthSpaceSeparation) return text
  56. return text.replace(/\n/g, '\n®\n').replace(/\u3000/g, '\n©\n')
  57. }
  58. function decodeFullwidthSpace(text) {
  59. if (!enableFullWidthSpaceSeparation) return text
  60. return text.replace(/\n©\n/g, '\u3000').replace(/\n®\n/g, '\n')
  61. }
  62. let requestDeferred = Promise.resolve();
  63.  
  64. const inPlaceArrayPush = (() => {
  65. // for details, see userscript-supports/library/misc.js
  66. const LIMIT_N = typeof AbortSignal !== 'undefined' && typeof (AbortSignal||0).timeout === 'function' ? 50000 : 10000;
  67. return function (dest, source) {
  68. let index = 0;
  69. const len = source.length;
  70. while (index < len) {
  71. let chunkSize = len - index; // chunkSize > 0
  72. if (chunkSize > LIMIT_N) {
  73. chunkSize = LIMIT_N;
  74. dest.push(...source.slice(index, index + chunkSize));
  75. } else if (index > 0) { // to the end
  76. dest.push(...source.slice(index));
  77. } else { // normal push.apply
  78. dest.push(...source);
  79. }
  80. index += chunkSize;
  81. }
  82. }
  83.  
  84. })();
  85. xhProxy.hook({
  86. onConfig(xhr, config) {
  87. const originalReqUrl = config.url;
  88. if (typeof ytcfg !== 'object' || !originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')){
  89. this.byPassRequest = true;
  90. }
  91. // config.byPassRequest = true;
  92. // console.log(xhr, config)
  93. },
  94. onRequest(xhr, config) {
  95. // console.log(xhr, config)
  96. },
  97. async onResponse(xhr, config) {
  98. const o = {}
  99. try {
  100. const originalReqUrl = config.url;
  101. if (!originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')) return;
  102. if (typeof ytcfg !== 'object') return; // not a valid youtube page
  103. let defaultJson = null
  104. const jsonResponse = xhr.xhJson;
  105. if (jsonResponse && jsonResponse.events) defaultJson = jsonResponse;
  106. if (defaultJson === null) return;
  107. const localeLang = localeLangFn()
  108. const langIdx = originalReqUrl.indexOf('lang=')
  109. if (langIdx > 5) {
  110. // &key=yt8&lang=en&fmt=json3&xorb=2&xobt=3&xovt=3
  111. // &key=yt8&lang=ja&fmt=json3&xorb=2&xobt=3&xovt=3
  112. // &key=yt8&lang=ja&name=Romaji&fmt=json3&xorb=2&xobt=3
  113. let ulc = originalReqUrl.charAt(langIdx - 1)
  114. if (ulc === '?' || ulc === '&') {
  115. let usp = new URLSearchParams(originalReqUrl.substring(langIdx))
  116. let uspLang = usp.get('lang')
  117. let uspName = usp.get('name')
  118. if (uspName === 'Romaji') return defaultAction()
  119. if (typeof uspLang === 'string' && uspLang.toLocaleLowerCase() === localeLang.toLocaleLowerCase()) return;
  120. }
  121. }
  122. const lines = []
  123. for (const event of defaultJson.events) {
  124. for (const seg of event.segs) {
  125. if (seg && typeof seg.utf8 === 'string') {
  126. inPlaceArrayPush(lines, seg.utf8.split('\n'));
  127. }
  128. }
  129. }
  130. if (lines.length === 0) return defaultAction()
  131. let linesText = lines.join('\n')
  132. linesText = encodeFullwidthSpace(linesText)
  133. const q = encodeURIComponent(linesText)
  134. o.defaultJson = defaultJson
  135. o.lines = lines
  136. o.requestURL = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${localeLang}&dj=1&dt=t&dt=rm&q=${q}`
  137. } catch (e) {
  138. console.warn(e)
  139. return;
  140. }
  141. return new Promise(xhrResolve => {
  142. function fetchData() {
  143. return new Promise(requestDeferredResolve => {
  144. fetch(o.requestURL, {
  145. method: "GET",
  146. headers: {
  147. "Accept": "application/json",
  148. "Accept-Encoding": "gzip, deflate, br"
  149. },
  150. credentials: "omit",
  151. referrerPolicy: "no-referrer",
  152. redirect: "error",
  153. keepalive: false,
  154. cache: "default"
  155. })
  156. .then(res => {
  157. requestDeferredResolve()
  158. return res.json()
  159. })
  160. .then(result => {
  161. let resultText = result.sentences.map((function (s) {
  162. return "trans" in s ? s.trans : ""
  163. })).join("")
  164. resultText = decodeFullwidthSpace(resultText)
  165. return resultText.split("\n")
  166. })
  167. .then(translatedLines => {
  168. const { lines, defaultJson } = o
  169. o.lines = null
  170. o.defaultJson = null
  171. const addTranslation = (line, idx) => {
  172. if (line !== lines[i + idx]) return line
  173. let translated = translatedLines[i + idx]
  174. if (line === translated) return line
  175. return `${line}\n${translated}`
  176. }
  177. let i = 0
  178. for (const event of defaultJson.events) {
  179. for (const seg of event.segs) {
  180. if (seg && typeof seg.utf8 === 'string') {
  181. let s = seg.utf8.split('\n')
  182. let st = s.map(addTranslation)
  183. seg.utf8 = st.join('\n')
  184. i += s.length
  185. }
  186. }
  187. }
  188. xhr.xhJson = defaultJson;
  189. xhrResolve()
  190. }).catch(e => {
  191. console.warn(e)
  192. xhrResolve()
  193. })
  194. })
  195. }
  196. requestDeferred = requestDeferred.then(fetchData)
  197. })
  198. }
  199. })
  200. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址