修改 Bilibili 视频播放器的推荐视频网址

修改 Bilibili 视频播放器的推荐视频网址,以支持鼠标中键点击

目前为 2019-01-19 提交的版本。查看 最新版本

// ==UserScript==
// @name                Modify the recommended video URL of the Bilibili video player
// @name:zh-CN          修改 Bilibili 视频播放器的推荐视频网址
// @namespace           https://gist.github.com/phtwo
// @version             0.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/av*
// @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')
            aid && item.setAttribute('href', '//www.bilibili.com/video/av' + aid)
        })
        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
        }
    }


})()

QingJ © 2025

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