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

B站观影 弹幕 字幕替换

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

您需要先安装一个扩展,例如 篡改猴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.7
// @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
// @grant        GM_download
// @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]
        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)
        }
        GM_setValue('subtitleArray', subtitleArray)
        // 弹幕
        let danmuApi = `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`
        console.log('当前弹幕接口=' + danmuApi)
        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'
    // 字幕 参考 <bilibili外挂字幕 哔哩哔哩外挂字幕>
    // https://raw.githubusercontent.com/jonwinters/bilibili-subtitles-plugin/master/build.dist.js
    let addSubtitle = () => {
        if (document.URL.startsWith('https://www.bilibili.com/')) { return }
        let subtitles = GM_getValue('subtitleArray')
        window.startTime = 0
        window.endTime = 0
        window.offsetSubtitle = 0
        window.fontsize = 20
        if (!subtitles.length) { return }
        subtitles.unshift({ content: '字幕加载成功[简体中文]', from: 0, location: 2, to: 3 })
        // 00:00
        let percentNode = document.querySelector("[class*='player-played']")
        let totalTime = document.querySelector("[class*='player-dtime']")
        let subtitleNode = document.querySelector("div[class*='player-subtitle']")
        if (!(percentNode && totalTime && subtitleNode)) { 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)
        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 = ""
            }
            // 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 && subtitleNode.children.length) { return }
            let subtitleDiv = document.createElement('div')
            subtitleDiv.setAttribute('class', 'leleplayer-subtitle yzmplayer-subtitle player-subtitle .leleplayer-danmaku')
            // 此处可以自定义一些东西 这里是字幕背景色与bilibili一致
            let customStyle = 'background-color: rgba(0, 0, 0, 0.4)'
            subtitleDiv.innerHTML = `<span style='${customStyle}'>${oneSubtitle.content}</span>`
            let duration = oneSubtitle.to - oneSubtitle.from - (timeSec - oneSubtitle.from)
            subtitleNode.appendChild(subtitleDiv)
            let offsetStyle = `bottom: ${30 * (parseInt(oneSubtitle.from) < window.endTime - 1 && mutationsList && observer)}px; font-size: ${window.fontsize}px;`
            subtitleDiv.style = `animation: danmaku-center ${duration}s linear;visibility: hidden; ${offsetStyle}`
            // 记录结束时间
            window.endTime = parseFloat(oneSubtitle.to)
            window.startTime = parseFloat(oneSubtitle.from)
            window.currentSubtitle = oneSubtitle.content
        }
        var config = {
            attributes: true,
            childList: true,
            subtree: true
        }
        var observer = new MutationObserver(insertSubtitle)
        observer.observe(percentNode, config)
        // 暂停播放事件
        document.querySelector('#player').addEventListener('click', () => {
            let timeSec = totalSec * parseFloat(percentNode.style.width.replace('%', '')) / 100
            if (document.querySelector('#player').className.search('playing') !== -1) {
                // 播放状态
                if (timeSec < window.endTime) {
                    subtitleNode.innerHTML = ""
                    insertSubtitle(null, null)
                }
            } else {
                // 暂停状态
                if (timeSec < window.endTime) {
                    if (subtitleNode.lastChild) { (subtitleNode.lastChild.style.visibility = 'visible') }
                }
            }
        })
        document.addEventListener('fullscreenchange', () => {
            window.fontsize = (window.fontsize == 20) ? 40 : 20
            subtitleNode.lastChild.style['font-size'] = window.fontsize + 'px'
        })
    }
    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) {
                location.reload()
            }).observe(obs, { childList: true })
        }
    }, 500)
}

let DanmuDownload = () => {
    if (!document.URL.startsWith('https://www.bilibili.com/')) { return }
    // 搭建的自动解析protobuf为xml接口
    let xmlApi = 'http://service-bo71w2uf-1256272652.bj.apigw.tencentcs.com/danmu'
    let protobufApi = 'https://api.bilibili.com/x/v2/dm/web/history/seg.so?type=1'
    let cid = GM_getValue('cid')
    // .bui-long-list-list 弹幕容器 .bpx-player-date-picker-year 年
    let yearNode = document.querySelector('.bpx-player-date-picker-year')
    let dayNode = document.querySelector('.bui-long-list-list span.dm-info-date')
    let yy, mmdd, label, date
    const cookie = ''
    if (!cookie&&!GM_getValue('isAlert')) { alert('当前未配置cookie,历史弹幕无法下载\n可在脚本320行配置cookie,如SESSDATA=xxx'); GM_setValue('isAlert', true)}
    if (dayNode && yearNode){
        mmdd = dayNode.textContent.split(' ')[0]
        yy = parseInt(yearNode.textContent.replace('年', ''))
        date = yy + '-' + mmdd
        protobufApi += `&oid=${cid}&date=${date}`
        xmlApi = `http://service-bo71w2uf-1256272652.bj.apigw.tencentcs.com/danmu?&cookie=${encodeURIComponent(cookie)}&url=${encodeURIComponent(protobufApi)}`
        label = '下载当天'
    } else {
        label = '下载当前'
        date = 'allDate'
        xmlApi = `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`
    }
    // 番剧支持
    let aParentNode = document.querySelector('.bui-dropdown-items')
    if (!aParentNode) { return }
    lastTag = document.querySelector('[data-value="DM_DOWNLOAD"]')
    if (lastTag) {
        if (lastTag.getAttribute('date') == date) {
             return 
        } else {
            aParentNode.removeChild(lastTag)
        }
    }
    let downloadDiv = document.createElement('div')
    let xmlName = document.title.split('_')[0] + '_' + date + '.xml'
    downloadDiv.setAttribute('class', 'bui-dropdown-item')
    downloadDiv.setAttribute('data-value', 'DM_DOWNLOAD')
    downloadDiv.setAttribute('id', 'DM_DOWNLOAD')
    downloadDiv.setAttribute('date', date)
    downloadDiv.innerHTML = `<a download='${xmlName}'>${label}</a>`
    GM_addStyle(`
        #DM_DOWNLOAD {
            color: #222;
        }
        #DM_DOWNLOAD:active {
            color: #222;
        }
    `)
    let aLink = downloadDiv.children[0]
    GM_xmlhttpRequest({
    url: xmlApi,
    onload: (res) => {
        let xml = res.responseText
        console.log(res)
        aLink.setAttribute('href', URL.createObjectURL(new Blob([xml])))
        }
    })
    aParentNode.append(downloadDiv)
    console.log('历史弹幕下载按钮添加成功 | ' + date)
}

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 danmu = GM_getValue('danmu')
            console.log('拦截弹幕接口成功 - ' + config.url)
            handler.resolve({
                config: config,
                status: 200,
                headers: { 'content-type': 'text/text' },
                response: JSON.stringify(danmu)
            })
        } else {handler.next(config)}
    }
})
window.addEventListener("load", () => {
    dataQuery().then(
        () => {
            setTimeout(pageChange, 1000)
        }
    )
    setInterval(DanmuDownload, 500)
})