Greasy Fork is available in English.
bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。
当前为
// ==UserScript==
// @name bilibili-显示精确时间
// @namespace http://tampermonkey.net/
// @description bilibili动态与评论发布时间替换为精确时间,格式为“yyyy-MM-dd hh:mm:ss”。
// @version 1.0
// @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==
const REPLY_API_PREFIX = 'https://api.bilibili.com/x/v2/reply';
const DYN_API_PREFIX = 'https://api.bilibili.com/x/polymer/web-dynamic';
const regex = /^\d{10}$/;
function getDateTime(ts) {
if (regex.test(ts)) {
let date = new Date(ts * 1000);
if (!isNaN(date.getTime())) {
let y = date.getFullYear();
let m = date.getMonth() + 1;
let d = date.getDate();
return y + "-" + (m < 10 ? "0" + m : m) + "-" + (d < 10 ? "0" + d : d) + " " + date.toTimeString().substring(0, 8);
}
}
return null;
}
function getNewText(origTxt, datetime) {
origTxt = origTxt.trim();
if (origTxt.indexOf('前') > -1) {
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('.time-location');
if (time) {
time.textContent = getNewText(time.textContent, datetime);
}
} 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 && !/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/.test(info.textContent)) {
const time = info.querySelector('.reply-time,.sub-reply-time');
if (time) {
time.textContent = getNewText(time.textContent, datetime);
}
break;
}
}
}
};
//TODO
const addLocationToDyn = function addLocationToDyn(dynId, userId, datetime, count = 1) {
const container = document.querySelector(`.bili-dyn-list__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) {
time.textContent = getNewText(time.textContent, datetime);
}
}
};
const handleReplies = function handleReplies(replies) {
replies.forEach((reply) => {
const datetime = getDateTime(reply.ctime);
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) {
dyns.forEach((dyn) => {
const ts = dyn?.modules?.module_author?.pub_ts || null;
const datetime = getDateTime(ts);
if (datetime) {
try {
addLocationToDyn(dyn.id_str, dyn.modules?.module_author?.mid, datetime);
} catch (ex) {
console.error(ex);
}
}
});
};
const handleResponse = async function handleResponse(url, response) {
if (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 (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 : []);
}, 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));
}
};