// ==UserScript==
// @name AO3: Quality Score Improved
// @description Calculates and displays quality scores for AO3 works with customizable options
// @namespace https://greasyfork.org/scripts/3144-ao3-kudos-hits-ratio
// @author Min (Extensive modifications by Assistant)
// @version 6.1
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @include http://archiveofourown.org/*
// @include https://archiveofourown.org/*
// @license MIT
// ==/UserScript==
(function ($) {
'use strict';
// Configuration
const CONFIG = {
weights: {
kudosHitRatio: 50,
chapterAdjustment: 0.05,
commentEngagement: 20,
bookmarkScore: 30,
wordCountFactor: 0.5,
timeDecayHalfLife: 365 // days
},
colorThresholds: {
low: 30,
medium: 60
},
options: {
autoSort: GM_getValue('autoSort', false),
showScores: GM_getValue('showScores', true),
hideWorks: GM_getValue('hideWorks', false),
hideThreshold: GM_getValue('hideThreshold', 20)
}
};
// CSS Styles
GM_addStyle(`
.quality-score {
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
margin-left: 10px;
}
.quality-score-low { background-color: #ffcccb; color: #8b0000; }
.quality-score-medium { background-color: #ffffa1; color: #8b8b00; }
.quality-score-high { background-color: #90EE90; color: #006400; }
.work-stats { display: flex; align-items: center; }
.work-stats > dd { margin-right: 10px; }
`);
// Core Functions
const calculateQualityScore = (stats) => {
if (stats.hits === 0) return 0;
const baseScore = (stats.kudos / Math.sqrt(stats.hits)) * CONFIG.weights.kudosHitRatio;
const chapterAdjustment = 1 + (stats.chapters - 1) * CONFIG.weights.chapterAdjustment;
const commentBonus = (stats.comments / stats.hits) * CONFIG.weights.commentEngagement;
const bookmarkBonus = (stats.bookmarks / stats.hits) * CONFIG.weights.bookmarkScore;
const wordCountFactor = Math.log(stats.wordCount) / Math.log(10000) * CONFIG.weights.wordCountFactor;
const daysSincePublish = (new Date() - stats.publishDate) / (1000 * 60 * 60 * 24);
const timeDecayFactor = Math.exp(-daysSincePublish / CONFIG.weights.timeDecayHalfLife);
const score = ((baseScore * chapterAdjustment + commentBonus + bookmarkBonus) * (1 + wordCountFactor)) * timeDecayFactor;
return Math.min(99, score * 0.9);
};
const getScoreClass = (score) => {
if (score >= CONFIG.colorThresholds.medium) return 'quality-score-high';
if (score >= CONFIG.colorThresholds.low) return 'quality-score-medium';
return 'quality-score-low';
};
const addScoresToWorks = () => {
$('ol.work.index > li').each(function () {
const $work = $(this);
const $stats = $work.find('dl.stats');
try {
const stats = {
hits: parseInt($stats.find('dd.hits').text().replace(/,/g, '')) || 0,
kudos: parseInt($stats.find('dd.kudos a').text().replace(/,/g, '')) || 0,
chapters: parseInt($stats.find('dd.chapters a').text().split('/')[0]) || 1,
comments: parseInt($stats.find('dd.comments a').text().replace(/,/g, '')) || 0,
bookmarks: parseInt($stats.find('dd.bookmarks a').text().replace(/,/g, '')) || 0,
wordCount: parseInt($stats.find('dd.words').text().replace(/,/g, '')) || 0,
publishDate: new Date($work.find('p.datetime').text())
};
const qualityScore = calculateQualityScore(stats);
$work.attr('data-quality-score', qualityScore);
if (CONFIG.options.showScores) {
const scoreDisplay = qualityScore.toFixed(1);
const $scoreElement = $('<dd>')
.addClass('quality-score')
.addClass(getScoreClass(qualityScore))
.text(`Score: ${scoreDisplay}`);
$stats.addClass('work-stats').append($scoreElement);
}
if (CONFIG.options.hideWorks && qualityScore < CONFIG.options.hideThreshold) {
$work.hide();
}
} catch (error) {
console.error(`Error processing work stats: ${error.message}`);
}
});
if (CONFIG.options.autoSort) {
sortWorksByScore();
}
};
const sortWorksByScore = () => {
const $workList = $('ol.work.index');
const $works = $workList.children('li').get();
$works.sort((a, b) => {
const scoreA = parseFloat($(a).attr('data-quality-score')) || 0;
const scoreB = parseFloat($(b).attr('data-quality-score')) || 0;
return scoreB - scoreA;
});
$workList.append($works);
};
const addQualityScoreMenu = () => {
const $headerMenu = $('ul.primary.navigation.actions');
if ($headerMenu.length === 0) {
console.error('Header menu not found, skipping menu addition');
return;
}
const $scoreMenu = $('<li class="dropdown">').html('<a href="#">Quality Score</a>');
$headerMenu.find('li.search').before($scoreMenu);
const $dropMenu = $('<ul class="menu dropdown-menu">');
$scoreMenu.append($dropMenu);
const addMenuItem = (text, clickHandler) => {
const $menuItem = $('<li>').html(`<a href="#">${text}</a>`);
$menuItem.on('click', (e) => {
e.preventDefault();
clickHandler();
});
$dropMenu.append($menuItem);
};
addMenuItem(`Auto-sort: ${CONFIG.options.autoSort ? 'ON' : 'OFF'}`, () => {
CONFIG.options.autoSort = !CONFIG.options.autoSort;
GM_setValue('autoSort', CONFIG.options.autoSort);
location.reload();
});
addMenuItem(`Show Scores: ${CONFIG.options.showScores ? 'ON' : 'OFF'}`, () => {
CONFIG.options.showScores = !CONFIG.options.showScores;
GM_setValue('showScores', CONFIG.options.showScores);
location.reload();
});
addMenuItem(`Hide Low Quality: ${CONFIG.options.hideWorks ? 'ON' : 'OFF'}`, () => {
CONFIG.options.hideWorks = !CONFIG.options.hideWorks;
GM_setValue('hideWorks', CONFIG.options.hideWorks);
location.reload();
});
addMenuItem('Set Hide Threshold', () => {
const newThreshold = prompt('Enter new hide threshold (0-100):', CONFIG.options.hideThreshold);
if (newThreshold !== null && !isNaN(newThreshold)) {
CONFIG.options.hideThreshold = Math.min(100, Math.max(0, parseInt(newThreshold)));
GM_setValue('hideThreshold', CONFIG.options.hideThreshold);
location.reload();
}
});
addMenuItem('Recalculate Scores', () => {
addScoresToWorks();
});
addMenuItem('Sort by Score (High to Low)', () => sortWorksByScore(false));
addMenuItem('Sort by Score (Low to High)', () => sortWorksByScore(true));
};
// Main execution
$(document).ready(() => {
addQualityScoreMenu();
addScoresToWorks();
});
})(jQuery);