Greasy Fork 支持简体中文。

YouTube Dual Subtitles for French, German, Russian, Ukrainian

Add dual subtitles to YouTube videos

// ==UserScript==
// @name         YouTube Dual Subtitles for French, German, Russian, Ukrainian
// @namespace    http://tampermonkey.net/
// @version      1.1
// @license      Unlicense
// @description  Add dual subtitles to YouTube videos
// @author       Jim Chen
// @homepage     https://jimchen.me
// @supportURL   https://github.com/jimchen2/youtube-dual-subtitles/issues
// @match        https://www.youtube.com/watch?*
// @match        https://www.youtube.com/embed/*
// @match        https://m.youtube.com/watch?*
// @match        https://m.youtube.com/embed/*
// @match        https://cdn.jimchen.me/*
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  console.log("[Dual Subs] Script initialized");

  async function handleVideoNavigation() {
    console.log("[Dual Subs] Navigation detected");
    removeSubs();
    await processSubtitles();
  }

  async function processSubtitles() {
    console.log("[Dual Subs] Starting subtitle processing");

    const playerData = await new Promise((resolve) => {
      const checkForPlayer = () => {
        console.log("[Dual Subs] Trying to get Caption Data");
        const ytAppData = window.location.href.startsWith("https://www.youtube") ? document.getElementsByTagName("ytd-app") : document.getElementsByTagName("ytm-app");
        const captionData = ytAppData[0].data?.playerResponse?.captions?.playerCaptionsTracklistRenderer.captionTracks;
        if (captionData) {
          console.log("[Dual Subs] Successfully retrieved caption data");
          resolve(captionData);
        } else {
          console.log("[Dual Subs] Caption data not found, retrying");
          setTimeout(checkForPlayer, 1000);
        }
      };
      checkForPlayer();
    });

    if (!playerData) {
      console.log("[Dual Subs] No player data available");
      return;
    }

    await addSubtitles(playerData);
  }
  async function addSubtitles(playerData) {
    console.log("[Dual Subs] Finding auto-generated track");

    const hasForeignTrack = playerData.some((track) => ["a.ru", "a.uk", "a.de", "a.fr", ".ru", ".uk", ".de", ".fr"].includes(track.vssId));

    if (hasForeignTrack) {
      const autoGeneratedTrack = playerData.find((track) => ["a.en", "a.ru", "a.uk", "a.de", "a.fr"].includes(track.vssId));
      const manualTrack = playerData.find((track) => [".en", ".ru", ".uk", ".de", ".fr"].includes(track.vssId));
      const otherTrack = autoGeneratedTrack || manualTrack;
      if (!otherTrack) {
        return;
      }
      await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt&tlang=zh`);
      await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt`);
    } else {
      const otherTrack = playerData.find((track) => ["a.zh", "zh", "zh-Hans", "zh-Hant"].includes(track.vssId));
      await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt`);
      await addOneSubtitle(`${otherTrack.baseUrl}&fmt=vtt&tlang=en`);
    }
  }

  async function addOneSubtitle(url, maxRetries = 5, delay = 1000) {
    const video = document.querySelector("video");

    try {
      console.log(`[Dual Subs] Fetching subtitles`);
      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";
      console.log(`[Dual Subs] Successfully added one subtitle`);
    } catch (error) {
      if (maxRetries > 0) {
        console.log(`[Dual Subs] Retrying... (${maxRetries} attempts remaining)`);
        await new Promise((resolve) => setTimeout(resolve, delay));
        return addOneSubtitle(url, maxRetries - 1, delay);
      }
    }
  }

  function removeSubs() {
    console.log("[Dual Subs] Attempting to remove subtitles");
    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);
    });
    console.log(`[Dual Subs] Successfully removed ${tracks.length} subtitle track(s)`);
  }

  document.addEventListener("yt-navigate-finish", handleVideoNavigation);
})();