Shikimori Rating

Ratings from Shikimori users

// ==UserScript==
// @name         Shikimori Rating
// @namespace    http://shikimori.org/
// @version      3.0.0
// @description  Ratings from Shikimori users
// @author       ImoutoChan
// @match        *://shikimori.org/*
// @match        *://shikimori.one/*
// @match        *://shikimori.me/*
// @license      MIT
// @grant        none
// ==/UserScript==

const DEBUG_MODE = false;

const log = (message) => {
    if (DEBUG_MODE) {
        console.log(`ShikiRating: ${message}`);
    }
};

const getLocale = () => document.body.getAttribute('data-locale');

const shouldAddRating = (urlPart) => ["/animes", "/mangas", "/ranobe"].includes(urlPart);

const removeLastClass = (element) => {
    const classes = element.classList;
    if (classes.length > 0) {
        classes.remove(classes[classes.length - 1]);
    }
};

const displayNoDataMessage = (element) => {
    element.innerHTML = '';
    const noDataMessage = document.createElement('p');
    noDataMessage.className = 'b-nothing_here';
    noDataMessage.innerText = getLocale() === 'ru' ? 'Недостаточно данных' : 'Insufficient data';
    Object.assign(noDataMessage.style, {
        textAlign: 'center',
        color: '#7b8084',
        marginTop: '15px',
    });
    element.appendChild(noDataMessage);
};

const addShikiRating = () => {
    'use strict';

    const urlPart = window.location.pathname.substring(0, 7);
    log(urlPart);

    if (!shouldAddRating(urlPart)) {
        log('Not a valid page for rating');
        return;
    }

    if (document.querySelector("#shiki-score")) {
        log('Rating already exists');
        return;
    }

    const malRatingElement = document.querySelector(".scores > .b-rate");
    if (!malRatingElement) {
        log("Default rating not found");
        return;
    }

    malRatingElement.id = 'mal-score';

    const shikiRatingElement = malRatingElement.cloneNode(true);
    shikiRatingElement.id = 'shiki-score';

    const scoresContainer = document.querySelector(".scores");
    scoresContainer.appendChild(shikiRatingElement);

    const scoreDataJson = document.querySelector("#rates_scores_stats")?.getAttribute("data-stats");
    if (!scoreDataJson) {
        log('No score data found');
        displayNoDataMessage(shikiRatingElement);
        return;
    }

    let scoreData;
    try {
        scoreData = JSON.parse(scoreDataJson);
    } catch (error) {
        log('Error parsing score data');
        displayNoDataMessage(shikiRatingElement);
        return;
    }

    if (!scoreData || scoreData.length === 0) {
        displayNoDataMessage(shikiRatingElement);
        return;
    }

    const { totalScore, totalVotes } = scoreData.reduce((acc, [score, count]) => {
        acc.totalScore += score * count;
        acc.totalVotes += count;
        return acc;
    }, { totalScore: 0, totalVotes: 0 });

    const shikiScore = totalScore / totalVotes;
    const roundedScore = Math.floor(shikiScore);
    log(shikiScore);

    const scoreValueElement = shikiRatingElement.querySelector("div.text-score > div.score-value");
    scoreValueElement.innerHTML = shikiScore.toFixed(2);
    removeLastClass(scoreValueElement);
    scoreValueElement.classList.add(`score-${roundedScore}`);

    const starContainerElement = shikiRatingElement.querySelector("div.stars-container > div.stars.score");
    removeLastClass(starContainerElement);
    starContainerElement.style.color = '#456';
    starContainerElement.classList.add(`score-${Math.round(shikiScore)}`);

    const scoreLabels = getLocale() === 'ru' ?
        { "0": "", "1": "Хуже некуда", "2": "Ужасно", "3": "Очень плохо", "4": "Плохо", "5": "Более-менее", "6": "Нормально", "7": "Хорошо", "8": "Отлично", "9": "Великолепно", "10": "Эпик вин!" } :
        { "0": "", "1": "Worst Ever", "2": "Terrible", "3": "Very Bad", "4": "Bad", "5": "So-so", "6": "Fine", "7": "Good", "8": "Excellent", "9": "Great", "10": "Masterpiece!" };

    shikiRatingElement.querySelector("div.text-score > div.score-notice").textContent = scoreLabels[roundedScore];

    const malSourceLabel = getLocale() === 'ru' ? 'На основе оценок MAL' : 'From MAL users';
    malRatingElement.insertAdjacentHTML('afterend', `<p class="score-source">${malSourceLabel}</p>`);

    const votesWord = (totalVotes % 10 === 1 && totalVotes % 100 !== 11) ? 'оценки' : 'оценок';
    const voteCountLabel = `<strong>${totalVotes}</strong>`;
    const shikiSourceLabel = getLocale() === 'ru' ?
        `На основе ${voteCountLabel} ${votesWord} Shikimori` :
        `From ${voteCountLabel} Shikimori users`;
    shikiRatingElement.insertAdjacentHTML('afterend', `<p class="score-counter">${shikiSourceLabel}</p>`);

    const malScoreLabelElement = document.querySelector('.score-source');
    Object.assign(malScoreLabelElement.style, {
        marginBottom: '15px',
        textAlign: 'center',
        color: '#7b8084',
    });

    const shikiScoreLabelElement = document.querySelector('.score-counter');
    Object.assign(shikiScoreLabelElement.style, {
        textAlign: 'center',
        color: '#7b8084',
    });
};

const onDocumentReady = (callback) => {
    document.addEventListener('page:load', callback);
    document.addEventListener('turbolinks:load', callback);

    if (document.readyState !== "loading") {
        callback();
    } else {
        document.addEventListener('DOMContentLoaded', callback);
    }
};

onDocumentReady(addShikiRating);