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

B站观影 弹幕 字幕替换

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

// ==UserScript==
// @name         B站大会员影视+弹幕+字幕
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  B站观影 弹幕 字幕替换
// @author       Polygon
// @match        https://www.cuan.la/m3u8.php*
// @match        https://vip.parwix.com/*
// @require      https://gf.qytechs.cn/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
    if (dayNode) { mmdd = dayNode.textContent.split(' ')[0] } else { return }
    if (yearNode) { yy = parseInt(yearNode.textContent.replace('年', '')) } else { return }
    let date = yy + '-' + mmdd
    protobufApi += `&oid=${cid}&date=${date}`
    xmlApi = `http://service-bo71w2uf-1256272652.bj.apigw.tencentcs.com/danmu?&cookie=${encodeURIComponent('SESSDATA=6d3d3e7a%2C1640953774%2C6bbde*71;')}&url=${encodeURIComponent(protobufApi)}`
    // <button class="bui-dropdown-item " data-value="DM_ADV">下载当天</button> .bui-dropdown-items
    // 番剧支持
    let aParentNode = document.querySelector('.bui-dropdown-items')
    lastTag = document.querySelector('[data-value="DM_DOWNLOAD"]')
    if (lastTag) {
        if (lastTag.getAttribute('date') == date) {
             return 
        } else {
            aParentNode.removeChild(lastTag)
        }
    }
    let aLink = document.createElement('a')
    aLink.setAttribute('class', 'bui-dropdown-item')
    aLink.setAttribute('data-value', 'DM_DOWNLOAD')
    aLink.setAttribute('date', date)
    aLink.innerText = '下载当天'
    // aLink.style = 'border: 0px;'
    aLink.addEventListener('click', () => {
        GM_download({
            url: xmlApi,
            name: document.title.split('_')[0] + '_' + date + '.txt',
            saveAs: false,
            onerror: function (error) {
                console.log('ERROR')
                console.log(xmlApi)
                console.log(error)
            },
            onload: function () {
                console.log('SUCCESS')
            }
        })
        GM_download(xmlApi, document.title.split('_')[0] + '_' + date + '.xml')
    })
    aParentNode.append(aLink)
    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 response = GM_getValue('danmu')
            console.log('拦截弹幕接口成功 - ' + config.url)
            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)
        }
    )
    setInterval(DanmuDownload, 500)
})

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址