// ==UserScript==
// @name B站显示点赞率、投币率、收藏率
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description 显示b站 | bilibili | 哔哩哔哩 点赞率、投币率、收藏率
// @license MIT
// @author 魂hp
// @website https://space.bilibili.com/474989498
// @match http*://www.bilibili.com/
// @match http*://www.bilibili.com/?*
// @match http*://www.bilibili.com/video/*
// @match http*://www.bilibili.com/list/watchlater*
// @match https://www.bilibili.com/v/*
// @match http*://search.bilibili.com/all?*
// @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);
location.reload();
}
);
// 判断当前页面
let currentPage = 'unknown';
if (location.pathname === '/') {
currentPage = 'mainPage';
} else if (location.pathname.match(/\/video\/.*\//)) {
currentPage = 'videoPage';
} else if (location.pathname.match(/list\/watchlater.*/)) {
currentPage = 'videoPageWatchList';
} else if (location.pathname === '/all') {
currentPage = 'searchPage';
} else if (location.pathname.startsWith('/v/')) {
currentPage = 'region';
}
// 工具函数:根据播放量和对应的数据算出比率并获取对应的颜色
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;
}
// 工具函数,用于对uri和stat进行加工,并添加到 urlToDataMap 中
function processURIAndStat(uri, stat, urlToDataMap) {
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, urlToDataMap, key) {
const stat = urlToDataMap.get(key);
// 下面的这一行代码会导致浏览器尺寸发生变化时部分视频卡片上的点赞率消失,如果你很介意这一点可以将下面这一行代码删掉或注释掉(就是代码前面加上//),但是注释掉或者删掉会导致脚本占用更多的空间(不会太多)
urlToDataMap.delete(key);
if (stat != null) {
const span = node.querySelector('div.bili-video-card__stats--left').firstElementChild.cloneNode(false);
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);
}
}
// 根据 currentPage 执行不同的逻辑
if (currentPage === 'mainPage') {
const originFetch = unsafeWindow.fetch;
const recommendUrlRegex = /^(?:http:|https:)?\/\/api.bilibili.com\/.*?\/rcmd\?.*$/;
const 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 (recommendUrlRegex.test(url)) {
const responseClone = response.clone();
let res = await responseClone.json();
for (let tmp of res.data.item) {
processURIAndStat(tmp.uri, tmp.stat, urlToDataMap);
}
}
return response;
});
};
// 当 DOMContentLoaded 事件发生后,将不是动态加载的那些视频的数据加入到 map 中
document.addEventListener('DOMContentLoaded', function () {
for (let tmp of unsafeWindow.__pinia.feed.data.recommend.item) {
processURIAndStat(tmp.uri, tmp.stat, urlToDataMap);
}
const cards = document.querySelectorAll('div.feed-card');
for (let card of cards) {
addLikeRateToCard(card, urlToDataMap, card.querySelector('.bili-video-card__image--link')?.href);
}
// 创建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('is-rcmd') || node.classList.contains('feed-card'))) {
addLikeRateToCard(node, urlToDataMap, node.querySelector('.bili-video-card__image--link')?.href);
}
});
}
}
}).observe(document, {
childList: true,
subtree: true
});
});
} else if (currentPage === 'videoPage' || currentPage === 'videoPageWatchList') {
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);
}
}
});
let observedElement = null;
if (currentPage === 'videoPage') {
observedElement = document.querySelector('#arc_toolbar_report');
} else if (currentPage === 'videoPageWatchList') {
observedElement = document.querySelector('#playlistToolbar');
}
addElementObserver.observe(observedElement, {
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
});
});
} else if (currentPage === 'searchPage') {
// 修改样式
GM.addStyle(`
.bili-video-card__stats--left{
flex-wrap: wrap;
align-self: flex-end;
}
`);
const bvToDataMap = new Map(); // 一个map,键是视频bv号,值为该视频的各项数据(播放、点赞)
// 劫持 fetch 请求
const originFetch = unsafeWindow.fetch;
window.unsafeWindow.fetch = (url, options) => {
return originFetch(url, options).then(async (response) => {
if (/^(?:http:|https:)?\/\/api\.bilibili\.com\/x\/web-interface\/wbi\/search\/type/.test(url)) {
const responseClone = response.clone();
let res = await responseClone.json();
res['data']['result']
.filter(data => data.type === 'video')
.forEach(data => processURIAndStat(data.bvid, { view: data.play, like: data.like }, bvToDataMap));
// console.log(bvToDataMap);
} else if (/^(?:http:|https:)?\/\/api\.bilibili\.com\/x\/web-interface\/wbi\/search\/all\/v2/.test(url)) {
const responseClone = response.clone();
let res = await responseClone.json();
res['data']['result'][11]['data']
.filter(data => data.type === 'video')
.forEach(data => processURIAndStat(data.bvid, { view: data.play, like: data.like }, bvToDataMap));
// console.log(bvToDataMap);
}
return response;
});
};
// 调试代码,暂时留着
// window.addEventListener('load', function () {
// new MutationObserver((mutationsList) => {
// // 遍历每一个发生变化的mutation
// for (let mutation of mutationsList) {
// if (mutation.target.nodeName !== 'svg' && !['feed-card-body', 'bili-video-card__image--wrap'].some(str => mutation.target.classList.contains(str))) {
// console.log(mutation)
// }
// }
// }).observe(document.querySelector('div.search-content'), {
// childList: true,
// subtree: true
// });
// });
// 当 DOMContentLoaded 事件发生后,将不是动态加载的那些视频的数据加入到 map 中
document.addEventListener('DOMContentLoaded', function () {
// debugger;
let data = unsafeWindow.__pinia.searchTypeResponse?.searchTypeResponse?.result;
if (data === undefined) {
data = unsafeWindow.__pinia.searchResponse.searchAllResponse.result[11].data;
}
data.filter(data => data.type === 'video')
.forEach(data => processURIAndStat(data.bvid, { view: data.play, like: data.like }, bvToDataMap));
document.querySelectorAll('div.video.search-all-list div.bili-video-card').values()
.filter(card => card.querySelector('a') != null && /bv\w{10}/i.exec(card.querySelector('a').href) != null)
.forEach(card => addLikeRateToCard(card, bvToDataMap, /bv\w{10}/i.exec(card.querySelector('a').href)[0]));
});
// 创建MutationObserver,监控新插入的视频卡片
new MutationObserver((mutationsList) => {
mutationsList
.filter(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0)
.forEach(
mutation => mutation.addedNodes
.forEach(node => {
if (node.nodeName === 'DIV' && node.classList.contains('search-page') && mutation.target.classList.contains('search-page-wrapper')) {
node.querySelectorAll('div.bili-video-card').values()
.filter(card => card.querySelector('a') != null && /bv\w{10}/i.exec(card.querySelector('a').href) != null)
.forEach(card => addLikeRateToCard(card, bvToDataMap, /bv\w{10}/i.exec(card.querySelector('a').href)[0]));
} else if (node.nodeName === 'DIV' && ['video', 'search-all-list'].every(str => node.classList.contains(str))) {
// 从其他页返回第一页时
node.querySelectorAll('div.video.search-all-list div.bili-video-card').values()
.filter(card => card.querySelector('a') != null && /bv\w{10}/i.exec(card.querySelector('a').href) != null)
.forEach(card => addLikeRateToCard(card, bvToDataMap, /bv\w{10}/i.exec(card.querySelector('a').href)[0]));
}
})
);
}).observe(document, {
childList: true,
subtree: true
});
} else if (currentPage === 'region') {
// 基本概念1:大分区(如:影视、科技、游戏);小分区(如影视下的:影视杂谈,影视剪辑,影视整活);tag(如影视杂谈下的:电视剧解说、吐槽、悬疑、科幻、爱情)
// 基本概念2:大分区首页:里面包含高能预警(可能没有)和所有「小分区全部」;小分区全部:小分区最左侧有一个 tag 叫 “全部”
const bvToDataMap = new Map(); // 一个 map,key 是 bv 号,value 是对应的视频数据
// 拦截请求,将请求中的数据放到 map 中
const originFetch = unsafeWindow.fetch;
window.unsafeWindow.fetch = (url, options) => {
return originFetch(url, options).then(async (response) => {
// https://api.bilibili.com/x/web-interface/dynamic/tag -> 获取小分区下某 tag 的视频数据的接口
// https://api.bilibili.com/x/web-interface/dynamic/region -> 获取小分区全部视频信息
if (/^(?:http:|https:)?\/\/api\.bilibili\.com\/x\/web-interface\/dynamic\/(?:tag|region)/.test(url)) {
const responseClone = response.clone();
const res = await responseClone.json();
res.data.archives.forEach(data => processURIAndStat(data['bvid'], data['stat'], bvToDataMap));
} else if (/^(?:http:|https:)?\/\/api\.bilibili\.com\/x\/web-show\/res\/locs/.test(url)) {
// http://api.bilibili.com/x/web-show/res/locs -> 当从小分区回到大分区首页时调用的接口
const responseClone = response.clone();
const res = await responseClone.json();
Object.values(res.data)
.filter(arr => Array.isArray(arr))
.forEach(
arr => arr.forEach(
(data) => {
if (data.hasOwnProperty('archive')) {
processURIAndStat(data['archive']['bvid'], data['archive']['stat'], bvToDataMap)
}
}
)
);
}
return response;
});
};
window.addEventListener('load', function () {
if (document.querySelector('main.channel-layout') != null) { // 有些分区不支持
// 将 document 自带的数据收集到 map 中
document.querySelectorAll('script').values()
.filter(s => s.innerText.startsWith('window.__INITIAL_DATA__='))
.forEach((s) => {
const initialPageRequestAndResponse = JSON.parse(s.innerText.substring(s.innerText.indexOf('[')));
// console.log(initialPageRequestAndResponse);
if (initialPageRequestAndResponse.length == 1 && initialPageRequestAndResponse[0]['request']['url'].includes('/x/web-show/res/locs') && initialPageRequestAndResponse[0]['response'] !== undefined) {
Object.values(initialPageRequestAndResponse[0]['response'])
.filter(arr => Array.isArray(arr))
.forEach(
arr => arr.forEach(
(data) => {
if (data.hasOwnProperty('archive')) {
processURIAndStat(data['archive']['bvid'], data['archive']['stat'], bvToDataMap)
}
}
)
);
} else if (initialPageRequestAndResponse.length > 1) {
initialPageRequestAndResponse
.filter(reqAndres => reqAndres['request']['url'].includes('dynamic/region'))
.forEach(
reqAndres => reqAndres['response']['archives']
.forEach(data => processURIAndStat(data['bvid'], data['stat'], bvToDataMap))
);
}
// console.log(bvToDataMap);
});
// 将不是动态加载的那些视频的数据(点赞率)显示出来
setTimeout(() => {
document.querySelectorAll('div.bili-video-card')
.values()
.filter(card => card.querySelector('.bili-video-card__image--link') != null && /bv\w{10}/i.exec(card.querySelector('.bili-video-card__image--link').href) != null)
.forEach(card => addLikeRateToCard(card, bvToDataMap, /bv\w{10}/i.exec(card.querySelector('.bili-video-card__image--link').href)[0]));
}, 500);
// 调试代码,暂时留着
// new MutationObserver((mutationsList) => {
// // 遍历每一个发生变化的mutation
// for (let mutation of mutationsList) {
// if (mutation.target.nodeName !== 'svg' && !['feed-card-body', 'bili-video-card__image--wrap'].some(str => mutation.target.classList.contains(str))) {
// console.log(mutation)
// }
// }
// }).observe(document.querySelector('main.channel-layout'), {
// childList: true,
// subtree: true
// });
// 创建MutationObserver,监控变化的视频卡片
new MutationObserver((mutationsList) => {
mutationsList
.filter(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0)
.forEach(
mutation => {
mutation.addedNodes.forEach(
(node) => {
if (
(node.classList != null && ['video-card-body', 'video-card-list', 'dyn-full-list'].some(str => node.classList.contains(str)))
|| (node.nodeName === 'DIV' && node.classList.length === 0 && mutation.target.classList.contains('channel-layout')) // 从 tag 返回首页的刷新逻辑
) {
node.querySelectorAll('div.bili-video-card').values()
.filter(card => card.querySelector('.bili-video-card__image--link') != null && /bv\w{10}/i.exec(card.querySelector('.bili-video-card__image--link').href) != null)
.forEach(
card => addLikeRateToCard(card, bvToDataMap, /bv\w{10}/i.exec(card.querySelector('.bili-video-card__image--link').href)[0])
);
}
}
)
}
);
}).observe(document.querySelector('main.channel-layout'), {
childList: true,
subtree: true
});
}
});
}
})();