// ==UserScript== // @name B站显示点赞率、投币率、收藏率 // @namespace http://tampermonkey.net/ // @version 1.0.9 // @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 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'; } // 工具函数:根据播放量和对应的数据算出比率并获取对应的颜色 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\/x\/web-interface\/index\/top\/feed\/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 = ` 1 `; } 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 urlToDataMap = new Map(); // 一个map,键是视频封面图片的url,值为该视频的各项数据(播放、点赞) // 劫持 fetch 请求 const originFetch = unsafeWindow.fetch; window.unsafeWindow.fetch = (url, options) => { return originFetch(url, options).then(async (response) => { if (url.startsWith('https://api.bilibili.com/x/web-interface/wbi/search/type')) { const responseClone = response.clone(); let res = await responseClone.json(); for (let tmp of res.data.result) { if (tmp.type == 'video') { processURIAndStat('https:' + tmp.pic, { view: tmp.play, like: tmp.like }, urlToDataMap); } } } else if (url.startsWith('https://api.bilibili.com/x/web-interface/wbi/search/all/v2')) { const responseClone = response.clone(); let res = await responseClone.json(); for (let tmp of res.data.result[11].data) { if (tmp.type == 'video') { processURIAndStat('https:' + tmp.pic, { view: tmp.play, like: tmp.like }, urlToDataMap); } } setTimeout(() => { const cards = document.querySelectorAll('div.bili-video-card'); for (let card of cards) { const rawPicUrl = card.querySelector('img').src; const key = rawPicUrl.slice(0, rawPicUrl.indexOf('@')); addLikeRateToCard(card, urlToDataMap, key); } }, 150); } return response; }); }; // 当 DOMContentLoaded 事件发生后,将不是动态加载的那些视频的数据加入到 map 中 document.addEventListener('DOMContentLoaded', function () { let data = unsafeWindow.__pinia.searchTypeResponse?.searchTypeResponse?.result; if (data == undefined) { data = unsafeWindow.__pinia.searchResponse.searchAllResponse.result[11].data; } for (let tmp of data) { if (tmp.type == 'video') { processURIAndStat('https:' + tmp.pic, { view: tmp.play, like: tmp.like }, urlToDataMap); } } const cards = document.querySelectorAll('div.bili-video-card'); for (let card of cards) { const rawPicUrl = card.querySelector('img').src; const key = rawPicUrl.slice(0, rawPicUrl.indexOf('@')); addLikeRateToCard(card, urlToDataMap, key); } }); // 创建MutationObserver,监控新插入的视频卡片 new MutationObserver((mutationsList) => { // 遍历每一个发生变化的mutation for (let mutation of mutationsList) { // 检查每个添加的子节点 if (mutation.type === 'childList' && mutation.addedNodes.length > 0 && mutation.target.classList.contains('search-page-wrapper')) { // 遍历每个添加的子节点 mutation.addedNodes.forEach(node => { if (node.nodeName == 'DIV' && node.classList.contains('search-page')) { const cards = node.querySelectorAll('div.bili-video-card'); for (const card of cards) { const rawPicUrl = card.querySelector('img').src; const key = rawPicUrl.slice(0, rawPicUrl.indexOf('@')); addLikeRateToCard(card, urlToDataMap, key); } } }); } } }).observe(document, { childList: true, subtree: true }); } })();