您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add dual subtitles to YouTube videos
当前为
// ==UserScript== // @name YouTube Dual Subtitles for French, German, Russian, Ukrainian // @namespace http://tampermonkey.net/ // @version 1.2 // @license Unlicense // @description Add dual subtitles to YouTube videos // @author Jim Chen // @homepage https://jimchen.me // @match https://www.youtube.com/* // @match https://m.youtube.com/* // @run-at document-idle // ==/UserScript== (function () { "use strict"; let processingSubtitles = false; async function handleVideoNavigation() { let videoID = extractYouTubeVideoID(); if (videoID == null) return; if (processingSubtitles) return; processingSubtitles = true; removeSubs(); await processSubtitles(); processingSubtitles = false; } function extractYouTubeVideoID() { const url = window.location.href; const patterns = { standard: /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/, embed: /(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([^?]+)/, mobile: /(?:https?:\/\/)?(?:www\.)?youtu\.be\/([^?]+)/, }; let videoID = null; if (patterns.standard.test(url)) { videoID = url.match(patterns.standard)[1]; } else if (patterns.embed.test(url)) { videoID = url.match(patterns.embed)[1]; } else if (patterns.mobile.test(url)) { videoID = url.match(patterns.mobile)[1]; } return videoID; } async function processSubtitles() { console.log("[Dual Subs] Starting subtitle processing"); const playerData = await new Promise((resolve) => { const checkForPlayer = () => { let ytAppData = document.querySelector("#movie_player"); let captionData = ytAppData?.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer?.captionTracks; if (captionData) { const fetchedBaseUrl = captionData[0].baseUrl; const fetchedVideoID = fetchedBaseUrl.match(/[?&]v=([^&]+)/)?.[1]; let videoID = extractYouTubeVideoID(); if (fetchedVideoID !== videoID) setTimeout(checkForPlayer, 1000); else resolve(captionData); } else setTimeout(checkForPlayer, 1000); }; checkForPlayer(); }); if (!playerData) return; await addSubtitles(playerData); } async function addSubtitles(playerData) { const hasForeignTrack = playerData.some(({ vssId }) => /(ru|uk|de|fr)/.test(vssId)); if (hasForeignTrack) { const autoGeneratedTrack = playerData.find((track) => ["a.ru", "a.uk", "a.de", "a.fr"].includes(track.vssId)); const manualTrack = playerData.find((track) => ["ru", "uk", "de", "fr"].some((code) => track.vssId.includes(code))); const otherTrack = autoGeneratedTrack || manualTrack; if (!otherTrack) return; await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt&tlang=en`); await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt`); } } async function addOneSubtitle(url, maxRetries = 5, delay = 1000) { const video = document.querySelector("video"); try { const response = await fetch(url); const subtitleData = (await response.text()).replaceAll("align:start position:0%", ""); const track = document.createElement("track"); track.src = "data:text/vtt," + encodeURIComponent(subtitleData); await new Promise((resolve) => setTimeout(resolve, delay)); video.appendChild(track); track.track.mode = "showing"; } catch (error) { if (maxRetries > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); return addOneSubtitle(url, maxRetries - 1, delay); } } } function removeSubs() { const video = document.getElementsByTagName("video")[0]; if (!video) return; const tracks = video.getElementsByTagName("track"); Array.from(tracks).forEach(function (ele) { ele.track.mode = "hidden"; ele.parentNode.removeChild(ele); }); } let lastUrl = location.href; const observer = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; handleVideoNavigation(); } }); observer.observe(document.body, { childList: true, subtree: true }); handleVideoNavigation(); })();