├── demo ├── static │ ├── img │ │ ├── face.webp │ │ └── background.jpg │ ├── css │ │ └── styles.css │ └── js │ │ ├── md5.min.js │ │ └── demo.js └── index.html ├── index.php ├── LICENSE ├── src └── Injahow │ ├── LICENSE │ └── Bilibili.php ├── autoLoader.php ├── config.php ├── libs ├── danmaku.php └── bilibili.php ├── README.MD └── core.php /demo/static/img/face.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaruhiYunona/Danmaku-PHP-API/HEAD/demo/static/img/face.webp -------------------------------------------------------------------------------- /demo/static/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaruhiYunona/Danmaku-PHP-API/HEAD/demo/static/img/background.jpg -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | run(); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 樱樱怪 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Injahow/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 injahow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autoLoader.php: -------------------------------------------------------------------------------- 1 | 404, 'msg' => '[Danmaku]你所访问的接口请求了一个自动加载的类文件,但未从中成功引入类']); 34 | } 35 | } else { 36 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]你所访问的接口请求了一个自动加载的类文件,但未能找到该文件']); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | spl_autoload_register('autoloader::loader'); 43 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | > 16) + (t >> 16) + (r >> 16); 7 | return e << 16 | 65535 & r 8 | } 9 | 10 | function r(n, t) { return n << t | n >>> 32 - t } 11 | 12 | function e(n, e, u, o, c, f) { return t(r(t(t(e, n), t(o, f)), c), u) } 13 | 14 | function u(n, t, r, u, o, c, f) { return e(t & r | ~t & u, n, t, o, c, f) } 15 | 16 | function o(n, t, r, u, o, c, f) { return e(t & u | r & ~u, n, t, o, c, f) } 17 | 18 | function c(n, t, r, u, o, c, f) { return e(t ^ r ^ u, n, t, o, c, f) } 19 | 20 | function f(n, t, r, u, o, c, f) { return e(r ^ (t | ~u), n, t, o, c, f) } 21 | 22 | function i(n, r) { 23 | n[r >> 5] |= 128 << r % 32, n[(r + 64 >>> 9 << 4) + 14] = r; 24 | var e, i, a, h, d, g = 1732584193, 25 | l = -271733879, 26 | v = -1732584194, 27 | C = 271733878; 28 | for (e = 0; e < n.length; e += 16) i = g, a = l, h = v, d = C, g = u(g, l, v, C, n[e], 7, -680876936), C = u(C, g, l, v, n[e + 1], 12, -389564586), v = u(v, C, g, l, n[e + 2], 17, 606105819), l = u(l, v, C, g, n[e + 3], 22, -1044525330), g = u(g, l, v, C, n[e + 4], 7, -176418897), C = u(C, g, l, v, n[e + 5], 12, 1200080426), v = u(v, C, g, l, n[e + 6], 17, -1473231341), l = u(l, v, C, g, n[e + 7], 22, -45705983), g = u(g, l, v, C, n[e + 8], 7, 1770035416), C = u(C, g, l, v, n[e + 9], 12, -1958414417), v = u(v, C, g, l, n[e + 10], 17, -42063), l = u(l, v, C, g, n[e + 11], 22, -1990404162), g = u(g, l, v, C, n[e + 12], 7, 1804603682), C = u(C, g, l, v, n[e + 13], 12, -40341101), v = u(v, C, g, l, n[e + 14], 17, -1502002290), l = u(l, v, C, g, n[e + 15], 22, 1236535329), g = o(g, l, v, C, n[e + 1], 5, -165796510), C = o(C, g, l, v, n[e + 6], 9, -1069501632), v = o(v, C, g, l, n[e + 11], 14, 643717713), l = o(l, v, C, g, n[e], 20, -373897302), g = o(g, l, v, C, n[e + 5], 5, -701558691), C = o(C, g, l, v, n[e + 10], 9, 38016083), v = o(v, C, g, l, n[e + 15], 14, -660478335), l = o(l, v, C, g, n[e + 4], 20, -405537848), g = o(g, l, v, C, n[e + 9], 5, 568446438), C = o(C, g, l, v, n[e + 14], 9, -1019803690), v = o(v, C, g, l, n[e + 3], 14, -187363961), l = o(l, v, C, g, n[e + 8], 20, 1163531501), g = o(g, l, v, C, n[e + 13], 5, -1444681467), C = o(C, g, l, v, n[e + 2], 9, -51403784), v = o(v, C, g, l, n[e + 7], 14, 1735328473), l = o(l, v, C, g, n[e + 12], 20, -1926607734), g = c(g, l, v, C, n[e + 5], 4, -378558), C = c(C, g, l, v, n[e + 8], 11, -2022574463), v = c(v, C, g, l, n[e + 11], 16, 1839030562), l = c(l, v, C, g, n[e + 14], 23, -35309556), g = c(g, l, v, C, n[e + 1], 4, -1530992060), C = c(C, g, l, v, n[e + 4], 11, 1272893353), v = c(v, C, g, l, n[e + 7], 16, -155497632), l = c(l, v, C, g, n[e + 10], 23, -1094730640), g = c(g, l, v, C, n[e + 13], 4, 681279174), C = c(C, g, l, v, n[e], 11, -358537222), v = c(v, C, g, l, n[e + 3], 16, -722521979), l = c(l, v, C, g, n[e + 6], 23, 76029189), g = c(g, l, v, C, n[e + 9], 4, -640364487), C = c(C, g, l, v, n[e + 12], 11, -421815835), v = c(v, C, g, l, n[e + 15], 16, 530742520), l = c(l, v, C, g, n[e + 2], 23, -995338651), g = f(g, l, v, C, n[e], 6, -198630844), C = f(C, g, l, v, n[e + 7], 10, 1126891415), v = f(v, C, g, l, n[e + 14], 15, -1416354905), l = f(l, v, C, g, n[e + 5], 21, -57434055), g = f(g, l, v, C, n[e + 12], 6, 1700485571), C = f(C, g, l, v, n[e + 3], 10, -1894986606), v = f(v, C, g, l, n[e + 10], 15, -1051523), l = f(l, v, C, g, n[e + 1], 21, -2054922799), g = f(g, l, v, C, n[e + 8], 6, 1873313359), C = f(C, g, l, v, n[e + 15], 10, -30611744), v = f(v, C, g, l, n[e + 6], 15, -1560198380), l = f(l, v, C, g, n[e + 13], 21, 1309151649), g = f(g, l, v, C, n[e + 4], 6, -145523070), C = f(C, g, l, v, n[e + 11], 10, -1120210379), v = f(v, C, g, l, n[e + 2], 15, 718787259), l = f(l, v, C, g, n[e + 9], 21, -343485551), g = t(g, i), l = t(l, a), v = t(v, h), C = t(C, d); 29 | return [g, l, v, C] 30 | } 31 | 32 | function a(n) { var t, r = ""; for (t = 0; t < 32 * n.length; t += 8) r += String.fromCharCode(255 & n[t >> 5] >>> t % 32); return r } 33 | 34 | function h(n) { var t, r = []; for (r[(n.length >> 2) - 1] = void 0, t = 0; t < r.length; t += 1) r[t] = 0; for (t = 0; t < 8 * n.length; t += 8) r[t >> 5] |= (255 & n.charCodeAt(t / 8)) << t % 32; return r } 35 | 36 | function d(n) { return a(i(h(n), 8 * n.length)) } 37 | 38 | function g(n, t) { 39 | var r, e, u = h(n), 40 | o = [], 41 | c = []; 42 | for (o[15] = c[15] = void 0, u.length > 16 && (u = i(u, 8 * n.length)), r = 0; 16 > r; r += 1) o[r] = 909522486 ^ u[r], c[r] = 1549556828 ^ u[r]; 43 | return e = i(o.concat(h(t)), 512 + 8 * t.length), a(i(c.concat(e), 640)) 44 | } 45 | 46 | function l(n) { 47 | var t, r, e = "0123456789abcdef", 48 | u = ""; 49 | for (r = 0; r < n.length; r += 1) t = n.charCodeAt(r), u += e.charAt(15 & t >>> 4) + e.charAt(15 & t); 50 | return u 51 | } 52 | 53 | function v(n) { return unescape(encodeURIComponent(n)) } 54 | 55 | function C(n) { return d(v(n)) } 56 | 57 | function m(n) { return l(C(n)) } 58 | 59 | function s(n, t) { return g(v(n), v(t)) } 60 | 61 | function A(n, t) { return l(s(n, t)) } 62 | 63 | function p(n, t, r) { return t ? r ? s(t, n) : A(t, n) : r ? C(n) : m(n) } 64 | "function" == typeof define && define.amd ? define(function() { return p }) : n.md5 = p 65 | }(this); -------------------------------------------------------------------------------- /demo/static/js/demo.js: -------------------------------------------------------------------------------- 1 | //网页布局设置,这个和演示关系不大 2 | const layout = () => { 3 | let width = document.body.clientWidth; 4 | let ratio = document.body.clientWidth / document.body.clientHeight; 5 | let levelSize = null; 6 | document.getElementsByTagName('body')[0].style.height = width / 855 * 1200; 7 | if (ratio >= 1) { 8 | levelSize = '0.8px'; 9 | document.getElementsByClassName('card')[0].style.width = width * 0.6; 10 | document.getElementsByClassName('card')[1].style.width = width * 0.6; 11 | descHeight = 0; 12 | document.getElementsByTagName('h2')[0].setAttribute('style', ''); 13 | document.getElementById('uploader').setAttribute('style', ''); 14 | document.getElementsByClassName('face')[0].setAttribute('style', ''); 15 | document.getElementById('up').setAttribute('style', 'margin-left: 10px;'); 16 | document.getElementsByClassName('desc')[0].setAttribute('style', ''); 17 | document.getElementsByClassName('desc-text')[0].setAttribute('style', ''); 18 | document.getElementById('reply').setAttribute('style', ''); 19 | setStyle(document.getElementsByClassName('tag-text'), ''); 20 | setStyle(document.getElementsByClassName('reply-head'), '') 21 | setStyle(document.getElementsByClassName('reply-user-name'), ''); 22 | setStyle(document.getElementsByClassName('reply-content'), ''); 23 | setStyle(document.getElementsByClassName('reply-comment-box'), ''); 24 | document.getElementsByTagName('ul')[0].style.height = document.getElementsByTagName('body')[0].offsetHeight - document.getElementsByClassName('card')[0].clientHeight - 240; 25 | } else { 26 | levelSize = '0.2vw'; 27 | document.getElementsByClassName('card')[0].style.width = width * 0.84; 28 | document.getElementsByClassName('card')[1].style.width = width * 0.84; 29 | document.getElementsByTagName('h2')[0].setAttribute('style', 'font-size:5vw;float:none;'); 30 | document.getElementById('uploader').setAttribute('style', 'float:none;width:100%;display: inline;justify - content: none;padding-left:10%;font-size:1.5rem;line-height:6vw;'); 31 | document.getElementsByClassName('face')[0].setAttribute('style', 'float:left;width:6vw;height:6vw;border-radius:6vw,margin-left:3vw'); 32 | document.getElementById('up').setAttribute('style', 'float:left;margin-top:0;margin-left:10px;font-size:4vw;line-height:6vw'); 33 | document.getElementsByClassName('desc')[0].setAttribute('style', 'font-size:4vw'); 34 | document.getElementsByClassName('desc-text')[0].setAttribute('style', 'font-size:3vw'); 35 | document.getElementById('reply').setAttribute('style', 'font-size:4vw;'); 36 | setStyle(document.getElementsByClassName('tag-text'), 'font-size:3vw;'); 37 | setStyle(document.getElementsByClassName('reply-head'), 'width:10vw;height:10vw;border-radius:10vw;') 38 | setStyle(document.getElementsByClassName('reply-user-name'), 'font-size:3vw;'); 39 | setStyle(document.getElementsByClassName('reply-content'), 'font-size:2.7vw;'); 40 | setStyle(document.getElementsByClassName('reply-comment-box'), 'font-size:2.4vw;'); 41 | document.getElementsByTagName('ul')[0].style.height = document.getElementsByTagName('body')[0].offsetHeight - document.getElementsByClassName('card')[0].clientHeight + 300; 42 | } 43 | document.getElementById('flex-box').style.height = (document.getElementById('flex-box').clientWidth * 9 / 16); 44 | document.getElementsByClassName('card')[0].style.height = document.getElementById('flex-box').clientHeight + document.getElementsByClassName('desc')[0].clientHeight + document.getElementsByClassName('video-top')[0].clientHeight + 50; 45 | let level = document.getElementsByClassName('reply-user-level'); 46 | let levelLength = level.length; 47 | let color = { 0: '#3CB371', 1: '#00CED1', 2: '#4682B4', 3: '#DAA520', 4: '#9932CC', 5: '#DC143C' }; 48 | for (let i = 0; i < levelLength; i++) { 49 | let lv = level[i].innerHTML.replace(/LV/g, ''); 50 | let lvColor = color[lv * 1 - 1]; 51 | level[i].setAttribute('style', 'margin-left:10px;-webkit-text-stroke: ' + levelSize + ' ' + lvColor + ';'); 52 | } 53 | } 54 | 55 | 56 | 57 | 58 | //滚动到底自动加载列表方法 59 | const scrollList = () => { 60 | if (document.getElementsByTagName('ul')[0].scrollTop + document.getElementsByTagName('ul')[0].clientHeight + 10 >= document.getElementsByTagName('ul')[0].scrollHeight) { 61 | if (scrollFlag == 0) { 62 | let aid = document.getElementById('aid').value; 63 | let page = document.getElementById('page').value * 1 + 1; 64 | document.getElementById('page').setAttribute('value', page); 65 | scrollFlag = 1; 66 | flashList(aid, page); 67 | } 68 | } 69 | } 70 | 71 | 72 | //自行定义的检查方法 73 | const check = (data) => { 74 | let msg = null; 75 | if (data.code !== 0) { 76 | try { 77 | msg = data.msg; 78 | } catch (e) { 79 | msg = null; 80 | } 81 | if (msg != null) { 82 | msg = data.msg; 83 | } else { 84 | msg = data.message; 85 | } 86 | console.log("%c Danmaku %c " + msg, "color: #fff; margin: 1e m 0; padding: 2px 0; background: #F08080;", "margin: 1e m 0; padding: 5 px 0; background_round: #EFEFEF;"); 87 | return true; 88 | } else { 89 | return false; 90 | } 91 | } 92 | 93 | //自行定义的随机数生成,用来随机生成用户id。真实应用场景可以将你网站的用户id填在Dplayer配置上,这样就可以识别是哪个用户发的弹幕了 94 | const getUser = () => { 95 | return md5(Math.random()).slice(0, 6); 96 | } 97 | 98 | //自行定义的批量设置样式的方法 99 | const setStyle = (doc, style) => { 100 | let docLength = doc.length; 101 | for (let i = 0; i < docLength; i++) { 102 | doc[i].setAttribute('style', style); 103 | } 104 | } 105 | 106 | //监听窗大小改变自适应布局 107 | const getWindowInfo = () => { 108 | layout(); 109 | } 110 | const debounce = (fn, delay) => { 111 | let timer; 112 | return function () { 113 | if (timer) { 114 | clearTimeout(timer); 115 | } 116 | timer = setTimeout(() => { 117 | fn(); 118 | }, delay); 119 | } 120 | }; 121 | const cancalDebounce = debounce(getWindowInfo, 500); 122 | 123 | window.addEventListener('resize', cancalDebounce); 124 | 125 | 126 | -------------------------------------------------------------------------------- /libs/danmaku.php: -------------------------------------------------------------------------------- 1 | 404, 'msg' => '[Danmaku]请求的接口版本已被禁用']); 44 | } 45 | 46 | //定义变量并校验 47 | list($cid, $bv, $page) = [Request::get('cid'), Request::get('bv'), Request::get('page')]; 48 | $verify = ($cid != null) ? 49 | Request::validator(['cid' => $cid], [ 50 | 'cid' => 'number|between 1,10' 51 | ]) : 52 | Request::validator(['bv' => $bv], [ 53 | 'bv' => 'bv', 54 | ]); 55 | if ($verify['code'] == -1) { 56 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 57 | } 58 | 59 | //如果不是通过CID获取弹幕,先通过BV获取CID 60 | if ($cid == null) { 61 | ($page == null) && $page = 1; 62 | $verify = Request::validator(['page' => $page], [ 63 | 'page' => 'number|between 1,10', 64 | ]); 65 | if ($verify['code'] == -1) { 66 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 67 | } 68 | $videoInfo = json_decode(Request::curlGet('https://api.bilibili.com/x/player/pagelist?bvid=' . $bv . '&jsonp=jsonp'), true); 69 | if ($videoInfo['code'] == 0) { 70 | $cid = $videoInfo['data'][$page - 1]['cid']; 71 | } 72 | } 73 | 74 | //如果获取CID失败,返回失败JSON 75 | if ($cid == null) { 76 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]通过BV查询CID失败']); 77 | } 78 | 79 | //获取弹幕XML并解析 80 | $reader = new XMLReader(); 81 | $reader->xml(Request::curlGet('https://api.bilibili.com/x/v1/dm/list.so?oid=' . $cid, true)); 82 | list($danmakuData, $danmakuText, $danmaku[], $node) = [[], null, [0, 0, '16777215', 'f5211eb2', ''], null]; 83 | while ($reader->read()) { 84 | ($reader->nodeType == XMLReader::ELEMENT && $reader->localName == 'd') && 85 | list($node, $danmakuData) = [$reader->name, explode(',', $reader->getAttribute('p'))]; 86 | ($reader->nodeType == XMLReader::TEXT && $node == 'd') && 87 | $danmakuText = $reader->value; 88 | ($danmakuData !== null && $danmakuText !== null) && 89 | list($danmakuData, $danmakuText, $danmaku[]) = [null, null, [(float)$danmakuData[0], (int)$danmakuData[5], $danmakuData[3], $danmakuData[6], $danmakuText]]; 90 | } 91 | 92 | //响应接口请求:返回弹幕JSON 93 | $result = ($apiVersion == 'v2') ? 94 | [ 95 | 'code' => 0, 96 | 'version' => 2, 97 | 'danmaku' => $danmaku, 98 | 'msg' => '[Danmaku]B站弹幕加载完成' 99 | ] : 100 | [ 101 | 'code' => 0, 102 | 'version' => 3, 103 | 'data' => $danmaku, 104 | 'msg' => '[Danmaku]B站弹幕加载完成' 105 | ]; 106 | return Respons::json($result); 107 | } 108 | } 109 | 110 | class server 111 | { 112 | /** 113 | * 处理Dplayer弹幕请求 114 | * @return json 115 | */ 116 | public static function entrance() 117 | { 118 | if (!Db::isTableExist('pool')) { 119 | if (!Db::createTable('pool', [ 120 | 'user' => ['varchar', 50], 121 | 'vid' => ['varchar', 50], 122 | 'text' => ['text', null], 123 | 'color' => ['varchar', 20], 124 | 'type' => ['varchar', 20], 125 | 'time' => ['varchar', 20] 126 | ])) { 127 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]数据库配置出现错误,请联系管理员进行检查']); 128 | } 129 | } 130 | if (Request::get('text', 'POST') !== null) { 131 | server::post(); 132 | } else { 133 | server::get(); 134 | } 135 | } 136 | 137 | /** 138 | * 获取本站弹幕 139 | * @return json 140 | */ 141 | public static function get() 142 | { 143 | Config::options(); 144 | //判断请求接口是否开放 145 | $apiVersion = Request::version(); 146 | if (($apiVersion == 'v2') ? ( 147 | (PORT_VERSION_DUB == true) ? false : true 148 | ) : ( 149 | (PORT_VERSION_TRD == true) ? false : true 150 | )) { 151 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]请求的接口版本已被禁用']); 152 | } 153 | 154 | //获取参数 155 | $id = Request::get('id'); 156 | $verify = Request::validator(['id' => $id], [ 157 | 'id' => 'id|between 1,50', 158 | ]); 159 | if ($verify['code'] == -1) { 160 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 161 | } 162 | 163 | //查询弹幕池 164 | $result = Db::get('pool', ['vid' => $id], false); 165 | if ($result == false) { 166 | $result = [['time' => 0, 'type' => 0, 'color' => '0', 'user' => '0', 'text' => '']]; 167 | } 168 | 169 | foreach ($result as $data) { 170 | $danmaku[] = [(float)$data['time'], (int)$data['type'], $data['color'], $data['user'], $data['text']]; 171 | } 172 | 173 | 174 | //响应接口请求:返回弹幕JSON 175 | $result = ($apiVersion == 'v2') ? 176 | [ 177 | 'code' => 0, 178 | 'version' => 2, 179 | 'danmaku' => $danmaku, 180 | 'msg' => '[Danmaku]本站弹幕加载完成' 181 | ] : 182 | [ 183 | 'code' => 0, 184 | 'version' => 3, 185 | 'data' => $danmaku, 186 | 'msg' => '[Danmaku]本站弹幕加载完成' 187 | ]; 188 | return Respons::json($result); 189 | } 190 | 191 | 192 | /** 193 | * 提交弹幕 194 | */ 195 | public static function post() 196 | { 197 | Config::options(); 198 | 199 | $postData = Request::all('POST'); 200 | //校验参数 201 | $verify = Request::validator($postData, [ 202 | 'author' => 'id|between 1,50', 203 | 'color' => 'between 1,20', 204 | 'id' => 'id|between 1,50', 205 | 'text' => 'between 1,30', 206 | 'time' => 'float|between 1,20', 207 | 'token' => 'token', 208 | 'type' => 'number|between 1,20' 209 | ]); 210 | if ($verify['code'] == -1) { 211 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 212 | } 213 | 214 | //token校验: 215 | if (TOKEN_VERIFY && strtoupper($postData['token']) != strtoupper(md5($postData['author'] . $postData['id']))) { 216 | return Respons::json(['code' => 506, 'msg' => 'TOKEN不正确']); 217 | } 218 | 219 | //向弹幕池插入数据 220 | return (Db::insertTable('pool', [ 221 | 'user' => $postData['author'], 222 | 'vid' => $postData['id'], 223 | 'text' => $postData['text'], 224 | 'color' => $postData['color'], 225 | 'type' => $postData['type'], 226 | 'time' => $postData['time'] 227 | ])) ? (Respons::json([ 228 | 'code' => 0, 229 | 'msg' => '弹幕发送成功~' 230 | ])) : (Respons::json([ 231 | 'code' => 500, 232 | 'msg' => '发送弹幕出现了错误' 233 | ])); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dplayer-弹幕接口测试Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

