// ==UserScript==
// @name B站显示点赞率、投币率、收藏率
// @namespace http://tampermonkey.net/
// @version 1.0.5
// @description 显示b站 | bilibili | 哔哩哔哩 点赞率、投币率、收藏率
// @license MIT
// @author 魂hp
// @website https://space.bilibili.com/474989498
// @match *://www.bilibili.com/
// @match *://www.bilibili.com/?*
// @match *://www.bilibili.com/video/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant GM.addStyle
// @grant unsafeWindow
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @downloadURL none
// ==/UserScript==
(function () {
// representation字段表示比率的表示形式,该字段为 fractions 时表示为分数,该字段为 percentage 时表示为百分比
let representation = GM_getValue('representation');
if (representation == null) {
GM_setValue('representation', 'percentage');
representation = 'percentage';
}
const isFractions = representation == 'fractions';
// 注册脚本菜单以实现两种表示方式的相互转换
GM_registerMenuCommand(
'切换比率的表示方式(百分比和分数)',
function () {
representation = representation == 'fractions' ? 'percentage' : 'fractions';
GM_setValue('representation', representation);
unsafeWindow.location.reload();
}
);
// 判断当前页面,如果 pathname 符合对应的正则说明在视频页,如果 pathname 符合对应的正则说明在视频页
let currentPage = 'unknown';
if (unsafeWindow.location.pathname == '/') {
currentPage = 'mainPage';
} else if (unsafeWindow.location.pathname.match(/\/video\/.*\//)) {
currentPage = 'videoPage';
}
// 工具函数:根据播放量和对应的数据算出比率并获取对应的颜色
function getRateAndColor(view, oneOfVideoStat) {
let res = {
rate: 0,
color: currentPage == 'videoPage' ? '#222' : 'inherit'
};
let num = view / oneOfVideoStat;
if (num == Infinity) {
return res;
}
// 当比率大于十分之一设置为橘色,大于二十五分之一设置为紫色,其他则设置为黑色(如果需要添加其他的范围对应的颜色或修改颜色可以改这部分)
if (num <= 10) {
if (isFractions) {
res.rate = num.toFixed(2);
} else {
res.rate = (oneOfVideoStat * 100 / view).toFixed(2);
}
res.color = 'DarkOrange';
} else if (num <= 25) {
if (isFractions) {
res.rate = num.toFixed(1);
} else {
res.rate = (oneOfVideoStat * 100 / view).toFixed(2);
}
res.color = 'violet';
} else {
if (isFractions) {
res.rate = num.toFixed(0);
} else {
res.rate = (oneOfVideoStat * 100 / view).toFixed(2);
}
}
return res;
}
// 当前 pathname 为 / 说明在首页
if (currentPage == 'mainPage') {
const originFetch = unsafeWindow.fetch;
let urlToDataMap = new Map(); // 一个map,键是视频uri,值为该视频的各项数据(播放、点赞、弹幕)
// 劫持b站的 fetch 请求,判断该 fetch 请求是不是包含视频数据,如果包含则将视频数据加入 map
// 参考自 https://juejin.cn/post/7135590843544502308
window.unsafeWindow.fetch = (url, options) => {
return originFetch(url, options).then(async (response) => {
if (url.startsWith('https://api.bilibili.com/x/web-interface/wbi/index/top/feed/rcmd')) {
const responseClone = response.clone();
let res = await responseClone.json();
for (let tmp of res.data.item) {
processURIAndStat(tmp.uri, tmp.stat);
}
}
return response;
});
};
// 当 DOMContentLoaded 事件发生后,将不是动态加载的哪些视频的数据加入到 map 中
document.addEventListener('DOMContentLoaded', function () {
for (let tmp of unsafeWindow.__pinia.feed.data.recommend.item) {
processURIAndStat(tmp.uri, tmp.stat);
}
const cards = document.querySelectorAll('div.feed-card');
for (let card of cards) {
addLikeRateToCard(card);
}
});
// 一个工具函数,用于对uri和stat进行加工,并添加到 urlToDataMap 中
function processURIAndStat(uri, stat) {
if (uri != null && uri != '' && stat != null) {
const rateAndColor = getRateAndColor(stat.view, stat.like);
stat.rate = rateAndColor.rate;
stat.color = rateAndColor.color;
urlToDataMap.set(uri, stat);
}
}
// 一个工具函数,用于将对应格式的点赞路添加到视频卡片上
function addLikeRateToCard(node) {
const uri = node.querySelector('.bili-video-card__image--link')?.href;
const stat = urlToDataMap.get(uri);
// 下面的这一行代码会导致浏览器尺寸发生变化时部分视频卡片上的点赞率消失,如果你很介意这一点可以将下面这一行代码删掉或注释掉(就是代码前面加上//),但是注释掉或者删掉会导致脚本占用更多的空间(不会太多)
urlToDataMap.delete(uri);
if (stat != null) {
let span = document.createElement('span');
span.classList.add('bili-video-card__stats--item');
if (isFractions) {
span.innerHTML = `
1/
`;
} else {
span.innerHTML = `
%
`;
}
let data = span.querySelector('#data');
data.style.color = stat.color;
data.textContent = stat.rate;
node.querySelector('.bili-video-card__stats--left').appendChild(span);
}
}
// 创建MutationObserver,监控新插入的视频卡片
new MutationObserver((mutationsList) => {
// 遍历每一个发生变化的mutation
for (let mutation of mutationsList) {
// 检查每个添加的子节点
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// 遍历每个添加的子节点
mutation.addedNodes.forEach(node => {
if (node.nodeName == 'DIV' && node.classList.contains('enable-no-interest')) {
addLikeRateToCard(node);
}
});
}
}
}).observe(document, {
childList: true,
subtree: true
});
} else if (currentPage == 'videoPage') {
document.addEventListener('DOMContentLoaded', function () {
if (!(unsafeWindow?.__INITIAL_STATE__?.videoData?.stat?.view)) {
return;
}
// 修改样式
GM.addStyle(`
.video-toolbar-left-item{
width:auto !important;
}
.toolbar-left-item-wrap{
display:flex !important;
margin-right: 12px !important;
}
.video-share-info{
width:auto !important;
max-width:90px;
}
.video-share-info-text{
position: relative !important;
}
`);
// 百分比形式会占用更大的空间,需要额外添加样式
if (!isFractions) {
GM.addStyle(`
.video-toolbar-item-icon {
margin-right:6px !important;
}
.toolbar-right-note{
margin-right:5px !important;
}
.toolbar-right-ai{
margin-right:12px !important;
}
`);
}
class videoData {
videoStat = {
view: 0,
like: 0,
coin: 0,
favorite: 0,
share: 0
};
constructor() {
this.initVideoStat();
}
initVideoStat() {
for (let key in this.videoStat) {
this.videoStat[key] = unsafeWindow.__INITIAL_STATE__.videoData.stat[key];
}
}
// 计算点赞率、投币率、收藏率、转发率,并获取对应的颜色
getRateAndColorByNameStr(nameStr) {
return getRateAndColor(this.videoStat.view, this.videoStat[nameStr]);
}
}
const vData = new videoData();
//添加元素
const div = {
like: {},
coin: {},
favorite: {},
share: {}
};
for (let e in div) {
div[e] = document.createElement('div');
div[e].style.setProperty('display', 'flex')
div[e].style.setProperty('align-items', 'center')
if (isFractions) {
div[e].innerHTML = `
≈
`;
} else {
div[e].innerHTML = `
≈
%
`;
}
}
// 更新数据
function updateRate() {
for (let e in div) {
let data = div[e].querySelector('#data');
let rateAndColor = vData.getRateAndColorByNameStr(e);
data.style.color = rateAndColor.color;
data.textContent = rateAndColor.rate;
}
}
updateRate();
let addElementObserver = new MutationObserver(function (mutationsList) {
for (let mutation of mutationsList) {
if (mutation.type == 'attributes') {
addElementObserver.disconnect();
document
.querySelector('.video-like')
.parentNode.appendChild(div.like);
document
.querySelector('.video-coin')
.parentNode.appendChild(div.coin);
document
.querySelector('.video-fav')
.parentNode.appendChild(div.favorite);
document
.querySelector('.video-share-wrap')
.parentNode.appendChild(div.share);
}
}
});
addElementObserver.observe(document.querySelector('#arc_toolbar_report'), {
childList: true,
subtree: true,
attributes: true
});
// 当 bvid 发生改变时更新数据
let currentBvid = unsafeWindow.__INITIAL_STATE__.videoData.bvid;
new MutationObserver(function () {
const newBvid = unsafeWindow.__INITIAL_STATE__.videoData.bvid;
if (newBvid !== currentBvid) {
vData.initVideoStat();
updateRate();
currentBvid = newBvid;
}
}).observe(document.body, {
childList: true,
subtree: true
});
});
}
})();