// ==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://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, label, date
const cookie = 'SESSDATA=6d3d3e7a%2C1640953774%2C6bbde*71;'
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)
})