Greasy Fork is available in English.

解除B站区域限制

通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;

Mint 2018.10.08.. Lásd a legutóbbi verzió

  1. // ==UserScript==
  2. // @name 解除B站区域限制
  3. // @namespace http://tampermonkey.net/
  4. // @version 6.9.6
  5. // @description 通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;
  6. // @author ipcjs
  7. // @supportURL https://github.com/ipcjs/bilibili-helper/issues
  8. // @compatible chrome
  9. // @compatible firefox
  10. // @license MIT
  11. // @require https://static.hdslb.com/js/md5.js
  12. // @include *://www.bilibili.com/video/av*
  13. // @include *://www.bilibili.com/bangumi/play/ep*
  14. // @include *://www.bilibili.com/bangumi/play/ss*
  15. // @include *://m.bilibili.com/bangumi/play/ep*
  16. // @include *://m.bilibili.com/bangumi/play/ss*
  17. // @include *://bangumi.bilibili.com/anime/*
  18. // @include *://bangumi.bilibili.com/movie/*
  19. // @include *://www.bilibili.com/bangumi/media/md*
  20. // @include *://www.bilibili.com/blackboard/html5player.html*
  21. // @run-at document-start
  22. // @grant none
  23. // ==/UserScript==
  24.  
  25. 'use strict';
  26. const log = console.log.bind(console, 'injector:')
  27.  
  28. function injector() {
  29. if (document.getElementById('balh-injector-source')) {
  30. log(`脚本已经注入过, 不需要执行`)
  31. return
  32. }
  33. // @require https://static.hdslb.com/js/md5.js
  34. GM_info.scriptMetaStr.replace(new RegExp('// @require\\s+https?:(//.*)'), (match, /*p1:*/url) => {
  35. log('@require:', url)
  36. let $script = document.createElement('script')
  37. $script.className = 'balh-injector-require'
  38. $script.setAttribute('type', 'text/javascript')
  39. $script.setAttribute('src', url)
  40. document.head.appendChild($script)
  41. return match
  42. })
  43. let $script = document.createElement('script')
  44. $script.id = 'balh-injector-source'
  45. $script.appendChild(document.createTextNode(`
  46. ;(function(GM_info){
  47. ${scriptSource.toString()}
  48. ${scriptSource.name}('${GM_info.scriptHandler}.${injector.name}')
  49. })(${JSON.stringify(GM_info)})
  50. `))
  51. document.head.appendChild($script)
  52. log('注入完成')
  53. }
  54.  
  55. if (!Object.getOwnPropertyDescriptor(window, 'XMLHttpRequest').writable) {
  56. log('XHR对象不可修改, 需要把脚本注入到页面中', GM_info.script.name, location.href, document.readyState)
  57. injector()
  58. return
  59. }
  60.  
  61. /** 脚本的主体部分, 在GM4中, 需要把这个函数转换成字符串, 注入到页面中, 故不要引用外部的变量 */
  62. function scriptSource(invokeBy) {
  63. 'use strict';
  64. let log = console.log.bind(console, 'injector:')
  65. if (document.getElementById('balh-injector-source') && invokeBy === GM_info.scriptHandler) {
  66. // 当前, 在Firefox+GM4中, 当返回缓存的页面时, 脚本会重新执行, 并且此时XMLHttpRequest是可修改的(为什么会这样?) + 页面中存在注入的代码
  67. // 导致scriptSource的invokeBy直接是GM4...
  68. log(`页面中存在注入的代码, invokeBy却等于${GM_info.scriptHandler}, 这种情况不合理, 终止脚本执行`)
  69. return
  70. }
  71. if (document.readyState === 'uninitialized') { // Firefox上, 对于ifame中执行的脚本, 会出现这样的状态且获取到的href为about:blank...
  72. log('invokeBy:', invokeBy, 'readState:', document.readyState, 'href:', location.href, '需要等待进入loading状态')
  73. setTimeout(() => scriptSource(invokeBy + '.timeout'), 0) // 这里会暴力执行多次, 直到状态不为uninitialized...
  74. return
  75. }
  76.  
  77. const r_text = {
  78. ok: { en: 'OK', zh_cn: '确定', },
  79. close: { en: 'Close', zh_cn: '关闭' },
  80. welcome_to_acfun: '<p><b>缺B乐 了解下?</b></p><p>硬广: <a href="https://github.com/esterTion/AcFun-HTML5-Player">AcFun HTML5 Player: 比快更快 从未如此流畅</a></p>',
  81. }
  82. const _t = (key) => {
  83. const text = r_text[key]
  84. const lang = 'zh_cn'
  85. return typeof text === 'string' ? text : text[lang]
  86. }
  87.  
  88. const r = {
  89. html: {},
  90. css: {
  91. settings: '#balh-settings {font-size: 12px;color: #6d757a;} #balh-settings h1 {color: #161a1e} #balh-settings a {color: #00a1d6;} #balh-settings a:hover {color: #f25d8e} #balh-settings input {margin-left: 3px;margin-right: 3px;} @keyframes balh-settings-bg { from {background: rgba(0, 0, 0, 0)} to {background: rgba(0, 0, 0, .7)} } #balh-settings label {width: 100%;display: inline-block;cursor: pointer} #balh-settings label:after {content: "";width: 0;height: 1px;background: #4285f4;transition: width .3s;display: block} #balh-settings label:hover:after {width: 100%} form {margin: 0} #balh-settings input[type="radio"] {-webkit-appearance: radio;-moz-appearance: radio;appearance: radio;} #balh-settings input[type="checkbox"] {-webkit-appearance: checkbox;-moz-appearance: checkbox;appearance: checkbox;} ',
  92. },
  93. attr: {},
  94. url: {
  95. issue: 'https://github.com/ipcjs/bilibili-helper/issues',
  96. issue_new: 'https://github.com/ipcjs/bilibili-helper/issues/new',
  97. },
  98. script: {
  99. is_dev: GM_info.script.name.includes('.dev'),
  100. },
  101. const: {
  102. mode: {
  103. DEFAULT: 'default',// 默认模式, 自动判断使用何种模式, 推荐;
  104. REPLACE: 'replace', // 替换模式, 替换有区域限制的视频的接口的返回值;
  105. REDIRECT: 'redirect',// 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
  106. },
  107. server: {
  108. S0: 'https://biliplus.ipcjs.win',
  109. S1: 'https://www.biliplus.com',
  110. defaultServer: function () {
  111. return this.S1
  112. },
  113. },
  114. TRUE: 'Y',
  115. FALSE: '',
  116. },
  117. baipiao: [
  118. { key: 'zomble_land_saga', match: () => (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.epInfo && window.__INITIAL_STATE__.epInfo.ep_id) === 251255, link: 'http://www.acfun.cn/bangumi/ab5022161_31405_278830', message: r_text.welcome_to_acfun },
  119. { key: 'zomble_land_saga', match: () => (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.mediaInfo && window.__INITIAL_STATE__.mediaInfo.media_id) === 140772, link: 'http://www.acfun.cn/bangumi/aa5022161', message: r_text.welcome_to_acfun },
  120. ]
  121. }
  122. const util_stringify = (item) => {
  123. if (typeof item === 'object') {
  124. try {
  125. return JSON.stringify(item)
  126. } catch (e) {
  127. console.debug(e)
  128. return item.toString()
  129. }
  130. } else {
  131. return item
  132. }
  133. }
  134. const util_arr_stringify = function (arr) {
  135. return arr.map(util_stringify).join(' ')
  136. }
  137.  
  138. const util_str_multiply = function (str, multiplier) {
  139. let result = ''
  140. for (let i = 0; i < multiplier; i++) {
  141. result += str
  142. }
  143. return result
  144. }
  145.  
  146. const util_log_hub = (function () {
  147. const tag = GM_info.script.name + '.msg'
  148.  
  149. // 计算"楼层", 若当前window就是顶层的window, 则floor为0, 以此类推
  150. function computefloor(w = window, floor = 0) {
  151. if (w === window.top) {
  152. return floor
  153. } else {
  154. return computefloor(w.parent, floor + 1)
  155. }
  156. }
  157.  
  158. let floor = computefloor()
  159. let msgList = []
  160. if (floor === 0) { // 只有顶层的Window才需要收集日志
  161. window.addEventListener('message', (event) => {
  162. if (event.data instanceof Array && event.data[0] === tag) {
  163. let [/*tag*/, fromFloor, msg] = event.data
  164. msgList.push(util_str_multiply(' ', fromFloor) + msg)
  165. }
  166. })
  167. }
  168. return {
  169. msg: function (msg) {
  170. window.top.postMessage([tag, floor, msg], '*')
  171. },
  172. getAllMsg: function () {
  173. return msgList.join('\n')
  174. }
  175. }
  176. }())
  177. const util_log_impl = function (type) {
  178. if (r.script.is_dev) {
  179. // 直接打印, 会显示行数
  180. return window.console[type].bind(window.console, type + ':');
  181. } else {
  182. // 将log收集到util_log_hub中, 显示的行数是错误的...
  183. return function (...args) {
  184. args.unshift(type + ':')
  185. window.console[type].apply(window.console, args)
  186. util_log_hub.msg(util_arr_stringify(args))
  187. }
  188. }
  189. }
  190. const util_log = util_log_impl('log')
  191. const util_debug = util_log_impl('debug')
  192. const util_error = util_log_impl('error')
  193. log = util_log
  194. log(`[${GM_info.script.name} v${GM_info.script.version} (${invokeBy})] run on: ${window.location.href}`);
  195.  
  196. const util_func_noop = function () { }
  197. const util_func_catched = function (func, onError) {
  198. let ret = function () {
  199. try {
  200. return func.apply(this, arguments)
  201. } catch (e) {
  202. if (onError) return onError(e) // onError可以处理报错时的返回值
  203. // 否则打印log, 并返回undefined
  204. util_error('Exception while run %o: %o\n%o', func, e, e.stack)
  205. return undefined
  206. }
  207. }
  208. // 函数的name属性是不可写+可配置的, 故需要如下代码实现类似这样的效果: ret.name = func.name
  209. // 在Edge上匿名函数的name的描述符会为undefined, 需要做特殊处理, fuck
  210. let funcNameDescriptor = Object.getOwnPropertyDescriptor(func, 'name') || {
  211. value: '',
  212. writable: false,
  213. configurable: true,
  214. }
  215. Object.defineProperty(ret, 'name', funcNameDescriptor)
  216. return ret
  217. }
  218.  
  219. const util_init = (function () {
  220. const RUN_AT = {
  221. DOM_LOADED: 0,
  222. DOM_LOADED_AFTER: 1,
  223. COMPLETE: 2,
  224. }
  225. const PRIORITY = {
  226. FIRST: 1e6,
  227. HIGH: 1e5,
  228. BEFORE: 1e3,
  229. DEFAULT: 0,
  230. AFTER: -1e3,
  231. LOW: -1e5,
  232. LAST: -1e6,
  233. }
  234. const callbacks = {
  235. [RUN_AT.DOM_LOADED]: [],
  236. [RUN_AT.DOM_LOADED_AFTER]: [],
  237. [RUN_AT.COMPLETE]: [],
  238. }
  239. const util_page_valid = () => true // 是否要运行
  240. const dclCreator = function (runAt) {
  241. let dcl = function () {
  242. util_init.atRun = runAt // 更新运行状态
  243. const valid = util_page_valid()
  244. // 优先级从大到小, index从小到大, 排序
  245. callbacks[runAt].sort((a, b) => b.priority - a.priority || a.index - b.index)
  246. .filter(item => valid || item.always)
  247. .forEach(item => item.func(valid))
  248. }
  249. return dcl
  250. }
  251.  
  252. if (window.document.readyState !== 'loading') {
  253. throw new Error('unit_init must run at loading, current is ' + document.readyState)
  254. }
  255.  
  256. window.document.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED))
  257. window.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED_AFTER))
  258. window.addEventListener('load', dclCreator(RUN_AT.COMPLETE))
  259.  
  260. const util_init = function (func, priority = PRIORITY.DEFAULT, runAt = RUN_AT.DOM_LOADED, always = false) {
  261. func = util_func_catched(func)
  262. if (util_init.atRun < runAt) { // 若还没运行到runAt指定的状态, 则放到队列里去
  263. callbacks[runAt].push({
  264. priority,
  265. index: callbacks[runAt].length, // 使用callback数组的长度, 作为添加元素的index属性
  266. func,
  267. always
  268. })
  269. } else { // 否则直接运行
  270. let valid = util_page_valid()
  271. setTimeout(() => (valid || always) && func(valid), 1)
  272. }
  273. return func
  274. }
  275. util_init.atRun = -1 // 用来表示当前运行到什么状态
  276. util_init.RUN_AT = RUN_AT
  277. util_init.PRIORITY = PRIORITY
  278. return util_init
  279. }())
  280. /** 通知模块 剽窃自 YAWF 用户脚本 硬广:https://tiansh.github.io/yawf/ */
  281. const util_notify = (function () {
  282. var avaliable = {};
  283. var shown = [];
  284. var use = {
  285. 'hasPermission': function () { return null; },
  286. 'requestPermission': function (callback) { return null; },
  287. 'hideNotification': function (notify) { return null; },
  288. 'showNotification': function (id, title, body, icon, delay, onclick) { return null; }
  289. };
  290.  
  291. // 检查一个微博是不是已经被显示过了,如果显示过了不重复显示
  292. var shownFeed = function (id) {
  293. return false;
  294. };
  295.  
  296. // webkitNotifications
  297. // Tab Notifier 扩展实现此接口,但显示的桌面提示最多只能显示前两行
  298. if (typeof webkitNotifications !== 'undefined') avaliable.webkit = {
  299. 'hasPermission': function () {
  300. return [true, null, false][webkitNotifications.checkPermission()];
  301. },
  302. 'requestPermission': function (callback) {
  303. return webkitNotifications.requestPermission(callback);
  304. },
  305. 'hideNotification': function (notify) {
  306. notify.cancel();
  307. afterHideNotification(notify);
  308. },
  309. 'showNotification': function (id, title, body, icon, delay, onclick) {
  310. if (shownFeed(id)) return null;
  311. var notify = webkitNotifications.createNotification(icon, title, body);
  312. if (delay && delay > 0) notify.addEventListener('display', function () {
  313. setTimeout(function () { hideNotification(notify); }, delay);
  314. });
  315. if (onclick) notify.addEventListener('click', function () {
  316. onclick.apply(this, arguments);
  317. hideNotification(notify);
  318. });
  319. notify.show();
  320. return notify;
  321. },
  322. };
  323.  
  324. // Notification
  325. // Firefox 22+
  326. // 显示4秒会自动关闭 https://bugzil.la/875114
  327. if (typeof Notification !== 'undefined') avaliable.standard = {
  328. 'hasPermission': function () {
  329. return {
  330. 'granted': true,
  331. 'denied': false,
  332. 'default': null,
  333. }[Notification.permission];
  334. },
  335. 'requestPermission': function (callback) {
  336. return Notification.requestPermission(callback);
  337. },
  338. 'hideNotification': function (notify) {
  339. notify.close();
  340. afterHideNotification(notify);
  341. },
  342. 'showNotification': function (id, title, body, icon, delay, onclick) {
  343. if (shownFeed(id)) return null;
  344. var notify = new Notification(title, { 'body': body, 'icon': icon, 'requireInteraction': !delay });
  345. if (delay && delay > 0) notify.addEventListener('show', function () {
  346. setTimeout(function () {
  347. hideNotification(notify);
  348. }, delay);
  349. });
  350. if (onclick) notify.addEventListener('click', function () {
  351. onclick.apply(this, arguments);
  352. hideNotification(notify);
  353. });
  354. return notify;
  355. },
  356. };
  357.  
  358. // 有哪些接口可用
  359. var avaliableNotification = function () {
  360. return Object.keys(avaliable);
  361. };
  362. // 选择用哪个接口
  363. var choseNotification = function (prefer) {
  364. return (use = prefer && avaliable[prefer] || avaliable.standard);
  365. };
  366. choseNotification();
  367. // 检查权限
  368. var hasPermission = function () {
  369. return use.hasPermission.apply(this, arguments);
  370. };
  371. // 请求权限
  372. var requestPermission = function () {
  373. return use.requestPermission.apply(this, arguments);
  374. };
  375. // 显示消息
  376. var showNotification = function (id, title, body, icon, delay, onclick) {
  377. var notify = use.showNotification.apply(this, arguments);
  378. shown.push(notify);
  379. return notify;
  380. };
  381. // 隐藏已经显示的消息
  382. var hideNotification = function (notify) {
  383. use.hideNotification.apply(this, arguments);
  384. return notify;
  385. };
  386. var afterHideNotification = function (notify) {
  387. shown = shown.filter(function (x) { return x !== notify; });
  388. };
  389.  
  390. document.addEventListener('unload', function () {
  391. shown.forEach(hideNotification);
  392. shown = [];
  393. });
  394. var showNotificationAnyway = function (id, title, body, icon, delay, onclick) {
  395. var that = this, thatArguments = arguments;
  396. switch (that.hasPermission()) {
  397. case null: // default
  398. that.requestPermission(function () {
  399. showNotificationAnyway.apply(that, thatArguments);
  400. });
  401. break;
  402. case true: // granted
  403. // 只有已获取了授权, 才能有返回值...
  404. return that.showNotification.apply(that, thatArguments);
  405. break;
  406. case false: // denied
  407. log('Notification permission: denied');
  408. break;
  409. }
  410. return null;
  411. }
  412.  
  413. return {
  414. 'avaliableNotification': avaliableNotification,
  415. 'choseNotification': choseNotification,
  416. 'hasPermission': hasPermission,
  417. 'requestPermission': requestPermission,
  418. 'showNotification': showNotification,
  419. 'hideNotification': hideNotification,
  420. show: function (body, onclick, delay = 3e3) {
  421. return this.showNotificationAnyway(Date.now(), GM_info.script.name, body, '//bangumi.bilibili.com/favicon.ico', delay, onclick)
  422. },
  423. showNotificationAnyway
  424. };
  425. }())
  426. const util_cookie = (function () {
  427. function getCookies() {
  428. var map = document.cookie.split('; ').reduce(function (obj, item) {
  429. var entry = item.split('=');
  430. obj[entry[0]] = entry[1];
  431. return obj;
  432. }, {});
  433. return map;
  434. }
  435.  
  436. function getCookie(key) {
  437. return getCookies()[key];
  438. }
  439.  
  440. /**
  441. * @param key key
  442. * @param value 为undefined时, 表示删除cookie
  443. * @param options 为undefined时, 表示过期时间为3年
  444. * 为''时, 表示Session cookie
  445. * 为数字时, 表示指定过期时间
  446. * 为{}时, 表示指定所有的属性
  447. * */
  448. function setCookie(key, value, options) {
  449. if (typeof options !== 'object') {
  450. options = {
  451. domain: '.bilibili.com',
  452. path: '/',
  453. 'max-age': value === undefined ? 0 : (options === undefined ? 94608000 : options)
  454. };
  455. }
  456. var c = Object.keys(options).reduce(function (str, key) {
  457. return str + '; ' + key + '=' + options[key];
  458. }, key + '=' + value);
  459. document.cookie = c;
  460. return c;
  461. }
  462.  
  463. return new Proxy({ set: setCookie, get: getCookie, all: getCookies }, {
  464. get: function (target, prop) {
  465. if (prop in target) return target[prop]
  466. return getCookie(prop)
  467. },
  468. set: function (target, prop, value) {
  469. setCookie(prop, value)
  470. return true
  471. }
  472. })
  473. }())
  474. const Promise = window.Promise // 在某些情况下, 页面中会修改window.Promise... 故我们要备份一下原始的Promise
  475. const util_promise_plus = (function () {
  476. /**
  477. * 模仿RxJava中的compose操作符
  478. * @param transformer 转换函数, 传入Promise, 返回Promise; 若为空, 则啥也不做
  479. */
  480. Promise.prototype.compose = function (transformer) {
  481. return transformer ? transformer(this) : this
  482. }
  483. }())
  484. const util_ajax = function (options) {
  485. return new Promise(function (resolve, reject) {
  486. typeof options !== 'object' && (options = { url: options });
  487.  
  488. options.async === undefined && (options.async = true);
  489. options.xhrFields === undefined && (options.xhrFields = { withCredentials: true });
  490. options.success = function (data) {
  491. resolve(data);
  492. };
  493. options.error = function (err) {
  494. reject(err);
  495. };
  496. util_debug('ajax:', options.url)
  497. $.ajax(options);
  498. });
  499. }
  500. /**
  501. * @param promiseCeator 创建Promise的函数
  502. * @param resultTranformer 用于变换result的函数, 返回新的result或Promise
  503. * @param errorTranformer 用于变换error的函数, 返回新的error或Promise, 返回的Promise可以做状态恢复...
  504. */
  505. const util_async_wrapper = function (promiseCeator, resultTranformer, errorTranformer) {
  506. return function (...args) {
  507. return new Promise((resolve, reject) => {
  508. // log(promiseCeator, ...args)
  509. promiseCeator(...args)
  510. .then(r => resultTranformer ? resultTranformer(r) : r)
  511. .then(r => resolve(r))
  512. .catch(e => {
  513. e = errorTranformer ? errorTranformer(e) : e
  514. if (!(e instanceof Promise)) {
  515. // 若返回值不是Promise, 则表示是一个error
  516. e = Promise.reject(e)
  517. }
  518. e.then(r => resolve(r)).catch(e => reject(e))
  519. })
  520. })
  521. }
  522. }
  523. /**
  524. * 创建元素的快捷方法:
  525. * 1. type, props, children
  526. * 2. type, props, innerHTML
  527. * 3. 'text', text
  528. * @param type string, 标签名; 特殊的, 若为text, 则表示创建文字, 对应的t为文字的内容
  529. * @param props object, 属性; 特殊的属性名有: className, 类名; style, 样式, 值为(样式名, 值)形式的object; event, 值为(事件名, 监听函数)形式的object;
  530. * @param children array, 子元素; 也可以直接是html文本;
  531. */
  532. const util_ui_element_creator = (type, props, children) => {
  533. let elem = null;
  534. if (type === "text") {
  535. return document.createTextNode(props);
  536. } else {
  537. elem = document.createElement(type);
  538. }
  539. for (let n in props) {
  540. if (n === "style") {
  541. for (let x in props.style) {
  542. elem.style[x] = props.style[x];
  543. }
  544. } else if (n === "className") {
  545. elem.className = props[n];
  546. } else if (n === "event") {
  547. for (let x in props.event) {
  548. elem.addEventListener(x, props.event[x]);
  549. }
  550. } else {
  551. elem.setAttribute(n, props[n]);
  552. }
  553. }
  554. if (children) {
  555. if (typeof children === 'string') {
  556. elem.innerHTML = children;
  557. } else {
  558. for (let i = 0; i < children.length; i++) {
  559. if (children[i] != null)
  560. elem.appendChild(children[i]);
  561. }
  562. }
  563. }
  564. return elem;
  565. }
  566. const _ = util_ui_element_creator
  567. const util_jsonp = function (url, callback) {
  568. return new Promise((resolve, reject) => {
  569. document.head.appendChild(_('script', {
  570. src: url,
  571. event: {
  572. load: function () {
  573. resolve()
  574. },
  575. error: function () {
  576. reject()
  577. }
  578. }
  579. }));
  580. })
  581. }
  582. const util_generate_sign = function (params, key) {
  583. var s_keys = [];
  584. for (var i in params) {
  585. s_keys.push(i);
  586. }
  587. s_keys.sort();
  588. var data = "";
  589. for (var i = 0; i < s_keys.length; i++) {
  590. // encodeURIComponent 返回的转义数字必须为大写( 如 %2F )
  591. data += (data ? "&" : "") + s_keys[i] + "=" + encodeURIComponent(params[s_keys[i]]);
  592. }
  593. return {
  594. "sign": hex_md5(data + key),
  595. "params": data
  596. };
  597. }
  598. const util_xml2obj = (xml) => {
  599. try {
  600. var obj = {}, text;
  601. var children = xml.children;
  602. if (children.length > 0) {
  603. for (var i = 0; i < children.length; i++) {
  604. var item = children.item(i);
  605. var nodeName = item.nodeName;
  606.  
  607. if (typeof (obj[nodeName]) == "undefined") { // 若是新的属性, 则往obj中添加
  608. obj[nodeName] = util_xml2obj(item);
  609. } else {
  610. if (typeof (obj[nodeName].push) == "undefined") { // 若老的属性没有push方法, 则把属性改成Array
  611. var old = obj[nodeName];
  612.  
  613. obj[nodeName] = [];
  614. obj[nodeName].push(old);
  615. }
  616. obj[nodeName].push(util_xml2obj(item));
  617. }
  618. }
  619. } else {
  620. text = xml.textContent;
  621. if (/^\d+(\.\d+)?$/.test(text)) {
  622. obj = Number(text);
  623. } else if (text === 'true' || text === 'false') {
  624. obj = Boolean(text);
  625. } else {
  626. obj = text;
  627. }
  628. }
  629. return obj;
  630. } catch (e) {
  631. util_error(e);
  632. }
  633. }
  634. const util_ui_popframe = function (iframeSrc) {
  635. if (!document.getElementById('balh-style-login')) {
  636. var style = document.createElement('style');
  637. style.id = 'balh-style-login';
  638. document.head.appendChild(style).innerHTML = '@keyframes pop-iframe-in{0%{opacity:0;transform:scale(.7);}100%{opacity:1;transform:scale(1)}}@keyframes pop-iframe-out{0%{opacity:1;transform:scale(1);}100%{opacity:0;transform:scale(.7)}}.GMBiliPlusCloseBox{position:absolute;top:5%;right:8%;font-size:40px;color:#FFF}';
  639. }
  640.  
  641. var div = document.createElement('div');
  642. div.id = 'GMBiliPlusLoginContainer';
  643. div.innerHTML = '<div style="position:fixed;top:0;left:0;z-index:10000;width:100%;height:100%;background:rgba(0,0,0,.5);animation-fill-mode:forwards;animation-name:pop-iframe-in;animation-duration:.5s;cursor:pointer"><iframe src="' + iframeSrc + '" style="background:#e4e7ee;position:absolute;top:10%;left:10%;width:80%;height:80%"></iframe><div class="GMBiliPlusCloseBox">×</div></div>';
  644. div.firstChild.addEventListener('click', function (e) {
  645. if (e.target === this || e.target.className === 'GMBiliPlusCloseBox') {
  646. if (!confirm('确认关闭?')) {
  647. return false;
  648. }
  649. div.firstChild.style.animationName = 'pop-iframe-out';
  650. setTimeout(function () {
  651. div.remove();
  652. }, 5e2);
  653. }
  654. });
  655. document.body.appendChild(div);
  656. }
  657.  
  658. const util_ui_alert = function (message, resolve, reject) {
  659. setTimeout(() => {
  660. if (resolve) {
  661. if (window.confirm(message)) {
  662. resolve()
  663. } else {
  664. if (reject) {
  665. reject()
  666. }
  667. }
  668. } else {
  669. alert(message)
  670. }
  671. }, 500)
  672. }
  673.  
  674. /**
  675. * - param.content: 内容元素数组/HTML
  676. * - param.showConfirm: 是否显示确定按钮
  677. * - param.confirmBtn: 确定按钮的文字
  678. * - param.onConfirm: 确定回调
  679. * - param.onClose: 关闭回调
  680. */
  681. const util_ui_pop = function (param) {
  682. if (typeof param.content === 'string') {
  683. let template = _('template');
  684. template.innerHTML = param.content.trim()
  685. param.content = Array.from(template.content.childNodes)
  686. } else if (!(param.content instanceof Array)) {
  687. util_log(`param.content(${param.content}) 不是数组`)
  688. return;
  689. }
  690.  
  691. if (document.getElementById('AHP_Notice_style') == null) {
  692. let noticeWidth = Math.min(500, innerWidth - 40);
  693. document.head.appendChild(_('style', { id: 'AHP_Notice_style' }, [_('text', `#AHP_Notice{ line-height:normal;position:fixed;left:0;right:0;top:0;height:0;z-index:20000;transition:.5s;cursor:default } .AHP_down_banner{ margin:2px;padding:2px;color:#FFFFFF;font-size:13px;font-weight:bold;background-color:green } .AHP_down_btn{ margin:2px;padding:4px;color:#1E90FF;font-size:14px;font-weight:bold;border:#1E90FF 2px solid;display:inline-block;border-radius:5px } body.ABP-FullScreen{ overflow:hidden } @keyframes pop-iframe-in{0%{opacity:0;transform:scale(.7);}100%{opacity:1;transform:scale(1)}} @keyframes pop-iframe-out{0%{opacity:1;transform:scale(1);}100%{opacity:0;transform:scale(.7)}} #AHP_Notice>div{ position:absolute;bottom:0;left:0;right:0;font-size:15px } #AHP_Notice>div>div{ border:1px #AAA solid;width:${noticeWidth}px;margin:0 auto;padding:20px 10px 5px;background:#EFEFF4;color:#000;border-radius:5px;box-shadow:0 0 5px -2px } #AHP_Notice>div>div *{ margin:5px 0; } #AHP_Notice input[type=text]{ border: none;border-bottom: 1px solid #AAA;width: 60%;background: transparent } #AHP_Notice input[type=text]:active{ border-bottom-color:#4285f4 } #AHP_Notice input[type=button] { border-radius: 2px; border: #adadad 1px solid; padding: 3px; margin: 0 5px; min-width:50px } #AHP_Notice input[type=button]:hover { background: #FFF; } #AHP_Notice input[type=button]:active { background: #CCC; } .noflash-alert{display:none}`)]));
  694. }
  695.  
  696. if (document.querySelector('#AHP_Notice') != null)
  697. document.querySelector('#AHP_Notice').remove();
  698.  
  699. let div = _('div', { id: 'AHP_Notice' });
  700. let childs = [];
  701. if (param.showConfirm || param.confirmBtn || param.onConfirm) {
  702. childs.push(_('input', { value: param.confirmBtn || _t('ok'), type: 'button', className: 'confirm', event: { click: param.onConfirm } }));
  703. }
  704. childs.push(_('input', {
  705. value: _t('close'), type: 'button', className: 'close', event: {
  706. click: function () {
  707. param.onClose && param.onClose();
  708. div.style.height = 0;
  709. setTimeout(function () { div.remove(); }, 500);
  710. }
  711. }
  712. }));
  713. div.appendChild(_('div', {}, [_('div', {},
  714. param.content.concat([_('hr'), _('div', { style: { textAlign: 'right' } }, childs)])
  715. )]));
  716. document.body.appendChild(div);
  717. div.style.height = div.firstChild.offsetHeight + 'px';
  718. }
  719.  
  720.  
  721. /**
  722. * MessageBox -> from base.core.js
  723. * MessageBox.show(referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback)
  724. * MessageBox.close()
  725. */
  726. const util_ui_msg = (function () {
  727. function MockMessageBox() {
  728. this.show = (...args) => util_log(MockMessageBox.name, 'show', args)
  729. this.close = (...args) => util_log(MockMessageBox.name, 'close', args)
  730. }
  731.  
  732. let popMessage = null
  733. let mockPopMessage = new MockMessageBox()
  734. let notifyPopMessage = {
  735. _current_notify: null,
  736. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  737. this.close()
  738. this._current_notify = util_notify.show(message, buttonTypeConfirmCallback, closeTime)
  739. },
  740. close: function () {
  741. if (this._current_notify) {
  742. util_notify.hideNotification(this._current_notify)
  743. this._current_notify = null
  744. }
  745. }
  746. }
  747. let alertPopMessage = {
  748. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  749. util_ui_alert(message, buttonTypeConfirmCallback)
  750. },
  751. close: util_func_noop
  752. }
  753.  
  754. util_init(() => {
  755. if (!popMessage && window.MessageBox) {
  756. popMessage = new window.MessageBox()
  757. let orignShow = popMessage.show
  758. popMessage.show = function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  759. // 这个窗,有一定机率弹不出来。。。不知道为什么
  760. orignShow.call(this, referenceElement, message.replace('\n', '<br>'), closeTime, boxType, buttonTypeConfirmCallback)
  761. }
  762. popMessage.close = function () {
  763. // 若没调用过show, 就调用close, msgbox会为null, 导致报错
  764. this.msgbox != null && window.MessageBox.prototype.close.apply(this, arguments)
  765. }
  766. }
  767. }, util_init.PRIORITY.FIRST, util_init.RUN_AT.DOM_LOADED_AFTER)
  768.  
  769. return {
  770. _impl: function () {
  771. return popMessage || alertPopMessage
  772. },
  773. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  774. let pop = this._impl()
  775. return pop.show.apply(pop, arguments)
  776. },
  777. close: function () {
  778. let pop = this._impl()
  779. return pop.close.apply(pop, arguments)
  780. },
  781. setMsgBoxFixed: function (fixed) {
  782. if (popMessage) {
  783. popMessage.msgbox[0].style.position = fixed ? 'fixed' : ''
  784. } else {
  785. util_log(MockMessageBox.name, 'setMsgBoxFixed', fixed)
  786. }
  787. },
  788. showOnNetError: function (e) {
  789. if (e.readyState === 0) {
  790. this.show($('.balh_settings'), '哎呀,服务器连不上了,进入设置窗口,换个服务器试试?', 0, 'button', balh_ui_setting.show);
  791. }
  792. },
  793. showOnNetErrorInPromise: function () {
  794. return p => p
  795. .catch(e => {
  796. this.showOnNetError(e)
  797. return Promise.reject(e)
  798. })
  799. }
  800. }
  801. }())
  802. const util_ui_player_msg = function (message) {
  803. const msg = util_stringify(message)
  804. log('player msg:', msg)
  805. const $panel = document.querySelector('.bilibili-player-video-panel-text')
  806. if ($panel) {
  807. let stage = $panel.children.length + 1000 // 加1000和B站自己发送消息的stage区别开来
  808. $panel.appendChild(_('div', { className: 'bilibili-player-video-panel-row', stage: stage }, [_('text', `[${GM_info.script.name}] ${msg}`)]))
  809. }
  810. }
  811. const util_ui_copy = function (text, textarea) {
  812. textarea.value = text
  813. textarea.select()
  814. try {
  815. return document.execCommand('copy')
  816. } catch (e) {
  817. util_error('复制文本出错', e)
  818. }
  819. return false
  820. }
  821. const util_url_param = function (url, key) {
  822. return (url.match(new RegExp('[?|&]' + key + '=(\\w+)')) || ['', ''])[1];
  823. }
  824.  
  825. const util_page = {
  826. player: () => location.href.includes('www.bilibili.com/blackboard/html5player'),
  827. // 在av页面中的iframe标签形式的player
  828. player_in_av: util_func_catched(() => util_page.player() && window.top.location.href.includes('www.bilibili.com/video/av'), (e) => log(e), false),
  829. av: () => location.href.includes('www.bilibili.com/video/av'),
  830. bangumi: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/?$')),
  831. bangumi_md: () => location.href.includes('www.bilibili.com/bangumi/media/md'),
  832. // movie页面使用window.aid, 保存当前页面av号
  833. movie: () => location.href.includes('bangumi.bilibili.com/movie/'),
  834. // anime页面使用window.season_id, 保存当前页面season号
  835. anime: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/play.*')),
  836. anime_ep: () => location.href.includes('www.bilibili.com/bangumi/play/ep'),
  837. anime_ss: () => location.href.includes('www.bilibili.com/bangumi/play/ss'),
  838. anime_ep_m: () => location.href.includes('m.bilibili.com/bangumi/play/ep'),
  839. anime_ss_m: () => location.href.includes('m.bilibili.com/bangumi/play/ss'),
  840. }
  841.  
  842. const balh_config = (function () {
  843. const cookies = util_cookie.all() // 缓存的cookies
  844. return new Proxy({ /*保存config的对象*/ }, {
  845. get: function (target, prop) {
  846. if (prop in target) {
  847. return target[prop]
  848. } else { // 若target中不存在指定的属性, 则从缓存的cookies中读取, 并保存到target中
  849. let value = cookies['balh_' + prop]
  850. switch (prop) {
  851. case 'server':
  852. value = value || r.const.server.defaultServer()
  853. // 从tk域名迁移到新的默认域名
  854. if (value.includes('biliplus.ipcjsdev.tk')) {
  855. value = r.const.server.defaultServer()
  856. balh_config.server = value
  857. }
  858. break
  859. case 'mode':
  860. value = value || (balh_config.blocked_vip ? r.const.mode.REDIRECT : r.const.mode.DEFAULT)
  861. break
  862. case 'flv_prefer_ws':
  863. value = r.const.FALSE // 关闭该选项
  864. break
  865. default:
  866. // case 'blocked_vip':
  867. // case 'remove_pre_ad':
  868. break
  869. }
  870. target[prop] = value
  871. return value
  872. }
  873. },
  874. set: function (target, prop, value) {
  875. target[prop] = value // 更新值
  876. util_cookie['balh_' + prop] = value // 更新cookie中的值
  877. return true
  878. }
  879. })
  880. }())
  881.  
  882. const balh_api_plus_view = function (aid, update = true) {
  883. return util_ajax(`${balh_config.server}/api/view?id=${aid}&update=${update}`)
  884. }
  885. const balh_api_plus_season = function (season_id) {
  886. return util_ajax(`${balh_config.server}/api/bangumi?season=${season_id}`)
  887. }
  888. // https://www.biliplus.com/BPplayurl.php?otype=json&cid=30188339&module=bangumi&qn=16&src=vupload&vid=vupload_30188339
  889. // qn = 16, 能看
  890. const balh_api_plus_playurl = function (cid, qn = 16, bangumi = true) {
  891. return util_ajax(`${balh_config.server}/BPplayurl.php?otype=json&cid=${cid}${bangumi ? '&module=bangumi' : ''}&qn=${qn}&src=vupload&vid=vupload_${cid}`)
  892. }
  893. // https://www.biliplus.com/api/h5play.php?tid=33&cid=31166258&type=vupload&vid=vupload_31166258&bangumi=1
  894. const balh_api_plus_playurl_for_mp4 = (cid, bangumi = true) => util_ajax(`${balh_config.server}/api/h5play.php?tid=33&cid=${cid}&type=vupload&vid=vupload_${cid}&bangumi=${bangumi ? 1 : 0}`)
  895. .then(text => (text.match(/srcUrl=\{"mp4":"(https?.*)"\};/) || ['', ''])[1]); // 提取mp4的url
  896.  
  897. const balh_feature_area_limit = (function () {
  898. function injectXHR() {
  899. util_debug('XMLHttpRequest的描述符:', Object.getOwnPropertyDescriptor(window, 'XMLHttpRequest'))
  900. let firstCreateXHR = true
  901. window.XMLHttpRequest = new Proxy(window.XMLHttpRequest, {
  902. construct: function (target, args) {
  903. // 第一次创建XHR时, 打上断点...
  904. if (firstCreateXHR && r.script.is_dev) {
  905. firstCreateXHR = false
  906. // debugger
  907. }
  908. let container = {} // 用来替换responseText等变量
  909. return new Proxy(new target(...args), {
  910. set: function (target, prop, value, receiver) {
  911. if (prop === 'onreadystatechange') {
  912. let cb = value
  913. value = function () {
  914. if (target.readyState === 4) {
  915. if (target.responseURL.includes('bangumi.bilibili.com/view/web_api/season/user/status')) {
  916. log('/season/user/status:', target.responseText)
  917. let json = JSON.parse(target.responseText)
  918. let rewriteResult = false
  919. if (json.code === 0 && json.result) {
  920. areaLimit(json.result.area_limit !== 0)
  921. if (json.result.area_limit !== 0) {
  922. json.result.area_limit = 0 // 取消区域限制
  923. rewriteResult = true
  924. }
  925. if (balh_config.blocked_vip) {
  926. json.result.pay = 1
  927. rewriteResult = true
  928. }
  929. if (rewriteResult) {
  930. container.responseText = JSON.stringify(json)
  931. }
  932. }
  933. } else if (target.responseURL.includes('bangumi.bilibili.com/web_api/season_area')) {
  934. log('/season_area', target.responseText)
  935. let json = JSON.parse(target.responseText)
  936. if (json.code === 0 && json.result) {
  937. areaLimit(json.result.play === 0)
  938. if (json.result.play === 0) {
  939. json.result.play = 1
  940. container.responseText = JSON.stringify(json)
  941. }
  942. }
  943. } else if (target.responseURL.includes('api.bilibili.com/x/web-interface/nav')) {
  944. let json = JSON.parse(target.responseText)
  945. log('/x/web-interface/nav', (json.data && json.data.isLogin)
  946. ? { uname: json.data.uname, isLogin: json.data.isLogin, level: json.data.level_info.current_level, vipType: json.data.vipType, vipStatus: json.data.vipStatus }
  947. : target.responseText)
  948. if (json.code === 0 && json.data && balh_config.blocked_vip) {
  949. json.data.vipType = 2; // 类型, 年度大会员
  950. json.data.vipStatus = 1; // 状态, 启用
  951. container.responseText = JSON.stringify(json)
  952. }
  953. }
  954. }
  955. // 这里的this是原始的xhr, 在container.responseText设置了值时需要替换成代理对象
  956. cb.apply(container.responseText ? receiver : this, arguments)
  957. }
  958. }
  959. target[prop] = value
  960. return true
  961. },
  962. get: function (target, prop, receiver) {
  963. if (prop in container) return container[prop]
  964. let value = target[prop]
  965. if (typeof value === 'function') {
  966. let func = value
  967. // open等方法, 必须在原始的xhr对象上才能调用...
  968. value = function () {
  969. return func.apply(target, arguments)
  970. }
  971. }
  972. return value
  973. }
  974. })
  975. }
  976. })
  977. }
  978.  
  979. function injectAjax() {
  980. let originalAjax = $.ajax;
  981. $.ajax = function (arg0, arg1) {
  982. // log(arguments);
  983. let param;
  984. if (arg1 === undefined) {
  985. param = arg0;
  986. } else {
  987. arg0 && (arg1.url = arg0);
  988. param = arg1;
  989. }
  990. let oriSuccess = param.success;
  991. let oriError = param.error;
  992. let mySuccess, myError;
  993. // 投递结果的transformer, 结果通过oriSuccess/Error投递
  994. let dispatchResultTransformer = p => p
  995. .then(r => oriSuccess(r))
  996. .catch(e => oriError(e))
  997. // 转换原始请求的结果的transformer
  998. let oriResultTransformer
  999. let one_api;
  1000. if (param.url.match('/web_api/get_source')) {
  1001. one_api = bilibiliApis._get_source;
  1002. oriResultTransformer = p => p
  1003. .then(json => {
  1004. log(json);
  1005. if (json.code === -40301 // 区域限制
  1006. || json.result.payment && json.result.payment.price != 0 && balh_config.blocked_vip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
  1007. areaLimit(true);
  1008. return one_api.asyncAjax(param.url)
  1009. .catch(e => json)// 新的请求报错, 也应该返回原来的数据
  1010. } else {
  1011. areaLimit(false);
  1012. if ((balh_config.blocked_vip || balh_config.remove_pre_ad) && json.code === 0 && json.result.pre_ad) {
  1013. json.result.pre_ad = 0; // 去除前置广告
  1014. }
  1015. return json;
  1016. }
  1017. })
  1018. } else if (param.url.match('/player/web_api/playurl') // 老的番剧页面playurl接口
  1019. || param.url.match('/player/web_api/v2/playurl') // 新的番剧页面playurl接口
  1020. || (balh_config.enable_in_av && param.url.match('//interface.bilibili.com/v2/playurl')) // 普通的av页面playurl接口
  1021. ) {
  1022. one_api = bilibiliApis._playurl;
  1023. oriResultTransformer = p => p
  1024. .then(json => {
  1025. log(json)
  1026. if (balh_config.blocked_vip || json.code || isAreaLimitForPlayUrl(json)) {
  1027. areaLimit(true)
  1028. return one_api.asyncAjax(param.url)
  1029. .catch(e => json)
  1030. } else {
  1031. areaLimit(false)
  1032. return json
  1033. }
  1034. })
  1035. const oriDispatchResultTransformer = dispatchResultTransformer
  1036. dispatchResultTransformer = p => p
  1037. .then(r => {
  1038. if (!r.from && !r.result && !r.accept_description) {
  1039. util_log('playurl的result缺少必要的字段:', r)
  1040. r.from = 'local'
  1041. r.result = 'suee'
  1042. r.accept_description = ['未知 3P']
  1043. // r.timelength = r.durl.map(it => it.length).reduce((a, b) => a + b, 0)
  1044. if (r.durl && r.durl[0] && r.durl[0].url.includes('biliplus-vid.win')) {
  1045. const aid = window.__INITIAL_STATE__ && window.__INITIAL_STATE__.aid || 'fuck'
  1046. util_ui_pop({
  1047. content: `原视频已被删除, 当前播放的是<a href="https://bg.biliplus-vid.win/">转存服务器</a>中的视频, 速度较慢<br>被删的原因可能是:<br>1. 视频违规<br>2. 视频被归类到番剧页面 => 试下<a href="https://search.bilibili.com/bangumi?keyword=${aid}">搜索av${aid}</a>`
  1048. })
  1049. }
  1050. }
  1051. return r
  1052. })
  1053. .compose(oriDispatchResultTransformer)
  1054. } else if (param.url.match('//interface.bilibili.com/player?')) {
  1055. if (balh_config.blocked_vip) {
  1056. mySuccess = function (data) {
  1057. try {
  1058. let xml = new window.DOMParser().parseFromString(`<userstatus>${data.replace(/\&/g, '&amp;')}</userstatus>`, 'text/xml');
  1059. let vipTag = xml.querySelector('vip');
  1060. if (vipTag) {
  1061. let vip = JSON.parse(vipTag.innerHTML);
  1062. vip.vipType = 2; // 类型, 年度大会员
  1063. vip.vipStatus = 1; // 状态, 启用
  1064. vipTag.innerHTML = JSON.stringify(vip);
  1065. data = xml.documentElement.innerHTML;
  1066. }
  1067. } catch (e) {
  1068. log('parse xml error: ', e);
  1069. }
  1070. oriSuccess(data);
  1071. };
  1072. }
  1073. } else if (param.url.match('//api.bilibili.com/x/ad/video?')) {
  1074. if (balh_config.remove_pre_ad) {
  1075. mySuccess = function (data) {
  1076. log('/ad/video', data)
  1077. if (data && data.code === 0 && data.data) {
  1078. data.data = [] // 移除广告接口返回的数据
  1079. }
  1080. oriSuccess(data)
  1081. }
  1082. }
  1083. }
  1084.  
  1085. if (one_api && oriResultTransformer) {
  1086. if (needRedirect()) {
  1087. // 清除原始请求的回调
  1088. mySuccess = util_func_noop
  1089. myError = util_func_noop
  1090. // 通过proxy, 执行请求
  1091. one_api.asyncAjax(param.url)
  1092. .compose(dispatchResultTransformer)
  1093. } else {
  1094. // 请求结果通过mySuccess/Error获取, 将其包装成Promise, 方便处理
  1095. new Promise((resolve, reject) => {
  1096. mySuccess = resolve
  1097. myError = reject
  1098. }).compose(oriResultTransformer)
  1099. .compose(dispatchResultTransformer)
  1100. }
  1101. }
  1102.  
  1103. // 若外部使用param.success处理结果, 则替换param.success
  1104. if (oriSuccess && mySuccess) {
  1105. param.success = mySuccess;
  1106. }
  1107. // 处理替换error
  1108. if (oriError && myError) {
  1109. param.error = myError;
  1110. }
  1111. // default
  1112. let xhr = originalAjax.apply(this, [param]);
  1113.  
  1114. // 若外部使用xhr.done()处理结果, 则替换xhr.done()
  1115. if (!oriSuccess && mySuccess) {
  1116. xhr.done(mySuccess);
  1117. xhr.done = function (success) {
  1118. oriSuccess = success; // 保存外部设置的success函数
  1119. return xhr;
  1120. };
  1121. }
  1122. // 处理替换error
  1123. if (!oriError && myError) {
  1124. xhr.fail(myError);
  1125. xhr.fail = function (error) {
  1126. oriError = error;
  1127. return xhr;
  1128. }
  1129. }
  1130. return xhr;
  1131. };
  1132. }
  1133.  
  1134. function injectFetch() {
  1135. window.fetch = util_async_wrapper(window.fetch,
  1136. resp => new Proxy(resp, {
  1137. get: function (target, prop, receiver) {
  1138. if (prop === 'json') {
  1139. return util_async_wrapper(target.json.bind(target),
  1140. oriResult => {
  1141. util_debug('injectFetch:', target.url)
  1142. if (target.url.includes('/player/web_api/v2/playurl/html5')) {
  1143. let cid = util_url_param(target.url, 'cid')
  1144. return balh_api_plus_playurl(cid)
  1145. .then(result => {
  1146. if (result.code) {
  1147. return Promise.reject('error: ' + JSON.stringify(result))
  1148. } else {
  1149. return balh_api_plus_playurl_for_mp4(cid)
  1150. .then(url => {
  1151. util_debug(`mp4地址, 移动版: ${url}, pc版: ${result.durl[0].url}`)
  1152. return {
  1153. "code": 0,
  1154. "cid": `http://comment.bilibili.com/${cid}.xml`,
  1155. "timelength": result.timelength,
  1156. "src": url || result.durl[0].url, // 只取第一个片段的url...
  1157. }
  1158. })
  1159. }
  1160. })
  1161. .catch(e => {
  1162. // 若拉取视频地址失败, 则返回原始的结果
  1163. log('fetch mp4 url failed', e)
  1164. return oriResult
  1165. })
  1166. }
  1167. return oriResult
  1168. },
  1169. error => error)
  1170. }
  1171. return target[prop]
  1172. }
  1173. }),
  1174. error => error)
  1175. }
  1176.  
  1177. function isAreaLimitSeason() {
  1178. return util_cookie['balh_season_' + getSeasonId()];
  1179. }
  1180.  
  1181. function needRedirect() {
  1182. return balh_config.mode === r.const.mode.REDIRECT || (balh_config.mode === r.const.mode.DEFAULT && isAreaLimitSeason())
  1183. }
  1184.  
  1185. function areaLimit(limit) {
  1186. balh_config.mode === r.const.mode.DEFAULT && setAreaLimitSeason(limit)
  1187. }
  1188.  
  1189. function setAreaLimitSeason(limit) {
  1190. var season_id = getSeasonId();
  1191. util_cookie.set('balh_season_' + season_id, limit ? '1' : undefined, ''); // 第三个参数为'', 表示时Session类型的cookie
  1192. log('setAreaLimitSeason', season_id, limit);
  1193. }
  1194.  
  1195. function getSeasonId() {
  1196. var seasonId;
  1197. // 取anime页面的seasonId
  1198. try {
  1199. // 若w, 是其frame的window, 则有可能没有权限, 而抛异常
  1200. seasonId = window.season_id || window.top.season_id;
  1201. } catch (e) {
  1202. log(e);
  1203. }
  1204. if (!seasonId) {
  1205. try {
  1206. seasonId = (window.top.location.pathname.match(/\/anime\/(\d+)/) || ['', ''])[1];
  1207. } catch (e) {
  1208. log(e);
  1209. }
  1210. }
  1211.  
  1212. // 若没取到, 则取movie页面的seasonId, 以m开头
  1213. if (!seasonId) {
  1214. try {
  1215. seasonId = (window.top.location.pathname.match(/\/movie\/(\d+)/) || ['', ''])[1];
  1216. if (seasonId) {
  1217. seasonId = 'm' + seasonId;
  1218. }
  1219. } catch (e) {
  1220. log(e);
  1221. }
  1222. }
  1223.  
  1224. // 若没取到, 则去新的番剧播放页面的ep或ss
  1225. if (!seasonId) {
  1226. try {
  1227. seasonId = (window.top.location.pathname.match(/\/bangumi\/play\/((ep|ss)\d+)/) || ['', ''])[1];
  1228. } catch (e) {
  1229. log(e);
  1230. }
  1231. }
  1232. // 若没取到, 则去取av页面的av号
  1233. if (!seasonId) {
  1234. try {
  1235. seasonId = (window.top.location.pathname.match(/\/video\/(av\d+)/) || ['', ''])[1]
  1236. } catch (e) {
  1237. log(e);
  1238. }
  1239. }
  1240. // 最后, 若没取到, 则试图取出当前页面url中的aid
  1241. if (!seasonId) {
  1242. seasonId = util_url_param(window.location.href, 'aid');
  1243. if (seasonId) {
  1244. seasonId = 'aid' + seasonId;
  1245. }
  1246. }
  1247. return seasonId || '000';
  1248. }
  1249.  
  1250. function isAreaLimitForPlayUrl(json) {
  1251. return json.durl && json.durl.length === 1 && json.durl[0].length === 15126 && json.durl[0].size === 124627;
  1252. }
  1253.  
  1254. var bilibiliApis = (function () {
  1255. function AjaxException(message, code = 0/*用0表示未知错误*/) {
  1256. this.name = 'AjaxException'
  1257. this.message = message
  1258. this.code = code
  1259. }
  1260. AjaxException.prototype.toString = function () {
  1261. return `${this.name}: ${this.message}(${this.code})`
  1262. }
  1263. function BilibiliApi(props) {
  1264. Object.assign(this, props);
  1265. }
  1266.  
  1267. BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
  1268. var one_api = this;
  1269. $.ajax({
  1270. url: one_api.transToProxyUrl(originUrl),
  1271. async: true,
  1272. xhrFields: { withCredentials: true },
  1273. success: function (result) {
  1274. log('==>', result);
  1275. success(one_api.processProxySuccess(result));
  1276. // log('success', arguments, this);
  1277. },
  1278. error: function (e) {
  1279. log('error', arguments, this);
  1280. error(e);
  1281. }
  1282. });
  1283. };
  1284. BilibiliApi.prototype.asyncAjax = function (originUrl) {
  1285. return util_ajax(this.transToProxyUrl(originUrl))
  1286. .then(r => this.processProxySuccess(r))
  1287. .compose(util_ui_msg.showOnNetErrorInPromise()) // 出错时, 提示服务器连不上
  1288. }
  1289. var get_source_by_aid = new BilibiliApi({
  1290. transToProxyUrl: function (url) {
  1291. return balh_config.server + '/api/view?id=' + window.aid + '&update=true';
  1292. },
  1293. processProxySuccess: function (data) {
  1294. if (data && data.list && data.list[0] && data.movie) {
  1295. return {
  1296. code: 0,
  1297. message: 'success',
  1298. result: {
  1299. cid: data.list[0].cid,
  1300. formal_aid: data.aid,
  1301. movie_status: balh_config.blocked_vip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
  1302. pay_begin_time: 1507708800,
  1303. pay_timestamp: 0,
  1304. pay_user_status: data.movie.pay_user.status, // 一般都是0
  1305. player: data.list[0].type, // 一般为movie
  1306. vid: data.list[0].vid,
  1307. vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
  1308. vipType: balh_config.blocked_vip ? 2 : 0,
  1309. vipStatus: balh_config.blocked_vip ? 1 : 0,
  1310. }
  1311. }
  1312. };
  1313. } else {
  1314. return {
  1315. code: -404,
  1316. message: '不存在该剧集'
  1317. };
  1318. }
  1319. }
  1320. });
  1321. var get_source_by_season_id = new BilibiliApi({
  1322. transToProxyUrl: function (url) {
  1323. return balh_config.server + '/api/bangumi?season=' + window.season_id;
  1324. },
  1325. processProxySuccess: function (data) {
  1326. var found = null;
  1327. if (!data.code) {
  1328. for (var i = 0; i < data.result.episodes.length; i++) {
  1329. if (data.result.episodes[i].episode_id == window.episode_id) {
  1330. found = data.result.episodes[i];
  1331. }
  1332. }
  1333. } else {
  1334. util_ui_alert('代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', window.location.reload.bind(window.location));
  1335. }
  1336. var returnVal = found !== null
  1337. ? {
  1338. "code": 0,
  1339. "message": "success",
  1340. "result": {
  1341. "aid": found.av_id,
  1342. "cid": found.danmaku,
  1343. "episode_status": balh_config.blocked_vip ? 2 : found.episode_status,
  1344. "payment": { "price": "9876547210.33" },
  1345. "pay_user": {
  1346. "status": balh_config.blocked_vip ? 1 : 0 // 是否已经支付过
  1347. },
  1348. "player": "vupload",
  1349. "pre_ad": 0,
  1350. "season_status": balh_config.blocked_vip ? 2 : data.result.season_status
  1351. }
  1352. }
  1353. : { code: -404, message: '不存在该剧集' };
  1354. return returnVal;
  1355. }
  1356. });
  1357. var playurl_by_bilibili = new BilibiliApi({
  1358. dataType: 'xml',
  1359. transToProxyUrl: function (originUrl) {
  1360. const api_url = 'https://interface.bilibili.com/playurl?'
  1361. const bangumi_api_url = 'https://bangumi.bilibili.com/player/web_api/playurl?'
  1362. const SEC_NORMAL = '1c15888dc316e05a15fdd0a02ed6584f'
  1363. const SEC_BANGUMI = '9b288147e5474dd2aa67085f716c560d'
  1364.  
  1365. // 不设置module; 带module的接口都是有区域限制的...
  1366. let module = undefined /*util_url_param(originUrl, 'module')*/
  1367. // 不使用json; 让服务器直接返回json时, 获取的视频url不能直接播放...天知道为什么
  1368. let useJson = false
  1369. let paramDict = {
  1370. cid: util_url_param(originUrl, 'cid'),
  1371. quality: util_url_param(originUrl, 'quality'),
  1372. qn: util_url_param(originUrl, 'qn'), // 增加这个参数, 返回的清晰度更多
  1373. player: 1,
  1374. ts: Math.floor(Date.now() / 1000),
  1375. }
  1376. if (module) paramDict.module = module
  1377. if (useJson) paramDict.otype = 'json'
  1378. let { sign, params } = util_generate_sign(paramDict, module ? SEC_BANGUMI : SEC_NORMAL)
  1379. let url = module ? bangumi_api_url : api_url + params + '&sign=' + sign
  1380. return url
  1381. },
  1382. processProxySuccess: function (result, alertWhenError = true) {
  1383. // 将xml解析成json
  1384. let obj = util_xml2obj(result.documentElement)
  1385. if (!obj || obj.code) {
  1386. if (alertWhenError) {
  1387. util_ui_alert(`从B站接口获取视频地址失败\nresult: ${JSON.stringify(obj)}\n\n点击确定, 进入设置页面关闭'使用B站接口获取视频地址'功能`, balh_ui_setting.show)
  1388. } else {
  1389. return Promise.reject(`服务器错误: ${JSON.stringify(obj)}`)
  1390. }
  1391. } else {
  1392. obj.accept_quality && (obj.accept_quality = obj.accept_quality.split(',').map(n => +n))
  1393. if (!obj.durl.push) {
  1394. obj.durl = [obj.durl]
  1395. }
  1396. obj.durl.forEach((item) => {
  1397. if (item.backup_url === '') {
  1398. item.backup_url = undefined
  1399. } else if (item.backup_url && item.backup_url.url) {
  1400. item.backup_url = item.backup_url.url
  1401. }
  1402. })
  1403. }
  1404. log('xml2obj', result, '=>', obj)
  1405. return obj
  1406. },
  1407. _asyncAjax: function (originUrl) {
  1408. return util_ajax(this.transToProxyUrl(originUrl))
  1409. .then(r => this.processProxySuccess(r, false))
  1410. }
  1411. })
  1412. var playurl_by_proxy = new BilibiliApi({
  1413. _asyncAjax: function (originUrl, bangumi) {
  1414. return util_ajax(this.transToProxyUrl(originUrl, bangumi))
  1415. .then(r => this.processProxySuccess(r, false))
  1416. },
  1417. transToProxyUrl: function (url, bangumi) {
  1418. if (bangumi === undefined) {
  1419. // av页面中的iframe标签形式的player, 不是番剧视频
  1420. bangumi = !util_page.player_in_av()
  1421. // season_type, 1 为动画, 5 为电视剧; 为5/3时, 不是番剧视频
  1422. let season_type_param = util_url_param(url, 'season_type')
  1423. if (season_type_param === '5' || season_type_param === '3') {
  1424. bangumi = false
  1425. }
  1426. }
  1427. var params = url.split('?')[1];
  1428. if (!bangumi) {
  1429. params = params.replace(/&?module=(\w+)/, '') // 移除可能存在的module参数
  1430. }
  1431. return `${balh_config.server}/BPplayurl.php?${params}`;
  1432. },
  1433. processProxySuccess: function (data, alertWhenError = true) {
  1434. // data有可能为null
  1435. if (data && data.code === -403) {
  1436. util_ui_alert(`突破黑洞失败\n当前代理服务器(${balh_config.server})依然有区域限制\n\n可以考虑进行如下尝试:\n1. 进行“帐号授权”\n2. 换个代理服务器\n\n点击确定, 打开设置页面`, balh_ui_setting.show)
  1437. } else if (data === null || data.code) {
  1438. util_error(data);
  1439. if (alertWhenError) {
  1440. util_ui_alert(`突破黑洞失败\n${JSON.stringify(data)}\n点击确定刷新界面`, window.location.reload.bind(window.location));
  1441. } else {
  1442. return Promise.reject(new AjaxException(`服务器错误: ${JSON.stringify(data)}`, data ? data.code : 0))
  1443. }
  1444. } else if (isAreaLimitForPlayUrl(data)) {
  1445. util_error('>>area limit');
  1446. util_ui_alert(`突破黑洞失败\n需要登录\n点此确定进行登录`, balh_feature_sign.showLogin);
  1447. } else {
  1448. if (balh_config.flv_prefer_ws) {
  1449. data.durl.forEach(function (seg) {
  1450. var t, url, i;
  1451. if (!seg.url.includes('ws.acgvideo.com')) {
  1452. for (i in seg.backup_url) {
  1453. url = seg.backup_url[i];
  1454. if (url.includes('ws.acgvideo.com')) {
  1455. log('flv prefer use:', url);
  1456. t = seg.url;
  1457. seg.url = url;
  1458. url = t;
  1459. break;
  1460. }
  1461. }
  1462.  
  1463. }
  1464. });
  1465. }
  1466. }
  1467. return data;
  1468. }
  1469. })
  1470. const playurl = new BilibiliApi({
  1471. asyncAjax: function (originUrl) {
  1472. util_ui_player_msg('从代理服务器拉取视频地址中...')
  1473. return playurl_by_proxy._asyncAjax(originUrl) // 优先从代理服务器获取
  1474. .catch(e => {
  1475. if (e instanceof AjaxException && e.code === 1) { // code: 1 表示非番剧视频, 不能使用番剧视频参数
  1476. util_ui_player_msg(e)
  1477. util_ui_player_msg('尝试使用非番剧视频接口拉取视频地址...')
  1478. return playurl_by_proxy._asyncAjax(originUrl, false)
  1479. .catch(e2 => Promise.reject(e)) // 忽略e2, 返回原始错误e
  1480. } else {
  1481. return Promise.reject(e)
  1482. }
  1483. })
  1484. .catch(e => {
  1485. util_ui_player_msg(e)
  1486. util_ui_player_msg('尝试换用B站接口拉取视频地址(清晰度低)...')
  1487. // 失败时, 转而从B站获取
  1488. return playurl_by_bilibili._asyncAjax(originUrl)
  1489. .catch(e2 => {
  1490. util_ui_player_msg(e2) // 打印错误日志
  1491. // 直接忽略playurl_by_bilibili的错误, 改成返回playurl_by_proxy的错误...
  1492. return Promise.reject(e)
  1493. })
  1494. })
  1495. .catch(e => {
  1496. util_ui_alert(`拉取视频地址失败\n${util_stringify(e)}\n\n可以考虑进行如下尝试:\n1. 多刷新几下页面\n2. 进入设置页面更换代理服务器\n3. 耐心等待代理服务器端修复问题\n\n点击确定按钮, 刷新页面`, window.location.reload.bind(window.location))
  1497. return Promise.reject(e)
  1498. })
  1499. }
  1500. })
  1501. return {
  1502. _get_source: util_page.movie() ? get_source_by_aid : get_source_by_season_id,
  1503. _playurl: playurl,
  1504. };
  1505. })();
  1506.  
  1507. if (util_page.anime_ep_m() || util_page.anime_ss_m()) {
  1508. // balh_api_plus_playurl_for_mp4返回的url能在移动设备上播放的前提是, 请求头不包含Referer...
  1509. // 故这里设置meta, 使页面不发送Referer
  1510. // 注意动态改变引用策略的方式并不是标准行为, 目前在Chrome上测试是有用的
  1511. document.head.appendChild(_('meta', { name: "referrer", content: "no-referrer" }))
  1512. injectFetch()
  1513. util_init(() => {
  1514. const $wrapper = document.querySelector('.player-wrapper')
  1515. new MutationObserver(function (mutations, observer) {
  1516. for (let mutation of mutations) {
  1517. if (mutation.type === 'childList') {
  1518. for (let node of mutation.addedNodes) {
  1519. if (node.tagName === 'DIV' && node.className.split(' ').includes('player-mask')) {
  1520. log('隐藏添加的mask')
  1521. node.style.display = 'none'
  1522. }
  1523. }
  1524. }
  1525. }
  1526. }).observe($wrapper, {
  1527. childList: true,
  1528. attributes: false,
  1529. });
  1530. })
  1531. }
  1532. injectXHR();
  1533. if (!window.jQuery) { // 若还未加载jQuery, 则监听
  1534. var jQuery;
  1535. Object.defineProperty(window, 'jQuery', {
  1536. configurable: true, enumerable: true, set: function (v) {
  1537. jQuery = v;
  1538. injectAjax();// 设置jQuery后, 立即注入
  1539. }, get: function () {
  1540. return jQuery;
  1541. }
  1542. });
  1543. } else {
  1544. injectAjax();
  1545. }
  1546. }())
  1547. const balh_feature_remove_pre_ad = (function () {
  1548. if (util_page.player()) {
  1549. // 播放页面url中的pre_ad参数, 决定是否播放广告...
  1550. if (balh_config.remove_pre_ad && util_url_param(location.href, 'pre_ad') == 1) {
  1551. log('需要跳转到不含广告的url')
  1552. location.href = location.href.replace(/&?pre_ad=1/, '')
  1553. }
  1554. }
  1555. }())
  1556. const balh_feature_check_html5 = (function () {
  1557. function isHtml5Player() {
  1558. return localStorage.defaulth5 === '1'
  1559. }
  1560.  
  1561. function checkHtml5() {
  1562. var playerContent = document.querySelector('.player-content');
  1563. if (!localStorage.balh_h5_not_first && !isHtml5Player() && window.GrayManager && playerContent) {
  1564. new MutationObserver(function (mutations, observer) {
  1565. observer.disconnect();
  1566. localStorage.balh_h5_not_first = r.const.TRUE;
  1567. if (window.confirm(GM_info.script.name + '只在HTML5播放器下有效,是否切换到HTML5?')) {
  1568. window.GrayManager.clickMenu('change_h5');// change_flash, change_h5
  1569. }
  1570. }).observe(playerContent, {
  1571. childList: true, // 监听child的增减
  1572. attributes: false, // 监听属性的变化
  1573. });
  1574. }
  1575. }
  1576.  
  1577. util_init(() => {
  1578. // 除了播放器和番剧列表页面, 其他页面都需要检测html5
  1579. if (!(util_page.bangumi() || util_page.bangumi_md() || util_page.player())) {
  1580. checkHtml5()
  1581. }
  1582. })
  1583. return isHtml5Player
  1584. }())
  1585. const balh_feature_runPing = function () {
  1586. var pingOutput = document.getElementById('balh_server_ping');
  1587.  
  1588. var xhr = new XMLHttpRequest(), testUrl = [r.const.server.S0, r.const.server.S1],
  1589. testUrlIndex = 0, isReused = false, prevNow, outputArr = [];
  1590. pingOutput.textContent = '正在进行服务器测速…';
  1591. pingOutput.style.height = '100px';
  1592. xhr.open('GET', '', true);
  1593. xhr.onreadystatechange = function () {
  1594. this.readyState == 4 && pingResult();
  1595. };
  1596. var pingLoop = function () {
  1597. prevNow = performance.now();
  1598. xhr.open('GET', testUrl[testUrlIndex] + '/api/bangumi', true);
  1599. xhr.send();
  1600. };
  1601. var pingResult = function () {
  1602. var duration = (performance.now() - prevNow) | 0;
  1603. if (isReused)
  1604. outputArr.push('\t复用连接:' + duration + 'ms'), isReused = false, testUrlIndex++;
  1605. else
  1606. outputArr.push(testUrl[testUrlIndex] + ':'), outputArr.push('\t初次连接:' + duration + 'ms'), isReused = true;
  1607. pingOutput.textContent = outputArr.join('\n');
  1608. testUrlIndex < testUrl.length ? pingLoop() : pingOutput.appendChild(_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '\n再测一次?')]));
  1609. };
  1610. pingLoop();
  1611. }
  1612. const balh_feature_sign = (function () {
  1613. function isLogin() {
  1614. return localStorage.oauthTime !== undefined
  1615. }
  1616. function clearLoginFlag() {
  1617. delete localStorage.oauthTime
  1618. }
  1619.  
  1620. function updateLoginFlag(loadCallback) {
  1621. util_jsonp(balh_config.server + '/login?act=expiretime')
  1622. .then(() => loadCallback && loadCallback(true))
  1623. // .catch(() => loadCallback && loadCallback(false)) // 请求失败不需要回调
  1624. }
  1625. function isLoginBiliBili() {
  1626. return util_cookie['DedeUserID'] !== undefined
  1627. }
  1628. // 当前在如下情况才会弹一次登录提示框:
  1629. // 1. 第一次使用
  1630. // 2. 主站+服务器都退出登录后, 再重新登录主站
  1631. function checkLoginState() {
  1632. // 给一些状态,设置初始值
  1633. localStorage.balh_must_remind_login_v1 === undefined && (localStorage.balh_must_remind_login_v1 = r.const.TRUE)
  1634.  
  1635. if (isLoginBiliBili()) {
  1636. if (!localStorage.balh_old_isLoginBiliBili // 主站 不登录 => 登录
  1637. || localStorage.balh_pre_server !== balh_config.server // 代理服务器改变了
  1638. || localStorage.balh_must_remind_login_v1) { // 设置了"必须提醒"flag
  1639. clearLoginFlag()
  1640. updateLoginFlag(() => {
  1641. if (!isLogin()) {
  1642. localStorage.balh_must_remind_login_v1 = r.const.FALSE
  1643. util_ui_alert(`${GM_info.script.name}\n要不要考虑进行一下授权?\n\n授权后可以观看区域限定番剧的1080P\n(如果你是大会员或承包过这部番的话)\n\n你可以随时在设置中打开授权页面`, balh_feature_sign.showLogin)
  1644. }
  1645. })
  1646. } else if ((isLogin() && Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000) // 已登录,每天为周期检测key有效期,过期前五天会自动续期
  1647. || localStorage.balh_must_updateLoginFlag) {// 某些情况下,必须更新一次
  1648. updateLoginFlag(() => localStorage.balh_must_updateLoginFlag = r.const.FALSE);
  1649. }
  1650. }
  1651. localStorage.balh_old_isLoginBiliBili = isLoginBiliBili() ? r.const.TRUE : r.const.FALSE
  1652. localStorage.balh_pre_server = balh_config.server
  1653. }
  1654.  
  1655. function showLogin() {
  1656. const loginUrl = balh_config.server + '/login'
  1657. const iframeSrc = 'https://passport.bilibili.com/login?appkey=27eb53fc9058f8c3&api=' + encodeURIComponent(loginUrl) + '&sign=' + hex_md5('api=' + loginUrl + 'c2ed53a74eeefe3cf99fbd01d8c9c375')
  1658. util_ui_popframe(iframeSrc)
  1659. }
  1660.  
  1661. function showLogout() {
  1662. util_ui_popframe(balh_config.server + '/login?act=logout')
  1663. }
  1664.  
  1665. // 监听登录message
  1666. window.addEventListener('message', function (e) {
  1667. switch (e.data) {
  1668. case 'BiliPlus-Login-Success':
  1669. //登入
  1670. localStorage.balh_must_updateLoginFlag = r.const.TRUE
  1671. Promise.resolve('start')
  1672. .then(() => util_jsonp(balh_config.server + '/login?act=getlevel'))
  1673. .then(() => location.reload())
  1674. .catch(() => location.reload())
  1675. break
  1676. case 'BiliPlus-Logout-Success':
  1677. //登出
  1678. clearLoginFlag()
  1679. location.reload()
  1680. break
  1681. }
  1682. })
  1683.  
  1684.  
  1685. util_init(() => {
  1686. if (!(util_page.player() || util_page.av())) {
  1687. checkLoginState()
  1688. }
  1689. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  1690. return {
  1691. showLogin,
  1692. showLogout,
  1693. isLogin,
  1694. isLoginBiliBili,
  1695. }
  1696. }())
  1697. const balh_feature_RedirectToBangumiOrInsertPlayer = (function () {
  1698. // 重定向到Bangumi页面, 或者在当前页面直接插入播放页面
  1699. function tryRedirectToBangumiOrInsertPlayer() {
  1700. let $errorPanel;
  1701. if (!($errorPanel = document.querySelector('.error-container > .error-panel'))) {
  1702. return;
  1703. }
  1704. let msg = document.createElement('a');
  1705. $errorPanel.insertBefore(msg, $errorPanel.firstChild);
  1706. msg.innerText = '获取番剧页Url中...';
  1707. let aid = location.pathname.replace(/.*av(\d+).*/, '$1'),
  1708. page = (location.pathname.match(/\/index_(\d+).html/) || ['', '1'])[1],
  1709. cid,
  1710. season_id,
  1711. episode_id;
  1712. let avData;
  1713. balh_api_plus_view(aid)
  1714. .then(function (data) {
  1715. avData = data;
  1716. if (data.code) {
  1717. return Promise.reject(JSON.stringify(data));
  1718. }
  1719. // 计算当前页面的cid
  1720. for (let i = 0; i < data.list.length; i++) {
  1721. if (data.list[i].page == page) {
  1722. cid = data.list[i].cid;
  1723. break;
  1724. }
  1725. }
  1726. if (!data.bangumi) {
  1727. generatePlayer(data, aid, page, cid)
  1728. // return Promise.reject('该AV号不属于任何番剧页');//No bangumi in api response
  1729. } else {
  1730. // 当前av属于番剧页面, 继续处理
  1731. season_id = data.bangumi.season_id;
  1732. return balh_api_plus_season(season_id);
  1733. }
  1734. })
  1735. .then(function (result) {
  1736. if (result === undefined) return // 上一个then不返回内容时, 不需要处理
  1737. if (result.code === 10) { // av属于番剧页面, 通过接口却未能找到番剧信息
  1738. let ep_id_newest = avData && avData.bangumi && avData.bangumi.newest_ep_id
  1739. if (ep_id_newest) {
  1740. episode_id = ep_id_newest // 此时, 若avData中有最新的ep_id, 则直接使用它
  1741. } else {
  1742. log(`av${aid}属于番剧${season_id}, 但却不能找到番剧页的信息, 试图直接创建播放器`)
  1743. generatePlayer(avData, aid, page, cid)
  1744. return
  1745. }
  1746. } else if (result.code) {
  1747. return Promise.reject(JSON.stringify(result))
  1748. } else {
  1749. let ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid,
  1750. episodes = result.result.episodes,
  1751. ep
  1752. // 为何要用三种不同方式匹配, 详见: https://greasyfork.org/zh-CN/forum/discussion/22379/x#Comment_34127
  1753. for (let i = 0; i < episodes.length; i++) {
  1754. ep = episodes[i]
  1755. if (ep.danmaku == cid) {
  1756. ep_id_by_cid = ep.episode_id
  1757. }
  1758. if (ep.av_id == aid && ep.page == page) {
  1759. ep_id_by_aid_page = ep.episode_id
  1760. }
  1761. if (ep.av_id == aid) {
  1762. ep_id_by_aid = ep.episode_id
  1763. }
  1764. }
  1765. episode_id = ep_id_by_cid || ep_id_by_aid_page || ep_id_by_aid
  1766. }
  1767. if (episode_id) {
  1768. let bangumi_url = `//www.bilibili.com/bangumi/play/ss${season_id}#${episode_id}`
  1769. log('Redirect', 'aid:', aid, 'page:', page, 'cid:', cid, '==>', bangumi_url, 'season_id:', season_id, 'ep_id:', episode_id)
  1770. msg.innerText = '即将跳转到:' + bangumi_url
  1771. location.href = bangumi_url
  1772. } else {
  1773. return Promise.reject('查询episode_id失败')
  1774. }
  1775. })
  1776. .catch(function (e) {
  1777. log('error:', arguments);
  1778. msg.innerText = 'error:' + e;
  1779. });
  1780. }
  1781.  
  1782. function generatePlayer(data, aid, page, cid) {
  1783. let generateSrc = function (aid, cid) {
  1784. return `//www.bilibili.com/blackboard/html5player.html?cid=${cid}&aid=${aid}&player_type=1`;
  1785. }
  1786. let generatePageList = function (pages) {
  1787. let $curPage = null;
  1788. function onPageBtnClick(e) {
  1789. e.target.className = 'curPage'
  1790. $curPage && ($curPage.className = '')
  1791.  
  1792. let index = e.target.attributes['data-index'].value;
  1793. iframe.src = generateSrc(aid, pages[index].cid);
  1794. }
  1795.  
  1796. return pages.map(function (item, index) {
  1797. let isCurPage = item.page == page
  1798. let $item = _('a', { 'data-index': index, className: isCurPage ? 'curPage' : '', event: { click: onPageBtnClick } }, [_('text', item.page + ': ' + item.part)])
  1799. if (isCurPage) $curPage = $item
  1800. return $item
  1801. });
  1802. }
  1803. // 当前av不属于番剧页面, 直接在当前页面插入一个播放器的iframe
  1804. let $pageBody = document.querySelector('.b-page-body');
  1805. if (!$pageBody) { // 若不存在, 则创建
  1806. $pageBody = _('div', { className: '.b-page-body' });
  1807. document.querySelector('.error-body').parentNode.appendChild($pageBody)
  1808. // 添加相关样式
  1809. document.head.appendChild(_('link', { type: 'text/css', rel: 'stylesheet', href: '//static.hdslb.com/css/core-v5/page-core.css' }))
  1810. }
  1811. let iframe = _('iframe', { className: 'player bilibiliHtml5Player', style: { position: 'relative' }, src: generateSrc(aid, cid) });
  1812.  
  1813. // 添加播放器
  1814. $pageBody.appendChild(_('div', { className: 'player-wrapper' }, [
  1815. _('div', { className: 'main-inner' }, [
  1816. _('div', { className: 'v-plist' }, [
  1817. _('div', { id: 'plist', className: 'plist-content open' }, generatePageList(data.list))
  1818. ])
  1819. ]),
  1820. _('div', { id: 'bofqi', className: 'scontent' }, [iframe])
  1821. ]));
  1822. // 添加评论区
  1823. $pageBody.appendChild(_('div', { className: 'main-inner' }, [
  1824. _('div', { className: 'common report-scroll-module report-wrap-module', id: 'common_report' }, [
  1825. _('div', { className: 'b-head' }, [
  1826. _('span', { className: 'b-head-t results' }),
  1827. _('span', { className: 'b-head-t' }, [_('text', '评论')]),
  1828. _('a', { className: 'del-log', href: `//www.bilibili.com/replydeletelog?aid=${aid}&title=${data.title}`, target: '_blank' }, [_('text', '查看删除日志')])
  1829. ]),
  1830. _('div', { className: 'comm', id: 'bbComment' }, [
  1831. _('div', { id: 'load_comment', className: 'comm_open_btn', onclick: "var fb = new bbFeedback('.comm', 'arc');fb.show(" + aid + ", 1);", style: { cursor: 'pointer' } })
  1832. ])
  1833. ])
  1834. ]));
  1835. // 添加包含bbFeedback的js
  1836. document.head.appendChild(_('script', { type: 'text/javascript', src: '//static.hdslb.com/js/core-v5/base.core.js' }))
  1837.  
  1838. document.title = data.title;
  1839. (document.querySelector('.error-body') || document.querySelector('.error-container')).remove(); // 移除错误信息面板
  1840. }
  1841.  
  1842. util_init(() => {
  1843. if (util_page.av()) {
  1844. tryRedirectToBangumiOrInsertPlayer()
  1845. }
  1846. })
  1847. return true // 随便返回一个值...
  1848. }())
  1849. const balh_feature_FillSeasonList = (function () {
  1850. function tryFillSeasonList() {
  1851. var error_container, season_id;
  1852. if (!(error_container = document.querySelector('div.error-container'))) {
  1853. return;
  1854. }
  1855. if (!(season_id = window.location.pathname.match(/^\/anime\/(\d+)\/?$/)[1])) {
  1856. return;
  1857. }
  1858.  
  1859. //尝试解决怪异模式渲染
  1860. /*
  1861. 会造成变量丢失,等待官方重写doctype
  1862. try{
  1863. window.stop();
  1864. var xhr = new XMLHttpRequest();
  1865. xhr.open('GET',location.href,false);
  1866. xhr.send();
  1867. document.head.appendChild(_('script',{},[_('text',
  1868. 'document.write(unescape("'+escape(xhr.response.replace(/<!DOCTYPE.+?>/,'<!DOCTYPE HTML>'))+'"));window.stop()'
  1869. )]));
  1870. }catch(e){util_error(e);}
  1871. */
  1872.  
  1873. var msg = _('a', { href: '//bangumi.bilibili.com/anime/' + season_id + '/play', style: { fontSize: '20px' } }, [_('text', `【${GM_info.script.name}】尝试获取视频列表中...`)]),
  1874. content = _('div');
  1875.  
  1876. error_container.insertBefore(content, error_container.firstChild);
  1877. content.appendChild(msg);
  1878. log('season>:', season_id);
  1879. balh_api_plus_season(season_id)
  1880. .then(function (data) {
  1881. log('season>then:', data);
  1882. if (data.code) {
  1883. return Promise.reject(data);
  1884. }
  1885.  
  1886. function generateEpisodeList(episodes) {
  1887. var childs = [];
  1888. episodes.reverse().forEach(function (i) {
  1889. childs.push(_('li', { className: 'v1-bangumi-list-part-child', 'data-episode-id': i.episode_id }, [_('a', { className: 'v1-complete-text', href: '//bangumi.bilibili.com/anime/' + season_id + '/play#' + i.episode_id, title: i.index + ' ' + i.index_title, target: '_blank', style: { height: '60px' } }, [
  1890. _('div', { className: 'img-wrp' }, [_('img', { src: i.cover, style: { opacity: 1 }, loaded: 'loaded', alt: i.index + ' ' + i.index_title })]),
  1891. _('div', { className: 'text-wrp' }, [
  1892. _('div', { className: 'text-wrp-num' }, [_('div', { className: 'text-wrp-num-content' }, [_('text', `第${i.index}话`)])]),
  1893. _('div', { className: 'text-wrp-title trunc' }, [_('text', i.index_title)])
  1894. ])
  1895. ])]));
  1896. });
  1897. return childs;
  1898. }
  1899.  
  1900. function generateSeasonList(seasons) {
  1901. function onSeasonClick(event) {
  1902. window.location.href = '//bangumi.bilibili.com/anime/' + event.target.attributes['data-season-id'].value;
  1903. }
  1904.  
  1905. return seasons.map(function (season) {
  1906. return _('li', { className: season.season_id == season_id ? 'cur' : '', 'data-season-id': season.season_id, event: { click: onSeasonClick } }, [_('text', season.title)]);
  1907. });
  1908. }
  1909.  
  1910. if (data.result) {
  1911. document.title = data.result.title;
  1912. document.head.appendChild(_('link', { href: 'https://s3.hdslb.com/bfs/static/anime/css/tag-index.css?v=110', rel: 'stylesheet' }));
  1913. document.head.appendChild(_('link', { href: 'https://s1.hdslb.com/bfs/static/anime/css/bangumi-index.css?v=110', rel: 'stylesheet' }));
  1914. document.body.insertBefore(_('div', { className: 'main-container-wrapper' }, [_('div', { className: 'main-container' }, [
  1915. _('div', { className: 'page-info-wrp' }, [_('div', { className: 'bangumi-info-wrapper' }, [
  1916. _('div', { className: 'bangumi-info-blurbg-wrapper' }, [_('div', { className: 'bangumi-info-blurbg blur', style: { backgroundImage: 'url(' + data.result.cover + ')' } })]),
  1917. _('div', { className: 'main-inner' }, [_('div', { className: 'info-content' }, [
  1918. _('div', { className: 'bangumi-preview' }, [_('img', { alt: data.result.title, src: data.result.cover })]),
  1919. _('div', { className: 'bangumi-info-r' }, [
  1920. _('div', { className: 'b-head' }, [_('h1', { className: 'info-title', 'data-seasonid': season_id, title: data.result.title }, [_('text', data.result.title)])]),
  1921. _('div', { className: 'info-count' }, [
  1922. _('span', { className: 'info-count-item info-count-item-play' }, [_('span', { className: 'info-label' }, [_('text', '总播放')]), _('em', {}, [_('text', data.result.play_count)])]),
  1923. _('span', { className: 'info-count-item info-count-item-fans' }, [_('span', { className: 'info-label' }, [_('text', '追番人数')]), _('em', {}, [_('text', data.result.favorites)])]),
  1924. _('span', { className: 'info-count-item info-count-item-review' }, [_('span', { className: 'info-label' }, [_('text', '弹幕总数')]), _('em', {}, [_('text', data.result.danmaku_count)])])
  1925. ]),
  1926. //_('div',{className:'info-row info-update'},[]),
  1927. //_('div',{className:'info-row info-cv'},[]),
  1928. _('div', { className: 'info-row info-desc-wrp' }, [
  1929. _('div', { className: 'info-row-label' }, [_('text', '简介:')]),
  1930. _('div', { className: 'info-desc' }, [_('text', data.result.evaluate)])
  1931. ]),
  1932. ])
  1933. ])])
  1934. ])]),
  1935. _('div', { className: 'main-inner' }, [_('div', { className: 'v1-bangumi-list-wrapper clearfix' }, [
  1936. _('div', { className: 'v1-bangumi-list-season-wrapper' }, [
  1937. _('div', { className: 'v1-bangumi-list-season-content slider-list-content' }, [
  1938. _('div', {}, [
  1939. _('ul', { className: 'v1-bangumi-list-season clearfix slider-list', 'data-current-season-id': season_id, style: { opacity: 1 } }, generateSeasonList(data.result.seasons))
  1940. ])
  1941. ])
  1942. ]),
  1943. _('div', { className: 'v1-bangumi-list-part-wrapper slider-part-wrapper' }, [_('div', { className: 'v1-bangumi-list-part clearfix', 'data-current-season-id': season_id, style: { display: 'block' } }, [
  1944. _('div', { className: 'complete-list', style: { display: 'block' } }, [_('div', { className: 'video-slider-list-wrapper' }, [_('div', { className: 'slider-part-wrapper' }, [_('ul', { className: 'slider-part clearfix hide', style: { display: 'block' } }, generateEpisodeList(data.result.episodes))])])])
  1945. ])])
  1946. ])])
  1947. ])]), msg.parentNode.parentNode);
  1948. msg.parentNode.parentNode.remove();
  1949. }
  1950. })
  1951. .catch(function (error) {
  1952. log('season>catch', error);
  1953. msg.innerText = 'error:' + JSON.stringify(error) + '\n点击跳转到播放界面 (不一定能够正常播放...)';
  1954. });
  1955. }
  1956.  
  1957. util_init(() => {
  1958. if (util_page.bangumi()) {
  1959. tryFillSeasonList()
  1960. }
  1961. })
  1962. return true
  1963. }())
  1964.  
  1965. const balh_ui_setting = (function () {
  1966. function addSettingsButton() {
  1967. var indexNav = document.getElementById('index_nav') || document.querySelector('.bangumi-nav-right') || document.querySelector('#fixnav_report'),
  1968. bottom = '110px',
  1969. size = '46px', settingBtnSvgContainer;
  1970. if (indexNav == null) {
  1971. // 信息页添加到按钮右侧
  1972. if (util_page.bangumi_md()) {
  1973. indexNav = document.querySelector('.media-info-btns');
  1974. indexNav.appendChild(_('style', {}, [_('text', `
  1975. #balh-settings-btn {
  1976. float: left;
  1977. margin: 3px 0 0 20px;
  1978. height: 44px;
  1979. width: 44px;
  1980. background: #FFF;
  1981. border-radius: 10px;
  1982. cursor: pointer;
  1983. }
  1984. #balh-settings-btn:hover {
  1985. background: #00a1d6;
  1986. border-color: #00a1d6;
  1987. }
  1988. #balh-settings-btn>:first-child {
  1989. text-align: center;
  1990. height: 100%;
  1991. }
  1992. #balh-settings-btn .icon-saturn {
  1993. width: 30px;
  1994. height: ${size};
  1995. fill: rgb(153,162,170);
  1996. }
  1997. #balh-settings-btn:hover .icon-saturn {
  1998. fill: white;
  1999. }
  2000. `)]))
  2001. settingBtnSvgContainer = indexNav.appendChild(_('div', { id: 'balh-settings-btn', title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', {})])).firstChild;
  2002. }
  2003. } else {
  2004. // 视频页添加到回顶部下方
  2005. window.dispatchEvent(new Event('resize'));
  2006. indexNav.style.display = 'block';
  2007. indexNav.appendChild(_('style', {}, [_('text', `
  2008. #balh-settings-btn {
  2009. bottom: 110px;
  2010. border: 1px solid #e5e9ef;
  2011. border-radius: 4px;
  2012. background: #f6f9fa;
  2013. margin-top: 4px;
  2014. width: ${size};
  2015. height: ${size};
  2016. cursor: pointer;
  2017. }
  2018. #balh-settings-btn:hover {
  2019. background: #00a1d6;
  2020. border-color: #00a1d6;
  2021. }
  2022. #balh-settings-btn .btn-gotop {
  2023. text-align: center;
  2024. }
  2025. #balh-settings-btn .icon-saturn {
  2026. width: 30px;
  2027. height: ${size};
  2028. fill: rgb(153,162,170);
  2029. }
  2030. #balh-settings-btn:hover .icon-saturn {
  2031. fill: white;
  2032. }
  2033. `)]))
  2034. settingBtnSvgContainer = indexNav.appendChild(_('div', { id: 'balh-settings-btn', title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', { className: 'btn-gotop' })])).firstChild;
  2035. }
  2036. settingBtnSvgContainer && (settingBtnSvgContainer.innerHTML = `<!-- https://www.flaticon.com/free-icon/saturn_53515 --><svg class="icon-saturn" viewBox="0 0 612.017 612.017"><path d="M596.275,15.708C561.978-18.59,478.268,5.149,380.364,68.696c-23.51-7.384-48.473-11.382-74.375-11.382c-137.118,0-248.679,111.562-248.679,248.679c0,25.902,3.998,50.865,11.382,74.375C5.145,478.253-18.575,561.981,15.724,596.279c34.318,34.318,118.084,10.655,216.045-52.949c23.453,7.365,48.378,11.344,74.241,11.344c137.137,0,248.679-111.562,248.679-248.68c0-25.862-3.979-50.769-11.324-74.24C606.931,133.793,630.574,50.026,596.275,15.708zM66.435,545.53c-18.345-18.345-7.919-61.845,23.338-117.147c22.266,39.177,54.824,71.716,94.02,93.943C128.337,553.717,84.837,563.933,66.435,545.53z M114.698,305.994c0-105.478,85.813-191.292,191.292-191.292c82.524,0,152.766,52.605,179.566,125.965c-29.918,41.816-68.214,87.057-113.015,131.839c-44.801,44.819-90.061,83.116-131.877,113.034C167.303,458.76,114.698,388.479,114.698,305.994z M305.99,497.286c-3.156,0-6.236-0.325-9.354-0.459c35.064-27.432,70.894-58.822,106.11-94.059c35.235-35.235,66.646-71.046,94.058-106.129c0.153,3.118,0.479,6.198,0.479,9.354C497.282,411.473,411.469,497.286,305.99,497.286z M428.379,89.777c55.303-31.238,98.803-41.683,117.147-23.338c18.402,18.383,8.187,61.902-23.204,117.377C500.095,144.62,467.574,112.043,428.379,89.777z"/></svg>`);
  2037. }
  2038.  
  2039. function _showSettings() {
  2040. document.body.appendChild(settingsDOM);
  2041. var form = settingsDOM.querySelector('form');
  2042. // elements包含index的属性, 和以name命名的属性, 其中以name命名的属性是不可枚举的, 只能通过这种方式获取出来
  2043. Object.getOwnPropertyNames(form.elements).forEach(function (name) {
  2044. if (name.startsWith('balh_')) {
  2045. var key = name.replace('balh_', '')
  2046. var ele = form.elements[name]
  2047. if (ele.type === 'checkbox') {
  2048. ele.checked = balh_config[key];
  2049. } else {
  2050. ele.value = balh_config[key];
  2051. }
  2052. }
  2053. })
  2054. document.body.style.overflow = 'hidden';
  2055. }
  2056.  
  2057. // 往顶层窗口发显示设置的请求
  2058. function showSettings() {
  2059. window.top.postMessage('balh-show-setting', '*')
  2060. }
  2061.  
  2062. // 只有顶层窗口才接收请求
  2063. if (window === window.top) {
  2064. window.addEventListener('message', (event) => {
  2065. if (event.data === 'balh-show-setting') {
  2066. _showSettings();
  2067. $('#upos-server')[0].value = balh_config.upos_server || '';
  2068. }
  2069. })
  2070. }
  2071.  
  2072. function onSignClick(event) {
  2073. settingsDOM.click();
  2074. switch (event.target.attributes['data-sign'].value) {
  2075. default:
  2076. case 'in':
  2077. balh_feature_sign.showLogin();
  2078. break;
  2079. case 'out':
  2080. balh_feature_sign.showLogout();
  2081. break;
  2082. }
  2083. }
  2084.  
  2085. function onSettingsFormChange(e) {
  2086. var name = e.target.name;
  2087. var value = e.target.type === 'checkbox' ? (e.target.checked ? r.const.TRUE : r.const.FALSE) : e.target.value
  2088. balh_config[name.replace('balh_', '')] = value
  2089. log(name, ' => ', value);
  2090. }
  2091.  
  2092. // 第一次点击时:
  2093. // 1. '复制日志&问题反馈' => '复制日志'
  2094. // 2. 显示'问题反馈'
  2095. // 3. 复制成功后请求跳转到GitHub
  2096. // 之后的点击, 只是正常的复制功能~~
  2097. function onCopyClick(event) {
  2098. let issueLink = document.getElementById('balh-issue-link')
  2099. let continueToIssue = issueLink.style.display === 'none'
  2100. if (continueToIssue) {
  2101. issueLink.style.display = 'inline'
  2102. let copyBtn = document.getElementById('balh-copy-log')
  2103. copyBtn.innerText = '复制日志'
  2104. }
  2105.  
  2106. let textarea = document.getElementById('balh-textarea-copy')
  2107. textarea.style.display = 'inline-block'
  2108. if (util_ui_copy(util_log_hub.getAllMsg(), textarea)) {
  2109. textarea.style.display = 'none'
  2110. util_ui_msg.show($(this),
  2111. continueToIssue ? '复制日志成功; 点击确定, 继续提交问题(需要GitHub帐号)\n请把日志粘贴到问题描述中' : '复制成功',
  2112. continueToIssue ? 0 : 3e3,
  2113. continueToIssue ? 'button' : undefined,
  2114. continueToIssue ? () => window.open(r.url.issue) : undefined)
  2115. } else {
  2116. util_ui_msg.show($(this), '复制失败, 请从下面的文本框手动复制', 5e3)
  2117. }
  2118. }
  2119.  
  2120. let printSystemInfoOk = false
  2121.  
  2122. // 鼠标移入设置底部的时候, 打印一些系统信息, 方便问题反馈
  2123. function onMouseEnterSettingBottom(event) {
  2124. if (!printSystemInfoOk) {
  2125. printSystemInfoOk = true
  2126. util_debug('userAgent', navigator.userAgent)
  2127. }
  2128. }
  2129.  
  2130. var settingsDOM = _('div', { id: 'balh-settings', style: { position: 'fixed', top: 0, bottom: 0, left: 0, right: 0, background: 'rgba(0,0,0,.7)', animationName: 'balh-settings-bg', animationDuration: '.5s', zIndex: 10000, cursor: 'pointer' }, event: { click: function (e) { if (e.target === this) util_ui_msg.close(), document.body.style.overflow = '', this.remove(); } } }, [
  2131. _('style', {}, [_('text', r.css.settings)]),
  2132. _('div', { style: { position: 'absolute', background: '#FFF', borderRadius: '10px', padding: '20px', top: '50%', left: '50%', width: '600px', transform: 'translate(-50%,-50%)', cursor: 'default' } }, [
  2133. _('h1', {}, [_('text', `${GM_info.script.name} v${GM_info.script.version} 参数设置`)]),
  2134. _('br'),
  2135. _('form', { id: 'balh-settings-form', event: { change: onSettingsFormChange } }, [
  2136. _('text', '代理服务器:'), _('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '测速')]), _('br'),
  2137. _('div', { style: { display: 'flex' } }, [
  2138. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: r.const.server.S0 }), _('text', '默认代理服务器(土豆服)')]),
  2139. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: r.const.server.S1 }), _('text', '备选代理服务器(更稳定)')]),
  2140. ]), _('br'),
  2141. _('div', { id: 'balh_server_ping', style: { whiteSpace: 'pre-wrap', overflow: 'auto' } }, []),
  2142. _('text', 'upos服务器:'), _('br'),
  2143. _('div', { title: '变更后 切换清晰度 或 刷新 生效' }, [
  2144. _('input', { style: { visibility: 'hidden' }, type: 'checkbox' }),
  2145. _('text', '替换upos视频服务器:'),
  2146. _('select', {
  2147. id: 'upos-server',
  2148. event: {
  2149. change: function () {
  2150. let server = this.value;
  2151. let message = $('#upos-server-message');
  2152. let clearMsg = function () { message.text('') }
  2153. message.text('保存中...')
  2154. $.ajax(balh_config.server + '/api/setUposServer?server=' + server, {
  2155. xhrFields: { withCredentials: true },
  2156. dataType: 'json',
  2157. success: function (json) {
  2158. if (json.code == 0) {
  2159. message.text('已保存');
  2160. setTimeout(clearMsg, 3e3);
  2161. balh_config.upos_server = server;
  2162. }
  2163. },
  2164. error: function () {
  2165. message.text('保存出错');
  2166. setTimeout(clearMsg, 3e3);
  2167. }
  2168. })
  2169. }
  2170. }
  2171. }, [
  2172. _('option', { value: "" }, [_('text', '不替换')]),
  2173. _('option', { value: "ks3" }, [_('text', 'ks3(金山)')]),
  2174. _('option', { value: "oss" }, [_('text', 'oss(阿里)')]),
  2175. _('option', { value: "kodo" }, [_('text', 'kodo(七牛)')]),
  2176. _('option', { value: "cos" }, [_('text', 'cos(腾讯)')]),
  2177. _('option', { value: "bos" }, [_('text', 'bos(百度)')])
  2178. ]),
  2179. _('span', { 'id': 'upos-server-message' })
  2180. ]), _('br'),
  2181. _('text', '脚本工作模式:'), _('br'),
  2182. _('div', { style: { display: 'flex' } }, [
  2183. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.DEFAULT }), _('text', '默认:自动判断')]),
  2184. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.REPLACE }), _('text', '替换:在需要时处理番剧')]),
  2185. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.REDIRECT }), _('text', '重定向:完全代理所有番剧')])
  2186. ]), _('br'),
  2187. _('text', '其他:'), _('br'),
  2188. _('div', { style: { display: 'flex' } }, [
  2189. _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_blocked_vip' }), _('text', '被永封的大会员'), _('a', { href: 'https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md#大会员账号被b站永封了', target: '_blank' }, [_('text', '(?)')])]),
  2190. _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_enable_in_av' }), _('text', '在AV页面启用'), _('a', { href: 'https://github.com/ipcjs/bilibili-helper/issues/172', target: '_blank' }, [_('text', '(?)')])]),
  2191. _('div', { style: { flex: 1, display: 'flex' } }, [
  2192. _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_remove_pre_ad' }), _('text', '去前置广告')]),
  2193. // _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_flv_prefer_ws' }), _('text', '优先使用ws')]),
  2194. ])
  2195. ]), _('br'),
  2196. _('a', { href: 'javascript:', 'data-sign': 'in', event: { click: onSignClick } }, [_('text', '帐号授权')]),
  2197. _('text', ' '),
  2198. _('a', { href: 'javascript:', 'data-sign': 'out', event: { click: onSignClick } }, [_('text', '取消授权')]),
  2199. _('text', '  '),
  2200. _('a', { href: 'javascript:', event: { click: function () { util_ui_msg.show($(this), '如果你的帐号进行了付费,不论是大会员还是承包,\n进行授权之后将可以在解除限制时正常享有这些权益\n\n你可以随时在这里授权或取消授权\n\n不进行授权不会影响脚本的正常使用,但可能会缺失1080P', 1e4); } } }, [_('text', '(这是什么?)')]),
  2201. _('br'), _('br'),
  2202. _('div', { style: { whiteSpace: 'pre-wrap' }, event: { mouseenter: onMouseEnterSettingBottom } }, [
  2203. _('a', { href: 'https://greasyfork.org/zh-CN/scripts/25718-%E8%A7%A3%E9%99%A4b%E7%AB%99%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6', target: '_blank' }, [_('text', '脚本主页')]),
  2204. _('text', ' '),
  2205. _('a', { href: 'https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md', target: '_blank' }, [_('text', '帮助说明')]),
  2206. _('text', ' '),
  2207. _('a', { id: 'balh-copy-log', href: 'javascript:;', event: { click: onCopyClick } }, [_('text', '复制日志&问题反馈')]),
  2208. _('text', ' '),
  2209. _('a', { id: 'balh-issue-link', href: r.url.issue, target: '_blank', style: { display: 'none' } }, [_('text', '问题反馈')]),
  2210. _('text', '作者: ipcjs esterTion FlandreDaisuki 接口:BiliPlus')
  2211. ]),
  2212. _('textarea', { id: 'balh-textarea-copy', style: { display: 'none' } })
  2213. ])
  2214. ])
  2215. ]);
  2216.  
  2217. util_init(() => {
  2218. if (!(util_page.player() || (util_page.av() && !balh_config.enable_in_av))) {
  2219. if (!util_page.av()) { // av页面添加这个按钮不知道为啥页面会混乱...屏蔽掉(;¬_¬)
  2220. addSettingsButton()
  2221. }
  2222. }
  2223. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  2224. return {
  2225. dom: settingsDOM,
  2226. show: showSettings,
  2227. }
  2228. }())
  2229.  
  2230. const balh_jump_to_baipiao = (function () {
  2231. function main() {
  2232. for (let bp of r.baipiao) {
  2233. const cookie_key = `balh_baipao_${bp.key}`
  2234. if (bp.match() && !util_cookie[cookie_key]) {
  2235. util_ui_pop({
  2236. content: [
  2237. _('text', '发现白嫖地址: '), _('a', { href: bp.link }, bp.link),
  2238. _('div', {}, bp.message),
  2239. ],
  2240. confirmBtn: '一键跳转',
  2241. onConfirm: () => { location.href = bp.link },
  2242. onClose: () => { util_cookie.set(cookie_key, r.const.TRUE, '') }
  2243. })
  2244. break
  2245. }
  2246. }
  2247. }
  2248. util_init(() => {
  2249. main()
  2250. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  2251. }())
  2252.  
  2253. function main() {
  2254. util_log(
  2255. 'mode:', balh_config.mode,
  2256. 'blocked_vip:', balh_config.blocked_vip,
  2257. 'server:', balh_config.server,
  2258. 'upos_server:', balh_config.upos_server,
  2259. 'flv_prefer_ws:', balh_config.flv_prefer_ws,
  2260. 'remove_pre_ad:', balh_config.remove_pre_ad,
  2261. 'readyState:', document.readyState,
  2262. 'isLogin:', balh_feature_sign.isLogin(),
  2263. 'isLoginBiliBili:', balh_feature_sign.isLoginBiliBili()
  2264. )
  2265. // 暴露接口
  2266. window.bangumi_area_limit_hack = {
  2267. setCookie: util_cookie.set,
  2268. getCookie: util_cookie.get,
  2269. login: balh_feature_sign.showLogin,
  2270. logout: balh_feature_sign.showLogout,
  2271. getAllMsg: util_log_hub.getAllMsg,
  2272. _clear_local_value: function () {
  2273. delete localStorage.oauthTime
  2274. delete localStorage.balh_h5_not_first
  2275. delete localStorage.balh_old_isLoginBiliBili
  2276. delete localStorage.balh_must_remind_login_v1
  2277. delete localStorage.balh_must_updateLoginFlag
  2278. }
  2279. }
  2280. }
  2281.  
  2282. main();
  2283. }
  2284.  
  2285. scriptSource(GM_info.scriptHandler);