AniList True Average

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();