试着写了个油猴脚本,但是时灵时不灵

试着写了个油猴脚本,哔哩哔哩播放多part视频时,在标题中显示当前是第几part,当前part的名字。主要功能已经实现,但是时灵时不灵(发生问题时脚本根本不加载)。

试过多种等待页面完成加载的方法似乎都不行,干脆加了个延时,发现只在第一次打开页面时正常,自动播放下一part或者手动点下一part,都不生效。

请坛友康康怎么破?此处@dms

代码如下

// ==UserScript==
// @name         Bilibili分P标题
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  从列表中抽取Bilibili分P视频中当前part的标题,并写入到标题中
// @author       tumuyan
// @match        https://www.bilibili.com/video/*
// @icon         https://www.google.com/s2/favicons?domain=bilibili.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    setTimeout(function () {
        var searchStr = location.search;
        var t=0;

        if(searchStr.length>1){
            t= Number(searchStr.replace(/.*(\?|&)p=([0-9]+)(&.*|$)/,"$2"))-1;
        }

        console.log("bili分p p="+t);

        var p= document.getElementsByClassName("list-box")[0];
        var l=p.childNodes[t];

        console.log("bili分p list-box.html="+l.innerHTML);

        if(l!=null){

            let h=document.getElementsByTagName("h1")[0];
            let m=h.innerText+" / "+l.innerText.replace(/[\s0-9:]+$/,'');

            console.log("bilibili分P = "+m);
            h.textContent=m;
            let n=document.getElementsByTagName("title")[0];
            n.textContent=m+" - 哔哩哔哩_bilibili";

        }

    },3000)

})();

百度了个监控dom变化的方法,修改后正常了。这是成品

因为跳转的时候并没有刷新页面,所以脚本也没有被重载。

在打开这个页面的时候,脚本已经运行过了,后面的跳转又没有刷新,所以脚本自然不会再运行。

