给 Bilibili 视频播放器结束播放后的推荐视频界面添加网址,以支持新标签页打开
// ==UserScript==
// @name Add URL to the recommend interface when Bilibili video player finishes playing
// @name:zh-CN 给 Bilibili 视频播放器结束播放后的推荐视频界面添加网址
// @namespace https://gist.github.com/phtwo
// @version 0.2.1
// @description Modify the recommended video URL of the Bilibili video player to support the middle mouse button clicks
// @description:zh-CN 给 Bilibili 视频播放器结束播放后的推荐视频界面添加网址,以支持新标签页打开
// @match *://www.bilibili.com/video/*
// @grant none
//
// @author phtwo
// @homepage https://gist.github.com/phtwo/b7bee4787e3dcce1bda7c17535538097
// @supportURL https://gist.github.com/phtwo/b7bee4787e3dcce1bda7c17535538097
//
// @noframes
// @nocompat Chrome
//
// ==/UserScript==
(function () {
'use strict'
init()
function init() {
startMonitor()
observeVideoChange(videoPageUrl => {
// 每次切换视频后,直接重新再执行一次。嗯,就这样。
setTimeout(startMonitor, 2e3)
console.log('Bilibili recommended video URL modifier observeVideoChange', videoPageUrl)
})
}
function startMonitor() {
let waitVideosNodes = () => {
return isRecommendVideosNodesExist() ? Promise.resolve() : Promise.reject()
}
waitingForChildElement('.bilibili-player-video-wrap', 'bilibili-player-ending-panel') // 首次进入首页 等待 播放器结束面板 渲染
.then(waitVideosNodes)
.catch(() => waitingForChildElement('.bilibili-player-ending-panel-box-videos', 'bilibili-player-ending-panel-box-recommend')) // 播放器结束面板渲染后,等待推荐视频模块渲染
.then(modifyRecommendVideosNodesLink)
.catch(error => console.error('Bilibili recommended video URL modifier error', error))
console.log('Bilibili recommended video URL modifier is waiting to be modified.')
}
function isRecommendVideosNodesExist() {
return getRecommendVideosNodes().length > 0
}
function getRecommendVideosNodes() {
return document.querySelectorAll('a.bilibili-player-ending-panel-box-recommend')
}
function modifyRecommendVideosNodesLink() {
getRecommendVideosNodes().forEach(item => {
const aid = item.getAttribute('data-aid')
const bvId = item.getAttribute('data-bvid')
const videoId = aid ?
`av${aid}` :
bvId ? `BV${bvId}` : ''
videoId && item.setAttribute('href', '//www.bilibili.com/video/' + videoId)
})
console.log('Bilibili recommended video URL modifier has been modified.')
}
/**
* @name waitingForChildElement
* @description 使用 MutationObserver 接口观察 父 element, childList addedNodes 中的直接子代,有任意一个具有 childClass 类名即为完成
* @param {string} parentSlector - 父 element 选择器
* @param {string} childClass - 直接子代类名,不支持选择器语法
* @return {Promise}
*/
function waitingForChildElement(parentSlector, childClass) {
const deferred = createPromiseDeferred()
const parentDom = document.querySelector(parentSlector)
const options = {
childList: true,
}
const observer = new MutationObserver(mutationCallback)
observer.observe(parentDom, options)
return deferred.promise
function mutationCallback(mutations) {
for (let mutation of mutations) {
if ('childList' !== mutation.type) {
continue
}
if (Array.from(mutation.addedNodes).some(node => 1 === node.nodeType &&
node.classList.contains(childClass))) {
observer.takeRecords()
observer.disconnect()
deferred.resolve()
break
}
}
}
}
/**
* @name observeVideoChange
* @description 因为每次切视频,都会销毁旧的播放器实例。 因此切换视频后,必须重新对新生成的 DOM 创建 MutationObserver。
* 这里采用监听 'head> meta[itemprop=url]' 的 content 变化来跟踪页面的切换
* ps: 这里可对 head 的检测进行节流处理,回调里直接读取 meta 更快,b 站都是先改 url 和 meta url 的值
* @param {function} [fCallback]
* @return {Promise}
*/
function observeVideoChange(fCallback) {
let lastVideo = getVideoPageUrlFromMetaTag()
const parentSlector = 'head'
const parentDom = document.querySelector(parentSlector)
const options = {
childList: true,
}
const observer = new MutationObserver(mutationCallback)
observer.observe(parentDom, options)
function mutationCallback(mutations) {
for (let mutation of mutations) {
if ('childList' !== mutation.type) {
continue
}
let urlMetaTag = Array.from(mutation.addedNodes).find(node => {
return 1 === node.nodeType && 'meta' === node.tagName.toLowerCase() &&
'url' === node.getAttribute('itemprop')
})
if (!urlMetaTag) {
continue
}
let currVideoPage = getVideoPageUrlFromMetaTag(urlMetaTag)
if (currVideoPage === lastVideo) {
continue
}
lastVideo = currVideoPage
observer.takeRecords() // 已经确定当前有切换视频了,忽略其他变动
fCallback(lastVideo)
break
}
}
}
/**
* @name getVideoPageUrlFromMetaTag
* @description 无需取 avid ,这个 url 的格式不包含其他参数的,仅仅只有 avid
* @param {HTMLMetaElement=} metaTag
* @return {string}
*/
function getVideoPageUrlFromMetaTag(metaTag) {
let meta = metaTag || document.querySelector('meta[itemprop=url]')
return (meta && meta.getAttribute('content')) || ''
}
function createPromiseDeferred() {
let resolve, reject
let promise = new Promise((res, rej) => {
resolve = res
reject = rej
})
return {
promise,
resolve,
reject
}
}
})()