Vecorder

直播间内容记录 https://github.com/Xinrea/Vecorder

当前为 2022-07-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Vecorder
  3. // @namespace https://www.joi-club.cn/
  4. // @version 0.56
  5. // @description 直播间内容记录 https://github.com/Xinrea/Vecorder
  6. // @author Xinrea
  7. // @license MIT
  8. // @match https://live.bilibili.com/*
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_deleteValue
  12. // @require https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js
  13. // @require https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js
  14. // @run-at document-end
  15. // ==/UserScript==
  16.  
  17. function vlog(msg) {
  18. console.log("[Vecorder]" + msg);
  19. }
  20.  
  21. function p(msg) {
  22. return {
  23. time: new Date().getTime(),
  24. content: msg,
  25. };
  26. }
  27.  
  28. var dbname = "vdb" + getRoomID();
  29.  
  30. var db = JSON.parse(GM_getValue(dbname, "[]"));
  31. var Option = JSON.parse(GM_getValue("vop", '{"reltime":false,"toffset":0}'));
  32.  
  33. function nindexOf(n) {
  34. for (let i in db) {
  35. if (!db[i].del && db[i].name == n) return i;
  36. }
  37. return -1;
  38. }
  39.  
  40. function tindexOf(id, t) {
  41. for (let i in db[id].lives) {
  42. if (!db[id].lives[i].del && db[id].lives[i].title == t) return i;
  43. }
  44. return -1;
  45. }
  46.  
  47. function gc() {
  48. for (let i = db.length - 1; i >= 0; i--) {
  49. if (db[i].del) {
  50. db.splice(i, 1);
  51. continue;
  52. }
  53. for (let j = db[i].lives.length - 1; j >= 0; j--) {
  54. if (db[i].lives[j].del) {
  55. db[i].lives.splice(j, 1);
  56. continue;
  57. }
  58. }
  59. }
  60. GM_setValue(dbname, JSON.stringify(db));
  61. }
  62.  
  63. function addPoint(t, msg) {
  64. let ltime = t * 1000;
  65. if (ltime == 0) return;
  66. let name = getName();
  67. let title = getRoomTitle();
  68. let id = nindexOf(name);
  69. if (id == -1) {
  70. db.push({
  71. name: name,
  72. link: getLink(),
  73. del: false,
  74. lives: [
  75. {
  76. title: title,
  77. time: ltime,
  78. del: false,
  79. points: [p(msg)],
  80. },
  81. ],
  82. });
  83. } else {
  84. let lid = tindexOf(id, title);
  85. if (lid == -1) {
  86. db[id].lives.push({
  87. title: title,
  88. time: ltime,
  89. points: [p(msg)],
  90. });
  91. } else {
  92. db[id].lives[lid].points.push(p(msg));
  93. }
  94. }
  95. GM_setValue(dbname, JSON.stringify(db));
  96. }
  97.  
  98. function getMsg(body) {
  99. var vars = body.split("&");
  100. for (var i = 0; i < vars.length; i++) {
  101. var pair = vars[i].split("=");
  102. if (pair[0] == "msg") {
  103. return decodeURI(pair[1]);
  104. }
  105. }
  106. return false;
  107. }
  108.  
  109. function getName() {
  110. let resp = $.ajax({
  111. url: "https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id="+getRoomID(),
  112. async: false});
  113. return resp.responseJSON.data.anchor_info.base_info.uname;
  114. }
  115.  
  116. function getLink() {
  117. return "https://space.bilibili.com/"+$.ajax({
  118. url: "https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id="+getRoomID(),
  119. async: false}).responseJSON.data.room_info.uid;
  120. }
  121.  
  122. function getRoomTitle() {
  123. return $(
  124. "#head-info-vm > div > div > div.upper-row > div.left-ctnr.left-header-area > div.live-title > div > div"
  125. ).text();
  126. }
  127.  
  128. // 根据当前地址获取直播间ID
  129. function getRoomID() {
  130. var url_text = window.location.href + "";
  131. var i = url_text.indexOf("roomid=");
  132. var m = 0;
  133. if (i != -1) m = url_text.slice(i + 7);
  134. else m = url_text.slice(url_text.indexOf(".com/") + 5);
  135. let rid = parseInt(m);
  136. console.log("Try1:",m);
  137. if (isNaN(rid)) {
  138. m = url_text.slice(url_text.indexOf(".com/blanc/") + 11);
  139. console.log("Try2:"+m);
  140. rid = parseInt(m);
  141. }
  142. return rid; //获取当前房间号
  143. }
  144.  
  145. function tryAddPoint(msg) {
  146. // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID}
  147. console.log(msg, getRoomID());
  148. $.ajax({
  149. url:
  150. "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + getRoomID(),
  151. async: true,
  152. success: function (resp) {
  153. let t = 0;
  154. if (resp.data.live_status != 1) t = 0;
  155. else t = resp.data.live_time;
  156. addPoint(t, msg);
  157. },
  158. });
  159. }
  160.  
  161. let toggle = false;
  162.  
  163. waitForKeyElements(
  164. "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative",
  165. (n) => {
  166. let recordInput = $(
  167. '<textarea id="vecorder-input" placeholder="在此记录当前直播内容,回车确认" rows="1"></textarea>'
  168. );
  169. recordInput.attr(
  170. "style",
  171. `
  172. height: 12px;
  173. width: 266px;
  174. resize: none;
  175. outline: none;
  176. background-color: #fff;
  177. border-radius: 4px;
  178. padding: 8px 8px 10px;
  179. color: #333;
  180. overflow: hidden;
  181. font-size: 10px;
  182. line-height: 14px;
  183. display: flex;
  184. border: 1px solid #e9eaec;
  185. margin-top: 3px;
  186. `
  187. );
  188. recordInput.bind("keypress", function (event) {
  189. if (event.keyCode == "13") {
  190. window.event.returnValue = false;
  191. console.log("Enter detected");
  192. tryAddPoint($("#vecorder-input").val());
  193. $("#vecorder-input").val("");
  194. }
  195. });
  196. n.after(recordInput);
  197. }
  198. );
  199.  
  200. waitForKeyElements(
  201. "#chat-control-panel-vm > div > div.bottom-actions.p-relative > div",
  202. (n) => {
  203. // Resize original input
  204. $(
  205. "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative > div:nth-child(2) > textarea"
  206. ).css("height", "36px");
  207. $(
  208. "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative > div.medal-section"
  209. ).css("height", "36px");
  210. $("#control-panel-ctnr-box > div.bottom-actions.p-relative").css(
  211. "margin-top",
  212. "4px"
  213. );
  214. // Setup recordButton
  215. let recordBtn = $('<button><span class="txt">记录</span></button>');
  216. recordBtn.attr(
  217. "style",
  218. "font-family: sans-serif;\
  219. text-transform: none;\
  220. position: relative;\
  221. box-sizing: border-box;\
  222. line-height: 1;\
  223. margin: 0;\
  224. margin-left: 3px;\
  225. padding: 6px 12px;\
  226. border: 0;\
  227. cursor: pointer;\
  228. outline: none;\
  229. overflow: hidden;\
  230. background-color: #23ade5;\
  231. color: #fff;\
  232. border-radius: 4px;\
  233. min-width: 40px;\
  234. height: 24px;\
  235. font-size: 12px;"
  236. );
  237. recordBtn.hover(
  238. function () {
  239. if (!toggle) recordBtn.css("background-color", "#58bae2");
  240. },
  241. function () {
  242. if (!toggle) recordBtn.css("background-color", "#23ade5");
  243. }
  244. );
  245. recordBtn.click(function () {
  246. if (toggle) {
  247. $("#vPanel").remove();
  248. gc();
  249. toggle = false;
  250. $(this).css("background-color", "#58bae2");
  251. return;
  252. }
  253. let panel = $(
  254. '<div id="vPanel"><div id="vArrow"></div><p style="font-size:20px;font-weight:bold;margin:0px;" class="vName">🍊直播笔记</p></div>'
  255. );
  256. let contentList = dbToListview();
  257. panel.append(contentList);
  258. let clearBtn = $('<button><span class="txt">清空</span></button>');
  259. clearBtn.attr(
  260. "style",
  261. "font-family: sans-serif;\
  262. text-transform: none;\
  263. position: relative;\
  264. box-sizing: border-box;\
  265. line-height: 1;\
  266. margin: 0;\
  267. margin-left: 3px;\
  268. padding: 6px 12px;\
  269. border: 0;\
  270. cursor: pointer;\
  271. outline: none;\
  272. overflow: hidden;\
  273. background-color: #23ade5;\
  274. color: #fff;\
  275. border-radius: 4px;\
  276. min-width: 40px;\
  277. height: 24px;\
  278. font-size: 12px;"
  279. );
  280. clearBtn.hover(
  281. function () {
  282. clearBtn.css("background-color", "#58bae2");
  283. },
  284. function () {
  285. clearBtn.css("background-color", "#23ade5");
  286. }
  287. );
  288. clearBtn.click(function () {
  289. contentList.empty();
  290. db = [];
  291. GM_deleteValue(dbname);
  292. });
  293. panel.append(clearBtn);
  294. let closeBtn = $(
  295. '<a style="position:absolute;right:7px;top:5px;font-size:20px;" class="vName">&times;</a>'
  296. );
  297. closeBtn.click(function () {
  298. panel.remove();
  299. gc();
  300. toggle = false;
  301. recordBtn.css("background-color", "#23ade5");
  302. });
  303. panel.append(closeBtn);
  304. let timeop = $(`<hr style="border:0;height:1px;background-color:#58bae2;margin-top:10px;margin-bottom:10px;"/><div id="timeop">\
  305. <div><input type="checkbox" id="reltime" value="false" style="vertical-align:middle;margin-right:5px;"/><label for="reltime" class="vName" style="vertical-align:middle;">按相对时间导出</label></div>\
  306. <div style="margin-top:10px;"><label for="toffset" class="vName" style="vertical-align:middle;">时间偏移(秒):</label><input type="number" id="toffset" value="${Option.toffset}" style="vertical-align:middle;width:35px;outline-color:#23ade5;"/></div>\
  307. </div>`);
  308. panel.append(timeop);
  309. $("#chat-control-panel-vm > div").append(panel);
  310. if (Option.reltime) {
  311. $("#reltime").attr("checked", true);
  312. }
  313. $("#reltime").change(function () {
  314. Option.reltime = $(this).prop("checked");
  315. GM_setValue("vop", JSON.stringify(Option));
  316. });
  317. $("#toffset").change(function () {
  318. Option.toffset = $(this).val();
  319. GM_setValue("vop", JSON.stringify(Option));
  320. });
  321. $(this).css("background-color", "#0d749e");
  322. toggle = true;
  323. });
  324. n.append(recordBtn);
  325.  
  326. let styles = $("<style></style>");
  327. styles.text(
  328. "#vPanel {\
  329. line-height: 1.15;\
  330. font-size: 12px;\
  331. font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE8F6F96C59ED1!important;\
  332. display: block;\
  333. box-sizing: border-box;\
  334. background: #fff;\
  335. border: 1px solid #e9eaec;\
  336. border-radius: 8px;\
  337. box-shadow: 0 6px 12px 0 rgba(106,115,133,.22);\
  338. animation: scale-in-ease cubic-bezier(.22,.58,.12,.98) .4s;\
  339. padding: 16px;\
  340. position: absolute;\
  341. right: 0px;\
  342. bottom: 50px;\
  343. z-index: 999;\
  344. transform-origin: right bottom;\
  345. }\
  346. #vPanel ul {\
  347. list-style-type: none;\
  348. padding-inline-start: 0px;\
  349. color: #666;\
  350. }\
  351. #vPanel li {\
  352. margin-top: 10px;\
  353. white-space: nowrap;\
  354. }\
  355. .vName {\
  356. color: #23ade5;\
  357. cursor: pointer;\
  358. }\
  359. #vArrow {\
  360. line-height: 1.15;\
  361. font-size: 12px;\
  362. font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE8F6F96C59ED1!important;\
  363. position: absolute;\
  364. top: 100%;\
  365. width: 0;\
  366. height: 0;\
  367. border-left: 4px solid transparent;\
  368. border-right: 4px solid transparent;\
  369. border-top: 8px solid #fff;\
  370. right: 25px;\
  371. }\
  372. "
  373. );
  374. n.append(styles);
  375. }
  376. );
  377.  
  378. function dbToListview() {
  379. let urlObject = window.URL || window.webkitURL || window;
  380. let content = $("<ul></ul>");
  381. for (let i in db) {
  382. let list = $("<li></li>");
  383. if (db[i].del) {
  384. continue;
  385. }
  386. let innerlist = $("<ul></ul>");
  387. for (let j in db[i].lives) {
  388. if (db[i].lives[j].del) continue;
  389. let item = $(
  390. "<li>" +
  391. `[${moment(db[i].lives[j].time).format("YYYY/MM/DD")}]` +
  392. db[i].lives[j].title +
  393. "[" +
  394. db[i].lives[j].points.length +
  395. "]" +
  396. "</li>"
  397. );
  398. let ep = $('<a class="vName" style="font-weight:bold;">[导出]</a>');
  399. let cx = $(
  400. '<a class="vName" style="color:red;font-weight:bold;">[删除]</a>'
  401. );
  402. ep.click(function () {
  403. exportRaw(
  404. db[i].lives[j],
  405. db[i].name,
  406. `[${db[i].name}][${db[i].lives[j].title}][${moment(
  407. db[i].lives[j].time
  408. ).format("YYYY-MM-DD")}]`
  409. );
  410. });
  411. cx.click(function () {
  412. if (db[i].lives.length == 1) {
  413. db[i].del = true;
  414. item.remove();
  415. list.remove();
  416. } else {
  417. db[i].lives[j].del = true;
  418. item.remove();
  419. }
  420. GM_setValue(dbname, JSON.stringify(db));
  421. });
  422. item.append(ep);
  423. item.prepend(cx);
  424. innerlist.append(item);
  425. }
  426. list.append(innerlist);
  427. content.append(list);
  428. }
  429. return content;
  430. }
  431.  
  432. function exportRaw(live, v, fname) {
  433. var urlObject = window.URL || window.webkitURL || window;
  434. var export_blob = new Blob([rawToString(live, v)]);
  435. var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
  436. save_link.href = urlObject.createObjectURL(export_blob);
  437. save_link.download = fname;
  438. save_link.click();
  439. }
  440.  
  441. function rawToString(live, v) {
  442. let r =
  443. "# 由Vecorder自动生成,不妨关注下可爱的@轴伊Joi_Channel:https://space.bilibili.com/61639371/\n";
  444. r += `# ${v} \n`;
  445. r += `# ${live.title} - 直播开始时间:${moment(live.time).format(
  446. "YYYY-MM-DD HH:mm:ss"
  447. )}\n\n`;
  448. for (let i in live.points) {
  449. if (!Option.reltime)
  450. r += `[${moment(live.points[i].time)
  451. .add(Option.toffset, "seconds")
  452. .format("HH:mm:ss")}] ${live.points[i].content}\n`;
  453. else {
  454. let seconds =
  455. moment(live.points[i].time).diff(moment(live.time), "second") +
  456. Number(Option.toffset);
  457. let minutes = Math.floor(seconds / 60);
  458. let hours = Math.floor(minutes / 60);
  459. seconds = seconds % 60;
  460. minutes = minutes % 60;
  461. r += `[${f(hours)}:${f(minutes)}:${f(seconds)}] ${
  462. live.points[i].content
  463. }\n`;
  464. }
  465. }
  466. return r;
  467. }
  468.  
  469. function f(num) {
  470. if (String(num).length > 2) return num;
  471. return (Array(2).join(0) + num).slice(-2);
  472. }
  473.  
  474. function waitForKeyElements(
  475. selectorTxt /* Required: The jQuery selector string that
  476. specifies the desired element(s).
  477. */,
  478. actionFunction /* Required: The code to run when elements are
  479. found. It is passed a jNode to the matched
  480. element.
  481. */,
  482. bWaitOnce /* Optional: If false, will continue to scan for
  483. new elements even after the first match is
  484. found.
  485. */,
  486. iframeSelector /* Optional: If set, identifies the iframe to
  487. search.
  488. */
  489. ) {
  490. var targetNodes, btargetsFound;
  491.  
  492. if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
  493. else targetNodes = $(iframeSelector).contents().find(selectorTxt);
  494.  
  495. if (targetNodes && targetNodes.length > 0) {
  496. btargetsFound = true;
  497. /*--- Found target node(s). Go through each and act if they
  498. are new.
  499. */
  500. targetNodes.each(function () {
  501. var jThis = $(this);
  502. var alreadyFound = jThis.data("alreadyFound") || false;
  503.  
  504. if (!alreadyFound) {
  505. //--- Call the payload function.
  506. var cancelFound = actionFunction(jThis);
  507. if (cancelFound) btargetsFound = false;
  508. else jThis.data("alreadyFound", true);
  509. }
  510. });
  511. } else {
  512. btargetsFound = false;
  513. }
  514.  
  515. //--- Get the timer-control variable for this selector.
  516. var controlObj = waitForKeyElements.controlObj || {};
  517. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  518. var timeControl = controlObj[controlKey];
  519.  
  520. //--- Now set or clear the timer as appropriate.
  521. if (btargetsFound && bWaitOnce && timeControl) {
  522. //--- The only condition where we need to clear the timer.
  523. clearInterval(timeControl);
  524. delete controlObj[controlKey];
  525. } else {
  526. //--- Set a timer, if needed.
  527. if (!timeControl) {
  528. timeControl = setInterval(function () {
  529. waitForKeyElements(
  530. selectorTxt,
  531. actionFunction,
  532. bWaitOnce,
  533. iframeSelector
  534. );
  535. }, 300);
  536. controlObj[controlKey] = timeControl;
  537. }
  538. }
  539. waitForKeyElements.controlObj = controlObj;
  540. }