Shikimori Review Rate & Sorting

Shows user's compatibility rating next to their review on shikimori. Sorting by relevance is available with double-clicking on review's category title

// ==UserScript==
// @name            Shikimori Review Rate & Sorting
// @namespace       shikimori
// @description     Shows user's compatibility rating next to their review on shikimori. Sorting by relevance is available with double-clicking on review's category title
// @author          NightLancerX
// @match           https://shikimori.me/*
// @match           https://shikimori.org/*
// @match           https://shikimori.one/*
// @version         9.21
// @license         CC-BY-NC-SA
// @grant           none
// @noframes
// ==/UserScript==

(function(){
  let userAnimeList = {}, domain = '.me', count = 0;

  const ADJUST_SCORE = true; //apply bonus score for common tens in rates and penalty for difference for more than 2 points

  function makeUserAnimeListRequest()
  {
    return new Promise(function (resolve, reject) {
      let xhr = new XMLHttpRequest();
      let url = document.querySelector('.menu-dropdown.profile a').href + "/list/anime?mylist=completed";
      xhr.responseType = 'document';
      xhr.timeout = 5000;
      xhr.withCredentials = true;
      xhr.open('GET', url, true);

      xhr.onload = function (){
        xhr.response.querySelector('[class="entries"]').childNodes.forEach(e => {
          if (!!e.lastChild.textContent.match(/(Сериал|TV Series)/) && e.querySelector('[data-field="score"]').textContent > 0)
            userAnimeList[e.attributes["data-target_id"].value] = e.querySelector('[data-field="score"]').textContent});
        resolve(true);
      };

      xhr.send();
    });
  }

  function makeAnimeListRequest(comment)
  {
    return new Promise(function (resolve, reject) {
      let xhr = new XMLHttpRequest();
      let name = comment.querySelector('.name').href.split('/').pop();
      let url = 'https://shikimori'+ domain +'/api/users/' + name + '/anime_rates?limit=5000';
      xhr.responseType = 'json';
      xhr.timeout = 5000;
      xhr.open('GET', url, true);

      comment.classList.add("done");

      xhr.onload = function (){
        if (this.status == 200){
          let total = 0, common = 0, score = 0, rate = 0,
              animeList = this.response;


          for(let i=0; i<animeList.length; i++){
            if (animeList[i].status == "completed" && animeList[i].anime.kind == "tv"){
              ++total;
              if (userAnimeList[animeList[i].anime.id]>0){
                ++common;

                let animeId = animeList[i].anime.id,
                    comparedScore = animeList[i].score,
                    userScore = userAnimeList[animeId];

                if (comparedScore>0){
                  if (comparedScore == userScore){
                    ++score;
                    if (ADJUST_SCORE && userScore==10) score+=0.5;
                  }

                  if (ADJUST_SCORE){
                    if (userScore==10 && comparedScore<8) score-=userScore-comparedScore-2
                    else if (Math.abs(userScore-comparedScore)>2) score-=(Math.abs(userScore-comparedScore)-2)/2;
                  }
                }
              };
            }
          }
          rate = (score/common * 100).toFixed(2);

          let colorArray = ['grey','red','orange','green']; //0,1,2,3
          let r,c,t; //
          r = (rate>=30)?3:(rate<15)?1:2;
          c = (common>=40)?3:(common<20)?1:2;
          t = (total>=100)?3:(total<50)?1:2;

          if (r==1 || c==1 || t==1){
            if (r>1) r=0;
            if (c>1) c=0;
            if (t>1) t=0;
          }

          let similarity = t+c +(common/Object.keys(userAnimeList).length*1.5) +(r*c*t>0?rate/30*3:0);
          comment.closest('article').setAttribute('similarity', similarity.toFixed(2));

          comment.getElementsByClassName('name')[0].insertAdjacentHTML('beforeend','<span style="color:' + colorArray[r] + '"> '   +rate+'%</span>');
          comment.getElementsByClassName('name')[0].insertAdjacentHTML('beforeend','<span style="color:' + colorArray[c] + '"> ['  +common+ ']</span>');
          comment.getElementsByClassName('name')[0].insertAdjacentHTML('beforeend','<span style="color:' + colorArray[t] + '"> T: '+total+'</span>');
          console.log(count, ':', decodeURI(name).replace('+','_'), '-', total, '-', rate,'%');

          resolve(true);
        }
        else{
          reject(this);
        }
      };

      xhr.onerror = xhr.ontimeout = function(){
        reject(this);
      };

      xhr.send();
    });
  }

  function sleep(ms){
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function dblclick(){
    let reviews = document.querySelectorAll('.is-active article'), p = document.querySelector('.content-node.is-active'), footer = p.lastChild;
    Object.values(reviews).sort((a,b)=>b?.getAttribute('similarity')-a?.getAttribute('similarity')).forEach(e => p.insertBefore(e, footer));
    console.log('Sorted');
  }

  function check(){
    let navigation = document.querySelector('.navigation-container');
    if (!navigation || navigation.classList.contains('done')) return;
    navigation.addEventListener('dblclick', dblclick);
    navigation.classList.add("done");
  }

  window.onload = async function()
  {
    domain = location.hostname.match(/\.me|\.org|\.one/)?.[0];
    //if (domain != ".me") location.href = location.href.replace(domain,'.me'); //auto-redirect on .me until further updates.
    await makeUserAnimeListRequest();
    await sleep(1000);

    async function process(){
      check();

      let comment = document.querySelector('.review-details:not(.done)');
      if (comment){
        console.log(++count, ':', comment);
        let done = false, c = 3, delay = 100;

        while (!done && c--){
          try{
            await makeAnimeListRequest(comment);
            done = true, delay = 100;
          }
          catch(err){
            console.error(count,':', err.status, err);
            switch (err.status){
              case 0:
              case 429: delay = 1000; break;
              case 403:
              case 404: done = true;  break;
            }
            if (done==true || c==0){
              comment.getElementsByClassName('name')[0].insertAdjacentHTML('beforeend','<span style="color:grey">   --% T: ---</span>');
              comment.closest('article').setAttribute('similarity', 0);
            }
          };
          await sleep(delay);
        }
      }
      setTimeout(process, 0);
    };

    process();
  }
}) ();