Vecorder

直播间内容记录

目前為 2020-12-18 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Vecorder
  3. // @namespace https://www.joi-club.cn/
  4. // @version 0.53
  5. // @description 直播间内容记录
  6. // @author Xinrea
  7. // @match https://live.bilibili.com/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_deleteValue
  11. // @require https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js
  12. // @require https://gf.qytechs.cn/scripts/407985-ajax-hook/code/Ajax-hook.js?version=832614
  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]['+getLiveStatus()+']'+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(msg) {
  64. let ltime = getLiveStartTime()*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. 'title':title,
  76. 'time':ltime,
  77. 'del':false,
  78. 'points':[p(msg)]
  79. }]
  80. })
  81. } else {
  82. let lid = tindexOf(id,title)
  83. if (lid == -1) {
  84. db[id].lives.push({
  85. 'title':title,
  86. 'time':ltime,
  87. 'points':[p(msg)]
  88. })
  89. } else {
  90. db[id].lives[lid].points.push(p(msg))
  91. }
  92. }
  93. GM_setValue(dbname,JSON.stringify(db))
  94. }
  95.  
  96. vlog('Init')
  97. ah.proxy({
  98. onRequest: (config, handler) => {
  99. if (config.url === 'https://api.live.bilibili.com/msg/send') {
  100. let danmu = getMsg(config.body)
  101. if (danmu.charAt(danmu.length-1) == '】' && danmu.charAt(0) == '【') {
  102. vlog('Add Point: '+danmu.substring(1,danmu.length-1))
  103. addPoint(danmu.substring(1,danmu.length-1))
  104. handler.resolve({
  105. config: config,
  106. status: 200,
  107. headers: {'content-type': 'application/json; charset=utf-8'},
  108. response: '{"code":0,"data":[],"message":"","msg":"recorded"}'
  109. })
  110. } else {
  111. vlog('Normal Danmu')
  112. handler.next(config);
  113. }
  114. } else {
  115. handler.next(config);
  116. }
  117. },
  118. onError: (err, handler) => {
  119. handler.next(err)
  120. },
  121. onResponse: (response, handler) => {
  122. handler.next(response)
  123. }
  124. })
  125.  
  126. function getLiveStatus() {
  127. return $('#head-info-vm > div > div > div.room-info-upper-row.p-relative > div.normal-mode > div:nth-child(1) > h1 > span.live-status-label.live-skin-highlight-text.live-skin-highlight-border.v-middle.preparing').text()
  128. }
  129.  
  130. function getMsg(body)
  131. {
  132. var vars = body.split("&");
  133. for (var i=0;i<vars.length;i++) {
  134. var pair = vars[i].split("=");
  135. if(pair[0] == 'msg'){return decodeURI(pair[1]);}
  136. }
  137. return(false);
  138. }
  139.  
  140. function getName(){
  141. return $('#head-info-vm > div > div > div.room-info-down-row > a.room-owner-username.live-skin-normal-a-text.dp-i-block.v-middle').text()
  142. }
  143.  
  144. function getLink() {
  145. return $('#head-info-vm > div > div > div.room-info-down-row > a.room-owner-username.live-skin-normal-a-text.dp-i-block.v-middle').attr('href')
  146. }
  147.  
  148. function getRoomTitle() {
  149. return $('#head-info-vm > div > div > div.room-info-upper-row.p-relative > div.normal-mode > div:nth-child(1) > h1 > span.title-length-limit.live-skin-main-text.v-middle.dp-i-block').text()
  150. }
  151.  
  152. // 根据当前地址获取直播间ID
  153. function getRoomID() {
  154. var url_text = window.location.href+"";
  155. var i = url_text.indexOf("roomid=")
  156. var m = 0
  157. if (i != -1) m = url_text.slice(i+7);
  158. else m = url_text.slice(url_text.indexOf(".com/")+5);
  159. return parseInt(m);//获取当前房间号
  160. }
  161.  
  162. function getLiveStartTime() {
  163. // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID}
  164. let resp = JSON.parse($.ajax({
  165. url: "https://api.live.bilibili.com/room/v1/Room/room_init?id="+getRoomID(),
  166. async: false
  167. }).responseText)
  168. if (resp.data.live_status != 1) return 0
  169. else return resp.data.live_time
  170. }
  171.  
  172. let toggle = false
  173.  
  174. waitForKeyElements('#chat-control-panel-vm > div > div.bottom-actions.p-relative > div',(n)=>{
  175. let recordBtn = $('<button><span class="txt">记录</span></button>')
  176. recordBtn.attr('style','font-family: sans-serif;\
  177. text-transform: none;\
  178. position: relative;\
  179. box-sizing: border-box;\
  180. line-height: 1;\
  181. margin: 0;\
  182. margin-left: 3px;\
  183. padding: 6px 12px;\
  184. border: 0;\
  185. cursor: pointer;\
  186. outline: none;\
  187. overflow: hidden;\
  188. background-color: #23ade5;\
  189. color: #fff;\
  190. border-radius: 4px;\
  191. min-width: 40px;\
  192. height: 24px;\
  193. font-size: 12px;')
  194. recordBtn.hover(function(){
  195. if (!toggle) recordBtn.css('background-color','#58bae2')
  196. },
  197. function() {
  198. if (!toggle) recordBtn.css('background-color','#23ade5')
  199. })
  200. recordBtn.click(function(){
  201. if (toggle) {
  202. $('#vPanel').remove()
  203. gc()
  204. toggle = false
  205. $(this).css('background-color','#58bae2')
  206. return
  207. }
  208. let panel = $('<div id="vPanel"><div id="vArrow"></div><p style="font-size:20px;font-weight:bold;margin:0px;" class="vName">🍊直播笔记</p></div>')
  209. let contentList = dbToListview()
  210. panel.append(contentList)
  211. let clearBtn = $('<button><span class="txt">清空</span></button>')
  212. clearBtn.attr('style','font-family: sans-serif;\
  213. text-transform: none;\
  214. position: relative;\
  215. box-sizing: border-box;\
  216. line-height: 1;\
  217. margin: 0;\
  218. margin-left: 3px;\
  219. padding: 6px 12px;\
  220. border: 0;\
  221. cursor: pointer;\
  222. outline: none;\
  223. overflow: hidden;\
  224. background-color: #23ade5;\
  225. color: #fff;\
  226. border-radius: 4px;\
  227. min-width: 40px;\
  228. height: 24px;\
  229. font-size: 12px;')
  230. clearBtn.hover(function(){
  231. clearBtn.css('background-color','#58bae2')
  232. },
  233. function() {
  234. clearBtn.css('background-color','#23ade5')
  235. })
  236. clearBtn.click(function(){
  237. contentList.empty()
  238. db = []
  239. GM_deleteValue(dbname)
  240. })
  241. panel.append(clearBtn)
  242. let closeBtn = $('<a style="position:absolute;right:7px;top:5px;font-size:20px;" class="vName">&times;</a>')
  243. closeBtn.click(function(){
  244. panel.remove()
  245. gc()
  246. toggle = false
  247. recordBtn.css('background-color','#23ade5')
  248. })
  249. panel.append(closeBtn)
  250. let timeop = $(`<hr style="border:0;height:1px;background-color:#58bae2;margin-top:10px;margin-bottom:10px;"/><div id="timeop">\
  251. <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>\
  252. <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>\
  253. </div>`)
  254. panel.append(timeop)
  255. $('#chat-control-panel-vm > div').append(panel)
  256. if (Option.reltime) {
  257. $('#reltime').attr('checked',true)
  258. }
  259. $('#reltime').change(function(){
  260. Option.reltime = $(this).prop("checked")
  261. GM_setValue('vop',JSON.stringify(Option))
  262. })
  263. $('#toffset').change(function(){
  264. Option.toffset = $(this).val()
  265. GM_setValue('vop',JSON.stringify(Option))
  266. })
  267. $(this).css('background-color','#0d749e')
  268. toggle = true
  269. })
  270. n.append(recordBtn)
  271.  
  272. let styles = $('<style></style>')
  273. styles.text(
  274. '#vPanel {\
  275. line-height: 1.15;\
  276. font-size: 12px;\
  277. font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE\8F6F\96C5\9ED1!important;\
  278. display: block;\
  279. box-sizing: border-box;\
  280. background: #fff;\
  281. border: 1px solid #e9eaec;\
  282. border-radius: 8px;\
  283. box-shadow: 0 6px 12px 0 rgba(106,115,133,.22);\
  284. animation: scale-in-ease cubic-bezier(.22,.58,.12,.98) .4s;\
  285. padding: 16px;\
  286. position: absolute;\
  287. right: 0px;\
  288. bottom: 50px;\
  289. z-index: 999;\
  290. transform-origin: right bottom;\
  291. }\
  292. #vPanel ul {\
  293. list-style-type: none;\
  294. padding-inline-start: 0px;\
  295. color: #666;\
  296. }\
  297. #vPanel li {\
  298. margin-top: 10px;\
  299. white-space: nowrap;\
  300. }\
  301. .vName {\
  302. color: #23ade5;\
  303. cursor: pointer;\
  304. }\
  305. #vArrow {\
  306. line-height: 1.15;\
  307. font-size: 12px;\
  308. font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE\8F6F\96C5\9ED1!important;\
  309. position: absolute;\
  310. top: 100%;\
  311. width: 0;\
  312. height: 0;\
  313. border-left: 4px solid transparent;\
  314. border-right: 4px solid transparent;\
  315. border-top: 8px solid #fff;\
  316. right: 25px;\
  317. }\
  318. '
  319. )
  320. n.append(styles)
  321. })
  322.  
  323. function dbToListview() {
  324. let urlObject = window.URL || window.webkitURL || window;
  325. let content = $('<ul></ul>')
  326. for (let i in db) {
  327. let list = $('<li></li>')
  328. if (db[i].del) {
  329. continue
  330. }
  331. let innerlist = $('<ul></ul>')
  332. for (let j in db[i].lives) {
  333. if (db[i].lives[j].del) continue
  334. let item = $('<li>'+`[${moment(db[i].lives[j].time).format('YYYY/MM/DD')}]`+db[i].lives[j].title+'['+db[i].lives[j].points.length+']'+'</li>')
  335. let ep = $('<a class="vName" style="font-weight:bold;">[导出]</a>')
  336. let cx = $('<a class="vName" style="color:red;font-weight:bold;">[删除]</a>')
  337. ep.click(function(){
  338. exportRaw(db[i].lives[j],db[i].name,`[${db[i].name}][${db[i].lives[j].title}][${moment(db[i].lives[j].time).format('YYYY-MM-DD')}]`)
  339. })
  340. cx.click(function(){
  341. if (db[i].lives.length == 1) {
  342. db[i].del = true
  343. item.remove()
  344. list.remove()
  345. } else {
  346. db[i].lives[j].del = true
  347. item.remove()
  348. }
  349. GM_setValue(dbname,JSON.stringify(db))
  350. })
  351. item.append(ep)
  352. item.prepend(cx)
  353. innerlist.append(item)
  354. }
  355. list.append(innerlist)
  356. content.append(list)
  357. }
  358. return content
  359. }
  360.  
  361. function exportRaw(live, v, fname) {
  362. var urlObject = window.URL || window.webkitURL || window;
  363. var export_blob = new Blob([rawToString(live,v)]);
  364. var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
  365. save_link.href = urlObject.createObjectURL(export_blob);
  366. save_link.download = fname;
  367. save_link.click();
  368. }
  369.  
  370. function rawToString(live,v){
  371. let r = "# 由Vecorder自动生成,不妨关注下可爱的@轴伊Joi_Channel:https://space.bilibili.com/61639371/\n"
  372. r += `# ${v} \n`
  373. r += `# ${live.title} - 直播开始时间:${moment(live.time).format('YYYY-MM-DD HH:mm:ss')}\n\n`
  374. for (let i in live.points) {
  375. if (!Option.reltime) r += `[${moment(live.points[i].time).add(Option.toffset,'seconds').format('HH:mm:ss')}] ${live.points[i].content}\n`
  376. else {
  377. let seconds = moment(live.points[i].time).diff(moment(live.time),'second') + Number(Option.toffset)
  378. let minutes = Math.floor(seconds/60)
  379. let hours = Math.floor(minutes/60)
  380. seconds = seconds%60
  381. minutes = minutes%60
  382. r += `[${f(hours)}:${f(minutes)}:${f(seconds)}] ${live.points[i].content}\n`
  383. }
  384. }
  385. return r
  386. }
  387.  
  388. function f(num) {
  389. if(String(num).length > 2) return num;
  390. return (Array(2).join(0) + num).slice(-2);
  391. }
  392.  
  393. function waitForKeyElements (
  394. selectorTxt, /* Required: The jQuery selector string that
  395. specifies the desired element(s).
  396. */
  397. actionFunction, /* Required: The code to run when elements are
  398. found. It is passed a jNode to the matched
  399. element.
  400. */
  401. bWaitOnce, /* Optional: If false, will continue to scan for
  402. new elements even after the first match is
  403. found.
  404. */
  405. iframeSelector /* Optional: If set, identifies the iframe to
  406. search.
  407. */
  408. ) {
  409. var targetNodes, btargetsFound;
  410.  
  411. if (typeof iframeSelector == "undefined")
  412. targetNodes = $(selectorTxt);
  413. else
  414. targetNodes = $(iframeSelector).contents ()
  415. .find (selectorTxt);
  416.  
  417. if (targetNodes && targetNodes.length > 0) {
  418. btargetsFound = true;
  419. /*--- Found target node(s). Go through each and act if they
  420. are new.
  421. */
  422. targetNodes.each ( function () {
  423. var jThis = $(this);
  424. var alreadyFound = jThis.data ('alreadyFound') || false;
  425.  
  426. if (!alreadyFound) {
  427. //--- Call the payload function.
  428. var cancelFound = actionFunction (jThis);
  429. if (cancelFound)
  430. btargetsFound = false;
  431. else
  432. jThis.data ('alreadyFound', true);
  433. }
  434. } );
  435. }
  436. else {
  437. btargetsFound = false;
  438. }
  439.  
  440. //--- Get the timer-control variable for this selector.
  441. var controlObj = waitForKeyElements.controlObj || {};
  442. var controlKey = selectorTxt.replace (/[^\w]/g, "_");
  443. var timeControl = controlObj [controlKey];
  444.  
  445. //--- Now set or clear the timer as appropriate.
  446. if (btargetsFound && bWaitOnce && timeControl) {
  447. //--- The only condition where we need to clear the timer.
  448. clearInterval (timeControl);
  449. delete controlObj [controlKey]
  450. }
  451. else {
  452. //--- Set a timer, if needed.
  453. if ( ! timeControl) {
  454. timeControl = setInterval ( function () {
  455. waitForKeyElements ( selectorTxt,
  456. actionFunction,
  457. bWaitOnce,
  458. iframeSelector
  459. );
  460. },
  461. 300
  462. );
  463. controlObj [controlKey] = timeControl;
  464. }
  465. }
  466. waitForKeyElements.controlObj = controlObj;
  467. }

QingJ © 2025

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