// ==UserScript==
// @name 阿里云盘字幕
// @namespace http://tampermonkey.net/
// @version 0.2
// @description aliyun subtitle
// @author polygon
// @match https://www.aliyundrive.com/drive*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_addStyle
// @runat document-start
// ==/UserScript==
(function() {
'use strict'
// create new XMLHttpRequest
const regex = {
ass: {
line: /Dialogue:.+/g,
info: /Dialogue: 0,(.+?),(.+?),Default,,0000,0000,0000,,.*?{.+?}([^\n]+)/,
pureContent(content) {return content.replace('{\\r}\\N', '<br/>').replace('{\\r}', '').replace(/{.+?}/, '')}
},
other: {
line: null,
info: null,
pureContent() {}
}
}
const subtitleType = 'ass'
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
}
})
}
nativeSend.apply(this, arguments)
}
// 把小时 分钟 秒解析为秒
let toSeconds = timeStr => {
let timeArr = timeStr.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 parseAssToArray = (assText) => {
// Dialogue: 0,0:00:05.75,0:00:07.42,Default,,0000,0000,0000,,{\fnMicrosoft YaHei\fs20\bord1\shad1\3c&HFF8000&\b0}- 大家好 - 菲比{\r}\N{\fnTahoma\fs12\bord1\shad1\1c&HC0C0C0&\b0}- Hi, guys. JOEY: Hey, Phoebe.{\r}
let assLineArray = assText.match(regex[subtitleType].line)
let assInfoArray = []
assLineArray.forEach((assLine) => {
let [_, from, to, content] = regex[subtitleType].info.exec(assLine)
assInfoArray.push({
from: toSeconds(from),
to: toSeconds(to),
content: regex[subtitleType].pureContent(content)
})
})
console.log(assInfoArray)
return assInfoArray
}
// add subtitle to video
let addSubtitle = (subtitles) => {
console.log('addSubtitle...')
window.startTime = 0
window.endTime = 0
if (!subtitles.length) { return }
// 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]")
let subtitleNode = document.createElement('div')
subtitleNode.setAttribute('id', 'subtitle')
subtitleNode.innerHTML = `
<p class="subtitleText">
</p>
`
GM_addStyle(`
#subtitle {
position: absolute;
display: flex;
flex-direction: row;
align-items: flex-end;
color: white;
width: 100%;
height: 100%;
z-index: 9;
}
#subtitle .subtitleText {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
height: 20%;
color: white;
-webkit-text-stroke: 0.04rem black;
font-weight: bold;
font-size: 4.23vh;
position: absolute;
}
@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 = ""
}
let binarySearch = function (target, arr) {
var from = 0;
var to = arr.length - 1;
while (from <= to) {
var mid = parseInt(from + (to - from) / 2);
if (target >= arr[mid].from && target <= arr[mid].to) {
return mid
} else if (target > arr[mid].to) {
from = mid + 1;
} else {
to = mid - 1;
}
}
return -1;
}
var index = binarySearch(timeSec, subtitles)
if (index == -1) { return }
let oneSubtitle = subtitles[index]
if (oneSubtitle.content == window.currentSubtitle && subtitleNode.children.length) { return }
let subtitleText = document.createElement('p')
subtitleText.setAttribute('class', 'subtitleText')
subtitleText.innerHTML = oneSubtitle.content
let duration = oneSubtitle.to - oneSubtitle.from - (timeSec - oneSubtitle.from)
subtitleNode.appendChild(subtitleText)
console.log(oneSubtitle.from, window.endTime)
let offsetStyle = `bottom: ${3 * Number(oneSubtitle.from < window.endTime - 0.1 && mutationsList !== null && observer !== null)}em;`
console.log(offsetStyle)
subtitleText.style = `animation: subtitle ${duration}s linear;
visibility: hidden;
${offsetStyle}`
// 记录结束时间
window.endTime = parseFloat(oneSubtitle.to)
window.startTime = parseFloat(oneSubtitle.from)
window.currentSubtitle = oneSubtitle.content
}
var config = {
attributes: true,
childList: true,
subtree: true
}
var observer = new MutationObserver(insertSubtitle)
observer.observe(percentNode, config)
// 暂停播放事件
let playBtnEvent = () => {
let timeSec = totalSec * parseFloat(percentNode.style.width.replace('%', '')) / 100
let isPlay = !videoStageNode.querySelector("video").paused
console.log(isPlay)
console.log(window.event.type)
// isPlay = (window.event.type == 'click') ? !isPlay : isPlay
if (isPlay) {
// 播放状态
console.log('play')
if (timeSec < window.endTime) {
subtitleNode.innerHTML = ""
insertSubtitle(null, null)
}
} else {
// 暂停状态
console.log('pause')
if (timeSec < window.endTime) {
if (subtitleNode.lastChild) { (subtitleNode.lastChild.style.visibility = 'visible') }
}
}
}
window.addEventListener('keydown', () => {
if (window.event.which == 32) {
playBtnEvent()
} else if (window.event.which == 39 | window.event.which == 37) {
subtitleNode.innerHTML = ""
insertSubtitle(null, null)
}
})
document.querySelector('[class^=video-player]').addEventListener('click', () => {
setTimeout(() => {
playBtnEvent()
}, 0)
}, false)
}
// observer root
const rootNode = document.querySelector('#root')
// no root, exist
if (!rootNode) { return }
const callback = function (mutationList, observer) {
// add subtitle
let subtitleNode = document.querySelector('#subtitle')
if (subtitleNode) {subtitleNode.parentNode.removeChild(subtitleNode)}
let Node = mutationList[0].addedNodes[0]
if (!Node.getAttribute('class').includes('modal')) { return }
console.log('add a video modal')
let modal = Node
// find title name
let title = modal.querySelector('[class^=header-file-name]').innerText
let assFilename = title.replace(title.split('.').slice(-1)[0], subtitleType)
console.log(assFilename)
console.log(fileInfoList)
// search the corresponding ass url
let assInfo = fileInfoList.filter((fileInfo) => {
return fileInfo.name == assFilename
})
// no ass file, exist
console.log(assInfo)
if (!assInfo.length) {console.log('subtitle exit...'); return}
assInfo = assInfo[0]
console.log(assInfo)
// download ass file
fetch(assInfo.download_url, {headers: {Referer: 'https://www.aliyundrive.com/'}})
.then(e => e.text())
.then(text => {
console.log('parse ass text...')
let assInfoArray = parseAssToArray(text)
addSubtitle(assInfoArray)
})
rootNode.querySelector('[class*=next]').addEventListener('click', () => {
console.log('next')
console.log(modal.querySelector('[class^=header-file-name]').innerText)
console.log(title)
setTimeout(() => {
while (modal.querySelector('[class^=header-file-name]').innerText == title) {
console.log('loading')
}
callback([{addedNodes: [modal]}], null)
}, 0)
})
}
const observer = new MutationObserver(callback)
observer.observe(rootNode, {attributes: false, childList: true, subtree: false})
})();