专门针对油管手机版:自动打开声音,自动开启字幕并机翻中英双语,自动跳广告。安卓浏览器可使用Firefox或者旧版本yandex。有问题联系:知乎@邓强龙,不一定每个视频都有中英字幕。 测试中英双语字幕视频网址: https: //m.youtube.com/watch?v=xFQGKwVijaM&t=2s
// ==UserScript==
// @name YouTube移动端中英双语字幕
// @namespace https://github.com/warmilk/UserScript
// @match *://m.youtube.com
// @match *://m.youtube.com/*
// @match *://m.youtube.com/watch?v=*
// @grant unsafeWindow
// @author github@warmilk
// @version 1.0.3
// @description 专门针对油管手机版:自动打开声音,自动开启字幕并机翻中英双语,自动跳广告。安卓浏览器可使用Firefox或者旧版本yandex。有问题联系:知乎@邓强龙,不一定每个视频都有中英字幕。 测试中英双语字幕视频网址: https: //m.youtube.com/watch?v=xFQGKwVijaM&t=2s
// ==/UserScript==
(function() {
// @require https://cdn.bootcdn.net/ajax/libs/vConsole/2.5.0/vconsole.min.js
// const vConsole = new VConsole();
const FORMET_SUFFIX = '&fmt=json3&xorb=2&xobt=3&xovt=3&tlang='
const THIS = this; //当前作用域的window对象
const ELEMENTID = { //字幕轨道的HTML元素的id属性值
zh: 'QIANG_LONG_CAPTION_zh', //中文
en: 'QIANG_LONG_CAPTION_en', //英文
}
var styleOptions = { // 样式配置项
height: '18%', //中英字幕的父容器相对于<video>区域的高度占比
};
var captionFetchUrl = {
zh: '', //中文字幕str格式array json的下载地址
en: '', //英文字幕str格式array json的下载地址
}
var isHasCaption = { //该视频是否存在某种语言的字幕
zh: true,
en: true,
}
var videoNode //video标签的dom节点
function isVideoAdsTime() {
var ad = document.querySelector('.ad-showing');
var skipAdButton = document.querySelector('.ytp-ad-skip-button');
if (skipAdButton) {
skipAdButton.click();
}
return ad != null;
}
//自动跳过广告
function FuckAds() {
setInterval(function() {
try {
isVideoAdsTime();
} catch (err) {}
}, 1000);
}
//自动点击左上角音量按钮,打开声音
function OpenVolume() {
var volumeButton = document.querySelector('.ytp-unmute.ytp-popup.ytp-button.ytp-unmute-animated.ytp-unmute-shrink');
volumeButton.click();
return volumeButton != null;
}
function GenerateCaptionsUrl(toLang) {
var url = THIS.ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer.captionTracks[0].baseUrl;
if (url.indexOf('&lang=zh-Hant') !== -1) {
url = url.replace('&lang=zh-Hant', '&lang=zh-Hans');
}
return url + FORMET_SUFFIX + toLang;
}
//获取字幕的请求地址
function GetCaptionUrl() {
try {
// if (THIS == null) {
// setTimeout(function() {
// continue;
// }, 1000);
// }
var captionTracks = THIS.ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer.captionTracks
if (captionTracks.length > 0) { //判断是否存在字幕可用
for (var i = 0; i < captionTracks.length; i++) {
var item = captionTracks[i];
switch (item.languageCode) {
case 'zh-Hans':
isHasCaption.zh = true;
console.log('这个视频有中文字幕(zh-Hans)');
break;
case 'zh-CN':
console.log('这个视频有中文字幕(zh-CN)');
break;
case 'en':
isHasCaption.en = true;
console.log('这个视频有英文字幕(en)');
break;
default:
isHasCaption.en = true;
isHasCaption.zh = true;
break;
}
}
captionFetchUrl.zh = GenerateCaptionsUrl('zh-Hans')
captionFetchUrl.en = GenerateCaptionsUrl('en')
} else {
console.log('这个视频没有字幕');
}
} catch (err) {}
}
function both_ZH_EN(parentNode) {
var wrapper_zh = document.createElement('div');
var wrapper_en = document.createElement('div');
parentNode.appendChild(wrapper_zh)
parentNode.appendChild(wrapper_en)
var caption_zh = document.createElement('div');
var caption_en = document.createElement('div');
wrapper_zh.appendChild(caption_zh)
wrapper_en.appendChild(caption_en)
parentNode.style.pointerEvents = 'none';
parentNode.style.position = 'absolute';
parentNode.style.zIndex = 2;
parentNode.style.width = '100%';
parentNode.style.height = styleOptions.height;
// parentNode.style.backgroundColor = 'red';
parentNode.style.bottom = 0;
parentNode.style.left = 0;
wrapper_zh.style.position = wrapper_en.style.position = 'absolute';
wrapper_zh.style.zIndex = wrapper_en.style.zIndex = 3;
wrapper_zh.style.width = wrapper_en.style.width = '100%';
wrapper_zh.style.height = wrapper_en.style.height = '50%';
wrapper_zh.style.left = wrapper_en.style.left = 0;
wrapper_zh.style.textAlign = wrapper_en.style.textAlign = 'center';
// wrapper_zh.style.backgroundColor = 'yellow';
// wrapper_en.style.backgroundColor = 'blue';
wrapper_zh.style.bottom = 0;
wrapper_en.style.top = 0;
caption_zh.style.backgroundColor = caption_en.style.backgroundColor = '#000';
caption_zh.style.color = caption_en.style.color = '#fff';
caption_zh.style.display = caption_en.style.display = 'inline-block';
caption_zh.id = ELEMENTID.zh;
caption_en.id = ELEMENTID.en;
caption_zh.innerText = '';
caption_en.innerText = '';
}
function just_EN(parentNode) {
var wrapper_en = document.createElement('div');
parentNode.appendChild(wrapper_en)
var caption_en = document.createElement('div');
wrapper_en.appendChild(caption_en)
parentNode.style.pointerEvents = 'none';
parentNode.style.position = 'absolute';
parentNode.style.zIndex = 2;
parentNode.style.width = '100%';
parentNode.style.height = styleOptions.height;
// parentNode.style.backgroundColor = 'red';
parentNode.style.bottom = 0;
parentNode.style.left = 0;
wrapper_en.style.position = 'absolute';
wrapper_en.style.zIndex = 3;
wrapper_en.style.width = '100%';
wrapper_en.style.height = '50%';
wrapper_en.style.left = 0;
wrapper_en.style.textAlign = 'center';
wrapper_en.style.bottom = 0;
caption_en.style.backgroundColor = '#000';
caption_en.style.color = '#fff';
caption_en.style.display = 'inline-block';
caption_en.id = ELEMENTID.en;
caption_en.innerText = '';
}
function just_ZH(parentNode) {
var wrapper_zh = document.createElement('div');
parentNode.appendChild(wrapper_zh)
var caption_zh = document.createElement('div');
wrapper_zh.appendChild(caption_zh)
parentNode.style.pointerEvents = 'none';
parentNode.style.position = 'absolute';
parentNode.style.zIndex = 2;
parentNode.style.width = '100%';
parentNode.style.height = styleOptions.height;
// parentNode.style.backgroundColor = 'red';
parentNode.style.bottom = 0;
parentNode.style.left = 0;
wrapper_zh.style.position = 'absolute';
wrapper_zh.style.zIndex = 3;
wrapper_zh.style.width = '100%';
wrapper_zh.style.height = '50%';
wrapper_zh.style.left = 0;
wrapper_zh.style.textAlign = 'center';
wrapper_zh.style.bottom = 0;
caption_zh.style.backgroundColor = '#000';
caption_zh.style.color = '#fff';
caption_zh.style.display = 'inline-block';
caption_zh.id = ELEMENTID.en;
caption_zh.innerText = '';
}
//创建字幕控件的 HTML dom结构
function CreateCaptionWidget() {
var videoPlayerWrapper = document.querySelector('#player-container-id')
if (videoPlayerWrapper == null) {
console.log('无法获取#player-container-id的元素,导致无法创建字幕控件html dom');
return;
}
var captionWidget = document.createElement('div');
videoPlayerWrapper.appendChild(captionWidget)
if (isHasCaption.zh && isHasCaption.en) {
both_ZH_EN(captionWidget);
}
if (!isHasCaption.zh && isHasCaption.en) {
just_EN(captionWidget);
}
if (isHasCaption.zh && !isHasCaption.en) {
just_ZH(captionWidget);
}
if (!isHasCaption.zh && !isHasCaption.en) {
return;
}
}
/**
* 发请求获取YouTube提供的str格式的字幕
*
* @param ajaxUrl {String} 获取字幕的请求url
* @param languageTag {String} 字幕语言的标记,zh:中文,en:英文
* @param callbackFn {Function} 成功fetch字幕数据后的回调函数
*/
function FetchCaptionFromYoutube(ajaxUrl, languageTag) {
var fullUrl = location.origin + ajaxUrl
var langMap = {
'en': '【英文】',
'zh': '【中文】',
}
if (ajaxUrl !== '') {
fetch(ajaxUrl)
.then(function(response) {
response.json().then(function(response) {
var resultList = response.events;
console.log('获取', langMap[languageTag], '字幕成功。url:', fullUrl);
InjectCaptionsToPage(resultList, languageTag)
}).catch(function(err) {
console.log(err);
console.log('获取', langMap[languageTag], '字幕失败。url:', fullUrl);
});
}).catch(function(err) {
console.log('获取', langMap[languageTag], '字幕失败。url:', fullUrl);
});
}
}
function GetTackNode(languageTag) {
var id = '#' + ELEMENTID[languageTag]
return document.querySelector(id)
}
/**
* 把字幕数据注入到页面(str就是time txt也就是带时间轴的txt)
*
* @param captionContentList {Array} 字幕数据
* @param languageTag {String} zh:中文,en:英文
*/
function InjectCaptionsToPage(captionContentList, languageTag) {
var targetNode = GetTackNode(languageTag); //{HTMLnode} 字幕轨道的html元素节点(注入数据的目标节点)
videoNode = document.querySelector('video.video-stream.html5-main-video') //<video>标签对应的元素
setInterval(function() {
videoNode.addEventListener('timeupdate', function() {
videoNode.currentTime = videoNode.currentTime * 1000; //<video>元素的currentTime属性单位为秒
if (captionContentList != null && captionContentList instanceof Array) {
for (var i = 0; i < captionContentList.length; i++) {
var item = captionContentList[i];
if (!item || !item.segs || !(item.segs instanceof Array)) {
continue;
}
if (videoNode.currentTime >= item.tStartMs && videoNode.currentTime <= item.tStartMs + item.dDurationMs) {
if (targetNode !== null) {
try {
var text = [];
for (var k = 0; k < item.segs.length; k++) {
text.push(item.segs[k].utf8);
}
var displayText = text.join(' ');
displayText = displayText.replace(/\s+/ig, ' ');
if (targetNode.innerText !== displayText) {
targetNode.innerText = displayText;
}
} catch (err) {
continue;
}
}
break;
}
}
}
})
}, 300);
}
function Main() {
GetCaptionUrl();
CreateCaptionWidget();
FetchCaptionFromYoutube(captionFetchUrl.zh, 'zh');
FetchCaptionFromYoutube(captionFetchUrl.en, 'en');
}
window.addEventListener("load", function() {
OpenVolume();
//FuckAds();
Main();
})
// window.addEventListener("popstate", function() {
// console.log("popstate");
// Main();
// })
// window.addEventListener("pushState", function() {
// console.log("pushState");
// Main();
// })
// window.addEventListener("replaceState", function() {
// console.log("replaceState");
// Main();
// })
// window.addEventListener("hashchange", function() {
// console.log("hashchange");
// Main();
// })
// THIS.history.forward = function() {
// console.log('forward');
// }
})();