// ==UserScript== // @name Coursera SubEx / Coursera multiple subtitles show below the video / Coursera多字幕显示在视频下方插件 // @namespace http://tampermonkey.net/ // @version 0.1 // @description Coursera SubEx , Show multiple subtitles/captions of any languge below the video in coursera.org's learning page at your wish. // @description:zh-CN Coursera SubEx: 根据你的选择,同时显示多种语言的字幕显示在coursera.org课程学习页面的视频播放器下方。 不占用视频内容区域,还可以方便拷贝字幕做笔记。 // @description:zh-TW Coursera SubEx: 根據你的選擇,同時顯示多種語言的字幕顯示在coursera.org課程學習頁面的視頻播放器下方。 不占用視頻內容區域,還可以方便拷貝字幕做筆記。 // @author DryTofu // @match *://www.coursera.org/learn/* // @match *://coursera.org/learn/* // @icon https://www.google.com/s2/favicons?sz=64&domain=coursera.org // @require https://cdn.bootcss.com/jquery/1.11.1/jquery.min.js // @license MIT // @grant GM_addStyle // @downloadURL none // ==/UserScript== (function() { 'use strict'; // coursera字幕处理 // 参考 https://stackoverflow.com/questions/32252337/how-to-style-text-tracks-in-html5-video-via-css/45087610#45087610 // https://stackoverflow.com/questions/64505385/html5-video-subtitles-positioning let videoCss = ` .ex-subtitle-line { margin: 5px auto; text-align: center; font-size: 24px; } .exLngToolbar label { font-weight: normal; padding: 5px 10px 5px 5px; background-color: transparent; } .exLngToolbar label.selected { font-weight: bold; background-color: #aaa; } #exSubtitleInitBtn { position: absolute; left: 0px; top: 0px; z-index: 9999; background-color: #34A853; color: #fff; border: none; } `; (function(factory) { factory(document, jQuery) })(function (document, $) { GM_addStyle(videoCss) const Setting = (function() { let selectedLngs = null const LS_KEY = "coursera_video_ext_selected_lngs" function removeElm(arr, elm) { let i = arr.indexOf(elm) if(i > -1) { arr.splice(i, 1) } return arr // return arr.filter(l => l != elm) } function readSelectedLngsFromLs() { let arr = JSON.parse(localStorage.getItem(LS_KEY)); if(!arr) { arr = [] saveSelectedLngsToLs(arr) } return arr } function saveSelectedLngsToLs(arr) { localStorage.setItem(LS_KEY, JSON.stringify(arr)) } function getSelectedLngs() { if(!selectedLngs) { selectedLngs = readSelectedLngsFromLs() } return selectedLngs } function setSelectedLngs(arr) { selectedLngs = arr saveSelectedLngsToLs(selectedLngs) return selectedLngs } function addLng(lng) { if(!lng) { return } if(!selectedLngs) { selectedLngs = readSelectedLngsFromLs() } if(!selectedLngs.includes(lng)) { selectedLngs.push(lng) saveSelectedLngsToLs(selectedLngs) } return selectedLngs } function removeLng(lng) { if(!lng) { return } if(!selectedLngs) { selectedLngs = readSelectedLngsFromLs() } return removeElm(selectedLngs, lng) } function clearSelectedLngs() { selectedLngs = [] saveSelectedLngsToLs(selectedLngs) return selectedLngs } return { getSelectedLngs, clearSelectedLngs, removeLng, addLng, setSelectedLngs } })() let $video = null, video = null, textTracks = null, selectedLngs; // if (i.language == "zh-CN" || i.language == "zh-TW" || i.language == "en-US" || i.language == "en") { const lngWeightMap = { "zh-CN": 10, "zh": 20, "en-US": 30, "en-GB": 40, "en": 50, "zh-TW": 60, } const lngWeight = function(lng) { return lngWeightMap[lng] || 1000 } function printTracks(msg, $tracks) { let arr = [] $tracks.forEach($track => { arr.push($track.attr('srclang') + '-' + $track.attr('label') + '-' + $track.data('lngweight')) }) console.log(msg, arr) } function printTextTracks(msg, textTracks) { for(let i=0; i !allLngs.includes(lng)) if(notValidLngs && notValidLngs.length) { notValidLngs.forEach(lng => Setting.removeLng(lng)) } return Setting.getSelectedLngs() } // 根据选中的语言,处理video中的textTracks 和 生成对应的字幕栏 function applySelectedLngs(selectedLngs, textTracks, $subtitleWrap) { let subtitleLineMap = {} $subtitleWrap.find(">.ex-subtitle-line").each(function() { let $subtitleLine = $(this) let lng = $subtitleLine.attr("data-lng") // 尝试把去掉的字幕栏也存起来,如果出现奇怪错误,就用后面的判断语句 subtitleLineMap[lng] = $subtitleLine /* if(selectedLngs.includes(lng)) { subtitleLineMap[lng] = $subtitleLine } */ $subtitleLine.remove() }) selectedLngs.forEach(lng => { let $subtitleLine = subtitleLineMap[lng] if(!$subtitleLine) { $subtitleLine = $(`
${lng}
`).attr('data-lng', lng) subtitleLineMap[lng] = $subtitleLine } $subtitleWrap.append($subtitleLine) }) if(selectedLngs && selectedLngs.length) { // 有选中字幕才处理 // 先设置 track hidden 和 disabled 属性 for (const track of textTracks) { if(selectedLngs.includes(track.language)) { track.mode = 'hidden' } else { track.mode = 'disabled' } } // 绑定 cue事件 let maxTryTime = 1000, tryTime = 0; let bindCueEvent = function() { let cueLoadFlag = true; ++tryTime // console.log("开始尝试第" + tryTime + "次------->") for (const track of textTracks) { // 循环字幕track let trackLng = track.language if(selectedLngs.includes(trackLng)) { // 选中语言包含这个字幕 let cues = track.cues if(cues && cues.length) { // console.log("尝试" + tryTime + "次:" + trackLng + '--成功找到cues:' + cues.length, cues) for (let j=0; j`) $videoDiv.after($subtitleWrap) } selectedLngs = Setting.getSelectedLngs() textTracks = video.textTracks console.log("tracks", textTracks); // 求 selectedLngs 和 textTracks 的交集,如果 selectedLngs 有textTracks中不存在的,则需要删除 selectedLngs = checkValid(selectedLngs, textTracks) console.log('selectedLngs', selectedLngs) let trackInfoObjs = textTracksToSortedObjs(textTracks) console.log('trackInfoObjs', trackInfoObjs) let $lngToolbar = $('
') trackInfoObjs.forEach(obj => { $lngToolbar.append(`   `) }) $lngToolbar.find('input[type=checkbox]').each(function() { let $checkbox = $(this), lng = $checkbox.val() if(selectedLngs.includes(lng)) { $checkbox.prop('checked', true) $checkbox.closest('label').addClass('selected') } $checkbox.click(function() { if($checkbox.prop('checked')) { console.log(lng + ' 选中') selectedLngs = Setting.addLng(lng) $checkbox.closest('label').addClass('selected') } else { console.log(lng + ' 取消选中') selectedLngs = Setting.removeLng(lng) $checkbox.closest('label').removeClass('selected') } console.log('selectedLngs', selectedLngs) applySelectedLngs(selectedLngs, video.textTracks, $subtitleWrap) }) }) // 语言checkbox toolbar let $toolbarWrap = $("div.rc-VideoToolbar > .cds-grid-item:first") $toolbarWrap.find("> .exLngToolbar").remove() $toolbarWrap.append($lngToolbar) // let $videoToolbar = $("div.rc-VideoToolbar > .cds-grid-item").append($lngToolbar) // --- 语言选择栏处理完毕 -- applySelectedLngs(selectedLngs, video.textTracks, $subtitleWrap) console.log("---------结束执行 YM Coursera 字幕处理 ") } let _findVideoCount = 0 function findVideo() { ++_findVideoCount $video = $("video.vjs-tech"); if($video.length > 0) { video = $video.get(0) console.log("## 找到video") _findVideoCount = 0 doWork() return } else { if(_findVideoCount >= 60) { console.log("-- 没有找到video元素,尝试超过60次,退出") return } else { setTimeout(findVideo, 3000) } } } console.log("---------开始执行 YM Coursera 字幕处理 ") // 先注销改成 按钮触发 // findVideo() $(function() { // document ready let $exSubtitleInitBtn = $('#exSubtitleInitBtn') if(!$exSubtitleInitBtn.length) { $exSubtitleInitBtn = $(``) $exSubtitleInitBtn.click(function() { findVideo() }) $(document.body).append($exSubtitleInitBtn) } }) }) })();