tampermonkey油猴脚本, 动画疯评分显示增强脚本
- 🎬 按需加载:在页面左下角提供一个控制面板,只有当您点击【获取评分】按钮时,脚本才会开始工作,避免了不必要的资源消耗。
- ⭐ 自定义高亮:在获取评分前,会弹窗询问您希望高亮显示的分数阈值(默认≥4.5分),让您一眼就能找到符合您标准的优质动漫。
- ⏯️ 高级控制:在评分获取过程中,您可以随时暂停、继续或停止当前任务,完全掌控脚本的行為。
- 📊 进度显示:在处理过程中,会实时显示进度(例如
处理中: 15 / 24
),让您对进度一目了然。 - 🌐 通用支持:完美支持动画疯的首页和所有动画列表页 (
animeList.php
) 和我的动画 三种不同的页面布局。 - ⚡ 持久化缓存:已获取过的评分会被自动缓存24小时。在有效期内重复浏览,将直接从本地读取评分,实现秒级加载,并极大减少网络请求。
- 🛡️ 防屏蔽策略:内置了随机延迟和伪装请求头等策略,模拟人类用户的浏览行为,有效避免触发网站的反爬虫机制。
- 📍 UI位置:控制面板位于页面左下角,避免与网站右下角的官方弹窗重叠。
点击查看代码
// ==UserScript==
// @name 動畫瘋评分显示增强
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 支持暂停、继续、停止操作,UI移至左下角,按需获取评分并可自定义高亮阈值。
// @author Your Name
// @match https://ani.gamer.com.tw/*
// @connect ani.gamer.com.tw
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// --- 全局配置与状态管理 ---
// [配置项] 高亮的评分阈值,可通过弹窗动态修改
let RATING_THRESHOLD = 4.5;
// [配置项] 网络请求的基础延迟(毫秒),防止请求过快
const BASE_DELAY_MS = 500;
// [配置项] 在基础延迟上增加的随机延迟最大值,模拟人类行为
const RANDOM_DELAY_MS = 1000;
// [配置项] 本地缓存中存储评分的前缀,防止键名冲突
const CACHE_PREFIX = 'anime_rating_';
// [配置项] 缓存有效期(24小时),过期后会重新获取
const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000;
// [状态变量] 存放待处理动漫卡片元素的任务队列
let processingQueue = [];
// [状态变量] 标记当前是否处于暂停状态
let isPaused = false;
// [状态变量] 标记当前是否已手动停止
let isStopped = false;
// [状态变量] 记录当前任务队列的总数,用于计算进度
let totalQueueCount = 0;
// --- UI 元素引用 ---
// 将UI元素声明为全局变量,方便在不同函数中调用
let controlContainer, startButton, pauseResumeButton, stopButton, progressIndicator;
// --- 核心处理逻辑 ---
/**
* @description 任务队列的“引擎”,负责从队列中取出一个任务并处理。
* 这是整个脚本实现暂停/继续的核心。
*/
function processQueue() {
// 1. 检查状态:如果已暂停或已停止,则中断后续所有操作
if (isPaused || isStopped) return;
// 2. 检查队列是否为空:如果队列处理完毕,则重置UI并结束
if (processingQueue.length === 0) {
resetUI();
return;
}
// 3. 更新进度条并从队列中取出第一个任务
updateProgress();
const card = processingQueue.shift(); // .shift()会移除并返回数组的第一个元素
// 4. 处理这个取出的任务
processAnimeCard(card);
}
/**
* @description 处理单个动漫卡片的函数。
* @param {HTMLElement} card - 需要处理的动漫卡片<a>元素。
*/
function processAnimeCard(card) {
// 如果卡片已被处理过,则立即调度下一个任务
if (card.classList.contains('rating-processed')) {
setTimeout(processQueue, 50); // 用一个极短的延迟防止栈溢出
return;
}
card.classList.add('rating-processed');
const animeLink = card.href;
// 基本的有效性检查
if (!animeLink) { setTimeout(processQueue, 50); return; }
const snMatch = animeLink.match(/sn=(\d+)/);
if (!snMatch) { setTimeout(processQueue, 50); return; }
const animeSN = snMatch[1];
// 优先从缓存读取数据
const cachedData = getFromCache(animeSN);
if (cachedData) {
injectRating(card, cachedData);
// 即便从缓存读取,也加入一个随机延迟,让整体进度看起来更自然
const delay = BASE_DELAY_MS / 2 + Math.random() * RANDOM_DELAY_MS / 2;
// 关键:当前任务处理完后,调度下一个任务
setTimeout(processQueue, delay);
return;
}
// 如果缓存中没有,则发起网络请求
GM_xmlhttpRequest({
method: "GET",
url: animeLink,
headers: { "User-Agent": navigator.userAgent, "Referer": window.location.href },
onload: function (response) {
if (response.status >= 200 && response.status < 400) {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const ratingElement = doc.querySelector('.score-overall-number');
const rating = ratingElement ? parseFloat(ratingElement.textContent).toFixed(1) : 'N/A';
injectRating(card, rating);
saveToCache(animeSN, rating);
} else {
injectRating(card, 'Error');
}
// 关键:请求成功后,调度下一个任务
const delay = BASE_DELAY_MS + Math.random() * RANDOM_DELAY_MS;
setTimeout(processQueue, delay);
},
onerror: function () {
injectRating(card, 'Error');
// 关键:请求失败后,同样要调度下一个任务,确保队列能继续走下去
const delay = BASE_DELAY_MS + Math.random() * RANDOM_DELAY_MS;
setTimeout(processQueue, delay);
}
});
}
// --- UI 控制与事件处理 ---
/**
* @description 创建并初始化所有控制按钮和面板。
*/
function createControls() {
// 创建主容器
controlContainer = document.createElement('div');
controlContainer.style.position = 'fixed';
controlContainer.style.bottom = '20px';
controlContainer.style.left = '20px'; // 移到左下角
controlContainer.style.zIndex = '9999';
controlContainer.style.display = 'flex';
controlContainer.style.gap = '10px';
controlContainer.style.alignItems = 'center';
// 创建按钮的辅助函数,避免重复代码
const createButton = (id, text, onClick) => {
const button = document.createElement('button');
button.id = id;
button.textContent = text;
// 定义通用样式
button.style.padding = '8px 12px';
button.style.fontSize = '14px';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.cursor = 'pointer';
button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
button.addEventListener('click', onClick);
return button;
};
// 创建各个按钮和指示器
startButton = createButton('startBtn', '获取评分', promptAndFetch);
startButton.style.backgroundColor = '#00a0d8';
pauseResumeButton = createButton('pauseResumeBtn', '暂停', handlePauseResume);
pauseResumeButton.style.backgroundColor = '#ffc107';
stopButton = createButton('stopBtn', '停止', handleStop);
stopButton.style.backgroundColor = '#dc3545';
progressIndicator = document.createElement('span');
progressIndicator.style.color = 'black';
progressIndicator.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
progressIndicator.style.padding = '5px 10px';
progressIndicator.style.borderRadius = '5px';
progressIndicator.style.fontSize = '14px';
// 将元素添加入容器,并最终添加入页面
controlContainer.append(startButton, pauseResumeButton, stopButton, progressIndicator);
document.body.appendChild(controlContainer);
resetUI(); // 初始化UI到“待命”状态
}
/**
* @description 重置UI到初始状态(只显示“获取评分”按钮)。
*/
function resetUI() {
startButton.style.display = 'inline-block';
pauseResumeButton.style.display = 'none';
stopButton.style.display = 'none';
progressIndicator.style.display = 'none';
pauseResumeButton.textContent = '暂停'; // 确保暂停/继续按钮的文字状态被重置
}
/**
* @description 设置UI到“处理中”状态。
*/
function setProcessingUI() {
startButton.style.display = 'none';
pauseResumeButton.style.display = 'inline-block';
stopButton.style.display = 'inline-block';
progressIndicator.style.display = 'inline-block';
}
/**
* @description 更新进度指示器的文本。
*/
function updateProgress() {
const processedCount = totalQueueCount - processingQueue.length;
progressIndicator.textContent = `处理中: ${processedCount} / ${totalQueueCount}`;
}
/**
* @description “获取评分”按钮的点击事件处理函数,负责弹出询问框。
*/
function promptAndFetch() {
const userInput = prompt('需要高亮≥多少分的动漫?', RATING_THRESHOLD);
if (userInput === null) return; // 用户点取消则中止
const newThreshold = parseFloat(userInput);
if (!isNaN(newThreshold)) {
RATING_THRESHOLD = newThreshold; // 更新全局阈值
}
startProcessing(); // 开始处理流程
}
/**
* @description 初始化任务队列并开始处理。
*/
function startProcessing() {
// 重置状态变量
isStopped = false;
isPaused = false;
// 查找所有未处理的卡片,并构建任务队列
const animeCards = document.querySelectorAll('a.anime-card-block:not(.rating-processed), a.theme-list-main:not(.rating-processed)');
processingQueue = Array.from(animeCards);
totalQueueCount = processingQueue.length;
if (totalQueueCount === 0) {
alert('当前页面已无未获取评分的动漫。');
return;
}
setProcessingUI(); // 切换UI到处理中状态
processQueue(); // 启动队列引擎
}
/**
* @description “暂停/继续”按钮的点击事件处理函数。
*/
function handlePauseResume() {
isPaused = !isPaused; // 切换暂停状态
if (isPaused) {
pauseResumeButton.textContent = '继续';
pauseResumeButton.style.backgroundColor = '#28a745'; // 绿色代表“继续”
} else {
pauseResumeButton.textContent = '暂停';
pauseResumeButton.style.backgroundColor = '#ffc107'; // 黄色代表“暂停”
processQueue(); // 关键:在“继续”时,需要手动调用一次processQueue来重启处理链条
}
}
/**
* @description “停止”按钮的点击事件处理函数。
*/
function handleStop() {
isStopped = true;
processingQueue = []; // 清空任务队列,中断所有后续操作
resetUI(); // 将UI恢复到初始状态
}
// --- 辅助函数 (缓存, 注入DOM) ---
/**
* @description 将评分标签和高亮样式注入到动漫卡片上。
* @param {HTMLElement} card - 目标卡片元素
* @param {string} rating - 评分字符串 (e.g., "4.8" or "N/A")
*/
function injectRating(card, rating) {
// 确定评分标签应该被注入到哪个元素。
// 因为“所有动画”页的卡片结构不同,我们需要一个判断。
// 如果是“所有动画”页的卡片(a.theme-list-main),目标是其内部的图片容器(div.theme-img-block)。
// 否则,目标就是卡片本身(a.anime-card-block)。
const injectionTarget = card.classList.contains('theme-list-main') ? card.querySelector('.theme-img-block') : card;
if (!injectionTarget) return; // 如果找不到目标,则退出
// 创建评分标签<div>元素
const ratingDiv = document.createElement('div');
// 设置绝对定位,使其可以浮动在卡片右上角
ratingDiv.style.position = 'absolute';
ratingDiv.style.top = '5px';
ratingDiv.style.right = '5px';
// 设置样式使其美观
ratingDiv.style.padding = '2px 6px';
ratingDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
ratingDiv.style.color = 'white';
ratingDiv.style.fontSize = '14px';
ratingDiv.style.fontWeight = 'bold';
ratingDiv.style.borderRadius = '4px';
ratingDiv.style.zIndex = '10'; // 确保在顶层显示
// 设置评分文本
ratingDiv.textContent = `★ ${rating}`;
// 检查评分是否达到高亮阈值
const numericRating = parseFloat(rating);
if (!isNaN(numericRating) && numericRating >= RATING_THRESHOLD) {
// 确定高亮边框应该应用到哪个元素。
// 对于“所有动画”页的卡片,我们希望高亮整个<li>,即<a>的父元素。
// 对于首页卡片,直接高亮<a>元素即可。
const highlightTarget = card.classList.contains('theme-list-main') ? card.parentElement : card;
highlightTarget.style.outline = '3px solid #FFD700'; // 应用金色外边框
ratingDiv.style.color = '#FFD700'; // 同时将评分文字也变为金色
}
// 为确保绝对定位生效,注入目标的position必须是relative, absolute, 或 fixed。
injectionTarget.style.position = 'relative';
// 将创建好的评分标签添加入目标元素
injectionTarget.appendChild(ratingDiv);
}
/**
* @description 将评分数据存入localStorage。
* @param {string} key - 缓存键(通常是动漫的SN号)
* @param {string} value - 要缓存的值(评分)
*/
function saveToCache(key, value) {
const item = {
value: value,
timestamp: new Date().getTime() // 存入当前时间戳,用于判断是否过期
};
localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(item));
}
/**
* @description 从localStorage读取有效的缓存数据。
* @param {string} key - 缓存键
* @returns {string|null} - 如果找到有效缓存则返回值,否则返回null
*/
function getFromCache(key) {
const itemStr = localStorage.getItem(CACHE_PREFIX + key);
if (!itemStr) return null; // 如果不存在,返回null
const item = JSON.parse(itemStr);
// 检查缓存是否已过期
if (new Date().getTime() - item.timestamp > CACHE_EXPIRATION_MS) {
localStorage.removeItem(CACHE_PREFIX + key); // 如果过期,删除该缓存
return null;
}
return item.value; // 返回有效的缓存值
}
// --- 脚本入口 ---
/**
* @description 当页面加载完成后,执行脚本的入口函数。
* 这里只创建UI控件,等待用户交互。
*/
window.addEventListener('load', createControls);
})();
本文来自博客园,作者:舟清颺,转载请注明原文链接:https://www.cnblogs.com/zqingyang/p/19129812