// ==UserScript==
// @name 【移动端】bilibili优化
// @namespace https://github.com/WhiteSevs/TamperMonkeyScript
// @version 2024.10.2
// @author WhiteSevs
// @description 移动端专用,免登录(但登录后可以看更多评论)、阻止跳转App、App端推荐视频流、解锁视频画质(番剧解锁需配合其它插件)、美化显示、去广告等
// @license GPL-3.0-only
// @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png
// @supportURL https://github.com/WhiteSevs/TamperMonkeyScript/issues
// @match *://m.bilibili.com/*
// @match *://live.bilibili.com/*
// @match *://www.bilibili.com/read/*
// @require https://update.greasyfork.icu/scripts/494167/1413255/CoverUMD.js
// @require https://update.greasyfork.icu/scripts/497907/1413262/QRCodeJS.js
// @require https://fastly.jsdelivr.net/npm/qmsg@1.2.3/dist/index.umd.js
// @require https://fastly.jsdelivr.net/npm/@whitesev/utils@2.3.3/dist/index.umd.js
// @require https://fastly.jsdelivr.net/npm/@whitesev/domutils@1.3.3/dist/index.umd.js
// @require https://fastly.jsdelivr.net/npm/@whitesev/pops@1.7.2/dist/index.umd.js
// @require https://fastly.jsdelivr.net/npm/md5@2.3.0/dist/md5.min.js
// @require https://fastly.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.js
// @require https://fastly.jsdelivr.net/npm/artplayer-plugin-danmuku@5.1.4/dist/artplayer-plugin-danmuku.js
// @require https://fastly.jsdelivr.net/npm/artplayer@5.1.7/dist/artplayer.js
// @connect *
// @connect m.bilibili.com
// @connect www.bilibili.com
// @connect api.bilibili.com
// @connect app.bilibili.com
// @connect passport.bilibili.com
// @grant GM_addStyle
// @grant GM_deleteValue
// @grant GM_getValue
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_unregisterMenuCommand
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @downloadURL none
// ==/UserScript==
(a=>{if(typeof GM_addStyle=="function"){GM_addStyle(a);return}function n(e){let p=document.createElement("style");return p.innerHTML=e,document.head?document.head.appendChild(p):document.documentElement.appendChild(p),p}n(a)})(' @charset "UTF-8";.m-video2-awaken-btn,.openapp-dialog,.m-head .launch-app-btn.m-nav-openapp,.m-head .launch-app-btn.home-float-openapp,.m-home .launch-app-btn.home-float-openapp,.m-space .launch-app-btn.m-space-float-openapp,.m-space .launch-app-btn.m-nav-openapp{display:none!important}#app .video .launch-app-btn.m-video-main-launchapp:has([class^=m-video2-awaken]),#app .video .launch-app-btn.m-nav-openapp,#app .video .mplayer-widescreen-callapp,#app .video .launch-app-btn.m-float-openapp,#app .video .m-video-season-panel .launch-app-btn .open-app{display:none!important}#app.LIVE .open-app-btn.bili-btn-warp,#app .m-dynamic .launch-app-btn.m-nav-openapp,#app .m-dynamic .dynamic-float-openapp.dynamic-float-btn,#app .m-opus .float-openapp.opus-float-btn,#app .m-opus .v-switcher .launch-app-btn.list-more,#app .m-opus .opus-nav .launch-app-btn.m-nav-openapp,#app .topic-detail .launch-app-btn.m-nav-openapp,#app .topic-detail .launch-app-btn.m-topic-float-openapp{display:none!important}#app.main-container bili-open-app.btn-download{display:none!important}#app .read-app-main bili-open-app{display:none!important}html{--bili-color: #fb7299;--bili-color-rgb: 251, 114, 153} ');
(function (Qmsg, Utils, DOMUtils, pops, md5, Artplayer, artplayerPluginDanmuku, flvjs) {
'use strict';
var _a;
var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_info = /* @__PURE__ */ (() => typeof GM_info != "undefined" ? GM_info : void 0)();
var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
var _GM_unregisterMenuCommand = /* @__PURE__ */ (() => typeof GM_unregisterMenuCommand != "undefined" ? GM_unregisterMenuCommand : void 0)();
var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
var _monkeyWindow = /* @__PURE__ */ (() => window)();
const HttpxCookieManager = {
$data: {
/** 是否启用 */
get enable() {
return PopsPanel.getValue("httpx-use-cookie-enable");
},
/** 是否使用document.cookie */
get useDocumentCookie() {
return PopsPanel.getValue("httpx-use-document-cookie");
},
cookieRule: [
{
key: "httpx-cookie-bilibili.com",
hostname: /bilibili.com/g
}
]
},
/**
* 补充cookie末尾分号
*/
fixCookieSplit(str) {
if (utils.isNotNull(str) && !str.trim().endsWith(";")) {
str += ";";
}
return str;
},
/**
* 合并两个cookie
*/
concatCookie(targetCookie, newCookie) {
if (utils.isNull(targetCookie)) {
return newCookie;
}
targetCookie = targetCookie.trim();
newCookie = newCookie.trim();
targetCookie = this.fixCookieSplit(targetCookie);
if (newCookie.startsWith(";")) {
newCookie = newCookie.substring(1);
}
return targetCookie.concat(newCookie);
},
/**
* 处理cookie
* @param details
* @returns
*/
handle(details) {
if (details.fetch) {
return;
}
if (!this.$data.enable) {
return;
}
let ownCookie = "";
let url = details.url;
if (url.startsWith("//")) {
url = window.location.protocol + url;
}
let urlObj = new URL(url);
if (this.$data.useDocumentCookie && urlObj.hostname.endsWith(
window.location.hostname.split(".").slice(-2).join(".")
)) {
ownCookie = this.concatCookie(ownCookie, document.cookie.trim());
}
for (let index = 0; index < this.$data.cookieRule.length; index++) {
let rule = this.$data.cookieRule[index];
if (urlObj.hostname.match(rule.hostname)) {
let cookie = PopsPanel.getValue(rule.key);
if (utils.isNull(cookie)) {
break;
}
ownCookie = this.concatCookie(ownCookie, cookie);
}
}
if (utils.isNotNull(ownCookie)) {
if (details.headers && details.headers["Cookie"]) {
details.headers.Cookie = this.concatCookie(
details.headers.Cookie,
ownCookie
);
} else {
details.headers["Cookie"] = ownCookie;
}
log.info(["Httpx => 设置cookie:", details]);
}
if (details.headers && details.headers.Cookie != null && utils.isNull(details.headers.Cookie)) {
delete details.headers.Cookie;
}
}
};
const _SCRIPT_NAME_ = "【移动端】bilibili优化";
const utils = Utils.noConflict();
const domutils = DOMUtils.noConflict();
const __pops = pops;
const QRCodeJS = _monkeyWindow.QRCode || _unsafeWindow.QRCode;
const log = new utils.Log(
_GM_info,
_unsafeWindow.console || _monkeyWindow.console
);
const SCRIPT_NAME = ((_a = _GM_info == null ? void 0 : _GM_info.script) == null ? void 0 : _a.name) || _SCRIPT_NAME_;
const GMCookie = new utils.GM_Cookie();
const DEBUG = false;
log.config({
debug: DEBUG,
logMaxCount: 1e3,
autoClearConsole: true,
tag: true
});
Qmsg.config(
Object.defineProperties(
{
html: true,
autoClose: true,
showClose: false
},
{
position: {
get() {
return PopsPanel.getValue("qmsg-config-position", "bottom");
}
},
maxNums: {
get() {
return PopsPanel.getValue("qmsg-config-maxnums", 5);
}
},
showReverse: {
get() {
return PopsPanel.getValue("qmsg-config-showreverse", true);
}
},
zIndex: {
get() {
let maxZIndex = Utils.getMaxZIndex();
let popsMaxZIndex = pops.config.InstanceUtils.getPopsMaxZIndex(maxZIndex).zIndex;
return Utils.getMaxValue(maxZIndex, popsMaxZIndex) + 100;
}
}
}
)
);
const GM_Menu = new utils.GM_Menu({
GM_getValue: _GM_getValue,
GM_setValue: _GM_setValue,
GM_registerMenuCommand: _GM_registerMenuCommand,
GM_unregisterMenuCommand: _GM_unregisterMenuCommand
});
const httpx = new utils.Httpx(_GM_xmlhttpRequest);
httpx.interceptors.request.use((data2) => {
HttpxCookieManager.handle(data2);
return data2;
});
httpx.interceptors.response.use(void 0, (data2) => {
log.error(["拦截器-请求错误", data2]);
if (data2.type === "onabort") {
Qmsg.warning("请求取消");
} else if (data2.type === "onerror") {
Qmsg.error("请求异常");
} else if (data2.type === "ontimeout") {
Qmsg.error("请求超时");
} else {
Qmsg.error("其它错误");
}
return data2;
});
httpx.config({
logDetails: DEBUG
});
const OriginPrototype = {
Object: {
defineProperty: _unsafeWindow.Object.defineProperty
},
Function: {
apply: _unsafeWindow.Function.prototype.apply,
call: _unsafeWindow.Function.prototype.call
},
Element: {
appendChild: _unsafeWindow.Element.prototype.appendChild
},
setTimeout: _unsafeWindow.setTimeout
};
const addStyle = utils.addStyle.bind(utils);
const KEY = "GM_Panel";
const ATTRIBUTE_INIT = "data-init";
const ATTRIBUTE_KEY = "data-key";
const ATTRIBUTE_DEFAULT_VALUE = "data-default-value";
const ATTRIBUTE_INIT_MORE_VALUE = "data-init-more-value";
const UISwitch = function(text, key, defaultValue, clickCallBack, description) {
let result = {
text,
type: "switch",
description,
attributes: {},
getValue() {
return Boolean(PopsPanel.getValue(key, defaultValue));
},
callback(event, value) {
log.success(`${value ? "开启" : "关闭"} ${text}`);
if (typeof clickCallBack === "function") {
if (clickCallBack(event, value)) {
return;
}
}
PopsPanel.setValue(key, Boolean(value));
},
afterAddToUListCallBack: void 0
};
if (result.attributes) {
result.attributes[ATTRIBUTE_KEY] = key;
result.attributes[ATTRIBUTE_DEFAULT_VALUE] = Boolean(defaultValue);
}
return result;
};
const UITextArea = function(text, key, defaultValue, description, changeCallBack, placeholder = "", disabled) {
let result = {
text,
type: "textarea",
attributes: {},
description,
placeholder,
disabled,
getValue() {
let localValue = PopsPanel.getValue(key, defaultValue);
return localValue;
},
callback(event, value) {
PopsPanel.setValue(key, value);
}
};
if (result.attributes) {
result.attributes[ATTRIBUTE_KEY] = key;
result.attributes[ATTRIBUTE_DEFAULT_VALUE] = defaultValue;
}
return result;
};
const UISelect = function(text, key, defaultValue, data2, callback, description) {
let selectData = [];
if (typeof data2 === "function") {
selectData = data2();
} else {
selectData = data2;
}
let result = {
text,
type: "select",
description,
attributes: {},
getValue() {
return PopsPanel.getValue(key, defaultValue);
},
callback(event, isSelectedValue, isSelectedText) {
PopsPanel.setValue(key, isSelectedValue);
if (typeof callback === "function") {
callback(event, isSelectedValue, isSelectedText);
}
},
data: selectData
};
if (result.attributes) {
result.attributes[ATTRIBUTE_KEY] = key;
result.attributes[ATTRIBUTE_DEFAULT_VALUE] = defaultValue;
}
return result;
};
const UISlider = function(text, key, defaultValue, min, max, step, changeCallBack, getToolTipContent, description) {
let result = {
text,
type: "slider",
description,
attributes: {},
getValue() {
return PopsPanel.getValue(key, defaultValue);
},
getToolTipContent(value) {
if (typeof getToolTipContent === "function") {
return getToolTipContent(value);
} else {
return `${value}`;
}
},
callback(event, value) {
if (typeof changeCallBack === "function") {
if (changeCallBack(event, value)) {
return;
}
}
PopsPanel.setValue(key, value);
},
min,
max,
step
};
if (result.attributes) {
result.attributes[ATTRIBUTE_KEY] = key;
result.attributes[ATTRIBUTE_DEFAULT_VALUE] = defaultValue;
}
return result;
};
const UIOwn = function(getLiElementCallBack, initConfig, props, afterAddToUListCallBack) {
let result = {
attributes: {},
type: "own",
props,
getLiElementCallBack,
afterAddToUListCallBack
};
if (result.attributes) {
result.attributes[ATTRIBUTE_INIT] = () => {
if (initConfig) {
Object.keys(initConfig).forEach((key) => {
let defaultValue = initConfig[key];
if (PopsPanel.$data.data.has(key)) {
log.warn("请检查该key(已存在): " + key);
}
PopsPanel.$data.data.set(key, defaultValue);
});
}
return false;
};
}
return result;
};
const BilibiliPlayerToast = {
$flag: {
isInitCSS: false
},
$data: {
/** 默认的toast的className */
originToast: "mplayer-toast",
/** 让Toast显示的className */
showClassName: "mplayer-show",
/** 自定义的toast的class,避免和页面原有的toast冲突 */
prefix: "mplayer-toast-gm"
},
$el: {
get $mplayer() {
return document.querySelector(".mplayer");
}
},
/**
* 弹出吐司
* @param config
*/
toast(config) {
if (typeof config === "string") {
config = {
text: config
};
}
this.initCSS();
let $parent = config.parent ?? this.$el.$mplayer;
if (!$parent) {
throw new TypeError("toast parent is null");
}
this.mutationMPlayerOriginToast($parent);
let $toast = domutils.createElement("div", {
"data-from": "gm"
});
domutils.addClass($toast, this.$data.prefix);
domutils.addClass($toast, this.$data.showClassName);
if (config.showCloseBtn) {
let $closeBtn = domutils.createElement("div", {
className: this.$data.prefix + "-close",
innerHTML: (
/*html*/
`
`
)
});
$toast.appendChild($closeBtn);
domutils.on($closeBtn, "click", (event) => {
utils.preventEvent(event);
this.closeToast($toast);
});
}
let $text = domutils.createElement("span", {
className: this.$data.prefix + "-text",
innerText: config.text
});
$toast.appendChild($text);
if (typeof config.timeText === "string" && config.timeText.trim() != "") {
let $time = domutils.createElement("span", {
className: this.$data.prefix + "-time",
innerText: config.timeText
});
$toast.appendChild($time);
}
if (typeof config.jumpText === "string" && config.jumpText.trim() != "") {
let $jump = domutils.createElement("span", {
className: this.$data.prefix + "-jump",
innerText: config.jumpText
});
$toast.appendChild($jump);
domutils.on($jump, "click", (event) => {
if (typeof config.jumpClickCallback === "function") {
utils.preventEvent(event);
config.jumpClickCallback(event);
}
});
}
this.setTransitionendEvent($toast);
let timeout = typeof config.timeout === "number" && !isNaN(config.timeout) ? config.timeout : 3500;
Array.from(
document.querySelectorAll(`.mplayer-toast`)
).forEach(($mplayerOriginToast) => {
var _a2;
if ($mplayerOriginToast.hasAttribute("data-is-set-transitionend")) {
return;
}
$mplayerOriginToast.setAttribute("data-is-set-transitionend", "true");
if ((_a2 = $mplayerOriginToast.textContent) == null ? void 0 : _a2.includes("记忆你上次看到")) {
setTimeout(() => {
let $close = $mplayerOriginToast.querySelector(
".mplayer-toast-close"
);
if ($close) {
$close.click();
} else {
$mplayerOriginToast.remove();
}
}, 3e3);
}
this.setTransitionendEvent($mplayerOriginToast);
});
$parent.appendChild($toast);
setTimeout(() => {
this.closeToast($toast);
}, timeout);
return {
$toast,
close: () => {
this.closeToast($toast);
}
};
},
/**
* 初始化css
*/
initCSS() {
if (this.$flag.isInitCSS) {
return;
}
this.$flag.isInitCSS = true;
addStyle(
/*css*/
`
.${this.$data.prefix}.mplayer-show {
opacity: 1;
visibility: visible;
z-index: 40;
}
.mplayer-toast, .${this.$data.prefix} {
-webkit-transition-property: opacity, bottom;
transition-property: opacity, bottom;
}
.${this.$data.prefix} {
background-color: rgba(0, 0, 0, .8);
border-radius: 4px;
bottom: 48px;
color: #fafafa;
font-size: 12px;
left: 8px;
line-height: 24px;
opacity: 0;
overflow: hidden;
padding: 6px 8px;
position: absolute;
text-align: center;
-webkit-transition: opacity .3s;
transition: opacity .3s;
visibility: hidden;
z-index: 4;
}
.${this.$data.prefix}-close {
fill: #fff;
float: left;
height: 14px;
margin-right: 4px;
position: relative;
top: 1px;
width: 26px;
}
.${this.$data.prefix}-jump {
color: #f25d8e;
margin: 0 8px 0 16px;
text-decoration: none;
}
`
);
},
/**
* 观察mplayer
* 用于关闭页面自己的toast
* 动态更新自己的toast位置
*/
mutationMPlayerOriginToast($parent) {
let $mplayer = this.$el.$mplayer;
if (!$mplayer) {
return;
}
if ($mplayer.hasAttribute("data-mutation")) {
return;
}
log.success(`添加观察器,动态更新toast的位置`);
$mplayer.setAttribute("data-mutation", "gm");
utils.mutationObserver($mplayer, {
config: {
subtree: true,
childList: true
},
immediate: true,
callback: () => {
this.updatePageToastBottom();
}
});
},
/**
* 更新页面上的bottom的位置
*/
updatePageToastBottom() {
let pageToastList = Array.from(
document.querySelectorAll(`.${this.$data.prefix}`)
).concat(
Array.from(
document.querySelectorAll(
".".concat(this.$data.originToast).concat(".").concat(this.$data.showClassName)
)
)
);
if (pageToastList.length) {
let count = pageToastList.length - 1;
const toastHeight = 46;
pageToastList.forEach(($pageToast, index) => {
let bottom = toastHeight + toastHeight * (count - index);
$pageToast.setAttribute("data-transition", "move");
$pageToast.style.bottom = bottom + "px";
});
}
},
/**
* 关闭吐司
*/
closeToast($ele) {
$ele.classList.remove(this.$data.showClassName);
},
/**
* 获取事件名称列表
* @private
*/
getTransitionendEventNameList() {
return [
"webkitTransitionEnd",
"mozTransitionEnd",
"MSTransitionEnd",
"otransitionend",
"transitionend"
];
},
/**
* 监听过渡结束
* @private
*/
setTransitionendEvent($toast) {
let that = this;
let animationEndNameList = this.getTransitionendEventNameList();
domutils.on(
$toast,
animationEndNameList,
function(event) {
let dataTransition = $toast.getAttribute("data-transition");
if (!$toast.classList.contains(that.$data.showClassName)) {
$toast.remove();
return;
}
if (dataTransition === "move") {
$toast.removeAttribute("data-transition");
return;
}
},
{
capture: true
}
);
}
};
const BilibiliRouter = {
/**
* 视频页面
* + /video/
*/
isVideo() {
return window.location.pathname.startsWith("/video/");
},
/**
* 番剧
* + /banggumi/
*/
isBangumi() {
return window.location.pathname.startsWith("/bangumi/");
},
/**
* 搜索
* + /search
*/
isSearch() {
return window.location.pathname.startsWith("/search");
},
/**
* 搜索结果页面
*
* + /search?keyword=xxx
*/
isSearchResult() {
let urlSearchParams = new URLSearchParams(window.location.search);
return this.isSearch() && urlSearchParams.has("keyword");
},
/**
* 直播
* + live.bilibili.com
*/
isLive() {
return window.location.hostname === "live.bilibili.com";
},
/**
* 专栏稿件
* + /opus
*/
isOpus() {
return window.location.pathname.startsWith("/opus");
},
/**
* 话题
* + /topic-detail
*/
isTopicDetail() {
return window.location.pathname.startsWith("/topic-detail");
},
/**
* 动态
* + /dynamic
*/
isDynamic() {
return window.location.pathname.startsWith("/dynamic");
},
/**
* 首页
* + /
* + /channel
*/
isHead() {
return window.location.pathname === "/" || window.location.pathname.startsWith("/channel");
},
/**
* 个人空间
* + /space
*/
isSpace() {
return window.location.pathname.startsWith("/space");
}
};
const BilibiliPCRouter = {
/**
* 桌面端
*/
isPC() {
return window.location.hostname === "www.bilibili.com";
},
/**
* 应该是动态?
*/
isReadMobile() {
return this.isPC() && window.location.pathname.startsWith("/read/mobile");
}
};
let _ajaxHooker_ = null;
const XhrHook = {
get ajaxHooker() {
if (_ajaxHooker_ == null) {
log.info("启用ajaxHooker拦截网络");
_ajaxHooker_ = utils.ajaxHooker();
}
return _ajaxHooker_;
}
};
const BilibiliVideoPlayUrlQN = {
/**
* 仅mp4方式支持
* + 6
*/
"240P 极速": 6,
/**
* 仅mp4方式支持
* + 16
*/
"360P 流畅": 16,
/**
* 仅mp4方式支持
* + 32
*/
"480P 清晰": 32,
/**
* web端默认值
*
* B站前端需要登录才能选择,但是直接发送请求可以不登录就拿到720P的取流地址
*
* 无720P时则为720P60
* + 64
*/
"720P 高清": 64,
/**
* 需要认证登录账号
* + 74
*/
"720P60 高帧率": 74,
/**
* TV端与APP端默认值
*
* 需要认证登录账号
* + 80
*/
"1080P 高清": 80,
/**
* 大多情况需求认证大会员账号
* + 112
*/
"1080P+ 高码率": 112,
/**
* 大多情况需求认证大会员账号
* + 116
*/
"1080P60 高帧率": 116,
/**
* 需要fnval&128=128且fourk=1
*
* 大多情况需求认证大会员账号
* + 120
*/
"4K 超清": 120,
/**
* 仅支持dash方式
*
* 需要fnval&64=64
* + 125
*/
"HDR 真彩色": 125,
/**
* 仅支持dash方式
*
* 需要fnval&512=512
*
* 大多情况需求认证大会员账号
* + 126
*/
杜比视界: 126,
/**
* 仅支持dash方式
*
* 需要fnval&1024=1024
*
* 大多情况需求认证大会员账号
* + 127
*/
"8K 超高清": 127
};
const BilibiliVideoPlayUrlQN_Value = {};
Object.keys(BilibiliVideoPlayUrlQN).forEach((text) => {
Reflect.set(
BilibiliVideoPlayUrlQN_Value,
BilibiliVideoPlayUrlQN[text],
text
);
});
const BilibiliNetworkHook = {
$flag: {
is_hook_video_playurl: false,
is_hook_bangumi_html5: false
},
init() {
if (BilibiliRouter.isVideo()) {
PopsPanel.execMenuOnce("bili-video-xhr-unlockQuality", () => {
this.hook_video_playurl();
});
} else if (BilibiliRouter.isBangumi()) ;
},
/**
* 视频播放地址获取
*
* + //api.bilibili.com/x/player/wbi/playurl
* + //api.bilibili.com/x/player/playurl
*
*/
hook_video_playurl() {
if (this.$flag.is_hook_video_playurl) {
return;
}
this.$flag.is_hook_video_playurl = true;
XhrHook.ajaxHooker.hook((request) => {
if (request.url.includes("//api.bilibili.com/x/player/wbi/playurl")) {
if (request.url.startsWith("//")) {
request.url = window.location.protocol + request.url;
}
let playUrl = new URL(request.url);
playUrl.searchParams.set("platform", "html5");
playUrl.searchParams.set(
"qn",
BilibiliVideoPlayUrlQN["1080P60 高帧率"].toString()
);
playUrl.searchParams.set("high_quality", "1");
playUrl.searchParams.set("fnver", "0");
playUrl.searchParams.set("fourk", "1");
if (playUrl.searchParams.has("__t")) {
playUrl.searchParams.delete("__t");
return;
}
request.url = playUrl.toString();
request.response = (res) => {
var _a2, _b;
let data2 = utils.toJSON(res.responseText);
let unlockQuality = (_a2 = data2 == null ? void 0 : data2["data"]) == null ? void 0 : _a2["quality"];
let support_formats = (_b = data2 == null ? void 0 : data2["data"]) == null ? void 0 : _b["support_formats"];
log.info("当前解锁的quality值:" + unlockQuality);
if (unlockQuality) {
BilibiliPlayer.initVideoQualityInfo(unlockQuality);
}
if (unlockQuality && support_formats) {
let findValue = support_formats.find((item) => {
return item["quality"] == unlockQuality;
});
if (findValue) {
let qualityText = findValue["new_description"] || findValue["display_desc"];
log.info("成功解锁画质 " + qualityText);
BilibiliPlayerToast.toast(`成功解锁画质 ${qualityText}`);
}
}
};
}
});
},
/**
* 番剧播放地址获取
*
* + //api.bilibili.com/pgc/player/web/playurl/html5
*
*/
hook_bangumi_html5() {
if (this.$flag.is_hook_bangumi_html5) {
return;
}
this.$flag.is_hook_bangumi_html5 = true;
XhrHook.ajaxHooker.hook((request) => {
if (request.url.includes("//api.bilibili.com/pgc/player/web/playurl/html5")) {
if (request.url.startsWith("//")) {
request.url = window.location.protocol + request.url;
}
let playUrl = new URL(request.url);
playUrl.pathname = "/pgc/player/web/playurl";
playUrl.searchParams.delete("bsource");
playUrl.searchParams.set(
"qn",
BilibiliVideoPlayUrlQN["1080P60 高帧率"].toString()
);
playUrl.searchParams.set("fnval", "1");
playUrl.searchParams.set("fnver", "0");
playUrl.searchParams.set("fourk", "1");
playUrl.searchParams.set("from_client", "BROWSER");
playUrl.searchParams.set("drm_tech_type", "2");
request.url = playUrl.toString();
request.response = (res) => {
let data2 = utils.toJSON(res.responseText);
let result = data2["result"];
log.info("当前解锁的quality值:" + result["quality"]);
if (result["quality"] && result["support_formats"]) {
let findValue = result["support_formats"].find((item) => {
return item["quality"] == result["quality"];
});
if (findValue) {
log.info(
"当前已解锁的画质:" + findValue["new_description"] || findValue["display_desc"]
);
}
}
};
}
});
}
};
const BilibiliApiUtils = {
/**
* 合并并检查是否传入aid或者bvid
*/
mergeAndCheckSearchParamsData(searchParamsData, config) {
if ("aid" in config && config["aid"] != null) {
Reflect.set(searchParamsData, "aid", config.aid);
} else if ("bvid" in config && config["bvid"] != null) {
Reflect.set(searchParamsData, "bvid", config.bvid);
} else {
throw new TypeError("avid or bvid must give one");
}
}
};
const BilibiliApiConfig = {
web_host: "api.bilibili.com"
};
const BilibiliVideoCodingCode = {
AVC: 7,
HEVC: 12,
AV1: 13
};
const BilibiliResponseCheck = {
/**
* check json has {code: 0, message: "0"}
*/
isWebApiSuccess(json) {
return (json == null ? void 0 : json.code) === 0 && ((json == null ? void 0 : json.message) === "0" || (json == null ? void 0 : json.message) === "success");
},
/**
* 是否是区域限制
*/
isAreaLimit(data2) {
let areaLimitCode = {
"6002003": "抱歉您所在地区不可观看!"
};
let flag = false;
Object.keys(areaLimitCode).forEach((code) => {
let codeMsg = areaLimitCode[code];
if (data2.code.toString() === code.toString() || data2.message.includes(codeMsg)) {
flag = true;
}
});
return flag;
}
};
const BilibiliVideoApi = {
/**
* 获取视频播放地址,avid或bvid必须给一个
* + /x/player/playurl
* @param config
* @param extraParams 额外参数,一般用于hook network参数内的判断
*/
async playUrl(config, extraParams) {
let searchParamsData = {
cid: config.cid,
qn: config.qn ?? BilibiliVideoPlayUrlQN["1080P60 高帧率"],
high_quality: config.high_quality ?? 1,
fnval: config.fnval ?? 1,
// 固定0
fnver: config.fnver ?? 0,
// 是否允许 4K 视频
fourk: config.fourk ?? 1
};
if (config.setPlatformHTML5) {
Reflect.set(searchParamsData, "platform", "html5");
}
BilibiliApiUtils.mergeAndCheckSearchParamsData(searchParamsData, config);
if (typeof extraParams === "object") {
Object.assign(searchParamsData, extraParams);
}
let getResp = await httpx.get(
"https://api.bilibili.com/x/player/playurl?" + utils.toSearchParamsStr(searchParamsData),
{
responseType: "json",
fetch: true
}
);
if (!getResp.status) {
return;
}
let data2 = utils.toJSON(getResp.data.responseText);
if (data2["code"] !== 0) {
return;
}
return data2["data"];
},
/**
* 获取视频在线观看人数
* + /x/player/online/total
*/
async onlineTotal(config) {
let searchParamsData = {
cid: config.cid
};
BilibiliApiUtils.mergeAndCheckSearchParamsData(searchParamsData, config);
if ("aid" in config) {
Reflect.set(searchParamsData, "aid", config.aid);
} else if ("bvid" in config) {
Reflect.set(searchParamsData, "bvid", config.bvid);
} else {
throw new TypeError("avid or bvid must give one");
}
let httpxResponse = await httpx.get(
`https://${BilibiliApiConfig.web_host}/x/player/online/total?${utils.toSearchParamsStr(searchParamsData)}`,
{
responseType: "json",
fetch: true
}
);
if (!httpxResponse.status) {
return;
}
let data2 = utils.toJSON(httpxResponse.data.responseText);
if (!BilibiliResponseCheck.isWebApiSuccess(data2)) {
log.error(`获取在线观看人数失败: ${JSON.stringify(data2)}`);
}
return data2["data"];
},
/**
* 点赞视频(web端)
* @param config
*/
async like(config) {
var _a2;
let searchParamsData = {
like: config.like,
csrf: ((_a2 = GMCookie.get("bili_jct")) == null ? void 0 : _a2.value) || ""
};
BilibiliApiUtils.mergeAndCheckSearchParamsData(searchParamsData, config);
let getResp = await httpx.get(
"https://api.bilibili.com/x/web-interface/archive/like?" + utils.toSearchParamsStr(searchParamsData),
{
fetch: true
}
);
if (!getResp.status) {
return false;
}
let data2 = utils.toJSON(getResp.data.responseText);
const code = data2["code"];
if (code === 0) {
return true;
}
if (code === -101) {
Qmsg.error("账号未登录");
} else if (code === -111) {
Qmsg.error("csrf校验失败");
} else if (code === -400) {
Qmsg.error("请求错误");
} else if (code === -403) {
Qmsg.error("账号异常");
} else if (code === 10003) {
Qmsg.error("不存在该稿件");
} else if (code === 65004) {
Qmsg.error("取消点赞失败");
} else if (code === 65006) {
Qmsg.warning("重复点赞");
} else {
Qmsg.error("未知错误:" + data2["message"]);
}
return false;
}
};
const VueUtils = {
/**
* 获取元素上的__vue__属性
* @param element
*/
getVue(element) {
return element == null ? void 0 : element.__vue__;
},
/**
* 等待vue属性并进行设置
* @param $target 目标对象
* @param needSetList 需要设置的配置
*/
waitVuePropToSet($target, needSetList) {
if (!Array.isArray(needSetList)) {
VueUtils.waitVuePropToSet($target, [needSetList]);
return;
}
function getTarget() {
let __target__ = null;
if (typeof $target === "string") {
__target__ = document.querySelector($target);
} else if (typeof $target === "function") {
__target__ = $target();
} else if ($target instanceof HTMLElement) {
__target__ = $target;
}
return __target__;
}
needSetList.forEach((needSetOption) => {
if (typeof needSetOption.msg === "string") {
log.info(needSetOption.msg);
}
function checkVue() {
let target = getTarget();
if (target == null) {
return false;
}
let vueObj = VueUtils.getVue(target);
if (vueObj == null) {
return false;
}
let needOwnCheck = needSetOption.check(vueObj);
return Boolean(needOwnCheck);
}
utils.waitVueByInterval(
() => {
return getTarget();
},
checkVue,
250,
1e4
).then((result) => {
if (!result) {
return;
}
let target = getTarget();
let vueObj = VueUtils.getVue(target);
if (vueObj == null) {
return;
}
needSetOption.set(vueObj);
});
});
},
/**
* 前往网址
* @param $vueNode 包含vue属性的元素
* @param path 需要跳转的路径
* @param [useRouter=false] 是否强制使用Vue的Router来进行跳转
*/
goToUrl($vueNode, path, useRouter = false) {
if ($vueNode == null) {
Qmsg.error("跳转Url: 获取根元素#app失败");
log.error("跳转Url: 获取根元素#app失败:" + path);
return;
}
let vueObj = VueUtils.getVue($vueNode);
if (vueObj == null) {
log.error("获取vue属性失败");
Qmsg.error("获取vue属性失败");
return;
}
let $router = vueObj.$router;
let isBlank = true;
log.info("即将跳转URL:" + path);
if (useRouter) {
isBlank = false;
}
if (isBlank) {
window.open(path, "_blank");
} else {
if (path.startsWith("http") || path.startsWith("//")) {
if (path.startsWith("//")) {
path = window.location.protocol + path;
}
let urlObj = new URL(path);
if (urlObj.origin === window.location.origin) {
path = urlObj.pathname + urlObj.search + urlObj.hash;
} else {
log.info("不同域名,直接本页打开,不用Router:" + path);
window.location.href = path;
return;
}
}
log.info("$router push跳转Url:" + path);
$router.push(path);
}
},
/**
* 手势返回
* @param option 配置
*/
hookGestureReturnByVueRouter(option) {
function popstateEvent() {
log.success("触发popstate事件");
resumeBack(true);
}
function banBack() {
log.success("监听地址改变");
option.vueInstance.$router.history.push(option.hash);
domutils.on(window, "popstate", popstateEvent);
}
async function resumeBack(isFromPopState = false) {
domutils.off(window, "popstate", popstateEvent);
let callbackResult = option.callback(isFromPopState);
if (callbackResult) {
return;
}
while (1) {
if (option.vueInstance.$router.history.current.hash === option.hash) {
log.info("后退!");
option.vueInstance.$router.back();
await utils.sleep(250);
} else {
return;
}
}
}
banBack();
return {
resumeBack
};
}
};
const BilibiliHook = {
$isHook: {
windowPlayerAgent: false,
hookWebpackJsonp_openApp: false,
overRideLaunchAppBtn_Vue_openApp: false,
overRideBiliOpenApp: false
},
$data: {
setTimeout: []
},
/**
* 劫持webpack
* @param webpackName 当前全局变量的webpack名
* @param mainCoreData 需要劫持的webpack的顶部core,例如:(window.webpackJsonp = window.webpackJsonp || []).push([["core:0"],{}])
* @param checkCallBack 如果mainCoreData匹配上,则调用此回调函数
*/
windowWebPack(webpackName = "webpackJsonp", mainCoreData, checkCallBack) {
let originObject = void 0;
OriginPrototype.Object.defineProperty(_unsafeWindow, webpackName, {
get() {
return originObject;
},
set(newValue) {
log.success("成功劫持webpack,当前webpack名:" + webpackName);
originObject = newValue;
const originPush = originObject.push;
originObject.push = function(...args) {
let _mainCoreData = args[0][0];
if (mainCoreData == _mainCoreData || Array.isArray(mainCoreData) && Array.isArray(_mainCoreData) && JSON.stringify(mainCoreData) === JSON.stringify(_mainCoreData)) {
Object.keys(args[0][1]).forEach((keyName) => {
let originSwitchFunc = args[0][1][keyName];
args[0][1][keyName] = function(..._args) {
let result = originSwitchFunc.call(this, ..._args);
_args[0] = checkCallBack(_args[0]);
return result;
};
});
}
return originPush.call(this, ...args);
};
}
});
},
/**
* window.PlayerAgent
*/
windowPlayerAgent() {
if (this.$isHook.windowPlayerAgent) {
return;
}
this.$isHook.windowPlayerAgent = true;
let PlayerAgent = void 0;
OriginPrototype.Object.defineProperty(_unsafeWindow, "PlayerAgent", {
get() {
return new Proxy(
{},
{
get(target, key) {
if (key === "openApp") {
return function(...args) {
let data2 = args[0];
log.info(["调用PlayerAgent.openApp", data2]);
if (data2["event"] === "fullScreen") {
let $wideScreen = document.querySelector(
".mplayer-btn-widescreen"
);
if ($wideScreen) {
$wideScreen.click();
} else {
log.warn(
"主动再次点击全屏按钮失败,原因:未获取到.mplayer-btn-widescreen元素"
);
}
}
};
} else {
return PlayerAgent[key];
}
}
}
);
},
set(v) {
PlayerAgent = v;
}
});
},
/**
* 劫持全局setTimeout
* + 视频页面/video
*
* window.setTimeout
* @param matchStr 需要进行匹配的函数字符串
*/
setTimeout(matchStr) {
this.$data.setTimeout.push(matchStr);
if (this.$data.setTimeout.length > 1) {
log.info("window.setTimeout hook新增劫持判断参数:" + matchStr);
return;
}
_unsafeWindow.setTimeout = function(...args) {
let callBackString = args[0].toString();
if (callBackString.match(matchStr)) {
log.success(["劫持setTimeout的函数", callBackString]);
return;
}
return OriginPrototype.setTimeout.apply(this, args);
};
},
/**
* 覆盖元素.launch-app-btn上的openApp
*
* 页面上有很多
*/
overRideLaunchAppBtn_Vue_openApp() {
if (this.$isHook.overRideLaunchAppBtn_Vue_openApp) {
return;
}
this.$isHook.overRideLaunchAppBtn_Vue_openApp = true;
function overrideOpenApp(vueObj) {
if (typeof vueObj.openApp !== "function") {
return;
}
let openAppStr = vueObj.openApp.toString();
if (openAppStr.includes("阻止唤醒App")) {
return;
}
vueObj.openApp = function(...args) {
log.success(["openApp:阻止唤醒App", args]);
};
}
utils.mutationObserver(document, {
config: {
subtree: true,
childList: true,
attributes: true
},
callback() {
document.querySelectorAll(".launch-app-btn").forEach(($launchAppBtn) => {
let vueObj = VueUtils.getVue($launchAppBtn);
if (!vueObj) {
return;
}
overrideOpenApp(vueObj);
if (vueObj.$children && vueObj.$children.length) {
vueObj.$children.forEach(($child) => {
overrideOpenApp($child);
});
}
});
}
});
},
/**
* 覆盖元素bili-open-app上的opener.open
*
* 页面上有很多
*/
overRideBiliOpenApp() {
if (this.$isHook.overRideBiliOpenApp) {
return;
}
this.$isHook.overRideBiliOpenApp = true;
utils.mutationObserver(document, {
config: {
subtree: true,
childList: true,
attributes: true
},
callback() {
document.querySelectorAll("bili-open-app").forEach(($biliOpenApp) => {
if ($biliOpenApp.hasAttribute("data-inject-opener-open")) {
return;
}
let opener = Reflect.get($biliOpenApp, "opener");
if (opener == null) {
return;
}
let originOpen = opener == null ? void 0 : opener.open;
if (typeof originOpen === "function") {
Reflect.set(opener, "open", (config) => {
log.success(
`拦截bili-open-app.open跳转: ${JSON.stringify(config)}`
);
});
$biliOpenApp.setAttribute("data-inject-opener-open", "true");
}
});
}
});
}
};
const BilibiliVideoHook = {
init() {
PopsPanel.execMenuOnce("bili-video-hook-callApp", () => {
log.info("hook window.PlayerAgent");
BilibiliHook.windowPlayerAgent();
});
}
};
const BilibiliUtils = {
/**
* 前往网址
* @param path
* @param [useRouter=false] 是否强制使用Router
*/
goToUrl(path, useRouter = false) {
let $app = document.querySelector("#app");
if ($app == null) {
Qmsg.error("跳转Url: 获取根元素#app失败");
log.error("跳转Url: 获取根元素#app失败:" + path);
return;
}
let vueObj = VueUtils.getVue($app);
if (vueObj == null) {
log.error("获取#app的vue属性失败");
Qmsg.error("获取#app的vue属性失败");
return;
}
let $router = vueObj.$router;
let isGoToUrlBlank = PopsPanel.getValue("bili-go-to-url-blank");
log.info("即将跳转URL:" + path);
if (useRouter) {
isGoToUrlBlank = false;
}
if (isGoToUrlBlank) {
window.open(path, "_blank");
} else {
if (path.startsWith("http") || path.startsWith("//")) {
if (path.startsWith("//")) {
path = window.location.protocol + path;
}
let urlObj = new URL(path);
if (urlObj.origin === window.location.origin) {
path = urlObj.pathname + urlObj.search + urlObj.hash;
} else {
log.info("不同域名,直接本页打开,不用Router:" + path);
window.location.href = path;
return;
}
}
log.info("$router push跳转Url:" + path);
$router.push(path);
}
},
/**
* 前往登录
*/
goToLogin(fromUrl = "") {
window.open(
`https://passport.bilibili.com/h5-app/passport/login?gourl=${encodeURIComponent(
fromUrl
)}`
);
},
/**
* 转换时长为显示的时长
*
* + 30 => 0:30
* + 120 => 2:00
* + 14400 => 4:00:00
* @param duration 秒
*/
parseDuration(duration) {
if (typeof duration !== "number") {
duration = parseInt(duration);
}
if (isNaN(duration)) {
return duration.toString();
}
function zeroPadding(num) {
if (num < 10) {
return `0${num}`;
} else {
return num;
}
}
if (duration < 60) {
return `0:${zeroPadding(duration)}`;
} else if (duration >= 60 && duration < 3600) {
return `${Math.floor(duration / 60)}:${zeroPadding(duration % 60)}`;
} else {
return `${Math.floor(duration / 3600)}:${zeroPadding(
Math.floor(duration / 60) % 60
)}:${zeroPadding(duration % 60)}`;
}
},
/**
* 手势返回
*/
hookGestureReturnByVueRouter(option) {
function popstateEvent() {
log.success("触发popstate事件");
resumeBack(true);
}
function banBack() {
log.success("监听地址改变");
option.vueObj.$router.history.push(option.hash);
domutils.on(window, "popstate", popstateEvent);
}
async function resumeBack(isFromPopState = false) {
domutils.off(window, "popstate", popstateEvent);
let callbackResult = option.callback(isFromPopState);
if (callbackResult) {
return;
}
while (1) {
if (option.vueObj.$router.history.current.hash === option.hash) {
log.info("后退!");
option.vueObj.$router.back();
await utils.sleep(250);
} else {
return;
}
}
}
banBack();
return {
resumeBack
};
},
/**
* 加载