Greasy Fork is available in English.

bilibili-显示精确时间

bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。附带一个显示动态转发中的图片的功能。

// ==UserScript==
// @name        bilibili-显示精确时间
// @namespace   http://tampermonkey.net/
// @description bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。附带一个显示动态转发中的图片的功能。
// @version     1.3
// @author      Y_jun
// @license     GPL-3.0
// @icon        https://www.bilibili.com/favicon.ico
// @grant       none
// @match       https://www.bilibili.com/*
// @match       https://live.bilibili.com/*
// @match       https://space.bilibili.com/*
// @match       https://t.bilibili.com/*
// @run-at      document-start
// ==/UserScript==

// 1打开,0关闭
const editDyn = 1; // 动态
const editReply = 1; // 评论
const editVideo = 1; // 视频
const editPics = 1; // 显示转发图片

const REPLY_API_PREFIX = 'https://api.bilibili.com/x/v2/reply';
const DYN_API_PREFIX = 'https://api.bilibili.com/x/polymer/web-dynamic';
const SPACE_VIDEO_API_PREFIX = 'https://api.bilibili.com/x/space/wbi/arc/search';
const SPACE_SERIES_API_PREFIX = 'https://api.bilibili.com/x/series/archives'; // TODO
const TS_REGEX = /^\d{10}$/;
const FULL_DATETIME_REGEX = /[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/;
const MIN_DATETIME_REGEX = /[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}/;

function getDateTime(ts, type) {
    if (TS_REGEX.test(ts)) {
        let date = new Date(ts * 1000);
        if (!isNaN(date.getTime())) {
            let y = date.getFullYear();
            if (y < 2000) return;
            let m = date.getMonth() + 1;
            let d = date.getDate();
            if (type === 'full') {
                return `${y}-${m < 10 ? "0" + m : m}-${d < 10 ? "0" + d : d} ${date.toTimeString().substring(0, 8)}`;
            } else if (type === 'min') {
                return `${y.toString().substring(2)}-${m < 10 ? "0" + m : m}-${d < 10 ? "0" + d : d} ${date.toTimeString().substring(0, 5)}`;
            }
        }
    }
    return null;
}

function getNewText(origTxt, datetime, isJoin) {
    origTxt = origTxt.trim();
    if (isJoin && /前|直播/.test(origTxt)) {
        return datetime + ' · ' + origTxt;
    }
    if (origTxt.indexOf(' · ') > -1) {
        let origTxtArr = origTxt.split(' · ');
        origTxtArr[0] = datetime;
        return origTxtArr.join(' · ');
    }
    return datetime;
}


const console = Object.create(Object.getPrototypeOf(window.console), Object.getOwnPropertyDescriptors(window.console));

const addLocationToReply = function addLocationToReply(rootId, rpId, userId, datetime, count = 1) {
    const id = rootId === 0 ? rpId : rootId;
    const container = document.querySelector(`.reply-wrap[data-id="${rpId}"]`);
    const containers = document.querySelectorAll(`[data-root-reply-id="${id}"][data-user-id="${userId}"]`);

    // 如果评论元素未找到,则在一定时间内重复尝试数次。
    if (container === null && containers.length === 0) {
        if (count <= 10) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(addLocationToReply, 50, ...args);
        }
        return;
    }

    if (container) {
        // old page: 直接在对应评论元素更改时间
        const info = container.querySelector('.info');
        const time = info.querySelector('.reply-time,.time');
        if (time && !FULL_DATETIME_REGEX.test(time.textContent)) {
            time.textContent = getNewText(time.textContent, datetime, 1);
        }
    } else {
        // new page: 由于无法直接定位评论元素,只能先定位其他有标识符的元素(比如用户头像),然后使用其父元素间接定位评论元素。
        for (let i = 0; i < containers.length; i++) {
            const container = containers[i];
            let parentElement = container.parentElement;
            const isSub = parentElement.classList.toString().includes('sub-');
            if (isSub) {
                parentElement = parentElement.parentElement;
            }
            const info = parentElement.querySelector(isSub ? '.sub-reply-info' : '.reply-info');
            if (info && !FULL_DATETIME_REGEX.test(info.textContent)) {
                const time = info.querySelector('.reply-time,.sub-reply-time');
                if (time) {
                    time.textContent = getNewText(time.textContent, datetime, 1);
                }
                break;
            }
        }
    }
};

