// ==UserScript== // @name 阿里云盘字幕 // @namespace http://tampermonkey.net/ // @version 0.3.5 // @description 让你的视频文件和字幕文件梦幻联动! // @author polygon // @match https://www.aliyundrive.com/drive* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM_addStyle // @runat document-start // @downloadURL none // ==/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 ` 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', '
') } }, }, 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('
').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('')) { console.log(text) notification.open(`登录信息已过期,请刷新`, 6) } fileInfo.text = text } reader.readAsText(blob, fileInfo.content_type.includes('text/plain') ? 'GBK' : '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.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 { if (subtitleNode.childNodes.length) { for (let i=0;i { // 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) => { return fileInfo.name !== filename && fileInfo.name.includes(title) }) // 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}) })();