NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Shiki Rating
// @namespace http://shikimori.one/
// @version 2.7.5
// @description Добавляет рейтинг произведений на основе оценок пользователей шики (также при наведении не учитываются низкие оценки)
// @author Chortowod
// @match *://shikimori.org/*
// @match *://shikimori.one/*
// @match *://shikimori.me/*
// @match *://shiki.one/*
// @match *://shikimori.io/*
// @icon https://www.google.com/s2/favicons?domain=shikimori.one
// @license MIT
// @require https://gist.githubusercontent.com/arantius/3123124/raw/grant-none-shim.js
// @copyright 2026, Chortowod (https://openuserjs.org/users/Chortowod)
// @updateURL https://openuserjs.org/meta/Chortowod/Shiki_Rating.meta.js
// @downloadURL https://openuserjs.org/install/Chortowod/Shiki_Rating.user.js
// @require https://gist.githubusercontent.com/Chortowod/814b010c68fc97e5f900df47bf79059c/raw/chtw_settings.js?v1
// @grant none
// ==/UserScript==
let settings = new ChtwSettings('chtwRating', '<a target="_blank" href="https://openuserjs.org/scripts/Chortowod/Shiki_Rating">Рейтинг шикимори</a>');
let debug;
function initSettings() {
settings.createOption('isDebug', 'Отладка', false);
settings.createOption('lowest', 'Минимальный порог', 4, 'number');
settings.createOption('starColor', 'Цвет звезд', '#a22123', 'color');
settings.createOption('textColor', 'Цвет надписи', '#7b8084', 'color');
debug = settings.getOption('isDebug');
}
function log(message) {
if (debug) {
console.log('Shiki Rating:');
console.log(message);
}
}
function getLocale() {
return document.querySelector('body').getAttribute('data-locale');
}
function getLabelData() {
return getLocale() === 'ru' ?
{"0":"Невыносимо","1":"Хуже некуда","2":"Ужасно","3":"Очень плохо","4":"Плохо","5":"Более-менее","6":"Нормально","7":"Хорошо","8":"Отлично","9":"Великолепно","10":"Эпик вин!"} :
{"0":"Unbearable","1":"Worst Ever","2":"Terrible","3":"Very Bad","4":"Bad","5":"So-so","6":"Fine","7":"Good","8":"Excellent","9":"Great","10":"Masterpiece!"};
}
function removeLastClass(domElement) {
let classes = domElement.classList;
classes.remove(classes.item(classes.length - 1));
}
function createElementFromHTML(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
}
function calculateShikiRating(scoreData, lowestRate = 0) {
let sumScore = 0;
let totalCount = 0;
for (let i = 0; i < scoreData.length; i++) {
if (scoreData[i][0] >= lowestRate) {
sumScore += parseInt(scoreData[i][1]) * scoreData[i][0];
totalCount += parseInt(scoreData[i][1]);
}
}
log(sumScore);
log(totalCount);
let shikiScore = sumScore / totalCount;
if (lowestRate) log('Оценка, не учитывая меньше '+lowestRate+': '+shikiScore);
else log('Оценка: '+shikiScore);
if (!shikiScore) return false;
else return {'score': shikiScore.toFixed(2), 'scoreMath': Math.round(shikiScore), 'count': totalCount};
}
function scoreOnHover(eventType, score, total) {
let shikiScoreElement = document.getElementById("shiki-score");
let dataScore = shikiScoreElement.getAttribute(score);
shikiScoreElement.addEventListener(eventType, function( event ) {
shikiScoreElement.querySelector('.text-score .score-value').innerHTML = dataScore;
let starElement = shikiScoreElement.querySelector("div.stars-container > div.stars.score");
removeLastClass(starElement);
starElement.classList.add("score-" + Math.round(dataScore));
document.querySelector('.score-counter > strong').textContent = shikiScoreElement.getAttribute(total);
shikiScoreElement.querySelector("div.text-score > div.score-notice").textContent = getLabelData()[Math.round(dataScore)];
}, false);
}
function appendShikiRating() {
'use strict';
let currentPath = window.location.pathname.substring(0, 7);
if (!(currentPath === "/animes" || currentPath === "/mangas" || currentPath === "/ranobe") || !document.querySelector(".c-info-right")) {
log('Неподходящая страница');
return;
}
if (document.querySelector("#shiki-score") !== null) {
log('Уже был создано');
scoreOnHover('mouseover', 'data-score-low', 'data-count-low');
scoreOnHover('mouseout', 'data-score', 'data-count');
return;
}
let newShikiRate, malRate;
if (document.querySelector(".scores > .b-rate") === null) {
// создаем контейнер с оценками вручную, если его нет
let rateFromZero = createElementFromHTML('<div class="block" itemprop="aggregateRating" itemscope="" itemtype="http://schema.org/AggregateRating"><div class="subheadline m5">Рейтинг</div><div class="scores"><div class="b-rate" id="shiki-score"><div class="stars-container"><div class="hoverable-trigger"></div><div class="stars score score-9" style="color: rgb(162, 33, 35);"></div><div class="stars hover"></div><div class="stars background"></div></div><div class="text-score"><div class="score-value score-9"></div><div class="score-notice"></div></div></div><p class="score-counter" style="text-align: center; color: rgb(123, 128, 132);"></p></div></div>');
document.querySelector(".c-info-right").prepend(rateFromZero);
newShikiRate = document.getElementById('shiki-score');
}
else {
// создаем контейнер с оценками по подобию уже имеющегося
malRate = document.querySelector(".scores > .b-rate");
malRate.setAttribute('id', 'mal-score');
newShikiRate = malRate.cloneNode(true);
newShikiRate.setAttribute('id', 'shiki-score');
document.querySelector(".scores").appendChild(newShikiRate);
}
// подгружаем оценки
let scoreDataJson = document.querySelector("#rates_scores_stats").getAttribute("data-stats");
let scoreData = JSON.parse(scoreDataJson);
// если оценок не оказалось
if (!scoreData || scoreData.length === 0) {
newShikiRate.innerHTML = getLocale() === 'ru' ? 'Недостаточно данных' : 'Insufficient data';
newShikiRate.style.textAlign = 'center';
newShikiRate.style.marginTop = '30px';
return;
}
// считаем рейтинг
let shikiRateData = calculateShikiRating(scoreData);
let shikiRateLowData = calculateShikiRating(scoreData, settings.getOption('lowest')) || shikiRateData;
// добавляем данные в аттрибуты для js-событий
let shikiRateDiv = document.getElementById('shiki-score');
shikiRateDiv.setAttribute('data-score', shikiRateData.score);
shikiRateDiv.setAttribute('data-score-low', shikiRateLowData.score);
shikiRateDiv.setAttribute('data-count', shikiRateData.count);
shikiRateDiv.setAttribute('data-count-low', shikiRateLowData.count);
// меняем значение оценки
let scoreElement = newShikiRate.querySelector("div.text-score > div.score-value");
scoreElement.innerHTML = shikiRateData.score;
removeLastClass(scoreElement);
scoreElement.classList.add("score-" + Math.round(shikiRateData.score));
// меняем количество звезд
let starElement = newShikiRate.querySelector("div.stars-container > div.stars.score");
removeLastClass(starElement);
starElement.style.color = settings.getOption('starColor');
starElement.classList.add("score-" + shikiRateData.scoreMath);
// меняем тип оценки (хорошо, плохо и т.п.)
newShikiRate.querySelector("div.text-score > div.score-notice").textContent = getLabelData()[shikiRateData.scoreMath];
// добавляем информацию к стандартной оценке, что это mal
if (document.getElementById('mal-score')) {
let malLabel = getLocale() === 'ru' ? 'На основе оценок MAL' : 'From MAL users';
malRate.insertAdjacentHTML('afterend', '<p class="score-source">' + malLabel + '</p>');
let malScoreLabelElement = document.querySelector('.score-source');
malScoreLabelElement.style.marginBottom = '15px';
malScoreLabelElement.style.textAlign = 'center';
malScoreLabelElement.style.color = settings.getOption('textColor');
}
// добавляем информацию к шики оценке, что это шики
let shikiCountLabel = '<strong>' + shikiRateData.count + '</strong>';
shikiCountLabel = (getLocale() === 'ru') ? 'На основе ' + shikiCountLabel + ' оценок Shikimori' : 'From ' + shikiCountLabel + ' shiki users';
newShikiRate.insertAdjacentHTML('afterend', '<p class="score-counter">' + shikiCountLabel + '</p>');
let shikiScoreLabelElement = document.querySelector('.score-counter');
shikiScoreLabelElement.style.textAlign = 'center';
shikiScoreLabelElement.style.color = settings.getOption('textColor');
// при наведении показывает оценку, не учитывая низкие
scoreOnHover('mouseover', 'data-score-low', 'data-count-low');
scoreOnHover('mouseout', 'data-score', 'data-count');
}
function ready(fn) {
document.addEventListener('page:load', fn);
document.addEventListener('turbolinks:load', fn);
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") fn();
else document.addEventListener('DOMContentLoaded', fn);
}
ready(initSettings);
ready(appendShikiRating);