// ==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 https://greasyfork.org/users/1210499 // @icon https://www.youtube.com/s/desktop/b9bfb983/img/favicon_32x32.png // @downloadURL none // ==/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(); })();