解除B站区域限制

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

נכון ליום 21-10-2017. ראה הגרסה האחרונה.

  1. // ==UserScript==
  2. // @name 解除B站区域限制
  3. // @namespace http://tampermonkey.net/
  4. // @version 5.5.5
  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 *://bangumi.bilibili.com/anime/*
  10. // @include *://bangumi.bilibili.com/movie/*
  11. // @include *://www.bilibili.com/blackboard/html5player.html*
  12. // @include *://www.bilibili.com/blackboard/html5playerbeta.html*
  13. // @run-at document-start
  14. // @grant none
  15. // ==/UserScript==
  16.  
  17. 'use strict';
  18.  
  19. log('[' + GM_info.script.name + '] run on: ' + window.location.href);
  20.  
  21. var MODE_DEFAULT = 'default'; // 默认模式, 自动判断使用何种模式, 推荐;
  22. var MODE_REPLACE = 'replace'; // 替换模式, 替换有区域限制的视频的接口的返回值; 因为替换的操作是同步的会卡一下界面, 但没有区域限制的视频不会受到影响;
  23. var MODE_REDIRECT = 'redirect'; // 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
  24.  
  25. var settings = getCookies();
  26. var proxyServer = settings.balh_server || 'https://biliplus.ipcjsdev.tk'; // 优先从cookie中读取服务器地址
  27. var isBlockedVip = settings.balh_blocked_vip; // "我是一位被永久封号的大会员"(by Google翻译)
  28. var mode = settings.balh_mode || (isBlockedVip ? MODE_REDIRECT : MODE_DEFAULT); // 若账号是被永封的大会员, 默认使用重定向模式
  29. // movie页面使用window.aid, 保存当前页面av号
  30. // anime页面使用window.season_id, 保存当前页面season号
  31. var isMoviePage = window.location.href.indexOf('bangumi.bilibili.com/movie/') !== -1;
  32.  
  33. log('Mode:', mode, 'isBlockedVip:', isBlockedVip, 'server:', proxyServer, 'readyState:', document.readyState);
  34.  
  35. var bilibiliApis = (function () {
  36. function BilibiliApi(props) {
  37. Object.assign(this, props);
  38. }
  39.  
  40. BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
  41. var one_api = this;
  42. $.ajax({
  43. url: one_api.transToProxyUrl(originUrl),
  44. async: true,
  45. xhrFields: { withCredentials: true },
  46. success: function (result) {
  47. log('==>', result);
  48. success(one_api.processProxySuccess(result));
  49. // log('success', arguments, this);
  50. },
  51. error: function (e) {
  52. log('error', arguments, this);
  53. error(e);
  54. }
  55. });
  56. };
  57. var get_source_by_aid = new BilibiliApi({
  58. transToProxyUrl: function (url) {
  59. return proxyServer + '/api/view?id=' + window.aid + '&update=true';
  60. },
  61. processProxySuccess: function (data) {
  62. if (data && data.list && data.list[0] && data.movie) {
  63. return {
  64. code: 0,
  65. message: 'success',
  66. result: {
  67. cid: data.list[0].cid,
  68. formal_aid: data.aid,
  69. movie_status: isBlockedVip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
  70. pay_begin_time: 1507708800,
  71. pay_timestamp: 0,
  72. pay_user_status: data.movie.pay_user.status, // 一般都是0
  73. player: data.list[0].type, // 一般为movie
  74. vid: data.list[0].vid,
  75. vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
  76. vipType: isBlockedVip ? 2 : 0,
  77. vipStatus: isBlockedVip ? 1 : 0,
  78. }
  79. }
  80. };
  81. } else {
  82. return {
  83. code: -404,
  84. message: '不存在该剧集'
  85. };
  86. }
  87. }
  88. });
  89. var get_source_by_season_id = new BilibiliApi({
  90. transToProxyUrl: function (url) {
  91. return proxyServer + '/api/bangumi?season=' + window.season_id;
  92. },
  93. processProxySuccess: function (data) {
  94. var found = null;
  95. if (!data.code) {
  96. for (var i = 0; i < data.result.episodes.length; i++) {
  97. if (data.result.episodes[i].episode_id == window.episode_id) {
  98. found = data.result.episodes[i];
  99. }
  100. }
  101. } else {
  102. notify.showNotification(Date.now(), GM_info.script.name, '代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', '//bangumi.bilibili.com/favicon.ico', 3e3, window.location.reload.bind(window.location));
  103. }
  104. var returnVal = found !== null ? {
  105. "code": 0,
  106. "message": "success",
  107. "result": {
  108. "aid": found.av_id,
  109. "cid": found.danmaku,
  110. "episode_status": isBlockedVip ? 2 : found.episode_status,
  111. "payment": { "price": "9876547210.33" },
  112. "pay_user": {
  113. "status": isBlockedVip ? 1 : 0 // 是否已经支付过
  114. },
  115. "player": "vupload",
  116. "pre_ad": 0,
  117. "season_status": isBlockedVip ? 2 : data.result.season_status
  118. }
  119. } : {
  120. code: -404,
  121. message: '不存在该剧集'
  122. };
  123. return returnVal;
  124. }
  125. });
  126.  
  127. return {
  128. _get_source: isMoviePage ? get_source_by_aid : get_source_by_season_id,
  129. _playurl: new BilibiliApi({
  130. transToProxyUrl: function (url) {
  131. return proxyServer + '/BPplayurl.php?' + url.split('?')[1].replace(/(cid=\d+)/, '$1|' + (url.match(/module=(\w+)/) || ['', 'bangumi'])[1]);
  132. },
  133. processProxySuccess: function (data) {
  134. // data有可能为null
  135. if (data && data.code === -403) {
  136. // window.alert('当前使用的服务器(' + proxyServer + ')依然有区域限制');
  137. showNotification(Date.now(), GM_info.script.name, '突破黑洞失败,我们未能穿透敌人的盔甲\n当前代理服务器(' + proxyServer + ')依然有区域限制Σ(  ̄□ ̄||)', '//bangumi.bilibili.com/favicon.ico', 3e3);
  138. } else if (data === null || data.code) {
  139. console.error(data);
  140. showNotification(Date.now(), GM_info.script.name, '突破黑洞失败\n' + JSON.stringify(data) + '\n点击刷新界面', '//bangumi.bilibili.com/favicon.ico', 3e3, window.location.reload.bind(window.location));
  141. } else if (isAreaLimitForPlayUrl(data)) {
  142. console.error('>>area limit');
  143. showNotification(Date.now(), GM_info.script.name, '突破黑洞失败,需要登录\n点此进行登录', '//bangumi.bilibili.com/favicon.ico', 3e3, showLogin);
  144. // if (window.confirm('试图获取视频地址失败, 请登录代理服务器' +
  145. // '\n注意: 只支持"使用bilibili账户密码进行登录"'
  146. // )) {
  147. // window.top.location = proxyServer + '/login';
  148. // }
  149. } else {
  150. // showNotification(Date.now(), GM_info.script.name, '已突破黑洞,开始加载视频', '//bangumi.bilibili.com/favicon.ico', 2e3);
  151. }
  152. return data;
  153. }
  154. })
  155. };
  156. })();
  157.  
  158. if (!window.jQuery) { // 若还未加载jQuery, 则监听
  159. var jQuery;
  160. Object.defineProperty(window, 'jQuery', {
  161. configurable: true, enumerable: true, set: function (v) {
  162. jQuery = v;
  163. injectDataFilter();// 设置jQuery后, 立即注入
  164. }, get: function () {
  165. return jQuery;
  166. }
  167. });
  168. } else {
  169. injectDataFilter();
  170. }
  171.  
  172. documentReady(function () {
  173. if (window.location.hostname === 'bangumi.bilibili.com') {
  174. checkLoginState();
  175. checkHtml5();
  176. if (window.location.pathname.match(/^\/anime\/\d+$/)) {
  177. tryFillSeasonList();
  178. }
  179. } else if (window.location.href.indexOf('www.bilibili.com/video/av') !== -1) {
  180. tryBangumiRedirect();
  181. }
  182. });
  183.  
  184. window.addEventListener('message', function (e) {
  185. console.log(e.data);
  186. switch (e.data) {
  187. case 'BiliPlus-Login-Success':
  188. //登入
  189. document.head.appendChild(_('script', {
  190. src: proxyServer + '/login?act=getlevel',
  191. event: {
  192. load: function () { location.reload(); }
  193. }
  194. }));
  195. break;
  196. case 'BiliPlus-Logout-Success':
  197. //登出
  198. location.reload();
  199. break;
  200. }
  201. });
  202.  
  203. // 暴露接口
  204. window.bangumi_area_limit_hack = {
  205. setCookie: setCookie,
  206. getCookie: getCookie,
  207. login: showLogin,
  208. logout: showLogout,
  209. _clear_local_value: function () {
  210. delete localStorage.balh_notFirst;
  211. delete localStorage.balh_login;
  212. delete localStorage.balh_mainLogin;
  213. delete localStorage.oauthTime;
  214. delete localStorage.balh_h5_not_first;
  215. }
  216. };
  217. window.bangumi_aera_limit_hack = window.bangumi_area_limit_hack; // 兼容...
  218.  
  219. ////////////////接下来全是函数/////////////////
  220.  
  221. function injectDataFilter() {
  222. window.jQuery.ajaxSetup({
  223. dataFilter: jqueryDataFilter
  224. });
  225. replaceAjax();
  226. }
  227.  
  228. function jqueryDataFilter(data, type) {
  229. var json, group;
  230. // log(arguments, this);
  231. if (this.url.startsWith(window.location.protocol + '//bangumi.bilibili.com/web_api/season_area')) {
  232. // 番剧页面是否要隐藏番剧列表 API
  233. log(data);
  234. json = JSON.parse(data);
  235. // 限制区域时的data为:
  236. // {"code":0,"message":"success","result":{"play":0}}
  237. if (json.code === 0 && json.result && json.result.play === 0) {
  238. mode === MODE_DEFAULT && setAreaLimitSeason(true);
  239. json.result.play = 1; // 改成1就能够显示
  240. data = JSON.stringify(json);
  241. log('==>', data);
  242. showNotification(Date.now(), GM_info.script.name, '检测到区域限制番剧,准备启动黑洞突破程序\n\n刷下存在感(°∀°)ノ', '//bangumi.bilibili.com/favicon.ico', 2e3);
  243. } else {
  244. mode === MODE_DEFAULT && setAreaLimitSeason(false);
  245. }
  246. }
  247. return data;
  248. }
  249.  
  250. /*
  251. {"code":0,"message":"success","result":{"aid":9854952,"cid":16292628,"episode_status":2,"payment":{"price":"0"},"player":"vupload","pre_ad":0,"season_status":2}}
  252. */
  253. function replaceAjax() {
  254. var originalAjax = $.ajax;
  255. $.ajax = function (arg0, arg1) {
  256. // log(arguments);
  257. var param;
  258. if (arg1 === undefined) {
  259. param = arg0;
  260. } else {
  261. arg0 && (arg1.url = arg0);
  262. param = arg1;
  263. }
  264. var oriSuccess = param.success;
  265. var mySuccess;
  266. var one_api;
  267. if (param.url.match('/web_api/get_source')) {
  268. one_api = bilibiliApis._get_source;
  269. if (mode === MODE_REDIRECT || (mode === MODE_DEFAULT && isAreaLimitSeason())) { // 对应redirect模式
  270. param.url = one_api.transToProxyUrl(param.url);
  271. param.type = 'GET';
  272. delete param.data;
  273. param.success = function (data) {
  274. var returnVal = one_api.processProxySuccess(data);
  275. log('Redirected request: get_source', returnVal);
  276. oriSuccess(returnVal);
  277. };
  278. } else { // 对应replace模式
  279. param.success = function (json) {
  280. log(json);
  281. if (json.code === -40301 // 区域限制
  282. || json.result.payment && json.result.payment.price != 0 && isBlockedVip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
  283. one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
  284. oriSuccess(json); // 新的请求报错, 也应该返回原来的数据
  285. });
  286. mode === MODE_DEFAULT && setAreaLimitSeason(true); // 只要默认模式才要跟踪是否有区域限制
  287. } else {
  288. mode === MODE_DEFAULT && setAreaLimitSeason(false);
  289. if (isBlockedVip && json.code === 0 && json.result.pre_ad) {
  290. json.result.pre_ad = 0; // 去除前置广告
  291. }
  292. oriSuccess(json); // 保证一定调用了原来的success
  293. }
  294. };
  295. }
  296. } else if (param.url.match('/player/web_api/playurl')) {
  297. one_api = bilibiliApis._playurl;
  298. if (mode === MODE_REDIRECT || (mode === MODE_DEFAULT && isAreaLimitSeason())) {
  299. param.url = one_api.transToProxyUrl(param.url);
  300. param.success = function (data) {
  301. oriSuccess(one_api.processProxySuccess(data));
  302. };
  303. log('Redirected request: bangumi playurl -> ', param.url);
  304. } else {
  305. param.success = function (json) {
  306. // 获取视频地址 API
  307. log(json);
  308. if (isBlockedVip || json.code || isAreaLimitForPlayUrl(json)) {
  309. one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
  310. oriSuccess(json);
  311. });
  312. mode === MODE_DEFAULT && setAreaLimitSeason(true);
  313. } else {
  314. mode === MODE_DEFAULT && setAreaLimitSeason(false);
  315. oriSuccess(json);
  316. }
  317. };
  318. }
  319. } else if (param.url.match('//interface.bilibili.com/player?')) {
  320. if (isBlockedVip) {
  321. mySuccess = function (data) {
  322. try {
  323. var xml = new window.DOMParser().parseFromString('<userstatus>' + data.replace(/\&/g, '&amp;') + '</userstatus>', 'text/xml');
  324. var vipTag = xml.querySelector('vip');
  325. if (vipTag) {
  326. var vip = JSON.parse(vipTag.innerHTML);
  327. vip.vipType = 2; // 类型, 年度大会员
  328. vip.vipStatus = 1; // 状态, 启用
  329. vipTag.innerHTML = JSON.stringify(vip);
  330. data = xml.documentElement.innerHTML;
  331. }
  332. } catch (e) {
  333. log('parse xml error: ', e);
  334. }
  335. oriSuccess(data);
  336. };
  337. }
  338. }
  339.  
  340. // 若外部使用param.success处理结果, 则替换param.success
  341. if (oriSuccess && mySuccess) {
  342. param.success = mySuccess;
  343. }
  344. // default
  345. var xhr = originalAjax.apply(this, [param]);
  346.  
  347. // 若外部使用xhr.done()处理结果, 则替换xhr.done()
  348. if (!oriSuccess && mySuccess) {
  349. xhr.done(mySuccess);
  350. xhr.done = function (success) {
  351. oriSuccess = success; // 保存外部设置的success函数
  352. return xhr;
  353. };
  354. }
  355. return xhr;
  356. };
  357. }
  358.  
  359. function isAreaLimitSeason() {
  360. return getCookie('balh_season_' + getSeasonId());
  361. }
  362.  
  363. function setAreaLimitSeason(limit) {
  364. var season_id = getSeasonId();
  365. setCookie('balh_season_' + season_id, limit ? '1' : undefined, ''); // 第三个参数为'', 表示时Session类型的cookie
  366. log('setAreaLimitSeason', season_id, limit);
  367. }
  368.  
  369. function getSeasonId() {
  370. var seasonId;
  371. // 取anime页面的seasonId
  372. try {
  373. // 若w, 是其frame的window, 则有可能没有权限, 而抛异常
  374. seasonId = window.season_id || window.top.season_id;
  375. } catch (e) {
  376. console.error(e);
  377. }
  378. if (!seasonId) {
  379. seasonId = (window.top.location.pathname.match(/\/anime\/(\d+)/) || ['', ''])[1];
  380. }
  381.  
  382. // 若没取到, 则取movie页面的seasonId, 以m开头
  383. if (!seasonId) {
  384. seasonId = (window.top.location.pathname.match(/\/movie\/(\d+)/) || ['', ''])[1];
  385. if (seasonId) {
  386. seasonId = 'm' + seasonId;
  387. }
  388. }
  389. return seasonId || '000';
  390. }
  391.  
  392. function isAreaLimitForPlayUrl(json) {
  393. return json.durl && json.durl.length === 1 && json.durl[0].length === 15126 && json.durl[0].size === 124627;
  394. }
  395.  
  396. function getParam(url, key) {
  397. return (url.match(new RegExp('[?|&]' + key + '=(\\w+)')) || ['', ''])[1];
  398. }
  399.  
  400. function getCookies() {
  401. var map = document.cookie.split('; ').reduce(function (obj, item) {
  402. var entry = item.split('=');
  403. obj[entry[0]] = entry[1];
  404. return obj;
  405. }, {});
  406. return map;
  407. }
  408.  
  409. function getCookie(key) {
  410. return getCookies()[key];
  411. }
  412.  
  413. // document.cookie=`bangumi_area_limit_hack_server=https://www.biliplus.com; domain=.bilibili.com; path=/; expires=${new Date("2020-01-01").toUTCString()}`;
  414.  
  415. /**
  416. * @key key
  417. * @value 为undefined时, 表示删除cookie
  418. * @options 为undefined时, 表示过期时间为3年
  419. * 为''时, 表示Session cookie
  420. * 为数字时, 表示指定过期时间
  421. * 为{}时, 表示指定所有的属性
  422. * */
  423. function setCookie(key, value, options) {
  424. if (typeof options !== 'object') {
  425. options = {
  426. domain: '.bilibili.com',
  427. path: '/',
  428. 'max-age': value === undefined ? 0 : (options === undefined ? 94608000 : options)
  429. };
  430. }
  431. var c = Object.keys(options).reduce(function (str, key) {
  432. return str + '; ' + key + '=' + options[key];
  433. }, key + '=' + value);
  434. document.cookie = c;
  435. return c;
  436. }
  437.  
  438. function showLogin() {
  439. var loginUrl = proxyServer + '/login',
  440. iframeSrc = 'https://passport.bilibili.com/login?appkey=27eb53fc9058f8c3&api=' + encodeURIComponent(loginUrl) + '&sign=' + hex_md5('api=' + loginUrl + 'c2ed53a74eeefe3cf99fbd01d8c9c375');
  441. showPopWindow(iframeSrc);
  442. delete localStorage.balh_login;
  443. }
  444.  
  445. function showLogout() {
  446. showPopWindow(proxyServer + '/login?act=logout');
  447. }
  448.  
  449. function showPopWindow(iframeSrc) {
  450. if (!document.getElementById('balh-style-login')) {
  451. var style = document.createElement('style');
  452. style.id = 'balh-style-login';
  453. 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}';
  454. }
  455.  
  456. var div = document.createElement('div');
  457. div.id = 'GMBiliPlusLoginContainer';
  458. 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>';
  459. div.firstChild.addEventListener('click', function (e) {
  460. if (e.target === this || e.target.className === 'GMBiliPlusCloseBox') {
  461. if (!confirm('确认关闭?')) {
  462. return false;
  463. }
  464. div.firstChild.style.animationName = 'pop-iframe-out';
  465. setTimeout(function () {
  466. div.remove();
  467. window.location.reload();
  468. }, 5e2);
  469. }
  470. });
  471. document.body.appendChild(div);
  472. }
  473.  
  474. // 逻辑有点乱, 当前在如下情况才会弹一次登录提示框:
  475. // 1. 第一次使用
  476. // 2. 主站+服务器都退出登录后, 再重新登录主站
  477. function checkLoginState() {
  478. if (getCookie("DedeUserID") === undefined) {
  479. //未登录主站,强制指定值
  480. localStorage.balh_notFirst = 1;
  481. localStorage.balh_login = 0;
  482. localStorage.balh_mainLogin = 0;
  483. } else if (localStorage.balh_mainLogin !== undefined) {
  484. //主站未登录变为登录,重置显示弹窗
  485. delete localStorage.balh_notFirst;
  486. delete localStorage.balh_login;
  487. delete localStorage.balh_mainLogin;
  488. delete localStorage.oauthTime;
  489. }
  490. if (!localStorage.balh_notFirst) {
  491. //第一次打开,确认是否已登陆;未登录显示确认弹窗
  492. localStorage.balh_notFirst = 1;
  493. checkExpiretime(function () {
  494. if (localStorage.oauthTime === undefined) {
  495. localStorage.balh_login = 0;
  496. if (confirm('看起来你是第一次使用' + GM_info.script.name + '\n要不要考虑进行一下授权?\n\n授权后可以观看区域限定番剧的1080P(如果你是大会员或承包过的话)\n\n你可以随时通过执行 bangumi_area_limit_hack.login() 来打开授权页面')) {
  497. showLogin();
  498. }
  499. } else {
  500. localStorage.balh_login = 1;
  501. }
  502. });
  503. } else if (localStorage.balh_login === undefined) {
  504. //非第一次打开,登录状态被重置,重新检测
  505. checkExpiretime(function () {
  506. localStorage.balh_login = (localStorage.oauthTime === undefined) ? 0 : 1;
  507. });
  508. } else if (localStorage.balh_login == 1 && Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000) {
  509. //已登录,每天为周期检测key有效期,过期前五天会自动续期
  510. checkExpiretime();
  511. }
  512.  
  513. function checkExpiretime(loadCallback) {
  514. var script = document.createElement('script');
  515. script.src = proxyServer + '/login?act=expiretime';
  516. loadCallback && script.addEventListener('load', loadCallback);
  517. document.head.appendChild(script);
  518. }
  519. }
  520.  
  521. function checkHtml5() {
  522. if (!localStorage.balh_h5_not_first && localStorage.defaulth5 == 0 && window.GrayManager) {
  523. new MutationObserver(function (mutations, observer) {
  524. observer.disconnect();
  525. localStorage.balh_h5_not_first = 'yes';
  526. if (window.confirm(GM_info.script.name + '只在HTML5播放器下有效,是否切换到HTML5?')) {
  527. window.GrayManager.clickMenu('change_h5');// change_flash, change_h5
  528. }
  529. }).observe(document.querySelector('.player-content'), {
  530. childList: true, // 监听child的增减
  531. attributes: false, // 监听属性的变化
  532. });
  533. }
  534. }
  535.  
  536. // document载入完成后回调, 相当于$(cb);
  537. function documentReady(cb) {
  538. if (document.readyState !== 'loading') {
  539. return setTimeout(cb, 1);
  540. }
  541. var cbWrapper = function () {
  542. document.removeEventListener('DOMContentLoaded', cbWrapper);
  543. cb();
  544. };
  545. document.addEventListener('DOMContentLoaded', cbWrapper);
  546. }
  547.  
  548. function log() {
  549. console.log.apply(console, arguments);
  550. }
  551.  
  552. /**
  553. * 通知模块
  554. * 剽窃自 YAWF 用户脚本
  555. * 硬广:https://tiansh.github.io/yawf/
  556. */
  557. var notify = (function () {
  558. var avaliable = {};
  559. var shown = [];
  560. var use = {
  561. 'hasPermission': function () {
  562. return null;
  563. },
  564. 'requestPermission': function (callback) {
  565. return null;
  566. },
  567. 'hideNotification': function (notify) {
  568. return null;
  569. },
  570. 'showNotification': function (id, title, body, icon, delay, onclick) {
  571. return null;
  572. }
  573. };
  574.  
  575. // 检查一个微博是不是已经被显示过了,如果显示过了不重复显示
  576. var shownFeed = function (id) {
  577. return false;
  578. };
  579.  
  580. // webkitNotifications
  581. // Tab Notifier 扩展实现此接口,但显示的桌面提示最多只能显示前两行
  582. if (typeof webkitNotifications !== 'undefined') avaliable.webkit = {
  583. 'hasPermission': function () {
  584. return [true, null, false][webkitNotifications.checkPermission()];
  585. },
  586. 'requestPermission': function (callback) {
  587. return webkitNotifications.requestPermission(callback);
  588. },
  589. 'hideNotification': function (notify) {
  590. notify.cancel();
  591. afterHideNotification(notify);
  592. },
  593. 'showNotification': function (id, title, body, icon, delay, onclick) {
  594. if (shownFeed(id)) return null;
  595. var notify = webkitNotifications.createNotification(icon, title, body);
  596. if (delay && delay > 0) notify.addEventListener('display', function () {
  597. setTimeout(function () {
  598. hideNotification(notify);
  599. }, delay);
  600. });
  601. if (onclick) notify.addEventListener('click', function () {
  602. onclick.apply(this, arguments);
  603. hideNotification(notify);
  604. });
  605. notify.show();
  606. return notify;
  607. },
  608. };
  609.  
  610. // Notification
  611. // Firefox 22+
  612. // 显示4秒会自动关闭 https://bugzil.la/875114
  613. if (typeof Notification !== 'undefined') avaliable.standard = {
  614. 'hasPermission': function () {
  615. return {
  616. 'granted': true,
  617. 'denied': false,
  618. 'default': null,
  619. }[Notification.permission];
  620. },
  621. 'requestPermission': function (callback) {
  622. return Notification.requestPermission(callback);
  623. },
  624. 'hideNotification': function (notify) {
  625. notify.close();
  626. afterHideNotification(notify);
  627. },
  628. 'showNotification': function (id, title, body, icon, delay, onclick) {
  629. if (shownFeed(id)) return null;
  630. var notify = new Notification(title, { 'body': body, 'icon': icon, 'requireInteraction': !delay });
  631. if (delay && delay > 0) notify.addEventListener('show', function () {
  632. setTimeout(function () {
  633. hideNotification(notify);
  634. }, delay);
  635. });
  636. if (onclick) notify.addEventListener('click', function () {
  637. onclick.apply(this, arguments);
  638. hideNotification(notify);
  639. });
  640. return notify;
  641. },
  642. };
  643.  
  644. // 有哪些接口可用
  645. var avaliableNotification = function () {
  646. return Object.keys(avaliable);
  647. };
  648. // 选择用哪个接口
  649. var choseNotification = function (prefer) {
  650. return (use = prefer && avaliable[prefer] || avaliable.standard);
  651. };
  652. choseNotification();
  653. // 检查权限
  654. var hasPermission = function () {
  655. return use.hasPermission.apply(this, arguments);
  656. };
  657. // 请求权限
  658. var requestPermission = function () {
  659. return use.requestPermission.apply(this, arguments);
  660. };
  661. // 显示消息
  662. var showNotification = function (id, title, body, icon, delay, onclick) {
  663. var notify = use.showNotification.apply(this, arguments);
  664. shown.push(notify);
  665. return notify;
  666. };
  667. // 隐藏已经显示的消息
  668. var hideNotification = function (notify) {
  669. use.hideNotification.apply(this, arguments);
  670. return notify;
  671. };
  672. var afterHideNotification = function (notify) {
  673. shown = shown.filter(function (x) {
  674. return x !== notify;
  675. });
  676. };
  677.  
  678. document.addEventListener('unload', function () {
  679. shown.forEach(hideNotification);
  680. shown = [];
  681. });
  682.  
  683. return {
  684. 'avaliableNotification': avaliableNotification,
  685. 'choseNotification': choseNotification,
  686. 'hasPermission': hasPermission,
  687. 'requestPermission': requestPermission,
  688. 'showNotification': showNotification,
  689. 'hideNotification': hideNotification
  690. };
  691.  
  692. }());
  693.  
  694. function showNotification() {
  695. switch (notify.hasPermission()) {
  696. case null: // default
  697. var thatArguments = arguments;
  698. notify.requestPermission(function () {
  699. showNotification.apply(null, thatArguments);
  700. });
  701. break;
  702. case true: // granted
  703. notify.showNotification.apply(null, arguments);
  704. break;
  705. case false: // denied
  706. log('Notification permission: denied');
  707. break;
  708. }
  709. }
  710.  
  711. /**
  712. * 创建元素的快捷方法
  713. * @param type string, 标签名; 特殊的, 若为text, 则表示创建文字, 对应的t为文字的内容
  714. * @param props object, 属性; 特殊的属性名有: className, 类名; style, 样式, 值为(样式名, 值)形式的object; event, 值为(事件名, 监听函数)形式的object;
  715. * @param children array, 子元素;
  716. */
  717. function _(type, props, children) {
  718. var elem = null;
  719. if (type === "text") {
  720. return document.createTextNode(props);
  721. } else {
  722. elem = document.createElement(type);
  723. }
  724. for (var n in props) {
  725. if (n === "style") {
  726. for (var x in props.style) {
  727. elem.style[x] = props.style[x];
  728. }
  729. } else if (n === "className") {
  730. elem.className = props[n];
  731. } else if (n === "event") {
  732. for (var x in props.event) {
  733. elem.addEventListener(x, props.event[x]);
  734. }
  735. } else {
  736. elem.setAttribute(n, props[n]);
  737. }
  738. }
  739. if (children) {
  740. for (var i = 0; i < children.length; i++) {
  741. if (children[i] != null)
  742. elem.appendChild(children[i]);
  743. }
  744. }
  745. return elem;
  746. }
  747.  
  748. function tryBangumiRedirect() {
  749. var msgBox;
  750. if (!(msgBox = document.querySelector('.b-page-body > .error-container > .error-panel'))) {
  751. return;
  752. }
  753. var msg = document.createElement('a');
  754. msgBox.insertBefore(msg, msgBox.firstChild);
  755. msg.innerText = '获取番剧页Url中...';
  756.  
  757. var aid = location.pathname.replace(/.*av(\d+).*/, '$1'),
  758. page = (location.pathname.match(/\/index_(\d+).html/) || ['', '1'])[1],
  759. cid,
  760. season_id,
  761. episode_id;
  762. ajaxPromise(proxyServer + '/api/view?id=' + aid + '&update=true')
  763. .then(function (data) {
  764. if (data.code) {
  765. return Promise.reject(JSON.stringify(data));
  766. }
  767. if (!data.bangumi) {
  768. return Promise.reject('该AV号不属于任何番剧页');//No bangumi in api response
  769. }
  770. season_id = data.bangumi.season_id;
  771. for (var i = 0; i < data.list.length; i++) {
  772. if (data.list[i].page == page) {
  773. cid = data.list[i].cid;
  774. break;
  775. }
  776. }
  777. return ajaxPromise(proxyServer + '/api/bangumi?season=' + season_id);
  778. })
  779. .then(function (result) {
  780. if (result.code) {
  781. return Promise.reject(JSON.stringify(result));
  782. }
  783. var ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid,
  784. episodes = result.result.episodes,
  785. ep;
  786. // 为何要用三种不同方式匹配, 详见: https://greasyfork.org/zh-CN/forum/discussion/22379/x#Comment_34127
  787. for (var i = 0; i < episodes.length; i++) {
  788. ep = episodes[i];
  789. if (ep.danmaku == cid) {
  790. ep_id_by_cid = ep.episode_id;
  791. }
  792. if (ep.av_id == aid && ep.page == page) {
  793. ep_id_by_aid_page = ep.episode_id;
  794. }
  795. if (ep.av_id == aid) {
  796. ep_id_by_aid = ep.episode_id;
  797. }
  798. }
  799. episode_id = ep_id_by_cid || ep_id_by_aid_page || ep_id_by_aid;
  800. if (episode_id) {
  801. var bangumi_url = '//bangumi.bilibili.com/anime/' + season_id + '/play#' + episode_id;
  802. 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, ')');
  803. msg.innerText = '即将跳转到:' + bangumi_url;
  804. location.href = bangumi_url;
  805. } else {
  806. return Promise.reject('查询episode_id失败');
  807. }
  808. })
  809. .catch(function (e) {
  810. log('error:', arguments);
  811. msg.innerText = 'error:' + e;
  812. });
  813. }
  814.  
  815. function tryFillSeasonList() {
  816. var error_container, season_id;
  817. if (!(error_container = document.querySelector('div.error-container'))) {
  818. return;
  819. }
  820. if (!(season_id = window.location.pathname.match(/^\/anime\/(\d+)$/)[1])) {
  821. return;
  822. }
  823.  
  824. //尝试解决怪异模式渲染
  825. /*
  826. 会造成变量丢失,等待官方重写doctype
  827. try{
  828. window.stop();
  829. var xhr = new XMLHttpRequest();
  830. xhr.open('GET',location.href,false);
  831. xhr.send();
  832. document.head.appendChild(_('script',{},[_('text',
  833. 'document.write(unescape("'+escape(xhr.response.replace(/<!DOCTYPE.+?>/,'<!DOCTYPE HTML>'))+'"));window.stop()'
  834. )]));
  835. }catch(e){console.error(e);}
  836. */
  837.  
  838. var msg = _('a', { href: '//bangumi.bilibili.com/anime/' + season_id + '/play', style: { fontSize: '20px' } }, [_('text', '【' + GM_info.script.name + '】尝试获取视频列表中...')]),
  839. content = _('div');
  840.  
  841. error_container.insertBefore(content, error_container.firstChild);
  842. content.appendChild(msg);
  843. log('season>:', season_id);
  844. ajaxPromise(proxyServer + '/api/bangumi?season=' + season_id)
  845. .then(function (data) {
  846. log('season>then:', data);
  847. if (data.code) {
  848. return Promise.reject(data);
  849. }
  850.  
  851. function generateEpisodeList(episodes) {
  852. var childs = [];
  853. episodes.reverse().forEach(function (i) {
  854. 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' } }, [
  855. _('div', { className: 'img-wrp' }, [_('img', { src: i.cover, style: { opacity: 1 }, loaded: 'loaded', alt: i.index + ' ' + i.index_title })]),
  856. _('div', { className: 'text-wrp' }, [
  857. _('div', { className: 'text-wrp-num' }, [_('div', { className: 'text-wrp-num-content' }, [_('text', '第' + i.index + '话')])]),
  858. _('div', { className: 'text-wrp-title trunc' }, [_('text', i.index_title)])
  859. ])
  860. ])]));
  861. });
  862. return childs;
  863. }
  864.  
  865. function generateSeasonList(seasons) {
  866. function onSeasonClick(event) {
  867. window.location.href = '//bangumi.bilibili.com/anime/' + event.target.attributes['data-season-id'].value;
  868. }
  869.  
  870. return seasons.map(function (season) {
  871. return _('li', { className: season.season_id == season_id ? 'cur' : '', 'data-season-id': season.season_id, event: { click: onSeasonClick } }, [_('text', season.title)]);
  872. });
  873. }
  874.  
  875. if (data.result) {
  876. document.title = data.result.title;
  877. document.head.appendChild(_('link', { href: 'https://s3.hdslb.com/bfs/static/anime/css/tag-index.css?v=110', rel: 'stylesheet' }));
  878. document.head.appendChild(_('link', { href: 'https://s1.hdslb.com/bfs/static/anime/css/bangumi-index.css?v=110', rel: 'stylesheet' }));
  879. document.body.insertBefore(_('div', { className: 'main-container-wrapper' }, [_('div', { className: 'main-container' }, [
  880. _('div', { className: 'page-info-wrp' }, [_('div', { className: 'bangumi-info-wrapper' }, [
  881. _('div', { className: 'bangumi-info-blurbg-wrapper' }, [_('div', { className: 'bangumi-info-blurbg blur', style: { backgroundImage: 'url(' + data.result.cover + ')' } })]),
  882. _('div', { className: 'main-inner' }, [_('div', { className: 'info-content' }, [
  883. _('div', { className: 'bangumi-preview' }, [_('img', { alt: data.result.title, src: data.result.cover })]),
  884. _('div', { className: 'bangumi-info-r' }, [
  885. _('div', { className: 'b-head' }, [_('h1', { className: 'info-title', 'data-seasonid': season_id, title: data.result.title }, [_('text', data.result.title)])]),
  886. _('div', { className: 'info-count' }, [
  887. _('span', { className: 'info-count-item info-count-item-play' }, [_('span', { className: 'info-label' }, [_('text', '总播放')]), _('em', {}, [_('text', data.result.play_count)])]),
  888. _('span', { className: 'info-count-item info-count-item-fans' }, [_('span', { className: 'info-label' }, [_('text', '追番人数')]), _('em', {}, [_('text', data.result.favorites)])]),
  889. _('span', { className: 'info-count-item info-count-item-review' }, [_('span', { className: 'info-label' }, [_('text', '弹幕总数')]), _('em', {}, [_('text', data.result.danmaku_count)])])
  890. ]),
  891. //_('div',{className:'info-row info-update'},[]),
  892. //_('div',{className:'info-row info-cv'},[]),
  893. _('div', { className: 'info-row info-desc-wrp' }, [
  894. _('div', { className: 'info-row-label' }, [_('text', '简介:')]),
  895. _('div', { className: 'info-desc' }, [_('text', data.result.evaluate)])
  896. ]),
  897. ])
  898. ])])
  899. ])]),
  900. _('div', { className: 'main-inner' }, [_('div', { className: 'v1-bangumi-list-wrapper clearfix' }, [
  901. _('div', { className: 'v1-bangumi-list-season-wrapper' }, [
  902. _('div', { className: 'v1-bangumi-list-season-content slider-list-content' }, [
  903. _('div', {}, [
  904. _('ul', { className: 'v1-bangumi-list-season clearfix slider-list', 'data-current-season-id': season_id, style: { opacity: 1 } }, generateSeasonList(data.result.seasons))
  905. ])
  906. ])
  907. ]),
  908. _('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' } }, [
  909. _('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))])])])
  910. ])])
  911. ])])
  912. ])]), msg.parentNode.parentNode);
  913. msg.parentNode.parentNode.remove();
  914. }
  915. })
  916. .catch(function (error) {
  917. log('season>catch', error);
  918. msg.innerText = 'error:' + JSON.stringify(error) + '\n点击跳转到播放界面 (不一定能够正常播放...)';
  919. });
  920. }
  921.  
  922. function ajaxPromise(options) {
  923. return new Promise(function (resolve, reject) {
  924. typeof options !== 'object' && (options = { url: options });
  925.  
  926. options.async === undefined && (options.async = true);
  927. options.xhrFields === undefined && (options.xhrFields = { withCredentials: true });
  928. options.success = function (data) {
  929. resolve(data);
  930. };
  931. options.error = function (err) {
  932. reject(err);
  933. };
  934. $.ajax(options);
  935. });
  936. }