// ==UserScript==
// @name ❀ 浮岚 Bilibili 链接净化器
// @name:zh-TW ❀ 浮嵐 Bilibili 連結淨化器
// @name:ja ❀ 浮嵐 Bilibili リンク浄化器
// @name:ko ❀ 부람 Bilibili 링크 정화기
// @name:en ❀ Fulan Bilibili Link Cleaner
// @description 清洁 B 站链接,移除跟踪参数。
// @description:zh-TW 清潔 B 站連結,移除追蹤參數。
// @description:ja Bilibili のリンクをクリーンアップして、追跡パラメータを削除する。
// @description:ko Bilibili 링크를 정리하고, 추적 파라미터를 제거합니다.
// @description:en Clean Bilibili links and remove tracking parameters.
// @version 1.2.0
// @author 嵐 @ranburiedbyacat
// @namespace https://bento.me/ranburiedbyacat
// @license CC-BY-NC-SA-4.0
// @match *://*.bilibili.com/*
// @compatible Safari
// @compatible Firefox
// @compatible Chrome
// @icon https://www.bilibili.com/favicon.ico
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
/**
* ───────────────────────────────────────────────
* ① 冗余参数
* ───────────────────────────────────────────────
*/
const chenruiMama = new Set([
// ────────────── 来源跟踪 ──────────────
'spm_id_from', 'from_source', 'sourceFrom', 'from_spmid', 'csource', 'vd_source', 'source', 'search_source',
'from', 'buvid', 'mid', 'timestamp',
// ────────────── 分享参数 ───────────────
'share_source', 'share_medium', 'share_plat', 'share_session_id', 'share_tag', 'share_from', 'plat_id', 'up_id',
// ────────────── 广告统计 ───────────────
'trackid', 'session_id', 'visit_id', 'unique_k', 'spmid', '-Arouter',
// ────────────── 功能标记 ───────────────
'msource', 'bsource', 'tab', 'is_story_h5', 'hotRank', 'launch_id', 'live_from', 'popular_rank',
]);
/**
* ───────────────────────────────────────────────
* ② URL 解析
* ───────────────────────────────────────────────
*/
function parseURL(str) {
try {
if (typeof str === 'string' && str.includes('.') && !/^[a-z]+:/.test(str)) {
// 以 // 开头则补充协议
str = str.startsWith("//") ? location.protocol + str : str;
}
return new URL(str, location.href);
} catch (e) {
return null;
}
}
/**
* ───────────────────────────────────────────────
* ③ URL 净化
* ───────────────────────────────────────────────
*/
function cleanUrl(urlStr) {
const url = parseURL(urlStr);
if (!url) return urlStr;
// 稍后再看接口放行(新增)
if (/^https?:\/\/api\.bilibili\.com\/x\/v2\/history\/toview\/(add|del)/.test(url.href)) {
return url.href;
}
if (!/bilibili\.com/.test(url.hostname)) return urlStr;
if (url.hostname.includes('bilibili.tv')) url.hostname = 'www.bilibili.com';
for (const key of Array.from(url.searchParams.keys())) {
if (chenruiMama.has(key)) url.searchParams.delete(key);
if (key==='p' && parseInt(url.searchParams.get('p'),10)===1) url.searchParams.delete(key);
}
if (/^\/video\/BV/i.test(url.pathname) && !url.pathname.endsWith('/')) url.pathname += '/';
return url.toString();
}
/**
* ───────────────────────────────────────────────
* ④ 地址栏即时替换
* ───────────────────────────────────────────────
*/
function replaceLocation(url) {
if (url !== location.href) {
history.replaceState(history.state, '', url);
}
}
replaceLocation(cleanUrl(location.href));
/**
* ───────────────────────────────────────────────
* ⑤ 链接点击拦截
* ───────────────────────────────────────────────
*/
window.addEventListener('click', e => {
if (e.button !== 0) return; // 左键点击才处理
const target = e.target;
// 1 检查是否点到「稍后再看」
const watchlaterBtn = target.closest('[data-action="watchlater"]');
if (watchlaterBtn) {
// 放行稍后再看
return;
}
// 2 查找 b 站链接
const a = target.closest('a[href*="bilibili.com"]');
if (!a) return;
// 3 立即净化 href(防闪烁)
const clean = cleanUrl(a.href);
if (a.href !== clean) a.href = clean;
// 4 阻止 B 站自己的跳转逻辑(视频链接或列表都适用)
e.preventDefault();
e.stopImmediatePropagation();
// 5 手动打开
if (a.target !== '_blank' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
location.assign(clean);
} else {
window.open(clean, '_blank');
}
});
/**
* ───────────────────────────────────────────────
* ⑥ window.open 拦截
* ───────────────────────────────────────────────
*/
const _open = window.open;
window.open = (url, target, features) => _open.call(window, cleanUrl(url), target || '_blank', features);
/**
* ───────────────────────────────────────────────
* ⑦ SPA 导航拦截
* ───────────────────────────────────────────────
*/
['pushState', 'replaceState'].forEach(fn => {
const orig = history[fn];
history[fn] = (...args) => {
if (typeof args[2] === 'string') {
args[2] = cleanUrl(args[2]);
}
return orig.apply(history, args);
};
});
/**
* ───────────────────────────────────────────────
* ⑧ Navigation API 拦截
* ───────────────────────────────────────────────
*/
if (window.navigation) {
window.navigation.addEventListener('navigate', e => {
const newURL = cleanUrl(e.destination.url);
if (newURL !== e.destination.url) {
e.preventDefault();
if (newURL !== location.href) {
history.replaceState(history.state, '', newURL);
}
}
});
}
/**
* ───────────────────────────────────────────────
* ⑨ 拦截 URL 变更(防止脏链接闪烁)
* ───────────────────────────────────────────────
*/
(function interceptHistory() {
const rawPush = history.pushState;
const rawReplace = history.replaceState;
function wrap(fn) {
return function (...args) {
try {
const urlArg = args[2];
if (typeof urlArg === 'string') {
const cleaned = cleanUrl(urlArg);
if (cleaned !== urlArg) {
console.log('🧼 拦截并净化历史记录 URL:', urlArg, '→', cleaned);
args[2] = cleaned;
}
}
} catch (err) {
console.warn('history 净化异常:', err);
}
return fn.apply(this, args);
};
}
history.pushState = wrap(rawPush);
history.replaceState = wrap(rawReplace);
})();
/**
* ───────────────────────────────────────────────
* ⑩ 动态节点净化
* ───────────────────────────────────────────────
*/
const observer = new MutationObserver(muts => {
for (const m of muts) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
const links = node.querySelectorAll ? node.querySelectorAll('a[href*="bilibili.com"]') : [];
for (const a of links) {
// 排除功能按钮(稍后再看)
if (a.closest('[data-action="watchlater"]')) continue;
a.href = cleanUrl(a.href);
a.removeAttribute('ping');
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
})();