// ==UserScript==
// @name Bilibili - 在未登录的情况下照常加载评论
// @namespace https://bilibili.com/
// @version 3.0
// @description 在未登录的情况下照常加载评论 | V3.0 根据网站改版进行适配
// @license GPL-3.0
// @author DD1969
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/bangumi/play/*
// @match https://t.bilibili.com/*
// @match https://www.bilibili.com/opus/*
// @match https://space.bilibili.com/*
// @match https://www.bilibili.com/read/cv*
// @match https://www.bilibili.com/festival*
// @match https://www.bilibili.com/list/*
// @icon https://www.bilibili.com/favicon.ico
// @require https://lib.baomitu.com/viewerjs/1.11.4/viewer.min.js
// @grant unsafeWindow
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @downloadURL none
// ==/UserScript==
(async function() {
'use strict';
// no need to continue this script if user already logged in
if (document.cookie.includes('DedeUserID')) return;
// patch for 'unsafeWindow is not defined'
const global = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
// initialize options
const options = {
// 使用分页加载主评论
enableReplyPagination: GM_getValue('enableReplyPagination', false),
// 一次性加载所有子评论
enableLoadAllSubRepliesAtOnce: GM_getValue('enableLoadAllSubRepliesAtOnce', false),
// 显示用户头像边框
enableAvatarPendent: GM_getValue('enableAvatarPendent', true),
// 显示评论右上角的大航海装饰
enableSailingDecoration: GM_getValue('enableSailingDecoration', true),
// 显示"笔记"前缀
enableNotePrefix: GM_getValue('enableNotePrefix', true),
// 显示"热评"标签
enableHotTag: GM_getValue('enableHotTag', true),
// 显示"UP主觉得很赞"标签
enableLikedTag: GM_getValue('enableLikedTag', true),
// 显示大会员用户名的颜色为粉色
enableVipUserNameColor: GM_getValue('enableVipUserNameColor', true),
}
// RegExp
const videoRE = /https:\/\/www\.bilibili\.com\/video\/.*/;
const bangumiRE = /https:\/\/www.bilibili.com\/bangumi\/play\/.*/;
const quoteRE = /https:\/\/t.bilibili.com\/\d+/;
const opusRE = /https:\/\/www.bilibili.com\/opus\/\d+/;
const spaceRE = /https:\/\/space.bilibili.com\/\d+/;
const articleRE = /https:\/\/www.bilibili.com\/read\/cv\d+.*/;
const festivalRE = /https:\/\/www.bilibili.com\/festival\/.*/;
const listRE = /https:\/\/www.bilibili.com\/list\/.*/;
// essential data or elements
let oid, createrID, commentType, replyList;
// define sort types
const sortTypeConstant = { LATEST: 0, HOT: 2 };
let currentSortType;
// use to prevent loading duplicated main reply
let replyPool;
// ---------- variables above ----------
// make comment buttons in dynamic page do their job
if (spaceRE.test(global.location.href)) { setupCommentBtnModifier(); return; }
// add style patch
await addStyle();
// setup standard comment container
await setupStandardCommentContainer();
// setup video change handler
setupVideoChangeHandler();
// setup setting panel & entry
setupSettingPanel();
setupSettingPanelEntry();
// start loading comments
start();
// ---------- functions below ----------
async function start() {
// initialize
oid = createrID = commentType = replyList = undefined;
replyPool = {};
currentSortType = sortTypeConstant.HOT;
// collect essential data or elements
await new Promise(resolve => {
const timer = setInterval(async () => {
// collect oid, createrID, commentType
if (videoRE.test(global.location.href)) {
const videoID = global.location.pathname.replace('/video/', '').replace('/', '');
if (videoID.startsWith('av')) oid = videoID.slice(2);
if (videoID.startsWith('BV')) oid = b2a(videoID);
createrID = global?.__INITIAL_STATE__?.upData?.mid;
commentType = 1;
} else if (bangumiRE.test(global.location.href)) {
oid = b2a(document.querySelector('a[class*=mediainfo_avLink]')?.textContent);
createrID = document.querySelector('a[class*=upinfo_upLink]')?.href?.split('/').filter(item => !!item).pop() || -1;
commentType = 1
} else if (quoteRE.test(global.location.href)) {
oid = global.location.pathname.replace('/', '');
createrID = await fetch(`https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${oid}`).then(res => res.json()).then(json => json.data.item.modules.module_author.mid).catch(err => console.log('无法获取UP主ID'));
commentType = 17;
const videoLinkElement = document.querySelector('.content .bili-dyn-item .bili-dyn-item__body .bili-dyn-content__orig:not(.reference) .bili-dyn-card-video');
if (videoLinkElement) {
oid = b2a(new URL(videoLinkElement.href).pathname.replace('/video/', '').replace('/', ''));
commentType = 1;
}
} else if (opusRE.test(global.location.href)) {
oid = global?.__INITIAL_STATE__?.detail?.basic?.comment_id_str;
createrID = global?.__INITIAL_STATE__?.detail?.basic?.uid;
commentType = global?.__INITIAL_STATE__?.detail?.basic?.comment_type; // should be '11'
} else if (articleRE.test(global.location.href)) {
oid = global?.__INITIAL_STATE__?.cvid;
createrID = global?.__INITIAL_STATE__?.readInfo?.author?.mid;
commentType = 12;
} else if (festivalRE.test(global.location.href)) {
oid = global?.__INITIAL_STATE__?.videoInfo?.aid;
createrID = global?.__INITIAL_STATE__?.videoInfo?.upMid;
commentType = 1;
} else if (listRE.test(global.location.href)) {
oid = new URLSearchParams(global.location.search).get('oid');
createrID = document.querySelector('.up-panel-container .up-avatar')?.getAttribute('href')?.split('/')?.pop();
commentType = 1;
}
// get reply container
replyList = document.querySelector('.reply-list');
// final check
if (oid && createrID && commentType && replyList) {
createrID = parseInt(createrID);
clearInterval(timer);
resolve();
}
}, 200);
});
// enable switching sort type
await enableSwitchingSortType();
// load first pagination
await loadFirstPagination();
}
async function enableSwitchingSortType() {
// collect elements
const { selectedReplyElement, hotSortElement, timeSortElement } = await new Promise(resolve => {
const timer = setInterval(() => {
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)) {
clearInterval(timer);
resolve({ selectedReplyElement, hotSortElement, timeSortElement });
}
}, 200);
});
// no need to setup click event listener if replies are selected
if (selectedReplyElement) return;
// reset text color
hotSortElement.style.color = '#18191C';
timeSortElement.style.color = '#9499A0';
// setup click event listener
hotSortElement.addEventListener('click', () => {
if (currentSortType === sortTypeConstant.HOT) return;
currentSortType = sortTypeConstant.HOT;
hotSortElement.style.color = '#18191C';
timeSortElement.style.color = '#9499A0';
loadFirstPagination();
});
timeSortElement.addEventListener('click', () => {
if (currentSortType === sortTypeConstant.LATEST) return;
currentSortType = sortTypeConstant.LATEST;
hotSortElement.style.color = '#9499A0';
timeSortElement.style.color = '#18191C';
loadFirstPagination();
});
}
async function loadFirstPagination() {
// get data of first pagination
const { data: firstPaginationData, code: resultCode } = await getPaginationData(1);
// make sure 'replyList' exists
await new Promise(resolve => {
const timer = setInterval(() => {
if (document.body.contains(replyList)) { clearInterval(timer); resolve(); }
else { replyList = document.querySelector('.reply-list'); }
}, 200);
});
// clear replyList
replyList.innerHTML = '';
// clear replyPool
replyPool = {};
// script ends here if not able to fetch pagination data
if (resultCode !== 0) {
replyList.innerHTML = '
无法从API获取评论数据
';
return;
}
// load the top reply if it exists
if (firstPaginationData.top_replies && firstPaginationData.top_replies.length !== 0) {
const topReplyData = firstPaginationData.top_replies[0];
appendReplyItem(topReplyData, true);
}
// script ends here if there is no reply of this video
if (firstPaginationData.replies.length === 0) return;
// load normal replies
for (const replyData of firstPaginationData.replies) {
appendReplyItem(replyData);
}
// add page loader
options.enableReplyPagination
? addReplyPageSwitcher()
: addAnchor();
}
async function getPaginationData(paginationNumber) {
return await fetch(`https://api.bilibili.com/x/v2/reply?oid=${oid}&type=${commentType}&sort=${currentSortType}&pn=${paginationNumber}`)
.then(res => res.json())
.then(json => ({
data: json.data,
code: json.code
}));
}
function appendReplyItem(replyData, isTopReply) {
if (!options.enableReplyPagination && replyPool[replyData.rpid_str]) return;
const replyItemElement = document.createElement('div');
replyItemElement.classList.add('reply-item');
replyItemElement.innerHTML = `
`;
replyList.appendChild(replyItemElement);
if (!options.enableReplyPagination) replyPool[replyData.rpid_str] = true;
// setup image viewer
const previewImageContainer = replyItemElement.querySelector('.preview-image-container');
if (previewImageContainer) new Viewer(previewImageContainer, { title: false, toolbar: false, tooltip: false, keyboard: false });
// setup view more button
const subReplyList = replyItemElement.querySelector('.sub-reply-list');
const viewMoreBtn = replyItemElement.querySelector('.view-more-btn');
viewMoreBtn && viewMoreBtn.addEventListener('click', () => {
options.enableLoadAllSubRepliesAtOnce
? loadAllSubReplies(replyData.rpid, subReplyList)
: loadPaginatedSubReplies(replyData.rpid, subReplyList, replyData.rcount, 1);
});
}
function getFormattedTime(ms) {
const time = new Date(ms * 1000);
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 ({
1: '#BBBBBB',
2: '#8BD29B',
3: '#7BCDEF',
4: '#FEBB8B',
5: '#EE672A',
6: '#F04C49'
})[level];
}
function getConvertedMessage(content) {
let result = content.message;
// convert emote tag to image
if (content.emote) {
for (const [key, value] of Object.entries(content.emote)) {
const imageElementHTML = ``;
result = result.replaceAll(key, imageElementHTML);
}
}
// convert timestamp to link
result = result.replaceAll(/(\d{1,2}[::]){1,2}\d{1,2}/g, (timestamp) => {
timestamp = timestamp.replaceAll(':', ':');
// return plain text if no video in page
if(!(videoRE.test(global.location.href) || bangumiRE.test(global.location.href) || festivalRE.test(global.location.href) || listRE.test(global.location.href))) return timestamp;
const parts = timestamp.split(':');
// return plain text if any part of timestamp equal to or bigger than 60
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]);
// return plain text if failed to get vaild number of second
if (Number.isNaN(totalSecond)) return timestamp;
return `${timestamp}`;
});
// convert url to link
if (Object.keys(content.jump_url).length) {
for (const [key, value] of Object.entries(content.jump_url)) {
const href = key.startsWith('BV') ? `https://www.bilibili.com/video/${key}` : (value.pc_url || key);
const linkElementHTML = `${value.title}`;
result = result.replaceAll(key, linkElementHTML);
}
}
// convert @ user
if (content.at_name_to_mid) {
for (const [key, value] of Object.entries(content.at_name_to_mid)) {
const linkElementHTML = `@${key}`;
result = result.replaceAll(`@${key}`, linkElementHTML);
}
}
return result;
}
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 += `