解除B站区域限制

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

2017-12-26 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name 解除B站区域限制
  3. // @namespace http://tampermonkey.net/
  4. // @version 6.0.1
  5. // @description 通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;
  6. // @author ipcjs
  7. // @require https://static.hdslb.com/js/md5.js
  8. // @include *://www.bilibili.com/video/av*
  9. // @include *://www.bilibili.com/bangumi/play/ep*
  10. // @include *://www.bilibili.com/bangumi/play/ss*
  11. // @include *://bangumi.bilibili.com/anime/*
  12. // @include *://bangumi.bilibili.com/movie/*
  13. // @include *://www.bilibili.com/blackboard/html5player.html*
  14. // @include *://www.bilibili.com/blackboard/html5playerbeta.html*
  15. // @run-at document-start
  16. // @grant none
  17. // ==/UserScript==
  18.  
  19. 'use strict';
  20. const r = {
  21. text: {
  22. ok: { en: 'OK', zh_cn: '确定', },
  23. },
  24. html: {},
  25. attr: {},
  26. const: {
  27. MODE: {
  28. DEFAULT: 'default',// 默认模式, 自动判断使用何种模式, 推荐;
  29. REPLACE: 'replace', // 替换模式, 替换有区域限制的视频的接口的返回值;
  30. REDIRECT: 'redirect',// 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
  31. },
  32. SERVER: {
  33. DEFAULT: 'https://biliplus.ipcjs.win',// 默认代理服务器
  34. OLD_HOST: 'biliplus.ipcjsdev.tk',
  35. }
  36. }
  37. }
  38. const util_func_noop = function () { }
  39. const util_func_catched = function (func, onError) {
  40. let ret = function () {
  41. try {
  42. return func.apply(this, arguments)
  43. } catch (e) {
  44. if (onError) return onError(e) // onError可以处理报错时的返回值
  45. // 否则打印log, 并返回undefined
  46. util_error('Exception while run %o: %o\n%o', func, e, e.stack)
  47. return undefined
  48. }
  49. }
  50. ret.name = func.name
  51. return ret
  52. }
  53.  
  54. const util_log = window.console.log.bind(window.console, 'log:')
  55. const util_error = window.console.error.bind(window.console, 'error:')
  56. const log = util_log
  57. log(`[${GM_info.script.name}] run on: ${window.location.href}`);
  58.  
  59. const util_init = (function () {
  60. const RUN_AT = {
  61. DOM_LOADED: 0,
  62. DOM_LOADED_AFTER: 1,
  63. COMPLETE: 2,
  64. }
  65. const PRIORITY = {
  66. FIRST: 1e6,
  67. HIGH: 1e5,
  68. BEFORE: 1e3,
  69. DEFAULT: 0,
  70. AFTER: -1e3,
  71. LOW: -1e5,
  72. LAST: -1e6,
  73. }
  74. const callbacks = {
  75. [RUN_AT.DOM_LOADED]: [],
  76. [RUN_AT.DOM_LOADED_AFTER]: [],
  77. [RUN_AT.COMPLETE]: [],
  78. }
  79. const util_page_valid = () => true // 是否要运行
  80. const dclCreator = function (runAt) {
  81. let dcl = function () {
  82. util_init.atRun = runAt // 更新运行状态
  83. const valid = util_page_valid()
  84. // 优先级从大到小, index从小到大, 排序
  85. callbacks[runAt].sort((a, b) => b.priority - a.priority || a.index - b.index)
  86. .filter(item => valid || item.always)
  87. .forEach(item => item.func(valid))
  88. }
  89. return dcl
  90. }
  91.  
  92. if (window.document.readyState !== 'loading') {
  93. throw new Error('must run at loading')
  94. }
  95.  
  96. window.document.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED))
  97. window.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED_AFTER))
  98. window.addEventListener('load', dclCreator(RUN_AT.COMPLETE))
  99.  
  100. const util_init = function (func, priority = PRIORITY.DEFAULT, runAt = RUN_AT.DOM_LOADED, always = false) {
  101. func = util_func_catched(func)
  102. if (util_init.atRun < runAt) { // 若还没运行到runAt指定的状态, 则放到队列里去
  103. callbacks[runAt].push({
  104. priority,
  105. index: callbacks[runAt].length, // 使用callback数组的长度, 作为添加元素的index属性
  106. func,
  107. always
  108. })
  109. } else { // 否则直接运行
  110. let valid = util_page_valid()
  111. setTimeout(() => (valid || always) && func(valid), 1)
  112. }
  113. return func
  114. }
  115. util_init.atRun = -1 // 用来表示当前运行到什么状态
  116. util_init.RUN_AT = RUN_AT
  117. util_init.PRIORITY = PRIORITY
  118. return util_init
  119. }())
  120. /** 通知模块 剽窃自 YAWF 用户脚本 硬广:https://tiansh.github.io/yawf/ */
  121. const util_notify = (function () {
  122. var avaliable = {};
  123. var shown = [];
  124. var use = {
  125. 'hasPermission': function () { return null; },
  126. 'requestPermission': function (callback) { return null; },
  127. 'hideNotification': function (notify) { return null; },
  128. 'showNotification': function (id, title, body, icon, delay, onclick) { return null; }
  129. };
  130.  
  131. // 检查一个微博是不是已经被显示过了,如果显示过了不重复显示
  132. var shownFeed = function (id) {
  133. return false;
  134. };
  135.  
  136. // webkitNotifications
  137. // Tab Notifier 扩展实现此接口,但显示的桌面提示最多只能显示前两行
  138. if (typeof webkitNotifications !== 'undefined') avaliable.webkit = {
  139. 'hasPermission': function () {
  140. return [true, null, false][webkitNotifications.checkPermission()];
  141. },
  142. 'requestPermission': function (callback) {
  143. return webkitNotifications.requestPermission(callback);
  144. },
  145. 'hideNotification': function (notify) {
  146. notify.cancel();
  147. afterHideNotification(notify);
  148. },
  149. 'showNotification': function (id, title, body, icon, delay, onclick) {
  150. if (shownFeed(id)) return null;
  151. var notify = webkitNotifications.createNotification(icon, title, body);
  152. if (delay && delay > 0) notify.addEventListener('display', function () {
  153. setTimeout(function () { hideNotification(notify); }, delay);
  154. });
  155. if (onclick) notify.addEventListener('click', function () {
  156. onclick.apply(this, arguments);
  157. hideNotification(notify);
  158. });
  159. notify.show();
  160. return notify;
  161. },
  162. };
  163.  
  164. // Notification
  165. // Firefox 22+
  166. // 显示4秒会自动关闭 https://bugzil.la/875114
  167. if (typeof Notification !== 'undefined') avaliable.standard = {
  168. 'hasPermission': function () {
  169. return {
  170. 'granted': true,
  171. 'denied': false,
  172. 'default': null,
  173. }[Notification.permission];
  174. },
  175. 'requestPermission': function (callback) {
  176. return Notification.requestPermission(callback);
  177. },
  178. 'hideNotification': function (notify) {
  179. notify.close();
  180. afterHideNotification(notify);
  181. },
  182. 'showNotification': function (id, title, body, icon, delay, onclick) {
  183. if (shownFeed(id)) return null;
  184. var notify = new Notification(title, { 'body': body, 'icon': icon, 'requireInteraction': !delay });
  185. if (delay && delay > 0) notify.addEventListener('show', function () {
  186. setTimeout(function () {
  187. hideNotification(notify);
  188. }, delay);
  189. });
  190. if (onclick) notify.addEventListener('click', function () {
  191. onclick.apply(this, arguments);
  192. hideNotification(notify);
  193. });
  194. return notify;
  195. },
  196. };
  197.  
  198. // 有哪些接口可用
  199. var avaliableNotification = function () {
  200. return Object.keys(avaliable);
  201. };
  202. // 选择用哪个接口
  203. var choseNotification = function (prefer) {
  204. return (use = prefer && avaliable[prefer] || avaliable.standard);
  205. };
  206. choseNotification();
  207. // 检查权限
  208. var hasPermission = function () {
  209. return use.hasPermission.apply(this, arguments);
  210. };
  211. // 请求权限
  212. var requestPermission = function () {
  213. return use.requestPermission.apply(this, arguments);
  214. };
  215. // 显示消息
  216. var showNotification = function (id, title, body, icon, delay, onclick) {
  217. var notify = use.showNotification.apply(this, arguments);
  218. shown.push(notify);
  219. return notify;
  220. };
  221. // 隐藏已经显示的消息
  222. var hideNotification = function (notify) {
  223. use.hideNotification.apply(this, arguments);
  224. return notify;
  225. };
  226. var afterHideNotification = function (notify) {
  227. shown = shown.filter(function (x) { return x !== notify; });
  228. };
  229.  
  230. document.addEventListener('unload', function () {
  231. shown.forEach(hideNotification);
  232. shown = [];
  233. });
  234. var showNotificationAnyway = function (id, title, body, icon, delay, onclick) {
  235. var that = this, thatArguments = arguments;
  236. switch (that.hasPermission()) {
  237. case null: // default
  238. that.requestPermission(function () {
  239. showNotificationAnyway.apply(that, thatArguments);
  240. });
  241. break;
  242. case true: // granted
  243. // 只有已获取了授权, 才能有返回值...
  244. return that.showNotification.apply(that, thatArguments);
  245. break;
  246. case false: // denied
  247. log('Notification permission: denied');
  248. break;
  249. }
  250. return null;
  251. }
  252.  
  253. return {
  254. 'avaliableNotification': avaliableNotification,
  255. 'choseNotification': choseNotification,
  256. 'hasPermission': hasPermission,
  257. 'requestPermission': requestPermission,
  258. 'showNotification': showNotification,
  259. 'hideNotification': hideNotification,
  260. show: function (body, onclick, delay = 3e3) {
  261. return this.showNotificationAnyway(Date.now(), GM_info.script.name, body, '//bangumi.bilibili.com/favicon.ico', delay, onclick)
  262. },
  263. showNotificationAnyway
  264. };
  265. }())
  266. const util_cookie = (function () {
  267. function getCookies() {
  268. var map = document.cookie.split('; ').reduce(function (obj, item) {
  269. var entry = item.split('=');
  270. obj[entry[0]] = entry[1];
  271. return obj;
  272. }, {});
  273. return map;
  274. }
  275.  
  276. function getCookie(key) {
  277. return getCookies()[key];
  278. }
  279.  
  280. /**
  281. * @param key key
  282. * @param value 为undefined时, 表示删除cookie
  283. * @param options 为undefined时, 表示过期时间为3年
  284. * 为''时, 表示Session cookie
  285. * 为数字时, 表示指定过期时间
  286. * 为{}时, 表示指定所有的属性
  287. * */
  288. function setCookie(key, value, options) {
  289. if (typeof options !== 'object') {
  290. options = {
  291. domain: '.bilibili.com',
  292. path: '/',
  293. 'max-age': value === undefined ? 0 : (options === undefined ? 94608000 : options)
  294. };
  295. }
  296. var c = Object.keys(options).reduce(function (str, key) {
  297. return str + '; ' + key + '=' + options[key];
  298. }, key + '=' + value);
  299. document.cookie = c;
  300. return c;
  301. }
  302.  
  303. return new Proxy({ set: setCookie, get: getCookie, all: getCookies }, {
  304. get: function (target, prop) {
  305. if (prop in target) return target[prop]
  306. return getCookie(prop)
  307. },
  308. set: function (target, prop, value) {
  309. setCookie(prop, value)
  310. return true
  311. }
  312. })
  313. }())
  314. const util_ajax = function (options) {
  315. return new Promise(function (resolve, reject) {
  316. typeof options !== 'object' && (options = { url: options });
  317.  
  318. options.async === undefined && (options.async = true);
  319. options.xhrFields === undefined && (options.xhrFields = { withCredentials: true });
  320. options.success = function (data) {
  321. resolve(data);
  322. };
  323. options.error = function (err) {
  324. reject(err);
  325. };
  326. $.ajax(options);
  327. });
  328. }
  329.  
  330. /**
  331. * 创建元素的快捷方法
  332. * @param type string, 标签名; 特殊的, 若为text, 则表示创建文字, 对应的t为文字的内容
  333. * @param props object, 属性; 特殊的属性名有: className, 类名; style, 样式, 值为(样式名, 值)形式的object; event, 值为(事件名, 监听函数)形式的object;
  334. * @param children array, 子元素;
  335. */
  336. const _ = (type, props, children) => {
  337. let elem = null;
  338. if (type === "text") {
  339. return document.createTextNode(props);
  340. } else {
  341. elem = document.createElement(type);
  342. }
  343. for (let n in props) {
  344. if (n === "style") {
  345. for (let x in props.style) {
  346. elem.style[x] = props.style[x];
  347. }
  348. } else if (n === "className") {
  349. elem.className = props[n];
  350. } else if (n === "event") {
  351. for (let x in props.event) {
  352. elem.addEventListener(x, props.event[x]);
  353. }
  354. } else {
  355. elem.setAttribute(n, props[n]);
  356. }
  357. }
  358. if (children) {
  359. for (let i = 0; i < children.length; i++) {
  360. if (children[i] != null)
  361. elem.appendChild(children[i]);
  362. }
  363. }
  364. return elem;
  365. }
  366.  
  367. const util_ui_popframe = function (iframeSrc) {
  368. if (!document.getElementById('balh-style-login')) {
  369. var style = document.createElement('style');
  370. style.id = 'balh-style-login';
  371. 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}';
  372. }
  373.  
  374. var div = document.createElement('div');
  375. div.id = 'GMBiliPlusLoginContainer';
  376. 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>';
  377. div.firstChild.addEventListener('click', function (e) {
  378. if (e.target === this || e.target.className === 'GMBiliPlusCloseBox') {
  379. if (!confirm('确认关闭?')) {
  380. return false;
  381. }
  382. div.firstChild.style.animationName = 'pop-iframe-out';
  383. setTimeout(function () {
  384. div.remove();
  385. }, 5e2);
  386. }
  387. });
  388. document.body.appendChild(div);
  389. }
  390.  
  391. /**
  392. * MessageBox -> from base.core.js
  393. * MessageBox.show(referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback)
  394. * MessageBox.close()
  395. */
  396. const util_ui_msg = (function () {
  397. function MockMessageBox() {
  398. this.show = (...args) => util_log(MockMessageBox.name, 'show', args)
  399. this.close = (...args) => util_log(MockMessageBox.name, 'close', args)
  400. }
  401.  
  402. let popMessage = null
  403. let mockPopMessage = new MockMessageBox()
  404. let notifyPopMessage = {
  405. _current_notify: null,
  406. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  407. this.close()
  408. this._current_notify = util_notify.show(message, buttonTypeConfirmCallback, closeTime)
  409. },
  410. close: function () {
  411. if (this._current_notify) {
  412. util_notify.hideNotification(this._current_notify)
  413. this._current_notify = null
  414. }
  415. }
  416. }
  417.  
  418. util_init(() => {
  419. if (!popMessage && window.MessageBox) {
  420. popMessage = new window.MessageBox()
  421. }
  422. }, util_init.PRIORITY.FIRST, util_init.RUN_AT.DOM_LOADED_AFTER)
  423.  
  424. return {
  425. _impl: function () {
  426. return popMessage || notifyPopMessage
  427. },
  428. show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
  429. let pop = this._impl()
  430. return pop.show.apply(pop, arguments)
  431. },
  432. close: function () {
  433. let pop = this._impl()
  434. return pop.close.apply(pop, arguments)
  435. },
  436. setMsgBoxFixed: function (fixed) {
  437. if (popMessage) {
  438. popMessage.msgbox[0].style.position = fixed ? 'fixed' : ''
  439. } else {
  440. util_log(MockMessageBox.name, 'setMsgBoxFixed', fixed)
  441. }
  442. },
  443. showOnError: function (e) {
  444. if (e.readyState === 0) {
  445. this.show($('.balh_settings'), '哎呀,服务器连不上了,确认一下连接?', 0, 'button', balh_ui_setting.show);
  446. }
  447. }
  448. }
  449. }())
  450. const util_url_param = function (url, key) {
  451. return (url.match(new RegExp('[?|&]' + key + '=(\\w+)')) || ['', ''])[1];
  452. }
  453.  
  454. const util_page = {
  455. player: () => location.href.includes('www.bilibili.com/blackboard/html5player'),
  456. // 在av页面中的iframe标签形式的player
  457. player_in_av: util_func_catched(() => util_page.player() && window.top.location.href.includes('www.bilibili.com/video/av'), (e) => log(e), false),
  458. av: () => location.href.includes('www.bilibili.com/video/av'),
  459. bangumi: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/?$')),
  460. // movie页面使用window.aid, 保存当前页面av号
  461. movie: () => location.href.includes('bangumi.bilibili.com/movie/'),
  462. // anime页面使用window.season_id, 保存当前页面season号
  463. anime: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/play.*')),
  464. anime_ep: () => location.href.includes('www.bilibili.com/bangumi/play/ep'),
  465. anime_ss: () => location.href.includes('www.bilibili.com/bangumi/play/ss'),
  466. }
  467.  
  468. const balh_config = (function () {
  469. const cookies = util_cookie.all() // 缓存的cookies
  470. return new Proxy({ /*保存config的对象*/ }, {
  471. get: function (target, prop) {
  472. if (prop in target) {
  473. return target[prop]
  474. } else { // 若target中不存在指定的属性, 则从缓存的cookies中读取, 并保存到target中
  475. let value = cookies['balh_' + prop]
  476. switch (prop) {
  477. case 'server':
  478. value = value || r.const.SERVER.DEFAULT
  479. // 从tk域名迁移到新的win域名
  480. if (value.includes(r.const.SERVER.OLD_HOST)) {
  481. value = r.const.SERVER.DEFAULT
  482. balh_config.server = value
  483. }
  484. break
  485. case 'mode':
  486. value = value || (balh_config.blocked_vip ? r.const.MODE.REDIRECT : r.const.MODE.DEFAULT)
  487. break
  488. default:
  489. // case 'blocked_vip':
  490. // case 'flv_prefer_ws':
  491. break
  492. }
  493. target[prop] = value
  494. return value
  495. }
  496. },
  497. set: function (target, prop, value) {
  498. target[prop] = value // 更新值
  499. util_cookie['balh_' + prop] = value // 更新cookie中的值
  500. return true
  501. }
  502. })
  503. }())
  504.  
  505. const balh_api_plus_view = function (aid, update = true) {
  506. return util_ajax(`${balh_config.server}/api/view?id=${aid}&update=${update}`)
  507. }
  508. const balh_api_plus_season = function (season_id) {
  509. return util_ajax(`${balh_config.server}/api/bangumi?season=${season_id}`)
  510. }
  511.  
  512. const balh_feature_area_limit = (function () {
  513. function injectXHR() {
  514. window.XMLHttpRequest = new Proxy(window.XMLHttpRequest, {
  515. construct: function (target, args) {
  516. let container = {} // 用来替换responseText等变量
  517. return new Proxy(new target(...args), {
  518. set: function (target, prop, value, receiver) {
  519. if (prop === 'onreadystatechange') {
  520. let cb = value
  521. value = function () {
  522. if (target.readyState === 4) {
  523. if (target.responseURL.includes('bangumi.bilibili.com/view/web_api/season/user/status')) {
  524. log('/season/user/status:', target.responseText)
  525. let json = JSON.parse(target.responseText)
  526. let rewriteResult = false
  527. if (json.code === 0 && json.result) {
  528. areaLimit(json.result.area_limit !== 0)
  529. if (json.result.area_limit !== 0) {
  530. json.result.area_limit = 0 // 取消区域限制
  531. rewriteResult = true
  532. }
  533. if (balh_config.blocked_vip) {
  534. json.result.pay = 1
  535. rewriteResult = true
  536. }
  537. if (rewriteResult) {
  538. container.responseText = JSON.stringify(json)
  539. }
  540. }
  541. } else if (target.responseURL.includes('bangumi.bilibili.com/web_api/season_area')) {
  542. log('/season_area', target.responseText)
  543. let json = JSON.parse(target.responseText)
  544. if (json.code === 0 && json.result) {
  545. areaLimit(json.result.play === 0)
  546. if (json.result.play === 0) {
  547. json.result.play = 1
  548. container.responseText = JSON.stringify(json)
  549. }
  550. }
  551. }
  552. }
  553. // 这里的this是原始的xhr, 在container.responseText设置了值时需要替换成代理对象
  554. cb.apply(container.responseText ? receiver : this, arguments)
  555. }
  556. }
  557. target[prop] = value
  558. return true
  559. },
  560. get: function (target, prop, receiver) {
  561. if (prop in container) return container[prop]
  562. let value = target[prop]
  563. if (typeof value === 'function') {
  564. let func = value
  565. // open等方法, 必须在原始的xhr对象上才能调用...
  566. value = function () {
  567. return func.apply(target, arguments)
  568. }
  569. }
  570. return value
  571. }
  572. })
  573. }
  574. })
  575. }
  576.  
  577. function injectAjax() {
  578. var originalAjax = $.ajax;
  579. $.ajax = function (arg0, arg1) {
  580. // log(arguments);
  581. var param;
  582. if (arg1 === undefined) {
  583. param = arg0;
  584. } else {
  585. arg0 && (arg1.url = arg0);
  586. param = arg1;
  587. }
  588. var oriSuccess = param.success;
  589. var mySuccess;
  590. var one_api;
  591. if (param.url.match('/web_api/get_source')) {
  592. one_api = bilibiliApis._get_source;
  593. if (needRedirect()) { // 对应redirect模式
  594. param.url = one_api.transToProxyUrl(param.url);
  595. param.type = 'GET';
  596. delete param.data;
  597. param.success = function (data) {
  598. var returnVal = one_api.processProxySuccess(data);
  599. log('Redirected request: get_source', returnVal);
  600. oriSuccess(returnVal);
  601. };
  602. var oriError = param.error;
  603. param.error = function (e) {
  604. util_ui_msg.showOnError(e);
  605. oriError(e);
  606. };
  607. } else { // 对应replace模式
  608. param.success = function (json) {
  609. log(json);
  610. if (json.code === -40301 // 区域限制
  611. || json.result.payment && json.result.payment.price != 0 && balh_config.blocked_vip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
  612. one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
  613. util_ui_msg.showOnError(e);
  614. oriSuccess(json); // 新的请求报错, 也应该返回原来的数据
  615. });
  616. areaLimit(true); // 只要默认模式才要跟踪是否有区域限制
  617. } else {
  618. areaLimit(false);
  619. if (balh_config.blocked_vip && json.code === 0 && json.result.pre_ad) {
  620. json.result.pre_ad = 0; // 去除前置广告
  621. }
  622. oriSuccess(json); // 保证一定调用了原来的success
  623. }
  624. };
  625. }
  626. } else if (param.url.match('/player/web_api/playurl')) {
  627. one_api = bilibiliApis._playurl;
  628. if (needRedirect()) {
  629. param.url = one_api.transToProxyUrl(param.url);
  630. param.success = function (data) {
  631. oriSuccess(one_api.processProxySuccess(data));
  632. };
  633. var oriError = param.error;
  634. param.error = function (e) {
  635. util_ui_msg.showOnError(e);
  636. oriError(e);
  637. };
  638. log('Redirected request: bangumi playurl -> ', param.url);
  639. } else {
  640. param.success = function (json) {
  641. // 获取视频地址 API
  642. log(json);
  643. if (balh_config.blocked_vip || json.code || isAreaLimitForPlayUrl(json)) {
  644. one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
  645. util_ui_msg.showOnError(e);
  646. oriSuccess(json);
  647. });
  648. areaLimit(true);
  649. } else {
  650. areaLimit(false);
  651. oriSuccess(json);
  652. }
  653. };
  654. }
  655. } else if (param.url.match('//interface.bilibili.com/player?')) {
  656. if (balh_config.blocked_vip) {
  657. mySuccess = function (data) {
  658. try {
  659. var xml = new window.DOMParser().parseFromString(`<userstatus>${data.replace(/\&/g, '&amp;')}</userstatus>`, 'text/xml');
  660. var vipTag = xml.querySelector('vip');
  661. if (vipTag) {
  662. var vip = JSON.parse(vipTag.innerHTML);
  663. vip.vipType = 2; // 类型, 年度大会员
  664. vip.vipStatus = 1; // 状态, 启用
  665. vipTag.innerHTML = JSON.stringify(vip);
  666. data = xml.documentElement.innerHTML;
  667. }
  668. } catch (e) {
  669. log('parse xml error: ', e);
  670. }
  671. oriSuccess(data);
  672. };
  673. }
  674. }
  675.  
  676. // 若外部使用param.success处理结果, 则替换param.success
  677. if (oriSuccess && mySuccess) {
  678. param.success = mySuccess;
  679. }
  680. // default
  681. var xhr = originalAjax.apply(this, [param]);
  682.  
  683. // 若外部使用xhr.done()处理结果, 则替换xhr.done()
  684. if (!oriSuccess && mySuccess) {
  685. xhr.done(mySuccess);
  686. xhr.done = function (success) {
  687. oriSuccess = success; // 保存外部设置的success函数
  688. return xhr;
  689. };
  690. }
  691. return xhr;
  692. };
  693. }
  694.  
  695. function isAreaLimitSeason() {
  696. return util_cookie['balh_season_' + getSeasonId()];
  697. }
  698.  
  699. function needRedirect() {
  700. return balh_config.mode === r.const.MODE.REDIRECT || (balh_config.mode === r.const.MODE.DEFAULT && isAreaLimitSeason())
  701. }
  702.  
  703. function areaLimit(limit) {
  704. balh_config.mode === r.const.MODE.DEFAULT && setAreaLimitSeason(limit)
  705. }
  706.  
  707. function setAreaLimitSeason(limit) {
  708. var season_id = getSeasonId();
  709. util_cookie.set('balh_season_' + season_id, limit ? '1' : undefined, ''); // 第三个参数为'', 表示时Session类型的cookie
  710. log('setAreaLimitSeason', season_id, limit);
  711. }
  712.  
  713. function getSeasonId() {
  714. var seasonId;
  715. // 取anime页面的seasonId
  716. try {
  717. // 若w, 是其frame的window, 则有可能没有权限, 而抛异常
  718. seasonId = window.season_id || window.top.season_id;
  719. } catch (e) {
  720. log(e);
  721. }
  722. if (!seasonId) {
  723. try {
  724. seasonId = (window.top.location.pathname.match(/\/anime\/(\d+)/) || ['', ''])[1];
  725. } catch (e) {
  726. log(e);
  727. }
  728. }
  729.  
  730. // 若没取到, 则取movie页面的seasonId, 以m开头
  731. if (!seasonId) {
  732. try {
  733. seasonId = (window.top.location.pathname.match(/\/movie\/(\d+)/) || ['', ''])[1];
  734. if (seasonId) {
  735. seasonId = 'm' + seasonId;
  736. }
  737. } catch (e) {
  738. log(e);
  739. }
  740. }
  741.  
  742. // 若没取到, 则去新的番剧播放页面的ep或ss
  743. if (!seasonId) {
  744. try {
  745. seasonId = (window.top.location.pathname.match(/\/bangumi\/play\/((ep|ss)\d+)/) || ['', ''])[1];
  746. } catch (e) {
  747. log(e);
  748. }
  749. }
  750.  
  751. // 最后, 若没取到, 则试图取出当前页面url中的aid
  752. if (!seasonId) {
  753. seasonId = util_url_param(window.location.href, 'aid');
  754. if (seasonId) {
  755. seasonId = 'av' + seasonId;
  756. }
  757. }
  758. return seasonId || '000';
  759. }
  760.  
  761. function isAreaLimitForPlayUrl(json) {
  762. return json.durl && json.durl.length === 1 && json.durl[0].length === 15126 && json.durl[0].size === 124627;
  763. }
  764.  
  765. var bilibiliApis = (function () {
  766. function BilibiliApi(props) {
  767. Object.assign(this, props);
  768. }
  769.  
  770. BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
  771. var one_api = this;
  772. $.ajax({
  773. url: one_api.transToProxyUrl(originUrl),
  774. async: true,
  775. xhrFields: { withCredentials: true },
  776. success: function (result) {
  777. log('==>', result);
  778. success(one_api.processProxySuccess(result));
  779. // log('success', arguments, this);
  780. },
  781. error: function (e) {
  782. log('error', arguments, this);
  783. error(e);
  784. }
  785. });
  786. };
  787. var get_source_by_aid = new BilibiliApi({
  788. transToProxyUrl: function (url) {
  789. return balh_config.server + '/api/view?id=' + window.aid + '&update=true';
  790. },
  791. processProxySuccess: function (data) {
  792. if (data && data.list && data.list[0] && data.movie) {
  793. return {
  794. code: 0,
  795. message: 'success',
  796. result: {
  797. cid: data.list[0].cid,
  798. formal_aid: data.aid,
  799. movie_status: balh_config.blocked_vip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
  800. pay_begin_time: 1507708800,
  801. pay_timestamp: 0,
  802. pay_user_status: data.movie.pay_user.status, // 一般都是0
  803. player: data.list[0].type, // 一般为movie
  804. vid: data.list[0].vid,
  805. vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
  806. vipType: balh_config.blocked_vip ? 2 : 0,
  807. vipStatus: balh_config.blocked_vip ? 1 : 0,
  808. }
  809. }
  810. };
  811. } else {
  812. return {
  813. code: -404,
  814. message: '不存在该剧集'
  815. };
  816. }
  817. }
  818. });
  819. var get_source_by_season_id = new BilibiliApi({
  820. transToProxyUrl: function (url) {
  821. return balh_config.server + '/api/bangumi?season=' + window.season_id;
  822. },
  823. processProxySuccess: function (data) {
  824. var found = null;
  825. if (!data.code) {
  826. for (var i = 0; i < data.result.episodes.length; i++) {
  827. if (data.result.episodes[i].episode_id == window.episode_id) {
  828. found = data.result.episodes[i];
  829. }
  830. }
  831. } else {
  832. util_notify.show('代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', window.location.reload.bind(window.location));
  833. }
  834. var returnVal = found !== null
  835. ? {
  836. "code": 0,
  837. "message": "success",
  838. "result": {
  839. "aid": found.av_id,
  840. "cid": found.danmaku,
  841. "episode_status": balh_config.blocked_vip ? 2 : found.episode_status,
  842. "payment": { "price": "9876547210.33" },
  843. "pay_user": {
  844. "status": balh_config.blocked_vip ? 1 : 0 // 是否已经支付过
  845. },
  846. "player": "vupload",
  847. "pre_ad": 0,
  848. "season_status": balh_config.blocked_vip ? 2 : data.result.season_status
  849. }
  850. }
  851. : { code: -404, message: '不存在该剧集' };
  852. return returnVal;
  853. }
  854. });
  855. var playurl = new BilibiliApi({
  856. transToProxyUrl: function (url) {
  857. var params = url.split('?')[1];
  858. // 只有在av页面中的iframe标签形式的player, 不需要插入'bangumi'参数, 其他页面都要插入这个参数
  859. if (!util_page.player_in_av()) {
  860. params = params.replace(/(cid=\d+)/, '$1|' + (url.match(/module=(\w+)/) || ['', 'bangumi'])[1])
  861. }
  862. return `${balh_config.server}/BPplayurl.php?${params}`;
  863. },
  864. processProxySuccess: function (data) {
  865. // data有可能为null
  866. if (data && data.code === -403) {
  867. // window.alert('当前使用的服务器(' + balh_config.server + ')依然有区域限制');
  868. util_notify.show(`突破黑洞失败,我们未能穿透敌人的盔甲\n当前代理服务器(${balh_config.server})依然有区域限制Σ(  ̄□ ̄||)`);
  869. } else if (data === null || data.code) {
  870. util_error(data);
  871. util_notify.show(`突破黑洞失败\n${JSON.stringify(data)}\n点击刷新界面`, window.location.reload.bind(window.location));
  872. } else if (isAreaLimitForPlayUrl(data)) {
  873. util_error('>>area limit');
  874. util_notify.show(`突破黑洞失败,需要登录\n点此进行登录`, balh_feature_sign.showLogin);
  875. } else {
  876. if (balh_config.flv_prefer_ws) {
  877. data.durl.forEach(function (seg) {
  878. var t, url, i;
  879. if (!seg.url.includes('ws.acgvideo.com')) {
  880. for (i in seg.backup_url) {
  881. url = seg.backup_url[i];
  882. if (url.includes('ws.acgvideo.com')) {
  883. log('flv prefer use:', url);
  884. t = seg.url;
  885. seg.url = url;
  886. url = t;
  887. break;
  888. }
  889. }
  890.  
  891. }
  892. });
  893. }
  894. }
  895. return data;
  896. }
  897. })
  898.  
  899. return {
  900. _get_source: util_page.movie() ? get_source_by_aid : get_source_by_season_id,
  901. _playurl: playurl
  902. };
  903. })();
  904.  
  905. injectXHR();
  906. if (!window.jQuery) { // 若还未加载jQuery, 则监听
  907. var jQuery;
  908. Object.defineProperty(window, 'jQuery', {
  909. configurable: true, enumerable: true, set: function (v) {
  910. jQuery = v;
  911. injectAjax();// 设置jQuery后, 立即注入
  912. }, get: function () {
  913. return jQuery;
  914. }
  915. });
  916. } else {
  917. injectAjax();
  918. }
  919. }())
  920. const balh_feature_check_html5 = (function () {
  921. function isHtml5Player() {
  922. return localStorage.defaulth5 === '1'
  923. }
  924.  
  925. function checkHtml5() {
  926. var playerContent = document.querySelector('.player-content');
  927. if (!localStorage.balh_h5_not_first && !isHtml5Player() && window.GrayManager && playerContent) {
  928. new MutationObserver(function (mutations, observer) {
  929. observer.disconnect();
  930. localStorage.balh_h5_not_first = 'yes';
  931. if (window.confirm(GM_info.script.name + '只在HTML5播放器下有效,是否切换到HTML5?')) {
  932. window.GrayManager.clickMenu('change_h5');// change_flash, change_h5
  933. }
  934. }).observe(playerContent, {
  935. childList: true, // 监听child的增减
  936. attributes: false, // 监听属性的变化
  937. });
  938. }
  939. }
  940.  
  941. util_init(() => {
  942. // 除了播放器和番剧列表页面, 其他页面都需要检测html5
  943. if (!(util_page.bangumi() || util_page.player())) {
  944. checkHtml5()
  945. }
  946. })
  947. return isHtml5Player
  948. }())
  949. const balh_feature_runPing = function () {
  950. var pingOutput = document.getElementById('balh_server_ping');
  951.  
  952. var xhr = new XMLHttpRequest(), testUrl = ['https://biliplus.ipcjs.win', 'https://www.biliplus.com'],
  953. testUrlIndex = 0, isReused = false, prevNow, outputArr = [];
  954. pingOutput.textContent = '正在进行服务器测速…';
  955. pingOutput.style.height = '100px';
  956. xhr.open('GET', '', true);
  957. xhr.onreadystatechange = function () {
  958. this.readyState == 4 && pingResult();
  959. };
  960. var pingLoop = function () {
  961. prevNow = performance.now();
  962. xhr.open('GET', testUrl[testUrlIndex] + '/api/bangumi', true);
  963. xhr.send();
  964. };
  965. var pingResult = function () {
  966. var duration = (performance.now() - prevNow) | 0;
  967. if (isReused)
  968. outputArr.push('\t复用连接:' + duration + 'ms'), isReused = false, testUrlIndex++;
  969. else
  970. outputArr.push(testUrl[testUrlIndex] + ':'), outputArr.push('\t初次连接:' + duration + 'ms'), isReused = true;
  971. pingOutput.textContent = outputArr.join('\n');
  972. testUrlIndex < testUrl.length ? pingLoop() : pingOutput.appendChild(_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '\n再测一次?')]));
  973. };
  974. pingLoop();
  975. }
  976. const balh_feature_sign = (function () {
  977. // 逻辑有点乱, 当前在如下情况才会弹一次登录提示框:
  978. // 1. 第一次使用
  979. // 2. 主站+服务器都退出登录后, 再重新登录主站
  980. function checkLoginState() {
  981. if (util_cookie["DedeUserID"] === undefined) {
  982. //未登录主站,强制指定值
  983. localStorage.balh_notFirst = 1;
  984. localStorage.balh_login = 0;
  985. localStorage.balh_mainLogin = 0;
  986. } else if (localStorage.balh_mainLogin !== undefined) {
  987. //主站未登录变为登录,重置显示弹窗
  988. delete localStorage.balh_notFirst;
  989. delete localStorage.balh_login;
  990. delete localStorage.balh_mainLogin;
  991. delete localStorage.oauthTime;
  992. }
  993. if (!localStorage.balh_notFirst) {
  994. //第一次打开,确认是否已登陆;未登录显示确认弹窗
  995. localStorage.balh_notFirst = 1;
  996. checkExpiretime(function () {
  997. if (localStorage.oauthTime === undefined) {
  998. localStorage.balh_login = 0;
  999. util_ui_msg.show($('.balh_settings'), `看起来你是第一次使用${GM_info.script.name}<br>要不要考虑进行一下授权?<br><br>授权后可以观看区域限定番剧的1080P<br>(如果你是大会员或承包过这部番的话)<br><br>你可以随时在设置中打开授权页面`, 0, 'button', balh_feature_sign.showLogin);
  1000. util_ui_msg.setMsgBoxFixed(true)
  1001. /*if (confirm()) {
  1002. showLogin();
  1003. }*/
  1004. } else {
  1005. localStorage.balh_login = 1;
  1006. }
  1007. });
  1008. } else if (localStorage.balh_login === undefined) {
  1009. //非第一次打开,登录状态被重置,重新检测
  1010. checkExpiretime(function () {
  1011. localStorage.balh_login = (localStorage.oauthTime === undefined) ? 0 : 1;
  1012. });
  1013. } else if (localStorage.balh_login == 1 && Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000) {
  1014. //已登录,每天为周期检测key有效期,过期前五天会自动续期
  1015. checkExpiretime();
  1016. }
  1017.  
  1018. function checkExpiretime(loadCallback) {
  1019. var script = document.createElement('script');
  1020. script.src = balh_config.server + '/login?act=expiretime';
  1021. loadCallback && script.addEventListener('load', loadCallback);
  1022. document.head.appendChild(script);
  1023. }
  1024. }
  1025.  
  1026. function showLogin() {
  1027. var loginUrl = balh_config.server + '/login',
  1028. iframeSrc = 'https://passport.bilibili.com/login?appkey=27eb53fc9058f8c3&api=' + encodeURIComponent(loginUrl) + '&sign=' + hex_md5('api=' + loginUrl + 'c2ed53a74eeefe3cf99fbd01d8c9c375');
  1029. util_ui_popframe(iframeSrc);
  1030. delete localStorage.balh_login;
  1031. }
  1032.  
  1033. function showLogout() {
  1034. util_ui_popframe(balh_config.server + '/login?act=logout');
  1035. }
  1036.  
  1037. // 监听登录message
  1038. window.addEventListener('message', function (e) {
  1039. switch (e.data) {
  1040. case 'BiliPlus-Login-Success':
  1041. //登入
  1042. document.head.appendChild(_('script', {
  1043. src: balh_config.server + '/login?act=getlevel',
  1044. event: {
  1045. load: function () { location.reload(); },
  1046. error: function () { location.reload(); }
  1047. }
  1048. }));
  1049. break;
  1050. case 'BiliPlus-Logout-Success':
  1051. //登出
  1052. location.reload();
  1053. break;
  1054. }
  1055. });
  1056.  
  1057.  
  1058. util_init(() => {
  1059. if (!(util_page.player() || util_page.av())) {
  1060. checkLoginState()
  1061. }
  1062. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  1063. return {
  1064. showLogin,
  1065. showLogout,
  1066. }
  1067. }())
  1068. const balh_feature_RedirectToBangumiOrInsertPlayer = (function () {
  1069. // 重定向到Bangumi页面, 或者在当前页面直接插入播放页面
  1070. function tryRedirectToBangumiOrInsertPlayer() {
  1071. var msgBox;
  1072. if (!(msgBox = document.querySelector('.b-page-body > .error-container > .error-panel'))) {
  1073. return;
  1074. }
  1075. var msg = document.createElement('a');
  1076. msgBox.insertBefore(msg, msgBox.firstChild);
  1077. msg.innerText = '获取番剧页Url中...';
  1078.  
  1079. var aid = location.pathname.replace(/.*av(\d+).*/, '$1'),
  1080. page = (location.pathname.match(/\/index_(\d+).html/) || ['', '1'])[1],
  1081. cid,
  1082. season_id,
  1083. episode_id;
  1084. balh_api_plus_view(aid)
  1085. .then(function (data) {
  1086. if (data.code) {
  1087. return Promise.reject(JSON.stringify(data));
  1088. }
  1089. // 计算当前页面的cid
  1090. for (var i = 0; i < data.list.length; i++) {
  1091. if (data.list[i].page == page) {
  1092. cid = data.list[i].cid;
  1093. break;
  1094. }
  1095. }
  1096. if (!data.bangumi) {
  1097. // 当前av不属于番剧页面, 直接在当前页面插入一个播放器的iframe
  1098. var pageBodyEle = document.querySelector('.b-page-body');
  1099. var iframe = _('iframe', { className: 'player bilibiliHtml5Player', style: { position: 'relative' }, src: generateSrc(aid, cid) });
  1100.  
  1101. var generatePageList = function (pages) {
  1102. function onPageBtnClick(e) {
  1103. var index = e.target.attributes['data-index'];
  1104. iframe.src = generateSrc(aid, pages[index].cid);
  1105. }
  1106.  
  1107. return pages.map(function (item, index) {
  1108. return _('a', { 'data-index': index, event: { click: onPageBtnClick } }, [_('text', item.page + ': ' + item.part)]);
  1109. });
  1110. }
  1111.  
  1112. var generateSrc = function (aid, cid) {
  1113. return `//www.bilibili.com/blackboard/html5player.html?cid=${cid}&aid=${aid}&player_type=1`;
  1114. }
  1115.  
  1116. // 添加播放器
  1117. pageBodyEle.insertBefore(_('div', { className: 'player-wrapper' }, [
  1118. _('div', { className: 'main-inner' }, [
  1119. _('div', { className: 'v-plist' }, [
  1120. _('div', { id: 'plist', className: 'plist-content' }, generatePageList(data.list))
  1121. ])
  1122. ]),
  1123. _('div', { id: 'bofqi', className: 'scontent' }, [iframe])
  1124. ]), pageBodyEle.firstChild);
  1125. // 添加评论区
  1126. pageBodyEle.appendChild(_('div', { className: 'main-inner' }, [
  1127. _('div', { className: 'common report-scroll-module report-wrap-module', id: 'common_report' }, [
  1128. _('div', { className: 'b-head' }, [
  1129. _('span', { className: 'b-head-t results' }),
  1130. _('span', { className: 'b-head-t' }, [_('text', '评论')]),
  1131. _('a', { className: 'del-log', href: `//www.bilibili.com/replydeletelog?aid=${aid}&title=${data.title}`, target: '_blank' }, [_('text', '查看删除日志')])
  1132. ]),
  1133. _('div', { className: 'comm', id: 'bbComment' }, [
  1134. _('div', { id: 'load_comment', className: 'comm_open_btn', onclick: "var fb = new bbFeedback('.comm', 'arc');fb.show(" + aid + ", 1);", style: { cursor: 'pointer' } })
  1135. ])
  1136. ])
  1137. ]));
  1138. document.title = data.title;
  1139. msgBox.parentNode.remove(); // 移除 .error-container
  1140. // return Promise.reject('该AV号不属于任何番剧页');//No bangumi in api response
  1141. } else {
  1142. // 当前av属于番剧页面, 继续处理
  1143. season_id = data.bangumi.season_id;
  1144. return balh_api_plus_season(season_id);
  1145. }
  1146. })
  1147. .then(function (result) {
  1148. if (result.code) {
  1149. return Promise.reject(JSON.stringify(result));
  1150. }
  1151. var ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid,
  1152. episodes = result.result.episodes,
  1153. ep;
  1154. // 为何要用三种不同方式匹配, 详见: https://greasyfork.org/zh-CN/forum/discussion/22379/x#Comment_34127
  1155. for (var i = 0; i < episodes.length; i++) {
  1156. ep = episodes[i];
  1157. if (ep.danmaku == cid) {
  1158. ep_id_by_cid = ep.episode_id;
  1159. }
  1160. if (ep.av_id == aid && ep.page == page) {
  1161. ep_id_by_aid_page = ep.episode_id;
  1162. }
  1163. if (ep.av_id == aid) {
  1164. ep_id_by_aid = ep.episode_id;
  1165. }
  1166. }
  1167. episode_id = ep_id_by_cid || ep_id_by_aid_page || ep_id_by_aid;
  1168. if (episode_id) {
  1169. var bangumi_url = `//bangumi.bilibili.com/anime/${season_id}/play#${episode_id}`;
  1170. log('Redirect', 'aid:', aid, 'page:', page, 'cid:', cid, '==>', bangumi_url, '(ep_id:', ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid, ')');
  1171. msg.innerText = '即将跳转到:' + bangumi_url;
  1172. location.href = bangumi_url;
  1173. } else {
  1174. return Promise.reject('查询episode_id失败');
  1175. }
  1176. })
  1177. .catch(function (e) {
  1178. log('error:', arguments);
  1179. msg.innerText = 'error:' + e;
  1180. });
  1181. }
  1182.  
  1183. util_init(() => {
  1184. if (util_page.av()) {
  1185. tryRedirectToBangumiOrInsertPlayer()
  1186. }
  1187. })
  1188. return true // 随便返回一个值...
  1189. }())
  1190. const bash_feature_FillSeasonList = (function () {
  1191. function tryFillSeasonList() {
  1192. var error_container, season_id;
  1193. if (!(error_container = document.querySelector('div.error-container'))) {
  1194. return;
  1195. }
  1196. if (!(season_id = window.location.pathname.match(/^\/anime\/(\d+)\/?$/)[1])) {
  1197. return;
  1198. }
  1199.  
  1200. //尝试解决怪异模式渲染
  1201. /*
  1202. 会造成变量丢失,等待官方重写doctype
  1203. try{
  1204. window.stop();
  1205. var xhr = new XMLHttpRequest();
  1206. xhr.open('GET',location.href,false);
  1207. xhr.send();
  1208. document.head.appendChild(_('script',{},[_('text',
  1209. 'document.write(unescape("'+escape(xhr.response.replace(/<!DOCTYPE.+?>/,'<!DOCTYPE HTML>'))+'"));window.stop()'
  1210. )]));
  1211. }catch(e){util_error(e);}
  1212. */
  1213.  
  1214. var msg = _('a', { href: '//bangumi.bilibili.com/anime/' + season_id + '/play', style: { fontSize: '20px' } }, [_('text', `【${GM_info.script.name}】尝试获取视频列表中...`)]),
  1215. content = _('div');
  1216.  
  1217. error_container.insertBefore(content, error_container.firstChild);
  1218. content.appendChild(msg);
  1219. log('season>:', season_id);
  1220. balh_api_plus_season(season_id)
  1221. .then(function (data) {
  1222. log('season>then:', data);
  1223. if (data.code) {
  1224. return Promise.reject(data);
  1225. }
  1226.  
  1227. function generateEpisodeList(episodes) {
  1228. var childs = [];
  1229. episodes.reverse().forEach(function (i) {
  1230. 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' } }, [
  1231. _('div', { className: 'img-wrp' }, [_('img', { src: i.cover, style: { opacity: 1 }, loaded: 'loaded', alt: i.index + ' ' + i.index_title })]),
  1232. _('div', { className: 'text-wrp' }, [
  1233. _('div', { className: 'text-wrp-num' }, [_('div', { className: 'text-wrp-num-content' }, [_('text', `第${i.index}话`)])]),
  1234. _('div', { className: 'text-wrp-title trunc' }, [_('text', i.index_title)])
  1235. ])
  1236. ])]));
  1237. });
  1238. return childs;
  1239. }
  1240.  
  1241. function generateSeasonList(seasons) {
  1242. function onSeasonClick(event) {
  1243. window.location.href = '//bangumi.bilibili.com/anime/' + event.target.attributes['data-season-id'].value;
  1244. }
  1245.  
  1246. return seasons.map(function (season) {
  1247. return _('li', { className: season.season_id == season_id ? 'cur' : '', 'data-season-id': season.season_id, event: { click: onSeasonClick } }, [_('text', season.title)]);
  1248. });
  1249. }
  1250.  
  1251. if (data.result) {
  1252. document.title = data.result.title;
  1253. document.head.appendChild(_('link', { href: 'https://s3.hdslb.com/bfs/static/anime/css/tag-index.css?v=110', rel: 'stylesheet' }));
  1254. document.head.appendChild(_('link', { href: 'https://s1.hdslb.com/bfs/static/anime/css/bangumi-index.css?v=110', rel: 'stylesheet' }));
  1255. document.body.insertBefore(_('div', { className: 'main-container-wrapper' }, [_('div', { className: 'main-container' }, [
  1256. _('div', { className: 'page-info-wrp' }, [_('div', { className: 'bangumi-info-wrapper' }, [
  1257. _('div', { className: 'bangumi-info-blurbg-wrapper' }, [_('div', { className: 'bangumi-info-blurbg blur', style: { backgroundImage: 'url(' + data.result.cover + ')' } })]),
  1258. _('div', { className: 'main-inner' }, [_('div', { className: 'info-content' }, [
  1259. _('div', { className: 'bangumi-preview' }, [_('img', { alt: data.result.title, src: data.result.cover })]),
  1260. _('div', { className: 'bangumi-info-r' }, [
  1261. _('div', { className: 'b-head' }, [_('h1', { className: 'info-title', 'data-seasonid': season_id, title: data.result.title }, [_('text', data.result.title)])]),
  1262. _('div', { className: 'info-count' }, [
  1263. _('span', { className: 'info-count-item info-count-item-play' }, [_('span', { className: 'info-label' }, [_('text', '总播放')]), _('em', {}, [_('text', data.result.play_count)])]),
  1264. _('span', { className: 'info-count-item info-count-item-fans' }, [_('span', { className: 'info-label' }, [_('text', '追番人数')]), _('em', {}, [_('text', data.result.favorites)])]),
  1265. _('span', { className: 'info-count-item info-count-item-review' }, [_('span', { className: 'info-label' }, [_('text', '弹幕总数')]), _('em', {}, [_('text', data.result.danmaku_count)])])
  1266. ]),
  1267. //_('div',{className:'info-row info-update'},[]),
  1268. //_('div',{className:'info-row info-cv'},[]),
  1269. _('div', { className: 'info-row info-desc-wrp' }, [
  1270. _('div', { className: 'info-row-label' }, [_('text', '简介:')]),
  1271. _('div', { className: 'info-desc' }, [_('text', data.result.evaluate)])
  1272. ]),
  1273. ])
  1274. ])])
  1275. ])]),
  1276. _('div', { className: 'main-inner' }, [_('div', { className: 'v1-bangumi-list-wrapper clearfix' }, [
  1277. _('div', { className: 'v1-bangumi-list-season-wrapper' }, [
  1278. _('div', { className: 'v1-bangumi-list-season-content slider-list-content' }, [
  1279. _('div', {}, [
  1280. _('ul', { className: 'v1-bangumi-list-season clearfix slider-list', 'data-current-season-id': season_id, style: { opacity: 1 } }, generateSeasonList(data.result.seasons))
  1281. ])
  1282. ])
  1283. ]),
  1284. _('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' } }, [
  1285. _('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))])])])
  1286. ])])
  1287. ])])
  1288. ])]), msg.parentNode.parentNode);
  1289. msg.parentNode.parentNode.remove();
  1290. }
  1291. })
  1292. .catch(function (error) {
  1293. log('season>catch', error);
  1294. msg.innerText = 'error:' + JSON.stringify(error) + '\n点击跳转到播放界面 (不一定能够正常播放...)';
  1295. });
  1296. }
  1297.  
  1298. util_init(() => {
  1299. if (util_page.bangumi()) {
  1300. tryFillSeasonList()
  1301. }
  1302. })
  1303. return true
  1304. }())
  1305.  
  1306. const balh_ui_setting = (function () {
  1307. function addSettingsButton() {
  1308. var indexNav = document.getElementById('index_nav') || document.querySelector('.bangumi-nav-right'),
  1309. bottom = '110px';
  1310. if (indexNav == null) {
  1311. document.head.appendChild(_('style', {}, [_('text', '.index-nav{opacity:1;display:block;bottom:50px;left:calc(50% + 500px);z-index:100} @media screen and (min-width:1160px){.index-nav{left:calc(50% + 590px)}}')]));
  1312. indexNav = document.body.appendChild(_('div', {
  1313. id: 'index_nav',
  1314. className: 'index-nav'
  1315. }));
  1316. bottom = 0;
  1317. } else {
  1318. window.dispatchEvent(new Event('resize'));
  1319. indexNav.style.display = 'block';
  1320. }
  1321. indexNav.appendChild(_('div', { className: 'n-i gotop balh_settings', style: { bottom: bottom }, title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', { className: 'btn_gotop', style: { background: '#f6f9fa' } })]));
  1322. indexNav.lastChild.firstChild.innerHTML = '<!-- https://www.flaticon.com/free-icon/saturn_53515 --><svg style="width:30px;height:50px;fill:rgb(153,162,170)" 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>';
  1323. }
  1324.  
  1325. function showSettings() {
  1326. document.body.appendChild(settingsDOM);
  1327. var form = settingsDOM.querySelector('form');
  1328. // elements包含index的属性, 和以name命名的属性, 其中以name命名的属性是不可枚举的, 只能通过这种方式获取出来
  1329. Object.getOwnPropertyNames(form.elements).forEach(function (name) {
  1330. if (name.startsWith('balh_')) {
  1331. var key = name.replace('balh_', '')
  1332. var ele = form.elements[name]
  1333. if (ele.type === 'checkbox') {
  1334. ele.checked = balh_config[key];
  1335. } else {
  1336. ele.value = balh_config[key];
  1337. }
  1338. }
  1339. })
  1340. document.body.style.overflow = 'hidden';
  1341. }
  1342.  
  1343. function onSignClick(event) {
  1344. settingsDOM.click();
  1345. switch (event.target.attributes['data-sign']) {
  1346. default:
  1347. case 'in':
  1348. balh_feature_sign.showLogin();
  1349. break;
  1350. case 'out':
  1351. balh_feature_sign.showLogout();
  1352. break;
  1353. }
  1354. }
  1355.  
  1356. function onSettingsFormChange(e) {
  1357. var name = e.target.name;
  1358. var value = e.target.type === 'checkbox' ? (e.target.checked ? 'Y' : '') : e.target.value
  1359. balh_config[name.replace('balh_', '')] = value
  1360. log(name, ' => ', value);
  1361. }
  1362.  
  1363. 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: 1000, cursor: 'pointer' }, event: { click: function (e) { if (e.target === this) util_ui_msg.close(), document.body.style.overflow = '', this.remove(); } } }, [
  1364. _('style', {}, [_('text', '@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;}')]),
  1365. _('div', { style: { position: 'absolute', background: '#FFF', borderRadius: '10px', padding: '20px', top: '50%', left: '50%', width: '600px', transform: 'translate(-50%,-50%)', cursor: 'default' } }, [
  1366. _('h1', {}, [_('text', GM_info.script.name + ' 参数设置')]),
  1367. _('br'),
  1368. _('form', { id: 'balh-settings-form', event: { change: onSettingsFormChange } }, [
  1369. _('text', '使用的服务器:'), _('br'),
  1370. _('div', { style: { display: 'flex' } }, [
  1371. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: 'https://biliplus.ipcjs.win' }), _('text', 'https://biliplus.ipcjs.win')]),
  1372. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: 'https://www.biliplus.com' }), _('text', 'https://www.biliplus.com')])
  1373. ]), _('br'),
  1374. _('div', { id: 'balh_server_ping', style: { whiteSpace: 'pre-wrap', overflow: 'auto' } }, [_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '服务器测速')])]), _('br'),
  1375. _('text', '脚本工作模式:'), _('br'),
  1376. _('div', { style: { display: 'flex' } }, [
  1377. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.DEFAULT }), _('text', '默认:自动判断')]),
  1378. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.REPLACE }), _('text', '替换:在需要时处理番剧')]),
  1379. _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.REDIRECT }), _('text', '重定向:完全代理所有番剧')])
  1380. ]), _('br'),
  1381. _('text', '其他:'), _('br'),
  1382. _('div', { style: { display: 'flex' } }, [
  1383. _('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', '(详细说明)')])]),
  1384. _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_flv_prefer_ws' }), _('text', '优先使用ws.acgvideo.com')]),
  1385. ]), _('br'),
  1386. _('a', { href: 'javascript:', 'data-sign': 'in', event: { click: onSignClick } }, [_('text', '帐号授权')]),
  1387. _('text', ' '),
  1388. _('a', { href: 'javascript:', 'data-sign': 'out', event: { click: onSignClick } }, [_('text', '取消授权')]),
  1389. _('text', '  '),
  1390. _('a', { href: 'javascript:', event: { click: function () { util_ui_msg.show($(this), '如果你的帐号进行了付费,不论是大会员还是承包,<br>进行授权之后将可以在解除限制时正常享有这些权益<br><br>你可以随时在这里授权或取消授权<br><br>不进行授权不会影响脚本的正常使用,但可能会缺失1080P', 1e4); } } }, [_('text', '(这是什么?)')]),
  1391. _('br'), _('br'),
  1392. _('div', { style: { whiteSpace: 'pre-wrap' } }, [
  1393. _('a', { href: 'https://greasyfork.org/zh-CN/scripts/25718-%E8%A7%A3%E9%99%A4b%E7%AB%99%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6', target: '_blank' }, [_('text', '脚本主页')]),
  1394. _('text', '\n作者: ipcjs\n代码贡献: esterTion FlandreDaisuki\n接口提供:BiliPlus')
  1395. ])
  1396. ])
  1397. ])
  1398. ]);
  1399.  
  1400. util_init(() => {
  1401. if (!(util_page.player() || util_page.av())) {
  1402. addSettingsButton()
  1403. }
  1404. }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
  1405. return {
  1406. dom: settingsDOM,
  1407. show: showSettings,
  1408. }
  1409. }())
  1410.  
  1411. function main() {
  1412. util_log(
  1413. 'mode:', balh_config.mode,
  1414. 'blocked_vip:', balh_config.blocked_vip,
  1415. 'server:', balh_config.server,
  1416. 'flv_prefer_ws:', balh_config.flv_prefer_ws,
  1417. 'readyState:', document.readyState
  1418. )
  1419. // 暴露接口
  1420. window.bangumi_area_limit_hack = {
  1421. setCookie: util_cookie.set,
  1422. getCookie: util_cookie.get,
  1423. login: balh_feature_sign.showLogin,
  1424. logout: balh_feature_sign.showLogout,
  1425. _clear_local_value: function () {
  1426. delete localStorage.balh_notFirst;
  1427. delete localStorage.balh_login;
  1428. delete localStorage.balh_mainLogin;
  1429. delete localStorage.oauthTime;
  1430. delete localStorage.balh_h5_not_first;
  1431. }
  1432. }
  1433. }
  1434.  
  1435. main();