Skip to content

Commit eefe491

Browse files
deploy: 73f907d
1 parent 9ecdefb commit eefe491

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

bilibili/bilibili-search-filter.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Bilibili 视频质量筛选器
2+
3+
通过播放量和 UP 主粉丝量加权筛选 Bilibili 搜索结果中的低质量视频,并使用毛玻璃遮罩隐藏。
4+
5+
## 功能
6+
7+
- 对搜索结果中的视频卡片进行评分(播放量 + 粉丝数对数归一化)
8+
- 低于阈值的视频以毛玻璃遮罩覆盖,鼠标悬停可查看
9+
- 集成到搜索页面的筛选条件栏,可调节播放权重、粉丝权重和阈值
10+
- 粉丝数 API 结果缓存 24 小时,减少请求
11+
12+
## 安装
13+
14+
[从 GitHub Pages 安装](https://xiaolinxiaozhu.github.io/JavaScriptTools/bilibili/bilibili-search-filter.user.js)
15+
16+
## 更新日志
17+
18+
### v1.0
19+
20+
- 初始版本
21+
- 播放量 + 粉丝数加权评分算法
22+
- 毛玻璃遮罩,悬停可查看
23+
- 搜索条件栏内嵌设置面板
24+
- 粉丝数 API 缓存
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// ==UserScript==
2+
// @name Bilibili 视频质量筛选器
3+
// @namespace http://tampermonkey.net/
4+
// @version 1.0.1
5+
// @description 通过播放量和UP主粉丝量加权筛选低质量视频,并使用毛玻璃遮罩
6+
// @author XLXZ
7+
// @license MIT
8+
// @match *://search.bilibili.com/*
9+
// @connect api.bilibili.com
10+
// @grant GM_xmlhttpRequest
11+
// @grant GM_setValue
12+
// @grant GM_getValue
13+
// @grant GM_addStyle
14+
// @grant GM_registerMenuCommand
15+
// @downloadURL https://xiaolinxiaozhu.github.io/JavaScriptTools/bilibili/bilibili-search-filter.user.js
16+
// @updateURL https://xiaolinxiaozhu.github.io/JavaScriptTools/bilibili/bilibili-search-filter.user.js
17+
// ==/UserScript==
18+
19+
"use strict";
20+
(() => {
21+
var __defProp = Object.defineProperty;
22+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
23+
24+
// src/bilibili-search-filter/style.css
25+
var style_default = '/* \u6BDB\u73BB\u7483\u906E\u7F69\u57FA\u7840\u6837\u5F0F */\n.qf-low-quality {\n position: relative !important;\n}\n.qf-low-quality::before {\n content: "\u4F4E\u8D28\u91CF\u89C6\u9891\u5DF2\u9690\u85CF";\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background: rgba(255, 255, 255, 0.3);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n z-index: 100;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #666;\n font-size: 14px;\n font-weight: bold;\n pointer-events: auto;\n transition: opacity 0.3s;\n border-radius: 8px;\n}\n.qf-low-quality:hover::before {\n opacity: 0;\n pointer-events: none;\n}\n\n/* \u96C6\u6210\u5230\u641C\u7D22\u6761\u4EF6\u884C\u7684\u6837\u5F0F */\n.qf-search-condition-row {\n display: flex;\n align-items: center;\n padding: 8px 0;\n border-bottom: 1px solid #e7e7e7;\n}\n.qf-search-condition-row:last-child {\n border-bottom: none;\n}\n.qf-condition-label {\n display: inline-block;\n width: 80px;\n font-size: 13px;\n color: #222;\n font-weight: 500;\n margin-right: 10px;\n}\n.qf-condition-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n}\n.qf-condition-input {\n flex: 1;\n height: 28px;\n padding: 0 8px;\n border: 1px solid #e7e7e7;\n border-radius: 4px;\n font-size: 12px;\n outline: none;\n transition: border-color 0.2s;\n}\n.qf-condition-input:hover {\n border-color: #fb7299;\n}\n.qf-condition-input:focus {\n border-color: #fb7299;\n box-shadow: 0 0 0 2px rgba(251, 114, 153, 0.1);\n}\n.qf-condition-value {\n display: inline-block;\n min-width: 30px;\n text-align: center;\n font-size: 12px;\n color: #666;\n}\n.qf-save-btn {\n background: #fb7299;\n color: #fff;\n border: none;\n padding: 6px 12px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 12px;\n transition: background-color 0.2s;\n}\n.qf-save-btn:hover {\n background: #f57fb9;\n}\n';
26+
27+
// src/bilibili-search-filter/index.ts
28+
GM_addStyle(style_default);
29+
var CONFIG = {
30+
playWeight: GM_getValue("playWeight", 50),
31+
fansWeight: GM_getValue("fansWeight", 50),
32+
threshold: GM_getValue("threshold", 30),
33+
blurIntensity: GM_getValue("blurIntensity", 10),
34+
cacheTTL: 24 * 60 * 60 * 1e3
35+
// 24小时缓存
36+
};
37+
var FANS_CACHE = JSON.parse(
38+
GM_getValue("fansCache", "{}")
39+
);
40+
function parseBiliNum(str) {
41+
if (!str) return 0;
42+
str = str.trim();
43+
if (str.includes("\u4E07")) return parseFloat(str) * 1e4;
44+
if (str.includes("\u4EBF")) return parseFloat(str) * 1e8;
45+
return parseInt(str.replace(/,/g, "")) || 0;
46+
}
47+
__name(parseBiliNum, "parseBiliNum");
48+
async function getFansCount(mid) {
49+
const now = Date.now();
50+
if (FANS_CACHE[mid] && now - FANS_CACHE[mid].ts < CONFIG.cacheTTL) {
51+
return FANS_CACHE[mid].count;
52+
}
53+
return new Promise((resolve) => {
54+
GM_xmlhttpRequest({
55+
method: "GET",
56+
url: `https://api.bilibili.com/x/web-interface/card?mid=${mid}`,
57+
onload(res) {
58+
try {
59+
const data = JSON.parse(res.responseText);
60+
const fans = data.data.card.fans;
61+
FANS_CACHE[mid] = { count: fans, ts: now };
62+
GM_setValue("fansCache", JSON.stringify(FANS_CACHE));
63+
resolve(fans);
64+
} catch {
65+
resolve(0);
66+
}
67+
},
68+
onerror: /* @__PURE__ */ __name(() => resolve(0), "onerror")
69+
});
70+
});
71+
}
72+
__name(getFansCount, "getFansCount");
73+
function calculateScore(plays, fans) {
74+
const playScore = Math.min(100, Math.log10(plays + 1) / 6 * 100);
75+
const fansScore = Math.min(100, Math.log10(fans + 1) / 6 * 100);
76+
return playScore * (CONFIG.playWeight / 100) + fansScore * (CONFIG.fansWeight / 100);
77+
}
78+
__name(calculateScore, "calculateScore");
79+
async function processCard(card) {
80+
if (card.dataset.qfProcessed) return;
81+
card.dataset.qfProcessed = "true";
82+
const statsText = card.querySelector(".bili-video-card__stats--item span")?.textContent;
83+
const plays = parseBiliNum(statsText);
84+
const midLink = card.querySelector('a[href*="space.bilibili.com/"]');
85+
const midMatch = midLink?.href.match(/space\.bilibili\.com\/(\d+)/);
86+
const mid = midMatch ? midMatch[1] : null;
87+
if (!mid) return;
88+
const fans = await getFansCount(mid);
89+
const score = calculateScore(plays, fans);
90+
if (score < CONFIG.threshold) {
91+
card.classList.add("qf-low-quality");
92+
}
93+
}
94+
__name(processCard, "processCard");
95+
var cardObserver = new MutationObserver((mutations) => {
96+
for (const mutation of mutations) {
97+
mutation.addedNodes.forEach((node) => {
98+
if (node.nodeType !== 1) return;
99+
const el = node;
100+
const cards = el.classList.contains("bili-video-card") ? [el] : Array.from(el.querySelectorAll(".bili-video-card"));
101+
cards.forEach(processCard);
102+
});
103+
}
104+
});
105+
function createSettings() {
106+
const container = document.createElement("div");
107+
container.className = "qf-search-condition-row";
108+
container.id = "qf-settings-row";
109+
container.innerHTML = `
110+
<span class="qf-condition-label">\u8D28\u91CF\u8FC7\u6EE4</span>
111+
<div class="qf-condition-controls">
112+
<div style="display: flex; align-items: center; gap: 4px; flex: 1;">
113+
<label style="font-size: 12px; color: #666;">\u64AD\u653E\u6743\u91CD:</label>
114+
<input type="range" id="qf-in-pw" class="qf-condition-input" min="0" max="100" value="${CONFIG.playWeight}" style="flex: 0.5;">
115+
<span id="qf-val-pw" class="qf-condition-value">${CONFIG.playWeight}%</span>
116+
</div>
117+
<div style="display: flex; align-items: center; gap: 4px; flex: 1;">
118+
<label style="font-size: 12px; color: #666;">\u7C89\u4E1D\u6743\u91CD:</label>
119+
<input type="range" id="qf-in-fw" class="qf-condition-input" min="0" max="100" value="${CONFIG.fansWeight}" style="flex: 0.5;">
120+
<span id="qf-val-fw" class="qf-condition-value">${CONFIG.fansWeight}%</span>
121+
</div>
122+
<div style="display: flex; align-items: center; gap: 4px; flex: 1;">
123+
<label style="font-size: 12px; color: #666;">\u9608\u503C:</label>
124+
<input type="range" id="qf-in-th" class="qf-condition-input" min="0" max="100" value="${CONFIG.threshold}" style="flex: 0.5;">
125+
<span id="qf-val-th" class="qf-condition-value">${CONFIG.threshold}</span>
126+
</div>
127+
<button class="qf-save-btn">\u4FDD\u5B58</button>
128+
</div>
129+
`;
130+
container.querySelector("#qf-in-pw").oninput = (e) => {
131+
document.getElementById("qf-val-pw").textContent = e.target.value + "%";
132+
};
133+
container.querySelector("#qf-in-fw").oninput = (e) => {
134+
document.getElementById("qf-val-fw").textContent = e.target.value + "%";
135+
};
136+
container.querySelector("#qf-in-th").oninput = (e) => {
137+
document.getElementById("qf-val-th").textContent = e.target.value;
138+
};
139+
container.querySelector(".qf-save-btn").onclick = () => {
140+
GM_setValue("playWeight", parseInt(container.querySelector("#qf-in-pw").value));
141+
GM_setValue("fansWeight", parseInt(container.querySelector("#qf-in-fw").value));
142+
GM_setValue("threshold", parseInt(container.querySelector("#qf-in-th").value));
143+
location.reload();
144+
};
145+
return container;
146+
}
147+
__name(createSettings, "createSettings");
148+
function insertSettingsIntoMoreConditions() {
149+
const moreConditionsContainer = document.querySelector(".more-conditions");
150+
if (!moreConditionsContainer) return;
151+
if (document.getElementById("qf-settings-row")) return;
152+
const settingsRow = createSettings();
153+
moreConditionsContainer.appendChild(settingsRow);
154+
}
155+
__name(insertSettingsIntoMoreConditions, "insertSettingsIntoMoreConditions");
156+
function observeMoreConditions() {
157+
const settingsObserver = new MutationObserver(() => {
158+
insertSettingsIntoMoreConditions();
159+
});
160+
settingsObserver.observe(document.body, {
161+
childList: true,
162+
subtree: true
163+
});
164+
insertSettingsIntoMoreConditions();
165+
}
166+
__name(observeMoreConditions, "observeMoreConditions");
167+
function init() {
168+
observeMoreConditions();
169+
document.querySelectorAll(".bili-video-card").forEach(processCard);
170+
cardObserver.observe(document.body, { childList: true, subtree: true });
171+
}
172+
__name(init, "init");
173+
if (document.readyState === "complete") init();
174+
else window.addEventListener("load", init);
175+
})();

sync-manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@
4141
"greasyforkId": 497929,
4242
"greasyforkUrl": "https://greasyfork.org/scripts/497929"
4343
},
44+
{
45+
"name": "bilibili-search-filter",
46+
"mode": "userscript",
47+
"category": "bilibili",
48+
"distPath": "bilibili/bilibili-search-filter.user.js",
49+
"pagesUrl": "https://xiaolinxiaozhu.github.io/JavaScriptTools/bilibili/bilibili-search-filter.user.js",
50+
"version": "1.0.1"
51+
},
4452
{
4553
"name": "evaluation-auto",
4654
"mode": "userscript",

0 commit comments

Comments
 (0)