const addLocationToDyn = function addLocationToDyn(dynId, datetime, isDetail, count = 1) {
    let container;
    if (isDetail === 0) {
        container = document.querySelector(`.bili-dyn-list__item[data-did="${dynId}"]`);
    } else if (isDetail === 1) {
        container = document.querySelector(`.bili-dyn-item[data-did="${dynId}"]`);
    }

    // 如果评论元素未找到,则在一定时间内重复尝试数次。
    if (container === null) {
        if (count <= 10) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(addLocationToDyn, 50, ...args);
        }
        return;
    }

    if (container) {
        // old page: 直接在对应评论元素更改时间
        const dynMain = container.querySelector('.bili-dyn-item__main');
        const time = dynMain.querySelector('.bili-dyn-time');
        if (time && !FULL_DATETIME_REGEX.test(time.textContent)) {
            time.textContent = getNewText(time.textContent, datetime, 1);
        }
    }
};

const addDynPics = function addDynPics(dyn, isDetail, count = 1) {
    const dynId = dyn.id_str;
    let container;
    if (isDetail === 0) {
        container = document.querySelector(`.bili-dyn-list__item[data-did="${dynId}"]`);
    } else if (isDetail === 1) {
        container = document.querySelector(`.bili-dyn-item[data-did="${dynId}"]`);
    }

    // 如果评论元素未找到,则在一定时间内重复尝试数次。
    if (container === null) {
        if (count <= 10) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(addDynPics, 50, ...args);
        }
        return;
    }

    if (container) {
        // old page: 直接在对应评论元素更改时间
        const dynJsons = dyn.modules?.module_dynamic?.desc?.rich_text_nodes;
        if (dynJsons) {
            dynJsons.forEach(json => {
                if (json.type === 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') {
                    const dynMain = container.querySelector('.bili-dyn-content__forw__desc');
                    let picContainer = document.createElement("div");
                    picContainer.style.display = 'flex';
                    picContainer.style.justifyContent = 'start';
                    picContainer.style.position = 'relative';
                    picContainer.style.overflow = 'scroll';
                    picContainer.style.flexWrap = 'wrap';
                    dynMain.appendChild(picContainer);
                    const pics = json.pics;
                    pics.forEach(pic => {
                        let picImg = document.createElement("img");
                        picImg.onclick = `window.open('${pic.src}')`;
                        picImg.onclick = new Function(`event.stopPropagation();window.open('${pic.src}')`);
                        picImg.src = `${pic.src}@135h_!web-comment-note.webp`;
                        picImg.style.width = '135px';
                        picImg.style.height = '135px';
                        picImg.style.objectFit = 'cover';
                        picImg.style.objectFit = 'cover';
                        picImg.style.borderRadius = '5px';
                        picImg.style.margin = '2px';
                        picImg.style.cursor = 'pointer';
                        picContainer.appendChild(picImg);
                    });
                }
            });
        }
    }
};

const addLocationToVideo = function addLocationToVideo(videoId, datetime, count = 1) {
    const containers = document.querySelectorAll(`[data-aid="${videoId}"]`);

    // 如果视频元素未找到,则在一定时间内重复尝试数次。
    if (containers.length === 0) {
        if (count <= 10) {
            const args = Array.from(arguments).slice(0, arguments.length);
            args.push(count + 1);
            setTimeout(addLocationToVideo, 50, ...args);
        }
        return;
    }

    for (let i = 0; i < containers.length; i++) {
        // old page: 直接在对应视频元素更改时间
        const container = containers[i];
        const time = container.querySelector('.time');
        if (time && !MIN_DATETIME_REGEX.test(time.textContent)) {
            time.textContent = getNewText(time.textContent, datetime, 0);
            time.style.overflow = 'visible';
        }
    }
};

const handleReplies = function handleReplies(replies) {
    replies.forEach((reply) => {
        const datetime = getDateTime(reply.ctime, 'full');
        if (datetime) {
            try {
                addLocationToReply(reply.root, reply.rpid, reply.mid, datetime);
            } catch (ex) {
                console.error(ex);
            }
        }
        if (reply.replies) {
            handleReplies(reply.replies);
        }
    });
};

const handleDyns = function handleDyns(dyns, isDetail) {
    dyns.forEach((dyn) => {
        const ts = dyn?.modules?.module_author?.pub_ts || null;
        const datetime = getDateTime(ts, 'full');
        if (datetime) {
            try {
                addLocationToDyn(dyn.id_str, datetime, isDetail);
            } catch (ex) {
                console.error(ex);
            }
        }
    });
};

