Average Reviews Calculator for MAL

Transform MAL into Rotten Tomatoes

As of 2021-01-01. See the latest version.

// ==UserScript==
// @name         Average Reviews Calculator for MAL
// @namespace    Transform MAL into Rotten Tomatoes
// @version      0.4
// @description  Transform MAL into Rotten Tomatoes
// @author       Only_Brad
// @include      /^https:\/\/myanimelist\.net\/(anime|manga)\/[\d]+\/.*\/reviews/
// @icon         https://www.google.com/s2/favicons?domain=myanimelist.net
// @run-at       document-end
// @grant        GM_addStyle
// ==/UserScript==
(async function() {
  const
    REVIEWS_TAB_SELECTOR = "#content > table > tbody > tr > td:nth-child(2) > div.js-scrollfix-bottom-rel",
    SCORE_TABLES_SELECTOR = "table.borderClass",
    OVERALL_RATING_SELECTOR = "tbody > tr > td:nth-child(2) > strong",
    SEPERATOR_SELECTOR = ".reviews-horiznav-nav-sort-block";

  const
    currentUrl = window.location.href,
    firstPageUrl = getFirstPageUrl(),
    currentPage = getCurrentPage(),
    score = setPlaceholderScore();

  showAverageOverallScore();

  function setPlaceholderScore() {
    const separator = document.querySelector(SEPERATOR_SELECTOR);
    const score = document.createElement("div");
    score.style = "padding: 15px 0 15px 10px;";
    score.textContent = "Calculating total average review score...";
    separator.insertAdjacentElement("afterend", score);
    return score;
  }

  async function showAverageOverallScore() {
    const overallScores = await getAllOverallScore();
    const average = overallScores.reduce((acc, val) => acc + val, 0) / overallScores.length;
    score.textContent = "Average score: " + average.toFixed(2);
  }

  async function getAllOverallScore() {
    const NB_OF_SIMULTANOUS_DL = 5;
    const overallScores = [];

    let httpRequests = [];
    let i = 1;
    let loopCount = 1;

    while (true) {
      for (i; i <= NB_OF_SIMULTANOUS_DL * loopCount; i++) {
        httpRequests.push(getDocument(i));
      }
      const documents = await Promise.all(httpRequests);

      for (const currentDocument of documents) {
        if (hasReviews(currentDocument))
          overallScores.push(getOverallScores(currentDocument));
        else
          return overallScores.flat();
      }

      httpRequests = [];
      loopCount++;
    }
  }

  async function getDocument(pageNumber) {
    let currentDocument;
    if (pageNumber === currentPage) currentDocument = document;
    else {
      const url = pageNumber === 1 ? firstPageUrl : `${firstPageUrl}?p=${pageNumber}`;
      const response = await fetch(url);
      const html = await response.text();
      currentDocument = new DOMParser().parseFromString(html, "text/html");
    }
    return currentDocument;
  }

  function getOverallScores(document) {
    const reviewsTab = document.querySelector(REVIEWS_TAB_SELECTOR);
    const scoreTables = reviewsTab.querySelectorAll(SCORE_TABLES_SELECTOR);
    return [...scoreTables].map(table => parseInt(table.querySelector(OVERALL_RATING_SELECTOR).textContent));
  }

  function hasReviews(document) {
    const reviewsTab = document.querySelector(REVIEWS_TAB_SELECTOR);
    const scoreTables = reviewsTab.querySelectorAll(SCORE_TABLES_SELECTOR);
    return scoreTables.length > 0;
  }

  function getFirstPageUrl() {
    const PAGE_REGEX = /\?p=[\d]+/;
    const search = currentUrl.search(PAGE_REGEX);

    if (search !== -1) return currentUrl.substring(0, search);
    return currentUrl;
  }

  function getCurrentPage() {
    const PAGE_REGEX = /\?p=[\d]+/;
    const match = currentUrl.match(PAGE_REGEX);

    if (match) return parseInt(match[1]);
    else return 1;
  }
})();