// ==UserScript==
// @name 解除B站区域限制
// @namespace http://tampermonkey.net/
// @version 5.6.3
// @description 通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;
// @author ipcjs
// @require https://static.hdslb.com/js/md5.js
// @include *://www.bilibili.com/video/av*
// @include *://bangumi.bilibili.com/anime/*
// @include *://bangumi.bilibili.com/movie/*
// @include *://www.bilibili.com/blackboard/html5player.html*
// @include *://www.bilibili.com/blackboard/html5playerbeta.html*
// @run-at document-start
// @grant none
// @downloadURL none
// ==/UserScript==
'use strict';
var log = window.console.log.bind(window.console); // console.log简写为log
log('[' + GM_info.script.name + '] run on: ' + window.location.href);
var MODE_DEFAULT = 'default'; // 默认模式, 自动判断使用何种模式, 推荐;
var MODE_REPLACE = 'replace'; // 替换模式, 替换有区域限制的视频的接口的返回值; 因为替换的操作是同步的会卡一下界面, 但没有区域限制的视频不会受到影响;
var MODE_REDIRECT = 'redirect'; // 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
var settings = getCookies();
var proxyServer = settings.balh_server || 'https://biliplus.ipcjsdev.tk'; // 优先从cookie中读取服务器地址
var isBlockedVip = settings.balh_blocked_vip; // "我是一位被永久封号的大会员"(by Google翻译)
var mode = settings.balh_mode || (isBlockedVip ? MODE_REDIRECT : MODE_DEFAULT); // 若账号是被永封的大会员, 默认使用重定向模式
// movie页面使用window.aid, 保存当前页面av号
// anime页面使用window.season_id, 保存当前页面season号
var isMoviePage = window.location.href.indexOf('bangumi.bilibili.com/movie/') !== -1;
log('Mode:', mode, 'isBlockedVip:', isBlockedVip, 'server:', proxyServer, 'readyState:', document.readyState);
var bilibiliApis = (function () {
function BilibiliApi(props) {
Object.assign(this, props);
}
BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
var one_api = this;
$.ajax({
url: one_api.transToProxyUrl(originUrl),
async: true,
xhrFields: { withCredentials: true },
success: function (result) {
log('==>', result);
success(one_api.processProxySuccess(result));
// log('success', arguments, this);
},
error: function (e) {
log('error', arguments, this);
error(e);
}
});
};
var get_source_by_aid = new BilibiliApi({
transToProxyUrl: function (url) {
return proxyServer + '/api/view?id=' + window.aid + '&update=true';
},
processProxySuccess: function (data) {
if (data && data.list && data.list[0] && data.movie) {
return {
code: 0,
message: 'success',
result: {
cid: data.list[0].cid,
formal_aid: data.aid,
movie_status: isBlockedVip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
pay_begin_time: 1507708800,
pay_timestamp: 0,
pay_user_status: data.movie.pay_user.status, // 一般都是0
player: data.list[0].type, // 一般为movie
vid: data.list[0].vid,
vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
vipType: isBlockedVip ? 2 : 0,
vipStatus: isBlockedVip ? 1 : 0,
}
}
};
} else {
return {
code: -404,
message: '不存在该剧集'
};
}
}
});
var get_source_by_season_id = new BilibiliApi({
transToProxyUrl: function (url) {
return proxyServer + '/api/bangumi?season=' + window.season_id;
},
processProxySuccess: function (data) {
var found = null;
if (!data.code) {
for (var i = 0; i < data.result.episodes.length; i++) {
if (data.result.episodes[i].episode_id == window.episode_id) {
found = data.result.episodes[i];
}
}
} else {
notify.showNotification(Date.now(), GM_info.script.name, '代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', '//bangumi.bilibili.com/favicon.ico', 3e3, window.location.reload.bind(window.location));
}
var returnVal = found !== null ? {
"code": 0,
"message": "success",
"result": {
"aid": found.av_id,
"cid": found.danmaku,
"episode_status": isBlockedVip ? 2 : found.episode_status,
"payment": { "price": "9876547210.33" },
"pay_user": {
"status": isBlockedVip ? 1 : 0 // 是否已经支付过
},
"player": "vupload",
"pre_ad": 0,
"season_status": isBlockedVip ? 2 : data.result.season_status
}
} : {
code: -404,
message: '不存在该剧集'
};
return returnVal;
}
});
return {
_get_source: isMoviePage ? get_source_by_aid : get_source_by_season_id,
_playurl: new BilibiliApi({
transToProxyUrl: function (url) {
return proxyServer + '/BPplayurl.php?' + url.split('?')[1].replace(/(cid=\d+)/, '$1|' + (url.match(/module=(\w+)/) || ['', 'bangumi'])[1]);
},
processProxySuccess: function (data) {
// data有可能为null
if (data && data.code === -403) {
// window.alert('当前使用的服务器(' + proxyServer + ')依然有区域限制');
showNotification(Date.now(), GM_info.script.name, '突破黑洞失败,我们未能穿透敌人的盔甲\n当前代理服务器(' + proxyServer + ')依然有区域限制Σ(  ̄□ ̄||)', '//bangumi.bilibili.com/favicon.ico', 3e3);
} else if (data === null || data.code) {
console.error(data);
showNotification(Date.now(), GM_info.script.name, '突破黑洞失败\n' + JSON.stringify(data) + '\n点击刷新界面', '//bangumi.bilibili.com/favicon.ico', 3e3, window.location.reload.bind(window.location));
} else if (isAreaLimitForPlayUrl(data)) {
console.error('>>area limit');
showNotification(Date.now(), GM_info.script.name, '突破黑洞失败,需要登录\n点此进行登录', '//bangumi.bilibili.com/favicon.ico', 3e3, showLogin);
// if (window.confirm('试图获取视频地址失败, 请登录代理服务器' +
// '\n注意: 只支持"使用bilibili账户密码进行登录"'
// )) {
// window.top.location = proxyServer + '/login';
// }
} else {
// showNotification(Date.now(), GM_info.script.name, '已突破黑洞,开始加载视频', '//bangumi.bilibili.com/favicon.ico', 2e3);
}
return data;
}
})
};
})();
if (!window.jQuery) { // 若还未加载jQuery, 则监听
var jQuery;
Object.defineProperty(window, 'jQuery', {
configurable: true, enumerable: true, set: function (v) {
jQuery = v;
injectDataFilter();// 设置jQuery后, 立即注入
}, get: function () {
return jQuery;
}
});
} else {
injectDataFilter();
}
documentReady(function () {
if (window.location.hostname === 'bangumi.bilibili.com') {
checkHtml5();
if (window.location.pathname.match(/^\/anime\/\d+$/)) {
tryFillSeasonList();
}
} else if (window.location.href.indexOf('www.bilibili.com/video/av') !== -1) {
tryBangumiRedirect();
}
});
windowReady(function () {
if (window.location.hostname === 'bangumi.bilibili.com') {
addSettingsButton();
checkLoginState();
}
});
// 监听登录message
window.addEventListener('message', function (e) {
switch (e.data) {
case 'BiliPlus-Login-Success':
//登入
document.head.appendChild(_('script', {
src: proxyServer + '/login?act=getlevel',
event: {
load: function () { location.reload(); },
error: function () { location.reload(); }
}
}));
break;
case 'BiliPlus-Logout-Success':
//登出
location.reload();
break;
}
});
// 添加设置入口节点
/*
MessageBox -> from base.core.js
MessageBox.show(referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback)
MessageBox.close()
*/
var popMessage = null;
function addSettingsButton() {
popMessage == null && (popMessage = new MessageBox);
var indexNav = document.getElementById('index_nav'), bottom = '110px';
if (indexNav == null) {
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)}}')]));
indexNav = document.body.appendChild(_('div', {
id: 'index_nav',
className: 'index-nav'
}));
bottom = 0;
} else {
window.dispatchEvent(new Event('resize'));
indexNav.style.display = 'block';
}
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' } })]));
indexNav.lastChild.firstChild.innerHTML = '';
}
function onSettingsFormChange(e) {
var name = e.target.name;
console.log(name, ' => ', e.target.type == 'checkbox' ? e.target.checked : e.target.value);
switch (name) {
case 'balh_proxy_server':
proxyServer = e.target.value;
setCookie('balh_server', proxyServer);
break;
case 'balh_mode':
mode = e.target.value;
setCookie('balh_mode', mode);
break;
case 'balh_blocked_vip':
isBlockedVip = e.target.checked ? 'Y' : '';
setCookie('balh_blocked_vip', isBlockedVip);
break;
}
}
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) popMessage.msgbox != null && popMessage.close(), document.body.style.overflow = '', this.remove(); } } }, [
_('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}')]),
_('div', { style: { position: 'absolute', background: '#FFF', borderRadius: '10px', padding: '20px', top: '50%', left: '50%', width: '600px', transform: 'translate(-50%,-50%)', cursor: 'default' } }, [
_('h1', {}, [_('text', GM_info.script.name + ' 参数设置')]),
_('br'),
_('form', { id: 'balh-settings-form', event: { change: onSettingsFormChange } }, [
_('text', '使用的服务器:'), _('br'),
_('div', { style: { display: 'flex' } }, [
_('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_proxy_server', value: 'https://biliplus.ipcjsdev.tk' }), _('text', 'https://biliplus.ipcjsdev.tk')]),
_('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_proxy_server', value: 'https://www.biliplus.com' }), _('text', 'https://www.biliplus.com')])
]), _('br'),
_('div', { id: 'balh_server_ping', style: { whiteSpace: 'pre-wrap', overflow: 'auto' } }, [_('a', { href: 'javascript:', event: { click: runPing } }, [_('text', '服务器测速')])]), _('br'),
_('text', '脚本工作模式:'), _('br'),
_('div', { style: { display: 'flex' } }, [
_('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: MODE_DEFAULT }), _('text', '默认:自动判断')]),
_('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: MODE_REPLACE }), _('text', '替换:在需要时处理番剧')]),
_('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: MODE_REDIRECT }), _('text', '重定向:完全代理所有番剧')])
]), _('br'),
_('text', '其他:'), _('br'),
_('div', { style: { display: 'flex' } }, [
_('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', '(详细说明)')])]),
]), _('br'),
_('a', { href: 'javascript:', event: { click: function () { settingsDOM.click(); showLogin(); } } }, [_('text', '帐号授权')]),
_('text', ' '),
_('a', { href: 'javascript:', event: { click: function () { settingsDOM.click(); showLogout(); } } }, [_('text', '取消授权')]),
_('text', ' '),
_('a', { href: 'javascript:', event: { click: function () { popMessage.show($(this), '如果你的帐号进行了付费,不论是大会员还是承包,
进行授权之后将可以在解除限制时正常享有这些权益
你可以随时在这里授权或取消授权
不进行授权不会影响脚本的正常使用,但可能会缺失1080P', 1e4)[0]; } } }, [_('text', '(这是什么?)')]),
_('br'), _('br'),
_('div', { style: { whiteSpace: 'pre-wrap' } }, [
_('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', '脚本主页')]),
_('text', '\n作者: ipcjs\n代码贡献: esterTion FlandreDaisuki\n接口提供:BiliPlus')
])
])
])
]);
var pingOutput;
function showSettings() {
document.body.appendChild(settingsDOM);
var form = settingsDOM.querySelector('form');
form.elements['balh_proxy_server'].value = proxyServer;
form.elements['balh_mode'].value = mode;
form.elements['balh_blocked_vip'].checked = isBlockedVip;
pingOutput = document.getElementById('balh_server_ping');
document.body.style.overflow = 'hidden';
}
// 测速
function runPing() {
var xhr = new XMLHttpRequest(), testUrl = ['https://biliplus.ipcjsdev.tk', 'https://www.biliplus.com'], testUrlIndex = 0, isReused = false, prevNow, outputArr = [];
pingOutput.textContent = '正在进行服务器测速…';
pingOutput.style.height = '100px';
xhr.open('GET', '', true);
xhr.onreadystatechange = function () {
this.readyState == 4 && pingResult();
};
var pingLoop = function () {
prevNow = performance.now();
xhr.open('GET', testUrl[testUrlIndex] + '/api/bangumi', true);
xhr.send();
};
var pingResult = function () {
var duration = (performance.now() - prevNow) | 0;
if (isReused)
outputArr.push('\t复用连接:' + duration + 'ms'), isReused = false, testUrlIndex++;
else
outputArr.push(testUrl[testUrlIndex] + ':'), outputArr.push('\t初次连接:' + duration + 'ms'), isReused = true;
pingOutput.textContent = outputArr.join('\n');
testUrlIndex < testUrl.length ? pingLoop() : pingOutput.appendChild(_('a', { href: 'javascript:', event: { click: runPing } }, [_('text', '\n再测一次?')]));
};
pingLoop();
}
// 暴露接口
window.bangumi_area_limit_hack = {
setCookie: setCookie,
getCookie: getCookie,
login: showLogin,
logout: showLogout,
_clear_local_value: function () {
delete localStorage.balh_notFirst;
delete localStorage.balh_login;
delete localStorage.balh_mainLogin;
delete localStorage.oauthTime;
delete localStorage.balh_h5_not_first;
}
};
window.bangumi_aera_limit_hack = window.bangumi_area_limit_hack; // 兼容...
////////////////接下来全是函数/////////////////
function injectDataFilter() {
window.jQuery.ajaxSetup({
dataFilter: jqueryDataFilter
});
replaceAjax();
}
function jqueryDataFilter(data, type) {
var json, group;
// log(arguments, this);
if (this.url.startsWith(window.location.protocol + '//bangumi.bilibili.com/web_api/season_area')) {
// 番剧页面是否要隐藏番剧列表 API
log(data);
json = JSON.parse(data);
// 限制区域时的data为:
// {"code":0,"message":"success","result":{"play":0}}
if (json.code === 0 && json.result && json.result.play === 0) {
mode === MODE_DEFAULT && setAreaLimitSeason(true);
json.result.play = 1; // 改成1就能够显示
data = JSON.stringify(json);
log('==>', data);
// showNotification(Date.now(), GM_info.script.name, '检测到区域限制番剧,准备启动黑洞突破程序\n\n刷下存在感(°∀°)ノ', '//bangumi.bilibili.com/favicon.ico', 2e3);
} else {
mode === MODE_DEFAULT && setAreaLimitSeason(false);
}
}
return data;
}
/*
{"code":0,"message":"success","result":{"aid":9854952,"cid":16292628,"episode_status":2,"payment":{"price":"0"},"player":"vupload","pre_ad":0,"season_status":2}}
*/
function replaceAjax() {
var originalAjax = $.ajax;
$.ajax = function (arg0, arg1) {
// log(arguments);
var param;
if (arg1 === undefined) {
param = arg0;
} else {
arg0 && (arg1.url = arg0);
param = arg1;
}
var oriSuccess = param.success;
var mySuccess;
var one_api;
if (param.url.match('/web_api/get_source')) {
one_api = bilibiliApis._get_source;
if (mode === MODE_REDIRECT || (mode === MODE_DEFAULT && isAreaLimitSeason())) { // 对应redirect模式
param.url = one_api.transToProxyUrl(param.url);
param.type = 'GET';
delete param.data;
param.success = function (data) {
var returnVal = one_api.processProxySuccess(data);
log('Redirected request: get_source', returnVal);
oriSuccess(returnVal);
};
} else { // 对应replace模式
param.success = function (json) {
log(json);
if (json.code === -40301 // 区域限制
|| json.result.payment && json.result.payment.price != 0 && isBlockedVip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
oriSuccess(json); // 新的请求报错, 也应该返回原来的数据
});
mode === MODE_DEFAULT && setAreaLimitSeason(true); // 只要默认模式才要跟踪是否有区域限制
} else {
mode === MODE_DEFAULT && setAreaLimitSeason(false);
if (isBlockedVip && json.code === 0 && json.result.pre_ad) {
json.result.pre_ad = 0; // 去除前置广告
}
oriSuccess(json); // 保证一定调用了原来的success
}
};
}
} else if (param.url.match('/player/web_api/playurl')) {
one_api = bilibiliApis._playurl;
if (mode === MODE_REDIRECT || (mode === MODE_DEFAULT && isAreaLimitSeason())) {
param.url = one_api.transToProxyUrl(param.url);
param.success = function (data) {
oriSuccess(one_api.processProxySuccess(data));
};
log('Redirected request: bangumi playurl -> ', param.url);
} else {
param.success = function (json) {
// 获取视频地址 API
log(json);
if (isBlockedVip || json.code || isAreaLimitForPlayUrl(json)) {
one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
oriSuccess(json);
});
mode === MODE_DEFAULT && setAreaLimitSeason(true);
} else {
mode === MODE_DEFAULT && setAreaLimitSeason(false);
oriSuccess(json);
}
};
}
} else if (param.url.match('//interface.bilibili.com/player?')) {
if (isBlockedVip) {
mySuccess = function (data) {
try {
var xml = new window.DOMParser().parseFromString('