阿里云盘字幕

让你的视频文件和字幕文件梦幻联动!

// ==UserScript==
// @name         阿里云盘字幕
// @namespace    http://tampermonkey.net/
// @version      0.3.8
// @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^=filename] span').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.includes('.')) return false
            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()
            }
            // 单音频单字幕文件直接匹配,因为没得选哦
            if (fileInfoList.length == 2) {
                flag = true
            }
            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})
})();