Greasy Fork

YtDLS: Youtube 双语字幕(改)

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

目前为 2023-08-16 提交的版本。查看 最新版本

// ==UserScript==
// @name                YtDLS: YouTube Dual Language Subtitle (Modified)
// @name:zh-CN          YtDLS: Youtube 双语字幕(改)
// @name:zh-TW          YtDLS: Youtube 雙語字幕(改)
// @version             2.0.7
// @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)[^\/]*$/
// @require             https://cdn.jsdelivr.net/gh/cyfung1031/ajax-hook@813bba9059da84e18219d8b9a4f8fe9c7f59a77f/dist/ajaxhook.min.js
// @grant               none
// @unwrap
// @allFrames           true
// @inject-into         page
// @run-at              document-start
// @namespace           Y2BDoubleSubs
// @license             MIT
// @supportURL          https://github.com/cyfung1031/Y2BDoubleSubs/tree/translation-api
// ==/UserScript==

/* global ah */

/* 

original script: https://greasyfork.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.org/scripts/457476 & https://greasyfork.org/scripts/464879 ) which are fork from v1.8.0

*/

(function () {
    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 _ah = ah
    const fetch = window.fetch.bind(window)
    const pReady = Promise.resolve()
    pReady.then(() => {

        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()
        _ah.proxy({
            onRequest: (config, handler) => {
                handler.next(config)
            },
            onResponse: (response, handler) => {
                function defaultAction() {
                    handler.resolve(response)
                }
                const o = {}
                try {

                    /** @type {string} */
                    const originalReqUrl = response.config.url
                    if (!originalReqUrl.includes('/api/timedtext') || originalReqUrl.includes('&translate_h00ked')) return defaultAction()
                    if (typeof ytcfg !== 'object') return defaultAction() // not a valid youtube page
                    let defaultJson = null
                    if (response.response) {
                        const jsonResponse = JSON.parse(response.response)
                        if (jsonResponse.events) defaultJson = jsonResponse
                    }
                    if (defaultJson === null) return defaultAction()

                    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 defaultAction()
                        }

                    }
                    const lines = []
                    for (const event of defaultJson.events) {
                        for (const seg of event.segs) {
                            if (seg && typeof seg.utf8 === 'string') {
                                lines.push(...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)
                    defaultAction()
                }

                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
                                        }
                                    }
                                }
                                response.response = JSON.stringify(defaultJson)
                                handler.resolve(response)
                            }).catch(e => {
                                console.warn(e)
                                defaultAction()
                            })

                    })

                }

                requestDeferred = requestDeferred.then(fetchData)

            }
        })

    })

})();