Bangumi shared book collections

共读 @ Bangumi。Ref: https://github.com/bangumi/scripts/tree/b0113743743dba35accb28e9b7b9da8cbbea6952/yonjar#%E7%94%A8%E6%88%B7%E8%AF%A6%E6%83%85%E7%88%AC%E5%8F%96

目前为 2022-08-12 提交的版本。查看 最新版本

// ==UserScript==
// @name        Bangumi shared book collections
// @namespace   http://tampermonkey.net/
// @version     1.0.1
// @author      txfs19260817
// @source      https://github.com/txfs19260817/bangumi-shared-book-collections
// @license     WTFPL
// @icon        https://bangumi.tv/img/favicon.ico
// @match       http*://*.bangumi.tv/
// @match       http*://*.bgm.tv/
// @match       http*://*.chii.in/
// @grant       GM.xmlHttpRequest
// @run-at      document-end
// @description 共读 @ Bangumi。Ref: https://github.com/bangumi/scripts/tree/b0113743743dba35accb28e9b7b9da8cbbea6952/yonjar#%E7%94%A8%E6%88%B7%E8%AF%A6%E6%83%85%E7%88%AC%E5%8F%96
// ==/UserScript==


/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
var __webpack_exports__ = {};

;// CONCATENATED MODULE: ./src/TabItem.ts
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

class TabItem {
  constructor() {
    _defineProperty(this, "states", {
      loading: {
        text: "⏳",
        cursor: "wait"
      },
      done: {
        text: "共读",
        cursor: "pointer"
      }
    });

    _defineProperty(this, "li", document.createElement("li"));

    _defineProperty(this, "a", document.createElement("a"));

    // initialize
    this.applyState(this.states.loading);
    this.li.appendChild(this.a);
    document.getElementById('timelineTabs').appendChild(this.li);
  }

  applyState(state) {
    this.a.text = state.text;
    this.a.style.cursor = state.cursor;
  }

  loaded() {
    this.applyState(this.states.done);
  }

}
;// CONCATENATED MODULE: ./src/utils.ts
const parseTimestamp = s => {
  var _s$match, _s$match2, _s$match3;

  if (!s.includes("ago")) {
    return new Date(s);
  }

  const now = new Date();
  const d = ((_s$match = s.match(/(\d+)d/i)) === null || _s$match === void 0 ? void 0 : _s$match[1]) || "0";
  const h = ((_s$match2 = s.match(/(\d+)h/i)) === null || _s$match2 === void 0 ? void 0 : _s$match2[1]) || "0";
  const m = ((_s$match3 = s.match(/(\d+)m/i)) === null || _s$match3 === void 0 ? void 0 : _s$match3[1]) || "0";
  now.setDate(now.getDate() - +d);
  now.setHours(now.getHours() - +h);
  now.setMinutes(now.getMinutes() - +m);
  return now;
};

const getUID = () => {
  return document.querySelector("#headerNeue2 > div > div.idBadgerNeue > a").href.split("user/")[1];
};

const fetchHTMLDocument = function (url) {
  let fetchMethod = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "GET";
  return fetch(url, {
    method: fetchMethod,
    credentials: "include"
  }).then(r => r.text(), err => Promise.reject(err)).then(t => {
    const parser = new DOMParser();
    return parser.parseFromString(t, "text/html");
  });
};

const defaultAvatarElem = userUrl => {
  const avatar = document.createElement("span");
  const userAvatarAnchor = document.createElement("a");
  const userAvatarSpan = document.createElement("span");
  userAvatarSpan.classList.add("avatarNeue", "avatarReSize40", "ll");
  userAvatarSpan.style.backgroundImage = 'url("//lain.bgm.tv/pic/user/l/icon.jpg")';
  userAvatarAnchor.href = userUrl;
  userAvatarAnchor.appendChild(userAvatarSpan);
  avatar.classList.add("avatar");
  avatar.appendChild(userAvatarAnchor);
  return avatar;
};

const MAX_PAGES = 5;