这种处理起来比较麻烦,我现在是加几个事件监听器,然后再对事件进行具体判断(因为当页面载入完成的时候,也有可能触发相应的事件

var mz = location.href;
let glt=1000;
var inq = setInterval(function () {
if (mz != location.href) {
clearInterval(inq);
ks();
}
}, glt);

在你的主函数里加个循环,不停检测当前网址就行了,ks() 换成你要触发的函数。

如果连网址都没改变,就监测个会改变的 dom 的innerText ,也一样

不停检测网址也是个办法,但是感觉有点浪费资源。百度了个监控dom变化的方法,解决问题了。

把 setTimeout 改成 setInterval,每隔几秒都做一次咯。

设置计时器的方法可行,但不优雅。计时器的判断如果设置得好,其实资源占用并不高,但是难以做到即时响应,如果想要提高响应速度,就必然会提高资源占用,因为循环的频率增加了。即便设置为一毫秒执行一次,在实际运行中并不会达到这么高的频率,所以可以认为这种方法的响应必然存在延迟。

而监听事件则可以在事件发生的第一时间触发。

相关知识点:HTML5 的 history API。

当活动历史记录条目更改时,将触发 popstate 事件

const handler = /* 你要执行的函数 */
window.addEventListener('popstate', handler)

这样添加一个监听器,当事件发生变化时执行对应的函数。

但仅这样操作还会面临两个问题:

history.pushState()history.replaceState() 不会触发 popstate 事件。

而这两种方法正是网页中最常用的,所以要为他们也添加监听器。下面是一种比较常见的添加监听器的方法。

const addHistoryEvent = function(type) {
    var originalMethod = history[type];
    return function() {
        var recallMethod = originalMethod.apply(this, arguments);
        var e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return recallMethod;
    };
};
history.pushState = addHistoryEvent('pushState');
history.replaceState = addHistoryEvent('replaceState');

const handler = /* 你要执行的函数 */
window.addEventListener('pushState', handler);
window.addEventListener('replaceState', handler);

页面加载时 Chrome 和 Safari 通常会触发 (emit ) popstate 事件

就是说在页面刚刚加载完成时也会触发一次 popstate 事件,但这又不是普遍的必然现象,处理起来就比较尴尬,所以我的做法是,对这个事件,判断一下网址是否发生改变。

这个操作很简单,就不写示例代码了。

hashchange

但是还有一种另外的情况,就是对于网址的改变并没有使用上述方法,而是借用了网址中的锚点,就是井号后面的内容。这部分在网址中被称作为 hash。hashchange 这个事件就是在 hash 发生变化时被触发。

所以有必要的话,也可以为他添加一个监听器,具体方法和第 1 条示例代码一样。

(然后欢迎小可爱们用微信扫描下面小程序码)

Coffee

通过监控地址栏的方式并不能把b站所有的播放器变化都纳入观测范围内,我是Chrome91,至少对我来说只监听"replaceState"、"pushState"两个事件并不是所有情况都有效(比如播放列表)。

只通过简单延时的话,有时候网络稍微慢一点就会出现提前注入页面而触发b站奇怪机制而加载失败的情况,快的时候内容有时候又会延迟过久,个人在这里稍微有点强迫症。

其实B站在window顶层里留了一些接口,使用这些接口可以简单快速的知道播放器的状态。

在播放器完成加载后,window里会出现一个player对象,并且player.isInitialized()方法值为true,想个办法把这个方法的值监控到,就可以知道什么时候播放器加载完成了。我知道有很多方法可以做但是我用了本办法,循环查这个方法是否存在并且值为真,并且如果真的很久都没有加载完成,那就算了:

    async function playerReady() {
        let i = 90;
        while (--i >= 0) {
            await wait(100);
            if (!('player' in unsafeWindow)) continue;
            if (!('isInitialized' in unsafeWindow.player)) continue;
            if (!unsafeWindow.player.isInitialized()) continue;
            return true;
        }
        return false;
    }

一次检查100ms的延迟,这个值很小,并且播放器加载完成的100ms内就可以做出响应,对于脚本来说100ms又很大,足以避开奇怪的机制。

首次加载完成后就可以等着下一次变化了。每次视频分p什么的变化的时候,window内的很多接口都会变化,可以直接使用。监测变化的方式改为使用自带的MutationObserver检查video的资源地址,一般来说这个地址不会变化,甚至切换清晰度都不变,指向了一个blob地址。但是由于b站自己的机制,播放器会在每次切换视频时重载,这个地址也会跟着变化,因此这个地址变化可以用于检测视频改变。

    async function registerVideoChangeHandler() {
        const video = await waitForDom(".bilibili-player-video video");
        if (!video) return;
        const observer = new MutationObserver(e => {
            if (e[0].target.src) {
                tryInject(true);
            }
        });
        observer.observe(video, {attributes:true});
    }

我这里的实现肯定不是最好的实现(甚至有一些蠢),我也在学习制作这类脚本。如果有更好的方案或者脚本不足之处欢迎指出,顺便附上我之前做的关于显示分p标题和av、bvid号的脚本:

希望可以帮到你。

1 Like

给大佬点赞!

playerReady函数感觉可以用上es6的新特性,简写为这样

async function playerReady() {
    let i = 90;
    while (--i >= 0) {
        await wait(100);
        if (unsafeWindow?.player?.isInitialized()) {
            return true;
        }
    }
    return false;
}

?.可选链https://zh.javascript.info/optional-chaining
初学js,刚好想到这个特性

确实是可以的,但是考虑到很多国产浏览器虽然可以用油猴脚本但是内核还是比较老旧的,不知道支持的怎么样,所以这个懒我没敢偷…

其实是萌新,在学着做这些脚本的萌新23333

chrome支持是80+,刚看了360极速版最新是86版本的内核。不过你是打算支持的更广泛,不支持新特性也是正常的。
不过由于我学的就是es6,而且写脚本也是只为了自己,所以不用考虑兼容性…

嗯,不过有的浏览器虽然官方一直有在更新,但是浏览器软件本体自己不检查更新…

之前看the1812大佬的Bilibili Evolved的issues区时候就遇到有人反馈过更新完不能用了,后来查出来是用了??导致不兼容,全换成||就好了,之后就开始注意这个了。。。