// ==UserScript== // @name 【移动端】bilibili优化 // @namespace https://github.com/WhiteSevs/TamperMonkeyScript // @version 2024.9.9 // @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.1/dist/index.umd.js // @require https://fastly.jsdelivr.net/npm/@whitesev/utils@2.2.5/dist/index.umd.js // @require https://fastly.jsdelivr.net/npm/@whitesev/domutils@1.3.2/dist/index.umd.js // @require https://fastly.jsdelivr.net/npm/@whitesev/pops@1.5.3/dist/index.umd.js // @require https://fastly.jsdelivr.net/npm/md5@2.3.0/dist/md5.min.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=>{function e(n){if(typeof n!="string")throw new TypeError("cssText must be a string");let p=document.createElement("style");return p.setAttribute("type","text/css"),p.innerHTML=n,document.head?document.head.appendChild(p):document.body?document.body.appendChild(p):document.documentElement.childNodes.length===0?document.documentElement.appendChild(p):document.documentElement.insertBefore(p,document.documentElement.childNodes[0]),p}if(typeof GM_addStyle=="function"){GM_addStyle(a);return}e(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) { '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 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"); }, /** * 直播 * + 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"); } }; const BilibiliPlayerToast = { $flag: { isInitCSS: false }, $data: { /** 让Toast显示的className */ showClassName: "mplayer-show" }, toast(config) { if (typeof config === "string") { config = { text: config }; } if (!this.$flag.isInitCSS) { this.$flag.isInitCSS = true; addStyle( /*css*/ ` .mplayer-toast{ -webkit-transition-property: opacity, bottom; transition-property: opacity, bottom; } ` ); } let $toast = domutils.createElement( "div", { className: "mplayer-toast " + this.$data.showClassName }, { "data-from": "gm" } ); if (config.showCloseBtn) { let $closeBtn = domutils.createElement("div", { className: "mplayer-toast-close", innerHTML: ( /*html*/ ` ` ) }); $toast.appendChild($closeBtn); } let $text = domutils.createElement("span", { className: "mplayer-toast-text", innerText: config.text }); $toast.appendChild($text); if (typeof config.timeText === "string" && config.timeText.trim() != "") { let $time = domutils.createElement("span", { className: "mplayer-toast-time", innerText: config.timeText }); $toast.appendChild($time); } if (typeof config.jumpText === "string" && config.jumpText.trim() != "") { let $jump = domutils.createElement("span", { className: "mplayer-toast-jump", innerText: config.jumpText }); $toast.appendChild($jump); } let $parent = config.parent ?? document.querySelector(".mplayer"); this.setTransitionendEvent($toast); let timeout = typeof config.timeout === "number" && !isNaN(config.timeout) ? config.timeout : 3500; if ($parent) { let pageToastList = Array.from( document.querySelectorAll( `.mplayer-toast[data-from="gm"]` ) ); Array.from( document.querySelectorAll( `.mplayer-toast:not([data-from="gm"])` ) ).forEach(($ele) => { if (!$ele.classList.contains(this.$data.showClassName)) { $ele.remove(); return; } this.setTransitionendEvent($ele); }); if (pageToastList.length > 1) { for (let index = 0; index <= pageToastList.length - 1 - 1; index++) { const $ele = pageToastList[index]; $ele.classList.remove(this.$data.showClassName); pageToastList.splice(index, 1); index--; } } if (pageToastList.length) { if (pageToastList.length > 1) { log.warn("意外情况。pageToastList内不止一个"); } const $ele = pageToastList[0]; let bottom = 46 + 46 * 1; $ele.setAttribute("data-transition", "move"); $ele.style.bottom = bottom + "px"; Reflect.set($ele, "__nextToast", $toast); } else { $parent.appendChild($toast); } } else { throw new TypeError("toast parent is null"); } setTimeout(() => { $toast.classList.remove(this.$data.showClassName); }, timeout); return $toast; }, /** * 监听过渡结束 */ setTransitionendEvent($toast) { let animationEndNameList = [ "webkitTransitionEnd", "mozTransitionEnd", "MSTransitionEnd", "otransitionend", "transitionend" ]; let that = this; domutils.on( $toast, animationEndNameList, function(event) { let $nextToast = Reflect.get($toast, "__nextToast"); let dataTransition = $toast.getAttribute("data-transition"); if ($nextToast) { domutils.after($toast, $nextToast); } if (!$toast.classList.contains(that.$data.showClassName)) { $toast.remove(); return; } if (dataTransition === "move") { $toast.removeAttribute("data-transition"); return; } }, { capture: true } ); } }; 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()) { PopsPanel.execMenuOnce("bili-bangumi-xhr-unlockQuality", () => { this.hook_bangumi_html5(); }); } }, /** * 视频播放地址获取 * * + //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.$data.videoQuality.forEach((item) => { if (item.quality == unlockQuality) { item.isActive = true; } }); } 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 BilibiliApi_Video = { /** * 获取视频播放地址,avid或bvid必须给一个 * + /x/player/playurl * @param config * @param extraParams 额外参数,一般用于hook network参数内的判断 */ async playUrl(config, extraParams) { let getData = { 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(getData, "platform", "html5"); } if ("avid" in config) { Reflect.set(getData, "avid", config.avid); } else if ("bvid" in config) { Reflect.set(getData, "bvid", config.bvid); } else { throw new TypeError("avid or bvid must give one"); } if (typeof extraParams === "object") { Object.assign(getData, extraParams); } let getResp = await httpx.get( "https://api.bilibili.com/x/player/playurl?" + utils.toSearchParamsStr(getData), { responseType: "json", fetch: true } ); if (!getResp.status) { return; } let data2 = utils.toJSON(getResp.data.responseText); if (data2["code"] !== 0) { return; } return data2["data"]; }, /** * 点赞视频(web端) * @param config */ async like(config) { var _a2; let getData = { like: config.like, csrf: ((_a2 = GMCookie.get("bili_jct")) == null ? void 0 : _a2.value) || "" }; if ("avid" in config) { Reflect.set(getData, "avid", config.avid); } else if ("bvid" in config) { Reflect.set(getData, "bvid", config.bvid); } else { throw new TypeError("avid or bvid must give one"); } let getResp = await httpx.get( "https://api.bilibili.com/x/web-interface/archive/like?" + utils.toSearchParamsStr(getData), { 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 BilibiliPlayerUI = { $flag: { /** 是否已经添加CSS */ isInitCSS: false, /** 是否已经覆盖mplayer */ isCoverMPlayer: false }, $el: { /** 播放器右边菜单 */ get $mplayerRight() { return document.querySelector(".mplayer-right"); } }, /** 右侧面板菜单 */ $mPlayerRight: { /** 被访问状态的className */ __activeClassName: "gf-mplayer-right-item-active", /** 每一个项的className */ __itemClassName: "gf-mplayer-right-item", /** 显示右侧菜单的className */ __showMPlayerRightClassName: "gf-mplayer-right-show", /** 显示右侧菜单 */ showMPlayerRight(delayTime = 50) { if (delayTime > 0) { setTimeout(() => { this.showMPlayerRight(0); }, 50); return; } BilibiliPlayerUI.$el.$mplayerRight.classList.add( this.__showMPlayerRightClassName ); }, /** 隐藏右侧菜单 */ hideMPlayerRight() { BilibiliPlayerUI.$el.$mplayerRight.classList.remove( this.__showMPlayerRightClassName ); }, /** 清空右侧菜单 */ clearMPlayerRight() { BilibiliPlayerUI.$el.$mplayerRight.innerHTML = ""; }, /** 设置某个项访问状态 */ setActive($el) { $el.classList.add(this.__activeClassName); }, /** 切换某个项访问状态,并清空其它的访问状态 */ switchActive($el) { this.clearAllActive(); this.setActive($el); }, /** 判断该项是否是访问状态 */ isActive($el) { return $el.classList.contains(this.__activeClassName); }, /** 清空某个项访问状态 */ clearActive($el) { $el.classList.remove(this.__activeClassName); }, /** 清空所有项的访问状态 */ clearAllActive() { BilibiliPlayerUI.$el.$mplayerRight.querySelectorAll("." + this.__activeClassName).forEach((item) => item.classList.remove(this.__activeClassName)); }, /** 创建一个项 */ createMPlayerItem(text) { return domutils.createElement("div", { className: this.__itemClassName, innerHTML: text ?? "" }); } }, init() { if (!this.$flag.isInitCSS) { this.$flag.isInitCSS = true; addStyle( /*css*/ ` .mplayer-right { --mplayer-right-w: 8em; --mplayer-right-deviation: var(--mplayer-right-w); background: #181212; width: var(--mplayer-right-w) !important; opacity: 0.9 !important; visibility: visible !important; color: #ffffff; -webkit-transform: translateX(8em) !important; transform: translateX(8em) !important; z-index: 1000; overflow-y: auto; display: block; align-content: center; position: absolute; transition: transform .4s; top: 0; bottom: 0; right: 0; } .gf-mplayer-right-show{ -webkit-transform: translateX(0) !important; transform: translateX(0) !important; } .gf-mplayer-right-item{ width: 100%; text-align: center; align-content: center; padding: 1em 0px; } .gf-mplayer-right-item-active { color: var(--bili-color); } ` ); } PopsPanel.execMenuOnce("bili-coverSpeedBtn", () => { this.coverMPlayer(); this.coverSpeedBtn(); }); PopsPanel.execMenuOnce("bili-coverQuality", () => { this.coverMPlayer(); this.coverQuality(); }); PopsPanel.onceExec("bili-repairPlayerToastCloseBtn", () => { this.repairPlayerToastCloseBtn(); }); }, /** * 设置.mplayer全局点击监听,用于取消.mplayer-right */ coverMPlayer() { if (this.$flag.isCoverMPlayer) { return; } this.$flag.isCoverMPlayer = true; domutils.on( document, "click", (event) => { var _a2, _b, _c; let $click = event.target; if (((_a2 = this.$el) == null ? void 0 : _a2.$mplayerRight) && !((_c = (_b = this.$el) == null ? void 0 : _b.$mplayerRight) == null ? void 0 : _c.contains($click))) { this.$mPlayerRight.hideMPlayerRight(); } }, { capture: true } ); }, /** * 覆盖【倍速】按钮 */ coverSpeedBtn() { domutils.on( document, "click", ".mplayer-control-btn-speed", async (event) => { utils.preventEvent(event); log.info("点击【倍速】"); this.$mPlayerRight.hideMPlayerRight(); this.$mPlayerRight.clearMPlayerRight(); let speedList = [ { text: "5.0X", value: 5 }, { text: "3.0X", value: 3 }, { text: "2.0X", value: 2 }, { text: "1.5X", value: 1.5 }, { text: "1.25X", value: 1.25 }, { text: "1.0X", value: 1 }, { text: "0.75X", value: 0.75 }, { text: "0.5X", value: 0.5 }, { text: "0.25X", value: 0.25 } ]; let videoBackRate = await BilibiliPlayer.getVideoPlayBackRate(); let $isActive = void 0; speedList.forEach((item) => { let $mplayerItem = this.$mPlayerRight.createMPlayerItem(item.text); if (videoBackRate == item.value) { $isActive = $mplayerItem; } domutils.on($mplayerItem, "click", async (__event__) => { utils.preventEvent(__event__); await BilibiliPlayer.setVideoSpeed(item.value); this.$mPlayerRight.switchActive($mplayerItem); this.$mPlayerRight.hideMPlayerRight(); }); this.$el.$mplayerRight.appendChild($mplayerItem); }); if ($isActive) { this.$mPlayerRight.switchActive($isActive); $isActive.scrollIntoView({ block: "center" }); } this.$mPlayerRight.showMPlayerRight(); }, { capture: true } ); }, /** * 覆盖【清晰度】按钮 * @param initChooseQuality 是否初始化选择清晰度 */ coverQuality(initChooseQuality) { const userChooseVideoQuality_KEY = "userChooseVideoQuality"; let qualityItemClickEvent = async (itemData, $mplayerItem) => { if ($mplayerItem && this.$mPlayerRight.isActive($mplayerItem)) { log.info(`该项已选中,无需重复点击`); return; } BilibiliPlayerToast.toast("切换中,请稍后"); let playerPromise = await BilibiliPlayer.$player.playerPromise(); let bvid = playerPromise.config.bvid; let cid = playerPromise.config.cid; if (!bvid) { BilibiliPlayerToast.toast("获取bvid失败"); return; } let videoInfo = await BilibiliApi_Video.playUrl( { bvid, cid, qn: itemData.quality, setPlatformHTML5: true }, { __t: Date.now() } ); if (!videoInfo) { BilibiliPlayerToast.toast("获取视频信息失败"); log.error("获取视频信息失败"); return; } log.success(["切换清晰度-成功获取当前视频的具体信息", videoInfo]); let quality = videoInfo.quality; if (!(videoInfo.durl && Array.isArray(videoInfo.durl) && videoInfo.durl.length > 0)) { log.error("请求的视频信息内没有视频地址url"); BilibiliPlayerToast.toast("请求的视频信息内没有视频地址url"); return; } if (quality != itemData.quality) { log.error( `切换画质失败,请求到的画质和切换的画质不同,切换的: ${itemData.quality},请求到的: ${quality}` ); BilibiliPlayerToast.toast("切换画质失败,画质不同"); return; } let url = videoInfo.durl[0].url; if (playerPromise.video && playerPromise.video instanceof HTMLVideoElement) { let setVideoUrlStatus = await BilibiliPlayer.setVideoUrl(url); if (setVideoUrlStatus.success) { log.success(`已成功切换至 ${itemData.text}`); BilibiliPlayer.$data.videoQuality.forEach((globalQualityItem) => { if (globalQualityItem.quality == itemData.quality) { globalQualityItem.isActive = true; } else { globalQualityItem.isActive = false; } }); if ($mplayerItem) { this.$mPlayerRight.switchActive($mplayerItem); } BilibiliPlayerToast.toast(`已成功切换至 ${itemData.text}`); _GM_setValue(userChooseVideoQuality_KEY, quality); BilibiliDanmaku.init(); } else { log.error("切换画质失败,未成功设置video的src"); BilibiliPlayerToast.toast("切换画质失败," + setVideoUrlStatus.msg); } } else { log.error("切换画质失败,未获取到video"); BilibiliPlayerToast.toast("切换画质失败,未获取到video"); } this.$mPlayerRight.hideMPlayerRight(); }; let qualityBtnEvent = async (event) => { log.info("点击【清晰度】"); this.$mPlayerRight.hideMPlayerRight(); this.$mPlayerRight.clearMPlayerRight(); let qualityInfoList = []; if (!BilibiliPlayer.$data.videoQuality.length) { let playerPromise = await BilibiliPlayer.$player.playerPromise(); let playerQuality = playerPromise.videoQuality; Object.keys(BilibiliVideoPlayUrlQN).forEach((qualityName) => { let qualityValue = BilibiliVideoPlayUrlQN[qualityName]; qualityInfoList.push({ text: qualityName, quality: qualityValue, isActive: playerQuality == qualityValue }); }); } else { qualityInfoList = [...BilibiliPlayer.$data.videoQuality]; } utils.sortListByProperty(qualityInfoList, (value) => { return value.quality; }); let $isActive = void 0; qualityInfoList.forEach((item) => { let $mplayerItem = this.$mPlayerRight.createMPlayerItem(item.text); if (item.isActive) { $isActive = $mplayerItem; } domutils.on($mplayerItem, "click", async (__event__) => { utils.preventEvent(__event__); qualityItemClickEvent(item, $mplayerItem); }); this.$el.$mplayerRight.appendChild($mplayerItem); }); if ($isActive) { this.$mPlayerRight.switchActive($isActive); $isActive.scrollIntoView({ block: "center" }); } this.$mPlayerRight.showMPlayerRight(); }; if (initChooseQuality) { let userChooseVideoQuality = _GM_getValue( userChooseVideoQuality_KEY ); if (userChooseVideoQuality) { let findIndex = BilibiliPlayer.$data.videoQuality.findIndex( (item) => item.quality == userChooseVideoQuality && !item.isActive ); if (findIndex != -1) { qualityItemClickEvent({ text: BilibiliVideoPlayUrlQN_Value[userChooseVideoQuality], quality: userChooseVideoQuality, isActive: true }); } } } else { domutils.on( document, "click", ".mplayer-control-btn-quality", async (event) => { utils.preventEvent(event); qualityBtnEvent(); }, { capture: true } ); } }, /** * 修复toast的关闭按钮点击无效的问题 */ repairPlayerToastCloseBtn() { domutils.on( document, "click", ".mplayer-toast.mplayer-show .mplayer-toast-close", (event) => { let $mplayerShow = event.target.closest( ".mplayer-show" ); $mplayerShow.classList.remove("mplayer-show"); } ); } }; const BilibiliPlayer = { /** 获取player对象 */ get player() { return _unsafeWindow.player; }, $player: { /** 获取player的异步结果 */ async playerPromise() { await utils.waitPropertyByInterval( _unsafeWindow, () => { var _a2, _b; return typeof BilibiliPlayer.player === "object" && typeof ((_a2 = BilibiliPlayer.player) == null ? void 0 : _a2.playerPromise) === "object" && ((_b = BilibiliPlayer.player) == null ? void 0 : _b.playerPromise) != null; }, 250, 1e4 ); let playerPromise = await BilibiliPlayer.player.playerPromise; return playerPromise; }, /** 将番剧页面的h5 player转为player对象 */ parseBiliH5PlayerToPlayer($h5Player) { let $player = $h5Player.player; let options = $h5Player.options; let player = { container: $player["elem"], config: options, danmaku: $player["danmaku"], video: $player["video"], videoQuality: options["qn"], // @ts-ignore VideoInfo: { avid: options["aid"], bvid: options["bvid"], cid: options["cid"], video_type: options["video_type"] } }; let winPlayer = { playerPromise: new Promise((resolve) => { resolve(player); }) }; _unsafeWindow.player = winPlayer; } }, $data: { /** 视频清晰度信息 */ videoQuality: [] }, init() { this.$data.videoQuality = []; BilibiliDanmaku.init(); this.setVideoSpeed(1); BilibiliPlayerUI.init(); this.generateVideoInfo(); PopsPanel.execMenu("bili-video-playerAutoPlayVideo", () => { this.autoPlay(); }); }, /** * 设置视频播放倍速 * @param value 倍速值 */ async setVideoSpeed(value) { return new Promise(async (resolve, reject) => { try { let playerPromise = await this.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); return playerPromise; }, () => { return typeof playerPromise.video != null && playerPromise.video instanceof HTMLVideoElement; }, 250, 1e4 ); playerPromise.video.playbackRate = value; log.success(`设置视频播放倍速: ${value}`); let config = await BilibiliDanmaku.DanmakuCoreConfig(); config.videoSpeed = value; log.success(`设置弹幕配置的视频播放倍速: ${value}`); resolve(true); } catch (error) { reject(error); } }); }, /** * 自动播放 */ async autoPlay() { return new Promise(async (resolve, reject) => { try { let playerPromise = await this.$player.playerPromise(); setTimeout(() => { var _a2; log.success("player:自动播放视频"); (_a2 = BilibiliPlayer.player) == null ? void 0 : _a2.play(); }, 500); } catch (error) { reject(error); } }); }, /** * 获取视频播放倍速 */ async getVideoPlayBackRate() { return new Promise(async (resolve, reject) => { try { let playerPromise = await this.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); return playerPromise; }, () => { return typeof playerPromise.video != null && playerPromise.video instanceof HTMLVideoElement; }, 250, 1e4 ); resolve(playerPromise.video.playbackRate); } catch (error) { reject(error); } }); }, /** * 根据avid或者bvid获取视频的播放地址信息 * * 一般用来给清晰度按钮使用 */ async generateVideoInfo() { let playerPromise = await this.$player.playerPromise(); let bvid = playerPromise.config.bvid; let cid = playerPromise.config.cid; if (!bvid) { log.error("获取bvid失败"); return; } let videoInfo = await BilibiliApi_Video.playUrl({ bvid, cid }); if (!videoInfo) { return; } log.success(["成功获取当前视频的具体信息", videoInfo]); videoInfo.quality; if (videoInfo.durl == null || Array.isArray(videoInfo.durl) && !videoInfo.durl.length) { log.error("意外情况,获取到的视频地址信息是空的"); return; } videoInfo.durl[0].url; let support_formats = videoInfo.support_formats; this.$data.videoQuality = support_formats.map((item) => { if (item.quality <= BilibiliVideoPlayUrlQN["720P 高清"]) { return { text: item.new_description, quality: item.quality, isActive: false }; } }).filter((item) => item != null); }, /** * 设置视频地址 * @param url 视频地址 * @returns * + true 设置成功 * + false 设置失败 */ async setVideoUrl(url) { try { let getResp = await httpx.head(url, { fetch: true, fetchInit: { credentials: "same-origin" }, allowInterceptConfig: false }); if (!getResp.status) { return { success: false, msg: "测试视频地址连接失败,状态码:" + getResp.data.status }; } let playerPromise = await BilibiliPlayer.$player.playerPromise(); if (playerPromise.video && playerPromise.video instanceof HTMLVideoElement) { let originVideoTime = playerPromise.video.currentTime; playerPromise.video.src = url; playerPromise.video.currentTime = originVideoTime; await utils.sleep(500); try { playerPromise.video.play(); if (playerPromise.video.paused) { playerPromise.video.play(); } } catch (error) { log.error(error); } return { success: true, msg: "设置成功" }; } else { return { success: false, msg: "不存在video元素" }; } } catch (error) { log.error(error); return { success: false, msg: error.toString() }; } } }; const BilibiliDanmakuFilter = { key: "bili-danmaku-filter", /** 弹幕类型 */ mode: { 6: "从左往右", 5: "顶部", 4: "底部", 1: "从右往左" }, $player: { async danmakuArray() { var _a2, _b; await utils.waitPropertyByInterval( _unsafeWindow, () => { var _a3; return typeof BilibiliPlayer.player === "object" && typeof ((_a3 = BilibiliPlayer.player) == null ? void 0 : _a3.playerPromise) === "object"; }, 250, 1e4 ); let playerPromise = await BilibiliPlayer.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); }, () => { var _a3, _b2, _c, _d, _e, _f; return typeof ((_b2 = (_a3 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a3.danmakuCore) == null ? void 0 : _b2.danmakuArray) === "object" && ((_d = (_c = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _c.danmakuCore) == null ? void 0 : _d.danmakuArray) != null && Array.isArray((_f = (_e = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _e.danmakuCore) == null ? void 0 : _f.danmakuArray); }, 250, 1e4 ); let danmakuArray = (_b = (_a2 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a2.danmakuCore) == null ? void 0 : _b.danmakuArray; return danmakuArray; }, async danmakuFilter() { var _a2, _b, _c; await utils.waitPropertyByInterval( _unsafeWindow, () => { var _a3; return typeof BilibiliPlayer.player === "object" && typeof ((_a3 = BilibiliPlayer.player) == null ? void 0 : _a3.playerPromise) === "object"; }, 250, 1e4 ); let playerPromise = await BilibiliPlayer.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); }, () => { var _a3, _b2, _c2; return typeof ((_c2 = (_b2 = (_a3 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a3.danmakuCore) == null ? void 0 : _b2.config) == null ? void 0 : _c2.danmakuFilter) === "function"; }, 250, 1e4 ); let danmakuFilter = (_c = (_b = (_a2 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a2.danmakuCore) == null ? void 0 : _b.config) == null ? void 0 : _c.danmakuFilter; return danmakuFilter; } }, $data: { danmakuArray: [] }, $fn: { updateDanmakuArray: new utils.LockFunction(async () => { BilibiliDanmakuFilter.$data.danmakuArray = await BilibiliDanmakuFilter.$player.danmakuArray(); }, 250) }, async init() { let totalRule = this.parseRule(); let danmakuFilter = await this.$player.danmakuFilter(); let that = this; if (typeof danmakuFilter == "function") { let playerPromise = await BilibiliPlayer.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); }, () => { var _a2, _b, _c; return typeof ((_c = (_b = (_a2 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a2.danmakuCore) == null ? void 0 : _b.config) == null ? void 0 : _c.danmakuFilter) === "function"; }, 250, 1e4 ); let isBangumi = BilibiliRouter.isBangumi(); let ownFilter = function(danmaConfig) { let isFilter = false; isFilter = that.filter(danmaConfig, totalRule); if (isBangumi) { isFilter = !isFilter; } return isFilter; }; playerPromise.danmaku.danmakuCore.config.danmakuFilter = ownFilter; log.success(`成功覆盖danmakuFilter`); } }, /** 更新弹幕列表 */ updateDanmakuArray() { this.$fn.updateDanmakuArray.run(); }, /** * 判断是否需要过滤 * @param danmaConfig * @param totalRule * @param danmakuArray * @returns */ filter(danmaConfig, totalRule) { this.updateDanmakuArray(); let filterFlag = false; if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter-type-roll")) { if (danmaConfig.mode === 1 || danmaConfig.mode === 6) { filterFlag = true; } } } if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter-type-top")) { if (danmaConfig.mode === 5 || danmaConfig.mode === 1 || danmaConfig.mode === 6) { filterFlag = true; } } } if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter-type-bottom")) { if (danmaConfig.mode === 4) { filterFlag = true; } } } if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter-type-colour")) { if (danmaConfig.color !== 16777215) { filterFlag = true; } } } if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter-type-repeat")) { let findIndex = this.$data.danmakuArray.findIndex( (__danmaConfig__, __index__) => { return danmaConfig.text === __danmaConfig__.text && danmaConfig != __danmaConfig__; } ); if (findIndex != -1) { filterFlag = true; console.log("重复:" + findIndex); } } } if (!filterFlag) { if (PopsPanel.getValue("bili-danmaku-filter")) { for (let ruleIndex = 0; ruleIndex < totalRule.length; ruleIndex++) { const rule = totalRule[ruleIndex]; if (typeof danmaConfig.text === "string" && danmaConfig.text.match(rule)) { filterFlag = true; break; } } } } return filterFlag; }, parseRule() { let localRule = this.getValue(); let rule = []; localRule.split("\n").forEach((ruleItemStr) => { let ruleItem = ruleItemStr.trim(); let regExpRule = new RegExp( utils.parseStringToRegExpString(ruleItem), "ig" ); rule.push(regExpRule); }); return rule; }, getValue() { return _GM_getValue(this.key, ""); }, setValue(value = "") { _GM_setValue(this.key, value); } }; const BilibiliDanmaku = { /** 弹幕字体 */ fontFamily: [ { text: "黑体", value: "SimHei, 'Microsoft JhengHei'" }, { text: "宋体", value: "SimSun" }, { text: "新宋体", value: "NSimSun" }, { text: "仿宋", value: "FangSong" }, { text: "微软雅黑", value: "'Microsoft YaHei'" }, { text: "微软雅黑 Light", value: "'Microsoft Yahei UI Light'" }, { text: "Noto Sans DemiLight", value: "'Noto Sans CJK SC DemiLight'" }, { text: "'Noto Sans CJK SC Regular'", value: "'Noto Sans CJK SC Regular'" } ], init() { BilibiliDanmakuFilter.init(); let opacity = PopsPanel.getValue("bili-danmaku-opacity"); let area = PopsPanel.getValue("bili-danmaku-area"); let fontSize = PopsPanel.getValue("bili-danmaku-fontSize"); let duration = PopsPanel.getValue("bili-danmaku-duration"); let bold = PopsPanel.getValue("bili-danmaku-bold"); let fullScreenSync = PopsPanel.getValue( "bili-danmaku-fullScreenSync" ); let speedSync = PopsPanel.getValue("bili-danmaku-speedSync"); let fontFamily = PopsPanel.getValue("bili-danmaku-fontFamily"); this.setOpacity(opacity); this.setArea(area); this.setFontSize(fontSize); this.setDuration(duration); this.setBold(bold); this.setFullScreenSync(fullScreenSync); this.setSpeedSync(speedSync); this.setFontFamily(fontFamily); }, async DanmakuCoreConfig() { let playerPromise = await BilibiliPlayer.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); return playerPromise; }, () => { var _a2, _b, _c, _d; return typeof ((_b = (_a2 = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _a2.danmakuCore) == null ? void 0 : _b.config) === "object" && ((_d = (_c = playerPromise == null ? void 0 : playerPromise.danmaku) == null ? void 0 : _c.danmakuCore) == null ? void 0 : _d.config) != null; }, 250, 1e4 ); return playerPromise.danmaku.danmakuCore.config; }, /** * 设置 不透明度 * @param value */ setOpacity(value) { this.DanmakuCoreConfig().then((config) => { if ("opacity" in config) { config.opacity = value; log.success(`设置-弹幕不透明度: ${value}`); } else { log.error("设置-弹幕不透明度失败, 不存在 opacity 属性"); } }); }, /** * 设置 显示区域 * @param value */ setArea(value) { let areaMapping = { 25: "1/4屏", 50: "半屏", 75: "3/4屏", 100: "全屏" }; this.DanmakuCoreConfig().then((config) => { if ("danmakuArea" in config) { config.danmakuArea = value; log.success(`设置-显示区域: ${value} => ${areaMapping[value]}`); } else { log.error("设置-显示区域失败, 不存在 danmakuArea 属性"); } }); }, /** * 设置 字体大小 * @param value */ setFontSize(value) { this.DanmakuCoreConfig().then((config) => { if ("fontSize" in config) { config.fontSize = value; log.success(`设置-字体大小: ${value}`); } else { log.error("设置-字体大小失败, 不存在 fontSize 属性"); } }); }, /** * 设置 持续时间(弹幕速度) * @param value */ setDuration(value) { this.DanmakuCoreConfig().then((config) => { if ("duration" in config) { config.duration = value; log.success(`设置-持续时间(弹幕速度): ${value}`); } else { log.error("设置-持续时间(弹幕速度)失败, 不存在 duration 属性"); } }); }, /** * 设置 粗体 * @param value */ setBold(value) { this.DanmakuCoreConfig().then((config) => { if ("bold" in config) { config.bold = value; log.success(`设置-粗体: ${value}`); } else { log.error("设置-粗体失败, 不存在 bold 属性"); } }); }, /** * 弹幕随屏幕缩放 * @param value */ setFullScreenSync(value) { this.DanmakuCoreConfig().then((config) => { if ("fullScreenSync" in config) { config.fullScreenSync = value; log.success(`设置-弹幕随屏幕缩放: ${value}`); } else { log.error("设置-弹幕随屏幕缩放失败, 不存在 fullScreenSync 属性"); } }); }, /** * 弹幕字体 * @param value */ setFontFamily(value) { this.DanmakuCoreConfig().then((config) => { if ("fontFamily" in config) { config.fontFamily = value; log.success(`设置-弹幕字体: ${value}`); } else { log.error("设置-弹幕字体失败, 不存在 fontFamily 属性"); } }); }, /** * 弹幕速度同步播放倍数 * @param value */ setSpeedSync(value) { this.DanmakuCoreConfig().then(async (config) => { let playerPromise = await BilibiliPlayer.$player.playerPromise(); await utils.waitPropertyByInterval( async () => { playerPromise = await BilibiliPlayer.$player.playerPromise(); return playerPromise; }, () => { return typeof playerPromise.video === "object" && playerPromise.video != null && playerPromise.video instanceof HTMLVideoElement; }, 250, 1e4 ); let videoSpeed = playerPromise.video.playbackRate; if ("videoSpeed" in config) { config.videoSpeed = videoSpeed; log.success(`设置-当前视频播放倍速: ${videoSpeed}`); } else { log.error("设置-弹幕速度同步播放倍数失败, 不存在 videoSpeed 属性"); } if ("speedSync" in config) { config.speedSync = value; log.success(`设置-弹幕速度同步播放倍数: ${value}`); } else { log.error("设置-弹幕速度同步播放倍数失败, 不存在 speedSync 属性"); } }); } }; 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 SettingUICommon = { id: "panel-common", title: "通用", forms: [ { text: "", type: "forms", forms: [ { text: "功能", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "监听路由-重载所有功能", "bili-listenRouterChange", true, void 0, "用于处理页面跳转(本页)时功能不生效问题" ), UISwitch( "修复VueRouter跳转404问题", "bili-repairVueRouter404", true, void 0, "例如:点击UP主正确进入空间" ), UISwitch( "新标签页打开", "bili-go-to-url-blank", false, void 0, "通过开启【覆盖点击事件】相关的设置,通过新标签页打开链接" ), UISwitch( "允许复制", "bili-allowCopy", true, void 0, "一般用于处理楼层的回复弹窗内无法选中复制问题" ), UISwitch( "修复【倍速】按钮", "bili-coverSpeedBtn", true, void 0, "可以自行选择视频倍速" ), UISwitch( "修复【清晰度】按钮", "bili-coverQuality", true, void 0, "可查看当前视频的清晰度" ), UISwitch( "记住选择的清晰度", "bili-rememberUserChooseQuality", true, void 0, "需开启 - 修复【清晰度】按钮" ) ] } ] }, { type: "deepMenu", text: "弹幕", forms: [ { text: "弹幕设置", type: "forms", forms: [ UISlider( "不透明度", "bili-danmaku-opacity", 0.75, 0.2, 1, 0.01, (event, value) => { BilibiliDanmaku.setOpacity(value); }, (value) => { return `${parseInt((value * 100).toString())}%`; } ), UISelect( "显示区域", "bili-danmaku-area", 25, [ { text: "1/4屏", value: 25 }, { text: "半屏", value: 50 }, { text: "3/4屏", value: 75 }, { text: "全屏", value: 100 } ], (event, isSelectValue, isSelectText) => { BilibiliDanmaku.setArea(isSelectValue); } ), UISlider( "字体大小", "bili-danmaku-fontSize", 0.7, 0.2, 2, 0.1, (event, value) => { BilibiliDanmaku.setFontSize(value); }, (value) => { return `${parseInt((value * 100).toString())}%`; } ), UISelect( "弹幕速度", "bili-danmaku-duration", 6, [ { text: "极慢", value: 10 }, { text: "较慢", value: 8 }, { text: "适中", value: 6 }, { text: "较快", value: 4 }, { text: "极快", value: 2 } ], (event, isSelectValue, isSelectText) => { BilibiliDanmaku.setDuration(isSelectValue); } ), UISwitch( "弹幕随屏幕缩放", "bili-danmaku-fullScreenSync", false, (event, value) => { BilibiliDanmaku.setFullScreenSync(value); } ), UISwitch( "弹幕速度同步播放倍数", "bili-danmaku-speedSync", true, (event, value) => { BilibiliDanmaku.setSpeedSync(value); } ) ] }, { type: "forms", text: "", forms: [ UISelect( "弹幕字体", "bili-danmaku-fontFamily", (() => { let findItem = BilibiliDanmaku.fontFamily.find( (item) => item.text === "黑体" ); return findItem.value; })(), BilibiliDanmaku.fontFamily, (event, isSelectValue, isSelectText) => { BilibiliDanmaku.setFontFamily(isSelectValue); } ), UISwitch("粗体", "bili-danmaku-bold", true, (event, value) => { BilibiliDanmaku.setBold(value); }) ] }, { text: "按类型屏蔽", type: "forms", forms: [ UIOwn( (liElement) => { let $dmSetting = domutils.createElement("div", { className: "bpx-player-dm-setting-left-block-content", innerHTML: ( /*html*/ `
重复
滚动
顶部
底部
彩色
` ) }); $dmSetting.querySelectorAll( ".bpx-player-block-filter-type" ).forEach(($filterItem) => { let $label = $filterItem.querySelector( ".bpx-player-block-filter-label" ); let key = $filterItem.getAttribute("data-key"); let value = PopsPanel.getValue(key); $filterItem.setAttribute("data-value", String(value)); domutils.on($filterItem, "click", (event) => { utils.preventEvent(event); let __value = PopsPanel.getValue(key); let switchValue = !__value; if (__value) { $filterItem.setAttribute( "data-value", String(switchValue) ); } else { $filterItem.setAttribute( "data-value", String(switchValue) ); } log.success( `${$label.innerText} ${switchValue ? "开启" : "关闭"}` ); PopsPanel.setValue(key, switchValue); }); }); liElement.appendChild($dmSetting); return liElement; }, { "bili-danmaku-filter-type-repeat": false, "bili-danmaku-filter-type-roll": false, "bili-danmaku-filter-type-top": false, "bili-danmaku-filter-type-bottom": false, "bili-danmaku-filter-type-colour": false // "bili-danmaku-filter-type-senior": false, } ), UISwitch( "屏蔽词", "bili-danmaku-filter", false, void 0, "开启后可使用↓自定义的规则过滤弹幕" ), UIOwn((liElement) => { let textareaDiv = domutils.createElement( "div", { className: "pops-panel-textarea", innerHTML: ( /*html*/ ` ` ) }, { style: "width: 100%;" } ); let $textarea = textareaDiv.querySelector("textarea"); $textarea.value = BilibiliDanmakuFilter.getValue(); domutils.on( $textarea, ["input", "propertychange"], void 0, utils.debounce(function(event) { BilibiliDanmakuFilter.setValue($textarea.value); }, 200) ); liElement.appendChild(textareaDiv); return liElement; }) ] } ] }, { text: "变量设置", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "isLogin", "bili-setLogin", true, void 0, "$store.state.common.noCallApp=true
$store.state.common.userInfo.isLogin=true
$store.state.loginInfo.isLogin=true" ), UISwitch( "isClient", "bili-setIsClient", true, void 0, "$store.state.video.isClient=true
$store.state.opus.isClient=true
$store.state.playlist.isClient=true
$store.state.ver.bili=true
$store.state.ver.biliVer=2333" ), UISwitch( "tinyApp", "bili-setTinyApp", true, void 0, "$store.state.common.tinyApp=true" ) ] } ] }, { text: "劫持/拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "覆盖.launch-app-btn openApp", "bili-overrideLaunchAppBtn_Vue_openApp", true, void 0, "覆盖.launch-app-btn元素上的openApp函数,可阻止点击唤醒/下载App" ), UISwitch( "劫持setTimeout-autoOpenApp", "bili-hookSetTimeout_autoOpenApp", true, void 0, "阻止自动调用App" ) ] } ] } ] }, { text: "", type: "forms", forms: [ { text: "Toast配置", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISelect( "Toast位置", "qmsg-config-position", "bottom", [ { value: "topleft", text: "左上角" }, { value: "top", text: "顶部" }, { value: "topright", text: "右上角" }, { value: "left", text: "左边" }, { value: "center", text: "中间" }, { value: "right", text: "右边" }, { value: "bottomleft", text: "左下角" }, { value: "bottom", text: "底部" }, { value: "bottomright", text: "右下角" } ], (event, isSelectValue, isSelectText) => { log.info("设置当前Qmsg弹出位置" + isSelectText); }, "Toast显示在页面九宫格的位置" ), UISelect( "最多显示的数量", "qmsg-config-maxnums", 3, [ { value: 1, text: "1" }, { value: 2, text: "2" }, { value: 3, text: "3" }, { value: 4, text: "4" }, { value: 5, text: "5" } ], void 0, "限制Toast显示的数量" ), UISwitch( "逆序弹出", "qmsg-config-showreverse", false, void 0, "修改Toast弹出的顺序" ) ] } ] }, { text: "Cookie配置", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "启用", "httpx-use-cookie-enable", false, void 0, "启用后,将根据下面的配置进行添加cookie" ), UISwitch( "使用document.cookie", "httpx-use-document-cookie", false, void 0, "自动根据请求的域名来获取对应的cookie" ), UITextArea( "bilibili.com", "httpx-cookie-bilibili.com", "", void 0, void 0, "Cookie格式:xxx=xxxx;xxx=xxxx" ) ] } ] } ] } ] }; const SettingUIVideo = { id: "panel-video", title: "视频", isDefault() { return BilibiliRouter.isVideo(); }, forms: [ { text: "", type: "forms", forms: [ { text: "功能", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "修复视频底部区域高度", "bili-video-repairVideoBottomAreaHeight", true, void 0, "添加margin-top" ), UISwitch( "自动点击【继续在网页观看】", "bili-video-autoClickContinueToWatchOnTheWebpage", true, void 0, "可避免弹窗出现且自动点击后播放视频" ), UISwitch( "美化显示", "bili-video-beautify", true, void 0, "调整底部推荐视频卡片样式类似哔哩哔哩App" ), UISwitch( "手势返回关闭评论区", "bili-video-gestureReturnToCloseCommentArea", true, void 0, "当浏览器手势触发浏览器回退页面时,关闭评论区" ), UISwitch( "强制本页刷新跳转", "bili-video-forceThisPageToRefreshAndRedirect", false, void 0, "用于解决跳转播放视频时,播放当前视频会有上一个播放视频的声音的情况" ), UISwitch( "initPlayer", "bili-video-initPlayer", true, void 0, "自动执行初始化播放器" ), UISwitch( "自动播放视频", "bili-video-playerAutoPlayVideo", true, void 0, "需开启【initPlayer】" ) ] } ] }, { text: "变量设置", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "playBtnNoOpenApp", "bili-video-setVideoPlayer", true, void 0, "playBtnNoOpenApp=true
playBtnOpenApp=false
coverOpenApp=false" ), UISwitch( "解锁充电限制", "bili-video-unlockUpower", false, void 0, "is_upower_exclusive=true
is_upower_play=false
is_upower_preview=false" ) ] } ] }, { text: "覆盖点击事件", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "相关视频", "bili-video-cover-bottomRecommendVideo", true, void 0, "点击下面的相关视频可正确跳转至该视频" ), UISwitch( "选集", "bili-video-cover-seasonNew", true, void 0, "点击下面的选集列表内的视频可正确跳转至该视频" ) ] } ] }, { text: "网络拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "解锁清晰度", "bili-video-xhr-unlockQuality", true, void 0, "最高清晰度为720P" ) ] } ] }, { text: "劫持/拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "阻止调用App", "bili-video-hook-callApp", true, void 0, "处理函数: PlayerAgent" ) ] } ] } ] } ] }; const SettingUIBangumi = { id: "panel-bangumi", title: "番剧", isDefault() { return BilibiliRouter.isBangumi(); }, forms: [ { text: "", type: "forms", forms: [ { text: "变量设置", type: "deepMenu", forms: [ { text: "变量设置", type: "forms", forms: [ UISwitch( "pay", "bili-bangumi-setPay", true, void 0, "$store.state.userStat.pay=1
$store.state.mediaInfo.user_status.pay=1" ) ] } ] }, { text: "覆盖点击事件", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "【选集】", "bili-bangumi-cover-clicl-event-chooseEp", true, void 0, "让【选集】的视频列表可点击跳转" ), UISwitch( "【其它】", "bili-bangumi-cover-clicl-event-other", true, void 0, "让【PV&其他】、【预告】、【主题曲】、【香境剧场】等的视频列表可点击跳转" ), UISwitch( "【更多推荐】", "bili-bangumi-cover-clicl-event-recommend", true, void 0, "让【更多推荐】的视频列表可点击跳转" ) ] } ] }, { text: "网络拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "解锁清晰度", "bili-bangumi-xhr-unlockQuality", true, void 0, "最高清晰度为720P" ) ] } ] }, { text: "劫持/拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "阻止调用App", "bili-bangumi-hook-callApp", true, void 0, "" ) ] } ] } ] } ] }; const SettingUISearch = { id: "panel-search", title: "搜索", isDefault() { return BilibiliRouter.isSearch(); }, forms: [ { type: "forms", text: "", forms: [ { type: "deepMenu", text: "覆盖点击事件", forms: [ { type: "forms", text: "", forms: [ UISwitch( "取消", "bili-search-cover-cancel", false, void 0, "点击取消按钮回退至上一页" ) ] } ] } ] } ] }; const SettingUILive = { id: "panel-live", title: "直播", isDefault() { return BilibiliRouter.isLive(); }, forms: [ { text: "", type: "forms", forms: [ { text: "屏蔽", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "【屏蔽】聊天室", "bili-live-block-chatRoom", false, void 0, "直接不显示底部的聊天室" ), UISwitch( "【屏蔽】xxx进入直播间", "bili-live-block-brush-prompt", false, void 0, "直接不显示底部的xxx进入直播间" ), UISwitch( "【屏蔽】控制面板", "bili-live-block-control-panel", false, void 0, "屏蔽底部的发个弹幕、送礼" ) ] } ] }, { text: "劫持/拦截", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "阻止open-app-btn元素点击事件触发", "bili-live-prevent-openAppBtn", true, void 0, "开启后可不跳转至唤醒App页面" ) ] } ] } ] } ] }; const SettingUIOpus = { id: "panel-opus", title: "专栏", isDefault() { return BilibiliRouter.isOpus(); }, forms: [ { text: "", type: "forms", forms: [ { text: "功能", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "自动展开阅读全文", "bili-opus-automaticallyExpandToReadFullText", true, void 0, "屏蔽【展开阅读全文】按钮并自动处理全文高度" ) ] } ] }, { text: "覆盖点击事件", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "话题", "bili-opus-cover-topicJump", true, void 0, "点击话题正确跳转" ), UISwitch( "header用户", "bili-opus-cover-header", true, void 0, "点击内容上的发布本动态的用户正确跳转个人空间" ) ] } ] } ] } ] }; const SettingUIDynamic = { id: "panel-dynamic", title: "动态", isDefault() { return BilibiliRouter.isDynamic(); }, forms: [ { text: "", type: "forms", forms: [ { text: "覆盖点击事件", type: "deepMenu", forms: [ { text: "", type: "forms", forms: [ UISwitch( "话题", "bili-dynamic-cover-topicJump", true, void 0, "点击话题正确跳转" ), UISwitch( "header用户", "bili-dynamic-cover-header", true, void 0, "点击内容上的发布本动态的用户正确跳转个人空间" ), UISwitch( "@用户", "bili-dynamic-cover-atJump", true, void 0, "点击@用户正确跳转个人空间" ), UISwitch( "引用", "bili-dynamic-cover-referenceJump", true, void 0, "点击引用的视频|用户正确跳转" ) ] } ] } ] } ] }; const TVKeyInfo = { appkey: "4409e2ce8ffd12b8", appsec: "59b43e04ad6965f34319062b478f83dd" }; function appSign(params, appkey, appsec) { params.appkey = appkey; const searchParams = new URLSearchParams(params); searchParams.sort(); return md5(searchParams.toString() + appsec); } const BilibiliUtils = { /** * 获取元素上的__vue__属性 * @param element * @returns */ getVue(element) { return element == null ? void 0 : element.__vue__; }, /** * 等待vue属性并进行设置 */ waitVuePropToSet($target, needSetList) { 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__; } if (!Array.isArray(needSetList)) { this.waitVuePropToSet($target, [needSetList]); return; } needSetList.forEach((needSetOption) => { if (typeof needSetOption.msg === "string") { log.info(needSetOption.msg); } function checkVue() { let target = getTarget(); if (target == null) { return false; } let vueObj = BilibiliUtils.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 = BilibiliUtils.getVue(target); if (vueObj == null) { return; } needSetOption.set(vueObj); }); }); }, /** * 前往网址 * @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 = BilibiliUtils.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 }; }, /** * 加载