bilibili.js 19 KB

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