列表页用评论数做缓存判定,详情页始终直接统计,评论计数范围限制在指定div
// ==UserScript==
// @name 剧集列表(页)显示评论中“神回”的次数
// @namespace https://jirehlov.com
// @version 0.4
// @description 列表页用评论数做缓存判定,详情页始终直接统计,评论计数范围限制在指定div
// @author Jirehlov
// @include /^https?://(bangumi|bgm|chii)\.(tv|in)\/subject\/\d+\/ep/
// @include /^https?://(bangumi|bgm|chii)\.(tv|in)\/ep\/\d+$/
// @license MIT
// ==/UserScript==
const STORE = "shen_hui", DBVER = 1;
$("head").append(`<style>
.shen-hui-count{display:inline-block;padding:0 6px;margin-left:6px;border-radius:6px;
color:#fff;font-weight:bold;font-size:13px;line-height:20px}
</style>`);
function color(c, max = 30) {
const r = Math.min(c / max, 1);
const s = { r: 33, g: 150, b: 243 }, e = { r: 244, g: 67, b: 54 };
return `rgb(${s.r + (e.r - s.r) * r | 0},${s.g + (e.g - s.g) * r | 0},${s.b + (e.b - s.b) * r | 0})`;
}
function openDB() {
return new Promise(r => {
const q = indexedDB.open(STORE, DBVER);
q.onupgradeneeded = e => e.target.result.createObjectStore(STORE, { keyPath: "epId" });
q.onsuccess = () => r(q.result);
q.onerror = () => r(null);
});
}
async function getCache(epId, curCount) {
const db = await openDB();
if (!db) return null;
return new Promise(res => {
const r = db.transaction(STORE, "readonly").objectStore(STORE).get(epId);
r.onsuccess = () => {
const d = r.result;
if (!d) return res(null);
if (curCount != null && d.commentCount === curCount) return res(d.shenCount);
res(null);
};
r.onerror = () => res(null);
});
}
async function setCache(epId, commentCount, shenCount) {
(await openDB())?.transaction(STORE, "readwrite").objectStore(STORE)
.put({ epId, commentCount, shenCount });
}
function countShen(doc) {
let sc = 0;
doc.querySelectorAll("#comment_list .cmt_sub_content, #comment_list .message").forEach(div => {
if (/神回/.test(div.textContent)) sc++;
});
return sc;
}
async function getCountForList(epId, curCount) {
const c = await getCache(epId, curCount);
if (c != null) return c;
const h = await fetch(`/ep/${epId}`).then(r => r.ok ? r.text() : "");
if (!h) return 0;
const doc = new DOMParser().parseFromString(h, "text/html");
const sc = countShen(doc);
await setCache(epId, curCount, sc);
return sc;
}
async function getCountForSingle(epId, doc) {
const sc = countShen(doc);
const cc = doc.querySelectorAll("#comment_list .row,#comment_list .item,#comment_list>li,#comment_list>div").length;
await setCache(epId, cc, sc);
return sc;
}
async function showList() {
for (const el of $("ul.line_list>li").get()) {
const epId = $("h6 a", el).attr("href")?.split("/").pop();
if (!epId) continue;
const t = $(el).find("small.grey").filter((_, x) => /\+(\d+)/.test(x.textContent)).text();
const cur = +((t.match(/\+(\d+)/) || [])[1] || 0);
const c = await getCountForList(epId, cur);
if (c > 0) $("h6", el).append(`<span class="shen-hui-count" style="background:${color(c)}">神回×${c}</span>`);
}
}
async function showSingle() {
const h2 = $("h2.title").get(0);
if (!h2) return;
let epId = $("a[href^='/ep/'][href$='/edit']", h2).attr("href")?.match(/\/ep\/(\d+)/)?.[1] || location.pathname.split("/").pop();
const c = await getCountForSingle(epId, document);
if (c > 0) $(h2).append(`<span class="shen-hui-count" style="background:${color(c)};vertical-align:middle;">神回×${c}</span>`);
}
if (/\/subject\/\d+\/ep/.test(location.pathname)) showList();
else if (/\/ep\/\d+$/.test(location.pathname)) showSingle();