演示视频

19 |
20 | 21 | 上传者 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 视频简介 30 |

31 | 这是一条视频简介 32 |

33 |

34 |
35 |
36 | 37 |
38 |
评论区
39 | 40 | 41 | 44 |
45 |
46 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /libs/bilibili.php: -------------------------------------------------------------------------------- 1 | 'bv', 33 | 'p' => 'number|between 0,10' 34 | ]); 35 | if ($verify['code'] == -1) { 36 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 37 | } 38 | isset($data['type']) || $data['type'] = 'video'; 39 | isset($data['ep']) || $data['ep'] = null; 40 | isset($data['bv']) || $data['bv'] = null; 41 | isset($data['p']) || $data['p'] = 1; 42 | isset($data['q']) || $data['q'] = 80; 43 | isset($data['format']) || $data['format'] = 'mp4'; 44 | list($type, $ep, $bv, $p, $q, $format) = [$data['type'], $data['ep'], $data['bv'], $data['p'], $data['q'], $data['format']]; 45 | $bp = new Bilibili($type); 46 | $bp->epid($ep); 47 | $bp->bvid($bv)->page($p); 48 | $bp->quality($q)->format($format); 49 | $result = json_decode($bp->result(), true); 50 | return Respons::json($result, true); 51 | } 52 | 53 | /** 54 | * B站视频ID链解析 55 | */ 56 | public static function chain() 57 | { 58 | //定义变量并校验 59 | list($bv, $aid, $page) = [Request::get('bv'), Request::get('aid'), Request::get('p')]; 60 | ($page == null) && $page = 1; 61 | $verify = ($aid != null) ? 62 | Request::validator(['aid' => $aid, 'page' => $page], [ 63 | 'aid' => 'number|between 1,15', 64 | 'page' => 'number|between 1,10' 65 | ]) : 66 | Request::validator(['bv' => $bv, 'page' => $page], [ 67 | 'bv' => 'bv', 68 | 'page' => 'number|between 1,10', 69 | ]); 70 | if ($verify['code'] == -1) { 71 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 72 | } 73 | 74 | //获取视频整个查询链信息 75 | $url = 'https://api.bilibili.com/x/web-interface/view/detail?' . ((empty($bv)) ? 'aid=' . $aid : 'bvid=' . $bv); 76 | $content = json_decode(Request::curlGet($url, true), true); 77 | if ($content['code'] !== 0) { 78 | return Respons::json(['code' => 404, 'msg' => '获取视频信息失败']); 79 | } 80 | 81 | return Respons::json(['code' => 0, 'data' => [ 82 | 'bv' => $content['data']['View']['bvid'], 83 | 'aid' => $content['data']['View']['aid'], 84 | 'cid' => $content['data']['View']['pages'][$page - 1]['cid'], 85 | 'p' => $page 86 | ]], true); 87 | } 88 | 89 | /** 90 | * B站视频基本信息获取 91 | */ 92 | public static function detail() 93 | { 94 | $bv = Request::get('bv'); 95 | $p = Request::get('p'); 96 | isset($p) || $p = 1; 97 | $verify = Request::validator(['bv' => $bv, 'p' => $p], [ 98 | 'bv' => 'bv', 99 | 'p' => 'number|between 0,10' 100 | ]); 101 | if ($verify['code'] == -1) { 102 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 103 | } 104 | 105 | $content = json_decode(Request::curlGet('https://api.bilibili.com/x/web-interface/view/detail?bvid=' . $bv, true), true); 106 | if ($content['code'] !== 0) { 107 | return Respons::json(['code' => 404, 'msg' => '获取视频信息失败']); 108 | } 109 | 110 | return Respons::json(['code' => 0, 'data' => [ 111 | 'cid' => $content['data']['View']['cid'], 112 | 'page' => $content['data']['View']['pages'][$p - 1]['page'], 113 | 'title' => $content['data']['View']['title'], 114 | 'part' => $content['data']['View']['pages'][$p - 1]['part'], 115 | 'desc' => $content['data']['View']['desc'], 116 | 'author' => $content['data']['Card']['card']['name'], 117 | 'mid' => (int)$content['data']['Card']['card']['mid'], 118 | 'face' => $content['data']['Card']['card']['face'], 119 | 'time' => $content['data']['View']['ctime'], 120 | 'pic' => $content['data']['View']['pages'][$p - 1]['first_frame'] 121 | ]], true); 122 | } 123 | 124 | /** 125 | * B站视频TAG获取 126 | */ 127 | public static function tag() 128 | { 129 | $aid = Request::get('aid'); 130 | $verify = Request::validator(['aid' => $aid], [ 131 | 'aid' => 'number|between 1,15', 132 | ]); 133 | if ($verify['code'] == -1) { 134 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 135 | } 136 | 137 | $content = json_decode(Request::curlGet('https://api.bilibili.com/x/web-interface/view/detail/tag?aid=' . $aid, true), true); 138 | if ($content['code'] !== 0) { 139 | return Respons::json(['code' => 404, 'msg' => '获取视频信息失败']); 140 | } 141 | 142 | foreach ($content['data'] as $tag) { 143 | $tags[] = ['id' => $tag['tag_id'], 'name' => $tag['tag_name']]; 144 | } 145 | 146 | return Respons::json(['code' => 0, 'data' => $tags], true); 147 | } 148 | 149 | /** 150 | * 获取视频评论 151 | */ 152 | public static function reply() 153 | { 154 | $aid = Request::get('aid'); 155 | $page = Request::get('page'); 156 | isset($page) || $page = 1; 157 | $verify = Request::validator(['aid' => $aid, 'page' => $page], [ 158 | 'aid' => 'number|between 1,15', 159 | 'page' => 'number|between 1,10' 160 | ]); 161 | if ($verify['code'] == -1) { 162 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 163 | } 164 | 165 | $url = 'https://api.bilibili.com/x/v2/reply/main?oid=' . $aid . '&type=1&next=' . $page; 166 | $content = json_decode(Request::curlGet($url, true), true); 167 | if ($content['code'] !== 0) { 168 | return Respons::json(['code' => 404, 'msg' => '获取视频信息失败']); 169 | } 170 | 171 | foreach ($content['data']['replies'] as $reply) { 172 | if (is_array($reply['replies'])) { 173 | foreach ($reply['replies'] as $comment) { 174 | $comments[] = [ 175 | 'name' => $comment['member']['uname'], 176 | 'sex' => $comment['member']['sex'], 177 | 'sign' => $comment['member']['sign'], 178 | 'avatar' => $comment['member']['avatar'], 179 | 'content' => $comment['content']['message'], 180 | 'time' => $comment['ctime'], 181 | 'like' => $comment['like'], 182 | ]; 183 | } 184 | } 185 | empty($comments) && $comments = []; 186 | $replies[] = [ 187 | 'mid' => $reply['mid'], 188 | 'name' => $reply['member']['uname'], 189 | 'sex' => $reply['member']['sex'], 190 | 'sign' => $reply['member']['sign'], 191 | 'avatar' => $reply['member']['avatar'], 192 | 'level' => $reply['member']['level_info']['current_level'], 193 | 'content' => $reply['content']['message'], 194 | 'time' => $reply['ctime'], 195 | 'like' => $reply['like'], 196 | 'comment' => $comments 197 | ]; 198 | $comments = []; 199 | } 200 | return Respons::json(['code' => 0, 'data' => $replies]); 201 | } 202 | 203 | /** 204 | * B站用户信息获取 205 | */ 206 | public static function user() 207 | { 208 | $mid = Request::get('mid'); 209 | $verify = Request::validator(['mid' => $mid], [ 210 | 'mid' => 'number|between 1,15', 211 | ]); 212 | if ($verify['code'] == -1) { 213 | return Respons::json(['code' => 502, 'msg' => join(';', $verify['msg'])]); 214 | } 215 | 216 | $b_page = Request::curlGet('https://space.bilibili.com/' . $mid , true); 217 | preg_match('/(?<=window.__INITIAL_STATE__=).*?(?=;\(function\(\)\{)/', $b_page, $str); 218 | $allInfo = json_decode($str[0], true); 219 | $array = $allInfo['space']['info']; 220 | if (empty($array['mid'])) { 221 | return Respons::json(['code' => 404, 'msg' => '未找到该用户']); 222 | } 223 | 224 | $fansCard = ($array['fans_medal']['wear']) ? 225 | [ 226 | 'name' => $array['fans_medal']['medal']['medal_name'], 227 | 'level' => $array['fans_medal']['medal']['level'], 228 | 'mid' => $array['fans_medal']['medal']['target_id'] 229 | ] : [ 230 | 'name' => null, 231 | 'level' => null, 232 | 'target' => null 233 | ]; 234 | 235 | return Respons::json(['code' => 0, 'data' => [ 236 | 'mid' => $array['mid'], 237 | 'name' => $array['name'], 238 | 'sex' => $array['sex'], 239 | 'face' => $array['face'], 240 | 'level' => $array['level'], 241 | 'banner' => $array['top_photo'], 242 | 'fans_card' => $fansCard 243 | ]], true); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Danmaku PHP API - 一个功能丰富的Dplayer.JS配套的视频站解析API 2 | 3 | ![](https://mdzz.pro/usr/uploads/2022/06/1211219036.jpg) 4 | 5 | ### 前言: 6 | 7 | ##### 本API是一个弹幕服务器API, 为DPlayer提供弹幕服务器支持。还包含丰富的扩展插件,可以提供额外的解析服务(目前支持Bilibili)。该API同时支持DPlayer V2 V3接口,自由度较高。 8 | 9 | **以下是本API已经实装的实例(下面介绍的其他API也可以照格式使用,不过就不一一赘述):** 10 | 11 | > **整个API实际应用的Demo:** https://api.mdzz.pro/demo/ 12 | > 13 | > **B站视频解析Demo:** https://api.mdzz.pro/bilibili/parse/video?bv=BV1Na411r7tN&p=1 14 | > 15 | > **B站弹幕解析Demo:** https://api.mdzz.pro/danmaku/bilibili/get/v3/?bv=BV1Na411r7tN 16 | > 17 | > **本站弹幕Demo:** https://api.mdzz.pro/danmaku/server/entrance/v3/?id=BV1Na411r7tN 18 | > 19 | > **我的个人站:** https://mdzz.pro/ 20 | 21 | ### 一、安装 22 | 23 | **本接口为即装即用式,将内容放置于服务器网站根目录下,绑定接口域名后,配置根目录下config.php即可使用。不过您需要对本API设置伪静态规则才能正常访问。** 24 | 25 | **1) 修改config.php配置** 26 | 27 | > 配置字段列表 define("配置名", "配置的值"): 28 | 29 | | 配置名 | 配置可能值 | 默认 | 作用 | 30 | | ---------------- | ------------------------------------- | --------- | ------------------------------------------------------------ | 31 | | DB_SERVER_NAME | 您的数据库地址。如为本机请填localhost | localhost | 数据库链接时应用的数据库地址 | 32 | | DB_USER_NAME | 您的数据库用户名 | root | 数据库链接时应用的数据库用户名 | 33 | | DB_PASSWORD | 您的数据库用户密码 | 123456 | 数据库链接时应用的数据库密码 | 34 | | DB_NAME | 您的数据库名 | danmaku | 数据库链接时应用的数据库名 | 35 | | DB_PORT | 您的数据库端口 | 3306 | 数据库链接时应用的数据库端口 | 36 | | ROOT | 网站域名 | Danmaku | 在将本API作为网站子文件夹使用时是一个有效的筛除多余路径的设置。它强制定义了本API的根目录名称 | 37 | | TABLE_PRE_NAME | /^[a-zA-Z0-9]+$/ | danmaku | 用于定义API所创建表的前缀,防止与其他项目共用同一个库时,数据表混淆 | 38 | | PORT_VERSION_TRD | boolean | true | 用于控制API接口版本开放(针对DPlayer)。不同版本的接口返回内容不一样 | 39 | | PORT_VERSION_DUB | boolean | true | 同上 | 40 | | TOKEN_VERIFY | boolean | true | 设置在用户向API发送弹幕时是否需要正确的token,如果不需要验证token,可设置关闭 | 41 | | API_CACHE_TIME | number | 600 | 用于控制缓存时间。部分允许缓存结果的缓存会生成缓存以节省服务器开支 | 42 | | API_HTTPS | boolean | false | 是否将接口返回的内容中URL全部转换为HTTPS | 43 | 44 | 45 | 46 | **2) 设置伪静态** 47 | 48 | 您需要在API根目录新建一个.htaccess 文件,然后写入以下对应的内容 49 | 50 | > Nginx设置伪静态 51 | 52 | ```php 53 | if (!-e $request_filename) { 54 | rewrite ^(.*)$ /index.php last; 55 | } 56 | //如果您使用面板,可以直接在面板的网站管理内伪静态选项写入这个 57 | 58 | ``` 59 | 60 | > Apache设置伪静态 61 | 62 | ```shell 63 | Options -MultiViews 64 | RewriteEngine On 65 | RewriteCond %{REQUEST_FILENAME} !-f 66 | RewriteRule ^(.*)$ /index.php [QSA,L] 67 | ``` 68 | 69 | 配置好后您应该就可以使用本API了。DEMO文件夹下有demo演示,但需要您根据注释修改一点内容才可以尝试demo。 70 | 71 | 72 | 73 | ### 二、API介绍 74 | 75 | **由于第三方API是通过域名面向服务对象的,所以以下接口域名均以domain.com示例。实际使用请将domain.com替换为您绑定的的域名** 76 | 77 | 78 | 79 | #### 1、本站弹幕API 80 | 81 | **由于DPlayer的设计,会自动在接口上增加/V3,该API将提供存储在本站的弹幕。如果您要请求V3版本弹幕,请在接口后加上V3, 请求V2弹幕同理** 82 | 83 | > http://domain.com/danmaku/server/entrance/ 84 | 85 | ##### 1) 允许请求类型 86 | 87 | **POST GET** 88 | 89 | ##### 2) 请求内容 90 | 91 | ###### GET:获取本站弹幕 92 | 93 | | 参数 | 请求允许值 | 94 | | ---- | ------------------------------------------------------------ | 95 | | id | 您在程序设计时所使用的视频ID。以Bilibili为例,您用BV作为id的话就会获取该BV所记录的弹幕。除此之外还可以用aid,cid,自己设计的id等 | 96 | 97 | ###### POST:向本站服务器发送一条弹幕 98 | 99 | | 参数 | 请求允许值 | 备注 | 100 | | ------ | --------------------- | -------------------------------------------------------- | 101 | | author | /^[a-zA-Z0-9]{1,50}$/ | 发送弹幕的用户ID | 102 | | color | /^[a-zA-Z0-9]{1,50}$/ | 发送弹幕的颜色。16进制色。 | 103 | | id | /^[a-zA-Z0-9]{1,50}$/ | 视频ID。您可以根据自己需要设置id。同GET的id,用于识别视频 | 104 | | text | string | 发送的弹幕内容 | 105 | | time | float | 弹幕处于视频的时间。浮点数 | 106 | | token | MD5(32bit) | TOKEN用于验证用户身份。默认为32位MD5码,逻辑为author+id | 107 | | type | number | 弹幕类型,控制弹幕是滚动还是悬停消失 | 108 | 109 | ##### 3) 响应样本 110 | 111 | ```json 112 | //GET返回弹幕(v2): 113 | { 114 | "code": 0, //状态码 115 | "version": 2, //版本号 116 | "danmaku": [ //弹幕内容 117 | [1.981745, 0, "16777215", "e29a22", "测试弹幕"], 118 | [4.155057, 0, "16777215", "deea32", "测试"] 119 | // 出现时间 类型 颜色 用户ID 弹幕内容 120 | ], 121 | "msg": "[Danmaku]本站弹幕加载完成" //消息内容 122 | } 123 | 124 | //GET返回弹幕(V3) 125 | { 126 | "code": 0, 127 | "version": 3, 128 | "data": [ 129 | [1.981745, 0, "16777215", "e29a22", "测试弹幕"], 130 | [4.155057, 0, "16777215", "deea32", "测试"] 131 | ], 132 | "msg": "[Danmaku]本站弹幕加载完成" 133 | } 134 | 135 | //POST返回发送弹幕的执行结果 136 | { 137 | "code": 0, 138 | "msg": "弹幕发送成功~" 139 | } 140 | 141 | //所有接口成功状态码均为0,如果失败会是别的参数。故代码判断返回结果中code即可判断执行结果 142 | ``` 143 | 144 | 145 | 146 | ##### 4) 调用代码示例(以DPlayer为例。如果是自己写的播放器需要自行写代码) 147 | 148 | ```javascript 149 | let domain = 'htts://mydomain.com'; 150 | let map = {bv:'BVtesttest00',user:'123333',url:'https://xxxxx.com/xxx.mp4'}; 151 | let dp = new DPlayer({ 152 | element: document.getElementById('dplayer'), 153 | loop: true, //自动循环播放 154 | lang: 'zh-cn', //播放器语言 155 | hotkey: true, //开启热键控制 156 | autoplay: true, //自动播放 157 | volume: 1, //音量大小 158 | playbackSpeed: '[0.5,0.75,1,1.25,1.5,2]', //播放速度调节 159 | preload: 'auto', //预加载方式 160 | video: { 161 | url: map['url'], //视频链接 162 | }, 163 | danmaku: { 164 | id: map['bv'], //视频对应的弹幕ID,这里我默认是BV 165 | api: domain + '/danmaku/server/entrance/', 166 | token: md5(map['user'] + map['bv']), //TOKEN 167 | unlimited: true, //弹幕数量限制 168 | user: map['user'], //用户身份信息 169 | addition: [domain + '/danmaku/bilibili/get/v3/?bv=' + map['bv']] //外挂弹幕 170 | } 171 | }); 172 | ``` 173 | 174 | 175 | 176 | #### 2、B站弹幕API 177 | 178 | ##### 该API用于向B站拉取弹幕。由于B站改版,老版本json接口已经取消,所以该接口会自动获取xml格式弹幕并解析。该代码响应结果与 *1、本站弹幕API* 一致 179 | 180 | > http://domain.com/danmaku/bilibili/get/ 181 | 182 | ##### 1) 允许请求类型 183 | 184 | **GET** 185 | 186 | ##### 2) 请求内容 187 | 188 | ###### GET:获取B站视频弹幕 189 | 190 | | 参数 | 请求允许值 | 备注 | 191 | | ------ | --------------------------------------------- | --------------------------------------------- | 192 | | bv | BV号 | B站BV号,例如BV23z2i1oPc2 | 193 | 194 | 195 | 196 | #### 3、B站视频解析API 197 | 198 | ##### 该API仅可以解析UP主上传的视频地址。其核心来自于 https://github.com/injahow/bilibili-parse 199 | 200 | > http://domain.com/bilibili/parse/video 201 | 202 | ##### 1) 允许请求类型 203 | 204 | **GET** 205 | 206 | ##### 2) 请求内容 207 | 208 | ###### GET:获取B站视频链接 209 | 210 | | 参数 | 请求允许值 | 默认 | 备注 | 211 | | ------ | ---------- | ---- | ---- | 212 | | bv | BV号 | - | B站BV号,例如BV23z2i1oPc2 | 213 | | p | >=1 | 1 | 视频分P | 214 | | q | 16/32/64/80 | 80 | 视频清晰度 | 215 | | type | video/bangumi | video | 视频类型 | 216 | | format | flv/dash/mp4 | mp4 | 视频格式 | 217 | 218 | ##### 3) 响应样本 219 | 220 | ```json 221 | { 222 | "code": 0, //状态码 223 | "quality": 208, //视频画质 224 | "accept_quality": [208], //视频允许的画质 225 | "url": "https://cn-hljheb-dx-v-04.bilivideo.com/upgcxcode/71/48/469894871/469894871-1-208.mp4?e=ig8euxZM2rNcNbhBhwdVhwdlhzUVhwdVhoNvNC8BqJIzNbfqXBvEuENvNC8aNEVEtEvE9IMvXBvE2ENvNCImNEVEIj0Y2J_aug859r1qXg8gNEVE5XREto8z5JZC2X2gkX5L5F1eTX1jkXlsTXHeux_f2o859IB_&ua=tvproj&deadline=1655056415&gen=playurl&nbs=1&oi=2095000411&os=bcache&trid=0000bf6a30fd45e3445b907202783d6b7851&uipk=5&upsig=e4e6775f93bf43310b633d18a97404ff&uparams=e,ua,deadline,gen,nbs,oi,os,trid,uipk&mid=0" //视频地址 226 | } 227 | 228 | ``` 229 | 230 | #### 4、B站视频ID关系链API 231 | 232 | ##### 该API主要用于解析出视频的各种ID,以方便后面的接口内容查询 233 | 234 | > http://domain.com/bilibili/parse/chain 235 | 236 | ##### 1) 允许请求类型 237 | 238 | **GET** 239 | 240 | ##### 2) 请求内容 241 | 242 | ###### GET:获取B站视频ID关系链 243 | 244 | | 参数 | 请求允许值 | 默认 | 备注 | 245 | | ---- | ---------- | ---- | --------------------------------------- | 246 | | bv | BV号 | - | B站BV号,例如BV23z2i1oPc2。与aid二选其一 | 247 | | aid | 视频aid | - | 视频aid。与BV二选其一 | 248 | | p | >=1 | 1 | 视频分P | 249 | 250 | ##### 3) 响应样本 251 | 252 | ```json 253 | { 254 | "code": 0, //状态码 255 | "data": { //数据内容 256 | "bv": "BV1Na411r7tN", //视频BV号 257 | "aid": 210182364, //视频aid 258 | "cid": 469894871, //视频cid 259 | "p": "1" //分p 260 | } 261 | } 262 | 263 | 264 | ``` 265 | 266 | #### 5、B站视频详情API 267 | 268 | **该API主要用于解析视频详情** 269 | 270 | > http://domain.com/bilibili/parse/detail 271 | 272 | ##### 1) 允许请求类型 273 | 274 | **GET** 275 | 276 | ##### 2) 请求内容 277 | 278 | ###### GET:获取B站视频ID关系链 279 | 280 | | 参数 | 请求允许值 | 默认 | 备注 | 281 | | ---- | ---------- | ---- | -------------------------- | 282 | | bv | BV号 | - | B站BV号,例如BV23z2i1oPc2。 | 283 | | p | >=1 | 1 | 视频分P | 284 | 285 | ##### 3) 响应样本 286 | 287 | ```json 288 | { 289 | "code": 0, //状态码 290 | "data": { //数据内容 291 | "cid": 469894871, //视频CID 292 | "page": 1, //视频分p 293 | "title": "【VRChat】一人一只不要抢 !!!", //视频合集名称 294 | "part": "【VRChat】一人一只不要抢", //视频名称(如果视频合集只有1集,则视频名称与合集相同) 295 | "desc": "游戏名:VRChat\nbgm:大喜\n模型:mia\n世界:MMD Dance World\n非常感谢@桜夜幽梦Channel @鱼香ruosi 以及其他两位朋友的参加\n希望各位客官能喜欢!", //视频描述 296 | "author": "未文无心", //视频作者 297 | "mid": 148830743, //视频作者id 298 | "face": "https://i2.hdslb.com/bfs/face/daaa54417c750c9f04846965f13371e54679471d.jpg", //视频作者头像 299 | "time": 1640609730, //视频上传时间 300 | "pic": "https://i1.hdslb.com/bfs/storyff/n211227qn2x3fivm0ghhd02mg4jqnvup_firsti.jpg" //视频头图 301 | } 302 | } 303 | ``` 304 | 305 | #### 6、B站视频TAG获取API 306 | 307 | **该API主要解析B站视频TAG内容** 308 | 309 | > http://domain.com/bilibili/parse/tag 310 | 311 | ##### 1) 允许请求类型 312 | 313 | **GET** 314 | 315 | ##### 2) 请求内容 316 | 317 | ###### GET:获取B站视频ID关系链 318 | 319 | | 参数 | 请求允许值 | 备注 | 320 | | ---- | ---------- | ------- | 321 | | aid | 视频aid | 视频aid | 322 | 323 | ##### 3) 响应样本 324 | 325 | ```json 326 | { 327 | "code": 0, //状态码 328 | "data": [{ //返回的数据。这里的data是一个数组.里面包含了很多有id和name的tag数组 329 | "id": 5007682, 330 | "name": "VRChat" 331 | }] 332 | } 333 | ``` 334 | 335 | #### 7、B站视频评论解析API 336 | 337 | **该API主要用于获取视频评论** 338 | 339 | > http://domain.com/bilibili/parse/reply 340 | 341 | ##### 1) 允许请求类型 342 | 343 | **GET** 344 | 345 | ##### 2) 请求内容 346 | 347 | ###### GET:获取B站视频ID关系链 348 | 349 | | 参数 | 请求允许值 | 备注 | 350 | | ---- | ---------- | ---------- | 351 | | aid | 视频aid | 视频aid | 352 | | page | number | 评论区页码 | 353 | 354 | ##### 3) 响应样本 355 | 356 | ```json 357 | { 358 | "code": 0, //状态码 359 | "data": [{ //这里返回的是一个个评论数组 360 | "mid": 190206497, //评论人ID 361 | "name": "地jo人", //评论人昵称 362 | "sex": "男", //评论人性别 363 | "sign": "嘤", //评论人签名 364 | "avatar": "https://i2.hdslb.com/bfs/face/665043557f9234e2e17ef5688df1ce1153e9147d.jpg", //评论人头像 365 | "level": 5, //评论人等级 366 | "content": "1:27[doge]", //评论内容 367 | "time": 1640760764, //评论时间 368 | "like": 1209, //点赞数 369 | "comment": [{ //回复该条评论的数组。以下数组内容与评论内容一致 370 | "name": "qweuir", 371 | "sex": "男", 372 | "sign": "mc", 373 | "avatar": "https://i1.hdslb.com/bfs/face/d963af73c334cb8968dd44d588e44cb4393b5ba3.jpg", 374 | "content": "我的头像说明了我是什么人[tv_doge]", 375 | "time": 1640775787, 376 | "like": 21 377 | }, { 378 | "name": "城云_", 379 | "sex": "男", 380 | "sign": "诶嘿,诶嘿嘿嘿嘿(", 381 | "avatar": "https://i0.hdslb.com/bfs/face/7517480dd4cf4b73adb3e8463091236a8c592111.jpg", 382 | "content": "回复 @bili_1637658766 :我觉得我的也能证明[doge]", 383 | "time": 1640965784, 384 | "like": 3 385 | }, { 386 | "name": "银姜", 387 | "sex": "女", 388 | "sign": "ヾ(*´∀`*)ノ", 389 | "avatar": "https://i2.hdslb.com/bfs/face/066b43b60f47f682e44b8c8a9081961d402cd466.jpg", 390 | "content": "up主觉得很涩[doge]", 391 | "time": 1642324392, 392 | "like": 3 393 | }] 394 | }] 395 | } 396 | ``` 397 | 398 | #### 8、B站用户公开信息获取API 399 | 400 | **该API用于获取B站对应mid用户的公开信息。请注意调动次数,频繁调动会被暂时封禁** 401 | 402 | > http://domain.com/bilibili/parse/user 403 | 404 | ##### 1) 允许请求类型 405 | 406 | **GET** 407 | 408 | ##### 2) 请求内容 409 | 410 | ###### GET:获取B站视频ID关系链 411 | 412 | | 参数 | 请求允许值 | 备注 | 413 | | ---- | ---------- | ---------------------- | 414 | | mid | 用户mid | 用户的mid,该id是唯一的 | 415 | 416 | ##### 3) 响应样本 417 | 418 | ```json 419 | { 420 | "code": 0, //状态码 421 | "data": { //返回数据 422 | "mid": 104092258, //用户mid 423 | "name": "绘理绘气星川真紀", //用户昵称 424 | "sex": "保密", //用户性别 425 | "face": "https://i1.hdslb.com/bfs/face/3330947c5e377abea9040560206e92fb94732f62.jpg", //用户头像 426 | "level": 4, //用户等级 427 | "banner": "https://i0.hdslb.com/bfs/space/cb1c3ef50e22b6096fde67febe863494caefebad.png", //用户banner 428 | "fans_card": { //用户粉丝牌 429 | "name": "绘理理", //粉丝牌名字 430 | "level": 24, //粉丝牌等级 431 | "mid": 278296 //粉丝牌所属主播id 432 | } 433 | } 434 | } 435 | ``` 436 | 437 | 438 | 439 | ### 三、API扩展 440 | 441 | **本API支持扩展功能和移除功能,工作原理非常简单,就是将对应的php类文件放进对应的文件夹。** 442 | 443 | #### 1、libs文件夹 444 | 445 | **该文件夹用于存放接口插件。例如我有一个名为`apidemo.php`的文件(注意:插件命名空间必须为Danmaku)** 446 | 447 | ```php 448 | // apidemo.php 449 | http://domain.com/apidemo/test/abc 463 | 464 | 即可访问到接口内容 465 | 466 | 467 | 468 | #### 2、src文件夹 469 | 470 | **该文件夹用于存放接口文件所引用的外部类,需要有不同的命名空间。外部类需要一层基本的命名空间为名字的文件夹,以及以类名为名字的php文件** 471 | 472 | **例如我有一个名为demophp的文件夹,里面有一个demosrc.php的文件。** 473 | 474 | ```php 475 | // Demophp/demosrc.php 476 | 501, 'msg' => '[Danmaku]缺失接口关键路径']); 40 | } 41 | if (!file_exists(__DIR__ . '/libs/' . $path[0] . '.php')) { 42 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]未找到接口请求对应的插件']); 43 | } 44 | require_once __DIR__ . '/libs/' . $path[0] . '.php'; 45 | if (!class_exists('Danmaku\\' . $path[1]) || !method_exists('Danmaku\\' . $path[1], $path[2])) { 46 | return Respons::json(['code' => 404, 'msg' => '[Danmaku]未找到接口所指定的操作']); 47 | } 48 | call_user_func('Danmaku\\' . $path[1] . '::' . $path[2]); 49 | } 50 | } 51 | 52 | /** 53 | * 请求类 54 | */ 55 | class Request 56 | { 57 | 58 | /** 59 | * 获取URL指向路径 60 | * @return string 61 | */ 62 | public static function path() 63 | { 64 | $request = $_SERVER['REQUEST_URI']; 65 | if (strstr($request, '?') !== false) { 66 | $request = substr($request, 0, strrpos($request, '?')); 67 | } 68 | return $request; 69 | } 70 | 71 | /** 72 | * 获取请求内容 73 | * @param string $name 需获取请求名 74 | * @param string $method 需获取请求类型,不填则优先输出有值的请求(默认为GET,可选GET/POST) 75 | * @return mixed; 76 | */ 77 | public static function get($name, $method = null) 78 | { 79 | $request = $_SERVER['REQUEST_URI']; 80 | $request = explode('&', substr($request, strripos($request, '?') + 1)); 81 | foreach ($request as $row) { 82 | preg_match('/^.*(?=\=)/', $row, $reqName); 83 | preg_match('/(?<=\=).*$/', $row, $reqValue); 84 | isset($reqName[0]) || $reqName[0] = null; 85 | isset($reqValue[0]) || $reqValue[0] = null; 86 | $reqs[$reqName[0]] = $reqValue[0]; 87 | } 88 | $req = json_decode(file_get_contents('php://input'), true)[$name]; 89 | isset($reqs[$name]) || $reqs[$name] = null; 90 | return ($method == null) ? ( 91 | ($reqs[$name] !== null) ? ($reqs[$name]) : ($req) 92 | ) : ( 93 | (strtoupper($method) == 'POST') ? ($req) : ($reqs[$name]) 94 | ); 95 | } 96 | 97 | /** 98 | * 获取所有请求 99 | * @param string $method 需请求类型(默认为AUTO,可选POST/GET/AUTO. AUTO即自动叠加GET与POST请求. 如果GET内已定义,POST的内容将不会被应用) 100 | * @return array 101 | */ 102 | public static function all($method = 'AUTO') 103 | { 104 | switch (strtoupper($method)) { 105 | case 'GET': 106 | $request = $_SERVER['REQUEST_URI']; 107 | $request = explode('&', substr($request, strripos($request, '?') + 1)); 108 | foreach ($request as $row) { 109 | preg_match('/^.*(?=\=)/', $row, $reqName); 110 | preg_match('/(?<=\=).*$/', $row, $reqValue); 111 | isset($reqName[0]) || $reqName[0] = null; 112 | isset($reqValue[0]) || $reqValue[0] = null; 113 | $reqs[$reqName[0]] = $reqValue[0]; 114 | } 115 | return $reqs; 116 | break; 117 | case 'POST': 118 | return json_decode(file_get_contents('php://input'), true); 119 | break; 120 | default: 121 | $request = $_SERVER['REQUEST_URI']; 122 | $request = explode('&', substr($request, strripos($request, '?') + 1)); 123 | $posted = json_decode(file_get_contents('php://input'), true); 124 | foreach ((array)$request as $row) { 125 | preg_match('/^.*(?=\=)/', $row, $reqName); 126 | preg_match('/(?<=\=).*$/', $row, $reqValue); 127 | isset($reqName[0]) || $reqName[0] = null; 128 | isset($reqValue[0]) || $reqValue[0] = null; 129 | if ($reqName[0] !== null && $reqValue[0] !== null) { 130 | isset($posted[$reqName[0]]) || $posted[$reqName[0]] = NULL; 131 | $reqs[$reqName[0]] = ($reqValue[0] != NULL) ? ($reqValue[0]) : ($posted[$reqName[0]]); 132 | } else { 133 | $reqs = null; 134 | } 135 | } 136 | foreach ((array)$posted as $key => $row) { 137 | isset($reqs[$key]) || $reqs[$key] = NULL; 138 | $reqs[$key] = ($reqs[$key] == NULL) ? $row : $reqs[$key]; 139 | } 140 | return $reqs; 141 | break; 142 | } 143 | } 144 | 145 | 146 | /** 147 | * 获取请求接口版本 148 | * @return string 149 | */ 150 | public static function version() 151 | { 152 | Config::options(); 153 | $request = Request::path(); 154 | $request = ltrim(str_replace('/' . ROOT, '', $request), '/'); 155 | if ($request != '') { 156 | $path = explode('/', $request); 157 | isset($path[3]) || $path[3] = null; 158 | return ($path[3] != null) ? strtolower($path[3]) : 'v3'; 159 | } else { 160 | return 'v3'; 161 | } 162 | } 163 | 164 | 165 | /** 166 | * CURL获取网页内容 167 | * @param string $link 网页链接 168 | * @param boolean $zip 是否启用GZIP压缩解析 169 | * @return mixed 170 | */ 171 | public static function curlGet($url, $zip = false) 172 | { 173 | $ch = curl_init(); 174 | curl_setopt($ch, CURLOPT_URL, trim($url)); 175 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 176 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 177 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 178 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5000); 179 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4')); 180 | curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, -1); 181 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 182 | if ($zip) { 183 | curl_setopt($ch, CURLOPT_ENCODING, "gzip,deflate"); 184 | } 185 | $return = curl_exec($ch); 186 | curl_close($ch); 187 | return $return; 188 | } 189 | 190 | 191 | /** 192 | * 参数校验 193 | * @param array $param 参数 194 | * @param array $verifier 校验条件 195 | * @return array 196 | */ 197 | public static function validator($param, $verifier) 198 | { 199 | $return = ['code' => 0, 'msg' => []]; 200 | foreach ($verifier as $key => $rules) { 201 | $rules = $verifier[$key]; 202 | $rules = explode('|', $rules); 203 | foreach ($rules as $rule) { 204 | $rule = strtolower($rule); 205 | switch ($rule) { 206 | case 'email': 207 | $Regx = '/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/'; 208 | $need = '邮箱'; 209 | break; 210 | case 'mobile': 211 | $Regx = '/^1[34578]\d{9}$/'; 212 | $need = '电话号码'; 213 | break; 214 | case 'en': 215 | $Regx = '/^[a-zA-Z\s]+$/'; 216 | $need = '纯字母'; 217 | break; 218 | case 'number': 219 | $Regx = '/^[0-9]*$/'; 220 | $need = '纯数字'; 221 | break; 222 | case 'zh': 223 | $Regx = '/^[\u4e00-\u9fa5]{0,}$/'; 224 | $need = '纯中文'; 225 | break; 226 | case 'url': 227 | $Regx = '/^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$/'; 228 | $need = '链接'; 229 | break; 230 | case 'id': 231 | $Regx = '/^[0-9a-zA-Z]*$/'; 232 | $need = 'ID'; 233 | break; 234 | case 'bv': 235 | $Regx = '/BV{1}[a-zA-Z0-9]{10}/'; 236 | $need = 'BV号'; 237 | break; 238 | case 'qq': 239 | $Regx = '/[1-9][0-9]{4,}/'; 240 | $need = 'QQ号'; 241 | break; 242 | case 'token': 243 | $Regx = '/^[0-9a-zA-Z]{32}$/'; 244 | $need = '32位MD5'; 245 | break; 246 | case 'float': 247 | $Regx = '/(^-?[1-9]\d*\.\d+$|^-?0\.\d+$|^-?[1-9]\d*$|^0$)/'; 248 | $need = '浮点数'; 249 | break; 250 | case preg_match('/between\s*\d*,\d*/', $rule): 251 | preg_match('/(?<=between{1}\s)\d*(?=,)/', $rule, $min); 252 | preg_match('/(?<=,)\d*/', $rule, $max); 253 | $Regx = '/^.{' . $min . ',' . $max . '}$/'; 254 | break; 255 | default: 256 | $Regx = '//'; 257 | break; 258 | } 259 | isset($param[$key]) || $param[$key] = NULL; 260 | if ($param[$key] === null) { 261 | if (!in_array('[Danmaku]参数验证 - [' . $key . '] 为空', $return['msg'])) { 262 | $return['msg'][] = '[Danmaku]参数验证 - [' . $key . '] 为空'; 263 | } 264 | if ($return['code'] == 0) { 265 | $return['code'] = -1; 266 | } 267 | } else { 268 | if (!preg_match($Regx, $param[$key])) { 269 | $return['msg'][] = '[Danmaku]参数验证 - [' . $key . '] 未通过格式校验:' . $need; 270 | if ($return['code'] == 0) { 271 | $return['code'] = -1; 272 | } 273 | } 274 | } 275 | } 276 | } 277 | $return['msg'] = (count($return['msg']) == 0) ? ['[Danmaku]参数验证通过'] : $return['msg']; 278 | return $return; 279 | } 280 | } 281 | 282 | 283 | /** 284 | * 响应类 285 | */ 286 | class Respons 287 | { 288 | /** 289 | * 返回json参数 290 | * @param array $array 需要返回的数组 291 | * @param boolean $cache 是否对接口响应内容进行缓存,默认为是 292 | */ 293 | public static function json(array $array, $cache = false) 294 | { 295 | Config::options(); 296 | header('content-type:application/json;charset=utf-8'); 297 | if ($cache) { 298 | $cache_time = API_CACHE_TIME; 299 | $modified_time = @$_SERVER['HTTP_IF_MODIFIED_SINCE']; 300 | if (strtotime($modified_time) + $cache_time > time()) { 301 | header("HTTP/1.1 304"); 302 | exit; 303 | } 304 | header("Last-Modified: " . gmdate("D, d M Y H:i:s", time()) . " GMT"); 305 | header("Expires: " . gmdate("D, d M Y H:i:s", time() + $cache_time) . " GMT"); 306 | header("Cache-Control: max-age=" . $cache_time); 307 | } else { 308 | header('Expires: Mon, 26 Jul 1970 05:00:00 GMT'); 309 | header('Cache-Control: no-cache, must-revalidate'); 310 | header('Pragma: no-cache'); 311 | } 312 | echo json_encode(Respons::https($array), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 313 | } 314 | 315 | /** 316 | * 自动修改返回的内容里包含链接部分http开头 317 | */ 318 | public static function https($content) 319 | { 320 | Config::options(); 321 | if (API_HTTPS) { 322 | if (is_array($content)) { 323 | $json = json_encode($content, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 324 | return json_decode(str_replace('http://', 'https://', $json), true); 325 | } else { 326 | return str_replace('http://', 'https://', $content); 327 | } 328 | }else{ 329 | return $content; 330 | } 331 | } 332 | } 333 | 334 | 335 | /** 336 | * 数据库类 337 | */ 338 | class Db 339 | { 340 | /** 341 | * 链接数据库 342 | * @return PdoObject 343 | */ 344 | public static function connect() 345 | { 346 | Config::db(); 347 | try { 348 | return new PDO("mysql:host=" . DB_SERVER_NAME . ";dbname=" . DB_NAME . ";port=" . DB_PORT . ";charset=utf8;", DB_USER_NAME, DB_PASSWORD); 349 | } catch (PDOException $e) { 350 | return $e->getMessage(); 351 | } 352 | } 353 | 354 | 355 | /** 356 | * 数据库查询语句 357 | * @param string $sql SQL语句 358 | * @param boolean $fetch 是否只输出第一行,默认全部输出 359 | * @return array/false 360 | */ 361 | public static function query($sql, $fetch = false) 362 | { 363 | $result = Db::connect()->query($sql); 364 | if ($result == false || $result == null) { 365 | return false; 366 | } 367 | return ($fetch == true) ? $result->fetch() : $result->fetchAll(); 368 | } 369 | 370 | 371 | /** 372 | * 数据库操作语句 373 | * @param string $sql SQL语句 374 | * @return int 375 | */ 376 | public static function exec($sql) 377 | { 378 | return Db::connect()->exec($sql); 379 | } 380 | 381 | 382 | /** 383 | * 检查是否存在对应表 384 | * @param string $name 表名 385 | * @return boolean 386 | */ 387 | public static function isTableExist($name) 388 | { 389 | Config::db(); 390 | $result = Db::query('SHOW TABLES LIKE "' . TABLE_PRE_NAME . '_' . $name . '"', true); 391 | return ($result == false) ? false : true; 392 | } 393 | 394 | 395 | /** 396 | * 创建表(自带前缀).请注意不要输入uid作为键名,uid为本程序链表默认关键字 397 | * @param string $table 表名 398 | * @param array $keys 键名列表,请使用 ['keyName'=>['type','length']]指定键的属性。例如['sid'=>['int',10],'token'=>['varchar',32]] 399 | * @return boolean 400 | */ 401 | public static function createTable($table, $keys) 402 | { 403 | Config::db(); 404 | if ($table == null || !is_array($keys) || in_array('uid', $keys, true)) { 405 | return false; 406 | } 407 | $keyArray[] = '`uid` int(11) NOT NULL AUTO_INCREMENT'; 408 | foreach ($keys as $key => $attribute) { 409 | if (!isset($attribute[0])) { 410 | return false; 411 | } 412 | $keyArray[] = '`' . $key . '` ' . $attribute[0] . (!isset($attribute[1]) ? '' : '(' . $attribute[1] . ')') . ' NOT NULL'; 413 | } 414 | $sql = 'CREATE TABLE `' . TABLE_PRE_NAME . '_' . $table . '` (' . join(',', $keyArray) . ',PRIMARY KEY (`uid`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;'; 415 | Db::exec($sql); 416 | return Db::isTableExist($table); 417 | } 418 | 419 | 420 | /** 421 | * 向表格插入数据 422 | * @param string $table 表名 423 | * @param array $insert 需要插入的数据,请使用带指定下标的数组,例如['uid'=>2,token=>'AS23213F','time'=>1698200333] 424 | * @return boolean 425 | */ 426 | public static function insertTable($table, $insert) 427 | { 428 | Config::db(); 429 | if ($table == null || !is_array($insert)) { 430 | return false; 431 | } 432 | list($name, $var) = [[], []]; 433 | foreach ($insert as $key => $value) { 434 | $name[] = $key; 435 | $var[] = (preg_match('/^\d*$/', $value) == 0) ? "'" . $value . "'" : $value; 436 | } 437 | $sql = 'INSERT INTO `' . TABLE_PRE_NAME . '_' . $table . '` (`' . join('`,`', $name) . '`) VALUES (' . join(',', $var) . ')'; 438 | return (Db::exec($sql) >= 1) ? true : false; 439 | } 440 | 441 | /** 442 | * 根据表名,条件数组来查询内容 443 | * @param string $table 表名 444 | * @param array $search 条件数组 445 | * @return array/boolean 446 | */ 447 | public static function get($table, $search, $fetch = true) 448 | { 449 | Config::options(); 450 | if (!is_array($search) || empty($search) || empty($table)) { 451 | return false; 452 | } 453 | foreach ($search as $key => $value) { 454 | $serachList[] = ($key !== null && $value !== null) ? '`' . $key . '`=' . '\'' . $value . '\'' : ''; 455 | } 456 | if (empty($serachList)) { 457 | return false; 458 | } 459 | $sql = 'SELECT * FROM `' . TABLE_PRE_NAME . '_' . $table . '` WHERE ' . join(' AND ', $serachList); 460 | return Db::query($sql, $fetch); 461 | } 462 | 463 | /** 464 | * 更改指定表的指定参数 465 | * @param string $table 表名 466 | * @param array $change 改变的内容数组 467 | * @param array $search 被改变的对象数组 468 | * @return boolean 469 | */ 470 | public static function set($table, $change, $search) 471 | { 472 | Config::options(); 473 | if (!is_array($change) || empty($change) || empty($table)) { 474 | return false; 475 | } 476 | 477 | foreach ($change as $key => $value) { 478 | $changeList[] = ($key !== null && $value !== null) ? '`' . $key . '`=' . '\'' . $value . '\'' : ''; 479 | } 480 | $searchList = Db::get($table, $search, false); 481 | foreach ($searchList as $row) { 482 | $uidList[] = $row['uid']; 483 | } 484 | if (empty($changeList) || !isset($uidList)) { 485 | return false; 486 | } 487 | $sql = 'UPDATE `' . TABLE_PRE_NAME . '_' . $table . '` SET ' . join(', ', $changeList) . ' WHERE `' . TABLE_PRE_NAME . '_' . $table . '`.`uid` IN (' . join(',', $uidList) . ');'; 488 | return (Db::exec($sql) >= 1) ? true : false; 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/Injahow/Bilibili.php: -------------------------------------------------------------------------------- 1 | type($value); 47 | } 48 | 49 | public function type($value) 50 | { 51 | $suppose = array('video', 'bangumi', 'cheese'); 52 | $this->type = in_array($value, $suppose) ? $value : 'video'; 53 | $this->header = $this->curlset(); 54 | 55 | return $this; 56 | } 57 | 58 | public function aid($value) 59 | { 60 | $this->aid = $value; 61 | 62 | return $this; 63 | } 64 | 65 | public function bvid($value) 66 | { 67 | $this->bvid = $value; 68 | 69 | return $this; 70 | } 71 | 72 | public function page($value) 73 | { 74 | $this->page = $value > 1 ? $value : 1; 75 | 76 | return $this; 77 | } 78 | 79 | public function epid($value) 80 | { 81 | $this->epid = $value; 82 | 83 | return $this; 84 | } 85 | 86 | public function cid($value) 87 | { 88 | $this->cid = $value; 89 | 90 | return $this; 91 | } 92 | 93 | public function quality($value, $force = false) 94 | { 95 | $value = intval($value); 96 | if (!$force) { 97 | $suppose = array(127, 125, 120, 116, 112, 80, 74, 64, 48, 32, 16); // todo 98 | foreach ($suppose as $v) { 99 | if ($v <= $value) { 100 | $this->quality = $v; 101 | 102 | return $this; 103 | } 104 | } 105 | $this->quality = 32; 106 | } else { 107 | $this->quality = $value; 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | public function format($value) 114 | { 115 | $suppose = array('flv', 'dash', 'mp4'); 116 | $this->format = in_array($value, $suppose) ? $value : 'flv'; 117 | 118 | return $this; 119 | } 120 | 121 | public function access_key($value) 122 | { 123 | $this->access_key = $value; 124 | 125 | return $this; 126 | } 127 | 128 | public function cache($value = true, $type = '') 129 | { 130 | $this->cache = $value; 131 | if (in_array($type, array('file', 'apcu'))) { 132 | $this->cache_type = $type; 133 | } 134 | 135 | return $this; 136 | } 137 | 138 | public function cache_time($value = 3600) 139 | { 140 | $value = intval($value); 141 | $this->cache_time = $value > 60 ? $value : 60; 142 | 143 | return $this; 144 | } 145 | 146 | public function cookie($value) 147 | { 148 | $this->has_cookie = !empty($value); 149 | $this->header['Cookie'] = $value; 150 | 151 | return $this; 152 | } 153 | 154 | public function proxy($value) 155 | { 156 | $this->proxy = $value; 157 | 158 | return $this; 159 | } 160 | 161 | public function data() 162 | { 163 | if (empty($this->cid)) { 164 | $this->setCid(); 165 | } 166 | 167 | if (empty($this->cid)) { 168 | return json_encode([array( 169 | 'code' => 1, 170 | 'message' => 'unknown cid' 171 | )]); 172 | } 173 | 174 | // if ($this->type == 'video' && $this->format != 'dash' && empty($this->access_key) && !$this->has_cookie) { 175 | // $api = $this->bilibili_api(); 176 | // } else { 177 | // $api = $this->bilibili_web_api(); 178 | // } 179 | $api = $this->bilibili_web_api(); 180 | 181 | return $this->exec($api); 182 | } 183 | 184 | public function result() 185 | { 186 | 187 | if ($this->cache) { 188 | $this->result = $this->getCache(); 189 | if (!empty($this->result)) { 190 | return $this->result; 191 | } 192 | } 193 | 194 | $data = $this->data(); 195 | $raw = json_decode($this->raw, true); 196 | if (!empty($raw['code'])) 197 | return $this->raw; 198 | 199 | $data = json_decode($data, true)[0]; 200 | 201 | if (!empty($data['is_preview'])) 202 | return json_encode(array( 203 | 'code' => 1, 204 | 'message' => '无访问权限' 205 | )); 206 | 207 | $result = null; 208 | switch ($this->format) { 209 | case 'dash': 210 | if (isset($data['dash'])) { 211 | $index = 0; 212 | foreach ($data['dash']['video'] as $i => $video) { 213 | if ($video['id'] <= $this->quality) { 214 | $index = $i; 215 | break; 216 | } 217 | } 218 | $result = array( 219 | 'code' => 0, 220 | 'quality' => $data['dash']['video'][$index]['id'], 221 | 'accept_quality' => $data['accept_quality'], 222 | 'video' => $data['dash']['video'][$index]['base_url'], 223 | 'audio' => $data['dash']['audio'][0]['base_url'] 224 | ); 225 | } 226 | break; 227 | 228 | case 'flv': 229 | case 'mp4': 230 | if (isset($data['durl'])) { 231 | $result = array( 232 | 'code' => 0, 233 | 'quality' => $data['quality'], 234 | 'accept_quality' => $data['accept_quality'], 235 | 'url' => $data['durl'][0]['url'] 236 | ); 237 | } 238 | break; 239 | } 240 | 241 | if (empty($result)) 242 | return json_encode(array( 243 | 'code' => 1, 244 | 'message' => '获取信息失败' 245 | )); 246 | 247 | if (in_array($this->quality, $result['accept_quality'])) { 248 | $quality = $this->quality; 249 | } else { // 修正quality 250 | foreach ($result['accept_quality'] as $v) { 251 | if ($v <= $this->quality) { 252 | $quality = $v; 253 | break; 254 | } 255 | } 256 | if (!isset($quality)) $quality = $result['accept_quality'][0]; 257 | } 258 | if ($this->format != 'mp4' && $result['quality'] < $quality && $quality <= $result['accept_quality'][0]) 259 | return json_encode(array( 260 | 'code' => 1, 261 | 'message' => '视频清晰度受限,可能需要会员' 262 | )); 263 | 264 | $this->result = json_encode($result); 265 | 266 | if ($this->cache) { 267 | $this->setCache($this->result); 268 | } 269 | 270 | return $this->result; 271 | } 272 | 273 | private function getCacheName() 274 | { 275 | // ! mkdir '/cache/*' 276 | if ($this->format == 'mp4') 277 | $suffix = 'mp4'; 278 | else 279 | $suffix = $this->quality . '_' . $this->format; 280 | 281 | if (empty($this->cid)) $this->setCid(); 282 | 283 | if (!empty($this->cid)) 284 | $path = '/../cache/cid/' . $this->cid . '_' . $suffix . '.json'; 285 | else 286 | $path = '/../cache/cid/0' . '_' . $this->format . '.json'; 287 | 288 | return __DIR__ . $path; 289 | } 290 | 291 | private function bilibili_api() 292 | { 293 | // error 294 | $this->setAppkey(); 295 | 296 | $api = array( 297 | 'method' => 'GET', 298 | 'url' => array( 299 | 'flv' => 'https://interface.bilibili.com/v2/playurl', 300 | 'mp4' => 'https://app.bilibili.com/v2/playurlproj' 301 | )[$this->format != 'flv' ? 'mp4' : 'flv'], // not dash 302 | 'body' => array( 303 | 'appkey' => $this->appkey, 304 | 'cid' => $this->cid, 305 | 'otype' => 'json', 306 | 'qn' => $this->quality, 307 | 'quality' => $this->quality, 308 | 'type' => '' 309 | ), 310 | 'format' => '' 311 | ); 312 | $api['body'] += array('sign' => md5(http_build_query($api['body']) . $this->sec)); 313 | 314 | return $api; 315 | } 316 | 317 | private function bilibili_web_api() 318 | { 319 | $f = $this->type == 'cheese' && $this->format == 'mp4'; 320 | $body = array( 321 | 'avid' => $this->aid, 322 | 'bvid' => $this->bvid, 323 | 'cid' => $this->cid, 324 | 'ep_id' => $this->epid, 325 | 'qn' => $this->format == 'mp4' ? 80 : $this->quality, 326 | 'type' => $this->format, 327 | 'otype' => 'json', 328 | 'fnver' => $f ? 1 : 0, 329 | 'fnval' => $f ? 80 : array('dash' => 4048, 'flv' => 4049, 'mp4' => 0)[$this->format], 330 | 'fourk' => 1, 331 | 'access_key' => $this->access_key 332 | ); 333 | 334 | if ($this->format == 'mp4') { 335 | $body += array('platform' => 'html5', 'high_quality' => 1); 336 | } 337 | 338 | switch ($this->type) { 339 | case 'video': 340 | $body['access_key'] = ''; // need cookie 341 | $api = array( 342 | 'method' => 'GET', 343 | 'url' => 'https://api.bilibili.com/x/player/playurl', 344 | 'body' => $body, 345 | 'format' => 'data' 346 | ); 347 | break; 348 | 349 | case 'bangumi': 350 | $api = array( 351 | 'method' => 'GET', 352 | 'url' => 'https://api.bilibili.com/pgc/player/web/playurl', 353 | 'body' => $body, 354 | 'format' => 'result' 355 | ); 356 | break; 357 | 358 | case 'cheese': 359 | $api = array( 360 | 'method' => 'GET', 361 | 'url' => 'https://api.bilibili.com/pugv/player/web/playurl', 362 | 'body' => $body, 363 | 'format' => 'data' 364 | ); 365 | break; 366 | } 367 | 368 | return $api; 369 | } 370 | 371 | private function bangumi_season_id($media_id) 372 | { 373 | // media_id -> season_id 374 | return array( 375 | 'method' => 'GET', 376 | 'url' => 'https://api.bilibili.com/pgc/review/user', 377 | 'body' => array( 378 | 'media_id' => $media_id 379 | ) 380 | ); 381 | } 382 | 383 | private function bangumi_cid($season_id) 384 | { 385 | // season_id -> all(cid) 386 | return array( 387 | 'method' => 'GET', 388 | 'url' => 'https://api.bilibili.com/pgc/web/season/section', 389 | 'body' => array( 390 | 'season_id' => $season_id 391 | ) 392 | ); 393 | } 394 | 395 | public function setCache($data) 396 | { 397 | $file_name = $this->getCacheName(); 398 | if ($this->cache_type == 'file') { 399 | file_put_contents($file_name, $data); 400 | } else if ($this->cache_type == 'apcu') { 401 | apcu_store(md5($file_name), $data, $this->cache_time); 402 | } 403 | } 404 | 405 | public function getCache() 406 | { 407 | $file_name = $this->getCacheName(); 408 | if ($this->cache_type == 'file') { 409 | if (file_exists($file_name) && $_SERVER['REQUEST_TIME'] - filemtime($file_name) < $this->cache_time) { 410 | return file_get_contents($file_name); 411 | } 412 | return null; 413 | } else if ($this->cache_type == 'apcu') { 414 | return apcu_fetch(md5($file_name)); 415 | } 416 | return null; 417 | } 418 | 419 | private function setCid() 420 | { 421 | if (!empty($this->epid)) { 422 | $api = array( 423 | 'method' => 'GET', 424 | 'url' => array( 425 | 'bangumi' => 'https://api.bilibili.com/pgc/view/web/season', 426 | 'cheese' => 'https://api.bilibili.com/pugv/view/web/season' 427 | )[$this->type != 'bangumi' ? 'cheese' : 'bangumi'], 428 | 'body' => array( 429 | 'ep_id' => $this->epid, 430 | 'access_key' => $this->access_key 431 | ), 432 | 'format' => ($this->type != 'bangumi' ? 'data' : 'result') . '.episodes' 433 | ); 434 | $episodes = json_decode($this->exec($api), true); 435 | if (!isset($episodes[0])) return; 436 | 437 | foreach ($episodes as $episode) { 438 | if ($episode['id'] == $this->epid) { 439 | $this->cid = $episode['cid']; 440 | 441 | return; 442 | } 443 | } 444 | } else if (!empty($this->aid) || !empty($this->bvid)) { 445 | $api = array( 446 | 'method' => 'GET', 447 | 'url' => 'https://api.bilibili.com/x/web-interface/view', 448 | 'body' => array( 449 | 'aid' => $this->aid, 450 | 'bvid' => $this->bvid, 451 | 'access_key' => $this->access_key 452 | ), 453 | 'format' => $this->type == 'bangumi' ? '' : 'data.pages.' . strval($this->page - 1) 454 | ); 455 | $res = json_decode($this->exec($api), true); 456 | if (!isset($res[0])) return; 457 | 458 | if ($this->type == 'bangumi' && isset($res[0]['data'])) 459 | $this->cid = $res[0]['data']['cid']; 460 | else 461 | $this->cid = $res[0]['cid']; 462 | } 463 | } 464 | 465 | private function setAppkey() 466 | { 467 | /************/ 468 | $entropy = 'rbMCKn@KuamXWlPMoJGsKcbiJKUfkPF_8dABscJntvqhRSETg'; 469 | $entropy_array = str_split(strrev($entropy), 1); 470 | $str = ''; 471 | $len = strlen($entropy); 472 | for ($i = 0; $i < $len; ++$i) { 473 | $str .= chr(ord($entropy_array[$i]) + 2); 474 | } 475 | list($this->appkey, $this->sec) = explode(':', $str); 476 | /************/ 477 | } 478 | 479 | private function curlset() 480 | { 481 | return array( 482 | 'Referer' => 'https://www.bilibili.com/', 483 | 'Cookie' => '_uuid=ECD29A42-D6E2-2C85-D76D-53E293C8053659853infoc; bfe_id=61a513175dc1ae8854a560f6b82b37af; CURRENT_BLACKGAP=1; CURRENT_FNVAL=80; CURRENT_QUALITY=80', 484 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/605.1.15 (KHTML, like Gecko)', 485 | 'X-Real-IP' => long2ip(mt_rand(1884815360, 1884890111)), 486 | 'Accept' => 'application/json, text/plain, */*', 487 | 'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7', 488 | 'Connection' => 'keep-alive', 489 | 'Content-Type' => 'application/x-www-form-urlencoded', 490 | ); 491 | } 492 | 493 | private function exec($api) 494 | { 495 | if ($api['method'] == 'GET') { 496 | if (isset($api['body'])) { 497 | $api['url'] .= '?' . http_build_query($api['body']); 498 | $api['body'] = null; 499 | } 500 | } 501 | 502 | $this->curl($api['url'], $api['body']); 503 | $this->data = $this->raw; 504 | 505 | if (isset($api['format'])) { 506 | $this->data = $this->clean($this->data, $api['format']); 507 | } 508 | 509 | return $this->data; 510 | } 511 | 512 | private function curl($url, $payload = null, $headerOnly = 0) 513 | { 514 | $header = array_map(function ($k, $v) { 515 | return $k . ': ' . $v; 516 | }, array_keys($this->header), $this->header); 517 | $curl = curl_init(); 518 | if (!is_null($payload)) { 519 | curl_setopt($curl, CURLOPT_POST, 1); 520 | curl_setopt($curl, CURLOPT_POSTFIELDS, is_array($payload) ? http_build_query($payload) : $payload); 521 | } 522 | curl_setopt($curl, CURLOPT_HEADER, $headerOnly); 523 | curl_setopt($curl, CURLOPT_TIMEOUT, 20); 524 | curl_setopt($curl, CURLOPT_ENCODING, 'gzip'); 525 | curl_setopt($curl, CURLOPT_IPRESOLVE, 1); 526 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 527 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); 528 | curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); 529 | curl_setopt($curl, CURLOPT_URL, $url); 530 | curl_setopt($curl, CURLOPT_HTTPHEADER, $header); 531 | if ($this->proxy) { 532 | curl_setopt($curl, CURLOPT_PROXY, $this->proxy); 533 | } 534 | for ($i = 0; $i < 3; ++$i) { 535 | $this->raw = curl_exec($curl); 536 | $this->info = curl_getinfo($curl); 537 | $this->error = curl_errno($curl); 538 | $this->status = $this->error ? curl_error($curl) : ''; 539 | if (!$this->error) { 540 | break; 541 | } 542 | } 543 | curl_close($curl); 544 | 545 | return $this; 546 | } 547 | 548 | private function pickup($array, $rule) 549 | { 550 | $t = explode('.', $rule); 551 | foreach ($t as $vo) { 552 | if (!isset($array[$vo])) { 553 | return array(); 554 | } 555 | $array = $array[$vo]; 556 | } 557 | 558 | return $array; 559 | } 560 | 561 | private function clean($raw, $rule) 562 | { 563 | $raw = json_decode($raw, true); 564 | if (!empty($rule)) { 565 | $raw = $this->pickup($raw, $rule); 566 | } 567 | if (!isset($raw[0]) && count($raw)) { 568 | $raw = array($raw); 569 | } 570 | $result = $raw; 571 | 572 | return json_encode($result); 573 | } 574 | } 575 | --------------------------------------------------------------------------------