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);

})();

posted @ 2025-10-08 16:43  舟清颺  阅读(26)  评论(0)    收藏  举报