├── 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 |
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 | 
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 |
--------------------------------------------------------------------------------