返回首頁 

Greasy Fork is available in English.

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

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


安装此脚本?
/* 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  https://greasyfork.org// @version         3.1.3// ==/UserScript==/**require:  @run-at document-startensure:  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 menuconst 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 menuconst 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 settingsMenuhandleRadioToPanelMenuClick(player, '', handleRadioClick);return;}// clicked on radio which will open subMenuconst 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 thereif (items.length < 3) {moreActionsMenuButton.click(); // close moreActionsMenureturn;}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 strif (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 specialCharacterAndNumberslet 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 r###lt = condition;if (r###lt) {clearInterval(interval);resolve(r###lt);}}, timeout);});}})();