解除B站区域限制

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

目前为 2020-09-16 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 解除B站区域限制
  3. // @namespace http://tampermonkey.net/
  4. // @version 7.9.5
  5. // @description 通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效;
  6. // @author ipcjs
  7. // @supportURL https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md
  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/video/BV*
  14. // @include *://www.bilibili.com/bangumi/play/ep*
  15. // @include *://www.bilibili.com/bangumi/play/ss*
  16. // @include *://m.bilibili.com/bangumi/play/ep*
  17. // @include *://m.bilibili.com/bangumi/play/ss*
  18. // @include *://bangumi.bilibili.com/anime/*
  19. // @include *://bangumi.bilibili.com/movie/*
  20. // @include *://www.bilibili.com/bangumi/media/md*
  21. // @include *://www.bilibili.com/blackboard/html5player.html*
  22. // @include https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png*
  23. // @run-at document-start
  24. // @grant none
  25. // ==/UserScript==
  26.  
  27. 'use strict';
  28. const log = console.log.bind(console, 'injector:')
  29.  
  30. if (location.href.match(/^https:\/\/www\.mcbbs\.net\/template\/mcbbs\/image\/special_photo_bg\.png/) != null) {
  31. if (location.href.match('access_key') != null && window.opener != null) {
  32. window.stop();
  33. document.children[0].innerHTML = '<title>BALH - 授权</title><meta charset="UTF-8" name="viewport" content="width=device-width">正在跳转……';
  34. window.opener.postMessage('balh-login-credentials: ' + location.href, '*');
  35. }
  36. return;
  37. }
  38.  
  39. function injector() {
  40. if (document.getElementById('balh-injector-source')) {
  41. log(`脚本已经注入过, 不需要执行`)
  42. return
  43. }
  44. // @require https://static.hdslb.com/js/md5.js
  45. GM_info.scriptMetaStr.replace(new RegExp('// @require\\s+https?:(//.*)'), (match, /*p1:*/url) => {
  46. log('@require:', url)
  47. let $script = document.createElement('script')
  48. $script.className = 'balh-injector-require'
  49. $script.setAttribute('type', 'text/javascript')
  50. $script.setAttribute('src', url)
  51. document.head.appendChild($script)
  52. return match
  53. })
  54. let $script = document.createElement('script')
  55. $script.id = 'balh-injector-source'
  56. $script.appendChild(document.createTextNode(`
  57. ;(function(GM_info){
  58. ${scriptSource.toString()}
  59. ${scriptSource.name}('${GM_info.scriptHandler}.${injector.name}')
  60. })(${JSON.stringify(GM_info)})
  61. `))
  62. document.head.appendChild($script)
  63. log('注入完成')
  64. }
  65.  
  66. if (!Object.getOwnPropertyDescriptor(window, 'XMLHttpRequest').writable) {
  67. log('XHR对象不可修改, 需要把脚本注入到页面中', GM_info.script.name, location.href, document.readyState)
  68. injector()
  69. return
  70. }
  71.  
  72. /** 脚本的主体部分, 在GM4中, 需要把这个函数转换成字符串, 注入到页面中, 故不要引用外部的变量 */
  73. function scriptSource(invokeBy) {
  74. 'use strict';
  75. let log = console.log.bind(console, 'injector:')
  76. if (document.getElementById('balh-injector-source') && invokeBy === GM_info.scriptHandler) {
  77. // 当前, 在Firefox+GM4中, 当返回缓存的页面时, 脚本会重新执行, 并且此时XMLHttpRequest是可修改的(为什么会这样?) + 页面中存在注入的代码
  78. // 导致scriptSource的invokeBy直接是GM4...
  79. log(`页面中存在注入的代码, invokeBy却等于${GM_info.scriptHandler}, 这种情况不合理, 终止脚本执行`)
  80. return
  81. }
  82. if (document.readyState === 'uninitialized') { // Firefox上, 对于ifame中执行的脚本, 会出现这样的状态且获取到的href为about:blank...
  83. log('invokeBy:', invokeBy, 'readState:', document.readyState, 'href:', location.href, '需要等待进入loading状态')
  84. setTimeout(() => scriptSource(invokeBy + '.timeout'), 0) // 这里会暴力执行多次, 直到状态不为uninitialized...
  85. return
  86. }
  87.  
  88. const r_text = {
  89. ok: { en: 'OK', zh_cn: '确定', },
  90. close: { en: 'Close', zh_cn: '关闭' },
  91. welcome_to_acfun: '<p><b>缺B乐 了解下?</b></p>',
  92. version_remind: ``,
  93. }
  94. const _t = (key) => {
  95. const text = r_text[key]
  96. const lang = 'zh_cn'
  97. return typeof text === 'string' ? text : text[lang]
  98. }
  99.  
  100. const r = {
  101. html: {},
  102. css: {
  103. 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;} ',
  104. },
  105. attr: {},
  106. url: {
  107. issue: 'https://github.com/ipcjs/bilibili-helper/issues',
  108. issue_new: 'https://github.com/ipcjs/bilibili-helper/issues/new',
  109. readme: 'https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md#%E8%A7%A3%E9%99%A4b%E7%AB%99%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6',
  110. },
  111. script: {
  112. is_dev: GM_info.script.name.includes('.dev'),
  113. },
  114. const: {
  115. mode: {
  116. DEFAULT: 'default',// 默认模式, 自动判断使用何种模式, 推荐;
  117. REPLACE: 'replace', // 替换模式, 替换有区域限制的视频的接口的返回值;
  118. REDIRECT: 'redirect',// 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
  119. },
  120. server: {
  121. S0: 'https://biliplus.ipcjs.top',
  122. S1: 'https://www.biliplus.com',
  123. CUSTOM: '__custom__',
  124. defaultServer: function () {
  125. return this.S1
  126. },
  127. },
  128. TRUE: 'Y',
  129. FALSE: '',
  130. },
  131. baipiao: [
  132. { 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 },
  133. { 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 },
  134. ]
  135. }
  136. const util_stringify = (item) => {
  137. if (typeof item === 'object') {
  138. try {
  139. return JSON.stringify(item)
  140. } catch (e) {
  141. console.debug(e)
  142. return item.toString()
  143. }
  144. } else {
  145. return item
  146. }
  147. }
  148. const util_arr_stringify = function (arr) {
  149. return arr.map(util_stringify).join(' ')
  150. }
  151.  
  152. const util_str_multiply = function (str, multiplier) {
  153. let result = ''
  154. for (let i = 0; i < multiplier; i++) {
  155. result += str
  156. }
  157. return result
  158. }
  159. const util_str_to_c_like = (str) => {
  160. return str.replace(/[A-Z]/g, (a) => `_${a.toLowerCase()}`).replace(/^_/, "")
  161. }
  162. // https://gf.qytechs.cn/zh-CN/scripts/398535-bv2av/code
  163. const util_str_bv2aid = function (bv) {
  164. var table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF';
  165. var tr = {};
  166. for (var i = 0; i < 58; ++i) {
  167. tr[table[i]] = i;
  168. }
  169.  
  170. var s = [11, 10, 3, 8, 4, 6];
  171. var xor = 177451812;
  172. var add = 8728348608;
  173.  
  174. var r = 0;
  175. for (var i = 0; i < 6; ++i) {
  176. r += tr[bv[s[i]]] * (Math.pow(58, i));
  177. }
  178. return String((r - add) ^ xor);
  179. }
  180. const util_obj_key_to_c_like = (obj) => {
  181. // log(typeof obj, Array.isArray(obj), obj)
  182. if (Array.isArray(obj)) {
  183. for (const item of obj) {
  184. util_obj_key_to_c_like(item)
  185. }
  186. } else if (typeof obj === 'object') {
  187. for (const key of Object.keys(obj)) {
  188. const value = obj[key]
  189. util_obj_key_to_c_like(value)
  190. obj[util_str_to_c_like(key)] = value
  191. }
  192. }
  193. return obj // 该方法会修改传入的obj的内容, 返回obj只是为了调用方便...
  194. }
  195. const _raw = (str) => str.replace(/(\.|\?)/g, '\\$1')
  196. const util_regex_url = (url) => new RegExp(`^(https?:)?//${_raw(url)}`)
  197. const util_regex_url_path = (path) => new RegExp(`^(https?:)?//[\\w\\-\\.]+${_raw(path)}`)
  198.  
  199. const util_log_hub = (function () {
  200. const tag = GM_info.script.name + '.msg'
  201.  
  202. // 计算"楼层", 若当前window就是顶层的window, 则floor为0, 以此类推
  203. function computefloor(w = window, floor = 0) {
  204. if (w === window.top) {
  205. return floor
  206. } else {
  207. return computefloor(w.parent, floor + 1)
  208. }
  209. }
  210.  
  211. let floor = computefloor()
  212. let msgList = []
  213. if (floor === 0) { // 只有顶层的Window才需要收集日志
  214. window.addEventListener('message', (event) => {
  215. if (event.data instanceof Array && event.data[0] === tag) {
  216. let [/*tag*/, fromFloor, msg] = event.data
  217. msgList.push(util_str_multiply(' ', fromFloor) + msg)
  218. }
  219. })
  220. }
  221. return {
  222. msg: function (msg) {
  223. window.top.postMessage([tag, floor, msg], '*')
  224. },
  225. getAllMsg: function () {
  226. return msgList.join('\n')
  227. }
  228. }
  229. }())
  230. const util_log_impl = function (type) {
  231. if (r.script.is_dev) {
  232. // 直接打印, 会显示行数
  233. return window.console[type].bind(window.console, type + ':');
  234. } else {
  235. // 将log收集到util_log_hub中, 显示的行数是错误的...
  236. return function (...args) {
  237. args.unshift(type + ':')
  238. window.console[type].apply(window.console, args)
  239. util_log_hub.msg(util_arr_stringify(args))
  240. }
  241. }
  242. }
  243. const util_log = util_log_impl('log')
  244. const util_info = util_log_impl('info')
  245. const util_debug = util_log_impl('debug')
  246. const util_warn = util_log_impl('warn')
  247. const util_error = util_log_impl('error')
  248. log = util_debug
  249. log(`[${GM_info.script.name} v${GM_info.script.version} (${invokeBy})] run on: ${window.location.href}`);
  250.  
  251. const util_func_noop = function () { }
  252. const util_func_catched = function (func, onError) {
  253. let ret = function () {
  254. try {
  255. return func.apply(this, arguments)
  256. } catch (e) {
  257. if (onError) return onError(e) // onError可以处理报错时的返回值
  258. // 否则打印log, 并返回undefined
  259. util_error('Exception while run %o: %o\n%o', func, e, e.stack)
  260. return undefined
  261. }
  262. }
  263. // 函数的name属性是不可写+可配置的, 故需要如下代码实现类似这样的效果: ret.name = func.name
  264. // 在Edge上匿名函数的name的描述符会为undefined, 需要做特殊处理, fuck
  265. let funcNameDescriptor = Object.getOwnPropertyDescriptor(func, 'name') || {
  266. value: '',
  267. writable: false,
  268. configurable: true,
  269. }
  270. Object.defineProperty(ret, 'name', funcNameDescriptor)
  271. return ret
  272. }
  273.  
  274. const util_safe_get = (code) => {
  275. return eval(`
  276. (()=>{
  277. try{
  278. return ${code}
  279. }catch(e){
  280. console.warn(e.toString())
  281. return null
  282. }
  283. })()
  284. `)
  285. }
  286.  
  287. const util_ui_alert = function (message, resolve, reject) {
  288. setTimeout(() => {
  289. if (resolve) {
  290. if (window.confirm(message)) {
  291. resolve()
  292. } else {
  293. if (reject) {
  294. reject()
  295. }
  296. }
  297. } else {
  298. alert(message)
  299. }
  300. }, 500)
  301. }
  302.  
  303. const util_init = (function () {
  304. const RUN_AT = {
  305. DOM_LOADED: 0,
  306. DOM_LOADED_AFTER: 1,
  307. COMPLETE: 2,
  308. }
  309. const PRIORITY = {
  310. FIRST: 1e6,
  311. HIGH: 1e5,
  312. BEFORE: 1e3,
  313. DEFAULT: 0,
  314. AFTER: -1e3,
  315. LOW: -1e5,
  316. LAST: -1e6,
  317. }
  318. const callbacks = {
  319. [RUN_AT.DOM_LOADED]: [],
  320. [RUN_AT.DOM_LOADED_AFTER]: [],
  321. [RUN_AT.COMPLETE]: [],
  322. }
  323. const util_page_valid = () => true // 是否要运行
  324. const dclCreator = function (runAt) {
  325. let dcl = function () {
  326. util_init.atRun = runAt // 更新运行状态
  327. const valid = util_page_valid()
  328. // 优先级从大到小, index从小到大, 排序
  329. callbacks[runAt].sort((a, b) => b.priority - a.priority || a.index - b.index)
  330. .filter(item => valid || item.always)
  331. .forEach(item => item.func(valid))
  332. }
  333. return dcl
  334. }
  335.  
  336. if (window.document.readyState !== 'loading') {
  337. const msg = `${GM_info.script.name} 加载时机不对, 不能保证正常工作\n\n1. 点击'确定', 刷新页面/重载脚本\n2. 若依然出现该提示, 请尝试'硬性重新加载'(快捷键一般为ctrl+f5)\n3. 若还是出现该提示, 请尝试关闭再重新打开该页面\n4. 若反复出现该提示, 请尝试换个浏览器\n`
  338. /*
  339. util_ui_alert(msg, () => {
  340. location.reload(true)
  341. })
  342. */
  343. // throw new Error('unit_init must run at loading, current is ' + document.readyState)
  344. util_warn(msg)
  345. }
  346.  
  347. window.document.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED))
  348. window.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED_AFTER))
  349. window.addEventListener('load', dclCreator(RUN_AT.COMPLETE))
  350.  
  351. const util_init = function (func, priority = PRIORITY.DEFAULT, runAt = RUN_AT.DOM_LOADED, always = false) {
  352. func = util_func_catched(func)
  353. if (util_init.atRun < runAt) { // 若还没运行到runAt指定的状态, 则放到队列里去
  354. callbacks[runAt].push({
  355. priority,
  356. index: callbacks[runAt].length, // 使用callback数组的长度, 作为添加元素的index属性
  357. func,
  358. always
  359. })
  360. } else { // 否则直接运行
  361. let valid = util_page_valid()
  362. setTimeout(() => (valid || always) && func(valid), 1)
  363. }
  364. return func
  365. }
  366. util_init.atRun = -1 // 用来表示当前运行到什么状态
  367. util_init.RUN_AT = RUN_AT
  368. util_init.PRIORITY = PRIORITY
  369. return util_init
  370. }())
  371. /** 通知模块 剽窃自 YAWF 用户脚本 硬广:https://tiansh.github.io/yawf/ */
  372. const util_notify = (function () {
  373. var avaliable = {};
  374. var shown = [];
  375. var use = {
  376. 'hasPermission': function () { return null; },
  377. 'requestPermission': function (callback) { return null; },
  378. 'hideNotification': function (notify) { return null; },
  379. 'showNotification': function (id, title, body, icon, delay, onclick) { return null; }
  380. };
  381.  
  382. // 检查一个微博是不是已经被显示过了,如果显示过了不重复显示
  383. var shownFeed = function (id) {
  384. return false;
  385. };
  386.  
  387. // webkitNotifications
  388. // Tab Notifier 扩展实现此接口,但显示的桌面提示最多只能显示前两行
  389. if (typeof webkitNotifications !== 'undefined') avaliable.webkit = {
  390. 'hasPermission': function () {
  391. return [true, null, false][webkitNotifications.checkPermission()];
  392. },
  393. 'requestPermission': function (callback) {
  394. return webkitNotifications.requestPermission(callback);
  395. },
  396. 'hideNotification': function (notify) {
  397. notify.cancel();
  398. afterHideNotification(notify);
  399. },
  400. 'showNotification': function (id, title, body, icon, delay, onclick) {
  401. if (shownFeed(id)) return null;
  402. var notify = webkitNotifications.createNotification(icon, title, body);
  403. if (delay && delay > 0) notify.addEventListener('display', function () {
  404. setTimeout(function () { hideNotification(notify); }, delay);
  405. });
  406. if (onclick) notify.addEventListener('click', function () {
  407. onclick.apply(this, arguments);
  408. hideNotification(notify);
  409. });
  410. notify.show();
  411. return notify;
  412. },
  413. };
  414.  
  415. // Notification
  416. // Firefox 22+
  417. // 显示4秒会自动关闭 https://bugzil.la/875114
  418. if (typeof Notification !== 'undefined') avaliable.standard = {
  419. 'hasPermission': function () {
  420. return {
  421. 'granted': true,
  422. 'denied': false,
  423. 'default': null,
  424. }[Notification.permission];
  425. },
  426. 'requestPermission': function (callback) {
  427. return Notification.requestPermission(callback);
  428. },
  429. 'hideNotification': function (notify) {
  430. notify.close();
  431. afterHideNotification(notify);
  432. },
  433. 'showNotification': function (id, title, body, icon, delay, onclick) {
  434. if (shownFeed(id)) return null;
  435. var notify = new Notification(title, { 'body': body, 'icon': icon, 'requireInteraction': !delay });
  436. if (delay && delay > 0) notify.addEventListener('show', function () {
  437. setTimeout(function () {
  438. hideNotification(notify);
  439. }, delay);
  440. });
  441. if (onclick) notify.addEventListener('click', function () {
  442. onclick.apply(this, arguments);
  443. hideNotification(notify);
  444. });
  445. return notify;
  446. },
  447. };
  448.  
  449. // 有哪些接口可用
  450. var avaliableNotification = function () {
  451. return Object.keys(avaliable);
  452. };
  453. // 选择用哪个接口
  454. var choseNotification = function (prefer) {
  455. return (use = prefer && avaliable[prefer] || avaliable.standard);
  456. };
  457. choseNotification();
  458. // 检查权限
  459. var hasPermission = function () {
  460. return use.hasPermission.apply(this, arguments);
  461. };
  462. // 请求权限
  463. var requestPermission = function () {
  464. return use.requestPermission.apply(this, arguments);
  465. };
  466. // 显示消息
  467. var showNotification = function (id, title, body, icon, delay, onclick) {
  468. var notify = use.showNotification.apply(this, arguments);
  469. shown.push(notify);
  470. return notify;
  471. };
  472. // 隐藏已经显示的消息
  473. var hideNotification = function (notify) {
  474. use.hideNotification.apply(this, arguments);
  475. return notify;
  476. };
  477. var afterHideNotification = function (notify) {
  478. shown = shown.filter(function (x) { return x !== notify; });
  479. };
  480.  
  481. document.addEventListener('unload', function () {
  482. shown.forEach(hideNotification);
  483. shown = [];
  484. });
  485. var showNotificationAnyway = function (id, title, body, icon, delay, onclick) {
  486. var that = this, thatArguments = arguments;
  487. switch (that.hasPermission()) {
  488. case null: // default
  489. that.requestPermission(function () {
  490. showNotificationAnyway.apply(that, thatArguments);
  491. });
  492. break;
  493. case true: // granted
  494. // 只有已获取了授权, 才能有返回值...
  495. return that.showNotification.apply(that, thatArguments);
  496. break;
  497. case false: // denied
  498. log('Notification permission: denied');
  499. break;
  500. }
  501. return null;
  502. }
  503.  
  504. return {
  505. 'avaliableNotification': avaliableNotification,
  506. 'choseNotification': choseNotification,
  507. 'hasPermission': hasPermission,
  508. 'requestPermission': requestPermission,
  509. 'showNotification': showNotification,
  510. 'hideNotification': hideNotification,
  511. show: function (body, onclick, delay = 3e3) {
  512. return this.showNotificationAnyway(Date.now(), GM_info.script.name, body, '//bangumi.bilibili.com/favicon.ico', delay, onclick)
  513. },
  514. showNotificationAnyway
  515. };
  516. }())
  517. const util_cookie = (function () {
  518. function getCookies() {
  519. var map = document.cookie.split('; ').reduce(function (obj, item) {
  520. var entry = item.split('=');
  521. obj[entry[0]] = entry[1];
  522. return obj;
  523. }, {});
  524. return map;
  525. }
  526.  
  527. function getCookie(key) {
  528. return getCookies()[key];
  529. }
  530.  
  531. /**
  532. * @param key key
  533. * @param value 为undefined时, 表示删除cookie
  534. * @param options 为undefined时, 表示过期时间为3年
  535. * 为''时, 表示Session cookie
  536. * 为数字时, 表示指定过期时间
  537. * 为{}时, 表示指定所有的属性
  538. * */
  539. function setCookie(key, value, options) {
  540. if (typeof options !== 'object') {
  541. options = {
  542. domain: '.bilibili.com',
  543. path: '/',
  544. 'max-age': value === undefined ? 0 : (options === undefined ? 94608000 : options)
  545. };
  546. }
  547. var c = Object.keys(options).reduce(function (str, key) {
  548. return str + '; ' + key + '=' + options[key];
  549. }, key + '=' + value);
  550. document.cookie = c;
  551. return c;
  552. }
  553.  
  554. return new Proxy({ set: setCookie, get: getCookie, all: getCookies }, {
  555. get: function (target, prop) {
  556. if (prop in target) return target[prop]
  557. return getCookie(prop)
  558. },
  559. set: function (target, prop, value) {
  560. setCookie(prop, value)
  561. return true
  562. }
  563. })
  564. }())
  565. const Promise = window.Promise // 在某些情况下, 页面中会修改window.Promise... 故我们要备份一下原始的Promise
  566. const util_promise_plus = (function () {
  567. /**
  568. * 模仿RxJava中的compose操作符
  569. * @param transformer 转换函数, 传入Promise, 返回Promise; 若为空, 则啥也不做
  570. */
  571. Promise.prototype.compose = function (transformer) {
  572. return transformer ? transformer(this) : this
  573. }
  574. }())
  575. const util_promise_timeout = function (timeout) {
  576. return new Promise((resolve, reject) => {
  577. setTimeout(resolve, timeout);
  578. })
  579. }
  580. // 直到满足condition()为止, 才执行promiseCreator(), 创建Promise
  581. // https://stackoverflow.com/questions/40328932/javascript-es6-promise-for-loop
  582. const util_promise_condition = function (condition, promiseCreator, retryCount = Number.MAX_VALUE, interval = 1) {
  583. const loop = (time) => {
  584. if (!condition()) {
  585. if (time < retryCount) {
  586. return util_promise_timeout(interval).then(loop.bind(null, time + 1))
  587. } else {
  588. return Promise.reject(`util_promise_condition timeout, condition: ${condition.toString()}`)
  589. }
  590. } else {
  591. return promiseCreator()
  592. }
  593. }
  594. return loop(0)
  595. }
  596.  
  597. const util_ajax = function (options) {
  598. const creator = () => new Promise(function (resolve, reject) {
  599. typeof options !== 'object' && (options = { url: options });
  600.  
  601. options.async === undefined && (options.async = true);
  602. options.xhrFields === undefined && (options.xhrFields = { withCredentials: true });
  603. options.success = function (data) {
  604. resolve(data);
  605. };
  606. options.error = function (err) {
  607. reject(err);
  608. };
  609. util_debug('ajax:', options.url)
  610. $.ajax(options);
  611. })
  612. return util_promise_condition(() => window.$, creator, 100, 100) // 重试 100 * 100 = 10s
  613. }
  614. /**
  615. * @param promiseCeator 创建Promise的函数
  616. * @param resultTranformer 用于变换result的函数, 返回新的result或Promise
  617. * @param errorTranformer 用于变换error的函数, 返回新的error或Promise, 返回的Promise可以做状态恢复...
  618. */
  619. const util_async_wrapper = function (promiseCeator, resultTranformer, errorTranformer) {
  620. return function (...args) {
  621. return new Promise((resolve, reject) => {
  622. // log(promiseCeator, ...args)
  623. promiseCeator(...args)
  624. .then(r => resultTranformer ? resultTranformer(r) : r)
  625. .then(r => resolve(r))
  626. .catch(e => {
  627. e = errorTranformer ? errorTranformer(e) : e
  628. if (!(e instanceof Promise)) {
  629. // 若返回值不是Promise, 则表示是一个error
  630. e = Promise.reject(e)
  631. }
  632. e.then(r => resolve(r)).catch(e => reject(e))
  633. })
  634. })
  635. }
  636. }
  637. /**
  638. * 创建元素的快捷方法:
  639. * 1. type, props, children
  640. * 2. type, props, innerHTML
  641. * 3. 'text', text
  642. * @param type string, 标签名; 特殊的, 若为text, 则表示创建文字, 对应的t为文字的内容
  643. * @param props object, 属性; 特殊的属性名有: className, 类名; style, 样式, 值为(样式名, 值)形式的object; event, 值为(事件名, 监听函数)形式的object;
  644. * @param children array, 子元素; 也可以直接是html文本;
  645. */
  646. const util_ui_element_creator = (type, props, children) => {
  647. let elem = null;
  648. if (type === "text") {
  649. return document.createTextNode(props);
  650. } else {
  651. elem = document.createElement(type);
  652. }
  653. for (let n in props) {
  654. if (n === "style") {
  655. for (let x in props.style) {
  656. elem.style[x] = props.style[x];
  657. }
  658. } else if (n === "className") {
  659. elem.className = props[n];
  660. } else if (n === "event") {
  661. for (let x in props.event) {
  662. elem.addEventListener(x, props.event[x]);
  663. }
  664. } else {
  665. elem.setAttribute(n, props[n]);
  666. }
  667. }
  668. if (children) {
  669. if (typeof children === 'string') {
  670. elem.innerHTML = children;
  671. } else {
  672. for (let i = 0; i < children.length; i++) {
  673. if (children[i] != null)
  674. elem.appendChild(children[i]);
  675. }
  676. }
  677. }
  678. return elem;
  679. }
  680. const _ = util_ui_element_creator
  681. const util_jsonp = function (url, callback) {
  682. return new Promise((resolve, reject) => {
  683. document.head.appendChild(_('script', {
  684. src: url,
  685. event: {
  686. load: function () {
  687. resolve()
  688. },
  689. error: function () {
  690. reject()
  691. }
  692. }
  693. }));
  694. })
  695. }
  696. const util_generate_sign = function (params, key) {
  697. var s_keys = [];
  698. for (var i in params) {
  699. s_keys.push(i);
  700. }
  701. s_keys.sort();
  702. var data = "";
  703. for (var i = 0; i < s_keys.length; i++) {
  704. // encodeURIComponent 返回的转义数字必须为大写( 如 %2F )
  705. data += (data ? "&" : "") + s_keys[i] + "=" + encodeURIComponent(params[s_keys[i]]);
  706. }
  707. return {
  708. "sign": hex_md5(data + key),
  709. "params": data
  710. };
  711. }
  712. const util_xml2obj = (xml) => {
  713. try {
  714. var obj = {}, text;
  715. var children = xml.children;
  716. if (children.length > 0) {
  717. for (var i = 0; i < children.length; i++) {
  718. var item = children.item(i);
  719. var nodeName = item.nodeName;
  720.  
  721. if (typeof (obj[nodeName]) == "undefined") { // 若是新的属性, 则往obj中添加
  722. obj[nodeName] = util_xml2obj(item);
  723. } else {
  724. if (typeof (obj[nodeName].push) == "undefined") { // 若老的属性没有push方法, 则把属性改成Array
  725. var old = obj[nodeName];
  726.  
  727. obj[nodeName] = [];
  728. obj[nodeName].push(old);
  729. }
  730. obj[nodeName].push(util_xml2obj(item));
  731. }
  732. }
  733. } else {
  734. text = xml.textContent;
  735. if (/^\d+(\.\d+)?$/.test(text)) {
  736. obj = Number(text);
  737. } else if (text === 'true' || text === 'false') {
  738. obj = Boolean(text);
  739. } else {
  740. obj = text;
  741. }
  742. }
  743. return obj;
  744. } catch (e) {
  745. util_error(e);
  746. }
  747. }
  748. const util_ui_popframe = function (iframeSrc) {
  749. if (!document.getElementById('balh-style-login')) {
  750. var style = document.createElement('style');
  751. style.id = 'balh-style-login';
  752. 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}';
  753. }
  754.  
  755. var div = document.createElement('div');
  756. div.id = 'GMBiliPlusLoginContainer';
  757. 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>';
  758. div.firstChild.addEventListener('click', function (e) {
  759. if (e.target === this || e.target.className === 'GMBiliPlusCloseBox') {
  760. if (!confirm('确认关闭?')) {
  761. return false;
  762. }
  763. div.firstChild.style.animationName = 'pop-iframe-out';
  764. setTimeout(function () {
  765. div.remove();
  766. }, 5e2);
  767. }
  768. });
  769. document.body.appendChild(div);
  770. }
  771.  
  772. /**
  773. * - param.content: 内容元素数组/HTML
  774. * - param.showConfirm: 是否显示确定按钮
  775. * - param.confirmBtn: 确定按钮的文字
  776. * - param.onConfirm: 确定回调
  777. * - param.onClose: 关闭回调
  778. */
  779. const util_ui_pop = function (param) {
  780. if (typeof param.content === 'string') {
  781. let template = _('template');
  782. template.innerHTML = param.content.trim()
  783. param.content = Array.from(template.content.childNodes)
  784. } else if (!(param.content instanceof Array)) {
  785. util_log(`param.content(${param.content}) 不是数组`)
  786. return;
  787. }
  788.  
  789. if (document.getElementById('AHP_Notice_style') == null) {
  790. let noticeWidth = Math.min(500, innerWidth - 40);
  791. 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;pointer-events:none } .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;pointer-events:auto;white-space:pre-wrap } #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}`)]));
  792. }
  793.  
  794. if (document.querySelector('#AHP_Notice') != null)
  795. document.querySelector('#AHP_Notice').remove();
  796.  
  797. let div = _('div', { id: 'AHP_Notice' });
  798. let childs = [];
  799. if (param.showConfirm || param.confirmBtn || param.onConfirm) {
  800. childs.push(_('input', { value: param.confirmBtn || _t('ok'), type: 'button', className: 'confirm', event: { click: param.onConfirm } }));
  801. }
  802. childs.push(_('input', {
  803. value: _t('close'), type: 'button', className: 'close', event: {
  804. click: function () {
  805. param.onClose && param.onClose();
  806. div.style.height = 0;
  807. setTimeout(function () { div.remove(); }, 500);
  808. }
  809. }
  810. }));
  811. div.appendChild(_('div', {}, [_('div', {},
  812. param.content.concat([_('hr'), _('div', { style: { textAlign: 'right' } }, childs)])
  813. )]));
  814. document.body.appendChild(div);
  815. div.style.height = div.firstChild.offsetHeight + 'px';
  816. }
  817.  
  818.  
  819. /**
  820. * MessageBox -> from base.core.js
  821. * MessageBox.show(referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback)
  822. * MessageBox.close()
  823. */
  824. const util_ui_msg = (function () {
  825. function MockMessageBox() {
  826. this.show = (...args) => util_log(MockMessageBox.name, 'show', args)
  827. this.close = (...args) => util_log(MockMessageBox.name, 'close', args)
  828. }
  829.  
  830. let popMessage = null
  831. let mockPopMessage = new MockMessageBox()
  832. let notifyPopMessage = {
  833. _current_notify: null,
  834. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  835. this.close()
  836. this._current_notify = util_notify.show(message, buttonTypeConfirmCallback, closeTime)
  837. },
  838. close: function () {
  839. if (this._current_notify) {
  840. util_notify.hideNotification(this._current_notify)
  841. this._current_notify = null
  842. }
  843. }
  844. }
  845. let alertPopMessage = {
  846. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  847. util_ui_alert(message, buttonTypeConfirmCallback)
  848. },
  849. close: util_func_noop
  850. }
  851.  
  852. util_init(() => {
  853. if (!popMessage && window.MessageBox) {
  854. popMessage = new window.MessageBox()
  855. let orignShow = popMessage.show
  856. popMessage.show = function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  857. // 这个窗,有一定机率弹不出来。。。不知道为什么
  858. orignShow.call(this, referenceElement, message.replace('\n', '<br>'), closeTime, boxType, buttonTypeConfirmCallback)
  859. }
  860. popMessage.close = function () {
  861. // 若没调用过show, 就调用close, msgbox会为null, 导致报错
  862. this.msgbox != null && window.MessageBox.prototype.close.apply(this, arguments)
  863. }
  864. }
  865. }, util_init.PRIORITY.FIRST, util_init.RUN_AT.DOM_LOADED_AFTER)
  866.  
  867. return {
  868. _impl: function () {
  869. return popMessage || alertPopMessage
  870. },
  871. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  872. let pop = this._impl()
  873. return pop.show.apply(pop, arguments)
  874. },
  875. close: function () {
  876. let pop = this._impl()
  877. return pop.close.apply(pop, arguments)
  878. },
  879. setMsgBoxFixed: function (fixed) {
  880. if (popMessage) {
  881. popMessage.msgbox[0].style.position = fixed ? 'fixed' : ''
  882. } else {
  883. util_log(MockMessageBox.name, 'setMsgBoxFixed', fixed)
  884. }
  885. },
  886. showOnNetError: function (e) {
  887. if (e.readyState === 0) {
  888. this.show($('.balh_settings'), '哎呀,服务器连不上了,进入设置窗口,换个服务器试试?', 0, 'button', balh_ui_setting.show);
  889. }
  890. },
  891. showOnNetErrorInPromise: function () {
  892. return p => p
  893. .catch(e => {
  894. this.showOnNetError(e)
  895. return Promise.reject(e)
  896. })
  897. }
  898. }
  899. }())
  900. const util_ui_player_msg = function (message) {
  901. const msg = util_stringify(message)
  902. util_info('player msg:', msg)
  903. const $panel = document.querySelector('.bilibili-player-video-panel-text')
  904. if ($panel) {
  905. let stage = $panel.children.length + 1000 // 加1000和B站自己发送消息的stage区别开来
  906. $panel.appendChild(_('div', { className: 'bilibili-player-video-panel-row', stage: stage }, [_('text', `[${GM_info.script.name}] ${msg}`)]))
  907. }
  908. }
  909. const util_ui_copy = function (text, textarea) {
  910. textarea.value = text
  911. textarea.select()
  912. try {
  913. return document.execCommand('copy')
  914. } catch (e) {
  915. util_error('复制文本出错', e)
  916. }
  917. return false
  918. }
  919. const util_url_param = function (url, key) {
  920. return (url.match(new RegExp('[?|&]' + key + '=(\\w+)')) || ['', ''])[1];
  921. }
  922.  
  923. const util_page = {
  924. player: () => location.href.includes('www.bilibili.com/blackboard/html5player'),
  925. // 在av页面中的iframe标签形式的player
  926. player_in_av: util_func_catched(() => util_page.player() && window.top.location.href.includes('www.bilibili.com/video/av'), (e) => log(e), false),
  927. av: () => location.href.includes('www.bilibili.com/video/av') || location.href.includes('www.bilibili.com/video/BV'),
  928. av_new: function () { return this.av() && (window.__playinfo__ || window.__playinfo__origin) },
  929. bangumi: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/?$')),
  930. bangumi_md: () => location.href.includes('www.bilibili.com/bangumi/media/md'),
  931. // movie页面使用window.aid, 保存当前页面av号
  932. movie: () => location.href.includes('bangumi.bilibili.com/movie/'),
  933. // anime页面使用window.season_id, 保存当前页面season号
  934. anime: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/play.*')),
  935. anime_ep: () => location.href.includes('www.bilibili.com/bangumi/play/ep'),
  936. anime_ss: () => location.href.includes('www.bilibili.com/bangumi/play/ss'),
  937. anime_ep_m: () => location.href.includes('m.bilibili.com/bangumi/play/ep'),
  938. anime_ss_m: () => location.href.includes('m.bilibili.com/bangumi/play/ss'),
  939. new_bangumi: () => location.href.includes('www.bilibili.com/bangumi')
  940. }
  941.  
  942. const balh_config = (function () {
  943. const cookies = util_cookie.all() // 缓存的cookies
  944. return new Proxy({ /*保存config的对象*/ }, {
  945. get: function (target, prop) {
  946. if (prop === 'server') {
  947. // const server_inner = balh_config.server_inner
  948. // const server = server_inner === r.const.server.CUSTOM ? balh_config.server_custom : server_inner
  949. // return server
  950. return balh_config.server_inner
  951. }
  952. if (prop in target) {
  953. return target[prop]
  954. } else { // 若target中不存在指定的属性, 则从缓存的cookies中读取, 并保存到target中
  955. let value = cookies['balh_' + prop]
  956. switch (prop) {
  957. case 'server_inner':
  958. value = value || r.const.server.defaultServer()
  959. // 迁移回biliplus, 只会执行一次
  960. if (util_page.new_bangumi() && !localStorage.balh_migrate_to_1) {
  961. localStorage.balh_migrate_to_1 = r.const.TRUE
  962. if (value.includes('biliplus.ipcjs.top')) {
  963. value = r.const.server.defaultServer()
  964. balh_config.server = value
  965. }
  966. }
  967. break
  968. case 'server_custom':
  969. value = value || ''
  970. break
  971. case 'mode':
  972. value = value || (balh_config.blocked_vip ? r.const.mode.REDIRECT : r.const.mode.DEFAULT)
  973. break
  974. case 'flv_prefer_ws':
  975. value = r.const.FALSE // 关闭该选项
  976. break
  977. default:
  978. // case 'blocked_vip':
  979. // case 'remove_pre_ad':
  980. break
  981. }
  982. target[prop] = value
  983. return value
  984. }
  985. },
  986. set: function (target, prop, value) {
  987. target[prop] = value // 更新值
  988. util_cookie['balh_' + prop] = value // 更新cookie中的值
  989. return true
  990. }
  991. })
  992. }())
  993.  
  994. const access_key_param_if_exist = function (isKghost) {
  995. // access_key是由B站验证的, B站帐号和BP帐号不同时, access_key无效
  996. // kghost的服务器使用的B站帐号, access_key有效
  997. return (localStorage.access_key && (!balh_config.blocked_vip || isKghost)) ? `&access_key=${localStorage.access_key}` : ''
  998. }
  999.  
  1000. const balh_api_plus_view = function (aid, update = true) {
  1001. return util_ajax(`${balh_config.server}/api/view?id=${aid}&update=${update}${access_key_param_if_exist()}`);
  1002. }
  1003. const balh_api_plus_season = function (season_id) {
  1004. return util_ajax(`${balh_config.server}/api/bangumi?season=${season_id}${access_key_param_if_exist()}`);
  1005. }
  1006. // https://www.biliplus.com/BPplayurl.php?otype=json&cid=30188339&module=bangumi&qn=16&src=vupload&vid=vupload_30188339
  1007. // qn = 16, 能看
  1008. const balh_api_plus_playurl = function (cid, qn = 16, bangumi = true) {
  1009. return util_ajax(`${balh_config.server}/BPplayurl.php?otype=json&cid=${cid}${bangumi ? '&module=bangumi' : ''}&qn=${qn}&src=vupload&vid=vupload_${cid}${access_key_param_if_exist()}`);
  1010. }
  1011. // https://www.biliplus.com/api/h5play.php?tid=33&cid=31166258&type=vupload&vid=vupload_31166258&bangumi=1
  1012. 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}${access_key_param_if_exist()}`)
  1013. .then(text => (text.match(/srcUrl=\{"mp4":"(https?.*)"\};/) || ['', ''])[1]); // 提取mp4的url
  1014.  
  1015. const balh_is_close = false
  1016.  
  1017. const balh_version_remind = (function () {
  1018. if (!util_page.new_bangumi()) return
  1019.  
  1020. util_init(() => {
  1021. if ((localStorage.balh_version || '0') < GM_info.script.version) {
  1022. localStorage.balh_version = GM_info.script.version
  1023. let version_remind = _t('version_remind')
  1024. if (version_remind) {
  1025. util_ui_pop({ content: `<h3>${GM_info.script.name} v${GM_info.script.version} 更新日志</h3>${version_remind}` })
  1026. }
  1027. }
  1028. })
  1029. })()
  1030.  
  1031. const balh_feature_switch_to_old_player = (function () {
  1032. if (util_page.av() && !localStorage.balh_disable_switch_to_old_player) {
  1033. util_init(() => {
  1034. let $switchToOldBtn = document.querySelector('#entryOld > .old-btn > a')
  1035. if ($switchToOldBtn) {
  1036. util_ui_pop({
  1037. content: `${GM_info.script.name} 对新版播放器的支持还在测试阶段, 不稳定, 推荐切换回旧版`,
  1038. confirmBtn: '切换回旧版',
  1039. onConfirm: () => $switchToOldBtn.click(),
  1040. onClose: () => localStorage.balh_disable_switch_to_old_player = r.const.TRUE,
  1041. })
  1042. }
  1043. })
  1044. }
  1045. if (util_page.new_bangumi()) {
  1046. if (util_cookie.stardustpgcv === '0606') {
  1047. util_init(() => {
  1048. let $panel = document.querySelector('.error-container > .server-error')
  1049. if ($panel) {
  1050. $panel.insertBefore(_('text', '临时切换到旧版番剧页面中...'), $panel.firstChild)
  1051. util_cookie.stardustpgcv = '0'
  1052. localStorage.balh_temp_switch_to_old_page = r.const.TRUE
  1053. location.reload()
  1054. }
  1055. })
  1056. }
  1057. if (localStorage.balh_temp_switch_to_old_page) {
  1058. util_cookie.stardustpgcv = '0606'
  1059. delete localStorage.balh_temp_switch_to_old_page
  1060. }
  1061. }
  1062. })()
  1063. const balh_feature_area_limit_new = (function () {
  1064. if (balh_is_close) return
  1065.  
  1066. if (!(
  1067. (util_page.av() && balh_config.enable_in_av) || util_page.new_bangumi()
  1068. )) {
  1069. return
  1070. }
  1071. function replacePlayInfo() {
  1072. log("window.__playinfo__", window.__playinfo__)
  1073. window.__playinfo__origin = window.__playinfo__
  1074. let playinfo = undefined
  1075. // 将__playinfo__置空, 让播放器去重新加载它...
  1076. Object.defineProperty(window, '__playinfo__', {
  1077. configurable: true,
  1078. enumerable: true,
  1079. get: () => {
  1080. log('__playinfo__', 'get')
  1081. return playinfo
  1082. },
  1083. set: (value) => {
  1084. // debugger
  1085. log('__playinfo__', 'set')
  1086. // 原始的playinfo为空, 且页面在loading状态, 说明这是html中对playinfo进行的赋值, 这个值可能是有区域限制的, 不能要
  1087. if (!window.__playinfo__origin && window.document.readyState === 'loading') {
  1088. log('__playinfo__', 'init in html', value)
  1089. window.__playinfo__origin = value
  1090. return
  1091. }
  1092. playinfo = value
  1093. },
  1094. })
  1095. }
  1096. function modifyGlobalValue(name, modifyFn) {
  1097. const name_origin = `${name}_origin`
  1098. window[name_origin] = window[name]
  1099. let value = undefined
  1100. Object.defineProperty(window, name, {
  1101. configurable: true,
  1102. enumerable: true,
  1103. get: () => {
  1104. return value
  1105. },
  1106. set: (val) => {
  1107. value = modifyFn(val)
  1108. }
  1109. })
  1110. if (window[name_origin]) {
  1111. window[name] = window[name_origin]
  1112. }
  1113. }
  1114. function replaceUserState() {
  1115. modifyGlobalValue('__PGC_USERSTATE__', (value) => {
  1116. if (value) {
  1117. // 区域限制
  1118. // todo : 调用areaLimit(limit), 保存区域限制状态
  1119. // 2019-08-17: 之前的接口还有用, 这里先不保存~~
  1120. value.area_limit = 0
  1121. // 会员状态
  1122. if (balh_config.blocked_vip && value.vip_info) {
  1123. value.vip_info.status = 1
  1124. value.vip_info.type = 2
  1125. }
  1126. }
  1127. return value
  1128. })
  1129. }
  1130. function replaceInitialState() {
  1131. modifyGlobalValue('__INITIAL_STATE__', (value) => {
  1132. if (value && value.epInfo && value.epList && balh_config.blocked_vip) {
  1133. for (let ep of [value.epInfo, ...value.epList]) {
  1134. // 13貌似表示会员视频, 2为普通视频
  1135. if (ep.epStatus === 13) {
  1136. log('epStatus 13 => 2', ep)
  1137. ep.epStatus = 2
  1138. }
  1139. }
  1140. }
  1141. return value
  1142. })
  1143. }
  1144. replaceInitialState()
  1145. replaceUserState()
  1146. replacePlayInfo()
  1147. })()
  1148. const balh_feature_area_limit = (function () {
  1149. if (balh_is_close) return
  1150.  
  1151. function injectXHR() {
  1152. util_debug('XMLHttpRequest的描述符:', Object.getOwnPropertyDescriptor(window, 'XMLHttpRequest'))
  1153. let firstCreateXHR = true
  1154. window.XMLHttpRequest = new Proxy(window.XMLHttpRequest, {
  1155. construct: function (target, args) {
  1156. // 第一次创建XHR时, 打上断点...
  1157. if (firstCreateXHR && r.script.is_dev) {
  1158. firstCreateXHR = false
  1159. // debugger
  1160. }
  1161. let container = {} // 用来替换responseText等变量
  1162. const dispatchResultTransformer = p => {
  1163. let event = {} // 伪装的event
  1164. return p
  1165. .then(r => {
  1166. container.readyState = 4
  1167. container.response = r
  1168. container.__onreadystatechange(event) // 直接调用会不会存在this指向错误的问题? => 目前没看到, 先这样(;¬_¬)
  1169. })
  1170. .catch(e => {
  1171. // 失败时, 让原始的response可以交付
  1172. container.__block_response = false
  1173. if (container.__response != null) {
  1174. container.readyState = 4
  1175. container.response = container.__response
  1176. container.__onreadystatechange(event) // 同上
  1177. }
  1178. })
  1179. }
  1180. return new Proxy(new target(...args), {
  1181. set: function (target, prop, value, receiver) {
  1182. if (prop === 'onreadystatechange') {
  1183. container.__onreadystatechange = value
  1184. let cb = value
  1185. value = function (event) {
  1186. if (target.readyState === 4) {
  1187. if (target.responseURL.match(util_regex_url('bangumi.bilibili.com/view/web_api/season/user/status'))
  1188. || target.responseURL.match(util_regex_url('api.bilibili.com/pgc/view/web/season/user/status'))) {
  1189. log('/season/user/status:', target.responseText)
  1190. let json = JSON.parse(target.responseText)
  1191. let rewriteResult = false
  1192. if (json.code === 0 && json.result) {
  1193. areaLimit(json.result.area_limit !== 0)
  1194. if (json.result.area_limit !== 0) {
  1195. json.result.area_limit = 0 // 取消区域限制
  1196. rewriteResult = true
  1197. }
  1198. if (balh_config.blocked_vip) {
  1199. json.result.pay = 1
  1200. rewriteResult = true
  1201. }
  1202. if (rewriteResult) {
  1203. container.responseText = JSON.stringify(json)
  1204. }
  1205. }
  1206. } else if (target.responseURL.match(util_regex_url('bangumi.bilibili.com/web_api/season_area'))) {
  1207. log('/season_area', target.responseText)
  1208. let json = JSON.parse(target.responseText)
  1209. if (json.code === 0 && json.result) {
  1210. areaLimit(json.result.play === 0)
  1211. if (json.result.play === 0) {
  1212. json.result.play = 1
  1213. container.responseText = JSON.stringify(json)
  1214. }
  1215. }
  1216. } else if (target.responseURL.match(util_regex_url('api.bilibili.com/x/web-interface/nav'))) {
  1217. const isFromReport = util_url_param(target.responseURL, 'from') === 'report'
  1218. let json = JSON.parse(target.responseText)
  1219. log('/x/web-interface/nav', (json.data && json.data.isLogin)
  1220. ? { uname: json.data.uname, isLogin: json.data.isLogin, level: json.data.level_info.current_level, vipType: json.data.vipType, vipStatus: json.data.vipStatus, isFromReport: isFromReport }
  1221. : target.responseText)
  1222. if (json.code === 0 && json.data && balh_config.blocked_vip
  1223. && !isFromReport // report时, 还是不伪装了...
  1224. ) {
  1225. json.data.vipType = 2; // 类型, 年度大会员
  1226. json.data.vipStatus = 1; // 状态, 启用
  1227. container.responseText = JSON.stringify(json)
  1228. }
  1229. } else if (target.responseURL.match(util_regex_url('api.bilibili.com/x/player.so'))) {
  1230. // 这个接口的返回数据貌似并不会影响界面...
  1231. if (balh_config.blocked_vip) {
  1232. log('/x/player.so')
  1233. const xml = new DOMParser().parseFromString(`<root>${target.responseText.replace(/\&/g, "&amp;")}</root>`, 'text/xml')
  1234. const vipXml = xml.querySelector('vip')
  1235. if (vipXml) {
  1236. const vip = JSON.parse(vipXml.innerHTML)
  1237. vip.vipType = 2 // 同上
  1238. vip.vipStatus = 1
  1239. vipXml.innerHTML = JSON.stringify(vip)
  1240. container.responseText = xml.documentElement.innerHTML
  1241. container.response = container.responseText
  1242. }
  1243. }
  1244. } else if (target.responseURL.match(util_regex_url('api.bilibili.com/x/player/playurl'))) {
  1245. log('/x/player/playurl', 'origin', `block: ${container.__block_response}`, target.response)
  1246. // todo : 当前只实现了r.const.mode.REPLACE, 需要支持其他模式
  1247. // 2018-10-14: 等B站全面启用新版再说(;¬_¬)
  1248. } else if (target.responseURL.match(util_regex_url('api.bilibili.com/pgc/player/web/playurl'))
  1249. && !util_url_param(target.responseURL, 'balh_ajax')) {
  1250. log('/pgc/player/web/playurl', 'origin', `block: ${container.__block_response}`, target.response)
  1251. if (!container.__redirect) { // 请求没有被重定向, 则需要检测结果是否有区域限制
  1252. let json = target.response
  1253. if (balh_config.blocked_vip || json.code || isAreaLimitForPlayUrl(json.result)) {
  1254. areaLimit(true)
  1255. container.__block_response = true
  1256. let url = container.__url
  1257. if (isBangumiPage()) {
  1258. url += `&module=bangumi`
  1259. }
  1260. bilibiliApis._playurl.asyncAjax(url)
  1261. .then(data => {
  1262. if (!data.code) {
  1263. data = { code: 0, result: data, message: "0" }
  1264. }
  1265. log('/pgc/player/web/playurl', 'proxy', data)
  1266. return data
  1267. })
  1268. .compose(dispatchResultTransformer)
  1269. } else {
  1270. areaLimit(false)
  1271. }
  1272. }
  1273. // 同上
  1274. }
  1275. if (container.__block_response) {
  1276. // 屏蔽并保存response
  1277. container.__response = target.response
  1278. return
  1279. }
  1280. }
  1281. // 这里的this是原始的xhr, 在container.responseText设置了值时需要替换成代理对象
  1282. cb.apply(container.responseText ? receiver : this, arguments)
  1283. }
  1284. }
  1285. target[prop] = value
  1286. return true
  1287. },
  1288. get: function (target, prop, receiver) {
  1289. if (prop in container) return container[prop]
  1290. let value = target[prop]
  1291. if (typeof value === 'function') {
  1292. let func = value
  1293. // open等方法, 必须在原始的xhr对象上才能调用...
  1294. value = function () {
  1295. if (prop === 'open') {
  1296. container.__method = arguments[0]
  1297. container.__url = arguments[1]
  1298. } else if (prop === 'send') {
  1299. let dispatchResultTransformerCreator = () => {
  1300. container.__block_response = true
  1301. return dispatchResultTransformer
  1302. }
  1303. if (container.__url.match(util_regex_url('api.bilibili.com/x/player/playurl')) && balh_config.enable_in_av) {
  1304. log('/x/player/playurl')
  1305. // debugger
  1306. bilibiliApis._playurl.asyncAjax(container.__url)
  1307. .then(data => {
  1308. if (!data.code) {
  1309. data = {
  1310. code: 0,
  1311. data: data,
  1312. message: "0",
  1313. ttl: 1
  1314. }
  1315. }
  1316. log('/x/player/playurl', 'proxy', data)
  1317. return data
  1318. })
  1319. .compose(dispatchResultTransformerCreator())
  1320. } else if (container.__url.match(util_regex_url('api.bilibili.com/pgc/player/web/playurl'))
  1321. && !util_url_param(container.__url, 'balh_ajax')
  1322. && needRedirect()) {
  1323. log('/pgc/player/web/playurl')
  1324. // debugger
  1325. container.__redirect = true // 标记该请求被重定向
  1326. let url = container.__url
  1327. if (isBangumiPage()) {
  1328. url += `&module=bangumi`
  1329. }
  1330. bilibiliApis._playurl.asyncAjax(url)
  1331. .then(data => {
  1332. if (!data.code) {
  1333. data = {
  1334. code: 0,
  1335. result: data,
  1336. message: "0",
  1337. }
  1338. }
  1339. log('/pgc/player/web/playurl', 'proxy(redirect)', data)
  1340. return data
  1341. })
  1342. .compose(dispatchResultTransformerCreator())
  1343. }
  1344. }
  1345. return func.apply(target, arguments)
  1346. }
  1347. }
  1348. return value
  1349. }
  1350. })
  1351. }
  1352. })
  1353. }
  1354.  
  1355. function injectAjax() {
  1356. log('injectAjax at:', window.jQuery)
  1357. let originalAjax = $.ajax;
  1358. $.ajax = function (arg0, arg1) {
  1359. let param;
  1360. if (arg1 === undefined) {
  1361. param = arg0;
  1362. } else {
  1363. arg0 && (arg1.url = arg0);
  1364. param = arg1;
  1365. }
  1366. let oriSuccess = param.success;
  1367. let oriError = param.error;
  1368. let mySuccess, myError;
  1369. // 投递结果的transformer, 结果通过oriSuccess/Error投递
  1370. let dispatchResultTransformer = p => p
  1371. .then(r => {
  1372. // debugger
  1373. oriSuccess(r)
  1374. })
  1375. .catch(e => oriError(e))
  1376. // 转换原始请求的结果的transformer
  1377. let oriResultTransformer
  1378. let oriResultTransformerWhenProxyError
  1379. let one_api;
  1380. // log(param)
  1381. if (param.url.match(util_regex_url_path('/web_api/get_source'))) {
  1382. one_api = bilibiliApis._get_source;
  1383. oriResultTransformer = p => p
  1384. .then(json => {
  1385. log(json);
  1386. if (json.code === -40301 // 区域限制
  1387. || json.result.payment && json.result.payment.price != 0 && balh_config.blocked_vip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
  1388. areaLimit(true);
  1389. return one_api.asyncAjax(param.url)
  1390. .catch(e => json)// 新的请求报错, 也应该返回原来的数据
  1391. } else {
  1392. areaLimit(false);
  1393. if ((balh_config.blocked_vip || balh_config.remove_pre_ad) && json.code === 0 && json.result.pre_ad) {
  1394. json.result.pre_ad = 0; // 去除前置广告
  1395. }
  1396. return json;
  1397. }
  1398. })
  1399. } else if (param.url.match(util_regex_url_path('/player/web_api/playurl')) // 老的番剧页面playurl接口
  1400. || param.url.match(util_regex_url_path('/player/web_api/v2/playurl')) // 新的番剧页面playurl接口
  1401. || param.url.match(util_regex_url('api.bilibili.com/pgc/player/web/playurl')) // 新的番剧页面playurl接口
  1402. || (balh_config.enable_in_av && param.url.match(util_regex_url('interface.bilibili.com/v2/playurl'))) // 普通的av页面playurl接口
  1403. ) {
  1404. // 新playrul:
  1405. // 1. 部分页面参数放在param.data中
  1406. // 2. 成功时, 返回的结果放到了result中: {"code":0,"message":"success","result":{}}
  1407. // 3. 失败时, 返回的结果没变
  1408. let isNewPlayurl
  1409. if (isNewPlayurl = param.url.match(util_regex_url('api.bilibili.com/pgc/player/web/playurl'))) {
  1410. if (param.data) {
  1411. param.url += `?${Object.keys(param.data).map(key => `${key}=${param.data[key]}`).join('&')}`
  1412. param.data = undefined
  1413. }
  1414. if (isBangumiPage()) {
  1415. log(`playurl add 'module=bangumi' param`)
  1416. param.url += `&module=bangumi`
  1417. }
  1418. // 加上这个参数, 防止重复拦截这个url
  1419. param.url += `&balh_ajax=1`
  1420. }
  1421. one_api = bilibiliApis._playurl;
  1422. if (isNewPlayurl) {
  1423. oriResultTransformerWhenProxyError = p => p
  1424. .then(json => !json.code ? json.result : json)
  1425. }
  1426. oriResultTransformer = p => p
  1427. .then(json => {
  1428. log(json)
  1429. if (isNewPlayurl && !json.code) {
  1430. json = json.result
  1431. }
  1432. if (balh_config.blocked_vip || json.code || isAreaLimitForPlayUrl(json)) {
  1433. areaLimit(true)
  1434. return one_api.asyncAjax(param.url)
  1435. .catch(e => json)
  1436. } else {
  1437. areaLimit(false)
  1438. return json
  1439. }
  1440. })
  1441. const oriDispatchResultTransformer = dispatchResultTransformer
  1442. dispatchResultTransformer = p => p
  1443. .then(r => {
  1444. if (!r.code && !r.from && !r.result && !r.accept_description) {
  1445. util_warn('playurl的result缺少必要的字段:', r)
  1446. r.from = 'local'
  1447. r.result = 'suee'
  1448. r.accept_description = ['未知 3P']
  1449. // r.timelength = r.durl.map(it => it.length).reduce((a, b) => a + b, 0)
  1450. if (r.durl && r.durl[0] && r.durl[0].url.includes('video-sg.biliplus.com')) {
  1451. const aid = window.__INITIAL_STATE__ && window.__INITIAL_STATE__.aid || window.__INITIAL_STATE__.epInfo && window.__INITIAL_STATE__.epInfo.aid || 'fuck'
  1452. util_ui_pop({
  1453. content: `原视频已被删除, 当前播放的是<a href="https://video-sg.biliplus.com/">转存服务器</a>中的视频, 速度较慢<br>被删的原因可能是:<br>1. 视频违规<br>2. 视频被归类到番剧页面 => 试下<a href="https://search.bilibili.com/bangumi?keyword=${aid}">搜索av${aid}</a>`
  1454. })
  1455. }
  1456. }
  1457. if (isNewPlayurl && !r.code) {
  1458. r = {
  1459. code: 0,
  1460. message: 'success',
  1461. result: r
  1462. }
  1463. }
  1464. return r
  1465. })
  1466. .compose(oriDispatchResultTransformer)
  1467. } else if (param.url.match(util_regex_url('interface.bilibili.com/player?'))) {
  1468. if (balh_config.blocked_vip) {
  1469. mySuccess = function (data) {
  1470. try {
  1471. let xml = new window.DOMParser().parseFromString(`<userstatus>${data.replace(/\&/g, '&amp;')}</userstatus>`, 'text/xml');
  1472. let vipTag = xml.querySelector('vip');
  1473. if (vipTag) {
  1474. let vip = JSON.parse(vipTag.innerHTML);
  1475. vip.vipType = 2; // 类型, 年度大会员
  1476. vip.vipStatus = 1; // 状态, 启用
  1477. vipTag.innerHTML = JSON.stringify(vip);
  1478. data = xml.documentElement.innerHTML;
  1479. }
  1480. } catch (e) {
  1481. log('parse xml error: ', e);
  1482. }
  1483. oriSuccess(data);
  1484. };
  1485. }
  1486. } else if (param.url.match(util_regex_url('api.bilibili.com/x/ad/video?'))) {
  1487. if (balh_config.remove_pre_ad) {
  1488. mySuccess = function (data) {
  1489. log('/ad/video', data)
  1490. if (data && data.code === 0 && data.data) {
  1491. data.data = [] // 移除广告接口返回的数据
  1492. }
  1493. oriSuccess(data)
  1494. }
  1495. }
  1496. }
  1497.  
  1498. if (one_api && oriResultTransformer) {
  1499. // 请求结果通过mySuccess/Error获取, 将其包装成Promise, 方便处理
  1500. let oriResultPromise = new Promise((resolve, reject) => {
  1501. mySuccess = resolve
  1502. myError = reject
  1503. })
  1504. if (needRedirect()) {
  1505. // 通过proxy, 执行请求
  1506. one_api.asyncAjax(param.url)
  1507. // proxy报错时, 返回原始请求的结果
  1508. .catch(e => oriResultPromise.compose(oriResultTransformerWhenProxyError))
  1509. .compose(dispatchResultTransformer)
  1510. } else {
  1511. oriResultPromise
  1512. .compose(oriResultTransformer)
  1513. .compose(dispatchResultTransformer)
  1514. }
  1515. }
  1516.  
  1517. // 若外部使用param.success处理结果, 则替换param.success
  1518. if (oriSuccess && mySuccess) {
  1519. param.success = mySuccess;
  1520. }
  1521. // 处理替换error
  1522. if (oriError && myError) {
  1523. param.error = myError;
  1524. }
  1525. // default
  1526. let xhr = originalAjax.apply(this, [param]);
  1527.  
  1528. // 若外部使用xhr.done()处理结果, 则替换xhr.done()
  1529. if (!oriSuccess && mySuccess) {
  1530. xhr.done(mySuccess);
  1531. xhr.done = function (success) {
  1532. oriSuccess = success; // 保存外部设置的success函数
  1533. return xhr;
  1534. };
  1535. }
  1536. // 处理替换error
  1537. if (!oriError && myError) {
  1538. xhr.fail(myError);
  1539. xhr.fail = function (error) {
  1540. oriError = error;
  1541. return xhr;
  1542. }
  1543. }
  1544. return xhr;
  1545. };
  1546. }
  1547.  
  1548. function injectFetch() {
  1549. window.fetch = util_async_wrapper(window.fetch,
  1550. resp => new Proxy(resp, {
  1551. get: function (target, prop, receiver) {
  1552. if (prop === 'json') {
  1553. return util_async_wrapper(target.json.bind(target),
  1554. oriResult => {
  1555. util_debug('injectFetch:', target.url)
  1556. if (target.url.match(util_regex_url_path('/player/web_api/v2/playurl/html5'))) {
  1557. let cid = util_url_param(target.url, 'cid')
  1558. return balh_api_plus_playurl(cid)
  1559. .then(result => {
  1560. if (result.code) {
  1561. return Promise.reject('error: ' + JSON.stringify(result))
  1562. } else {
  1563. return balh_api_plus_playurl_for_mp4(cid)
  1564. .then(url => {
  1565. util_debug(`mp4地址, 移动版: ${url}, pc版: ${result.durl[0].url}`)
  1566. return {
  1567. "code": 0,
  1568. "cid": `http://comment.bilibili.com/${cid}.xml`,
  1569. "timelength": result.timelength,
  1570. "src": url || result.durl[0].url, // 只取第一个片段的url...
  1571. }
  1572. })
  1573. }
  1574. })
  1575. .catch(e => {
  1576. // 若拉取视频地址失败, 则返回原始的结果
  1577. log('fetch mp4 url failed', e)
  1578. return oriResult
  1579. })
  1580. }
  1581. return oriResult
  1582. },
  1583. error => error)
  1584. }
  1585. return target[prop]
  1586. }
  1587. }),
  1588. error => error)
  1589. }
  1590.  
  1591. function isAreaLimitSeason() {
  1592. return util_cookie['balh_season_' + getSeasonId()];
  1593. }
  1594.  
  1595. function needRedirect() {
  1596. return balh_config.mode === r.const.mode.REDIRECT || (balh_config.mode === r.const.mode.DEFAULT && isAreaLimitSeason())
  1597. }
  1598.  
  1599. function areaLimit(limit) {
  1600. balh_config.mode === r.const.mode.DEFAULT && setAreaLimitSeason(limit)
  1601. }
  1602.  
  1603. function setAreaLimitSeason(limit) {
  1604. var season_id = getSeasonId();
  1605. util_cookie.set('balh_season_' + season_id, limit ? '1' : undefined, ''); // 第三个参数为'', 表示时Session类型的cookie
  1606. log('setAreaLimitSeason', season_id, limit);
  1607. }
  1608. /** 使用该方法判断是否需要添加module=bangumi参数, 并不准确... */
  1609. function isBangumi(season_type) {
  1610. log(`season_type: ${season_type}`)
  1611. // 1: 动画
  1612. // 2: 电影
  1613. // 3: 纪录片
  1614. // 4: 国创
  1615. // 5: 电视剧
  1616. return season_type != null // 存在season_type就是bangumi?
  1617. }
  1618.  
  1619. function isBangumiPage() {
  1620. return isBangumi(util_safe_get('window.__INITIAL_STATE__.mediaInfo.season_type || window.__INITIAL_STATE__.mediaInfo.ssType'))
  1621. }
  1622.  
  1623. function getSeasonId() {
  1624. var seasonId;
  1625. // 取anime页面的seasonId
  1626. try {
  1627. // 若w, 是其frame的window, 则有可能没有权限, 而抛异常
  1628. seasonId = window.season_id || window.top.season_id;
  1629. } catch (e) {
  1630. log(e);
  1631. }
  1632. if (!seasonId) {
  1633. try {
  1634. seasonId = (window.top.location.pathname.match(/\/anime\/(\d+)/) || ['', ''])[1];
  1635. } catch (e) {
  1636. log(e);
  1637. }
  1638. }
  1639.  
  1640. // 若没取到, 则取movie页面的seasonId, 以m开头
  1641. if (!seasonId) {
  1642. try {
  1643. seasonId = (window.top.location.pathname.match(/\/movie\/(\d+)/) || ['', ''])[1];
  1644. if (seasonId) {
  1645. seasonId = 'm' + seasonId;
  1646. }
  1647. } catch (e) {
  1648. log(e);
  1649. }
  1650. }
  1651.  
  1652. // 若没取到, 则去新的番剧播放页面的ep或ss
  1653. if (!seasonId) {
  1654. try {
  1655. seasonId = (window.top.location.pathname.match(/\/bangumi\/play\/((ep|ss)\d+)/) || ['', ''])[1];
  1656. } catch (e) {
  1657. log(e);
  1658. }
  1659. }
  1660. // 若没取到, 则去取av页面的av号
  1661. if (!seasonId) {
  1662. try {
  1663. seasonId = (window.top.location.pathname.match(/\/video\/((av|BV)\w+)/) || ['', ''])[1]
  1664. } catch (e) {
  1665. log(e);
  1666. }
  1667. }
  1668. // 最后, 若没取到, 则试图取出当前页面url中的aid
  1669. if (!seasonId) {
  1670. seasonId = util_url_param(window.location.href, 'aid');
  1671. if (seasonId) {
  1672. seasonId = 'aid' + seasonId;
  1673. }
  1674. }
  1675. return seasonId || '000';
  1676. }
  1677.  
  1678. function isAreaLimitForPlayUrl(json) {
  1679. return (json.errorcid && json.errorcid == '8986943') || (json.durl && json.durl.length === 1 && json.durl[0].length === 15126 && json.durl[0].size === 124627);
  1680. }
  1681.  
  1682. var bilibiliApis = (function () {
  1683. function AjaxException(message, code = 0/*用0表示未知错误*/) {
  1684. this.name = 'AjaxException'
  1685. this.message = message
  1686. this.code = code
  1687. }
  1688. AjaxException.prototype.toString = function () {
  1689. return `${this.name}: ${this.message}(${this.code})`
  1690. }
  1691. function BilibiliApi(props) {
  1692. Object.assign(this, props);
  1693. }
  1694.  
  1695. BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
  1696. var one_api = this;
  1697. $.ajax({
  1698. url: one_api.transToProxyUrl(originUrl),
  1699. async: true,
  1700. xhrFields: { withCredentials: true },
  1701. success: function (result) {
  1702. log('==>', result);
  1703. success(one_api.processProxySuccess(result));
  1704. // log('success', arguments, this);
  1705. },
  1706. error: function (e) {
  1707. log('error', arguments, this);
  1708. error(e);
  1709. }
  1710. });
  1711. };
  1712. BilibiliApi.prototype.asyncAjax = function (originUrl) {
  1713. return util_ajax(this.transToProxyUrl(originUrl))
  1714. .then(r => this.processProxySuccess(r))
  1715. .compose(util_ui_msg.showOnNetErrorInPromise()) // 出错时, 提示服务器连不上
  1716. }
  1717. var get_source_by_aid = new BilibiliApi({
  1718. transToProxyUrl: function (url) {
  1719. return balh_config.server + '/api/view?id=' + window.aid + `&update=true${access_key_param_if_exist()}`;
  1720. },
  1721. processProxySuccess: function (data) {
  1722. if (data && data.list && data.list[0] && data.movie) {
  1723. return {
  1724. code: 0,
  1725. message: 'success',
  1726. result: {
  1727. cid: data.list[0].cid,
  1728. formal_aid: data.aid,
  1729. movie_status: balh_config.blocked_vip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
  1730. pay_begin_time: 1507708800,
  1731. pay_timestamp: 0,
  1732. pay_user_status: data.movie.pay_user.status, // 一般都是0
  1733. player: data.list[0].type, // 一般为movie
  1734. vid: data.list[0].vid,
  1735. vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
  1736. vipType: balh_config.blocked_vip ? 2 : 0,
  1737. vipStatus: balh_config.blocked_vip ? 1 : 0,
  1738. }
  1739. }
  1740. };
  1741. } else {
  1742. return {
  1743. code: -404,
  1744. message: '不存在该剧集'
  1745. };
  1746. }
  1747. }
  1748. });
  1749. var get_source_by_season_id = new BilibiliApi({
  1750. transToProxyUrl: function (url) {
  1751. return balh_config.server + '/api/bangumi?season=' + window.season_id + access_key_param_if_exist();
  1752. },
  1753. processProxySuccess: function (data) {
  1754. var found = null;
  1755. if (!data.code) {
  1756. for (var i = 0; i < data.result.episodes.length; i++) {
  1757. if (data.result.episodes[i].episode_id == window.episode_id) {
  1758. found = data.result.episodes[i];
  1759. }
  1760. }
  1761. } else {
  1762. util_ui_alert('代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', window.location.reload.bind(window.location));
  1763. }
  1764. var returnVal = found !== null
  1765. ? {
  1766. "code": 0,
  1767. "message": "success",
  1768. "result": {
  1769. "aid": found.av_id,
  1770. "cid": found.danmaku,
  1771. "episode_status": balh_config.blocked_vip ? 2 : found.episode_status,
  1772. "payment": { "price": "9876547210.33" },
  1773. "pay_user": {
  1774. "status": balh_config.blocked_vip ? 1 : 0 // 是否已经支付过
  1775. },
  1776. "player": "vupload",
  1777. "pre_ad": 0,
  1778. "season_status": balh_config.blocked_vip ? 2 : data.result.season_status
  1779. }
  1780. }
  1781. : { code: -404, message: '不存在该剧集' };
  1782. return returnVal;
  1783. }
  1784. });
  1785. var playurl_by_bilibili = new BilibiliApi({
  1786. dataType: 'xml',
  1787. transToProxyUrl: function (originUrl) {
  1788. const api_url = 'https://interface.bilibili.com/playurl?'
  1789. const bangumi_api_url = 'https://bangumi.bilibili.com/player/web_api/playurl?'
  1790. const SEC_NORMAL = '1c15888dc316e05a15fdd0a02ed6584f'
  1791. const SEC_BANGUMI = '9b288147e5474dd2aa67085f716c560d'
  1792.  
  1793. // 不设置module; 带module的接口都是有区域限制的...
  1794. let module = undefined /*util_url_param(originUrl, 'module')*/
  1795. // 不使用json; 让服务器直接返回json时, 获取的视频url不能直接播放...天知道为什么
  1796. let useJson = false
  1797. let paramDict = {
  1798. cid: util_url_param(originUrl, 'cid'),
  1799. quality: util_url_param(originUrl, 'quality'),
  1800. qn: util_url_param(originUrl, 'qn'), // 增加这个参数, 返回的清晰度更多
  1801. player: 1,
  1802. ts: Math.floor(Date.now() / 1000),
  1803. }
  1804. if (localStorage.access_key) {
  1805. paramDict.access_key = localStorage.access_key
  1806. }
  1807. if (module) paramDict.module = module
  1808. if (useJson) paramDict.otype = 'json'
  1809. let { sign, params } = util_generate_sign(paramDict, module ? SEC_BANGUMI : SEC_NORMAL)
  1810. let url = module ? bangumi_api_url : api_url + params + '&sign=' + sign
  1811. return url
  1812. },
  1813. processProxySuccess: function (result, alertWhenError = true) {
  1814. // 将xml解析成json
  1815. let obj = util_xml2obj(result.documentElement)
  1816. if (!obj || obj.code) {
  1817. if (alertWhenError) {
  1818. util_ui_alert(`从B站接口获取视频地址失败\nresult: ${JSON.stringify(obj)}\n\n点击确定, 进入设置页面关闭'使用B站接口获取视频地址'功能`, balh_ui_setting.show)
  1819. } else {
  1820. return Promise.reject(`服务器错误: ${JSON.stringify(obj)}`)
  1821. }
  1822. } else {
  1823. obj.accept_quality && (obj.accept_quality = obj.accept_quality.split(',').map(n => +n))
  1824. if (!obj.durl.push) {
  1825. obj.durl = [obj.durl]
  1826. }
  1827. obj.durl.forEach((item) => {
  1828. if (item.backup_url === '') {
  1829. item.backup_url = undefined
  1830. } else if (item.backup_url && item.backup_url.url) {
  1831. item.backup_url = item.backup_url.url
  1832. }
  1833. })
  1834. }
  1835. log('xml2obj', result, '=>', obj)
  1836. return obj
  1837. },
  1838. _asyncAjax: function (originUrl) {
  1839. return util_ajax(this.transToProxyUrl(originUrl))
  1840. .then(r => this.processProxySuccess(r, false))
  1841. }
  1842. })
  1843. var playurl_by_proxy = new BilibiliApi({
  1844. _asyncAjax: function (originUrl, bangumi) {
  1845. return util_ajax(this.transToProxyUrl(originUrl, bangumi))
  1846. .then(r => this.processProxySuccess(r, false))
  1847. },
  1848. transToProxyUrl: function (url, bangumi) {
  1849. let params = url.split('?')[1];
  1850. if (bangumi === undefined) { // 自动判断
  1851. // av页面中的iframe标签形式的player, 不是番剧视频
  1852. bangumi = !util_page.player_in_av()
  1853. // url中存在season_type的情况
  1854. let season_type_param = util_url_param(url, 'season_type')
  1855. if (season_type_param && !isBangumi(+season_type_param)) {
  1856. bangumi = false
  1857. }
  1858. if (!bangumi) {
  1859. params = params.replace(/&?module=(\w+)/, '') // 移除可能存在的module参数
  1860. }
  1861. } else if (bangumi === true) { // 保证添加module=bangumi参数
  1862. params = params.replace(/&?module=(\w+)/, '')
  1863. params += '&module=bangumi'
  1864. } else if (bangumi === false) { // 移除可能存在的module参数
  1865. params = params.replace(/&?module=(\w+)/, '')
  1866. }
  1867. // 管他三七二十一, 强行将module=bangumi替换成module=pgc _(:3」∠)_
  1868. params = params.replace(/(&?module)=bangumi/, '$1=pgc')
  1869. return `${balh_config.server}/BPplayurl.php?${params}${access_key_param_if_exist()}`;
  1870. },
  1871. processProxySuccess: function (data, alertWhenError = true) {
  1872. // data有可能为null
  1873. if (data && data.code === -403) {
  1874. util_ui_pop({
  1875. content: `<b>code-403</b>: <i style="font-size:4px;white-space:nowrap;">${JSON.stringify(data)}</i>\n\n当前代理服务器(${balh_config.server})依然有区域限制\n\n可以考虑进行如下尝试:\n1. 进行“帐号授权”\n2. 换个代理服务器\n3. 耐心等待服务端修复问题\n\n点击确定, 打开设置页面`,
  1876. onConfirm: balh_ui_setting.show,
  1877. })
  1878. } else if (data === null || data.code) {
  1879. util_error(data);
  1880. if (alertWhenError) {
  1881. util_ui_alert(`突破黑洞失败\n${JSON.stringify(data)}\n点击确定刷新界面`, window.location.reload.bind(window.location));
  1882. } else {
  1883. return Promise.reject(new AjaxException(`服务器错误: ${JSON.stringify(data)}`, data ? data.code : 0))
  1884. }
  1885. } else if (isAreaLimitForPlayUrl(data)) {
  1886. util_error('>>area limit');
  1887. util_ui_pop({
  1888. content: `突破黑洞失败\n需要登录(不可用)\n点此确定进行登录(不可用)`,
  1889. onConfirm: balh_feature_sign.showLogin
  1890. })
  1891. } else {
  1892. if (balh_config.flv_prefer_ws) {
  1893. data.durl.forEach(function (seg) {
  1894. var t, url, i;
  1895. if (!seg.url.includes('ws.acgvideo.com')) {
  1896. for (i in seg.backup_url) {
  1897. url = seg.backup_url[i];
  1898. if (url.includes('ws.acgvideo.com')) {
  1899. log('flv prefer use:', url);
  1900. t = seg.url;
  1901. seg.url = url;
  1902. url = t;
  1903. break;
  1904. }
  1905. }
  1906.  
  1907. }
  1908. });
  1909. }
  1910. }
  1911. return data;
  1912. }
  1913. })
  1914. // https://github.com/kghost/bilibili-area-limit/issues/3
  1915. const playurl_by_kghost = new BilibiliApi({
  1916. _asyncAjax: function (originUrl) {
  1917. const proxyHostMap = [
  1918. [/僅.*台.*地區/, '//bilibili-tw-api.kghost.info/'],
  1919. [/僅.*港.*地區/, '//bilibili-hk-api.kghost.info/'],
  1920. [/仅限东南亚/, '//bilibili-sg-api.kghost.info/'],
  1921. [/.*/, '//bilibili-cn-api.kghost.info/'],
  1922. ];
  1923. let proxyHost
  1924. for (const [regex, host] of proxyHostMap) {
  1925. if (document.title.match(regex)) {
  1926. proxyHost = host
  1927. break;
  1928. }
  1929. }
  1930. if (proxyHost) {
  1931. return util_ajax(this.transToProxyUrl(originUrl, proxyHost))
  1932. .then(r => this.processProxySuccess(r))
  1933. } else {
  1934. return Promise.reject("没有支持的服务器")
  1935. }
  1936. },
  1937. transToProxyUrl: function (originUrl, proxyHost) {
  1938. return originUrl.replace(/^(https:)?(\/\/api\.bilibili\.com\/)/, `$1${proxyHost}`) + access_key_param_if_exist(true);
  1939. },
  1940. processProxySuccess: function (result) {
  1941. if (result.code) {
  1942. return Promise.reject(result)
  1943. }
  1944. return result.result
  1945. },
  1946. })
  1947. const playurl_by_custom = new BilibiliApi({
  1948. _asyncAjax: function (originUrl) {
  1949. return util_ajax(this.transToProxyUrl(originUrl, balh_config.server_custom))
  1950. .then(r => this.processProxySuccess(r))
  1951. },
  1952. transToProxyUrl: function (originUrl, proxyHost) {
  1953. return originUrl.replace(/^(https:)?(\/\/api\.bilibili\.com\/)/, `$1${proxyHost}/`) + access_key_param_if_exist(true);
  1954. },
  1955. processProxySuccess: function (result) {
  1956. if (result.code) {
  1957. return Promise.reject(result)
  1958. }
  1959. return result.result
  1960. },
  1961. })
  1962. const playurl = new BilibiliApi({
  1963. asyncAjax: function (originUrl) {
  1964. util_ui_player_msg(`从${r.const.server.CUSTOM?'自定义':'代理'}服务器拉取视频地址中...`)
  1965. return (r.const.server.CUSTOM ? playurl_by_custom._asyncAjax(originUrl) : (playurl_by_proxy._asyncAjax(originUrl) // 优先从代理服务器获取
  1966. .catch(e => {
  1967. if (e instanceof AjaxException) {
  1968. util_ui_player_msg(e)
  1969. if (e.code === 1 // code: 1 表示非番剧视频, 不能使用番剧视频参数
  1970. || (util_url_param(originUrl, 'module') === 'bangumi' && e.code === -404)) { // 某些番剧视频又不需要加module=bangumi, 详见: https://github.com/ipcjs/bilibili-helper/issues/494
  1971. util_ui_player_msg('尝试使用非番剧视频接口拉取视频地址...')
  1972. return playurl_by_proxy._asyncAjax(originUrl, false)
  1973. .catch(e2 => Promise.reject(e)) // 忽略e2, 返回原始错误e
  1974. } else if (e.code === 10004) { // code: 10004, 表示视频被隐藏, 一般添加module=bangumi参数可以拉取到视频
  1975. util_ui_player_msg('尝试使用番剧视频接口拉取视频地址...')
  1976. return playurl_by_proxy._asyncAjax(originUrl, true)
  1977. .catch(e2 => Promise.reject(e))
  1978. }
  1979. }
  1980. return Promise.reject(e)
  1981. })))
  1982. .catch(e => {
  1983. if ((typeof e === 'object' && e.statusText == 'error')
  1984. || (e instanceof AjaxException && e.code === -502)
  1985. ) {
  1986. util_ui_player_msg('尝试使用kghost的服务器拉取视频地址...')
  1987. return playurl_by_kghost._asyncAjax(originUrl)
  1988. .catch(e2 => Promise.reject(e))
  1989. }
  1990. return Promise.reject(e)
  1991. })
  1992. // 报错时, 延时1秒再发送错误信息
  1993. .catch(e => util_promise_timeout(1000).then(r => Promise.reject(e)))
  1994. .catch(e => {
  1995. let msg
  1996. if (typeof e === 'object' && e.statusText == 'error') {
  1997. msg = '代理服务器临时不可用'
  1998. util_ui_player_msg(msg)
  1999. } else {
  2000. msg = util_stringify(e)
  2001. }
  2002. util_ui_pop({
  2003. content: `## 拉取视频地址失败\n原因: ${msg}\n\n可以考虑进行如下尝试:\n1. 多<a href="">刷新</a>几下页面\n2. 进入<a href="javascript:bangumi_area_limit_hack.showSettings();">设置页面</a>更换代理服务器\n3. 耐心等待代理服务器端修复问题`,
  2004. onConfirm: window.location.reload.bind(window.location),
  2005. confirmBtn: '刷新页面'
  2006. })
  2007. return Promise.reject(e)
  2008. })
  2009. .then(data => {
  2010. if (data.dash) {
  2011. // dash中的字段全部变成了类似C语言的下划线风格...
  2012. util_obj_key_to_c_like(data.dash)
  2013. }
  2014. return data
  2015. })
  2016. }
  2017. })
  2018. return {
  2019. _get_source: util_page.movie() ? get_source_by_aid : get_source_by_season_id,
  2020. _playurl: playurl,
  2021. };
  2022. })();
  2023.  
  2024. if (util_page.anime_ep_m() || util_page.anime_ss_m()) {
  2025. // balh_api_plus_playurl_for_mp4返回的url能在移动设备上播放的前提是, 请求头不包含Referer...
  2026. // 故这里设置meta, 使页面不发送Referer
  2027. // 注意动态改变引用策略的方式并不是标准行为, 目前在Chrome上测试是有用的
  2028. document.head.appendChild(_('meta', { name: "referrer", content: "no-referrer" }))
  2029. injectFetch()
  2030. util_init(() => {
  2031. const $wrapper = document.querySelector('.player-wrapper')
  2032. new MutationObserver(function (mutations, observer) {
  2033. for (let mutation of mutations) {
  2034. if (mutation.type === 'childList') {
  2035. for (let node of mutation.addedNodes) {
  2036. if (node.tagName === 'DIV' && node.className.split(' ').includes('player-mask')) {
  2037. log('隐藏添加的mask')
  2038. node.style.display = 'none'
  2039. }
  2040. }
  2041. }
  2042. }
  2043. }).observe($wrapper, {
  2044. childList: true,
  2045. attributes: false,
  2046. });
  2047. })
  2048. }
  2049. injectXHR();
  2050. if (true) {
  2051. let jQuery = window.jQuery;
  2052. if (jQuery) { // 若已加载jQuery, 则注入
  2053. injectAjax()
  2054. }
  2055. // 需要监听jQuery变化, 因为有时会被设置多次...
  2056. Object.defineProperty(window, 'jQuery', {
  2057. configurable: true, enumerable: true, set: function (v) {
  2058. // debugger
  2059. log('set jQuery', jQuery, '->', v)
  2060. // 临时规避这个问题:https://github.com/ipcjs/bilibili-helper/issues/297
  2061. // 新的av页面中, 运行脚本的 injectXHR() 后, 页面会往该方法先后设置两个jQuery...原因未知
  2062. // 一个从jquery.min.js中设置, 一个从player.js中设置
  2063. // 并且点击/载入等事件会从两个jQuery中向下分发...导致很多功能失常
  2064. // 这里我们屏蔽掉jquery.min.js分发的一些事件, 避免一些问题
  2065. if (util_page.av_new() && balh_config.enable_in_av) {
  2066. try { // 获取调用栈的方法不是标准方法, 需要try-catch
  2067. const stack = (new Error()).stack.split('\n')
  2068. if (stack[stack.length - 1].includes('jquery')) { // 若从jquery.min.js中调用
  2069. log('set jQueury by jquery.min.js', v)
  2070. v.fn.balh_on = v.fn.on
  2071. v.fn.on = function (arg0, arg1) {
  2072. if (arg0 === 'click.reply' && arg1 === '.reply') {
  2073. // 屏蔽掉"回复"按钮的点击事件
  2074. log('block click.reply', arguments)
  2075. return
  2076. }
  2077. return v.fn.balh_on.apply(this, arguments)
  2078. }
  2079. }
  2080. // jQuery.fn.paging方法用于创建评论区的页标, 需要迁移到新的jQuery上
  2081. if (jQuery != null && jQuery.fn.paging != null
  2082. && v != null && v.fn.paging == null) {
  2083. log('迁移jQuery.fn.paging')
  2084. v.fn.paging = jQuery.fn.paging
  2085. }
  2086. } catch (e) {
  2087. util_error(e)
  2088. }
  2089. }
  2090.  
  2091. jQuery = v;
  2092. injectAjax();// 设置jQuery后, 立即注入
  2093. }, get: function () {
  2094. return jQuery;
  2095. }
  2096. });
  2097. }
  2098. }())
  2099. const balh_feature_remove_pre_ad = (function () {
  2100. if (util_page.player()) {
  2101. // 播放页面url中的pre_ad参数, 决定是否播放广告...
  2102. if (balh_config.remove_pre_ad && util_url_param(location.href, 'pre_ad') == 1) {
  2103. log('需要跳转到不含广告的url')
  2104. location.href = location.href.replace(/&?pre_ad=1/, '')
  2105. }
  2106. }
  2107. }())
  2108. const balh_feature_check_html5 = (function () {
  2109. function isHtml5Player() {
  2110. return localStorage.defaulth5 === '1'
  2111. }
  2112.  
  2113. function checkHtml5() {
  2114. var playerContent = document.querySelector('.player-content');
  2115. if (!localStorage.balh_h5_not_first && !isHtml5Player() && window.GrayManager && playerContent) {
  2116. new MutationObserver(function (mutations, observer) {
  2117. observer.disconnect();
  2118. localStorage.balh_h5_not_first = r.const.TRUE;
  2119. if (window.confirm(GM_info.script.name + '只在HTML5播放器下有效,是否切换到HTML5?')) {
  2120. window.GrayManager.clickMenu('change_h5');// change_flash, change_h5
  2121. }
  2122. }).observe(playerContent, {
  2123. childList: true, // 监听child的增减
  2124. attributes: false, // 监听属性的变化
  2125. });
  2126. }
  2127. }
  2128.  
  2129. util_init(() => {
  2130. // 除了播放器和番剧列表页面, 其他页面都需要检测html5
  2131. if (!(util_page.bangumi() || util_page.bangumi_md() || util_page.player())) {
  2132. checkHtml5()
  2133. }
  2134. })
  2135. return isHtml5Player
  2136. }())
  2137. const balh_feature_runPing = function () {
  2138. var pingOutput = document.getElementById('balh_server_ping');
  2139.  
  2140. var xhr = new XMLHttpRequest(), testUrl = [r.const.server.S0, r.const.server.S1],
  2141. testUrlIndex = 0, isReused = false, prevNow, outputArr = [];
  2142. if (balh_config.server_custom) {
  2143. testUrl.push(balh_config.server_custom)
  2144. }
  2145. pingOutput.textContent = '正在进行服务器测速…';
  2146. pingOutput.style.height = '100px';
  2147. xhr.open('GET', '', true);
  2148. xhr.onreadystatechange = function () {
  2149. this.readyState == 4 && pingResult();
  2150. };
  2151. var pingLoop = function () {
  2152. prevNow = performance.now();
  2153. xhr.open('GET', testUrl[testUrlIndex] + '/api/bangumi', true);
  2154. xhr.send();
  2155. };
  2156. var pingResult = function () {
  2157. var duration = (performance.now() - prevNow) | 0;
  2158. if (isReused)
  2159. outputArr.push('\t复用连接:' + duration + 'ms'), isReused = false, testUrlIndex++;
  2160. else
  2161. outputArr.push(testUrl[testUrlIndex] + ':'), outputArr.push('\t初次连接:' + duration + 'ms'), isReused = true;
  2162. pingOutput.textContent = outputArr.join('\n');
  2163. testUrlIndex < testUrl.length ? pingLoop() : pingOutput.appendChild(_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '\n再测一次?')]));
  2164. };
  2165. pingLoop();
  2166. }
  2167. const balh_feature_sign = (function () {
  2168. function isLogin() {
  2169. return localStorage.oauthTime !== undefined
  2170. }
  2171. function clearLoginFlag() {
  2172. delete localStorage.oauthTime
  2173. }
  2174.  
  2175. function updateLoginFlag(loadCallback) {
  2176. util_jsonp(balh_config.server + '/login?act=expiretime')
  2177. .then(() => loadCallback && loadCallback(true))
  2178. // .catch(() => loadCallback && loadCallback(false)) // 请求失败不需要回调
  2179. }
  2180. function isLoginBiliBili() {
  2181. return util_cookie['DedeUserID'] !== undefined
  2182. }
  2183. // 当前在如下情况才会弹一次登录(不可用)提示框:
  2184. // 1. 第一次使用
  2185. // 2. 主站+服务器都退出登录(不可用)后, 再重新登录(不可用)主站
  2186. function checkLoginState() {
  2187. // 给一些状态,设置初始值
  2188. localStorage.balh_must_remind_login_v3 === undefined && (localStorage.balh_must_remind_login_v3 = r.const.TRUE)
  2189.  
  2190. if (isLoginBiliBili()) {
  2191. if (!localStorage.balh_old_isLoginBiliBili // 主站 不登录(不可用) => 登录(不可用)
  2192. || localStorage.balh_pre_server !== balh_config.server // 代理服务器改变了
  2193. || localStorage.balh_must_remind_login_v3) { // 设置了"必须提醒"flag
  2194. clearLoginFlag()
  2195. updateLoginFlag(() => {
  2196. if (!isLogin() || !localStorage.access_key) {
  2197. localStorage.balh_must_remind_login_v3 = r.const.FALSE;
  2198. util_ui_pop({
  2199. content: [
  2200. _('text', `${GM_info.script.name}\n要不要考虑进行一下授权?\n\n授权后可以观看区域限定番剧的1080P\n(如果你是大会员或承包过这部番的话)\n\n你可以随时在设置中打开授权页面`)
  2201. ],
  2202. onConfirm: () => {
  2203. balh_feature_sign.showLogin();
  2204. document.querySelector('#AHP_Notice').remove()
  2205. }
  2206. })
  2207. }
  2208. })
  2209. } else if ((isLogin() && Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000) // 已登录(不可用),每天为周期检测key有效期,过期前五天会自动续期
  2210. || localStorage.balh_must_updateLoginFlag) {// 某些情况下,必须更新一次
  2211. updateLoginFlag(() => localStorage.balh_must_updateLoginFlag = r.const.FALSE);
  2212. }
  2213. }
  2214. localStorage.balh_old_isLoginBiliBili = isLoginBiliBili() ? r.const.TRUE : r.const.FALSE
  2215. localStorage.balh_pre_server = balh_config.server
  2216. }
  2217.  
  2218. function showLogin() {
  2219. const balh_auth_window = window.open('about:blank');
  2220. balh_auth_window.document.title = 'BALH - 授权';
  2221. balh_auth_window.document.body.innerHTML = '<meta charset="UTF-8" name="viewport" content="width=device-width">正在获取授权,请稍候……';
  2222. window.balh_auth_window = balh_auth_window;
  2223. $.ajax('https://passport.bilibili.com/login/app/third?appkey=27eb53fc9058f8c3&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a', {
  2224. xhrFields: { withCredentials: true },
  2225. type: 'GET',
  2226. dataType: 'json',
  2227. success: (data) => {
  2228. if (data.data.has_login) {
  2229. balh_auth_window.document.body.innerHTML = '<meta charset="UTF-8" name="viewport" content="width=device-width">正在跳转……';
  2230. balh_auth_window.location.href = data.data.confirm_uri;
  2231. } else {
  2232. balh_auth_window.close()
  2233. util_ui_alert('必须登录(不可用)B站才能正常授权', () => {
  2234. location.href = 'https://passport.bilibili.com/login'
  2235. })
  2236. }
  2237. },
  2238. error: (e) => {
  2239. alert('error');
  2240. }
  2241. })
  2242. }
  2243.  
  2244. function showLoginByPassword() {
  2245. const loginUrl = balh_config.server + '/login'
  2246. util_ui_pop({
  2247. content: `B站当前关闭了第三方登录(不可用)的接口<br>目前只能使用帐号密码的方式<a href="${loginUrl}">登录(不可用)代理服务器</a><br><br>登录(不可用)完成后, 请手动刷新当前页面`,
  2248. confirmBtn: '前往登录(不可用)页面',
  2249. onConfirm: () => {
  2250. window.open(loginUrl)
  2251. }
  2252. })
  2253. }
  2254.  
  2255. function showLogout() {
  2256. util_ui_popframe(balh_config.server + '/login?act=logout')
  2257. }
  2258.  
  2259. // 监听登录(不可用)message
  2260. window.addEventListener('message', function (e) {
  2261. if (typeof e.data !== 'string') return // 只处理e.data为string的情况
  2262. switch (e.data.split(':')[0]) {
  2263. case 'BiliPlus-Login-Success': {
  2264. //登入
  2265. localStorage.balh_must_updateLoginFlag = r.const.TRUE
  2266. Promise.resolve('start')
  2267. .then(() => util_jsonp(balh_config.server + '/login?act=getlevel'))
  2268. .then(() => location.reload())
  2269. .catch(() => location.reload())
  2270. break;
  2271. }
  2272. case 'BiliPlus-Logout-Success': {
  2273. //登出
  2274. clearLoginFlag()
  2275. location.reload()
  2276. break;
  2277. }
  2278. case 'balh-login-credentials': {
  2279. balh_auth_window.close();
  2280. let url = e.data.split(': ')[1];
  2281. const access_key = new URL(url).searchParams.get('access_key');
  2282. localStorage.access_key = access_key
  2283. util_ui_popframe(url.replace('https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png', balh_config.server + '/login'));
  2284. break;
  2285. }
  2286. }
  2287. })
  2288.  
  2289.  
  2290. util_init(() => {
  2291. if (!(util_page.player() || util_page.av())) {
  2292. checkLoginState()
  2293. }
  2294. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  2295. return {
  2296. showLogin,
  2297. showLogout,
  2298. isLogin,
  2299. isLoginBiliBili,
  2300. }
  2301. }())
  2302. const balh_feature_RedirectToBangumiOrInsertPlayer = (function () {
  2303. // 重定向到Bangumi页面, 或者在当前页面直接插入播放页面
  2304. function tryRedirectToBangumiOrInsertPlayer() {
  2305. let $errorPanel;
  2306. if (!($errorPanel = document.querySelector('.error-container > .error-panel'))) {
  2307. return;
  2308. }
  2309. let msg = document.createElement('a');
  2310. $errorPanel.insertBefore(msg, $errorPanel.firstChild);
  2311. msg.innerText = '获取番剧页Url中...';
  2312. let aid = (location.pathname.match('/\/video\/av(\d+)') || ['', ''])[1],
  2313. page = (location.pathname.match(/\/index_(\d+).html/) || ['', '1'])[1],
  2314. cid,
  2315. season_id,
  2316. episode_id;
  2317. let avData;
  2318. if (!aid) {
  2319. let bv = (location.pathname.match(/\/video\/(BV\w+)/) || ['', ''])[1]
  2320. if (bv) {
  2321. aid = util_str_bv2aid(bv)
  2322. }
  2323. }
  2324. balh_api_plus_view(aid)
  2325. .then(function (data) {
  2326. avData = data;
  2327. if (data.code) {
  2328. return Promise.reject(JSON.stringify(data));
  2329. }
  2330. // 计算当前页面的cid
  2331. for (let i = 0; i < data.list.length; i++) {
  2332. if (data.list[i].page == page) {
  2333. cid = data.list[i].cid;
  2334. break;
  2335. }
  2336. }
  2337. if (!data.bangumi) {
  2338. generatePlayer(data, aid, page, cid)
  2339. // return Promise.reject('该AV号不属于任何番剧页');//No bangumi in api response
  2340. } else {
  2341. // 当前av属于番剧页面, 继续处理
  2342. season_id = data.bangumi.season_id;
  2343. return balh_api_plus_season(season_id);
  2344. }
  2345. })
  2346. .then(function (result) {
  2347. if (result === undefined) return // 上一个then不返回内容时, 不需要处理
  2348. if (result.code === 10) { // av属于番剧页面, 通过接口却未能找到番剧信息
  2349. let ep_id_newest = avData && avData.bangumi && avData.bangumi.newest_ep_id
  2350. if (ep_id_newest) {
  2351. episode_id = ep_id_newest // 此时, 若avData中有最新的ep_id, 则直接使用它
  2352. } else {
  2353. log(`av${aid}属于番剧${season_id}, 但却不能找到番剧页的信息, 试图直接创建播放器`)
  2354. generatePlayer(avData, aid, page, cid)
  2355. return
  2356. }
  2357. } else if (result.code) {
  2358. return Promise.reject(JSON.stringify(result))
  2359. } else {
  2360. let ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid,
  2361. episodes = result.result.episodes,
  2362. ep
  2363. // 为何要用三种不同方式匹配, 详见: https://gf.qytechs.cn/zh-CN/forum/discussion/22379/x#Comment_34127
  2364. for (let i = 0; i < episodes.length; i++) {
  2365. ep = episodes[i]
  2366. if (ep.danmaku == cid) {
  2367. ep_id_by_cid = ep.episode_id
  2368. }
  2369. if (ep.av_id == aid && ep.page == page) {
  2370. ep_id_by_aid_page = ep.episode_id
  2371. }
  2372. if (ep.av_id == aid) {
  2373. ep_id_by_aid = ep.episode_id
  2374. }
  2375. }
  2376. episode_id = ep_id_by_cid || ep_id_by_aid_page || ep_id_by_aid
  2377. }
  2378. if (episode_id) {
  2379. let bangumi_url = `//www.bilibili.com/bangumi/play/ss${season_id}#${episode_id}`
  2380. log('Redirect', 'aid:', aid, 'page:', page, 'cid:', cid, '==>', bangumi_url, 'season_id:', season_id, 'ep_id:', episode_id)
  2381. msg.innerText = '即将跳转到:' + bangumi_url
  2382. location.href = bangumi_url
  2383. } else {
  2384. return Promise.reject('查询episode_id失败')
  2385. }
  2386. })
  2387. .catch(function (e) {
  2388. log('error:', arguments);
  2389. msg.innerText = 'error:' + e;
  2390. });
  2391. }
  2392.  
  2393. function generatePlayer(data, aid, page, cid) {
  2394. let generateSrc = function (aid, cid) {
  2395. return `//www.bilibili.com/blackboard/html5player.html?cid=${cid}&aid=${aid}&player_type=1`;
  2396. }
  2397. let generatePageList = function (pages) {
  2398. let $curPage = null;
  2399. function onPageBtnClick(e) {
  2400. e.target.className = 'curPage'
  2401. $curPage && ($curPage.className = '')
  2402.  
  2403. let index = e.target.attributes['data-index'].value;
  2404. iframe.src = generateSrc(aid, pages[index].cid);
  2405. }
  2406.  
  2407. return pages.map(function (item, index) {
  2408. let isCurPage = item.page == page
  2409. let $item = _('a', { 'data-index': index, className: isCurPage ? 'curPage' : '', event: { click: onPageBtnClick } }, [_('text', item.page + ': ' + item.part)])
  2410. if (isCurPage) $curPage = $item
  2411. return $item
  2412. });
  2413. }
  2414. // 当前av不属于番剧页面, 直接在当前页面插入一个播放器的iframe
  2415. let $pageBody = document.querySelector('.b-page-body');
  2416. if (!$pageBody) { // 若不存在, 则创建
  2417. $pageBody = _('div', { className: '.b-page-body' });
  2418. document.querySelector('body').insertBefore($pageBody, document.querySelector('#app'))
  2419. // 添加相关样式
  2420. document.head.appendChild(_('link', { type: 'text/css', rel: 'stylesheet', href: '//static.hdslb.com/css/core-v5/page-core.css' }))
  2421. }
  2422. let iframe = _('iframe', { className: 'player bilibiliHtml5Player', style: { position: 'relative' }, src: generateSrc(aid, cid) });
  2423.  
  2424. // 添加播放器
  2425. $pageBody.appendChild(_('div', { className: 'player-wrapper' }, [
  2426. _('div', { className: 'main-inner' }, [
  2427. _('div', { className: 'v-plist' }, [
  2428. _('div', { id: 'plist', className: 'plist-content open' }, generatePageList(data.list))
  2429. ])
  2430. ]),
  2431. _('div', { id: 'bofqi', className: 'scontent' }, [iframe])
  2432. ]));
  2433. // 添加评论区
  2434. $pageBody.appendChild(_('div', { className: 'main-inner' }, [
  2435. _('div', { className: 'common report-scroll-module report-wrap-module', id: 'common_report' }, [
  2436. _('div', { className: 'b-head' }, [
  2437. _('span', { className: 'b-head-t results' }),
  2438. _('span', { className: 'b-head-t' }, [_('text', '评论')]),
  2439. _('a', { className: 'del-log', href: `//www.bilibili.com/replydeletelog?aid=${aid}&title=${data.title}`, target: '_blank' }, [_('text', '查看删除日志')])
  2440. ]),
  2441. _('div', { className: 'comm', id: 'bbComment' }, [
  2442. _('div', { id: 'load_comment', className: 'comm_open_btn', onclick: "var fb = new bbFeedback('.comm', 'arc');fb.show(" + aid + ", 1);", style: { cursor: 'pointer' } })
  2443. ])
  2444. ])
  2445. ]));
  2446. // 添加包含bbFeedback的js
  2447. document.head.appendChild(_('script', { type: 'text/javascript', src: '//static.hdslb.com/js/core-v5/base.core.js' }))
  2448.  
  2449. document.title = data.title;
  2450. (document.querySelector('.error-body') || document.querySelector('.error-container')).remove(); // 移除错误信息面板
  2451. }
  2452.  
  2453. util_init(() => {
  2454. if (util_page.av()) {
  2455. tryRedirectToBangumiOrInsertPlayer()
  2456. }
  2457. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.COMPLETE)
  2458. return true // 随便返回一个值...
  2459. }())
  2460. const balh_feature_FillSeasonList = (function () {
  2461. function tryFillSeasonList() {
  2462. var error_container, season_id;
  2463. if (!(error_container = document.querySelector('div.error-container'))) {
  2464. return;
  2465. }
  2466. if (!(season_id = window.location.pathname.match(/^\/anime\/(\d+)\/?$/)[1])) {
  2467. return;
  2468. }
  2469.  
  2470. //尝试解决怪异模式渲染
  2471. /*
  2472. 会造成变量丢失,等待官方重写doctype
  2473. try{
  2474. window.stop();
  2475. var xhr = new XMLHttpRequest();
  2476. xhr.open('GET',location.href,false);
  2477. xhr.send();
  2478. document.head.appendChild(_('script',{},[_('text',
  2479. 'document.write(unescape("'+escape(xhr.response.replace(/<!DOCTYPE.+?>/,'<!DOCTYPE HTML>'))+'"));window.stop()'
  2480. )]));
  2481. }catch(e){util_error(e);}
  2482. */
  2483.  
  2484. var msg = _('a', { href: '//bangumi.bilibili.com/anime/' + season_id + '/play', style: { fontSize: '20px' } }, [_('text', `【${GM_info.script.name}】尝试获取视频列表中...`)]),
  2485. content = _('div');
  2486.  
  2487. error_container.insertBefore(content, error_container.firstChild);
  2488. content.appendChild(msg);
  2489. log('season>:', season_id);
  2490. balh_api_plus_season(season_id)
  2491. .then(function (data) {
  2492. log('season>then:', data);
  2493. if (data.code) {
  2494. return Promise.reject(data);
  2495. }
  2496.  
  2497. function generateEpisodeList(episodes) {
  2498. var childs = [];
  2499. episodes.reverse().forEach(function (i) {
  2500. 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' } }, [
  2501. _('div', { className: 'img-wrp' }, [_('img', { src: i.cover, style: { opacity: 1 }, loaded: 'loaded', alt: i.index + ' ' + i.index_title })]),
  2502. _('div', { className: 'text-wrp' }, [
  2503. _('div', { className: 'text-wrp-num' }, [_('div', { className: 'text-wrp-num-content' }, [_('text', `第${i.index}话`)])]),
  2504. _('div', { className: 'text-wrp-title trunc' }, [_('text', i.index_title)])
  2505. ])
  2506. ])]));
  2507. });
  2508. return childs;
  2509. }
  2510.  
  2511. function generateSeasonList(seasons) {
  2512. function onSeasonClick(event) {
  2513. window.location.href = '//bangumi.bilibili.com/anime/' + event.target.attributes['data-season-id'].value;
  2514. }
  2515.  
  2516. return seasons.map(function (season) {
  2517. return _('li', { className: season.season_id == season_id ? 'cur' : '', 'data-season-id': season.season_id, event: { click: onSeasonClick } }, [_('text', season.title)]);
  2518. });
  2519. }
  2520.  
  2521. if (data.result) {
  2522. document.title = data.result.title;
  2523. document.head.appendChild(_('link', { href: 'https://s3.hdslb.com/bfs/static/anime/css/tag-index.css?v=110', rel: 'stylesheet' }));
  2524. document.head.appendChild(_('link', { href: 'https://s1.hdslb.com/bfs/static/anime/css/bangumi-index.css?v=110', rel: 'stylesheet' }));
  2525. document.body.insertBefore(_('div', { className: 'main-container-wrapper' }, [_('div', { className: 'main-container' }, [
  2526. _('div', { className: 'page-info-wrp' }, [_('div', { className: 'bangumi-info-wrapper' }, [
  2527. _('div', { className: 'bangumi-info-blurbg-wrapper' }, [_('div', { className: 'bangumi-info-blurbg blur', style: { backgroundImage: 'url(' + data.result.cover + ')' } })]),
  2528. _('div', { className: 'main-inner' }, [_('div', { className: 'info-content' }, [
  2529. _('div', { className: 'bangumi-preview' }, [_('img', { alt: data.result.title, src: data.result.cover })]),
  2530. _('div', { className: 'bangumi-info-r' }, [
  2531. _('div', { className: 'b-head' }, [_('h1', { className: 'info-title', 'data-seasonid': season_id, title: data.result.title }, [_('text', data.result.title)])]),
  2532. _('div', { className: 'info-count' }, [
  2533. _('span', { className: 'info-count-item info-count-item-play' }, [_('span', { className: 'info-label' }, [_('text', '总播放')]), _('em', {}, [_('text', data.result.play_count)])]),
  2534. _('span', { className: 'info-count-item info-count-item-fans' }, [_('span', { className: 'info-label' }, [_('text', '追番人数')]), _('em', {}, [_('text', data.result.favorites)])]),
  2535. _('span', { className: 'info-count-item info-count-item-review' }, [_('span', { className: 'info-label' }, [_('text', '弹幕总数')]), _('em', {}, [_('text', data.result.danmaku_count)])])
  2536. ]),
  2537. //_('div',{className:'info-row info-update'},[]),
  2538. //_('div',{className:'info-row info-cv'},[]),
  2539. _('div', { className: 'info-row info-desc-wrp' }, [
  2540. _('div', { className: 'info-row-label' }, [_('text', '简介:')]),
  2541. _('div', { className: 'info-desc' }, [_('text', data.result.evaluate)])
  2542. ]),
  2543. ])
  2544. ])])
  2545. ])]),
  2546. _('div', { className: 'main-inner' }, [_('div', { className: 'v1-bangumi-list-wrapper clearfix' }, [
  2547. _('div', { className: 'v1-bangumi-list-season-wrapper' }, [
  2548. _('div', { className: 'v1-bangumi-list-season-content slider-list-content' }, [
  2549. _('div', {}, [
  2550. _('ul', { className: 'v1-bangumi-list-season clearfix slider-list', 'data-current-season-id': season_id, style: { opacity: 1 } }, generateSeasonList(data.result.seasons))
  2551. ])
  2552. ])
  2553. ]),
  2554. _('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' } }, [
  2555. _('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))])])])
  2556. ])])
  2557. ])])
  2558. ])]), msg.parentNode.parentNode);
  2559. msg.parentNode.parentNode.remove();
  2560. }
  2561. })
  2562. .catch(function (error) {
  2563. log('season>catch', error);
  2564. msg.innerText = 'error:' + JSON.stringify(error) + '\n点击跳转到播放界面 (不一定能够正常播放...)';
  2565. });
  2566. }
  2567.  
  2568. util_init(() => {
  2569. if (util_page.bangumi()) {
  2570. tryFillSeasonList()
  2571. }
  2572. })
  2573. return true
  2574. }())
  2575.  
  2576. const balh_ui_setting = (function () {
  2577. function addSettingsButton() {
  2578. let indexNav = document.querySelector('.bangumi-nav-right, #index_nav, #fixnav_report')
  2579. let settingBtnSvgContainer
  2580. const createBtnStyle = (size, diffCss) => {
  2581. diffCss = diffCss || `
  2582. #balh-settings-btn {
  2583. bottom: 110px;
  2584. border: 1px solid #e5e9ef;
  2585. border-radius: 4px;
  2586. background: #f6f9fa;
  2587. margin-top: 4px;
  2588. }
  2589. #balh-settings-btn .btn-gotop {
  2590. text-align: center;
  2591. }
  2592. `
  2593. return _('style', {}, [_('text', `
  2594. ${diffCss}
  2595. #balh-settings-btn {
  2596. width: ${size};
  2597. height: ${size};
  2598. cursor: pointer;
  2599. }
  2600. #balh-settings-btn:hover {
  2601. background: #00a1d6;
  2602. border-color: #00a1d6;
  2603. }
  2604. #balh-settings-btn .icon-saturn {
  2605. width: 30px;
  2606. height: ${size};
  2607. fill: rgb(153,162,170);
  2608. }
  2609. #balh-settings-btn:hover .icon-saturn {
  2610. fill: white;
  2611. }
  2612. `)])
  2613. }
  2614. if (indexNav == null) {
  2615. // 信息页添加到按钮右侧
  2616. if (util_page.bangumi_md()) {
  2617. indexNav = document.querySelector('.media-info-btns');
  2618. indexNav.appendChild(createBtnStyle('44px', `
  2619. #balh-settings-btn {
  2620. float: left;
  2621. margin: 3px 0 0 20px;
  2622. background: #FFF;
  2623. border-radius: 10px;
  2624. }
  2625. #balh-settings-btn>:first-child {
  2626. text-align: center;
  2627. height: 100%;
  2628. }
  2629. `))
  2630. } else {
  2631. // 新版视频页面的“返回页面顶部”按钮, 由Vue控制, 对内部html的修改会被重置, 故只能重新创建新的indexNav
  2632. let navTools = document.querySelector('.nav-tools, .float-nav')
  2633. if (navTools) {
  2634. let bottom = navTools.className.includes('float-nav') ? '53px' : '45px'
  2635. indexNav = document.body.appendChild(_('div', { style: { position: 'fixed', right: '6px', bottom: bottom, zIndex: '129', textAlign: 'center', display: 'none' } }))
  2636. indexNav.appendChild(createBtnStyle('45px'))
  2637. window.addEventListener('scroll', (event) => {
  2638. indexNav.style.display = window.scrollY < 600 ? 'none' : ''
  2639. })
  2640. }
  2641. }
  2642. if (indexNav) {
  2643. settingBtnSvgContainer = indexNav.appendChild(_('div', { id: 'balh-settings-btn', title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', {})])).firstChild;
  2644. }
  2645. } else {
  2646. // 视频页添加到回顶部下方
  2647. window.dispatchEvent(new Event('resize'));
  2648. indexNav.style.display = 'block';
  2649. indexNav.appendChild(createBtnStyle('46px'))
  2650. settingBtnSvgContainer = indexNav.appendChild(_('div', { id: 'balh-settings-btn', title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', { className: 'btn-gotop' })])).firstChild;
  2651. }
  2652. 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>`);
  2653. }
  2654.  
  2655. function _showSettings() {
  2656. document.body.appendChild(settingsDOM);
  2657. var form = settingsDOM.querySelector('form');
  2658. // elements包含index的属性, 和以name命名的属性, 其中以name命名的属性是不可枚举的, 只能通过这种方式获取出来
  2659. Object.getOwnPropertyNames(form.elements).forEach(function (name) {
  2660. if (name.startsWith('balh_')) {
  2661. var key = name.replace('balh_', '')
  2662. var ele = form.elements[name]
  2663. if (ele.type === 'checkbox') {
  2664. ele.checked = balh_config[key];
  2665. } else {
  2666. ele.value = balh_config[key];
  2667. }
  2668. }
  2669. })
  2670. document.body.style.overflow = 'hidden';
  2671. }
  2672.  
  2673. // 往顶层窗口发显示设置的请求
  2674. function showSettings() {
  2675. window.top.postMessage('balh-show-setting', '*')
  2676. }
  2677.  
  2678. // 只有顶层窗口才接收请求
  2679. if (window === window.top) {
  2680. window.addEventListener('message', (event) => {
  2681. if (event.data === 'balh-show-setting') {
  2682. _showSettings();
  2683. $('#upos-server')[0].value = balh_config.upos_server || '';
  2684. }
  2685. })
  2686. }
  2687.  
  2688. function onSignClick(event) {
  2689. settingsDOM.click();
  2690. switch (event.target.attributes['data-sign'].value) {
  2691. default:
  2692. case 'in':
  2693. balh_feature_sign.showLogin();
  2694. break;
  2695. case 'out':
  2696. balh_feature_sign.showLogout();
  2697. break;
  2698. }
  2699. }
  2700.  
  2701. function onSettingsFormChange(e) {
  2702. var name = e.target.name;
  2703. var value = e.target.type === 'checkbox' ? (e.target.checked ? r.const.TRUE : r.const.FALSE) : e.target.value.trim()
  2704. balh_config[name.replace('balh_', '')] = value
  2705. log(name, ' => ', value);
  2706. }
  2707.  
  2708. // 第一次点击时:
  2709. // 1. '复制日志&问题反馈' => '复制日志'
  2710. // 2. 显示'问题反馈'
  2711. // 3. 复制成功后请求跳转到GitHub
  2712. // 之后的点击, 只是正常的复制功能~~
  2713. function onCopyClick(event) {
  2714. let issueLink = document.getElementById('balh-issue-link')
  2715. let continueToIssue = issueLink.style.display === 'none'
  2716. if (continueToIssue) {
  2717. issueLink.style.display = 'inline'
  2718. let copyBtn = document.getElementById('balh-copy-log')
  2719. copyBtn.innerText = '复制日志'
  2720. }
  2721.  
  2722. let textarea = document.getElementById('balh-textarea-copy')
  2723. textarea.style.display = 'inline-block'
  2724. if (util_ui_copy(util_log_hub.getAllMsg(), textarea)) {
  2725. textarea.style.display = 'none'
  2726. util_ui_msg.show($(this),
  2727. continueToIssue ? '复制日志成功; 点击确定, 继续提交问题(需要GitHub帐号)\n请把日志粘贴到问题描述中' : '复制成功',
  2728. continueToIssue ? 0 : 3e3,
  2729. continueToIssue ? 'button' : undefined,
  2730. continueToIssue ? openIssuePage : undefined)
  2731. } else {
  2732. util_ui_msg.show($(this), '复制失败, 请从下面的文本框手动复制', 5e3)
  2733. }
  2734. }
  2735.  
  2736. function openIssuePage() {
  2737. // window.open(r.url.issue)
  2738. window.open(r.url.readme)
  2739. }
  2740.  
  2741. let printSystemInfoOk = false
  2742.  
  2743. // 鼠标移入设置底部的时候, 打印一些系统信息, 方便问题反馈
  2744. function onMouseEnterSettingBottom(event) {
  2745. if (!printSystemInfoOk) {
  2746. printSystemInfoOk = true
  2747. util_debug('userAgent', navigator.userAgent)
  2748. }
  2749. }
  2750.  
  2751. let customServerCheckText
  2752. 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(); } } }, [
  2753. _('style', {}, [_('text', r.css.settings)]),
  2754. _('div', { style: { position: 'absolute', background: '#FFF', borderRadius: '10px', padding: '20px', top: '50%', left: '50%', width: '600px', transform: 'translate(-50%,-50%)', cursor: 'default' } }, [
  2755. _('h1', {}, [_('text', `${GM_info.script.name} v${GM_info.script.version} 参数设置`)]),
  2756. _('br'),
  2757. _('form', { id: 'balh-settings-form', event: { change: onSettingsFormChange } }, [
  2758. _('text', '代理服务器:'), _('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '测速')]), _('br'),
  2759. _('div', { style: { display: 'flex' } }, [
  2760. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server_inner', value: r.const.server.S0 }), _('text', '土豆服')]),
  2761. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server_inner', value: r.const.server.S1 }), _('text', 'BiliPlus')]),
  2762. _('label', { style: { flex: 2 } }, [
  2763. _('input', { type: 'radio', name: 'balh_server_inner', value: r.const.server.CUSTOM }), _('text', `自定义: `),
  2764. _('input', {
  2765. type: 'text', name: 'balh_server_custom', placeholder: '形如:https://hd.pilipili.com', event: {
  2766. input: (event) => {
  2767. customServerCheckText.innerText = /^https?:\/\/[\w.]+$/.test(event.target.value.trim()) ? '✔️' : '❌'
  2768. onSettingsFormChange(event)
  2769. }
  2770. }
  2771. }),
  2772. customServerCheckText = _('span'),
  2773. ]),
  2774. ]), _('br'),
  2775. _('div', { id: 'balh_server_ping', style: { whiteSpace: 'pre-wrap', overflow: 'auto' } }, []),
  2776. _('div', { style: { display: '' } }, [ // 这个功能貌似没作用了...隐藏掉 => 貌似还有用...重新显示
  2777. _('text', 'upos服务器:'), _('br'),
  2778. _('div', { title: '变更后 切换清晰度 或 刷新 生效' }, [
  2779. _('input', { style: { visibility: 'hidden' }, type: 'checkbox' }),
  2780. _('text', '替换upos视频服务器:'),
  2781. _('select', {
  2782. id: 'upos-server',
  2783. event: {
  2784. change: function () {
  2785. let server = this.value;
  2786. let message = $('#upos-server-message');
  2787. let clearMsg = function () { message.text('') }
  2788. message.text('保存中...')
  2789. $.ajax(balh_config.server + '/api/setUposServer?server=' + server, {
  2790. xhrFields: { withCredentials: true },
  2791. dataType: 'json',
  2792. success: function (json) {
  2793. if (json.code == 0) {
  2794. message.text('已保存');
  2795. setTimeout(clearMsg, 3e3);
  2796. balh_config.upos_server = server;
  2797. }
  2798. },
  2799. error: function () {
  2800. message.text('保存出错');
  2801. setTimeout(clearMsg, 3e3);
  2802. }
  2803. })
  2804. }
  2805. }
  2806. }, [
  2807. _('option', { value: "" }, [_('text', '不替换')]),
  2808. _('option', { value: "ks3u" }, [_('text', 'ks3(金山)')]),
  2809. _('option', { value: "kodou" }, [_('text', 'kodo(七牛)')]),
  2810. _('option', { value: "cosu" }, [_('text', 'cos(腾讯)')]),
  2811. _('option', { value: "bosu" }, [_('text', 'bos(百度)')]),
  2812. _('option', { value: "wcsu" }, [_('text', 'wcs(网宿)')]),
  2813. _('option', { value: "xycdn" }, [_('text', 'xycdn(迅雷)')]),
  2814. _('option', { value: "hw" }, [_('text', 'hw(251)')]),
  2815. ]),
  2816. _('span', { 'id': 'upos-server-message' })
  2817. ]), _('br'),
  2818. ]),
  2819. _('text', '脚本工作模式:'), _('br'),
  2820. _('div', { style: { display: 'flex' } }, [
  2821. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.DEFAULT }), _('text', '默认:自动判断')]),
  2822. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.REPLACE }), _('text', '替换:在需要时处理番剧')]),
  2823. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.mode.REDIRECT }), _('text', '重定向:完全代理所有番剧')])
  2824. ]), _('br'),
  2825. _('text', '其他:'), _('br'),
  2826. _('div', { style: { display: 'flex' } }, [
  2827. _('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', '(?)')])]),
  2828. _('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', '(?)')])]),
  2829. _('div', { style: { flex: 1, display: 'flex' } }, [
  2830. _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_remove_pre_ad' }), _('text', '去前置广告')]),
  2831. // _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_flv_prefer_ws' }), _('text', '优先使用ws')]),
  2832. ])
  2833. ]), _('br'),
  2834. _('a', { href: 'javascript:', 'data-sign': 'in', event: { click: onSignClick } }, [_('text', '帐号授权')]),
  2835. _('text', ' '),
  2836. _('a', { href: 'javascript:', 'data-sign': 'out', event: { click: onSignClick } }, [_('text', '取消授权')]),
  2837. _('text', '  '),
  2838. _('a', { href: 'javascript:', event: { click: function () { util_ui_msg.show($(this), '如果你的帐号进行了付费,不论是大会员还是承包,\n进行授权之后将可以在解除限制时正常享有这些权益\n\n你可以随时在这里授权或取消授权\n\n不进行授权不会影响脚本的正常使用,但可能会缺失1080P', 1e4); } } }, [_('text', '(这是什么?)')]),
  2839. _('br'), _('br'),
  2840. _('div', { style: { whiteSpace: 'pre-wrap' }, event: { mouseenter: onMouseEnterSettingBottom } }, [
  2841. _('a', { href: 'https://gf.qytechs.cn/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', '脚本主页')]),
  2842. _('text', ' '),
  2843. _('a', { href: 'https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md', target: '_blank' }, [_('text', '帮助说明')]),
  2844. _('text', ' '),
  2845. _('a', { id: 'balh-copy-log', href: 'javascript:;', event: { click: onCopyClick } }, [_('text', '复制日志&问题反馈')]),
  2846. _('text', ' '),
  2847. _('a', { id: 'balh-issue-link', href: 'javascript:;', event: { click: openIssuePage }, style: { display: 'none' } }, [_('text', '问题反馈')]),
  2848. _('a', { href: 'https://github.com/ipcjs/bilibili-helper/graphs/contributors' }, [_('text', '贡献者')]),
  2849. _('text', ' 接口:'),
  2850. _('a', { href: 'https://www.biliplus.com/' }, [_('text', 'BiliPlus ')]),
  2851. _('a', { href: 'https://github.com/kghost/bilibili-area-limit' }, [_('text', 'kghost ')]),
  2852. _('a', { href: 'https://github.com/yujincheng08/BiliRoaming' }, [_('text', 'BiliRoaming ')]),
  2853. ]),
  2854. _('textarea', { id: 'balh-textarea-copy', style: { display: 'none' } })
  2855. ])
  2856. ])
  2857. ]);
  2858.  
  2859. util_init(() => {
  2860. if (!(util_page.player() || (util_page.av() && !balh_config.enable_in_av))) {
  2861. addSettingsButton()
  2862. }
  2863. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  2864. return {
  2865. dom: settingsDOM,
  2866. show: showSettings,
  2867. }
  2868. }())
  2869.  
  2870. const balh_jump_to_baipiao = (function () {
  2871. function main() {
  2872. for (let bp of r.baipiao) {
  2873. const cookie_key = `balh_baipao_${bp.key}`
  2874. if (bp.match() && !util_cookie[cookie_key]) {
  2875. util_ui_pop({
  2876. content: [
  2877. _('text', '发现白嫖地址: '), _('a', { href: bp.link }, bp.link),
  2878. _('div', {}, bp.message),
  2879. ],
  2880. confirmBtn: '一键跳转',
  2881. onConfirm: () => { location.href = bp.link },
  2882. onClose: () => { util_cookie.set(cookie_key, r.const.TRUE, '') }
  2883. })
  2884. break
  2885. }
  2886. }
  2887. }
  2888. util_init(() => {
  2889. main()
  2890. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  2891. }())
  2892.  
  2893. const balh_mark_serve_check_area_limit_state = (function () {
  2894. if (!util_page.bangumi_md()) {
  2895. return
  2896. }
  2897. // 服务器需要通过这个接口判断是否有区域限制
  2898. // 详见: https://github.com/ipcjs/bilibili-helper/issues/385
  2899. util_init(() => {
  2900. const season_id = util_safe_get(`window.__INITIAL_STATE__.mediaInfo.param.season_id`)
  2901. if (season_id) {
  2902. balh_api_plus_season(season_id)
  2903. .then(r => log(`season${season_id}`, r))
  2904. .catch(e => log(`season${season_id}`, e))
  2905. }
  2906. })
  2907. }())
  2908.  
  2909. function main() {
  2910. util_info(
  2911. 'mode:', balh_config.mode,
  2912. 'blocked_vip:', balh_config.blocked_vip,
  2913. 'server:', balh_config.server,
  2914. 'upos_server:', balh_config.upos_server,
  2915. 'flv_prefer_ws:', balh_config.flv_prefer_ws,
  2916. 'remove_pre_ad:', balh_config.remove_pre_ad,
  2917. 'enable_in_av:', balh_config.enable_in_av,
  2918. 'readyState:', document.readyState,
  2919. 'isLogin:', balh_feature_sign.isLogin(),
  2920. 'isLoginBiliBili:', balh_feature_sign.isLoginBiliBili()
  2921. )
  2922. // 暴露接口
  2923. window.bangumi_area_limit_hack = {
  2924. setCookie: util_cookie.set,
  2925. getCookie: util_cookie.get,
  2926. login: balh_feature_sign.showLogin,
  2927. logout: balh_feature_sign.showLogout,
  2928. getLog: util_log_hub.getAllMsg,
  2929. showSettings: balh_ui_setting.show,
  2930. set1080P: function () {
  2931. const settings = JSON.parse(localStorage.bilibili_player_settings)
  2932. const oldQuality = settings.setting_config.defquality
  2933. util_debug(`defauality: ${oldQuality}`)
  2934. settings.setting_config.defquality = 112 // 1080P
  2935. localStorage.bilibili_player_settings = JSON.stringify(settings)
  2936. location.reload()
  2937. },
  2938. _clear_local_value: function () {
  2939. delete localStorage.oauthTime
  2940. delete localStorage.balh_h5_not_first
  2941. delete localStorage.balh_old_isLoginBiliBili
  2942. delete localStorage.balh_must_remind_login_v3
  2943. delete localStorage.balh_must_updateLoginFlag
  2944. }
  2945. }
  2946. }
  2947.  
  2948. main();
  2949. }
  2950.  
  2951. scriptSource(GM_info.scriptHandler);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址