B站大会员影视+弹幕+字幕

B站观影 弹幕 字幕替换

当前为 2021-07-05 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         B站大会员影视+弹幕+字幕
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  B站观影 弹幕 字幕替换
// @author       Polygon
// @match        https://www.cuan.la/m3u8.php*
// @match        https://vip.parwix.com/*
// @require      https://greasyfork.org/scripts/407985-ajax-hook/code/Ajax-hook.js?version=940269
// @include      https://www.bilibili.com/bangumi/play/*
// @include      https://www.bilibili.com/video/BV*
// @require      http://code.jquery.com/jquery-1.11.0.min.js
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @run-at       document-ildm
// @connect      *
// ==/UserScript==
async function dataQuery() {
    'use strict'
    var cid, aid
    if (document.URL.startsWith('https://www.bilibili.com/')) {
        // 网页加载完后在bili网站获取cid,aid,并储存
        (function() {
            if (typeof cid == "undefined") {
                cid = unsafeWindow.__INITIAL_STATE__.epInfo.cid
                console.log('更新cid=' + unsafeWindow.__INITIAL_STATE__.epInfo.cid)
                GM_setValue('cid', cid)
            }
            if (typeof aid == "undefined") {
                aid = unsafeWindow.__INITIAL_STATE__.epInfo.aid
                console.log('更新aid=' + aid)
                GM_setValue('aid', aid)
            }
        })()
        var danmu = { code: 23, msg: "success", dannum: 0, danmuku: [], name: cid }
        // 字幕查询
        let subtitleApi = 'https://api.bilibili.com/x/player/v2?'
        let season_id = document.querySelector("meta[property='og:url']").content.match(/ss(\d+)/g)[0].replace('ss', '')
        let params = ['aid=' + aid, 'cid=' + cid, 'season_id=' + season_id]
        console.log(subtitleApi + params.join('&'))
        let subtitleUrl = await fetch(subtitleApi + params.join('&'), { credentials: 'include' })
            .then(res => res.json())
            .then(res => {
                console.log(res)
                if (!res['data']['subtitle']['subtitles'].length) {
                    console.log('未发现字幕'); return ''
                } else {
                    // 0中文简体 1中文繁体
                    let index = (res['data']['subtitle']['subtitles'][0]['lan_doc'].search('简体') !== -1) ? 0: 1
                    let url = 'https:' + res['data']['subtitle']['subtitles'][index]['subtitle_url']
                    console.log(`发现字幕 [${res['data']['subtitle']['subtitles'][index]['lan_doc']}]- ` + url)
                    return url
                }
            })
        var subtitleArray = []
        if (subtitleUrl) {
            subtitleArray = await fetch(subtitleUrl)
                .then(res => res.json())
                .then(res => res.body)
            console.log(subtitleArray)
        }
        GM_setValue('subtitleArray', subtitleArray)
        // 弹幕
        let danmuApi = `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`
        danmu = await fetch(danmuApi, { credentials: 'include' })
            .then(res => res.text())
            .then(restxt => {
                let matchObj = restxt.match(/<d p=".+?">.+?<\/d>/g)
                matchObj.forEach(ele => {
                    let r = /<d p="(.+?)">(.+?)<\/d>/g.exec(ele)
                    let params = r[1].split(',')
                    let content = r[2]
                    let time = params[0]
                    let direction = parseInt(params[1])
                    let fontsize = params[2]
                    let color = '#' + parseInt(params[3]).toString(16)
                    let direction_info
                    if (direction <= 3) {
                        direction_info = 'right'
                    } else if (direction == 4) {
                        direction_info = 'bottom'
                    } else if (direction == 5) {
                        direction_info = 'top'
                    } else {
                        direction_info = 'right'
                    }
                    danmu.danmuku.push([time, direction_info, color, "", content, "", "", `${fontsize}px`])
                })
                danmu.danmuku.push(["0", 'top', '#FF616D', "", `替换弹幕源成功,前方共有${danmu.danmuku.length}条弹幕,请做好准备哟`, "", "", "25px"])
                danmu.dannum = danmu.danmuku.length
                return danmu
            })
        GM_setValue('danmu', danmu)
        console.log(danmu)
    }
    return danmu
}
function pageChange() {
    // 添加按钮和frame
    let parentId = '#toolbar_module'
    let addSubtitle = () => {
        if (document.URL.startsWith('https://www.bilibili.com/')) { return }
        // 字幕 参考 <bilibili外挂字幕 哔哩哔哩外挂字幕>
        // https://raw.githubusercontent.com/jonwinters/bilibili-subtitles-plugin/master/build.dist.js
        let subtitles = GM_getValue('subtitleArray')
        window.endTime = 0
        window.offsetSubtitle = 0
        if (!subtitles.length) { return }
        subtitles.unshift({ content: '字幕加载成功[简体中文]', from: 1, location: 2, to: 4 })
        // 00:00
        let percentNode = document.querySelector("[class*='player-played']")
        let totalTime = document.querySelector("[class*='player-dtime']")
        if (!(percentNode && totalTime)) { return }
        console.log(subtitles)
        // 把小时 分钟 秒解析为秒
        let toSecond = timeStr => {
            let timeArr = timeStr.split(':')
            let timeSec = 0
            for (let i = 0; i < timeArr.length; i++) {
                timeSec += 60 ** (timeArr.length - i - 1) * parseInt(timeArr[i])
            }
            return timeSec
        }
        let totalSec = toSecond(totalTime.textContent)
        console.log(totalSec)
        let subtitleNode = document.querySelector("div[class*='player-subtitle']")
        let insertSubtitle = function (mutationsList, observer) {
            // 00:00:00 => 秒
            let timeSec = totalSec * parseFloat(percentNode.style.width.replace('%', '')) / 100
            // 保护时间,防止重复
            if (timeSec < window.endTime - 1 && timeSec >= window.endTime - 4) {
                return
            }
            // console.log(timeSec)
            // 快速查找
            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 }
            // 两个解析播放器都支持这个div,应该是基于一套模板
            //<div class="leleplayer-subtitle yzmplayer-subtitle" >替换弹幕源成功,前方共有223条弹幕,请做好准备哟</div>
            let oneSubtitle = subtitles[index]
            if (oneSubtitle.content == window.currentSubtitle) { return }
            let subtitleDiv = document.createElement('div')
            subtitleDiv.setAttribute('class', 'leleplayer-subtitle yzmplayer-subtitle player-subtitle .leleplayer-danmaku')
            subtitleDiv.innerHTML = oneSubtitle.content
            let duration = oneSubtitle.to - oneSubtitle.from - (timeSec - oneSubtitle.from)
            subtitleNode.appendChild(subtitleDiv)
            let offsetStyle = `bottom: ${30 * (parseInt(oneSubtitle.from) < window.endTime - 1)}px;`
            // 此处可以自定义一些东西
            let customStyle = ''
            subtitleDiv.style = `animation: danmaku-center ${duration}s linear;visibility: hidden; ${offsetStyle} ${customStyle}`
            // 记录结束时间
            window.endTime = parseFloat(oneSubtitle.to)
            window.currentSubtitle = oneSubtitle.content
        }
        var config = {
            attributes: true,
            childList: true,
            subtree: true
        }
        var observer = new MutationObserver(insertSubtitle)
        observer.observe(percentNode, config)
    }
    let addButton = () => {
        // 切换
        if (!document.URL.startsWith('https://www.bilibili.com/')) { return }
        let ele = document.querySelector(parentId)
        let switchButton = document.createElement("div")
        switchButton.setAttribute('class', 'share-info')
        switchButton.setAttribute('id', 'switch')
        switchButton.innerHTML = '<i class="iconfont"></i><span style="background-color: #FB7299; border: 1px solid #FB7299; color: #fff; border-radius: 16px; text-align: center;">切换</span> <!---->'
        ele.appendChild(switchButton)
        let modules = document.querySelectorAll('.player-module')
        switchButton.addEventListener('click', function () {
            let activateIndex
            for (let i = 0; i < modules.length; i++) {
                if (modules[i].style.display == 'block') {
                    activateIndex = i
                    modules[i].style.display = 'none'
                    break
                }
            }
            let nextIndex = (activateIndex < modules.length - 1) ? activateIndex + 1 : 0
            modules[nextIndex].style.display = 'block'
            // 按钮颜色
            let color
            if (nextIndex == 0) {
                color = '#fb7299'
            } else {
                color = origin[nextIndex - 1].color
            }
            switchButton.querySelector('span').style['background-color'] = color
            switchButton.querySelector('span').style['border-color'] = color
        })
        $(switchButton).hover(function () {
            let gray = '#757575'
            switchButton.querySelector('span').style['background-color'] = gray
            switchButton.querySelector('span').style['border-color'] = gray
        }, function () {
            let activateIndex, color
            for (let i = 0; i < modules.length; i++) {
                if (modules[i].style.display == 'block') {
                    activateIndex = i
                    break
                }
            }
            if (activateIndex == 0) {
                color = '#fb7299'
            } else {
                color = origin[activateIndex - 1].color
            }
            switchButton.querySelector('span').style['background-color'] = color
            switchButton.querySelector('span').style['border-color'] = color
        })
    }
    let addFrame = (index) => {
        if (!document.URL.startsWith('https://www.bilibili.com/')) { return }
        // 防止匹配到解析网址,控制台会输出错误
        if (!document.URL.startsWith('https://www.bilibili.com/')) return
        let biliDiv = document.querySelector('#player_module')
        biliDiv.style.display = 'block'
        // 创建新player_module
        let diyDiv = biliDiv.cloneNode(true)
        diyDiv.style['padding-left'] = '0px'
        diyDiv.style['margin-left'] = '0px'
        diyDiv.style.display = 'none'
        diyDiv.innerHTML = ""
        diyDiv.setAttribute('id', `diy_module_${index}`)
        let iframe = document.createElement("iframe")
        iframe.id = 'video-iframe'
        iframe.style.height = biliDiv.style.height
        diyDiv.append(iframe)
        let read_url = location.href
        iframe.src = origin[index].api + read_url
        console.log(iframe.src)
        if (document.body.className.includes('player-mode-widescreen')) {
            iframe.style.position = 'absolute'
            iframe.style.top = '0px'
        }
        iframe.height = '0%'
        iframe.width = '100%'
        iframe.setAttribute('frameborder', 'no')
        iframe.setAttribute('border', '0')
        iframe.setAttribute('allowfullscreen', 'allowfullscreen')
        iframe.setAttribute('webkitallowfullscreen', 'webkitallowfullscreen')
        document.querySelector('.plp-l').insertBefore(diyDiv, document.querySelector('.media-wrapper'))
    }
    let init = () => {
        // 将所源作为frmae添加到页面
        for (let i = 0; i < origin.length; i++) {
            addFrame(i)
        }
        // 设置button
        addSubtitle()
        addButton()
    }
    setTimeout(init, 800)
    // 这里延迟可以避免频繁刷新
    setTimeout(() => {
        let obs = document.querySelector('head title')
        if (obs) {
            new MutationObserver(function (mutations, observer) {
                setTimeout(() => { location.reload() }, 500)
            }).observe(obs, { childList: true })
        }
    }, 800)
}
const origin = [
    { regex: 'barrage', api: 'https://www.cuan.la/m3u8.php?url=', color: '#a62aee' },
    { regex: 'dmku', api: 'https://vip.parwix.com:4433/player/?url=', color: '#ff6429' }
]
// 开启代理
ah.proxy({
    onRequest: (config, handler) => {
        let match = false
        for (let i = 0; i < origin.length; i++) {
            if (config.url.search(origin[i].regex) !== -1) {
                match = true
                break
            }
        }
        if (match) {
            let response = GM_getValue('danmu')
            console.log('拦截弹幕接口 - ' + config.url)
            console.log(response)
            handler.resolve({
                config: config,
                status: 200,
                headers: { 'content-type': 'text/text' },
                response: JSON.stringify(response)
            })
        } else handler.next(config)
    }
})
window.addEventListener("load", () => {
    dataQuery().then(
        () => {
            setTimeout(pageChange, 1000)
        }
    )
})