Vecorder

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址