// ==UserScript== // @name lynda.com 字幕翻译 // @description lynda.com 字幕翻译脚本,并支持下载视频和字幕文件 // @namespace https://github.com/journey-ad // @version 0.3.1 // @icon https://cdn.lynda.com/static/favicon.ico // @author journey-ad // @match *://www.lynda.com/* // @require https://cdn.jsdelivr.net/npm/downloadjs@1.4.7/download.min.js // @license MIT // @run-at document-end // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_notification // @downloadURL none // ==/UserScript== (function () { "use strict"; var transServer = "caiyun"; // 配置翻译接口,可选值为 caiyun, sogou, google var entries = null; window.transTimer = window.setInterval(init, 100); // 用定时器检查待翻译文本是否已准备好 function init() { if (unsafeWindow.mejs && getDeepProperty(mejs, "players.mep_0.selectedTrack")) { window.clearInterval(window.transTimer); // 清除定时器 addTranslateBtn(); entries = mejs.players.mep_0.selectedTrack.entries; Object.defineProperty(mejs.players.mep_0.selectedTrack, "entries", { get: function get() { return entries; }, set: function set(data) { entries = data; if (entries.text) { console.log("字幕文本可用"); transText(); } } }); console.log("字幕文本可用"); transText(); } } function addTranslateBtn() { window.isTranslateEnable = true; var controls = document.getElementsByClassName("mejs-controls")[0], css = ".mejs-container .mejs-controls .mejs-button.translate {right: 110px;}@media (min-width: 768px){.mejs-container .mejs-controls .mejs-button.translate {right: 132px;}.mejs-container .mejs-controls .mejs-button.mejs-playback-rate-button {right: 166px;}}.modal.video-modal .modal-player-cont .mejs-container .mejs-controls .mejs-button.translate {right: 90px;}#translate-btn {height: 16px;margin-top: 2px;font-size: 16px;font-weight: bold;color: #ccc;transition: all .3s;}#translate-btn:hover {color: #fff;}#translate-btn.enable {color: #ffba00;}@media (max-width: 768px){.mejs-container .mejs-captions-layer {font-size: 16px; line-height:18px;}}", container = document.createElement("div"); addStyle(css); controls.appendChild(container); container.outerHTML = '
'; var transBtn = document.getElementById("translate-btn"); transBtn.addEventListener("click", function () { transBtn.classList.toggle("enable"); window.isTranslateEnable = !window.isTranslateEnable; }, false); } function addDownloadTab() { window.loading = true; var courseId = unsafeWindow.lynda.courseId, videoId = unsafeWindow.currentVideoId, elVideoName = document.querySelector('#course-page .course-toc .toc-items .current .video-name'), videoName = '{{ index }}_{{ zh }}_{{ en }}'.render({ index: elVideoName.dataset.index, en: elVideoName.dataset.original.replace(/\s+/g, '_'), zh: elVideoName.dataset.translate.replace(/\s+/g, '') }), panelTemplate = videoDownloadTabTemplate, videoData = { class: '', style: document.getElementsByClassName('tab-video-download')[0].classList.contains('selected') ? 'display:block' : 'display:none', subtitleNameZH: videoName + '_chs.srt', subtitleNameEN: videoName + '_eng.srt' }, _TIMECODE_REGEX = /\[(\d+:\d+:\d+[\.,]\d+)\]/; getVideoUrl(courseId, videoId, function (data) { data[0]['qualities'].forEach(function (quality) { videoData['videoName' + quality] = videoName + '_' + quality + 'p.' + (quality === '64' ? 'mp3' : 'mp4'); videoData['videoUrl' + quality] = data[0]['urls'][quality]; }); fixSubtitles(videoId, function (subtitles) { var zhSrt = '', enSrt = '', seqCounter = 0; for (var pos = 0; pos < subtitles.length - 1; pos++) { var seqCurrent = subtitles[pos]; var mCurrent = _TIMECODE_REGEX.exec(seqCurrent['Timecode']); if (mCurrent === null) continue; var seqNext = subtitles[pos + 1]; var mNext = _TIMECODE_REGEX.exec(seqNext['Timecode']); if (mNext === null) continue; var appearTime = mCurrent[1]; var disappearTime = mNext[1]; var text = seqCurrent['Caption'].trim(); if (text) { seqCounter++; text = text.replace(/\r?\n|\r/g, " ").trim(); zhSrt += '{{ num }}\r\n{{ from }} --> {{ to }}\r\n{{ translate }}\r\n{{ text }}\r\n\r\n'.render({ num: seqCounter, from: appearTime, to: disappearTime, translate: window.subtitleTrans[seqCounter - 1], text: text }); enSrt += '{{ num }}\r\n{{ from }} --> {{ to }}\r\n{{ text }}\r\n\r\n'.render({ num: seqCounter, from: appearTime, to: disappearTime, text: text }); } } window.downloadPanel = document.getElementById('tab-video-download'); window.downloadPanel.outerHTML = panelTemplate.render(videoData); document.getElementById('tab-video-download').addEventListener('click', function (e) { var el = e.target; while (el.tagName !== 'LI') { el = el.parentNode; if (el === document.getElementById('tab-video-download')) { el = null; break; } } if (el) { if (el.classList.contains('subtitle')) { var lang = el.dataset.lang; if (lang === 'zh') { download(zhSrt, videoData['subtitleNameZH'], 'text/plain'); } else { download(enSrt, videoData['subtitleNameEN'], 'text/plain'); } } else if (el.classList.contains('quality')) { el.classList.add('disable'); el.classList.add('progress'); var elProg = el.getElementsByClassName('file-prog')[0], prog = 0, quality = el.dataset.quality; elProg.innerText = prog + '%'; GM_download({ url: videoData['videoUrl' + quality], name: videoData['videoName' + quality], saveAs: true, onload: function onload() { GM_notification("下载完成", videoData['videoName' + quality]); el.classList.remove('disable'); el.classList.remove('progress'); }, onerror: function onerror() { GM_notification("下载失败", videoData['videoName' + quality]); el.classList.remove('disable'); }, onprogress: function onprogress(e) { prog = (e.loaded / e.total * 100).toFixed(2); elProg.innerText = prog + '%'; el.style.backgroundSize = prog + '% auto'; } }); GM_notification("开始下载", videoData['videoName' + quality]); } } }); window.loading = false; document.getElementById('tab-video-download').classList.remove('loading'); console.log("下载面板添加完成"); }); }); } function getVideoUrl(courseId, videoId, callback) { GM_xmlhttpRequest({ method: "GET", url: "https://www.lynda.com/ajax/course/".concat(courseId, "/").concat(videoId, "/play"), headers: { "accept": "application/json" }, onload: function onload(response) { var data = JSON.parse(response.responseText); callback(data); } }); } function fixSubtitles(videoId, callback) { GM_xmlhttpRequest({ method: "GET", url: "https://www.lynda.com/ajax/player?videoId=".concat(videoId, "&type=transcript"), headers: { "accept": "application/json" }, onload: function onload(response) { var data = JSON.parse(response.responseText); callback(data); } }); } function transText() { document.getElementById('tab-video-download').classList.add('loading'); window.subtitleTrans = []; var s = "", r = "", arr = [], num = 0, count = 0, subtitle = entries.text; subtitle.forEach(function (e) { // 去除每条字幕的换行符并按行排列 s += e.replace(/\r?\n|\r/g, " ") + "\n"; }); num = translate(s, function (data, index) { // 调用翻译方法并处理回调 count++; arr[index] = data; // 按分块原始下标放回结果数组 if (count >= num) { // 所有翻译文本已取回 r = arr.join("\n"); window.subtitleTrans = r.split("\n"); mejs.players.mep_0.displayCaptions = function () { // 重写displayCaptions方法 var subtitle = null; if ("undefined" != typeof mejs.players.mep_0.tracks) { var t, e = mejs.players.mep_0, i = e.selectedTrack; if (null !== i && i.isLoaded) { for (t = 0; t < i.entries.times.length; t++) { if (e.media.currentTime >= i.entries.times[t].start && e.media.currentTime <= i.entries.times[t].stop) { if (window.isTranslateEnable) { // 拼接双语字幕 subtitle = (window.subtitleTrans[t] || '[等待翻译文本]') + "\n" + i.entries.text[t].replace(/\r?\n|\r/g, " "); } else { subtitle = i.entries.text[t]; } return e.captionsText.html(subtitle).attr("class", "mejs-captions-text " + (i.entries.times[t].identifier || "")), void e.captions.show(); } } e.captions.hide(); } else e.captions.hide(); } }; console.log(r); console.log("使用 " + transServer + " 翻译完成"); if (!window.loading) { addDownloadTab(); } } }); } function translate(str, callback) { var textArr = [], count = 1; if (str.length > 5000) { //大于5000字符分块翻译 var strArr = str.split("\n"), i = 0; strArr.forEach(function (v) { textArr[i] = textArr[i] || ""; if ((textArr[i] + v).length > (i + 1) * 5000) { // 若加上此行后长度超出5000字符则分块 i++; textArr[i] = ""; } textArr[i] += v + "\n"; }); count = i + 1; // 记录块的数量 } else { textArr[0] = str; } textArr.forEach(function (text, index) { // 遍历每块分别进行翻译 server({ text: text.trim(), index: index }, callback); }); return count; // 返回分块数量 } function server() { var list = { sogou: function sogou(r, callback) { var KEY = "b33bf8c58706155663d1ad5dba4192dc"; // 硬编码于搜狗网页翻译js var data = { "from": "auto", "to": "zh-CHS", "client": "pc", "fr": "browser_pc", "text": r.text, "pid": "sogou-dict-vr", "useDetect": "on", "useDetectResult": "on", "oxford": "on", "isReturnSugg": "on", "needQc": 1, "s": md5("autozh-CHS".concat(r.text).concat(KEY)) // 签名算法 }; GM_xmlhttpRequest({ method: "POST", url: "https://fanyi.sogou.com/reventondc/translateV1", headers: { "accept": "application/json", "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }, data: serialize(data), onload: function onload(response) { var result = JSON.parse(response.responseText); callback(result.data.translate.dit, r.index); // 执行回调,在回调中拼接 } }); }, caiyun: function caiyun(r, callback) { var data = { "source": r.text.split("\n"), "trans_type": "en2zh", "request_id": "web_fanyi", "media": "text", "os_type": "web", "dict": true, "cached": true, "replaced": true }; GM_xmlhttpRequest({ method: "POST", url: "https://api.interpreter.caiyunai.com/v1/translator", headers: { "accept": "application/json", "content-type": "application/json; charset=UTF-8", "X-Authorization": "token:cy4fgbil24jucmh8jfr5" }, data: JSON.stringify(data), onload: function onload(response) { var result = JSON.parse(response.responseText); callback(result.target.join("\n"), r.index); // 执行回调,在回调中拼接 } }); }, google: function google(r, callback) { var data = { "q": r.text, "client": "webapp", "sl": "auto", "tl": "zh-CN", "hl": "zh-CN", "dt": "t", "otf": 1, "pc": 1, "ssel": 0, "tsel": 0, "kc": 5, "tk": tk(r.text) }; GM_xmlhttpRequest({ method: "POST", url: "https://translate.google.cn/translate_a/single", headers: { "accept": "application/json", "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }, data: serialize(data), onload: function onload(response) { var result = JSON.parse(response.responseText), arr = []; result[0].forEach(function (t) { t && arr.push(t[0]); }); callback(arr.join(""), r.index); // 执行回调,在回调中拼接 } }); } }; return list[transServer].apply(null, arguments); } var liVideoDownloadTemplate = "\n\u5B57\u5E55\u6587\u4EF6\u683C\u5F0F\u4E3A.srt\uFF0C\u4E0B\u8F7D\u5728\u540E\u53F0\u8FDB\u884C\uFF0C\u8BF7\u8010\u5FC3\u7B49\u5F85
\n\u4E0B\u8F7D\u6B64\u6587\u4EF6\u610F\u5473\u7740\u4F60\u540C\u610F\u4F7F\u7528\u6B64\u6587\u4EF6\u7684\u884C\u4E3A\u987B\u7B26\u5408\u670D\u52A1\u6761\u6B3E\u4E2D\u7684\u7EA6\u5B9A\uFF0C\u4F60\u53EA\u80FD\u4EE5\u4E2A\u4EBA\u7528\u9014\u5728\u8BFE\u7A0B\u8BA2\u9605\u671F\u95F4\u89C2\u770B\u6B64\u6587\u4EF6\u4E2D\u7684\u5185\u5BB9\uFF0C\u4E0D\u5141\u8BB8\u516C\u5F00\u6216\u6563\u5E03\u6B64\u6587\u4EF6\u5185\u5BB9
\n