bilibili.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. /*
  2. * @File : bilibili.js
  3. * @Author : jade
  4. * @Date : 2024/4/3 9:27
  5. * @Email : jadehh@1ive.com
  6. * @Software : Samples
  7. * @Desc : 哔哩哔哩
  8. */
  9. import {Spider} from "./spider.js";
  10. import * as Utils from "../lib/utils.js";
  11. import {Crypto, _, load} from "../lib/cat.js";
  12. import {VodDetail, VodShort} from "../lib/vod.js";
  13. class BilibiliSpider extends Spider {
  14. constructor() {
  15. super();
  16. this.siteUrl = "https://www.bilibili.com"
  17. this.apiUrl = "https://api.bilibili.com"
  18. this.cookie = ""
  19. this.bili_jct = '';
  20. this.is_login = false
  21. this.is_vip = false
  22. this.vod_audio_id = {
  23. 30280: 192000,
  24. 30232: 132000,
  25. 30216: 64000,
  26. };
  27. this.vod_codec = {
  28. // 13: 'AV1',
  29. 12: 'HEVC',
  30. 7: 'AVC',
  31. };
  32. this.play_url_obj = {
  33. 80: "1080P 高清",
  34. 64: "720P 高清",
  35. 32: "420P 清晰",
  36. 16: "360P 流畅"
  37. }
  38. }
  39. getHeader() {
  40. const headers = super.getHeader();
  41. if (!_.isEmpty(this.cookie)) {
  42. headers["cookie"] = this.cookie;
  43. }
  44. return headers;
  45. }
  46. initCookie(cookie) {
  47. this.cookie = cookie
  48. if (cookie.includes('bili_jct')) {
  49. this.bili_jct = cookie.split('bili_jct=')[1].split(";")[0];
  50. }
  51. }
  52. async spiderInit(Req) {
  53. this.is_login = await this.checkLogin()
  54. if (this.is_login) {
  55. await this.jadeLog.info("哔哩哔哩登录成功", true)
  56. } else {
  57. await this.jadeLog.error("哔哩哔哩登录失败", true)
  58. }
  59. if (Req === null) {
  60. // dash mpd 代理
  61. this.js2Base = await js2Proxy(true, this.siteType, this.siteKey, 'dash/', this.getHeader());
  62. } else {
  63. this.js2Base = await js2Proxy(Req, "dash", this.getHeader());
  64. }
  65. }
  66. async init(cfg) {
  67. await super.init(cfg);
  68. await this.initCookie(this.cfgObj["cookie"])
  69. await this.spiderInit(null)
  70. this.danmuStaus = true
  71. }
  72. getName() {
  73. return "🏰┃哔哩哔哩┃🏰"
  74. }
  75. getAppName() {
  76. return "哔哩哔哩"
  77. }
  78. getJSName() {
  79. return "bilibili"
  80. }
  81. getType() {
  82. return 3
  83. }
  84. async setClasses() {
  85. let $ = await this.getHtml(this.siteUrl)
  86. let navElements = $("[class=\"channel-items__left\"]").find("a")
  87. for (const navElement of navElements) {
  88. this.classes.push(this.getTypeDic($(navElement).text(), $(navElement).text()))
  89. }
  90. if (!_.isEmpty(this.bili_jct) && this.is_login) {
  91. this.classes.push(this.getTypeDic("历史记录", "历史记录"))
  92. }
  93. }
  94. async getFilter($) {
  95. return [
  96. {
  97. key: 'order',
  98. name: '排序',
  99. value: [
  100. {n: '综合排序', v: '0'},
  101. {n: '最多点击', v: 'click'},
  102. {n: '最新发布', v: 'pubdate'},
  103. {n: '最多弹幕', v: 'dm'},
  104. {n: '最多收藏', v: 'stow'},
  105. ],
  106. },
  107. {
  108. key: 'duration',
  109. name: '时长',
  110. value: [
  111. {n: '全部时长', v: '0'},
  112. {n: '60分钟以上', v: '4'},
  113. {n: '30~60分钟', v: '3'},
  114. {n: '10~30分钟', v: '2'},
  115. {n: '10分钟以下', v: '1'},
  116. ],
  117. },
  118. ];
  119. }
  120. async setFilterObj() {
  121. for (const typeDic of this.classes) {
  122. let type_id = typeDic["type_name"]
  123. if (type_id !== "最近更新" && type_id !== "历史记录") {
  124. this.filterObj[type_id] = await this.getFilter()
  125. }
  126. }
  127. }
  128. getFullTime(numberSec) {
  129. let totalSeconds = '';
  130. try {
  131. let timeParts = numberSec.split(":");
  132. let min = parseInt(timeParts[0]);
  133. let sec = parseInt(timeParts[1]);
  134. totalSeconds = min * 60 + sec;
  135. } catch (e) {
  136. totalSeconds = parseInt(numberSec);
  137. }
  138. if (isNaN(totalSeconds)) {
  139. return '无效输入';
  140. }
  141. if (totalSeconds >= 3600) {
  142. const hours = Math.floor(totalSeconds / 3600);
  143. const remainingSecondsAfterHours = totalSeconds % 3600;
  144. const minutes = Math.floor(remainingSecondsAfterHours / 60);
  145. const seconds = remainingSecondsAfterHours % 60;
  146. return `${hours}小时 ${minutes}分钟 ${seconds}秒`;
  147. } else {
  148. const minutes = Math.floor(totalSeconds / 60);
  149. const seconds = totalSeconds % 60;
  150. return `${minutes}分钟 ${seconds}秒`;
  151. }
  152. }
  153. removeTags(input) {
  154. return input.replace(/<[^>]*>/g, '');
  155. }
  156. async parseVodShortListFromJson(objList) {
  157. let vod_list = []
  158. for (const vodData of objList) {
  159. let vodShort = new VodShort()
  160. vodShort.vod_id = vodData["bvid"]
  161. if (vodData.hasOwnProperty("rcmd_reason")) {
  162. vodShort.vod_remarks = vodData["rcmd_reason"]["content"]
  163. } else {
  164. vodShort.vod_remarks = this.getFullTime(vodData["duration"])
  165. }
  166. vodShort.vod_name = this.removeTags(vodData["title"])
  167. let imageUrl = vodData["pic"];
  168. if (imageUrl.startsWith('//')) {
  169. imageUrl = 'https:' + imageUrl;
  170. }
  171. vodShort.vod_pic = imageUrl
  172. vod_list.push(vodShort)
  173. }
  174. return vod_list
  175. }
  176. async parseVodDetailfromJson(obj, bvid) {
  177. let cd = this.getFullTime(obj["duration"]);
  178. const aid = obj.aid;
  179. let vodDetail = new VodDetail()
  180. vodDetail.vod_name = obj["title"]
  181. vodDetail.vod_pic = obj["pic"]
  182. vodDetail.type_name = obj["tname"]
  183. vodDetail.vod_remarks = cd
  184. vodDetail.vod_content = obj["desc"]
  185. let params = {"avid": aid, "cid": obj["cid"], "qn": "127", "fnval": 4048, "fourk": 1}
  186. let playUrlDatas = JSON.parse(await this.fetch(this.apiUrl + "/x/player/playurl", params, this.getHeader()));
  187. let playUrldDataList = playUrlDatas["data"];
  188. const accept_quality = playUrldDataList["accept_quality"];
  189. const accept_description = playUrldDataList["accept_description"];
  190. const qualityList = [];
  191. const descriptionList = [];
  192. for (let i = 0; i < accept_quality.length; i++) {
  193. if (!this.is_vip) {
  194. if (this.is_login) {
  195. if (accept_quality[i] > 80) continue;
  196. } else {
  197. if (accept_quality[i] > 32) continue;
  198. }
  199. }
  200. descriptionList.push(Utils.base64Encode(accept_description[i]));
  201. qualityList.push(accept_quality[i]);
  202. }
  203. let treeMap = {};
  204. const jSONArray = obj["pages"];
  205. let playList = [];
  206. for (let j = 0; j < jSONArray.length; j++) {
  207. const jSONObject6 = jSONArray[j];
  208. const cid = jSONObject6.cid;
  209. const playUrl = j + '$' + aid + '+' + cid + '+' + qualityList.join(':') + '+' + descriptionList.join(':');
  210. playList.push(playUrl);
  211. }
  212. if (this.catOpenStatus) {
  213. for (let quality of qualityList) {
  214. treeMap[`dash - ${this.play_url_obj[quality]}`] = playList.join("#")
  215. }
  216. } else {
  217. await this.jadeLog.warning("TV暂不支持Dash播放")
  218. }
  219. for (let quality of qualityList) {
  220. treeMap[`mp4 - ${this.play_url_obj[quality]}`] = playList.join("#")
  221. }
  222. let relatedParams = {"bvid": bvid}
  223. const relatedData = JSON.parse(await this.fetch(this.apiUrl + "/x/web-interface/archive/related", relatedParams, this.getHeader())).data;
  224. playList = [];
  225. for (let j = 0; j < relatedData.length; j++) {
  226. const jSONObject6 = relatedData[j];
  227. const cid = jSONObject6.cid;
  228. const title = jSONObject6.title;
  229. const aaid = jSONObject6.aid;
  230. const playUrl = title + '$' + aaid + '+' + cid + '+' + qualityList.join(':') + '+' + descriptionList.join(':');
  231. playList.push(playUrl);
  232. }
  233. if (this.catOpenStatus) {
  234. for (let quality of qualityList) {
  235. treeMap["相关" + ` - ${this.play_url_obj[quality]}`] = playList.join("#")
  236. }
  237. } else {
  238. await this.jadeLog.warning("TV暂不支持相关播放")
  239. }
  240. vodDetail.vod_play_from = Object.keys(treeMap).join("$$$");
  241. vodDetail.vod_play_url = Object.values(treeMap).join("$$$");
  242. return vodDetail
  243. }
  244. async setHomeVod() {
  245. let params = {"ps": 20}
  246. let content = await this.fetch(this.apiUrl + "/x/web-interface/popular", params, this.getHeader())
  247. this.homeVodList = await this.parseVodShortListFromJson(JSON.parse(content)["data"]["list"])
  248. }
  249. async setDetail(id) {
  250. const detailUrl = this.apiUrl + "/x/web-interface/view";
  251. let params = {"bvid": id}
  252. const detailData = JSON.parse(await this.fetch(detailUrl, params, this.getHeader())).data
  253. // 记录历史
  254. if (!_.isEmpty(this.bili_jct)) {
  255. const historyReport = this.apiUrl + '/x/v2/history/report';
  256. let dataPost = {
  257. aid: detailData.aid,
  258. cid: detailData.cid,
  259. csrf: this.bili_jct,
  260. }
  261. await this.post(historyReport, dataPost, this.getHeader(), "form");
  262. }
  263. this.vodDetail = await this.parseVodDetailfromJson(detailData, id)
  264. }
  265. findKeyByValue(obj, value) {
  266. for (const key in obj) {
  267. if (obj[key] === value) {
  268. return key;
  269. }
  270. }
  271. return null;
  272. }
  273. async setPlay(flag, id, flags) {
  274. const ids = id.split('+');
  275. const aid = ids[0];
  276. const cid = ids[1];
  277. let quality_name = flag.split(" - ")[1]
  278. let quality_id = this.findKeyByValue(this.play_url_obj, quality_name)
  279. this.danmuUrl = this.apiUrl + '/x/v1/dm/list.so?oid=' + cid;
  280. this.result.header = this.getHeader()
  281. if (flag.indexOf("dash") > -1 || flag.indexOf('相关') > -1) {
  282. // dash mpd 代理
  283. if (this.catOpenStatus) {
  284. this.playUrl = this.js2Base + Utils.base64Encode(aid + '+' + cid + '+' + quality_id)
  285. }
  286. } else if (flag.indexOf('mp4') > -1) {
  287. // 直链
  288. const url = this.apiUrl + `/x/player/playurl`;
  289. let params = {"avid": aid, "cid": cid, "qn": parseInt(quality_id), "fourk": "1"}
  290. const resp = JSON.parse(await this.fetch(url, params, this.getHeader()));
  291. const data = resp.data;
  292. this.playUrl = data["durl"][0].url;
  293. } else {
  294. // 音频外挂
  295. let urls = [];
  296. let audios = [];
  297. const url = this.siteUrl + "/x/player/playurl"
  298. let params = {"avid": aid, "cid": cid, "qn": quality_id, "fnval": 4048, "fourk": 1};
  299. let resp = JSON.parse(await this.fetch(url, params, this.getHeader()));
  300. const dash = resp.data.dash;
  301. const video = dash.video;
  302. const audio = dash.audio;
  303. for (let j = 0; j < video.length; j++) {
  304. const dashjson = video[j];
  305. if (dashjson.id === quality_id) {
  306. for (const key in this.vod_codec) {
  307. if (dashjson["codecid"] === key) {
  308. urls.push(Utils.base64Decode(quality_id) + ' ' + this.vod_codec[key], dashjson["baseUrl"]);
  309. }
  310. }
  311. }
  312. }
  313. if (audios.length === 0) {
  314. for (let j = 0; j < audio.length; j++) {
  315. const dashjson = audio[j];
  316. for (const key in this.vod_audio_id) {
  317. if (dashjson.id === key) {
  318. audios.push({
  319. title: _.floor(parseInt(this.vod_audio_id[key]) / 1024) + 'Kbps',
  320. bit: this.vod_audio_id[key],
  321. url: dashjson["baseUrl"],
  322. });
  323. }
  324. }
  325. }
  326. audios = _.sortBy(audios, 'bit');
  327. }
  328. this.playUrl = urls
  329. this.extra = {"audio": audios}
  330. }
  331. }
  332. async checkLogin() {
  333. let result = JSON.parse(await this.fetch('https://api.bilibili.com/x/web-interface/nav', null, this.getHeader()));
  334. this.is_vip = result["data"]["vipStatus"]
  335. return result["data"]["isLogin"]
  336. }
  337. async setCategory(tid, pg, filter, extend) {
  338. let page;
  339. if (parseInt(pg) < 1) {
  340. page = 1;
  341. } else {
  342. page = parseInt(pg)
  343. }
  344. if (Object.keys(extend).length > 0 && extend.hasOwnProperty('tid') && extend['tid'].length > 0) {
  345. tid = extend['tid'];
  346. }
  347. let url = '';
  348. url = this.apiUrl + `/x/web-interface/search/type?search_type=video&keyword=${encodeURIComponent(tid)}`;
  349. if (Object.keys(extend).length > 0) {
  350. for (const k in extend) {
  351. if (k === 'tid') {
  352. continue;
  353. }
  354. url += `&${encodeURIComponent(k)}=${encodeURIComponent(extend[k])}`;
  355. }
  356. }
  357. url += `&page=${encodeURIComponent(page)}`;
  358. if (tid === "历史记录") {
  359. url = this.apiUrl + "/x/v2/history?pn=" + page;
  360. }
  361. const data = JSON.parse(await this.fetch(url, null, this.getHeader())).data;
  362. let items = data.result;
  363. if (tid === "历史记录") {
  364. items = data;
  365. }
  366. this.vodList = await this.parseVodShortListFromJson(items)
  367. }
  368. async setSearch(wd, quick, pg) {
  369. const ext = {
  370. duration: '0',
  371. };
  372. let page = parseInt(pg)
  373. const limit = 20
  374. let resp = JSON.parse(await this.category(wd, page, true, ext));
  375. this.vodList = resp["list"]
  376. let pageCount = page;
  377. if (this.vodList.length === limit) {
  378. pageCount = page + 1;
  379. }
  380. this.result.setPage(page, pageCount, limit, pageCount)
  381. }
  382. getDashMedia(dash) {
  383. try {
  384. let qnid = dash.id;
  385. const codecid = dash["codecid"];
  386. const media_codecs = dash["codecs"];
  387. const media_bandwidth = dash["bandwidth"];
  388. const media_startWithSAP = dash["startWithSap"];
  389. const media_mimeType = dash.mimeType;
  390. const media_BaseURL = dash["baseUrl"].replace(/&/g, '&amp;');
  391. const media_SegmentBase_indexRange = dash["SegmentBase"]["indexRange"];
  392. const media_SegmentBase_Initialization = dash["SegmentBase"]["Initialization"];
  393. const mediaType = media_mimeType.split('/')[0];
  394. let media_type_params = '';
  395. if (mediaType === 'video') {
  396. const media_frameRate = dash.frameRate;
  397. const media_sar = dash["sar"];
  398. const media_width = dash.width;
  399. const media_height = dash.height;
  400. media_type_params = `height='${media_height}' width='${media_width}' frameRate='${media_frameRate}' sar='${media_sar}'`;
  401. } else if (mediaType === 'audio') {
  402. for (const key in this.vod_audio_id) {
  403. if (qnid === key) {
  404. const audioSamplingRate = this.vod_audio_id[key];
  405. media_type_params = `numChannels='2' sampleRate='${audioSamplingRate}'`;
  406. }
  407. }
  408. }
  409. qnid += '_' + codecid;
  410. return `<AdaptationSet lang="chi">
  411. <ContentComponent contentType="${mediaType}"/>
  412. <Representation id="${qnid}" bandwidth="${media_bandwidth}" codecs="${media_codecs}" mimeType="${media_mimeType}" ${media_type_params} startWithSAP="${media_startWithSAP}">
  413. <BaseURL>${media_BaseURL}</BaseURL>
  414. <SegmentBase indexRange="${media_SegmentBase_indexRange}">
  415. <Initialization range="${media_SegmentBase_Initialization}"/>
  416. </SegmentBase>
  417. </Representation>
  418. </AdaptationSet>`;
  419. } catch (e) {
  420. // Handle exceptions here
  421. }
  422. }
  423. getDash(ja, videoList, audioList) {
  424. const duration = ja.data.dash["duration"];
  425. const minBufferTime = ja.data.dash["minBufferTime"];
  426. return `<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" type="static" mediaPresentationDuration="PT${duration}S" minBufferTime="PT${minBufferTime}S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">
  427. <Period duration="PT${duration}S" start="PT0S">
  428. ${videoList}
  429. ${audioList}
  430. </Period>
  431. </MPD>`;
  432. }
  433. async proxy(segments, headers) {
  434. let what = segments[0];
  435. let url = Utils.base64Decode(segments[1]);
  436. if (what === 'dash') {
  437. const ids = url.split('+');
  438. const aid = ids[0];
  439. const cid = ids[1];
  440. const str5 = ids[2];
  441. const urls = this.apiUrl + `/x/player/playurl?avid=${aid}&cid=${cid}&qn=${str5}&fnval=4048&fourk=1`;
  442. let videoList = '';
  443. let audioList = '';
  444. let content = await this.fetch(urls, null, headers);
  445. let resp = JSON.parse(content)
  446. const dash = resp.data.dash;
  447. const video = dash.video;
  448. const audio = dash.audio;
  449. for (let i = 0; i < video.length; i++) {
  450. // if (i > 0) continue; // 只取一个
  451. const dashjson = video[i];
  452. if (dashjson.id.toString() === str5) {
  453. videoList += this.getDashMedia(dashjson);
  454. }
  455. }
  456. for (let i = 0; i < audio.length; i++) {
  457. // if (i > 0) continue;
  458. const ajson = audio[i];
  459. for (const key in this.vod_audio_id) {
  460. if (ajson.id.toString() === key) {
  461. audioList += this.getDashMedia(ajson);
  462. }
  463. }
  464. }
  465. let mpd = this.getDash(resp, videoList, audioList);
  466. return JSON.stringify({
  467. code: 200,
  468. content: mpd,
  469. headers: {
  470. 'Content-Type': 'application/dash+xml',
  471. },
  472. });
  473. }
  474. return JSON.stringify({
  475. code: 500,
  476. content: '',
  477. });
  478. }
  479. }
  480. let spider = new BilibiliSpider()
  481. async function init(cfg) {
  482. await spider.init(cfg)
  483. }
  484. async function home(filter) {
  485. return await spider.home(filter)
  486. }
  487. async function homeVod() {
  488. return await spider.homeVod()
  489. }
  490. async function category(tid, pg, filter, extend) {
  491. return await spider.category(tid, pg, filter, extend)
  492. }
  493. async function detail(id) {
  494. return await spider.detail(id)
  495. }
  496. async function play(flag, id, flags) {
  497. return await spider.play(flag, id, flags)
  498. }
  499. async function search(wd, quick) {
  500. return await spider.search(wd, quick)
  501. }
  502. async function proxy(segments, headers) {
  503. return await spider.proxy(segments, headers)
  504. }
  505. export function __jsEvalReturn() {
  506. return {
  507. init: init,
  508. home: home,
  509. homeVod: homeVod,
  510. category: category,
  511. detail: detail,
  512. play: play,
  513. search: search,
  514. proxy: proxy
  515. };
  516. }
  517. export {spider}