// ==UserScript==
// @name 阿里云盘字幕
// @namespace http://tampermonkey.net/
// @version 0.3.6
// @description 让你的视频文件和字幕文件梦幻联动!
// @author polygon
// @match https://www.aliyundrive.com/drive*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_addStyle
// @runat document-start
// ==/UserScript==
const notification = (function() {
'use strict';
GM_addStyle(`
#notification {
box-sizing: border-box;
position: fixed;
left: calc(50% - 365.65px / 2);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 50px;
background-color: #ff7675;
border-radius: 50px;
padding: 0 0px 0px 20px;
top: -50px;
transition: top .5s ease-out;
z-index: 9999999999;
}
#notification .content {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 25px;
}
#notification .closeBox {
margin: 0 10px;
transform: rotate(90deg);
cursor: pointer;
}
#notification .closeBox .progress {
margin: 0 10px;
cursor: pointer;
}
#notification .closeBox .progress .circle {
stroke-dasharray: 100;
animation: progressOffset 0s linear;
}
@keyframes progressOffset {
from {
stroke-dashoffset: 100;
}
to {
stroke-dashoffset: 0;
}
}
`)
return {
open(info, timeout, autoClose=true) {
let eles = document.querySelectorAll('#notification')
for (let i=0;i<eles.length;i++) {
document.body.removeChild(eles[i])
}
this.box = document.createElement('div')
this.box.setAttribute('id', 'notification')
this.box.innerHTML = `
<div class="content"></div>
<svg class="closeBox" width="40" height="40">
<g class="close" style="stroke: white; stroke-width: 2; stroke-linecap: round;">
<line x1="13" y1="13" x2="27" y2="27"/>
<line x1="13" y1="27" x2="27" y2="13"/>
</g>
<g class="progress" fill="transparent" stroke-width="3">
<circle class="background" cx="20" cy="20" r="16" stroke="rgba(255,255,255,0.15)"/>
<circle class="circle" cx="20" cy="20" r="16" stroke="rgba(255,255,255,1)"/>
</g>
</svg>
`
document.body.appendChild(this.box)
this.box.querySelector('.content').innerHTML = info
let width = getComputedStyle(this.box).width
this.box.style.left = `clac(50%-${width}/2)`
this.box.querySelector('.closeBox .progress .circle').style['animation-duration'] = `${timeout}s`
this.box.style.top = '100px'
this.box.querySelector('.closeBox .progress').addEventListener('click', () => {
console.log('you close...')
this.close()
console.log('you clear...')
})
if (autoClose) {
setTimeout(() => {
console.log('timeout close...')
this.close()
console.log('timeout clear ...')
}, timeout * 1000)
}
},
close() {
this.box.style['transition-duration'] = '.23s'
this.box.style['transition-timing-function'] = 'eaer-out'
this.box.style.top = '-50px'
setTimeout(() => {
try {
document.body.removeChild(this.box)
} catch {
console.log('clear')
}
}, 1000)
}
}
})();
(function() {
'use strict'
// create new XMLHttpRequest
const subtitleParser = {
ass: {
getItems(text) { return text.match(/Dialogue:.+/g) },
getInfo(item) {
let [from, to, content] = /Dialogue: 0,(.+?),(.+?),.*?,.*?,.*?,.*?,.*?,.*?,([^\n]+)/.exec(item).slice(1)
return {
from: toSeconds(from),
to: toSeconds(to),
content: content.replace(/{[\s\S]*?}/g, '').replace('\\N', '<br/>')
}
},
},
srt: {
getItems(text) { return text.split('\r\n\r\n') },
getInfo(item) {
let lineArray = item.split('\r\n').slice(1)
let [from, to] = lineArray[0].split(' --> ')
return {
from: toSeconds(from),
to: toSeconds(to),
content: lineArray.slice(1).join('<br/>').replace(/{[\s\S]*?}/g, '')
}
},
},
}
let subtitleType
let fileInfoList = null
const nativeSend = window.XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.send = function() {
if (this.openParams[1].includes('file/list')) {
this.addEventListener("load", function(event) {
let target = event.currentTarget
if (target.readyState == 4 && target.status == 200) {
fileInfoList = JSON.parse(target.response).items
console.log('saving all subtitle text...')
fileInfoList.forEach(fileInfo => {
if (Object.keys(subtitleParser).includes(fileInfo.file_extension)) {
// download file
console.log('caching ' + fileInfo.name)
fetch(fileInfo.download_url, {headers: {Referer: 'https://www.aliyundrive.com/'}})
.then(e => e.blob())
.then(blob => {
let reader = new FileReader()
reader.onload = function(e) {
// 可能资源链接过期
let text = reader.result
if (text.includes('<Error>')) {
console.log(text)
notification.open(`资源链接已过期,请刷新页面`, 6)
return
}
// 可能错误编码方式
if (text.indexOf("�") !== -1) {
console.log('ERROR in UTF-8')
console.log(`GBK ${fileInfo.name}`)
return reader.readAsText(blob, 'GBK')
}
fileInfo.text = text
}
console.log(`UTF-8 ${fileInfo.name}`)
reader.readAsText(blob, 'UTF-8')
})
}
})
}
})
}
nativeSend.apply(this, arguments)
}
let toSeconds = (timeStr) => {
let timeArr = timeStr.replace(',', '.').split(':')
let timeSec = 0
for (let i = 0; i < timeArr.length; i++) {
timeSec += 60 ** (timeArr.length - i - 1) * parseFloat(timeArr[i])
}
return timeSec
}
// parse subtitle
let parseTextToArray = (text) => {
let itemArray = subtitleParser[subtitleType].getItems(text)
let InfoArray = []
itemArray.forEach((item) => {
try {
let info = subtitleParser[subtitleType].getInfo(item)
InfoArray.push(info)
} catch {
console.log(`[ERROR] ${item}`)
}
})
console.log(InfoArray)
return InfoArray
}
// add subtitle to video
let addSubtitle = (subtitles) => {
console.log('add subtitle...')
window.startTime = 0
window.endTime = 0
const fontsize = 4.23
// 00:00
let percentNode = document.querySelector("[class^=modal] [class^=progress-bar] [class^=current]")
let totalTimeNode = document.querySelector("[class^=modal] [class^=progress-bar] span:last-child")
// create a subtitle div
const videoStageNode = document.querySelector("[class^=video-stage]")
subtitleNode && subtitleNode.parentNode && subtitleNode.parentNode.removeChild(subtitleNode)
subtitleNode = document.createElement('div')
subtitleNode.setAttribute('id', 'subtitle')
GM_addStyle(`
#subtitle {
position: absolute;
display: flex;
flex-direction: column-reverse;
align-items: flex-end;
color: white;
width: 100%;
height: 100%;
bottom: 4vh;
transition: bottom .2s linear;
z-index: 9;
}
#subtitle .subtitleText {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
color: white;
-webkit-text-stroke: 0.04rem black;
font-weight: bold;
font-size: ${fontsize}vh;
visibility: hidden;
}
@keyframes subtitle {
from {
visibility: visible;
}
to {
visibility: visible;
}
}
`)
videoStageNode.appendChild(subtitleNode)
console.log('add subtitleNode')
// 观察变化
const totalSec = toSeconds(totalTimeNode.textContent)
console.log(`total time is ${totalSec}s`)
let insertSubtitle = function (mutationsList, observer) {
// 00:00:00 => 秒
let timeSec = totalSec * parseFloat(percentNode.style.width.replace('%', '')) / 100
// 保护时间,防止重复
if (timeSec > window.endTime || timeSec < window.startTime){
// 此时用户可能在拖动进度条,反之拖动后重叠,清空subtitleNode
subtitleNode.innerHTML = ""
} else {
let pTags = subtitleNode.querySelectorAll('[animationend]')
for (let i=0;i<pTags.length;i++) {
subtitleNode.removeChild(pTags[i])
}
}
let existIndex = (index) => {
if (subtitleNode.childNodes.length) {
for (let i=0;i<subtitleNode.childNodes.length;i++) {
if (subtitleNode.childNodes[i].getAttribute('index') == String(index)) {
return true
}
}
}
return false
}
let continueSearch = (index, target, arr, direction, flag=false) => {
// flag=true,为反向查找开一次路
if (existIndex(index) || flag) {
// 存在,继续向下查找
direction ? index ++ : index --
if (arr[index] && target >= arr[index].from && target <= arr[index].to) {
return continueSearch(index, target, arr)
} else {
// 没有包含,而且已存在当前,返回无
return ''
}
} else {
// 不存在index直接返回
// 返回string,因为0索引会被误认为false
return String(index)
}
}
let binarySearch = function (target, arr) {
var from = 0;
var to = arr.length - 1;
while (from <= to) {
let mid = parseInt(from + (to - from) / 2)
if (target >= arr[mid].from && target <= arr[mid].to) {
// 先向上查找,略过mid本身,在向下查找,包括mid
let index = continueSearch(mid, target, arr, false, true) || continueSearch(mid, target, arr, true)
return index ? Number(index) : -1
} else if (target > arr[mid].to) {
from = mid + 1;
} else {
to = mid - 1;
}
}
return -1;
}
var index = binarySearch(timeSec, subtitles)
if (index == -1) { return false}
let oneSubtitle = subtitles[index]
let subtitleText = document.createElement('p')
subtitleText.setAttribute('class', 'subtitleText')
subtitleText.setAttribute('index', String(index))
subtitleText.innerHTML = oneSubtitle.content
let duration = oneSubtitle.to - oneSubtitle.from - (timeSec - oneSubtitle.from)
subtitleText.addEventListener('animationend', function() {
subtitleText.setAttribute('animationend', '')
})
// 合适位置插入
if (subtitleNode.childNodes.length) {
// debugger
let bottom = '0px'
let i = 0
while (true) {
if (subtitleNode.childNodes[i]) {
if (parseFloat(bottom.replace('px', '')) < parseFloat(subtitleNode.childNodes[i].style.bottom.replace('px', ''))) {
subtitleText.style.bottom = bottom
subtitleNode.insertBefore(subtitleText, subtitleNode.childNodes[i])
break
} else {
bottom = getComputedStyle(subtitleNode.childNodes[i]).height
i ++
continue
}
} else {
// px -> vh 相对高度,调整窗口自适应
subtitleText.style.bottom = parseFloat(bottom.replace('px', '')) / parseFloat(window.innerHeight) * 100 + 'vh'
subtitleNode.appendChild(subtitleText)
break
}
}
} else {
subtitleNode.appendChild(subtitleText)
}
subtitleText.style.animation = `subtitle ${duration}s linear`
// 记录结束时间
window.startTime = oneSubtitle.from
window.endTime = oneSubtitle.to
return true
}
var config = { attributes: true, childList: true, subtree: true }
var observer = new MutationObserver(insertSubtitle)
observer.observe(percentNode, config)
// 暂停播放事件
let playBtnEvent = () => {
subtitleNode.innerHTML = ""
while (true) {
if (!insertSubtitle(null, null)) {
break
}
}
subtitleNode.childNodes.forEach((p) => {
p.style.visibility = 'visible'
})
}
window.addEventListener('keydown', () => {
if (window.event.which == 32 | window.event.which == 39 | window.event.which == 37) {
playBtnEvent()
}
})
document.querySelector('[class^=video-player]').addEventListener('click', () => {
playBtnEvent()
}, false)
return observer
}
// observer root
const rootNode = document.querySelector('#root')
// no root, exist
if (!rootNode) { return }
let obsArray = [], subtitleNode
const callback = function (mutationList, observer) {
// add subtitle
subtitleNode = document.querySelector('#subtitle')
if (subtitleNode) {subtitleNode.parentNode.removeChild(subtitleNode)}
let Node = mutationList[0].addedNodes[0]
if (!Node || !Node.getAttribute('class').includes('modal')) { return }
// clear observer
obsArray.forEach(obs => {
console.log(obs)
console.log('disconnect')
obs.disconnect()
})
obsArray = []
console.log('add a video modal')
let modal = Node
// find title name
let filename = modal.querySelector('[class^=header-file-name]').innerText
let title = filename.split('.').slice(0, -1).join('.')
console.log(title)
console.log(fileInfoList)
// search the corresponding ass url
let fileInfo = fileInfoList.filter((fileInfo) => {
if (fileInfo.name == filename) return false
// 你中有我,或我中有你
let flag = fileInfo.name.match(new RegExp(title, 'i')) || title.match(new RegExp(fileInfo.name.split('.').slice(0, -1).join('.'), 'i'))
// S01E01样式匹配
const reg = /s\d+e\d+/i
let subtitleMatch = fileInfo.name.match(reg)
let videoMatch = title.match(reg)
if (!flag) {
flag = subtitleMatch && videoMatch && subtitleMatch[0].toUpperCase() == videoMatch[0].toUpperCase()
}
return flag
})
console.log(fileInfo)
// no file, exit
if (!fileInfo.length) {console.log('subtitle exit...'); return}
fileInfo = fileInfo[0]
console.log(fileInfo)
subtitleType = fileInfo.name.split('.').slice(-1)
console.log(`[subtitleType] ${subtitleType}`)
// download file
let subtitles = parseTextToArray(fileInfo.text)
obsArray.push(addSubtitle(subtitles))
console.log(`${subtitles.length}条字幕添加成功`)
notification.open(`${subtitles.length}条字幕添加成功`, 3)
// 是否变更视频
let obs = new MutationObserver((mutationList, obs) => {
let filenameNode = modal.querySelector('[class^=header-file-name]')
if (filenameNode && filenameNode.innerText !== filename) {
setTimeout(() => {
callback([{addedNodes: [modal]}], null)
}, 0)
}
})
obs.observe(modal, {subtree: true, childList: true})
obsArray.push(obs)
// 是否触发控制条
let playerTool = document.querySelector('[class^=video-player]')
let offsetSubtitle = (mutationList, obs) => {
// let subtitleNode = document.querySelector('#subtitle')
if (subtitleNode && mutationList[0].attributeName == 'class') {
if (mutationList[0].target.classList.length == 2 && document.fullscreenElement) {
subtitleNode.style['bottom'] = '13vh'
} else {
subtitleNode.style['bottom'] = '4vh'
}
}
}
obs = new MutationObserver(offsetSubtitle)
obs.observe(playerTool, {attributes: true, childList: true})
offsetSubtitle([{attributeName: 'class', target: playerTool}])
obsArray.push(obs)
document.onfullscreenchange = () => {
offsetSubtitle([{attributeName: 'class', target: playerTool}], obs)
}
}
const observer = new MutationObserver(callback)
observer.observe(rootNode, {childList: true})
})();