您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays custom rating, reviews, dispersion, median, stddev. Fixed data type and HTML generation issues.
// ==UserScript== // @name Shiki Custom Anime Rating 2 // @namespace http://tampermonkey.net/ // @version 6.8 // @author arch_q with AI support // @description Displays custom rating, reviews, dispersion, median, stddev. Fixed data type and HTML generation issues. // @match *://shikimori.org/* // @match *://shikimori.one/* // @license MIT // @match *://shikimori.me/* // @grant none // ==/UserScript== (function() { 'use strict'; const DISPERSION_PRECISION = 3; const STD_DEV_PRECISION = 3; function getLocale() { return document.querySelector('body')?.getAttribute('data-locale') || 'ru'; } function getLabels() { return getLocale() === 'ru' ? { title: 'Статистика оценок и отзывов', median: 'Медиана', stddev: 'Стандартное отклонение', dispersion: 'Дисперсия', reviews: 'Отзывы', metaRating: 'Метаоценка' } : { title: 'Score & Review Stats', median: 'Median', stddev: 'Standard deviation', dispersion: 'Dispersion', reviews: 'Reviews', metaRating: 'Meta rating' }; } function getScoreData() { const node = document.querySelector("#rates_scores_stats"); if (!node) return null; try { return JSON.parse(node.getAttribute("data-stats")); } catch (e) { console.error("Shiki Custom Rating: Could not parse score data.", e); return null; } } function getTotalCount(scoreData) { return scoreData.reduce((sum, [, count]) => sum + parseInt(count, 10), 0); } function calculateAverage(scoreData) { const total = getTotalCount(scoreData); if (total === 0) return 0; const sum = scoreData.reduce((acc, [score, count]) => acc + parseInt(score, 10) * parseInt(count, 10), 0); return sum / total; } function calculateVariance(scoreData, avg) { const total = getTotalCount(scoreData); if (total <= 1) return 0; const sumSqDiff = scoreData.reduce((acc, [score, count]) => acc + parseInt(count, 10) * Math.pow(parseInt(score, 10) - avg, 2), 0); return sumSqDiff / total; } function calculateMedian(scoreData) { const scores = []; scoreData.forEach(([score, count]) => { const numericScore = parseInt(score, 10); const numericCount = parseInt(count, 10); for (let i = 0; i < numericCount; i++) { scores.push(numericScore); } }); if (scores.length === 0) return 0; scores.sort((a, b) => a - b); const mid = Math.floor(scores.length / 2); return scores.length % 2 !== 0 ? scores[mid] : (scores[mid - 1] + scores[mid]) / 2; } function getReviewCounts() { const counts = { positive: 0, neutral: 0, negative: 0 }; const links = document.querySelectorAll(".b-reviews_navigation a"); if (!links.length) return null; links.forEach(link => { const href = link.getAttribute('href'); if (!href) return; const countDiv = link.querySelector('.count'); const count = countDiv ? parseInt(countDiv.textContent.trim(), 10) : 0; if (href.endsWith('/positive')) counts.positive = count; else if (href.endsWith('/neutral')) counts.neutral = count; else if (href.endsWith('/negative')) counts.negative = count; }); return counts; } function createBlockHtml({ median, stddev, dispersion, totalVotes, reviews, stars, scoreValue }) { const L = getLabels(); // Corrected: Using template literal (backticks ``) instead of JSX return ` <div id="custom-stats-block" class="block"> <div class="subheadline m5">${L.title}</div> <div class="scores" style="margin-bottom: 10px;"> <div class="b-rate" id="custom-score-rating"> <div class="stars-container"> <div class="hoverable-trigger"></div> <div class="stars score score-${stars}" style="color: #a22123;"></div> <div class="stars hover"></div> <div class="stars background"></div> </div> <div class="text-score"> <div class="score-value score-${stars}">${scoreValue}</div> <div class="score-notice"></div> </div> </div> <p style="text-align: center; color: #7b8084; margin-top: 5px;">${L.metaRating}</p> </div> <div style="padding: 10px 0;"> <p style="text-align: center; color: #7b8084;">${L.median}: <strong>${median}</strong></p> <p style="text-align: center; color: #7b8084;">${L.stddev}: <strong>${stddev}</strong></p> <p style="text-align: center; color: #7b8084;">${L.dispersion}: <strong>${dispersion}</strong></p> <p style="text-align: center; color: #7b8084;">${L.reviews}: <strong style="color: #4f9222;">${reviews.positive}</strong> / <strong style="color: #66a2b3;">${reviews.neutral}</strong> / <strong style="color: #d13639;">${reviews.negative}</strong> </p> <p style="text-align: center; color: #7b8084;">${getLocale() === 'ru' ? 'На основе' : 'Based on'} <strong>${totalVotes}</strong> ${getLocale() === 'ru' ? 'оценок' : 'votes'}</p> </div> </div>`; } function insertStatsBlock() { if (document.querySelector('#custom-stats-block')) return; const scoreData = getScoreData(); if (!scoreData || scoreData.length === 0) return; const reviews = getReviewCounts(); if (!reviews) return; const avg = calculateAverage(scoreData); const variance = calculateVariance(scoreData, avg); const stddev = Math.sqrt(variance); const median = calculateMedian(scoreData); const totalVotes = getTotalCount(scoreData); const score = avg / (stddev || 1); const clamped = Math.max(1, Math.min(10, score)); const stars = Math.round(clamped); const html = createBlockHtml({ median, stddev: stddev.toFixed(STD_DEV_PRECISION), dispersion: variance.toFixed(DISPERSION_PRECISION), totalVotes, reviews, stars, scoreValue: clamped.toFixed(2) }); const container = document.querySelector(".c-info-right .scores"); if (container) { // Inserting after the whole .scores block, not inside it, to avoid nesting issues. // This matches the appearance in your screenshot where the new block is below the MAL/Shiki ratings. container.parentElement.insertAdjacentHTML('beforeend', html); } else { // Fallback for pages without a score block yet (e.g. anons) const infoRight = document.querySelector(".c-info-right"); if (infoRight) { infoRight.insertAdjacentHTML('beforeend', html); } } } // Shikimori uses turbolinks, so we need to observe for navigation changes const observer = new MutationObserver((mutations) => { // A simple check for URL change is often sufficient and more performant if (mutations.some(m => m.type === 'childList' && m.target === document.body)) { insertStatsBlock(); } }); // Initial run on page load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', insertStatsBlock); } else { insertStatsBlock(); } // Observe for dynamic content changes observer.observe(document.body, { childList: true, subtree: false }); // Also listen to turbolinks/Turbo events for SPA-like navigation document.addEventListener('turbolinks:load', insertStatsBlock); document.addEventListener('turbo:load', insertStatsBlock); })();