const handleVideos = function handleVideos(videos) {
    videos.forEach((video) => {
        const datetime = getDateTime(video.created ?? video.pubdate, 'min');
        if (datetime) {
            try {
                addLocationToVideo(video.bvid, datetime);
            } catch (ex) {
                console.error(ex);
            }
        }
    });
};

const handleDynPics = function handleDynPics(dyns, isDetail) {
    dyns.forEach((dyn) => {
        if (dyn) {
            try {
                addDynPics(dyn, isDetail);
            } catch (ex) {
                console.error(ex);
            }
        }
    });
};

const handleResponse = async function handleResponse(url, response) {
    if (editReply && url.startsWith(REPLY_API_PREFIX)) {
        const body = response instanceof Response ? await response.clone().text() : response.toString();
        try {
            const json = JSON.parse(body);
            if (json.code === 0) {
                setTimeout(() => {
                    handleReplies(Array.isArray(json.data.replies) ? json.data.replies : []);
                    handleReplies(Array.isArray(json.data.top_replies) ? json.data.top_replies : []);
                }, 50);
            }
        } catch (ex) {
            console.error(ex);
        }
    }
    if (editDyn && url.startsWith(DYN_API_PREFIX)) {
        const body = response instanceof Response ? await response.clone().text() : response.toString();
        try {
            const json = JSON.parse(body);
            if (json.code === 0) {
                setTimeout(() => {
                    handleDyns(Array.isArray(json.data.items) ? json.data.items : [], 0);
                    handleDyns(json.data.item ? [json.data.item] : [], 1);
                    if (editPics) {
                        handleDynPics(Array.isArray(json.data.items) ? json.data.items : [], 0)
                        handleDynPics(json.data.item ? [json.data.item] : [], 1);
                    }
                }, 50);
            }
        } catch (ex) {
            console.error(ex);
        }
    }
    if (editVideo && url.startsWith(SPACE_VIDEO_API_PREFIX)) {
        const body = response instanceof Response ? await response.clone().text() : response.toString();
        try {
            const json = JSON.parse(body);
            if (json.code === 0) {
                setTimeout(() => {
                    handleVideos(Array.isArray(json.data.list?.vlist) ? json.data.list.vlist : []);
                }, 50);
            }
        } catch (ex) {
            console.error(ex);
        }
    }
    if (editVideo && url.startsWith(SPACE_SERIES_API_PREFIX)) {
        const body = response instanceof Response ? await response.clone().text() : response.toString();
        try {
            const json = JSON.parse(body);
            if (json.code === 0) {
                setTimeout(() => {
                    handleVideos(Array.isArray(json.data.archives) ? json.data.archives : []);
                }, 50);
            }
        } catch (ex) {
            console.error(ex);
        }
    }
};

const $fetch = window.fetch;

window.fetch = async function fetchHacker() {
    const response = await $fetch(...arguments);
    if (response.status === 200 && response.headers.get('content-type')?.includes('application/json')) {
        await handleResponse(response.url, response);
    }
    return response;
};

/**
 * @this XMLHttpRequest
 */
const onReadyStateChange = function onReadyStateChange() {
    if (this.readyState === XMLHttpRequest.DONE && this.status === 200 && this.getAllResponseHeaders().split("\n").find((v) => v.toLowerCase().includes('content-type: application/json'))) {
        handleResponse(this.responseURL, this.response);
    }
};

const jsonpHacker = new MutationObserver((mutationList) => {
    mutationList.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
            if (node.nodeName.toLowerCase() !== 'script' || node.src.trim() === '') {
                return;
            }
            const u = new URL(node.src);
            if (u.searchParams.has('callback')) {
                const callbackName = u.searchParams.get('callback');
                const callback = window[callbackName];
                window[callbackName] = function (data) {
                    handleResponse(u.href, JSON.stringify(data));
                    callback(data);
                };
            }
        });
    });
});

document.addEventListener('DOMContentLoaded', () => {
    jsonpHacker.observe(document.head, {
        childList: true,
    });
});

window.XMLHttpRequest = class XMLHttpRequestHacker extends window.XMLHttpRequest {
    constructor() {
        super();
        this.addEventListener('readystatechange', onReadyStateChange.bind(this));
    }
};