Greasy Fork

Greasy Fork is available in English.

Youtube 双语字幕版

YouTube双语字幕,任何语言翻译成中文。支持移动端和桌面端,适配Via浏览器。

目前为 2024-12-28 提交的版本。查看 最新版本

// ==UserScript==
// @name                Youtube 双语字幕版
// @version             1.2.3
// @author              LR
// @license             MIT
// @description         YouTube双语字幕,任何语言翻译成中文。支持移动端和桌面端,适配Via浏览器。
// @match               *://www.youtube.com/*
// @match               *://m.youtube.com/*
// @require             https://unpkg.com/ajax-hook@latest/dist/ajaxhook.min.js
// @grant               GM_registerMenuCommand
// @run-at              document-start
// @namespace           http://greasyfork.icu/users/1210499
// @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png
// ==/UserScript==

(function () {
    'use strict';

    // 用户设置部分:允许通过脚本设置选择目标语言
    let TARGET_LANG = localStorage.getItem('yt_target_lang') || 'zh'; // 默认中文

    function updateLanguage() {
        const lang = prompt("请输入目标语言的ISO 639-1代码(例如中文:zh,英语:en,日语:ja)", TARGET_LANG);
        if (lang) {
            TARGET_LANG = lang;
            localStorage.setItem('yt_target_lang', lang);
            alert(`目标语言已更新为:${TARGET_LANG}`);
        }
    }

    // 添加脚本设置菜单(Via浏览器支持)
    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand("设置目标翻译语言", updateLanguage);
    }

    async function enableDualSubtitles() {
        // 获取翻译后的字幕数据
        async function fetchTranslatedSubtitles(url) {
            const cleanUrl = url.replace(/(^|[&?])tlang=[^&]*/g, '') + `&tlang=${TARGET_LANG}&translate_h00ked`;
            try {
                const response = await fetch(cleanUrl, { method: 'GET' });
                if (!response.ok) {
                    throw new Error(`Failed to fetch translated subtitles: ${response.status}`);
                }
                return await response.json();
            } catch (error) {
                console.error(error);
                return null;
            }
        }

        function mergeSubtitles(defaultSubs, translatedSubs) {
            const mergedSubs = JSON.parse(JSON.stringify(defaultSubs));
            const translatedEvents = translatedSubs.events.filter(event => event.segs);
            const translatedMap = new Map(translatedEvents.map(event => [event.tStartMs, event])); // 使用 Map 存储翻译事件

            for (let i = 0; i < mergedSubs.events.length; i++) {
                const defaultEvent = mergedSubs.events[i];
                if (!defaultEvent.segs) continue;

                const translatedEvent = [...translatedMap.keys()].reduce((closest, tStartMs) => {
                    return (Math.abs(tStartMs - defaultEvent.tStartMs) < Math.abs(closest - defaultEvent.tStartMs)) ? tStartMs : closest;
                }, Infinity);

                const eventToMerge = translatedMap.get(translatedEvent);
                if (eventToMerge) {
                    const defaultText = defaultEvent.segs.map(seg => seg.utf8).join('');
                    const translatedText = eventToMerge.segs.map(seg => seg.utf8).join('');

                    const timeOverlap = Math.min(defaultEvent.tStartMs + defaultEvent.dDurationMs, eventToMerge.tStartMs + eventToMerge.dDurationMs) - Math.max(defaultEvent.tStartMs, eventToMerge.tStartMs);
                    if (timeOverlap > 0) {
                        defaultEvent.segs[0].utf8 = `${defaultText}\n${translatedText}`;
                        defaultEvent.segs = [defaultEvent.segs[0]];
                    }
                }
            }

            return JSON.stringify(mergedSubs);
        }

        ah.proxy({
            onResponse: async (response, handler) => {
                if (response.config.url.includes('/api/timedtext') && !response.config.url.includes('&translate_h00ked')) {
                    try {
                        const defaultSubs = JSON.parse(response.response);
                        const translatedSubs = await fetchTranslatedSubtitles(response.config.url);
                        if (translatedSubs) {
                            response.response = mergeSubtitles(defaultSubs, translatedSubs);
                        }
                    } catch (error) {
                        console.error("Error processing subtitles:", error);
                    }
                }
                handler.resolve(response);
            }
        });
    }

    enableDualSubtitles();
})();