Greasy Fork

Greasy Fork is available in English.

阿里云盘字幕

aliyun subtitle

当前为 2021-08-30 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         阿里云盘字幕
// @namespace    http://tampermonkey.net/
// @version      0.3.3
// @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]")
        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%; 
                padding-bottom: 4vh;
                z-index: 9;
            }
            #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 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 (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
            console.log('添加' + oneSubtitle.content)
            let duration = oneSubtitle.to - oneSubtitle.from - (timeSec - oneSubtitle.from)
            subtitleText.addEventListener('animationend', function() {
                subtitleText.setAttribute('animationend', '')
            })
            subtitleNode.appendChild(subtitleText)
            subtitleText.style = `animation: subtitle ${duration}s linear; 
                                  visibility: hidden;`
            // 记录结束时间
            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 = () => {
            setTimeout(() => {
                console.log('==================按键事件==================')
                subtitleNode.innerHTML = ""
                while (true) {
                    if (!insertSubtitle(null, null)) {
                        break
                    }
                }
                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 = [], 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
        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)
        // 是否触发控制条
        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['padding-bottom'] = '12vh'
                } else {
                    subtitleNode.style['padding-bottom'] = '4vh'
                }
            }
        }
        obs = new MutationObserver(offsetSubtitle)
        document.onfullscreenchange = () => {
            offsetSubtitle([{attributeName: 'class', target: playerTool}], obs)
        }
        obs.observe(playerTool, {attributes: true, childList: true})
        obsArray.push(obs)
    }
    const observer = new MutationObserver(callback)
    observer.observe(rootNode, {childList: true})
})();