AniList True Average

Calculates and displays a 10% trimmed mean score to filter out extreme outlier ratings.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         AniList True Average
// @namespace    https://anilist.co/
// @version      1.0
// @description  Calculates and displays a 10% trimmed mean score to filter out extreme outlier ratings.
// @author       krokshut
// @match        https://anilist.co/anime/*
// @match        https://anilist.co/manga/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=anilist.co
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Cache variables to prevent API spam and instantly survive Vue.js DOM wipes
    let cachedMediaId = null;
    let cachedScore = null;
    let isFetching = false;

    // --- Inject Custom CSS ---
    function injectStyles() {
        if (document.getElementById('trim-score-styles')) return;
        const style = document.createElement('style');
        style.id = 'trim-score-styles';
        style.innerHTML = `
            #custom-trimmed-score { margin-bottom: 14px; }
            #custom-trimmed-score .type { font-size: 1.2rem !important; font-weight: 500; }
            #custom-trimmed-score .value { font-size: 1.3rem !important; color: var(--color-text-lighter, #8596a5) !important; }
            
            .trim-type-container { display: flex; align-items: center; gap: 6px; color: #3db4f2; }
            
            .trim-tooltip-icon {
                display: inline-flex; align-items: center; cursor: help; position: relative;
                color: var(--color-text-lighter, #8596a5); transition: color 0.2s ease;
            }
            .trim-tooltip-icon:hover { color: #3db4f2; }

            .trim-tooltip-icon::after {
                content: attr(data-tooltip); position: absolute; bottom: 150%; left: 50%;
                transform: translateX(-50%); background: rgb(11, 22, 34); color: rgb(159, 173, 189);
                padding: 10px 14px; border-radius: 4px; font-size: 1.2rem; font-weight: 500;
                white-space: normal; width: 220px; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.2);
                opacity: 0; visibility: hidden; transition: opacity 0.2s ease, visibility 0.2s ease, bottom 0.2s ease;
                z-index: 999; pointer-events: none;
            }
            .trim-tooltip-icon:hover::after { opacity: 1; visibility: visible; bottom: 120%; }
        `;
        document.head.appendChild(style);
    }

    // --- Core Math Function ---
    function calculateTrimmedMean(distribution, trimPercent = 0.10) {
        let expanded = [];
        distribution.forEach(d => {
            for (let i = 0; i < d.amount; i++) { expanded.push(d.score); }
        });
        
        if (expanded.length === 0) return null;
        expanded.sort((a, b) => a - b);

        let trimCount = Math.floor(expanded.length * trimPercent);
        let trimmed = expanded.slice(trimCount, expanded.length - trimCount);
        
        if (trimmed.length === 0) return null;

        let sum = trimmed.reduce((a, b) => a + b, 0);
        return (sum / trimmed.length).toFixed(1);
    }

    // --- API Fetch Function ---
    async function fetchScoreDistribution(mediaId) {
        const query = `
        query ($id: Int) {
          Media(id: $id) {
            stats {
              scoreDistribution { score amount }
            }
          }
        }`;

        try {
            const response = await fetch('https://graphql.anilist.co', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
                body: JSON.stringify({ query, variables: { id: mediaId } })
            });
            const data = await response.json();
            return data.data.Media.stats.scoreDistribution;
        } catch (error) {
            console.error("[AniList True Average] Fetch failed:", error);
            return null;
        }
    }

    // --- UI Injection Function ---
    async function injectTrimmedScore() {
        const pathParts = window.location.pathname.split('/');
        const currentMediaId = parseInt(pathParts[2]);

        if (isNaN(currentMediaId)) return;

        // 1. Find where to inject. If the sidebar isn't loaded yet, just exit and wait.
        const dataSets = document.querySelectorAll('.sidebar .data-set');
        let meanScoreElement = null;
        dataSets.forEach(el => {
            if (el.querySelector('.type')?.innerText.trim() === 'Mean Score') {
                meanScoreElement = el;
            }
        });

        if (!meanScoreElement) return; // Sidebar doesn't exist yet
        if (document.getElementById('custom-trimmed-score')) return; // We are already injected

        // 2. Check the Cache. If we navigated to a new anime, reset the cache.
        if (cachedMediaId !== currentMediaId) {
            cachedMediaId = currentMediaId;
            cachedScore = null;
        }

        // 3. Fetch data if we don't have it saved
        if (cachedScore === null) {
            if (isFetching) return; // Don't fire 50 API calls at once
            isFetching = true;
            
            const distribution = await fetchScoreDistribution(currentMediaId);
            if (distribution) {
                cachedScore = calculateTrimmedMean(distribution);
            }
            isFetching = false;
        }

        if (!cachedScore) return; // Math failed or no data

        // Double check it hasn't magically appeared while we were fetching
        if (document.getElementById('custom-trimmed-score')) return;

        // 4. Inject the HTML
        const trimmedElement = document.createElement('div');
        trimmedElement.className = 'data-set';
        trimmedElement.id = 'custom-trimmed-score';
        
        const questionIcon = `
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="10"></circle>
                <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
                <line x1="12" y1="17" x2="12.01" y2="17"></line>
            </svg>
        `;
        
        trimmedElement.innerHTML = `
            <div class="type trim-type-container">
                <span>Trimmed Average Score</span>
                <span class="trim-tooltip-icon" data-tooltip="Calculated by removing the highest 10% and lowest 10% of votes to filter out review bombing and blind hype.">
                    ${questionIcon}
                </span>
            </div>
            <div class="value">${cachedScore}%</div>
        `;

        meanScoreElement.insertAdjacentElement('afterend', trimmedElement);
    }

    // --- Initialization & SPA Navigation Observer ---
    injectStyles();

    // Just spam the injector whenever the page changes. 
    // Because of the cache, it costs 0 performance and guarantees the element stays on screen.
    const observer = new MutationObserver(() => {
        injectTrimmedScore();
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();