Greasy Fork

来自缓存

Greasy Fork is available in English.

Youtube记忆恢复双语字幕和播放速度-下载字幕

记忆播放器设置菜单(含自动翻译菜单)选择的字幕语言和播放速度。默认中文(简体)字幕/默认字幕(双语);找不到匹配的语言时,匹配前缀,例如中文(简体)->中文

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

/* eslint-disable no-use-before-define */
// ==UserScript==
// @name          Youtube记忆恢复双语字幕和播放速度-下载字幕
// @name:en    Youtube store/restore bilingual subtitles and playback speed - download subtitles
// @description  记忆播放器设置菜单(含自动翻译菜单)选择的字幕语言和播放速度。默认中文(简体)字幕/默认字幕(双语);找不到匹配的语言时,匹配前缀,例如中文(简体)->中文
// @description:en  The selected subtitle language and playback speed are stored and auto restored
// @license MIT
// @match       https://*.youtube.com/*
// @run-at       document-start
// @author      [email protected]
// @source      https://github.com/szdailei/GM-scripts
// @namespace  http://greasyfork.icu
// @version         3.1.3
// ==/UserScript==

/**
require:  @run-at document-start
ensure:  run handleYtNavigateFinish() when yt-navigate-finish event triggered
*/
(() => {
  const PLAY_SPEED_LOCAL_STORAGE_KEY = 'greasyfork-org-youtube-config-play-speed';
  const SUBTITLE_LOCAL_STORAGE_KEY = 'greasyfork-org-youtube-config-subtitle';
  const NOT_SUPPORT_LANGUAGE =
    'Only English/Chinese/Russian are supported. \n\nFor users who have signed in youtube, please change the account language to a supported language. \n\nFor users who have not signed in youtube, please change the browser language to a supported language.';
  const DEFAULT_SUBTITLES = 'chinese';
  const TIMER_OF_MENU_LOAD_AFTER_USER_CLICK = 20;
  const TIMER_OF_ELEMENT_LOAD = 100;
  const numbers = '0123456789';
  const specialCharacterAndNumbers = '`~!@#$%^&*()_+<>?:"{},./;\'[]0123456789-=()';

  class I18n {
    constructor(langCode, resource) {
      this.langCode = langCode;
      switch (langCode) {
        case 'zh':
        case 'zh-CN':
        case 'zh-SG':
        case 'zh-Hans-CN':
        case 'cmn-Hans-CN':
        case 'cmn-Hans-SG':
          this.resource = resource.cmnHans;
          break;
        case 'zh-TW':
        case 'zh-Hant-TW':
        case 'cmn-Hant-TW':
          this.resource = resource.cmnHant;
          break;
        case 'zh-HK':
        case 'zh-MO':
        case 'zh-Hant-HK':
        case 'zh-Hant-MO':
        case 'yue-Hant-HK':
        case 'yue-Hant-MO':
          this.resource = resource.cmnHantHK;
          break;
        case 'en':
        case 'en-AU':
        case 'en-BZ':
        case 'en-CA':
        case 'en-CB':
        case 'en-GB':
        case 'en-IE':
        case 'en-IN':
        case 'en-JM':
        case 'en-NZ':
        case 'en-PH':
        case 'en-TT':
        case 'en-US':
        case 'en-ZA':
        case 'en-ZW':
          this.resource = resource.en;
          break;
        case 'ru':
        case 'ru-RU':
          this.resource = resource.ru;
          break;
        default:
          this.resource = resource.en;
          break;
      }
    }

    t(key) {
      return this.resource[key];
    }
  }

  let lastHref = null;
  const hostLanguage = document.getElementsByTagName('html')[0].getAttribute('lang');
  if (hostLanguage === null) {
    return;
  }

  const i18n = new I18n(hostLanguage, getResource());
  if (getStorage(i18n.t('subtitles')) === null) {
    setStorage(i18n.t('subtitles'), i18n.t(DEFAULT_SUBTITLES));
  }

  window.addEventListener('yt-navigate-finish', handleYtNavigateFinish);

  function getResource() {
    const resource = {
      en: {
        playSpeed: 'Playback speed',
        subtitles: 'Subtitles',
        autoTranlate: 'Auto-translate',
        chinese: 'Chinese (Simplified)',
        downloadTranscript: 'Download transcript',
      },
      cmnHans: {
        playSpeed: '播放速度',
        subtitles: '字幕',
        autoTranlate: '自动翻译',
        chinese: '中文(简体)',
        downloadTranscript: '下载字幕',
      },
      cmnHant: {
        playSpeed: '播放速度',
        subtitles: '字幕',
        autoTranlate: '自動翻譯',
        chinese: '中文(簡體)',
        downloadTranscript: '下載字幕',
      },
      cmnHantHK: {
        playSpeed: '播放速度',
        subtitles: '字幕',
        autoTranlate: '自動翻譯',
        chinese: '中文(簡體字)',
        downloadTranscript: '下載字幕',
      },
      ru: {
        playSpeed: 'Скорость воспроизведения',
        subtitles: 'Субтитры',
        autoTranlate: 'Перевести',
        chinese: 'Русский',
        downloadTranscript: 'Скачать транскрибцию',
      },
    };
    return resource;
  }

  function handleYtNavigateFinish() {
    if (lastHref === window.location.href || window.location.href.indexOf('/watch') === -1) {
      return;
    }

    lastHref = window.location.href;
    // run once on https://www.youtube.com/watch*.
    youtubeConfig();
  }

  /**
require:  yt-navigate-finish event on https://www.youtube.com/watch*
ensure: 
    1. If there isn't subtitle enable button, exit.
    2. store/resotre play speed and subtitle. If can't restore subtitle, but there is auto-translate radio, translate to stored subtitle.
    3. If there is transcript, trun on transcript.
*/
  async function youtubeConfig() {
    const player = await waitUntil(document.getElementById('movie_player'));
    const rightControls = await waitUntil(player.getElementsByClassName('ytp-right-controls'));
    const rightControl = rightControls[0];
    if (isSubtitleEabled(rightControl) === false) {
      return;
    }

    const settingsButtons = await waitUntil(rightControl.getElementsByClassName('ytp-settings-button'));
    const settingsButton = settingsButtons[0];
    settingsButton.addEventListener('click', handleRadioClick);

    settingsButton.click();
    const settingsMenu = await waitUntil(getPanelMenuByTitle(player, ''));
    await restoreSettingOfTitle(player, settingsMenu, i18n.t('playSpeed'));

    const isSubtitlRestored = await restoreSettingOfTitle(player, settingsMenu, i18n.t('subtitles'));
    if (isSubtitlRestored === false) {
      const labels = settingsMenu.getElementsByClassName('ytp-menuitem-label');
      const subtitlesRadio = getElementByShortTextContent(labels, i18n.t('subtitles'));
      subtitlesRadio.click();
      const subtitleMenu = await waitUntil(getPanelMenuByTitle(player, i18n.t('subtitles')));
      const isAutoTransSubtitleRestored = await restoreSettingOfTitle(player, subtitleMenu, i18n.t('autoTranlate'));
      if (isAutoTransSubtitleRestored === false) {
        settingsButton.click(); // close settings menu
      }
    } else {
      settingsButton.click(); // close settings menu
    }

    await turnOnTranscript();
  }

  function isSubtitleEabled(rightControl) {
    const subtitlesEnableButtons = rightControl.getElementsByClassName('ytp-subtitles-button');
    if (
      subtitlesEnableButtons === null ||
      subtitlesEnableButtons[0] === null ||
      subtitlesEnableButtons[0].style.display === 'none'
    ) {
      return false;
    }

    if (!subtitlesEnableButtons[0].getAttribute('aria-pressed')) {
      return false;
    }

    if (subtitlesEnableButtons[0].getAttribute('aria-pressed') === 'false') {
      subtitlesEnableButtons[0].click();
    }
    return true;
  }

  async function restoreSettingOfTitle(player, openedMenu, subMenuTitle) {
    const value = getStorage(subMenuTitle);
    if (value === null) {
      return true;
    }

    const labels = openedMenu.getElementsByClassName('ytp-menuitem-label');
    const radio = getElementByShortTextContent(labels, subMenuTitle);
    if (radio === null) {
      return false;
    }
    radio.click();
    const subMenu = await waitUntil(getPanelMenuByTitle(player, subMenuTitle));
    return restoreSettingByValue(subMenu, value);
  }

  function getPanelMenuByTitle(player, title) {
    if (title === null || title === '') {
      // settings menu
      const panelMenus = player.getElementsByClassName('ytp-panel-menu');
      if (panelMenus === null || panelMenus.length === 0 || panelMenus[0].previousElementSibling !== null) {
        // no panelMenus or panelMenu has previousElementSibling (panelHeader)
        return null;
      }
      return panelMenus[0];
    }

    // other menu, not settings menu
    const panelHeaders = player.getElementsByClassName('ytp-panel-header');
    if (panelHeaders !== null) {
      for (let i = 0; i < panelHeaders.length; i += 1) {
        const panelHeaderTitle = getPanelHeaderTitle(panelHeaders[i]);
        if (getShortText(panelHeaderTitle.textContent) === title) {
          return panelHeaders[i].nextElementSibling;
        }
      }
    }
    return null;
  }

  function getPanelHeaderTitle(panelHeader) {
    const panelTitles = panelHeader.getElementsByClassName('ytp-panel-title');
    return panelTitles[0];
  }

  function restoreSettingByValue(openedMenu, value) {
    const panelheader = openedMenu.previousElementSibling;
    const panelTitle = getPanelHeaderTitle(panelheader);
    const labels = openedMenu.getElementsByClassName('ytp-menuitem-label');
    let storedRadio = getElementByTextContent(labels, value);
    if (storedRadio === null) {
      // if can't match '中文(简体)',try '中文'
      storedRadio = getElementByShortTextContent(labels, getShortText(value));
      if (storedRadio === null) {
        panelTitle.click();
        return false;
      }
    }

    if (storedRadio.parentElement.getAttribute('aria-checked') === 'true') {
      panelTitle.click();
      return true;
    }
    storedRadio.click();
    return true;
  }

  function handleRadioClick() {
    const player = document.getElementById('movie_player');

    if (this.textContent === '') {
      // clicked on settingsButton which will open settingsMenu
      handleRadioToPanelMenuClick(player, '', handleRadioClick);
      return;
    }

    // clicked on radio which will open subMenu
    const label = this.getElementsByClassName('ytp-menuitem-label')[0];
    const shortText = getShortText(label.textContent);
    if (
      shortText === i18n.t('playSpeed') ||
      shortText === i18n.t('subtitles') ||
      shortText === i18n.t('autoTranlate')
    ) {
      handleRadioToPanelMenuClick(player, shortText, handleRadioClick);
      return;
    }

    // in 'autoTranlate' menu, only one radio which seleted by default has parentNode, others are orphan nodes and can't get parentNode by 'this'
    const panelHeaders = player.getElementsByClassName('ytp-panel-header');
    const title = getShortText(getPanelHeaderTitle(panelHeaders[0]).textContent);
    setStorage(title, label.textContent);
  }

  async function handleRadioToPanelMenuClick(player, title, eventListener) {
    const panelMenu = await waitUntil(getPanelMenuByTitle(player, title), TIMER_OF_MENU_LOAD_AFTER_USER_CLICK);
    addEventListenerOnPanelMenu(panelMenu, eventListener);
  }

  function addEventListenerOnPanelMenu(panelMenu, eventListener) {
    const radios = panelMenu.getElementsByClassName('ytp-menuitem-label');
    Array.prototype.forEach.call(radios, (radio) => {
      radio.parentElement.addEventListener('click', eventListener);
    });
  }

  async function turnOnTranscript() {
    const infoContents = await waitUntil(document.getElementById('info-contents'));
    const moreActionsMenuButtons = await waitUntil(infoContents.getElementsByClassName('dropdown-trigger'));
    const moreActionsMenuButton = moreActionsMenuButtons[0];

    moreActionsMenuButton.click();
    const menuPopupRenderers = await waitUntil(document.getElementsByTagName('ytd-menu-popup-renderer'));

    const items = menuPopupRenderers[0].querySelector('#items');

    // The first item should be invisible, the second item be "Report", the third be "Show transcript"
    // "Show transcript" MUST be there
    if (items.length < 3) {
      moreActionsMenuButton.click(); // close moreActionsMenu
      return;
    }

    const showTranscriptRadio = items.childNodes[2];

    showTranscriptRadio.click();

    const engagementPanel = await getEngagementPanel();

    const titleContainer = engagementPanel.querySelector('div[id=title-container]');
    const transcriptTitle = titleContainer.querySelector('yt-formatted-string[id=title-text]');

    insertPaperButton(transcriptTitle, i18n.t('downloadTranscript'), onTranscriptDownloadButtonClicked);
  }

  async function getEngagementPanel() {
    const panels = await waitUntil(document.getElementById('panels'));
    const engagementPanel = panels.querySelector(
      'ytd-engagement-panel-section-list-renderer[visibility=ENGAGEMENT_PANEL_VISIBILITY_EXPANDED]'
    );
    return engagementPanel;
  }

  function insertPaperButton(transcriptTitle, textContent, clickCallback) {
    transcriptTitle.textContent = textContent;
    transcriptTitle.style.background = 'red';
    transcriptTitle.style.cursor = 'pointer';

    transcriptTitle.addEventListener('click', clickCallback);
  }

  async function onTranscriptDownloadButtonClicked() {
    const infoContents = document.getElementById('info-contents');
    const title = infoContents.querySelector('h1');
    const filename = `${title.textContent}.vtt`;

    const engagementPanel = await getEngagementPanel();

    const segmentsContainer = engagementPanel.querySelector('div[id=segments-container]');

    const cueGroups = segmentsContainer.childNodes;
    if (cueGroups === null) {
      return;
    }

    const ytpTimeDuration = await getYtpTimeDuration();
    const content = getFormattedSRT(cueGroups, ytpTimeDuration);
    saveTextAsFile(filename, content);
  }

  function convertTimeFormat(time) {
    const fields = time.split(':');

    if (fields.length === 2) {
      fields.unshift('00');
    }

    const convertedArray = []
    for (let i = 0; i < 2; i += 1) {
      const fieldInt = parseInt(fields[i],10)
      let str
      if (fieldInt < 10) {
        str = `0${fieldInt.toString()}`;
      } else {
        str = fieldInt.toString();
      }
      convertedArray.push(str)
    }

    return `${convertedArray[0]}:${convertedArray[1]}:${fields[2]}`;
  }

  function getFormattedSRT(cueGroups, ytpTimeDuration) {
    let content = 'WEBVTT\n\n';
    for (let i = 0; i < cueGroups.length; i += 1) {
      const currentSubtitleStartOffsets = cueGroups[i].getElementsByClassName('segment-timestamp');
      const startTime = convertTimeFormat(currentSubtitleStartOffsets[0].textContent.trim());
      let endTime;
      if (i === cueGroups.length - 1) {
        endTime = convertTimeFormat(ytpTimeDuration);
      } else {
        const nextSubtitleStartOffsets = cueGroups[i + 1].getElementsByClassName('segment-timestamp');
        endTime = convertTimeFormat(nextSubtitleStartOffsets[0].textContent.split('\n').join('').trim());
      }

      const timeLine = `${startTime}.000  -->  ${endTime}.000`;
      const cues = cueGroups[i].getElementsByClassName('segment-text');
      const contentLine = cues[0].textContent.split('\n').join('').trim();
      content += `${timeLine}\n${contentLine}\n\n`;
    }
    return content;
  }

  async function getYtpTimeDuration() {
    const player = await waitUntil(document.getElementById('movie_player'));
    const leftControls = await waitUntil(player.getElementsByClassName('ytp-left-controls'));
    const ytpTimeDurations = leftControls[0].getElementsByClassName('ytp-time-duration');
    return ytpTimeDurations[0].textContent;
  }

  function saveTextAsFile(filename, text) {
    const a = document.createElement('a');
    a.href = `data:text/txt;charset=utf-8,${encodeURIComponent(text)}`;
    a.download = filename;
    a.click();
  }

  function getElementByTextContent(elements, textContent) {
    for (let i = 0; i < elements.length; i += 1) {
      if (elements[i].textContent === textContent) {
        return elements[i];
      }
    }
    return null;
  }

  function getElementByShortTextContent(elements, textContent) {
    for (let i = 0; i < elements.length; i += 1) {
      if (getShortText(elements[i].textContent) === textContent) {
        return elements[i];
      }
    }
    return null;
  }

  function getShortText(text) {
    if (text === null) {
      return null;
    }
    if (text === '' || numbers.indexOf(text[0]) !== -1 || text === i18n.t('autoTranlate')) {
      return text.trim();
    }

    // return input text before specialCharacterAndNumbers
    let shortText = '';
    for (let i = 0; i < text.length; i += 1) {
      if (specialCharacterAndNumbers.indexOf(text[i]) !== -1) {
        break;
      }
      shortText += text[i];
    }
    return shortText.trim();
  }

  function getStorage(title) {
    let storedValue = null;
    switch (title) {
      case i18n.t('playSpeed'):
        storedValue = localStorage.getItem(PLAY_SPEED_LOCAL_STORAGE_KEY);
        break;
      case i18n.t('subtitles'):
      case i18n.t('autoTranlate'):
        storedValue = localStorage.getItem(SUBTITLE_LOCAL_STORAGE_KEY);
        break;
      default:
        break;
    }
    return storedValue;
  }

  function setStorage(title, value) {
    switch (title) {
      case i18n.t('playSpeed'):
        localStorage.setItem(PLAY_SPEED_LOCAL_STORAGE_KEY, value);
        break;
      case i18n.t('subtitles'):
      case i18n.t('autoTranlate'):
        localStorage.setItem(SUBTITLE_LOCAL_STORAGE_KEY, value);
        break;
      default:
        break;
    }
  }

  async function waitUntil(condition, timer) {
    let timeout = TIMER_OF_ELEMENT_LOAD;
    if (timer) {
      timeout = timer;
    }
    return new Promise((resolve) => {
      const interval = setInterval(() => {
        const result = condition;
        if (result) {
          clearInterval(interval);
          resolve(result);
        }
      }, timeout);
    });
  }
})();