// ==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 }; }, /** * 加载