const fetchComments = async uid => {
  const url = `${location.origin}/book/list/${uid}/collect`;
  const firstPage = await fetchHTMLDocument(url); // TODO: latest pages

  const maxPageNum = Math.min(MAX_PAGES, firstPage.getElementsByClassName('page_inner')[0].childElementCount - 1);
  const pageURLs = Array.from({
    length: maxPageNum
  }, (_, i) => i + 1).map(i => `${url}?page=${i}`);
  pageURLs.shift(); // [2, ..., maxPageNum]

  const pages = await Promise.all(pageURLs.map(u => fetchHTMLDocument(u)));
  pages.unshift(firstPage);
  const subjectUrls = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.firstElementChild.href + "/comments"));
  const subjectCovers = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.getElementsByTagName('img')[0].src));
  const commentPageDOMs = await Promise.all(subjectUrls.map(u => fetchHTMLDocument(u)));
  const commentElements = commentPageDOMs.map(p => Array.from(p.getElementsByClassName("text")));
  const avatarElements = commentPageDOMs.flatMap(p => Array.from(p.querySelectorAll('#comment_box > div > .avatar')));
  const uid2avatar = new Map();
  avatarElements.forEach(e => {
    // adjust avatar span class
    e.firstElementChild.classList.replace("rr", "ll");
    e.firstElementChild.classList.replace("avatarSize32", "avatarReSize40");
    e.firstElementChild.style.marginLeft = '6px'; // parse username from href

    const username = e.href.split('/').at(-1);
    uid2avatar.set(username, e);
  });
  const subjectTitles = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.getElementsByTagName('h3')[0].textContent.trim()));
  const data = commentElements.flatMap((cs, i) => {
    return cs.map(c => {
      const userAnchor = c.firstElementChild;
      return {
        subjectUrl: subjectUrls[i],
        subjectTitle: subjectTitles[i],
        subjectCover: subjectCovers[i],
        userAvatarElement: uid2avatar.get(userAnchor.href.split('/').at(-1)) ?? defaultAvatarElem(userAnchor.href),
        userUrl: userAnchor.href,
        username: userAnchor.text,
        date: parseTimestamp(c.getElementsByTagName('small')[0].textContent.slice(2)),
        comment: c.getElementsByTagName('p')[0].textContent
      };
    }).filter(c => !c.userUrl.includes(uid));
  });
  data.sort((a, b) => +b.date - +a.date);
  return data;
};

const commentDataToTLList = data => {
  const ul = document.createElement("ul");
  const lis = data.map(d => {
    const li = document.createElement("li");
    li.classList.add("clearit", "tml_item"); // avatar

    li.appendChild(d.userAvatarElement); // info

    const info = document.createElement("span");
    info.classList.add("clearit", "info"); // info - cover

    const coverAnchor = document.createElement("a");
    const coverImg = document.createElement("img");
    coverImg.classList.add("rr");
    coverImg.src = d.subjectCover;
    coverImg.height = 48;
    coverImg.width = 48;
    coverAnchor.appendChild(coverImg);
    info.appendChild(coverAnchor); // info - username

    const userAnchor = document.createElement("a");
    userAnchor.href = d.userUrl;
    userAnchor.textContent = d.username;
    userAnchor.classList.add("l");
    info.appendChild(userAnchor);
    const connector = document.createElement("span");
    connector.textContent = " 读过 ";
    info.appendChild(connector); // info - subject

    const subjectAnchor = document.createElement("a");
    subjectAnchor.href = d.subjectUrl;
    subjectAnchor.textContent = d.subjectTitle;
    subjectAnchor.classList.add("l");
    info.appendChild(subjectAnchor); // info - comment

    const collectInfo = document.createElement("div");
    collectInfo.classList.add("collectInfo");
    const quoteDiv = document.createElement("div");
    quoteDiv.classList.add("quote");
    const quoteQ = document.createElement("q");
    quoteQ.textContent = d.comment;
    quoteDiv.appendChild(quoteQ);
    collectInfo.appendChild(quoteDiv);
    info.appendChild(collectInfo); // info - date

    const dateP = document.createElement("p");
    dateP.classList.add("date");
    dateP.textContent = d.date.toLocaleString();
    info.appendChild(dateP); // info done

    li.appendChild(info);
    return li;
  });
  lis.forEach(l => {
    ul.appendChild(l);
  });
  return ul;
};


;// CONCATENATED MODULE: ./src/index.ts



async function main() {
  const uid = getUID();
  const tabItem = new TabItem();
  fetchComments(uid).then(r => {
    tabItem.loaded();
    const tl = document.getElementById("timeline"); // TODO: pagination?
    // FIXME: ugly access

    tabItem.a.onclick = function () {
      tabItem.a.classList.add("focus");
      tl.replaceChildren(commentDataToTLList(r.slice(0, 100)));
    };
  });
}

main().catch(e => {
  console.log(e);
});
/******/ })()
;

QingJ © 2025

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