// ==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();
}
}) ();