Greasy Fork

Greasy Fork is available in English.

bilibili 本地评论存储插件

存储用户于视频/专栏/动态下的评论到本地。其用途是保存评论区令人感动的瞬间,以方便日后查看,研究和学习。本脚本魔改自bilibili 枝网查重 API 版,并遵循其AGPL-3.0 License。在这里对其作者表示敬意,salute😎

当前为 2021-08-17 提交的版本,查看 最新版本

// ==UserScript==
// @name         bilibili 本地评论存储插件
// @namespace    https://github.com/FakeServerBot/userscript
// @supportURL   https://github.com/FakeServerBot/userscript/issues
// @version      0.4
// @description  存储用户于视频/专栏/动态下的评论到本地。其用途是保存评论区令人感动的瞬间,以方便日后查看,研究和学习。本脚本魔改自bilibili 枝网查重 API 版,并遵循其AGPL-3.0 License。在这里对其作者表示敬意,salute😎
// @author       Sparanoid,FakeServerBot
// @license      AGPL
// @compatible   chrome 80 or later
// @match        https://*.bilibili.com/*
// @icon         https://emoji.beeimg.com/🎯/mozilla
// @require http://greasyfork.icu/scripts/420061-super-gm-setvalue-and-gm-getvalue-greasyfork-mirror-js/code/Super_GM_setValue_and_GM_getValue_greasyfork_mirrorjs.js?version=890160
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @run-at       document-start
// ==/UserScript==

