// ==UserScript==
// @name Bilibili - 未登录无限试用最高画质
// @description 未登录下恢复评论区并持续试用最高画质
// @namespace https://bilibili.com/
// @version 2025.12.10
// @license GPL-3.0
// @author 会飞的蛋蛋面
// @match https://www.bilibili.com/video/*
// @match https://space.bilibili.com/*/dynamic*
// @match https://www.bilibili.com/festival/*
// @match https://www.bilibili.com/bangumi/play/*
// @match https://t.bilibili.com/*
// @match https://www.bilibili.com/opus/*
// @icon https://www.bilibili.com/favicon.ico
// @grant unsafeWindow
// @grant GM_getResourceText
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect api.aicu.cc
// @connect apibackup2.aicu.cc
// @require https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js
// @require https://cdn.jsdelivr.net/npm/viewerjs@1.11.7/dist/viewer.min.js
// @resource viewerCss https://cdn.jsdelivr.net/npm/viewerjs@1.11.7/dist/viewer.min.css
// @require https://update.greasyfork.icu/scripts/512574/1464548/inject-bilibili-comment-style.js
// @run-at document-start
// @downloadURL https://update.greasyfork.icu/scripts/470714/Bilibili%20-%20%E6%9C%AA%E7%99%BB%E5%BD%95%E6%97%A0%E9%99%90%E8%AF%95%E7%94%A8%E6%9C%80%E9%AB%98%E7%94%BB%E8%B4%A8.user.js
// @updateURL https://update.greasyfork.icu/scripts/470714/Bilibili%20-%20%E6%9C%AA%E7%99%BB%E5%BD%95%E6%97%A0%E9%99%90%E8%AF%95%E7%94%A8%E6%9C%80%E9%AB%98%E7%94%BB%E8%B4%A8.meta.js
// ==/UserScript==
(() => {
"use strict";
const global = typeof unsafeWindow === "undefined" ? window : unsafeWindow;
if (!isUserLoggedIn()) {
initQualityTrialBypass();
initHostGuards();
initHistoryLookup();
initCommentEnhancer();
}
function waitFor(condition, interval = 200) {
return new Promise(resolve => {
let running = false;
const timer = setInterval(async () => {
if (running) return;
running = true;
try {
const result = await condition();
if (!result) return;
clearInterval(timer);
resolve(result);
} finally {
running = false;
}
}, interval);
});
}
function isUserLoggedIn() {
return document.cookie.includes("DedeUserID");
}
function initQualityTrialBypass() {
const {setTimeout: nativeSetTimeout} = global;
global.setTimeout = (fn, delay) => nativeSetTimeout(fn, delay === 3e4 ? 3e8 : delay);
const nativeDefineProperty = Object.defineProperty;
Object.defineProperty = function(obj, prop, descriptor) {
if (prop === "isViewToday" || prop === "isVideoAble") descriptor = {
get: () => true,
enumerable: false,
configurable: true
};
return nativeDefineProperty.call(this, obj, prop, descriptor);
};
const tryClickTrialBtn = () => {
const btn = document.querySelector(".bpx-player-toast-confirm-login");
if (btn) setTimeout(() => btn.click(), 1e3);
};
setInterval(tryClickTrialBtn, 1e3);
}
function initHostGuards() {
const host = location.hostname;
if (host === "space.bilibili.com") {
const styleElement = document.createElement("style");
styleElement.textContent = ".bili-mini-mask, .login-panel-popover, .login-tip { display: none !important; }";
document.head.appendChild(styleElement);
setInterval(() => {
const maskElement = document.querySelector(".bili-mini-mask");
if (maskElement) window.location.reload();
}, 1e3);
let last = 0;
const nativeFetch = global.fetch;
global.fetch = (...args) => {
const url = args[0];
if (typeof url === "string" && url.includes("space/wbi/arc/search")) {
if (Date.now() - last < 200) return Promise.reject(new Error("重复请求"));
last = Date.now();
}
return nativeFetch(...args);
};
return;
}
if (host === "www.bilibili.com") {
const nativeAppend = Node.prototype.appendChild;
const scriptBlockList = [ "miniLogin" ];
Node.prototype.appendChild = function appendPatched(el) {
if (el.tagName === "SCRIPT") {
const src = el.src || "";
if (scriptBlockList.some(keyword => src.includes(keyword))) return el;
}
return nativeAppend.call(this, el);
};
waitFor(() => global.player?.getMediaInfo ? global.player : null, 1e3).then(player => {
if (!player) return;
const originalGetMediaInfo = player.getMediaInfo;
player.getMediaInfo = function patchedGetMediaInfo(...args) {
const info = originalGetMediaInfo.apply(this, args);
info.absolutePlayTime = 0;
return info;
};
let clicked = false;
document.body.addEventListener("click", () => {
clicked = true;
setTimeout(() => clicked = false, 500);
});
const originalPause = player.pause;
player.pause = function patchedPause(...args) {
if (clicked) return originalPause.apply(this, args);
};
});
}
}
function initHistoryLookup() {
if (document.documentElement.dataset.historyLookupInited === "1") return;
document.documentElement.dataset.historyLookupInited = "1";
const historyPanelId = "history-reply-panel";
const biliVideoLinkPrefix = "https://www.bilibili.com/video";
const biliLiveLinkPrefix = "https://live.bilibili.com";
const historyApiBase = [ "https://api.aicu.cc/api/v3/search", "https://apibackup2.aicu.cc:88/api/v3/search" ];
const historyApiEndpoints = {
reply: "/getreply",
danmu: "/getvideodm",
live: "/getlivedm"
};
const historyTabs = [ {
key: "reply",
name: "评论"
}, {
key: "danmu",
name: "视频弹幕"
}, {
key: "live",
name: "直播弹幕"
} ];
const historyCache = new Map;
let historyIsLoading = false;
let historyCurrentUid = null;
let historyCurrentPage = 1;
let historyCurrentTab = "reply";
let historyIsEnd = false;
let historyTotal = 0;
addHistoryStyle();
document.addEventListener("click", event => {
const target = event.target?.closest?.(".history-lookup-btn");
if (!target || historyIsLoading) return;
const uid = target.dataset.uid;
if (!uid) return;
const nickname = target.dataset.nickname || `UID${uid}`;
openHistoryPanel(event, uid, nickname);
});
function addHistoryStyle() {
GM_addStyle(`\n #${historyPanelId} {\n position: absolute;\n width: 380px;\n max-height: 70vh;\n overflow: auto;\n background: #fff;\n color: #333;\n border: 1px solid #ddd;\n border-radius: 8px;\n box-shadow: 0 6px 24px rgba(0,0,0,.18);\n z-index: 99999;\n padding: 12px;\n display: none;\n font-family: inherit;\n }\n body.dark #${historyPanelId} { background: #1f1f1f; color: #e9eaec; border-color: #333; }\n #${historyPanelId} .history-header { display: flex; justify-content: space-between; align-items: center; font-weight: 700; margin-bottom: 8px; }\n #${historyPanelId} .history-close { padding: 4px 8px; border: 0; background: #bbb; color: #fff; border-radius: 4px; cursor: pointer; }\n body.dark #${historyPanelId} .history-close { background: #444; color: #e9eaec; }\n #${historyPanelId} .history-tabs { display: flex; gap: 6px; margin-bottom: 8px; }\n #${historyPanelId} .history-tabs button { flex: 1; padding: 6px; border: 1px solid #ddd; background: #f5f5f5; border-radius: 4px; cursor: pointer; font-size: 12px; }\n #${historyPanelId} .history-tabs button.active { background: #00a1d6; color: #fff; border-color: #00a1d6; }\n body.dark #${historyPanelId} .history-tabs button { background: #333; border-color: #444; color: #e9eaec; }\n body.dark #${historyPanelId} .history-tabs button.active { background: #00a1d6; border-color: #00a1d6; }\n #${historyPanelId} .history-item { margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #f2f2f2; }\n body.dark #${historyPanelId} .history-item { border-color: #2c2c2c; }\n #${historyPanelId} .history-meta { font-size: 12px; color: #666; margin-bottom: 4px; }\n #${historyPanelId} .history-meta a { color: #00a1d6; text-decoration: none; }\n body.dark #${historyPanelId} .history-meta { color: #9ca3af; }\n #${historyPanelId} .history-text { font-size: 14px; white-space: pre-wrap; word-break: break-all; }\n #${historyPanelId} .history-room { font-size: 12px; color: #00a1d6; margin-bottom: 2px; }\n #${historyPanelId} .history-info { font-size: 12px; color: #999; margin-bottom: 8px; }\n #${historyPanelId} .history-pager { display: flex; justify-content: space-between; margin-top: 8px; }\n #${historyPanelId} .history-pager button { padding: 4px 12px; border: 1px solid #ddd; background: #f5f5f5; border-radius: 4px; cursor: pointer; }\n #${historyPanelId} .history-pager button:disabled { opacity: 0.5; cursor: not-allowed; }\n body.dark #${historyPanelId} .history-pager button { background: #333; border-color: #444; color: #e9eaec; }\n .history-lookup-btn { margin-left: 12px; color: #9499A0; cursor: pointer; font-size: 13px; }\n .history-lookup-btn:hover { color: #00a1d6; }\n html[data-history-lookup-loading="1"] .history-lookup-btn {\n pointer-events: none;\n cursor: not-allowed;\n color: #C9CCD0;\n }\n html[data-history-lookup-loading="1"] .history-lookup-btn:hover { color: #C9CCD0; }\n `);
}
async function openHistoryPanel(event, uid, nickname) {
if (historyIsLoading) return;
let panel = document.getElementById(historyPanelId);
if (!panel) {
panel = document.createElement("div");
panel.id = historyPanelId;
document.body.appendChild(panel);
}
const trigger = event.target.closest(".history-lookup-btn") || event.target;
const rect = trigger.getBoundingClientRect();
panel.style.left = `${rect.left + window.scrollX}px`;
panel.style.top = `${rect.bottom + window.scrollY + 5}px`;
const tabsHtml = historyTabs.map(t => `${t.name} `).join("");
panel.innerHTML = `\n
\n ${tabsHtml}
\n 加载中...
\n `;
panel.style.display = "block";
panel.querySelector(".history-close").onclick = () => panel.style.display = "none";
historyTabs.forEach(t => {
const btn = panel.querySelector(`.history-tab-${t.key}`);
btn.onclick = () => switchHistoryTab(panel, t.key);
});
historyCurrentUid = uid;
historyCurrentPage = 1;
historyCurrentTab = "reply";
historyIsEnd = false;
historyTotal = 0;
await loadHistoryPage(panel);
}
function switchHistoryTab(panel, tabKey) {
if (historyIsLoading || historyCurrentTab === tabKey) return;
historyCurrentTab = tabKey;
historyCurrentPage = 1;
historyIsEnd = false;
historyTotal = 0;
historyTabs.forEach(t => {
const btn = panel.querySelector(`.history-tab-${t.key}`);
if (btn) btn.classList.toggle("active", t.key === tabKey);
});
loadHistoryPage(panel);
}
function getHistoryCacheKey(uid, type, page) {
return `${uid}_${type}_${page}`;
}
function setHistoryTabsDisabled(panel, disabled) {
panel.querySelectorAll(".history-tabs button").forEach(btn => {
btn.disabled = disabled;
btn.style.opacity = disabled ? "0.5" : "";
btn.style.pointerEvents = disabled ? "none" : "";
});
}
function setHistoryLoading(panel, loading) {
historyIsLoading = loading;
if (loading) document.documentElement.dataset.historyLookupLoading = "1"; else delete document.documentElement.dataset.historyLookupLoading;
setHistoryTabsDisabled(panel, loading);
}
async function loadHistoryPage(panel) {
const uid = historyCurrentUid;
const tab = historyCurrentTab;
const page = historyCurrentPage;
const cacheKey = getHistoryCacheKey(uid, tab, page);
if (historyCache.has(cacheKey)) {
const cached = historyCache.get(cacheKey);
historyTotal = cached.total;
historyIsEnd = cached.isEnd;
renderHistoryList(panel, cached.list);
return;
}
if (historyIsLoading) return;
setHistoryLoading(panel, true);
panel.querySelector(".history-body").textContent = "加载中...";
try {
const response = await historyRequest(uid, page, tab);
if (!response.success) throw new Error(response.message || `接口异常: code=${response.code}`);
const list = parseHistoryResponse(response, tab);
historyTotal = response.data?.cursor?.all_count || historyTotal;
historyIsEnd = response.data?.cursor?.is_end || !list.length;
historyCache.set(cacheKey, {
list: list,
total: historyTotal,
isEnd: historyIsEnd
});
renderHistoryList(panel, list);
} catch (err) {
panel.querySelector(".history-body").textContent = `获取失败:${err.message}`;
} finally {
setHistoryLoading(panel, false);
}
}
function parseHistoryResponse(response, type) {
const data = response.data;
if (type === "reply") return (data?.replies || []).map(d => ({
time: d.time || 0,
message: d.message || "",
oid: d.dyn?.oid || "",
rpid: d.rpid || ""
}));
if (type === "danmu") return (data?.videodmlist || []).map(d => ({
ctime: d.ctime || 0,
content: d.content || "",
oid: d.oid || ""
}));
if (type === "live") {
const result = [];
const rooms = data?.list || [];
for (const room of rooms) {
const danmuList = room.danmu || [];
for (const dm of danmuList) result.push({
roomId: room.roominfo?.roomid || "",
roomName: room.roominfo?.roomname || "",
upName: room.roominfo?.upname || "",
text: dm.text || "",
ts: dm.ts || 0
});
}
return result;
}
return [];
}
function historyRequestOnce(baseUrl, uid, pn) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `${baseUrl}?uid=${uid}&pn=${pn}&ps=100&mode=0&keyword=`,
headers: {
Origin: "https://www.aicu.cc",
Referer: "https://www.aicu.cc/"
},
responseType: "json",
onload: res => resolve(new HistoryApiResponse(res.response)),
onerror: () => reject(new Error("网络错误"))
});
});
}
async function historyRequest(uid, pn, type) {
let lastResponse = null;
for (let i = 0; i < historyApiBase.length; i++) {
const baseUrl = historyApiBase[i] + historyApiEndpoints[type];
try {
const response = await historyRequestOnce(baseUrl, uid, pn);
if (response.success) return response;
lastResponse = response;
} catch (err) {
if (i === historyApiBase.length - 1) throw err;
}
}
return lastResponse;
}
function renderHistoryList(panel, list) {
const body = panel.querySelector(".history-body");
if (!list.length && historyCurrentPage === 1) {
body.textContent = "暂无记录";
return;
}
const tabName = historyTabs.find(t => t.key === historyCurrentTab)?.name || "";
const infoHtml = `共 ${historyTotal} 条${tabName} · 第 ${historyCurrentPage} 页
`;
const itemsHtml = list.map(item => renderHistoryItem(item)).join("");
const pagerHtml = `\n \n `;
body.innerHTML = infoHtml + itemsHtml + pagerHtml;
body.querySelector(".history-prev").onclick = () => {
if (historyIsLoading) return;
if (historyCurrentPage > 1) {
historyCurrentPage -= 1;
loadHistoryPage(panel);
}
};
body.querySelector(".history-next").onclick = () => {
if (historyIsLoading) return;
if (!historyIsEnd) {
historyCurrentPage += 1;
loadHistoryPage(panel);
}
};
}
function renderHistoryItem(item) {
if ("message" in item) {
const date = item.time ? new Date(item.time * 1e3).toLocaleString() : "";
const link = item.oid ? `${biliVideoLinkPrefix}/av${item.oid}/#reply${item.rpid}` : "";
const linkHtml = link ? `跳转 ` : "";
return `${date} ${linkHtml}
${escapeHistoryHtml(item.message)}
`;
}
if ("content" in item) {
const date = item.ctime ? new Date(item.ctime * 1e3).toLocaleString() : "";
const link = item.oid ? `${biliVideoLinkPrefix}/av${item.oid}` : "";
const linkHtml = link ? `跳转 ` : "";
return `${date} ${linkHtml}
${escapeHistoryHtml(item.content)}
`;
}
if ("roomId" in item) {
const date = item.ts ? new Date(item.ts * 1e3).toLocaleString() : "";
const link = item.roomId ? `${biliLiveLinkPrefix}/${item.roomId}` : "";
const linkHtml = link ? `${escapeHistoryHtml(item.roomName)} ` : escapeHistoryHtml(item.roomName);
return `${linkHtml} (${escapeHistoryHtml(item.upName)})
${date}
${escapeHistoryHtml(item.text)}
`;
}
return "";
}
function escapeHistoryHtml(text) {
return String(text ?? "").replace(/[&<>"']/g, c => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
}[c]));
}
class HistoryApiResponse {
constructor(data) {
this.code = data?.code ?? -1;
this.message = data?.message || "";
this.ttl = data?.ttl || 1;
this.data = data?.data || null;
}
get success() {
return this.code === 0;
}
}
}
function initCommentEnhancer() {
GM_addStyle(GM_getResourceText("viewerCss"));
async function getWbiQueryString(params) {
const {img_url: img_url, sub_url: sub_url} = await fetch("https://api.bilibili.com/x/web-interface/nav").then(res => res.json()).then(json => json.data.wbi_img);
const imgKey = img_url.slice(img_url.lastIndexOf("/") + 1, img_url.lastIndexOf("."));
const subKey = sub_url.slice(sub_url.lastIndexOf("/") + 1, sub_url.lastIndexOf("."));
const originKey = imgKey + subKey;
const mixinKeyEncryptTable = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ];
const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join("").slice(0, 32);
const query = Object.keys(params).sort().map(key => {
const value = params[key].toString().replace(/[!'()*]/g, "");
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}).join("&");
const md5Fn = typeof md5 === "function" ? md5 : typeof global.md5 === "function" ? global.md5 : null;
if (!md5Fn) throw new Error("md5 未加载");
const wbiSign = md5Fn(query + mixinKey);
return `${query}&w_rid=${wbiSign}`;
}
function b2a(bvid) {
const XOR_CODE = 23442827791579n;
const MASK_CODE = 2251799813685247n;
const BASE = 58n;
const BYTES = [ "B", "V", 1, "", "", "", "", "", "", "", "", "" ];
const BV_LEN = BYTES.length;
const ALPHABET = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf".split("");
const DIGIT_MAP = [ 0, 1, 2, 9, 7, 5, 6, 4, 8, 3, 10, 11 ];
let r = 0n;
for (let i = 3; i < BV_LEN; i++) r = r * BASE + BigInt(ALPHABET.indexOf(bvid[DIGIT_MAP[i]]));
return `${r & MASK_CODE ^ XOR_CODE}`;
}
const videoRE = /https:\/\/www\.bilibili\.com\/video\/.*/;
const bangumiRE = /https:\/\/www.bilibili.com\/bangumi\/play\/.*/;
const dynamicRE = /https:\/\/t.bilibili.com\/\d+/;
const opusRE = /https:\/\/www.bilibili.com\/opus\/\d+/;
const spaceRE = /https:\/\/space.bilibili.com\/\d+/;
const festivalRE = /https:\/\/www.bilibili.com\/festival\/.*/;
const sortTypeConstant = {
LATEST: 0,
HOT: 2
};
const defaultPaginationOffsetStr = `{"offset":""}`;
const state = {
oid: void 0,
creatorId: void 0,
commentType: void 0,
replyList: void 0,
currentSortType: sortTypeConstant.HOT,
paginationOffsets: createPaginationOffsetState()
};
let replyAutoLoadObserver = null;
let startIsRunning = false;
let startHasPending = false;
function createPaginationOffsetState() {
return {
[sortTypeConstant.HOT]: {
1: defaultPaginationOffsetStr
},
[sortTypeConstant.LATEST]: {
1: defaultPaginationOffsetStr
}
};
}
if (spaceRE.test(global.location.href)) {
setupCommentBtnModifier();
return;
}
addStyle();
setupVideoChangeHandler();
start();
async function start() {
if (startIsRunning) {
startHasPending = true;
return;
}
startIsRunning = true;
try {
state.oid = state.creatorId = state.commentType = state.replyList = void 0;
state.currentSortType = sortTypeConstant.HOT;
state.paginationOffsets = createPaginationOffsetState();
const {href: href, pathname: pathname} = global.location;
let bangumiSeasonPromise = null;
let dynamicDetailPromise = null;
await setupStandardCommentContainer();
await waitFor(async () => {
const initialState = global?.__INITIAL_STATE__;
if (videoRE.test(href)) {
const videoID = pathname.replace("/video/", "").replace("/", "");
if (videoID.startsWith("av")) state.oid = videoID.slice(2);
if (videoID.startsWith("BV")) state.oid = b2a(videoID);
state.creatorId = initialState?.upData?.mid;
state.commentType = 1;
} else if (bangumiRE.test(href)) {
const bangumiPathMatch = pathname.match(/\/bangumi\/play\/(ep|ss)(\d+)/);
if (bangumiPathMatch?.[1] && bangumiPathMatch?.[2]) {
const key = bangumiPathMatch[1] === "ep" ? "ep_id" : "season_id";
bangumiSeasonPromise = bangumiSeasonPromise || fetch(`https://api.bilibili.com/pgc/view/web/season?${key}=${bangumiPathMatch[2]}`).then(res => res.json());
const season = await bangumiSeasonPromise;
if (season?.code === 0) if (bangumiPathMatch[1] === "ep") {
const epId = parseInt(bangumiPathMatch[2], 10);
const episode = season?.result?.episodes?.find(item => item?.id === epId);
if (episode?.aid) state.oid = `${episode.aid}`;
} else {
const aid = season?.result?.episodes?.[0]?.aid;
if (aid) state.oid = `${aid}`;
}
}
const upHref = document.querySelector("a[class*=upinfo_upLink]")?.href;
state.creatorId = upHref ? upHref.split("/").filter(item => !!item).pop() : -1;
state.commentType = 1;
} else if (dynamicRE.test(href)) {
const dynamicID = pathname.replace("/", "");
dynamicDetailPromise = dynamicDetailPromise || fetch(`https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${dynamicID}`).then(res => res.json()).then(json => {
if (json?.code !== 0) console.error("[bili-comment-enhancer] dynamic detail 接口返回异常", json);
return json;
});
const dynamicDetail = await dynamicDetailPromise;
const {code: code, data: data} = dynamicDetail;
if (code !== 0) return;
const basic = data?.item?.basic;
const author = data?.item?.modules?.module_author;
state.oid = basic?.comment_id_str;
state.commentType = basic?.comment_type;
state.creatorId = author?.mid;
} else if (opusRE.test(href)) {
const basic = initialState?.detail?.basic;
state.oid = basic?.comment_id_str;
state.creatorId = basic?.uid;
state.commentType = basic?.comment_type;
} else if (festivalRE.test(href)) {
const videoInfo = initialState?.videoInfo;
state.oid = videoInfo?.aid;
state.creatorId = videoInfo?.upMid;
state.commentType = 1;
}
state.replyList = document.querySelector(".reply-list");
if (state.oid && state.creatorId && state.commentType && state.replyList) {
state.creatorId = parseInt(state.creatorId, 10);
return true;
}
});
await enableSwitchingSortType();
await loadFirstPagination();
} finally {
startIsRunning = false;
if (startHasPending) {
startHasPending = false;
start();
}
}
}
async function setupStandardCommentContainer() {
const container = await waitFor(() => {
const standardContainer = document.querySelector(".comment-container");
const outdatedContainer = document.querySelector(".comment-wrapper .common");
const shadowRootContainer = document.querySelector("bili-comments");
return standardContainer || outdatedContainer || shadowRootContainer || null;
});
if (!container.classList.contains("comment-container")) container.parentElement.innerHTML = `\n \n `;
}
async function enableSwitchingSortType() {
const elements = await waitFor(() => {
const selectedReplyElement = document.querySelector(".comment-container .reply-header .nav-select-reply");
const hotSortElement = document.querySelector(".comment-container .reply-header .hot-sort");
const timeSortElement = document.querySelector(".comment-container .reply-header .time-sort");
if (selectedReplyElement || hotSortElement && timeSortElement) return {
selectedReplyElement: selectedReplyElement,
hotSortElement: hotSortElement,
timeSortElement: timeSortElement
};
});
const {selectedReplyElement: selectedReplyElement, hotSortElement: hotSortElement, timeSortElement: timeSortElement} = elements;
if (selectedReplyElement) return;
hotSortElement.style.color = "#18191C";
timeSortElement.style.color = "#9499A0";
hotSortElement.addEventListener("click", () => {
if (state.currentSortType === sortTypeConstant.HOT) return;
state.currentSortType = sortTypeConstant.HOT;
hotSortElement.style.color = "#18191C";
timeSortElement.style.color = "#9499A0";
loadFirstPagination();
});
timeSortElement.addEventListener("click", () => {
if (state.currentSortType === sortTypeConstant.LATEST) return;
state.currentSortType = sortTypeConstant.LATEST;
hotSortElement.style.color = "#9499A0";
timeSortElement.style.color = "#18191C";
loadFirstPagination();
});
}
async function loadFirstPagination() {
state.paginationOffsets[state.currentSortType] = {
1: defaultPaginationOffsetStr
};
const {data: firstPaginationData, code: resultCode} = await getPaginationData(1);
await waitFor(() => {
if (document.body.contains(state.replyList)) return true;
state.replyList = document.querySelector(".reply-list");
return document.body.contains(state.replyList);
});
state.replyList.innerHTML = "";
if (resultCode !== 0) {
state.replyList.innerHTML = '无法从 API 获取评论数据
';
addAnchor(true);
return;
}
const totalReplyElement = document.querySelector(".comment-container .reply-header .total-reply");
const totalReplyCount = parseInt(firstPaginationData?.cursor?.all_count) || 0;
totalReplyElement.textContent = totalReplyCount;
if (totalReplyCount === 0) {
state.replyList.innerHTML = '没有更多评论
';
addAnchor(true);
return;
}
if (firstPaginationData.top_replies && firstPaginationData.top_replies.length !== 0) {
const topReplyData = firstPaginationData.top_replies[0];
appendReplyItem(topReplyData, true);
}
for (const replyData of firstPaginationData.replies) appendReplyItem(replyData);
addAnchor();
}
async function getPaginationData(paginationNumber) {
if (!state.paginationOffsets[state.currentSortType]) state.paginationOffsets[state.currentSortType] = {
1: defaultPaginationOffsetStr
};
const offsetCache = state.paginationOffsets[state.currentSortType];
if (!offsetCache[1]) offsetCache[1] = defaultPaginationOffsetStr;
if (!offsetCache[paginationNumber]) {
if (paginationNumber !== 1) return {
code: -1,
data: {
replies: []
}
};
offsetCache[paginationNumber] = defaultPaginationOffsetStr;
}
const params = {
oid: state.oid,
type: state.commentType,
wts: parseInt(Date.now() / 1e3, 10),
mode: state.currentSortType === sortTypeConstant.HOT ? 3 : 2,
pagination_str: offsetCache[paginationNumber]
};
const url = `https://api.bilibili.com/x/v2/reply/wbi/main?${await getWbiQueryString(params)}`;
const result = await fetch(url).then(res => res.json());
const nextOffset = extractNextOffset(result);
if (nextOffset) offsetCache[paginationNumber + 1] = `{"offset":"${nextOffset}"}`;
return result;
}
function extractNextOffset(result) {
const cursor = result?.data?.cursor;
let paginationReply = cursor?.pagination_reply;
if (!paginationReply) return cursor?.next_offset || cursor?.next;
if (typeof paginationReply === "string") try {
paginationReply = JSON.parse(paginationReply);
} catch (err) {
return paginationReply;
}
if (typeof paginationReply === "object") return paginationReply.next_offset || paginationReply.offset || paginationReply.next;
return;
}
function appendReplyItem(replyData, isTopReply) {
const replyItemElement = document.createElement("div");
replyItemElement.classList.add("reply-item");
replyItemElement.innerHTML = `\n \n
\n \n
\n
\n ${replyData.member.pendant.image ? `\n
\n
\n
\n ` : ""}\n
\n
\n
\n \n
\n
\n
\n ${replyData.member.user_sailing?.cardbg ? `\n
\n
\n NO. \n \n ${replyData.member.user_sailing.cardbg.fan.number.toString().padStart(6, "0")} \n
\n ` : ""}\n
\n
\n
\n
${replyData.member.uname} \n
LV${replyData.member.level_info.current_level} \n ${state.creatorId === replyData.mid ? '
' : ""}\n
\n
\n
\n ${isTopReply ? '置顶 ' : ""}${replyData.content.pictures ? `` : ""}${getConvertedMessage(replyData.content)} \n \n ${replyData.content.pictures ? `\n
\n
\n ${getImageItems(replyData.content.pictures)}\n
\n
\n ` : ""}\n
\n
${getFormattedTime(replyData.ctime)} \n
\n \n ${replyData.like} \n \n
\n \n \n
回复 \n
查看成分 \n
\n
\n ${replyData.card_label ? replyData.card_label.reduce((acc, cur) => acc + `${cur.text_content} `, "") : ""}\n
\n
\n
\n
\n \n
\n ${getSubReplyItems(replyData.replies) || ""}\n ${replyData.rcount > (replyData.replies?.length ?? 0) ? `\n
\n
\n 共${replyData.rcount}条回复 \n 点击查看 \n
\n
\n ` : ""}\n
\n
\n
\n `;
state.replyList.appendChild(replyItemElement);
const previewImageContainer = replyItemElement.querySelector(".preview-image-container");
if (previewImageContainer) new Viewer(previewImageContainer, {
title: false,
toolbar: false,
tooltip: false,
keyboard: false
});
const subReplyList = replyItemElement.querySelector(".sub-reply-list");
const viewMoreBtn = replyItemElement.querySelector(".view-more-btn");
viewMoreBtn && viewMoreBtn.addEventListener("click", () => {
loadPaginatedSubReplies(replyData.rpid, subReplyList, replyData.rcount, 1);
});
}
function getFormattedTime(ms) {
const time = new Date(ms * 1e3);
const year = time.getFullYear();
const month = (time.getMonth() + 1).toString().padStart(2, "0");
const day = time.getDate().toString().padStart(2, "0");
const hour = time.getHours().toString().padStart(2, "0");
const minute = time.getMinutes().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}`;
}
function getMemberLevelColor(level) {
return {
0: "#C0C0C0",
1: "#BBBBBB",
2: "#8BD29B",
3: "#7BCDEF",
4: "#FEBB8B",
5: "#EE672A",
6: "#F04C49"
}[level];
}
function getConvertedMessage(content) {
let result = content.message;
const keywordBlacklist = [ "https://www.bilibili.com/video/av", "https://b23.tv/mall-" ];
if (content.vote && content.vote.deleted === false) {
const linkElementHTML = `${content.vote.title} `;
keywordBlacklist.push(linkElementHTML);
result = result.replace(`{vote:${content.vote.id}}`, linkElementHTML);
}
if (content.emote) for (const [key, value] of Object.entries(content.emote)) {
const imageElementHTML = ` `;
keywordBlacklist.push(imageElementHTML);
result = result.replaceAll(key, imageElementHTML);
}
result = result.replaceAll(/(\d{1,2}[::]){1,2}\d{1,2}/g, timestamp => {
timestamp = timestamp.replaceAll(":", ":");
if (!(videoRE.test(global.location.href) || bangumiRE.test(global.location.href) || festivalRE.test(global.location.href))) return timestamp;
const parts = timestamp.split(":");
if (parts.some(part => parseInt(part) >= 60)) return timestamp;
let totalSecond;
if (parts.length === 2) totalSecond = parseInt(parts[0]) * 60 + parseInt(parts[1]); else if (parts.length === 3) totalSecond = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
if (Number.isNaN(totalSecond)) return timestamp;
const linkElementHTML = `${timestamp} `;
keywordBlacklist.push(linkElementHTML);
return linkElementHTML;
});
if (content.at_name_to_mid) for (const [key, value] of Object.entries(content.at_name_to_mid)) {
const linkElementHTML = `@${key} `;
keywordBlacklist.push(linkElementHTML);
result = result.replaceAll(`@${key}`, linkElementHTML);
}
if (Object.keys(content.jump_url).length) {
const entries = [].concat(Object.entries(content.jump_url).filter(entry => entry[0].startsWith("https://")), Object.entries(content.jump_url).filter(entry => !entry[0].startsWith("https://")));
for (const [key, value] of entries) {
const href = key.startsWith("BV") || /^av\d+$/.test(key) ? `https://www.bilibili.com/video/${key}` : value.pc_url || key;
if (href.includes("search.bilibili.com") && keywordBlacklist.join("").includes(key)) continue;
const linkElementHTML = `${value.title} `;
keywordBlacklist.push(linkElementHTML);
result = result.replaceAll(key, linkElementHTML);
}
}
return result;
}
function escapeAttr(text) {
return String(text ?? "").replace(/"/g, """).replace(/'/g, "'");
}
function getImageItems(images) {
images = images.slice(0, 3);
const imageSizeConfig = {
1: "max-width: 280px; max-height: 180px;",
2: "width: 128px; height: 128px;",
3: "width: 96px; height: 96px;"
}[images.length];
let result = "";
for (const image of images) result += ``;
return result;
}
function getSubReplyItems(subReplies) {
if (!subReplies || subReplies.length === 0) return;
let result = "";
for (const replyData of subReplies) result += `\n \n
\n
\n ${getConvertedMessage(replyData.content)} \n \n
\n
${getFormattedTime(replyData.ctime)} \n
\n \n ${replyData.like} \n \n
\n \n \n
回复 \n
查看成分 \n
\n
\n `;
return result;
}
async function loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, paginationNumber) {
const subReplyData = await fetch(`https://api.bilibili.com/x/v2/reply/reply?oid=${state.oid}&pn=${paginationNumber}&ps=10&root=${rootReplyID}&type=${state.commentType}`).then(res => res.json()).then(json => json.data);
if (subReplyData.replies) subReplyList.innerHTML = getSubReplyItems(subReplyData.replies);
addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, paginationNumber);
}
function addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, currentPageNumber) {
if (subReplyAmount <= 10) return;
const pageAmount = Math.ceil(subReplyAmount / 10);
const pageSwitcher = document.createElement("div");
pageSwitcher.classList.add("view-more");
pageSwitcher.innerHTML = `\n \n `;
pageSwitcher.querySelector(".pagination-to-prev-btn")?.addEventListener("click", () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber - 1));
pageSwitcher.querySelector(".pagination-to-next-btn")?.addEventListener("click", () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber + 1));
pageSwitcher.querySelectorAll(".pagination-page-number:not(.current-page)")?.forEach(pageNumberElement => {
const number = parseInt(pageNumberElement.textContent);
pageNumberElement.addEventListener("click", () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, number));
});
subReplyList.appendChild(pageSwitcher);
}
function addAnchor(cleanOnly) {
const oldPageSwitcher = document.querySelector(".comment-container .reply-warp .page-switcher");
oldPageSwitcher && oldPageSwitcher.remove();
const oldAnchor = document.querySelector(".comment-container .reply-warp .anchor-for-loading");
oldAnchor && oldAnchor.remove();
replyAutoLoadObserver && replyAutoLoadObserver.disconnect();
replyAutoLoadObserver = null;
if (cleanOnly) return;
const anchorElement = document.createElement("div");
anchorElement.classList.add("anchor-for-loading");
anchorElement.textContent = "正在加载...";
anchorElement.style = `\n width: calc(100% - 22px);\n height: 40px;\n margin-left: 22px;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: translateY(-60px);\n color: #61666d;\n `;
document.querySelector(".comment-container .reply-warp").appendChild(anchorElement);
let paginationCounter = 1;
let isLoading = false;
replyAutoLoadObserver = new IntersectionObserver(async entries => {
if (!entries[0].isIntersecting) return;
if (isLoading) return;
isLoading = true;
const nextPage = ++paginationCounter;
const {data: newPaginationData, code: resultCode} = await getPaginationData(nextPage);
const replyLength = newPaginationData?.replies?.length || 0;
if (!newPaginationData?.replies || replyLength === 0) {
anchorElement.textContent = "所有评论已加载完毕";
replyAutoLoadObserver.disconnect();
replyAutoLoadObserver = null;
return;
}
if (resultCode !== 0) {
anchorElement.textContent = "评论加载失败";
replyAutoLoadObserver.disconnect();
replyAutoLoadObserver = null;
return;
}
for (const replyData of newPaginationData.replies) appendReplyItem(replyData);
anchorElement.textContent = "正在加载...";
isLoading = false;
});
replyAutoLoadObserver.observe(anchorElement);
}
function setupCommentBtnModifier() {
if (document.documentElement.dataset.commentBtnModifierBound === "1") return;
document.documentElement.dataset.commentBtnModifierBound = "1";
const findDetailUrl = dynItem => {
const links = dynItem.querySelectorAll("a[href]");
for (const link of links) {
const href = link.href || link.getAttribute("href");
if (!href) continue;
if (/^https?:\/\/t\.bilibili\.com\/\d+/.test(href)) return href;
}
for (const link of links) {
const href = link.href || link.getAttribute("href");
if (!href) continue;
if (/^https?:\/\/www\.bilibili\.com\/opus\/\d+/.test(href)) return href;
}
return null;
};
document.addEventListener("click", event => {
const commentEl = event.target?.closest?.(".bili-dyn-action.comment");
if (!commentEl) return;
const dynItem = commentEl.closest(".bili-dyn-item");
if (!dynItem) return;
const url = findDetailUrl(dynItem);
if (!url) return;
event.preventDefault();
event.stopImmediatePropagation();
global.location.href = url;
}, true);
}
function addStyle() {
const avatarCSS = document.createElement("style");
avatarCSS.textContent = `\n .reply-item .root-reply-avatar .avatar .bili-avatar {\n width: 48px;\n height: 48px;\n }\n\n .sub-reply-item .sub-reply-avatar .avatar .bili-avatar {\n width: 30px;\n height: 30px;\n }\n\n @media screen and (max-width: 1620px) {\n .reply-item .root-reply-avatar .avatar .bili-avatar {\n width: 40px;\n height: 40px;\n }\n\n .sub-reply-item .sub-reply-avatar .avatar .bili-avatar {\n width: 24px;\n height: 24px;\n }\n }\n `;
document.head.appendChild(avatarCSS);
const viewMoreCSS = document.createElement("style");
viewMoreCSS.textContent = `\n .sub-reply-container .view-more-btn:hover {\n color: #00AEEC;\n }\n\n .view-more {\n padding-left: 8px;\n color: #222;\n font-size: 13px;\n user-select: none;\n }\n\n .pagination-page-count {\n margin-right: 10px;\n }\n\n .pagination-page-dot,\n .pagination-page-number {\n margin: 0 4px;\n }\n\n .pagination-btn,\n .pagination-page-number {\n cursor: pointer;\n }\n\n .current-page,\n .pagination-btn:hover,\n .pagination-page-number:hover {\n color: #00AEEC;\n }\n `;
document.head.appendChild(viewMoreCSS);
const pageSwitcherCSS = document.createElement("style");
pageSwitcherCSS.textContent = `\n .page-switcher-wrapper {\n display: flex;\n font-size: 14px;\n color: #666;\n user-select: none;\n }\n\n .page-switcher-wrapper span {\n margin-right: 6px;\n }\n\n .page-switcher-wrapper span:not(.page-switcher-dot){\n display: flex;\n padding: 0 14px;\n height: 38px;\n align-items: center;\n border: 1px solid #D7DDE4;\n border-radius: 4px;\n cursor: pointer;\n transition: border-color 0.2s;\n }\n\n .page-switcher-prev-btn:hover,\n .page-switcher-next-btn:hover,\n .page-switcher-number:hover {\n border-color: #00A1D6 !important;\n }\n\n .page-switcher-current-page {\n color: white;\n background-color: #00A1D6;\n border-color: #00A1D6 !important;\n }\n\n .page-switcher-dot {\n padding: 0 5px;\n display: flex;\n align-items: center;\n color: #CCC;\n }\n\n .page-switcher-prev-btn__disabled,\n .page-switcher-next-btn__disabled {\n color: #D7DDE4 !important;\n cursor: not-allowed !important;\n }\n `;
document.head.appendChild(pageSwitcherCSS);
const otherCSS = document.createElement("style");
otherCSS.textContent = `\n .jump-link {\n color: #008DDA;\n }\n\n .login-tip,\n .fixed-reply-box,\n .v-popover:has(.login-panel-popover) {\n display: none;\n }\n `;
document.head.appendChild(otherCSS);
if (dynamicRE.test(global.location.href) || opusRE.test(global.location.href)) {
const dynPageCSS = document.createElement("style");
dynPageCSS.textContent = `\n #app .opus-detail {\n min-width: 960px;\n }\n\n #app .opus-detail .right-sidebar-wrap {\n margin-left: 980px !important;\n transition: none;\n }\n\n #app > .content {\n min-width: 960px;\n }\n\n .v-popover:has(.login-panel-popover),\n .fixed-reply-box,\n .login-tip {\n display: none;\n }\n\n .note-prefix {\n fill: #BBBBBB;\n }\n\n .bili-comment-container svg {\n fill: inherit !important;\n }\n `;
document.head.appendChild(dynPageCSS);
}
if (festivalRE.test(global.location.href)) {
const miscCSS = document.createElement("style");
miscCSS.textContent = `\n :root {\n --text1: #18191C;\n --text3: #9499A0;\n --brand_pink: #FF6699;\n --graph_bg_thick: #e3e5e7;\n }\n\n .page-switcher {\n margin-top: 40px;\n }\n\n .van-popover:has(.unlogin-popover) {\n display: none !important;\n }\n `;
document.head.appendChild(miscCSS);
}
}
function setupVideoChangeHandler() {
if (festivalRE.test(global.location.href)) {
let record;
const getBVID = () => global?.__INITIAL_STATE__?.videoInfo?.bvid;
setInterval(() => {
if (!record) record = getBVID(); else if (record !== getBVID()) global.location.href = `${global.location.origin}${global.location.pathname}?bvid=${getBVID()}`;
}, 1e3);
}
if (videoRE.test(global.location.href) || bangumiRE.test(global.location.href)) {
const getPageKey = () => {
const url = new URL(global.location.href);
const p = url.searchParams.get("p");
const oid = url.searchParams.get("oid");
const params = new URLSearchParams;
if (p) params.set("p", p);
if (oid) params.set("oid", oid);
const qs = params.toString();
return url.origin + url.pathname + (qs ? `?${qs}` : "");
};
let oldHref = getPageKey();
setInterval(() => {
const newHref = getPageKey();
if (oldHref !== newHref) {
oldHref = newHref;
start();
}
}, 1e3);
}
}
}
})();