阿里云盘字幕

aliyun subtitle

目前为 2021-08-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         阿里云盘字幕
// @namespace    http://tampermonkey.net/
// @version      0.3.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==
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 regex = {
        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
                }
            })
        }
        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 = regex[subtitleType].getItems(text)
        let InfoArray = []
        itemArray.forEach((item) => {
            try {
                let info = regex[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
        // 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')
        GM_addStyle(`
            #subtitle {
                position: absolute; 
                display: flex; 
                flex-direction: column-reverse; 
                align-items: flex-end; 
                color: white; 
                width: 100%; 
                height: 100%; 
                z-index: 9;
                padding-bottom: 4vh;
            }
            #subtitle .subtitleText {
                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: 4.23vh;
                margin-top: 0px;
            }
            @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 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 ++
                        while (true) {
                            if (target >= arr[mid].from && target <= arr[mid].to) {
                                mid ++
                                continue
                            } else {
                                mid --
                                break
                            }
                        }
                        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 }
            // 遍历当前防止重复
            if (subtitleNode.childNodes.length) {
                for (let i=0;i<subtitleNode.childNodes.length;i++) {
                    if (subtitleNode.childNodes[i].getAttribute('index') == String(index)) {
                        return
                    }
                }
            }
            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', '')
            })
            // subtitleNode.appendChild(subtitleText)
            if (subtitleNode.firstChild) {
                subtitleNode.insertBefore(subtitleText, subtitleNode.firstChild)
            } else {
                subtitleNode.appendChild(subtitleText)
            }
            subtitleText.style = `animation: subtitle ${duration}s linear; 
                                  visibility: hidden;`
            // 记录结束时间
            window.startTime = oneSubtitle.from
            window.endTime = oneSubtitle.to
        }
        var config = { attributes: true, childList: true, subtree: true }
        var observer = new MutationObserver(insertSubtitle)
        observer.observe(percentNode, config)
        // 暂停播放事件
        let playBtnEvent = () => {
            setTimeout(() => {
                let isPlay = !videoStageNode.querySelector("video").paused
                if (isPlay) {
                    console.log('play')
                    insertSubtitle(null, null)
                } else {
                    console.log('pause')
                    insertSubtitle(null, null)
                    console.log('可见')
                    subtitleNode.childNodes.forEach((p) => {
                        p.style.visibility = 'visible'
                    })
                }
            }, 0)
        }
        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 = []
    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 || !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 ass file, exist
        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 ass file
        fetch(fileInfo.download_url, {headers: {Referer: 'https://www.aliyundrive.com/'}})
        .then(e => e.blob())
        .then(blob => {
            let reader = new FileReader()
            console.log('read subtitle text...')
            reader.onload = function(e) {
                let text = reader.result
                console.log('parse subtitle text...')
                let subtitles = parseTextToArray(text)
                let obs = addSubtitle(subtitles)
                console.log(`${subtitles.length}条字幕添加成功`)
                notification.open(`${subtitles.length}条字幕添加成功`, 3)
                obsArray.push(obs)
            }
            reader.readAsText(blob, fileInfo.content_type.includes('text/plain') ? 'GBK' : 'UTF-8')
        })
        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)
    }
    const observer = new MutationObserver(callback)
    observer.observe(rootNode, {childList: true})
})();