window.addEventListener('load', () => {
  let front_label_list = ['可爱型', '见多识广型', '真情实感型', '科普型', '技术型', '狗头型', '小作文型'];
  let end_label_list = ['B站用户', '歪嘴龙王', '话唠', '大佬', '萌新', '宅男', '宅女', '现充', '狗头', '酷盖', 'DD'];
  const DEBUG = true;
  const NAMESPACE = 'bilibili-local-marker';
  var show_details_dict = {}; // Use this to decide when to hide past comments
  console.log(`${NAMESPACE} loaded`);
  // remove_all();
  function debug(description = '', msg = '', force = false) {
    if (DEBUG || force) {
      console.log(`${NAMESPACE}: ${description}`, msg)
    }
  }

  function formatDate(timestamp) {
    let date = timestamp.toString().length === 10 ? new Date(+timestamp * 1000) : new Date(+timestamp);
    return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
  }

  function rateColor(percent) {
    return `hsl(${226 + parseInt(percent/100* (360 - 226))}, 100%, 50%)`;
  }

  function set_value(uid, string, timestamp, comment_link) {
    // first, see if you can get this value
    var uid_values = get_value(uid);
    // then, append new string to this dict...
    uid_values.push([timestamp, string, comment_link]);
    // Set the new values...
    GM_SuperValue.set(uid, uid_values);
  }

  function is_marked(uid){
      let result = get_value(uid);
      if (result.length === 0){
          return false;
      }else{
          return true;
      }
  }

  function get_value(uid){
    return GM_SuperValue.get (uid, []);
  }

  function downloadTextFile(text, name) {
      const a = document.createElement('a');
      const type = name.split(".").pop();
      a.href = URL.createObjectURL( new Blob([text], { type:`text/${type === "txt" ? "plain" : type}` }) );
      a.download = name;
      a.click();
  }

  function format_as_json(){
      // get all uid
      let arrayOfKeys = GM_listValues();
      // empty dict
      var res = {}
      for (let key of arrayOfKeys){
          let tmp_res = get_value(key);
          if (tmp_res.length !== 0){
              res[key] = tmp_res;
          }
      }
      let json_res = JSON.stringify(res);
      downloadTextFile(json_res, `marker_file.json`);
      return res;
  }

  function load_json(input){
      var count = 0;
      for (const [key, value] of Object.entries(input)) {
          if (get_value(key).length === 0){
            // console.log(value);
            GM_SuperValue.set(key, value);
            count += 1;
          }
          // console.log(key, value);
      }
      return count;
  }
    function remove_all(){
      // get all uid
      let arrayOfKeys = GM_listValues();
      for (let key of arrayOfKeys){
          remove_all_values_of_target_uid(key);
      }
    }

  function remove_all_values_of_target_uid(uid){
      GM_SuperValue.set(uid, []);
  }

  function change_to_mark_status(LocalMarkerEl, uid){
      var count = get_value(uid).length;
      if (count >= 10) {
          count = 10;
          LocalMarkerEl.innerHTML = `10+ 已标记👀`;
      } else {
          LocalMarkerEl.innerHTML = `${count} 已标记👀`;
      }
      LocalMarkerEl.style.color = rateColor(count * 10);
  }

  function cancel_mark_status(LocalMarkerEl){
      LocalMarkerEl.innerHTML = '标记👀';
      LocalMarkerEl.style.color = '#99A2AA';
  }

  function get_show_details_dict_value(id){
      if (show_details_dict.hasOwnProperty(id)){
          return show_details_dict[id];
      } else {
          show_details_dict[id] = true;
          return show_details_dict[id];
      }
  }
  function remove_https_and_split(url){
      return url.replace(/^https?\:\/\//i, "").split('/');
  }
  function handle_target_link(comment_id, uid_card){
      // first, determine where are we..
      let url = window.location.href;
      let split_list = remove_https_and_split(url);
      if (split_list[0] === 'www.bilibili.com' && split_list[1] === 'video'){
          // at video
          var split_val = split_list[2];
          // remove ?
          split_val = split_val.split('?')[0];
          // remove %
          split_val = split_val.split('%')[0];
          return `https://www.bilibili.com/video/${split_val}/#reply${comment_id}`;
      } else if (split_list[0] === 'www.bilibili.com' && split_list[1] === 'read'){
          // at read
          var split_val_0 = split_list[2];
          // remove ?
          split_val_0 = split_val_0.split('?')[0];
          // remove %
          split_val_0 = split_val_0.split('%')[0];
          return `https://www.bilibili.com/read/${split_val_0}/#reply${comment_id}`;
      } else if (split_list[0] === 'space.bilibili.com'){
          // at dynamic
          let dynamic_id = uid_card.closest(".card").getAttribute('data-did');
          return `https://t.bilibili.com/${dynamic_id}/#reply${comment_id}`;
      } else if (split_list[0] === 't.bilibili.com'){
          // at t.bilibili...
          var split_val_2 = split_list[1];
          // remove ?
          split_val_2 = split_val_2.split('?')[0];
          // remove %
          split_val_2 = split_val_2.split('%')[0];
          return `https://t.bilibili.com/${split_val_2}/#reply${comment_id}`;
      } else {
          // unknown position...
          return '不支持提供链接😨';
      }
  }

  function attachEl(item) {
    let injectWrap = item.querySelector('.con .info');

    // .text - comment content
    // .text-con - reply content
    let content = item.querySelector('.con .text') || item.querySelector('.reply-con .text-con');
    let id = item.dataset.id;
    // save user uid
    let uid_card = item.querySelector('.con .name') || item.querySelector('.reply-con .name');
    // debug('current_page', remove_https_and_split(window.location.href));
    let uid = uid_card.getAttribute('data-usercard-mid')
    let comment_link = handle_target_link(id, uid_card);
    // console.log(uid);
    // Simple way to attach element on replies initially loaded with comment
    // which wouldn't trigger mutation inside observeComments
    let replies = item.querySelectorAll('.con .reply-box .reply-item');
    if (replies.length > 0) {
      [...replies].map(reply => {
        attachEl(reply);
      });
    }

    if (injectWrap.querySelector('.LocalMarker')) {
      debug('already loaded for this comment');
    } else {
        // Insert LocalMarker check button
        let LocalMarkerEl = document.createElement('span');
        LocalMarkerEl.style.userSelect = 'none';
        LocalMarkerEl.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
        if (is_marked(uid) === true){
            change_to_mark_status(LocalMarkerEl, uid);
        } else {
            cancel_mark_status(LocalMarkerEl);
        }
        LocalMarkerEl.addEventListener('click', e => {
          let contentPrepared = '';
          // Copy meme icons alt text
          for (let node of content.childNodes.values()) {
              if (node.nodeType === 3) {
                  contentPrepared += node.textContent;
              } else if (node.nodeName === 'IMG' && node.nodeType === 1) {
                  contentPrepared += node.alt;
              } else if (node.nodeName === 'BR' && node.nodeType === 1) {
                  contentPrepared += '\n';
              } else if (node.nodeName === 'A' && node.nodeType === 1 && node.classList.contains('comment-jump-url')) {
                  contentPrepared += node.href.replace(/https?:\/\/www\.bilibili\.com\/video\//, '');
              } else {
                  contentPrepared += node.innerText;
              }
            }
          // Need regex to stripe `回复 @username  :`
          let contentProcessed = contentPrepared.replace(/回复 @.*:/, '');
          debug('content processed', contentProcessed);
          // debug('dynamic_id', dynamic_id);
          // remove_all_values_of_target_uid(uid);
          set_value(uid, contentProcessed, Date.now(), comment_link);
          change_to_mark_status(LocalMarkerEl, uid);
          if (injectWrap.querySelector('.LocalMarker-result')) {
               injectWrap.querySelector('.LocalMarker-result').remove();
           }
           if (!injectWrap.querySelector('.LocalMarker-marker-label')) {
             add_chunk(uid, injectWrap);
           }
           show_details_dict[id] = true;
      }, false);

      injectWrap.append(LocalMarkerEl);

      let show_message_button = document.createElement('span');
      show_message_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
      show_message_button.innerHTML = '历史评论';
      show_message_button.style.userSelect = 'none';
      show_message_button.addEventListener('click', e => {
          if (get_show_details_dict_value(id)){
                show_details_dict[id] = false;
                let message_list = get_value(uid);
                // debug('get value:', message_list);
                let resultContent = ``;
                if (message_list.length === 0){
                    resultContent = `无标记历史!`;
                }
                for (const [index, sig_meg] of message_list.entries()){
                  // debug('sig_meg', sig_meg);
                    //${formatDate(sig_meg[0])}
                  resultContent += `<p><b style="color: #222222">[${index + 1}]</b> <span style="color: #222222">${sig_meg[1]}</span></p><p><a href="${sig_meg[2]}" target="_blank">原评论链接:${sig_meg[2]}</a></p><p>-- 标记于: <span style="color: #FB7299">${formatDate(sig_meg[0])}</span></p>`;
                  if (index < message_list.length-1){
                  resultContent += `<p class="" style="margin: 6px;"></p>`;
                  }
                }
                // Insert result
                let resultWrap = document.createElement('div');

                resultWrap.style.position = 'relative';
                resultWrap.style.padding = '.5rem';
                resultWrap.style.margin = '.5rem 0';
                resultWrap.style.background = 'hsla(0, 0%, 50%, .1)';
                resultWrap.style.borderRadius = '4px';
                // resultWrap.style.lineHeight = '10px';
                // resultWrap.style.whiteSpace = 'pre';
                resultWrap.style.wordBreak = 'break-word';
                resultWrap.style.width = '90%';
                resultWrap.classList.add('LocalMarker-result');
                resultWrap.innerHTML = resultContent;

                let download_button = document.createElement('span');
              download_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
              download_button.innerHTML = '下载记录';
              download_button.style.userSelect = 'none';
              download_button.setAttribute('title', '导出所有人的历史记录到本地');
              download_button.addEventListener('click', e => {
                  format_as_json();
              }, false);
              // resultWrap.append(download_button);

              let upload_input = document.createElement('input');
              upload_input.setAttribute('type', 'file');
              upload_input.style.color = 'transparent';
              upload_input.setAttribute('title', '从本地导入历史记录json文件');
              upload_input.addEventListener('change', function() {
                  var GetFile = new FileReader();
                  GetFile.onload=function(){
                      const json_obj = JSON.parse(GetFile.result);
                      // console.log(json_obj);
                      let load_counts = load_json(json_obj);
                      alert(`${load_counts}条额外的标记信息已成功加载`);
                  }
                  GetFile.readAsText(this.files[0]);
              });
              // resultWrap.append(upload_input);
                // Remove previous result if exists
                if (injectWrap.querySelector('.LocalMarker-result')) {
                  injectWrap.querySelector('.LocalMarker-result').remove();
                }
                injectWrap.append(resultWrap);
              } else {
                show_details_dict[id] = true;
                if (injectWrap.querySelector('.LocalMarker-result')) {
                    injectWrap.querySelector('.LocalMarker-result').remove();
                }
            }
      }, false);
      injectWrap.append(show_message_button);

      let remove_button = document.createElement('span');

      remove_button.classList.add('LocalMarker', 'btn-hover', 'btn-highlight');
      remove_button.innerHTML = '不再标记';
      remove_button.style.userSelect = 'none';
      remove_button.addEventListener('click', e => {
         remove_all_values_of_target_uid(uid);
          if (injectWrap.querySelector('.LocalMarker-result')) {
             injectWrap.querySelector('.LocalMarker-result').remove();
         }
         if (injectWrap.querySelector('.LocalMarker-marker-label')) {
             injectWrap.querySelector('.LocalMarker-marker-label').remove();
         }
         show_details_dict[id] = true;
         cancel_mark_status(LocalMarkerEl);
      }, false);
      injectWrap.append(remove_button);
      if (is_marked(uid) === true){
          add_chunk(uid, injectWrap);
      }
    }
  }
  function add_chunk(uid, injectWrap){
      let out_div = document.createElement('span');
      out_div.classList.add('LocalMarker-marker-label');
      out_div.style.userSelect = 'none';
      out_div.style.disabled = true;
      var select_front_list = add_default_list(uid + '_front', front_label_list);
      out_div.append(select_front_list);
      var select_end_list = add_default_list(uid + '_end', end_label_list);
      out_div.append(select_end_list);
      injectWrap.append(out_div);
  }
  function add_default_list(list_id, array){
      //Create and append select list
      var selectList = document.createElement("select");
      selectList.id = "mySelect";
      // Set the new values...
      let default_value = GM_SuperValue.get(list_id, array[0]);
      //Create and append the options
      for (var i = 0; i < array.length; i++) {
          var option = document.createElement("option");
          option.value = array[i];
          option.text = array[i];
          if (default_value === option.text){
              option.selected = "selected";
          }
          selectList.appendChild(option);
      }
      selectList.style.userSelect = 'none';
      selectList.addEventListener("change", function() {
          GM_SuperValue.set(list_id, selectList.value);
      });
      return selectList;
  }
  function add_selective_list(list_name, injectWrap) {
      if (get_value(list_name).length === 0){
          GM_SuperValue.set(list_name, [[], Date.now(), "this is the empty front list"]);
      }
      let array = get_value(list_name)[0];
      var input_chart = document.createElement("input");
      input_chart.setAttribute('type', 'text');
      input_chart.setAttribute('list', 'mySelect');
      //Create and append select list
      var selectList = document.createElement("datalist");
      selectList.id = "mySelect";
      //Create and append the options
      for (var i = 0; i < array.length; i++) {
          var option = document.createElement("option");
          option.value = array[i];
          option.text = array[i];
          if (0 === i){
              option.selected = "selected";
          }
          selectList.appendChild(option);
      }
      input_chart.addEventListener("change", function() {
          input_chart.style.width = `${1.3 * Math.max(input_chart.value.length, 4)}em`;
          array = get_value(list_name)[0];
          array.push(input_chart.value);
          // console.log(get_value(list_name));
          GM_SuperValue.set(list_name, [array, Date.now(), "this is front list"]);
          console.log(get_value(list_name));
      });
      injectWrap.append(input_chart);
      injectWrap.append(selectList);
      return selectList;
  }
  function observeComments(wrapper) {
    // .comment-list - general list for video, zhuanlan, and dongtai
    // .reply-box - replies attached to specific comment
    let commentLists = wrapper ? wrapper.querySelectorAll('.comment-list, .reply-box') : document.querySelectorAll('.comment-list, .reply-box');

    if (commentLists) {

      [...commentLists].map(commentList => {

        // Directly attach elements for pure static server side rendered comments
        // and replies list. Used by zhuanlan posts with reply hash in URL.
        // TODO: need a better solution
        [...commentList.querySelectorAll('.list-item, .reply-item')].map(item => {
          attachEl(item);
        });

        const observer = new MutationObserver((mutationsList, observer) => {

          for (const mutation of mutationsList) {

            if (mutation.type === 'childList') {

              // debug('observed mutations', [...mutation.addedNodes].length);

              [...mutation.addedNodes].map(item => {
                attachEl(item);

                // Check if the comment has replies
                // I check replies here to make sure I can disable subtree option for
                // MutationObserver to get better performance.
                let replies = item.querySelectorAll('.con .reply-box .reply-item');

                if (replies.length > 0) {
                  observeComments(item)
                  // debug(item.dataset.id + ' has rendered reply(ies)', replies.length);
                }
              })
            }
          }
        });
        observer.observe(commentList, { attributes: false, childList: true, subtree: false });
      });
    }
  }

  // .bb-comment loads directly for zhuanlan post. So load it directly
  observeComments();

  // .bb-comment loads dynamcially for dontai and videos. So observe it first
  const wrapperObserver = new MutationObserver((mutationsList, observer) => {

    for (const mutation of mutationsList) {

      if (mutation.type === 'childList') {

        [...mutation.addedNodes].map(item => {
          // debug('mutation wrapper added', item);

          if (item.classList?.contains('bb-comment')) {
            debug('mutation wrapper added (found target)', item);

            observeComments(item);

            // Stop observing
            // TODO: when observer stops it won't work for dynamic homepage ie. https://space.bilibili.com/703007996/dynamic
            // so disable it here. This may have some performance impact on low-end machines.
            // wrapperObserver.disconnect();
          }
        })
      }
    }
  });
  wrapperObserver.observe(document.body, { attributes: false, childList: true, subtree: true });

}, false);